diff --git a/.gitignore b/.gitignore
index 1062418..dbf292f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
.idea/
*.iml
+authid.txt
diff --git a/analytics-view/.claude/settings.local.json b/analytics-view/.claude/settings.local.json
new file mode 100644
index 0000000..dc5e694
--- /dev/null
+++ b/analytics-view/.claude/settings.local.json
@@ -0,0 +1,7 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(find /c/Users/balex/IdeaProjects/post-hub-platform -type f \\\\\\(-name *.tsx -o -name *.ts -o -name *.jsx -o -name *.js -o -name *.vue \\\\\\) ! -path */node_modules/* ! -path */.next/*)"
+ ]
+ }
+}
diff --git a/gateway-service/pom.xml b/gateway-service/pom.xml
index a22e1b1..1344ffb 100644
--- a/gateway-service/pom.xml
+++ b/gateway-service/pom.xml
@@ -106,6 +106,13 @@
${lombok.version}
true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-client
+
+
diff --git a/gateway-service/src/main/java/com/posthub/gateway/config/SecurityConfig.java b/gateway-service/src/main/java/com/posthub/gateway/config/SecurityConfig.java
index 1856014..06b3b16 100644
--- a/gateway-service/src/main/java/com/posthub/gateway/config/SecurityConfig.java
+++ b/gateway-service/src/main/java/com/posthub/gateway/config/SecurityConfig.java
@@ -1,5 +1,7 @@
package com.posthub.gateway.config;
+import com.posthub.gateway.security.OAuth2AuthenticationSuccessHandler;
+import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
@@ -13,8 +15,11 @@ import reactor.core.publisher.Mono;
@Configuration
@EnableWebFluxSecurity
+@RequiredArgsConstructor
public class SecurityConfig {
+ private final OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;
+
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
@@ -27,7 +32,9 @@ public class SecurityConfig {
.pathMatchers(
"/api/auth/login",
"/api/auth/register",
- "/api/auth/refresh/token"
+ "/api/auth/refresh/token",
+ "/oauth2/authorization/**",
+ "/login/oauth2/code/**"
).permitAll()
.pathMatchers(
"/actuator/**",
@@ -36,6 +43,9 @@ public class SecurityConfig {
).permitAll()
.anyExchange().permitAll()
)
+ .oauth2Login(oauth2 -> oauth2
+ .authenticationSuccessHandler(oAuth2AuthenticationSuccessHandler)
+ )
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint((exchange, ex) -> {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
diff --git a/gateway-service/src/main/java/com/posthub/gateway/model/entity/User.java b/gateway-service/src/main/java/com/posthub/gateway/model/entity/User.java
index 0b99db9..a43b47f 100644
--- a/gateway-service/src/main/java/com/posthub/gateway/model/entity/User.java
+++ b/gateway-service/src/main/java/com/posthub/gateway/model/entity/User.java
@@ -1,5 +1,6 @@
package com.posthub.gateway.model.entity;
+import com.posthub.gateway.model.enums.AuthProvider;
import com.posthub.gateway.model.enums.RegistrationStatus;
import com.posthub.gateway.model.enums.UserRole;
import lombok.Getter;
@@ -40,4 +41,10 @@ public class User {
@Column("role")
private UserRole role;
+
+ @Column("auth_provider")
+ private AuthProvider authProvider;
+
+ @Column("provider_id")
+ private String providerId;
}
\ No newline at end of file
diff --git a/gateway-service/src/main/java/com/posthub/gateway/model/enums/AuthProvider.java b/gateway-service/src/main/java/com/posthub/gateway/model/enums/AuthProvider.java
new file mode 100644
index 0000000..05a81b1
--- /dev/null
+++ b/gateway-service/src/main/java/com/posthub/gateway/model/enums/AuthProvider.java
@@ -0,0 +1,8 @@
+package com.posthub.gateway.model.enums;
+
+public enum AuthProvider {
+ LOCAL,
+ GOOGLE,
+ FACEBOOK,
+ GITHUB
+}
\ No newline at end of file
diff --git a/gateway-service/src/main/java/com/posthub/gateway/repository/UserRepository.java b/gateway-service/src/main/java/com/posthub/gateway/repository/UserRepository.java
index 871eda4..23b7b39 100644
--- a/gateway-service/src/main/java/com/posthub/gateway/repository/UserRepository.java
+++ b/gateway-service/src/main/java/com/posthub/gateway/repository/UserRepository.java
@@ -1,6 +1,7 @@
package com.posthub.gateway.repository;
import com.posthub.gateway.model.entity.User;
+import com.posthub.gateway.model.enums.AuthProvider;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Mono;
@@ -11,4 +12,6 @@ public interface UserRepository extends ReactiveCrudRepository {
Mono findByUsername(String username);
Mono existsByEmail(String email);
+
+ Mono findByAuthProviderAndProviderId(AuthProvider authProvider, String providerId);
}
\ No newline at end of file
diff --git a/gateway-service/src/main/java/com/posthub/gateway/security/OAuth2AuthenticationSuccessHandler.java b/gateway-service/src/main/java/com/posthub/gateway/security/OAuth2AuthenticationSuccessHandler.java
new file mode 100644
index 0000000..1736fb9
--- /dev/null
+++ b/gateway-service/src/main/java/com/posthub/gateway/security/OAuth2AuthenticationSuccessHandler.java
@@ -0,0 +1,135 @@
+package com.posthub.gateway.security;
+
+import com.posthub.gateway.model.entity.RefreshToken;
+import com.posthub.gateway.model.entity.User;
+import com.posthub.gateway.model.enums.AuthProvider;
+import com.posthub.gateway.model.enums.RegistrationStatus;
+import com.posthub.gateway.model.enums.UserRole;
+import com.posthub.gateway.repository.RefreshTokenRepository;
+import com.posthub.gateway.repository.UserRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.security.web.server.WebFilterExchange;
+import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+import java.net.URI;
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class OAuth2AuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {
+
+ private final UserRepository userRepository;
+ private final RefreshTokenRepository refreshTokenRepository;
+ private final JwtTokenProvider jwtTokenProvider;
+
+ @Value("${oauth2.redirect-uri:https://balexvic.com/login}")
+ private String redirectUri;
+
+ @Override
+ public Mono onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
+ OAuth2AuthenticationToken authToken = (OAuth2AuthenticationToken) authentication;
+ OAuth2User oAuth2User = authToken.getPrincipal();
+ String registrationId = authToken.getAuthorizedClientRegistrationId();
+
+ AuthProvider provider = AuthProvider.valueOf(registrationId.toUpperCase());
+ String providerId = extractProviderId(oAuth2User, registrationId);
+ String email = extractEmail(oAuth2User, registrationId);
+ String name = extractName(oAuth2User, registrationId);
+
+ return userRepository.findByAuthProviderAndProviderId(provider, providerId)
+ .switchIfEmpty(
+ email != null
+ ? userRepository.findByEmail(email)
+ .flatMap(existingUser -> {
+ existingUser.setAuthProvider(provider);
+ existingUser.setProviderId(providerId);
+ existingUser.setUpdated(LocalDateTime.now());
+ return userRepository.save(existingUser);
+ })
+ .switchIfEmpty(Mono.defer(() -> createNewUser(provider, providerId, email, name)))
+ : Mono.defer(() -> createNewUser(provider, providerId, email, name))
+ )
+ .flatMap(user -> {
+ user.setLastLogin(LocalDateTime.now());
+ user.setUpdated(LocalDateTime.now());
+ return userRepository.save(user);
+ })
+ .flatMap(this::generateTokensAndRedirect)
+ .flatMap(redirectUrl -> {
+ webFilterExchange.getExchange().getResponse().setStatusCode(HttpStatus.FOUND);
+ webFilterExchange.getExchange().getResponse().getHeaders().setLocation(URI.create(redirectUrl));
+ return webFilterExchange.getExchange().getResponse().setComplete();
+ });
+ }
+
+ private Mono createNewUser(AuthProvider provider, String providerId, String email, String name) {
+ User user = new User();
+ user.setAuthProvider(provider);
+ user.setProviderId(providerId);
+ user.setEmail(email);
+ user.setUsername(name != null ? name : "user_" + providerId.substring(0, 8));
+ user.setRegistrationStatus(RegistrationStatus.ACTIVE);
+ user.setRole(UserRole.USER);
+ user.setCreated(LocalDateTime.now());
+ user.setUpdated(LocalDateTime.now());
+ user.setDeleted(false);
+ log.info("Creating new OAuth2 user: provider={}, email={}", provider, email);
+ return userRepository.save(user);
+ }
+
+ private Mono generateTokensAndRedirect(User user) {
+ return refreshTokenRepository.findByUserId(user.getId())
+ .flatMap(existing -> {
+ existing.setCreated(LocalDateTime.now());
+ existing.setToken(generateUuid());
+ existing.setSessionId(generateUuid());
+ return refreshTokenRepository.save(existing);
+ })
+ .switchIfEmpty(Mono.defer(() -> {
+ RefreshToken newToken = new RefreshToken();
+ newToken.setUserId(user.getId());
+ newToken.setCreated(LocalDateTime.now());
+ newToken.setToken(generateUuid());
+ newToken.setSessionId(generateUuid());
+ return refreshTokenRepository.save(newToken);
+ }))
+ .map(refreshToken -> {
+ String accessToken = jwtTokenProvider.generateToken(user, refreshToken.getSessionId());
+ return redirectUri + "?token=" + accessToken + "&refreshToken=" + refreshToken.getToken();
+ });
+ }
+
+ private String extractProviderId(OAuth2User oAuth2User, String registrationId) {
+ return switch (registrationId) {
+ case "google" -> oAuth2User.getAttribute("sub");
+ case "facebook" -> oAuth2User.getAttribute("id");
+ case "github" -> String.valueOf(oAuth2User.getAttributes().get("id"));
+ default -> oAuth2User.getName();
+ };
+ }
+
+ private String extractEmail(OAuth2User oAuth2User, String registrationId) {
+ return oAuth2User.getAttribute("email");
+ }
+
+ private String extractName(OAuth2User oAuth2User, String registrationId) {
+ return switch (registrationId) {
+ case "github" -> oAuth2User.getAttribute("login");
+ default -> oAuth2User.getAttribute("name");
+ };
+ }
+
+ private String generateUuid() {
+ return UUID.randomUUID().toString().replace("-", "");
+ }
+}
\ No newline at end of file
diff --git a/gateway-service/src/main/resources/application.yml b/gateway-service/src/main/resources/application.yml
index c609ba4..9c28462 100644
--- a/gateway-service/src/main/resources/application.yml
+++ b/gateway-service/src/main/resources/application.yml
@@ -6,6 +6,23 @@ spring:
application:
name: gateway-service
+ security:
+ oauth2:
+ client:
+ registration:
+ google:
+ client-id: ${GOOGLE_CLIENT_ID:}
+ client-secret: ${GOOGLE_CLIENT_SECRET:}
+ scope: email,profile
+ github:
+ client-id: ${GITHUB_CLIENT_ID:}
+ client-secret: ${GITHUB_CLIENT_SECRET:}
+ scope: user:email,read:user
+ facebook:
+ client-id: ${FACEBOOK_CLIENT_ID:}
+ client-secret: ${FACEBOOK_CLIENT_SECRET:}
+ scope: email,public_profile
+
# ---- R2DBC (reactive DB) ----
r2dbc:
url: r2dbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:appdb}
@@ -75,12 +92,18 @@ jwt:
secret: ${JWT_SECRET:}
expiration: ${JWT_EXPIRATION:103600000}
+# ---- OAuth2 redirect after success ----
+oauth2:
+ redirect-uri: ${OAUTH2_REDIRECT_URI:https://balexvic.com/login}
+
# ---- Auth path config ----
auth:
public-paths:
- /api/auth/login
- /api/auth/register
- /api/auth/refresh/token
+ - /oauth2/authorization/**
+ - /login/oauth2/code/**
- /actuator/**
- /api/*/v3/api-docs/**
- /api/*/swagger-ui/**
@@ -102,7 +125,7 @@ management:
health:
show-details: always
gateway:
- enabled: true
+ access: unrestricted
# ---- Logging ----
logging: