first
This commit is contained in:
parent
969362e4cc
commit
744a2fa40b
5
.cursor/worktrees.json
Normal file
5
.cursor/worktrees.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"setup-worktree": [
|
||||||
|
"npm install"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module version="4">
|
||||||
|
<component name="AdditionalModuleElements">
|
||||||
|
<content url="file://$MODULE_DIR$/../../../vega-hrm-report/build/generated/sources/annotationProcessor/java/main">
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/../../../vega-hrm-report/build/generated/sources/annotationProcessor/java/main" isTestSource="false" generated="true" />
|
||||||
|
</content>
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
BIN
logs/vega-hrm-auth/error/app-error-2025-11-15.log.gz
Normal file
BIN
logs/vega-hrm-auth/error/app-error-2025-11-15.log.gz
Normal file
Binary file not shown.
BIN
logs/vega-hrm-auth/error/app-error-2025-11-17.log.gz
Normal file
BIN
logs/vega-hrm-auth/error/app-error-2025-11-17.log.gz
Normal file
Binary file not shown.
BIN
logs/vega-hrm-auth/info/app-info-2025-11-15.log.gz
Normal file
BIN
logs/vega-hrm-auth/info/app-info-2025-11-15.log.gz
Normal file
Binary file not shown.
BIN
logs/vega-hrm-auth/info/app-info-2025-11-17.log.gz
Normal file
BIN
logs/vega-hrm-auth/info/app-info-2025-11-17.log.gz
Normal file
Binary file not shown.
BIN
logs/vega-hrm-report/error/app-error-2025-11-24.log.gz
Normal file
BIN
logs/vega-hrm-report/error/app-error-2025-11-24.log.gz
Normal file
Binary file not shown.
BIN
logs/vega-hrm-report/info/app-info-2025-11-15.log.gz
Normal file
BIN
logs/vega-hrm-report/info/app-info-2025-11-15.log.gz
Normal file
Binary file not shown.
BIN
logs/vega-hrm-report/info/app-info-2025-11-17.log.gz
Normal file
BIN
logs/vega-hrm-report/info/app-info-2025-11-17.log.gz
Normal file
Binary file not shown.
BIN
logs/vega-hrm-report/info/app-info-2025-11-24.log.gz
Normal file
BIN
logs/vega-hrm-report/info/app-info-2025-11-24.log.gz
Normal file
Binary file not shown.
|
|
@ -22,18 +22,16 @@ dependencies {
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-log4j2:3.4.0'
|
implementation 'org.springframework.boot:spring-boot-starter-log4j2:3.4.0'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-validation:3.4.0'
|
implementation 'org.springframework.boot:spring-boot-starter-validation:3.4.0'
|
||||||
implementation 'de.mkammerer:argon2-jvm:2.1'
|
implementation 'de.mkammerer:argon2-jvm:2.1'
|
||||||
|
// Google API Services
|
||||||
implementation "com.google.apis:google-api-services-youtube:v3-rev182-1.22.0"
|
implementation "com.google.apis:google-api-services-youtube:v3-rev182-1.22.0"
|
||||||
implementation("com.google.collections:google-collections:1.0")
|
|
||||||
implementation("com.google.guava:guava:31.1-jre")
|
|
||||||
implementation("com.google.apis:google-api-services-youtubeAnalytics:v2-rev272-1.25.0")
|
implementation("com.google.apis:google-api-services-youtubeAnalytics:v2-rev272-1.25.0")
|
||||||
implementation "com.google.http-client:google-http-client-jackson2:1.20.0"
|
|
||||||
|
|
||||||
// OAuth Client
|
|
||||||
implementation 'com.google.apis:google-api-services-oauth2:v2-rev157-1.25.0'
|
implementation 'com.google.apis:google-api-services-oauth2:v2-rev157-1.25.0'
|
||||||
|
|
||||||
|
// Google API Client (phiên bản mới)
|
||||||
|
implementation 'com.google.api-client:google-api-client:1.34.1'
|
||||||
implementation 'com.google.oauth-client:google-oauth-client-jetty:1.34.1'
|
implementation 'com.google.oauth-client:google-oauth-client-jetty:1.34.1'
|
||||||
implementation 'com.google.api-client:google-api-client:1.32.1'
|
implementation "com.google.http-client:google-http-client-jackson2:1.43.3"
|
||||||
// Google Collections
|
implementation 'com.google.guava:guava:32.1.3-jre'
|
||||||
implementation "com.google.collections:google-collections:1.0"
|
|
||||||
implementation 'com.google.code.gson:gson:2.11.0'
|
implementation 'com.google.code.gson:gson:2.11.0'
|
||||||
annotationProcessor 'org.projectlombok:lombok:1.18.38'
|
annotationProcessor 'org.projectlombok:lombok:1.18.38'
|
||||||
implementation project(":vega-hrm-core")
|
implementation project(":vega-hrm-core")
|
||||||
|
|
@ -50,11 +48,14 @@ configurations {
|
||||||
exclude group: 'ch.qos.logback', module: 'logback-classic'
|
exclude group: 'ch.qos.logback', module: 'logback-classic'
|
||||||
|
|
||||||
exclude group: 'com.google.guava', module: 'guava-jdk5'
|
exclude group: 'com.google.guava', module: 'guava-jdk5'
|
||||||
exclude group: 'com.google.collections', module: 'google-collections' // very old
|
exclude group: 'com.google.collections', module: 'google-collections'
|
||||||
|
|
||||||
resolutionStrategy {
|
resolutionStrategy {
|
||||||
// bắt buộc dùng guava hiện đại
|
// Bắt buộc dùng Guava mới nhất
|
||||||
force "com.google.guava:guava:32.1.3-jre"
|
force 'com.google.guava:guava:32.1.3-jre'
|
||||||
|
force 'com.google.api-client:google-api-client:1.34.1'
|
||||||
|
force 'com.google.oauth-client:google-oauth-client-jetty:1.34.1'
|
||||||
|
force 'com.google.http-client:google-http-client-jackson2:1.43.3'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package com.vega.hrm.config;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.models.OpenAPI;
|
||||||
|
import io.swagger.v3.oas.models.info.Contact;
|
||||||
|
import io.swagger.v3.oas.models.info.Info;
|
||||||
|
import io.swagger.v3.oas.models.info.License;
|
||||||
|
import org.springdoc.core.models.GroupedOpenApi;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class OpenApiConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public OpenAPI authOpenAPI() {
|
||||||
|
return new OpenAPI()
|
||||||
|
.info(new Info()
|
||||||
|
.title("Vega HRM Auth API")
|
||||||
|
.version("v1")
|
||||||
|
.description("Tài liệu mô tả các API xác thực, phân quyền và tích hợp Google cho Vega HRM.")
|
||||||
|
.contact(new Contact()
|
||||||
|
.name("Vega HRM Backend Team")
|
||||||
|
.email("backend@vegahrm.local"))
|
||||||
|
.license(new License().name("Proprietary")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public GroupedOpenApi authGroupedOpenApi() {
|
||||||
|
return GroupedOpenApi.builder()
|
||||||
|
.group("auth")
|
||||||
|
.packagesToScan("com.vega.hrm.controller")
|
||||||
|
.pathsToMatch("/api/**")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -13,7 +13,7 @@ import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("api/google/user")
|
@RequestMapping({"api/google/user", "api/auth/google"})
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Tag(name = "Auth - Google", description = "Luồng OAuth2 với Google phục vụ xác thực người dùng")
|
@Tag(name = "Auth - Google", description = "Luồng OAuth2 với Google phục vụ xác thực người dùng")
|
||||||
public class GoogleController {
|
public class GoogleController {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import com.google.api.client.auth.oauth2.AuthorizationCodeFlow;
|
||||||
import com.google.api.client.auth.oauth2.AuthorizationCodeRequestUrl;
|
import com.google.api.client.auth.oauth2.AuthorizationCodeRequestUrl;
|
||||||
import com.google.api.client.auth.oauth2.BearerToken;
|
import com.google.api.client.auth.oauth2.BearerToken;
|
||||||
import com.google.api.client.auth.oauth2.Credential;
|
import com.google.api.client.auth.oauth2.Credential;
|
||||||
import com.google.api.client.auth.oauth2.TokenResponse;
|
|
||||||
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow;
|
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow;
|
||||||
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
|
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
|
||||||
import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse;
|
import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse;
|
||||||
|
|
@ -21,14 +20,12 @@ import com.vega.hrm.core.models.responses.BaseResponse;
|
||||||
import com.vega.hrm.core.dto.GoogleOAuthConfig;
|
import com.vega.hrm.core.dto.GoogleOAuthConfig;
|
||||||
import com.google.api.services.oauth2.model.Userinfo;
|
import com.google.api.services.oauth2.model.Userinfo;
|
||||||
import com.vega.hrm.core.repositories.UserGoogleTokenRepository;
|
import com.vega.hrm.core.repositories.UserGoogleTokenRepository;
|
||||||
import com.vega.hrm.dto.CustomTokenResponse;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.Setter;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
|
@ -100,26 +97,40 @@ public class GoogleService {
|
||||||
|
|
||||||
String email = userInfo.getEmail();
|
String email = userInfo.getEmail();
|
||||||
var userGoogleToken = userGoogleTokenRepository.findUserGoogleTokenByEmail(email);
|
var userGoogleToken = userGoogleTokenRepository.findUserGoogleTokenByEmail(email);
|
||||||
|
|
||||||
if (userGoogleToken == null) {
|
if (userGoogleToken == null) {
|
||||||
userGoogleToken = new UserGoogleToken();
|
userGoogleToken = new UserGoogleToken();
|
||||||
userGoogleToken.setId(UUID.randomUUID());
|
userGoogleToken.setId(UUID.randomUUID());
|
||||||
userGoogleToken.setEmail(email);
|
userGoogleToken.setEmail(email);
|
||||||
|
userGoogleToken.setCreatedAt(Instant.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
Instant now = Instant.now();
|
||||||
|
Long expiresInSeconds = tokenResponse.getExpiresInSeconds();
|
||||||
|
Object refreshTokenExpiresInObj = tokenResponse.get("refresh_token_expires_in");
|
||||||
|
Long refreshTokenExpiresInSeconds = refreshTokenExpiresInObj != null
|
||||||
|
? Long.valueOf(refreshTokenExpiresInObj.toString())
|
||||||
|
: null;
|
||||||
|
|
||||||
userGoogleToken.setAccessToken(tokenResponse.getAccessToken());
|
userGoogleToken.setAccessToken(tokenResponse.getAccessToken());
|
||||||
userGoogleToken.setRefreshToken(tokenResponse.getRefreshToken());
|
userGoogleToken.setRefreshToken(tokenResponse.getRefreshToken());
|
||||||
userGoogleToken.setScope(tokenResponse.getScope());
|
userGoogleToken.setScope(tokenResponse.getScope());
|
||||||
userGoogleToken.setExpiresIn(tokenResponse.getExpiresInSeconds());
|
userGoogleToken.setExpiresIn(expiresInSeconds);
|
||||||
userGoogleToken.setRefreshTokenExpiresIn(tokenResponse.getExpiresInSeconds());
|
userGoogleToken.setRefreshTokenExpiresIn(refreshTokenExpiresInSeconds);
|
||||||
userGoogleToken.setExpiresAt(Instant.now().plusSeconds(tokenResponse.getExpiresInSeconds()));
|
|
||||||
userGoogleToken.setTokenType(tokenResponse.getTokenType());
|
userGoogleToken.setTokenType(tokenResponse.getTokenType());
|
||||||
|
userGoogleToken.setUpdatedAt(now);
|
||||||
|
|
||||||
userGoogleToken.setRefreshTokenExpiresAt(Instant.now().plusSeconds(tokenResponse.get("refresh_token_expires_in") != null
|
if (expiresInSeconds != null) {
|
||||||
? Long.valueOf(tokenResponse.get("refresh_token_expires_in").toString())
|
userGoogleToken.setExpiresAt(now.plusSeconds(expiresInSeconds));
|
||||||
: null));
|
} else if (userGoogleToken.getExpiresAt() == null) {
|
||||||
userGoogleToken.setCreatedAt(Instant.now());
|
userGoogleToken.setExpiresAt(now);
|
||||||
userGoogleTokenRepository.save(userGoogleToken);
|
}
|
||||||
|
if (refreshTokenExpiresInSeconds != null) {
|
||||||
|
userGoogleToken.setRefreshTokenExpiresAt(now.plusSeconds(refreshTokenExpiresInSeconds));
|
||||||
|
} else if (userGoogleToken.getRefreshTokenExpiresAt() == null) {
|
||||||
|
userGoogleToken.setRefreshTokenExpiresAt(now);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userGoogleTokenRepository.save(userGoogleToken);
|
||||||
tokenStore.storeToken(email, tokenResponse);
|
tokenStore.storeToken(email, tokenResponse);
|
||||||
return BaseResponse.success("00",true);
|
return BaseResponse.success("00",true);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,17 +44,15 @@ dependencies {
|
||||||
implementation 'org.bouncycastle:bcpkix-jdk18on:1.80'
|
implementation 'org.bouncycastle:bcpkix-jdk18on:1.80'
|
||||||
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
|
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
|
||||||
|
|
||||||
|
// Google API Services
|
||||||
implementation "com.google.apis:google-api-services-youtube:v3-rev182-1.22.0"
|
implementation "com.google.apis:google-api-services-youtube:v3-rev182-1.22.0"
|
||||||
implementation("com.google.collections:google-collections:1.0")
|
|
||||||
implementation("com.google.guava:guava:20.0")
|
|
||||||
implementation("com.google.apis:google-api-services-youtubeAnalytics:v2-rev272-1.25.0")
|
implementation("com.google.apis:google-api-services-youtubeAnalytics:v2-rev272-1.25.0")
|
||||||
implementation "com.google.http-client:google-http-client-jackson2:1.20.0"
|
|
||||||
|
|
||||||
// OAuth Client
|
// Google API Client (phiên bản mới)
|
||||||
implementation "com.google.oauth-client:google-oauth-client-jetty:1.20.0"
|
implementation 'com.google.api-client:google-api-client:1.34.1'
|
||||||
|
implementation 'com.google.oauth-client:google-oauth-client-jetty:1.34.1'
|
||||||
// Google Collections
|
implementation "com.google.http-client:google-http-client-jackson2:1.43.3"
|
||||||
implementation "com.google.collections:google-collections:1.0"
|
implementation 'com.google.guava:guava:32.1.3-jre'
|
||||||
implementation 'com.google.code.gson:gson:2.11.0'
|
implementation 'com.google.code.gson:gson:2.11.0'
|
||||||
|
|
||||||
// https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-orgjson
|
// https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-orgjson
|
||||||
|
|
@ -68,5 +66,17 @@ configurations {
|
||||||
|
|
||||||
// Loại bỏ logging mặc định
|
// Loại bỏ logging mặc định
|
||||||
exclude group: 'ch.qos.logback', module: 'logback-classic'
|
exclude group: 'ch.qos.logback', module: 'logback-classic'
|
||||||
|
|
||||||
|
// Loại bỏ các thư viện Guava/Google Collections cũ
|
||||||
|
exclude group: 'com.google.guava', module: 'guava-jdk5'
|
||||||
|
exclude group: 'com.google.collections', module: 'google-collections'
|
||||||
|
|
||||||
|
resolutionStrategy {
|
||||||
|
// Bắt buộc dùng Guava mới nhất
|
||||||
|
force 'com.google.guava:guava:32.1.3-jre'
|
||||||
|
force 'com.google.api-client:google-api-client:1.34.1'
|
||||||
|
force 'com.google.oauth-client:google-oauth-client-jetty:1.34.1'
|
||||||
|
force 'com.google.http-client:google-http-client-jackson2:1.43.3'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,26 +2,38 @@ package com.vega.hrm.core.component;
|
||||||
|
|
||||||
import com.google.api.client.auth.oauth2.Credential;
|
import com.google.api.client.auth.oauth2.Credential;
|
||||||
import com.google.api.client.auth.oauth2.TokenResponse;
|
import com.google.api.client.auth.oauth2.TokenResponse;
|
||||||
|
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
|
||||||
import com.google.api.client.http.HttpTransport;
|
import com.google.api.client.http.HttpTransport;
|
||||||
import com.google.api.client.json.JsonFactory;
|
import com.google.api.client.json.JsonFactory;
|
||||||
|
import com.vega.hrm.core.entities.UserGoogleToken;
|
||||||
|
import com.vega.hrm.core.repositories.UserGoogleTokenRepository;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class TokenStore {
|
public class TokenStore {
|
||||||
private final ConcurrentHashMap<String, TokenResponse> tokenMap = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<String, TokenResponse> tokenMap = new ConcurrentHashMap<>();
|
||||||
|
private final UserGoogleTokenRepository userGoogleTokenRepository;
|
||||||
|
|
||||||
public void storeToken(String userId, TokenResponse tokenResponse) {
|
public void storeToken(String userId, TokenResponse tokenResponse) {
|
||||||
tokenMap.put(userId, tokenResponse);
|
tokenMap.put(userId, tokenResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
public TokenResponse getTokenResponse(String userId) {
|
public TokenResponse getTokenResponse(String userId) {
|
||||||
return tokenMap.get(userId);
|
TokenResponse tokenResponse = tokenMap.get(userId);
|
||||||
|
if (tokenResponse == null) {
|
||||||
|
tokenResponse = loadTokenFromDatabase(userId);
|
||||||
|
if (tokenResponse != null) {
|
||||||
|
tokenMap.put(userId, tokenResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tokenResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Credential buildCredential(HttpTransport transport, JsonFactory jsonFactory, String userId, String clientId, String clientSecret) {
|
public Credential buildCredential(HttpTransport transport, JsonFactory jsonFactory, String userId, String clientId, String clientSecret) {
|
||||||
TokenResponse tokenResponse = tokenMap.get(userId);
|
TokenResponse tokenResponse = getTokenResponse(userId);
|
||||||
if (tokenResponse == null) {
|
if (tokenResponse == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -33,4 +45,24 @@ public class TokenStore {
|
||||||
.build()
|
.build()
|
||||||
.setFromTokenResponse(tokenResponse);
|
.setFromTokenResponse(tokenResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private TokenResponse loadTokenFromDatabase(String userId) {
|
||||||
|
UserGoogleToken userGoogleToken = userGoogleTokenRepository.findUserGoogleTokenByEmail(userId);
|
||||||
|
if (userGoogleToken == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
TokenResponse tokenResponse = new TokenResponse()
|
||||||
|
.setAccessToken(userGoogleToken.getAccessToken())
|
||||||
|
.setRefreshToken(userGoogleToken.getRefreshToken())
|
||||||
|
.setScope(userGoogleToken.getScope())
|
||||||
|
.setTokenType(userGoogleToken.getTokenType())
|
||||||
|
.setExpiresInSeconds(userGoogleToken.getExpiresIn());
|
||||||
|
|
||||||
|
if (userGoogleToken.getRefreshTokenExpiresIn() != null) {
|
||||||
|
tokenResponse.set("refresh_token_expires_in", userGoogleToken.getRefreshTokenExpiresIn());
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenResponse;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,28 +22,23 @@ dependencies {
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-log4j2:3.4.0'
|
implementation 'org.springframework.boot:spring-boot-starter-log4j2:3.4.0'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-validation:3.4.0'
|
implementation 'org.springframework.boot:spring-boot-starter-validation:3.4.0'
|
||||||
implementation 'de.mkammerer:argon2-jvm:2.1'
|
implementation 'de.mkammerer:argon2-jvm:2.1'
|
||||||
implementation "com.google.apis:google-api-services-youtube:v3-rev182-1.22.0"
|
// Google API Services
|
||||||
implementation("com.google.collections:google-collections:1.0")
|
implementation 'com.google.apis:google-api-services-youtube:v3-rev182-1.22.0'
|
||||||
implementation("com.google.guava:guava:20.0")
|
implementation 'com.google.apis:google-api-services-youtubeAnalytics:v2-rev272-1.25.0'
|
||||||
implementation("com.google.apis:google-api-services-youtubeAnalytics:v2-rev272-1.25.0")
|
implementation 'com.google.apis:google-api-services-youtubereporting:v1-rev10-1.22.0'
|
||||||
implementation "com.google.http-client:google-http-client-jackson2:1.20.0"
|
|
||||||
|
|
||||||
// OAuth Client
|
|
||||||
implementation "com.google.oauth-client:google-oauth-client-jetty:1.20.0"
|
|
||||||
implementation 'com.google.apis:google-api-services-oauth2:v2-rev157-1.25.0'
|
implementation 'com.google.apis:google-api-services-oauth2:v2-rev157-1.25.0'
|
||||||
implementation 'com.google.oauth-client:google-oauth-client-jetty:1.34.1'
|
|
||||||
implementation 'com.google.api-client:google-api-client:2.3.0'
|
|
||||||
|
|
||||||
// Google Collections
|
// Google API Client (phiên bản mới để tương thích với YouTube libs)
|
||||||
implementation "com.google.collections:google-collections:1.0"
|
implementation 'com.google.api-client:google-api-client:1.34.1'
|
||||||
|
implementation 'com.google.oauth-client:google-oauth-client-jetty:1.34.1'
|
||||||
|
implementation 'com.google.http-client:google-http-client-jackson2:1.43.3'
|
||||||
|
implementation 'com.google.guava:guava:32.1.3-jre'
|
||||||
implementation 'com.google.code.gson:gson:2.11.0'
|
implementation 'com.google.code.gson:gson:2.11.0'
|
||||||
|
|
||||||
annotationProcessor 'org.projectlombok:lombok:1.18.38'
|
annotationProcessor 'org.projectlombok:lombok:1.18.38'
|
||||||
implementation project(":vega-hrm-core")
|
implementation project(":vega-hrm-core")
|
||||||
implementation 'jakarta.annotation:jakarta.annotation-api:2.1.1'
|
implementation 'jakarta.annotation:jakarta.annotation-api:2.1.1'
|
||||||
|
|
||||||
// YouTube Reporting API
|
|
||||||
implementation "com.google.apis:google-api-services-youtubereporting:v1-rev10-1.22.0"
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
configurations {
|
configurations {
|
||||||
|
|
@ -53,5 +48,17 @@ configurations {
|
||||||
|
|
||||||
// Loại bỏ logging mặc định
|
// Loại bỏ logging mặc định
|
||||||
exclude group: 'ch.qos.logback', module: 'logback-classic'
|
exclude group: 'ch.qos.logback', module: 'logback-classic'
|
||||||
|
|
||||||
|
// Loại bỏ các thư viện Guava/Google Collections cũ
|
||||||
|
exclude group: 'com.google.guava', module: 'guava-jdk5'
|
||||||
|
exclude group: 'com.google.collections', module: 'google-collections'
|
||||||
|
|
||||||
|
resolutionStrategy {
|
||||||
|
// Bắt buộc dùng Guava mới nhất
|
||||||
|
force 'com.google.guava:guava:32.1.3-jre'
|
||||||
|
force 'com.google.api-client:google-api-client:1.34.1'
|
||||||
|
force 'com.google.oauth-client:google-oauth-client-jetty:1.34.1'
|
||||||
|
force 'com.google.http-client:google-http-client-jackson2:1.43.3'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package com.vega.hrm.report.config;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.models.OpenAPI;
|
||||||
|
import io.swagger.v3.oas.models.info.Contact;
|
||||||
|
import io.swagger.v3.oas.models.info.Info;
|
||||||
|
import io.swagger.v3.oas.models.info.License;
|
||||||
|
import org.springdoc.core.models.GroupedOpenApi;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class OpenApiConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public OpenAPI reportOpenAPI() {
|
||||||
|
return new OpenAPI()
|
||||||
|
.info(new Info()
|
||||||
|
.title("Vega HRM Report API")
|
||||||
|
.version("v1")
|
||||||
|
.description("Danh mục API phục vụ trích xuất báo cáo và đồng bộ dữ liệu YouTube/Google cho Vega HRM.")
|
||||||
|
.contact(new Contact()
|
||||||
|
.name("Vega HRM Backend Team")
|
||||||
|
.email("backend@vegahrm.local"))
|
||||||
|
.license(new License().name("Proprietary")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public GroupedOpenApi reportGroupedOpenApi() {
|
||||||
|
return GroupedOpenApi.builder()
|
||||||
|
.group("report")
|
||||||
|
.packagesToScan("com.vega.hrm.report.controller")
|
||||||
|
.pathsToMatch("/api/**")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -6,15 +6,26 @@ import com.vega.hrm.core.component.TokenStore;
|
||||||
import com.vega.hrm.core.dto.GoogleOAuthConfig;
|
import com.vega.hrm.core.dto.GoogleOAuthConfig;
|
||||||
import com.vega.hrm.core.models.responses.BaseResponse;
|
import com.vega.hrm.core.models.responses.BaseResponse;
|
||||||
import com.vega.hrm.report.request.GetDragRevenueRequest;
|
import com.vega.hrm.report.request.GetDragRevenueRequest;
|
||||||
|
import com.vega.hrm.report.request.GetRevenueRequest;
|
||||||
|
import com.vega.hrm.report.response.ReportTypeDto;
|
||||||
|
import com.vega.hrm.report.response.RevenueDataDto;
|
||||||
import com.vega.hrm.report.serivce.CreateReportingJobService;
|
import com.vega.hrm.report.serivce.CreateReportingJobService;
|
||||||
|
import com.vega.hrm.report.serivce.YouTubeReportService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.util.List;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
|
@ -24,19 +35,29 @@ import org.springframework.web.bind.annotation.RestController;
|
||||||
public class ReportGoogleController {
|
public class ReportGoogleController {
|
||||||
|
|
||||||
private final CreateReportingJobService createReportingJob;
|
private final CreateReportingJobService createReportingJob;
|
||||||
|
private final YouTubeReportService youTubeReportService;
|
||||||
private final GoogleOAuthConfig googleOAuthConfig;
|
private final GoogleOAuthConfig googleOAuthConfig;
|
||||||
|
private final TokenStore tokenStore;
|
||||||
|
|
||||||
@GetMapping("/youtube/demo")
|
@GetMapping("/youtube/demo")
|
||||||
@Operation(summary = "Khởi tạo job demo YouTube", description = "Thực thi job báo cáo mẫu với cấu hình được lưu trữ trong hệ thống.")
|
@Operation(summary = "Khởi tạo job demo YouTube", description = "Thực thi job báo cáo mẫu với cấu hình được lưu trữ trong hệ thống.")
|
||||||
public ResponseEntity<BaseResponse<Boolean>> getRevenue(GetDragRevenueRequest getDragRevenueRequest)
|
public ResponseEntity<BaseResponse<Boolean>> getRevenue(@RequestBody GetDragRevenueRequest getDragRevenueRequest)
|
||||||
throws GeneralSecurityException, IOException {
|
throws GeneralSecurityException, IOException {
|
||||||
var tokenStore = new TokenStore();
|
String email = getDragRevenueRequest != null ? getDragRevenueRequest.getEmail() : null;
|
||||||
|
if (!StringUtils.hasText(email)) {
|
||||||
|
return ResponseEntity.ok(BaseResponse.invalid("Email không được để trống"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokenStore.getTokenResponse(email) == null) {
|
||||||
|
return ResponseEntity.ok(BaseResponse.invalid("Không tìm thấy token Google cho email đã cung cấp"));
|
||||||
|
}
|
||||||
|
|
||||||
createReportingJob.createJobWithStoredCredential(
|
createReportingJob.createJobWithStoredCredential(
|
||||||
tokenStore,
|
tokenStore,
|
||||||
googleOAuthConfig,
|
googleOAuthConfig,
|
||||||
GoogleNetHttpTransport.newTrustedTransport(),
|
GoogleNetHttpTransport.newTrustedTransport(),
|
||||||
JacksonFactory.getDefaultInstance(),
|
JacksonFactory.getDefaultInstance(),
|
||||||
"default",
|
email,
|
||||||
"channel_monetized_playback_a1", // report type
|
"channel_monetized_playback_a1", // report type
|
||||||
"vega-monetization-daily" // job name
|
"vega-monetization-daily" // job name
|
||||||
);
|
);
|
||||||
|
|
@ -48,4 +69,59 @@ public class ReportGoogleController {
|
||||||
.build()
|
.build()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/report-types")
|
||||||
|
@Operation(summary = "Lấy danh sách Report Types", description = "Lấy tất cả các loại báo cáo có sẵn từ YouTube Reporting API.")
|
||||||
|
public ResponseEntity<BaseResponse<List<ReportTypeDto>>> getReportTypes(
|
||||||
|
@Parameter(description = "Email của người dùng đã xác thực với Google")
|
||||||
|
@RequestParam String email
|
||||||
|
) throws GeneralSecurityException, IOException {
|
||||||
|
if (!StringUtils.hasText(email)) {
|
||||||
|
return ResponseEntity.ok(BaseResponse.invalid("Email không được để trống"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokenStore.getTokenResponse(email) == null) {
|
||||||
|
return ResponseEntity.ok(BaseResponse.invalid("Không tìm thấy token Google cho email đã cung cấp"));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ReportTypeDto> reportTypes = youTubeReportService.getReportTypes(
|
||||||
|
email,
|
||||||
|
GoogleNetHttpTransport.newTrustedTransport(),
|
||||||
|
JacksonFactory.getDefaultInstance()
|
||||||
|
);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(
|
||||||
|
BaseResponse.<List<ReportTypeDto>>builder()
|
||||||
|
.code("00")
|
||||||
|
.message("Lấy danh sách report types thành công")
|
||||||
|
.data(reportTypes)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/revenue")
|
||||||
|
@Operation(summary = "Lấy dữ liệu doanh thu", description = "Lấy dữ liệu doanh thu ước tính từ YouTube Analytics API theo khoảng thời gian.")
|
||||||
|
public ResponseEntity<BaseResponse<List<RevenueDataDto>>> getRevenueData(
|
||||||
|
@Valid @RequestBody GetRevenueRequest request
|
||||||
|
) throws GeneralSecurityException, IOException {
|
||||||
|
if (tokenStore.getTokenResponse(request.getEmail()) == null) {
|
||||||
|
return ResponseEntity.ok(BaseResponse.invalid("Không tìm thấy token Google cho email đã cung cấp"));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<RevenueDataDto> revenueData = youTubeReportService.getRevenue(
|
||||||
|
request.getEmail(),
|
||||||
|
request.getStartDate(),
|
||||||
|
request.getEndDate(),
|
||||||
|
GoogleNetHttpTransport.newTrustedTransport(),
|
||||||
|
JacksonFactory.getDefaultInstance()
|
||||||
|
);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(
|
||||||
|
BaseResponse.<List<RevenueDataDto>>builder()
|
||||||
|
.code("00")
|
||||||
|
.message("Lấy dữ liệu doanh thu thành công")
|
||||||
|
.data(revenueData)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.vega.hrm.report.request;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class GetRevenueRequest {
|
||||||
|
@NotBlank(message = "Email không được để trống")
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
@NotBlank(message = "Ngày bắt đầu không được để trống")
|
||||||
|
private String startDate; // format: YYYY-MM-DD
|
||||||
|
|
||||||
|
@NotBlank(message = "Ngày kết thúc không được để trống")
|
||||||
|
private String endDate; // format: YYYY-MM-DD
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.vega.hrm.report.response;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ReportTypeDto {
|
||||||
|
private String id;
|
||||||
|
private String name;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.vega.hrm.report.response;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class RevenueDataDto {
|
||||||
|
private String date;
|
||||||
|
private Double estimatedRevenue;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -15,11 +15,9 @@ import com.google.api.services.youtubereporting.model.ReportType;
|
||||||
import com.vega.hrm.core.component.TokenStore;
|
import com.vega.hrm.core.component.TokenStore;
|
||||||
import com.vega.hrm.core.dto.GoogleOAuthConfig;
|
import com.vega.hrm.core.dto.GoogleOAuthConfig;
|
||||||
import com.vega.hrm.core.helpers.LogHelper;
|
import com.vega.hrm.core.helpers.LogHelper;
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
package com.vega.hrm.report.serivce;
|
||||||
|
|
||||||
|
import com.google.api.client.auth.oauth2.Credential;
|
||||||
|
import com.google.api.client.googleapis.json.GoogleJsonResponseException;
|
||||||
|
import com.google.api.client.http.HttpTransport;
|
||||||
|
import com.google.api.client.json.JsonFactory;
|
||||||
|
import com.google.api.services.youtubeAnalytics.v2.YouTubeAnalytics;
|
||||||
|
import com.google.api.services.youtubeAnalytics.v2.model.QueryResponse;
|
||||||
|
import com.google.api.services.youtubereporting.YouTubeReporting;
|
||||||
|
import com.google.api.services.youtubereporting.model.ListReportTypesResponse;
|
||||||
|
import com.google.api.services.youtubereporting.model.ReportType;
|
||||||
|
import com.vega.hrm.core.component.TokenStore;
|
||||||
|
import com.vega.hrm.core.dto.GoogleOAuthConfig;
|
||||||
|
import com.vega.hrm.core.helpers.LogHelper;
|
||||||
|
import com.vega.hrm.report.response.ReportTypeDto;
|
||||||
|
import com.vega.hrm.report.response.RevenueDataDto;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class YouTubeReportService {
|
||||||
|
private static final String APPLICATION_NAME = "vega-report";
|
||||||
|
private final TokenStore tokenStore;
|
||||||
|
private final GoogleOAuthConfig googleOAuthConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lấy danh sách tất cả report types từ YouTube Reporting API
|
||||||
|
*/
|
||||||
|
public List<ReportTypeDto> getReportTypes(String email, HttpTransport httpTransport, JsonFactory jsonFactory)
|
||||||
|
throws IOException {
|
||||||
|
Credential credential = tokenStore.buildCredential(
|
||||||
|
httpTransport,
|
||||||
|
jsonFactory,
|
||||||
|
email,
|
||||||
|
googleOAuthConfig.clientId,
|
||||||
|
googleOAuthConfig.clientSecret
|
||||||
|
);
|
||||||
|
|
||||||
|
if (credential == null) {
|
||||||
|
throw new IllegalStateException("Không tìm thấy token Google cho email: " + email);
|
||||||
|
}
|
||||||
|
|
||||||
|
YouTubeReporting youtubeReporting = new YouTubeReporting.Builder(httpTransport, jsonFactory, credential)
|
||||||
|
.setApplicationName(APPLICATION_NAME)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try {
|
||||||
|
ListReportTypesResponse reportTypesListResponse = youtubeReporting.reportTypes().list().execute();
|
||||||
|
List<ReportType> reportTypeList = reportTypesListResponse.getReportTypes();
|
||||||
|
|
||||||
|
if (reportTypeList == null || reportTypeList.isEmpty()) {
|
||||||
|
LogHelper.info("Không tìm thấy report types nào.");
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ReportTypeDto> result = new ArrayList<>();
|
||||||
|
for (ReportType reportType : reportTypeList) {
|
||||||
|
result.add(ReportTypeDto.builder()
|
||||||
|
.id(reportType.getId())
|
||||||
|
.name(reportType.getName())
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (GoogleJsonResponseException e) {
|
||||||
|
LogHelper.error("Lỗi khi lấy report types: " + e.getDetails().getMessage());
|
||||||
|
throw new IOException("Không thể lấy danh sách report types: " + e.getDetails().getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lấy dữ liệu doanh thu từ YouTube Analytics API
|
||||||
|
*/
|
||||||
|
public List<RevenueDataDto> getRevenue(String email, String startDate, String endDate,
|
||||||
|
HttpTransport httpTransport, JsonFactory jsonFactory)
|
||||||
|
throws IOException {
|
||||||
|
Credential credential = tokenStore.buildCredential(
|
||||||
|
httpTransport,
|
||||||
|
jsonFactory,
|
||||||
|
email,
|
||||||
|
googleOAuthConfig.clientId,
|
||||||
|
googleOAuthConfig.clientSecret
|
||||||
|
);
|
||||||
|
|
||||||
|
if (credential == null) {
|
||||||
|
throw new IllegalStateException("Không tìm thấy token Google cho email: " + email);
|
||||||
|
}
|
||||||
|
|
||||||
|
YouTubeAnalytics youtubeAnalytics = new YouTubeAnalytics.Builder(httpTransport, jsonFactory, credential)
|
||||||
|
.setApplicationName(APPLICATION_NAME)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try {
|
||||||
|
QueryResponse queryResponse = youtubeAnalytics.reports().query()
|
||||||
|
.setIds("channel==MINE")
|
||||||
|
.setStartDate(startDate)
|
||||||
|
.setEndDate(endDate)
|
||||||
|
.setMetrics("estimatedRevenue")
|
||||||
|
.setDimensions("day")
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
List<RevenueDataDto> result = new ArrayList<>();
|
||||||
|
|
||||||
|
if (queryResponse.getRows() != null) {
|
||||||
|
for (List<Object> row : queryResponse.getRows()) {
|
||||||
|
if (row.size() >= 2) {
|
||||||
|
String date = row.get(0).toString();
|
||||||
|
Double revenue = row.get(1) instanceof Number
|
||||||
|
? ((Number) row.get(1)).doubleValue()
|
||||||
|
: Double.parseDouble(row.get(1).toString());
|
||||||
|
|
||||||
|
result.add(RevenueDataDto.builder()
|
||||||
|
.date(date)
|
||||||
|
.estimatedRevenue(revenue)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LogHelper.info("Đã lấy được " + result.size() + " dòng dữ liệu doanh thu.");
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} catch (GoogleJsonResponseException e) {
|
||||||
|
LogHelper.error("Lỗi khi lấy dữ liệu revenue: " + e.getDetails().getMessage());
|
||||||
|
throw new IOException("Không thể lấy dữ liệu doanh thu: " + e.getDetails().getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user