diff --git a/gateway-service/pom.xml b/gateway-service/pom.xml index 9487884..a22e1b1 100644 --- a/gateway-service/pom.xml +++ b/gateway-service/pom.xml @@ -43,6 +43,50 @@ spring-cloud-starter-gateway + + + org.springframework.boot + spring-boot-starter-security + + + + + org.springframework.boot + spring-boot-starter-data-r2dbc + + + + + org.postgresql + r2dbc-postgresql + runtime + + + + + io.jsonwebtoken + jjwt-api + 0.13.0 + + + io.jsonwebtoken + jjwt-impl + 0.13.0 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.13.0 + runtime + + + + + org.springframework.boot + spring-boot-starter-validation + + org.springframework.cloud diff --git a/gateway-service/src/main/java/com/posthub/gateway/config/AuthProperties.java b/gateway-service/src/main/java/com/posthub/gateway/config/AuthProperties.java new file mode 100644 index 0000000..573c92b --- /dev/null +++ b/gateway-service/src/main/java/com/posthub/gateway/config/AuthProperties.java @@ -0,0 +1,19 @@ +package com.posthub.gateway.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +@Component +@ConfigurationProperties(prefix = "auth") +@Getter +@Setter +public class AuthProperties { + + private List publicPaths = new ArrayList<>(); + private List adminPaths = new ArrayList<>(); +} \ No newline at end of file diff --git a/gateway-service/src/main/java/com/posthub/gateway/config/R2dbcConfig.java b/gateway-service/src/main/java/com/posthub/gateway/config/R2dbcConfig.java new file mode 100644 index 0000000..3f27d9b --- /dev/null +++ b/gateway-service/src/main/java/com/posthub/gateway/config/R2dbcConfig.java @@ -0,0 +1,63 @@ +package com.posthub.gateway.config; + +import com.posthub.gateway.model.enums.RegistrationStatus; +import com.posthub.gateway.model.enums.UserRole; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.ReadingConverter; +import org.springframework.data.convert.WritingConverter; +import org.springframework.data.r2dbc.convert.R2dbcCustomConversions; +import org.springframework.data.r2dbc.dialect.PostgresDialect; +import org.springframework.lang.NonNull; + +import java.util.List; + +@Configuration +public class R2dbcConfig { + + @Bean + public R2dbcCustomConversions r2dbcCustomConversions() { + return R2dbcCustomConversions.of( + PostgresDialect.INSTANCE, + List.of( + new RegistrationStatusReadConverter(), + new RegistrationStatusWriteConverter(), + new UserRoleReadConverter(), + new UserRoleWriteConverter() + ) + ); + } + + @ReadingConverter + static class RegistrationStatusReadConverter implements Converter { + @Override + public RegistrationStatus convert(@NonNull String source) { + return RegistrationStatus.valueOf(source); + } + } + + @WritingConverter + static class RegistrationStatusWriteConverter implements Converter { + @Override + public String convert(@NonNull RegistrationStatus source) { + return source.name(); + } + } + + @ReadingConverter + static class UserRoleReadConverter implements Converter { + @Override + public UserRole convert(@NonNull String source) { + return UserRole.valueOf(source); + } + } + + @WritingConverter + static class UserRoleWriteConverter implements Converter { + @Override + public String convert(@NonNull UserRole source) { + return source.name(); + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..1856014 --- /dev/null +++ b/gateway-service/src/main/java/com/posthub/gateway/config/SecurityConfig.java @@ -0,0 +1,56 @@ +package com.posthub.gateway.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.server.SecurityWebFilterChain; +import reactor.core.publisher.Mono; + +@Configuration +@EnableWebFluxSecurity +public class SecurityConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + return http + .csrf(ServerHttpSecurity.CsrfSpec::disable) + .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable) + .formLogin(ServerHttpSecurity.FormLoginSpec::disable) + .logout(ServerHttpSecurity.LogoutSpec::disable) + .authorizeExchange(exchanges -> exchanges + .pathMatchers(HttpMethod.OPTIONS, "/**").permitAll() + .pathMatchers( + "/api/auth/login", + "/api/auth/register", + "/api/auth/refresh/token" + ).permitAll() + .pathMatchers( + "/actuator/**", + "/v3/api-docs/**", + "/swagger-ui/**" + ).permitAll() + .anyExchange().permitAll() + ) + .exceptionHandling(exceptions -> exceptions + .authenticationEntryPoint((exchange, ex) -> { + exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); + return Mono.empty(); + }) + .accessDeniedHandler((exchange, denied) -> { + exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN); + return Mono.empty(); + }) + ) + .build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/gateway-service/src/main/java/com/posthub/gateway/controller/AuthController.java b/gateway-service/src/main/java/com/posthub/gateway/controller/AuthController.java new file mode 100644 index 0000000..3cea092 --- /dev/null +++ b/gateway-service/src/main/java/com/posthub/gateway/controller/AuthController.java @@ -0,0 +1,66 @@ +package com.posthub.gateway.controller; + +import com.posthub.gateway.model.request.LoginRequest; +import com.posthub.gateway.model.request.RegistrationUserRequest; +import com.posthub.gateway.model.response.RagResponse; +import com.posthub.gateway.model.response.UserProfileDTO; +import com.posthub.gateway.service.AuthService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; +import org.springframework.http.ResponseEntity; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Mono; + +@RestController +@RequestMapping("/api/auth") +@RequiredArgsConstructor +public class AuthController { + + private final AuthService authService; + + @PostMapping("/login") + public Mono>> login( + @RequestBody @Valid LoginRequest request, + ServerHttpResponse response) { + return authService.login(request) + .map(result -> { + addAuthCookie(response, result.getPayload().getToken()); + return ResponseEntity.ok(result); + }); + } + + @PostMapping("/register") + public Mono>> register( + @RequestBody @Valid RegistrationUserRequest request, + ServerHttpResponse response) { + return authService.register(request) + .map(result -> { + addAuthCookie(response, result.getPayload().getToken()); + return ResponseEntity.status(HttpStatus.CREATED).body(result); + }); + } + + @GetMapping("/refresh/token") + public Mono>> refreshToken( + @RequestParam(name = "token") String refreshToken, + ServerHttpResponse response) { + return authService.refreshAccessToken(refreshToken) + .map(result -> { + addAuthCookie(response, result.getPayload().getToken()); + return ResponseEntity.ok(result); + }); + } + + private void addAuthCookie(ServerHttpResponse response, String token) { + ResponseCookie cookie = ResponseCookie.from("Authorization", token) + .httpOnly(true) + .secure(true) + .path("/") + .maxAge(300) + .build(); + response.addCookie(cookie); + } +} \ No newline at end of file diff --git a/gateway-service/src/main/java/com/posthub/gateway/filter/AuthFilter.java b/gateway-service/src/main/java/com/posthub/gateway/filter/AuthFilter.java deleted file mode 100644 index c315ebc..0000000 --- a/gateway-service/src/main/java/com/posthub/gateway/filter/AuthFilter.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.posthub.gateway.filter; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.cloud.gateway.filter.GatewayFilter; -import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.http.server.reactive.ServerHttpResponse; -import org.springframework.stereotype.Component; -import reactor.core.publisher.Mono; - -import java.util.List; - -/** - * Per-route filter: validates that Authorization header contains a Bearer token. - * Does NOT verify JWT signature — downstream services handle that. - * - * Usage in application.yml: - * filters: - * - AuthFilter - */ -@Slf4j -@Component -public class AuthFilter extends AbstractGatewayFilterFactory { - - public AuthFilter() { - super(Config.class); - } - - @Override - public GatewayFilter apply(Config config) { - return (exchange, chain) -> { - ServerHttpRequest request = exchange.getRequest(); - String path = request.getURI().getPath(); - - List authHeaders = request.getHeaders().getOrEmpty(HttpHeaders.AUTHORIZATION); - - if (authHeaders.isEmpty()) { - log.warn("Missing Authorization header for {}", path); - return unauthorized(exchange); - } - - String token = authHeaders.getFirst(); - if (token == null || !token.startsWith("Bearer ") || !token.contains(".")) { - log.warn("Invalid Authorization header format for {}", path); - return unauthorized(exchange); - } - - log.debug("Token present for {}", path); - return chain.filter(exchange); - }; - } - - private Mono unauthorized(org.springframework.web.server.ServerWebExchange exchange) { - ServerHttpResponse response = exchange.getResponse(); - response.setStatusCode(HttpStatus.UNAUTHORIZED); - return response.setComplete(); - } - - public static class Config { - } -} diff --git a/gateway-service/src/main/java/com/posthub/gateway/filter/JwtAuthGlobalFilter.java b/gateway-service/src/main/java/com/posthub/gateway/filter/JwtAuthGlobalFilter.java new file mode 100644 index 0000000..a5f50d0 --- /dev/null +++ b/gateway-service/src/main/java/com/posthub/gateway/filter/JwtAuthGlobalFilter.java @@ -0,0 +1,116 @@ +package com.posthub.gateway.filter; + +import com.posthub.gateway.config.AuthProperties; +import com.posthub.gateway.security.JwtTokenProvider; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.core.Ordered; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.stereotype.Component; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtAuthGlobalFilter implements GlobalFilter, Ordered { + + private final JwtTokenProvider jwtTokenProvider; + private final AuthProperties authProperties; + private final AntPathMatcher pathMatcher = new AntPathMatcher(); + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + ServerHttpRequest request = exchange.getRequest(); + String path = request.getURI().getPath(); + + // 1. Public paths — пропустить без JWT + if (isPublicPath(path)) { + return chain.filter(exchange); + } + + // 2. Извлечь токен + String token = extractToken(request); + if (token == null) { + log.warn("Missing JWT for path: {}", path); + return unauthorized(exchange); + } + + // 3. Валидация JWT + if (!jwtTokenProvider.validateToken(token)) { + log.warn("Invalid JWT for path: {}", path); + return unauthorized(exchange); + } + + // 4. Проверка роли для admin-paths + String role = jwtTokenProvider.getUserRole(token); + if (isAdminPath(path) && !"ADMIN".equals(role)) { + log.warn("Access denied for role {} to admin path: {}", role, path); + return forbidden(exchange); + } + + // 5. Добавить headers в downstream-запрос + String userId = jwtTokenProvider.getUserId(token); + String email = jwtTokenProvider.getUserEmail(token); + String username = jwtTokenProvider.getUsername(token); + + ServerHttpRequest modifiedRequest = request.mutate() + .header("X-User-Id", userId) + .header("X-User-Email", email) + .header("X-User-Name", username) + .header("X-User-Role", role) + .build(); + + log.debug("JWT validated for user {} (role={}) -> {}", email, role, path); + + return chain.filter(exchange.mutate().request(modifiedRequest).build()); + } + + private String extractToken(ServerHttpRequest request) { + List authHeaders = request.getHeaders().getOrEmpty(HttpHeaders.AUTHORIZATION); + if (authHeaders.isEmpty()) { + return null; + } + String header = authHeaders.getFirst(); + if (header != null && header.startsWith("Bearer ") && header.contains(".")) { + return header.substring(7); + } + return null; + } + + private boolean isPublicPath(String path) { + return authProperties.getPublicPaths().stream() + .anyMatch(pattern -> pathMatcher.match(pattern, path)); + } + + private boolean isAdminPath(String path) { + return authProperties.getAdminPaths().stream() + .anyMatch(pattern -> pathMatcher.match(pattern, path)); + } + + private Mono unauthorized(ServerWebExchange exchange) { + ServerHttpResponse response = exchange.getResponse(); + response.setStatusCode(HttpStatus.UNAUTHORIZED); + return response.setComplete(); + } + + private Mono forbidden(ServerWebExchange exchange) { + ServerHttpResponse response = exchange.getResponse(); + response.setStatusCode(HttpStatus.FORBIDDEN); + return response.setComplete(); + } + + @Override + public int getOrder() { + // После RequestLoggingFilter (HIGHEST_PRECEDENCE), но до route filters + return Ordered.HIGHEST_PRECEDENCE + 1; + } +} \ No newline at end of file diff --git a/gateway-service/src/main/java/com/posthub/gateway/model/constants/ApiConstants.java b/gateway-service/src/main/java/com/posthub/gateway/model/constants/ApiConstants.java new file mode 100644 index 0000000..9348a01 --- /dev/null +++ b/gateway-service/src/main/java/com/posthub/gateway/model/constants/ApiConstants.java @@ -0,0 +1,21 @@ +package com.posthub.gateway.model.constants; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class ApiConstants { + + public static final String DASH = "-"; + + public static final String PASSWORD_ALL_CHARACTERS = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~`!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?"; + public static final String PASSWORD_LETTERS_UPPER_CASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + public static final String PASSWORD_LETTERS_LOWER_CASE = "abcdefghijklmnopqrstuvwxyz"; + public static final String PASSWORD_DIGITS = "0123456789"; + public static final String PASSWORD_CHARACTERS = "~`!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?"; + public static final Integer REQUIRED_MIN_PASSWORD_LENGTH = 8; + public static final Integer REQUIRED_MIN_LETTERS_NUMBER_EVERY_CASE_IN_PASSWORD = 1; + public static final Integer REQUIRED_MIN_DIGITS_NUMBER_IN_PASSWORD = 1; + public static final Integer REQUIRED_MIN_CHARACTERS_NUMBER_IN_PASSWORD = 1; +} \ No newline at end of file diff --git a/gateway-service/src/main/java/com/posthub/gateway/model/constants/ApiErrorMessage.java b/gateway-service/src/main/java/com/posthub/gateway/model/constants/ApiErrorMessage.java new file mode 100644 index 0000000..42304ac --- /dev/null +++ b/gateway-service/src/main/java/com/posthub/gateway/model/constants/ApiErrorMessage.java @@ -0,0 +1,29 @@ +package com.posthub.gateway.model.constants; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public enum ApiErrorMessage { + + USERNAME_ALREADY_EXISTS("Username: %s already exists"), + EMAIL_ALREADY_EXISTS("Email: %s already exists"), + INVALID_USER_OR_PASSWORD("Invalid email or password. Try again"), + NOT_FOUND_REFRESH_TOKEN("Refresh token not found"), + MISMATCH_PASSWORDS("Password does not match"), + INVALID_PASSWORD("Invalid password. It must have: " + + "length at least " + ApiConstants.REQUIRED_MIN_PASSWORD_LENGTH + ", including " + + ApiConstants.REQUIRED_MIN_LETTERS_NUMBER_EVERY_CASE_IN_PASSWORD + " letter(s) in upper and lower cases, " + + ApiConstants.REQUIRED_MIN_CHARACTERS_NUMBER_IN_PASSWORD + " character(s), " + + ApiConstants.REQUIRED_MIN_DIGITS_NUMBER_IN_PASSWORD + " digit(s). "), + ACCOUNT_NOT_ACTIVE("Account is not active"), + ; + + private final String message; + + public String getMessage(Object... args) { + return String.format(message, args); + } +} \ No newline at end of file diff --git a/gateway-service/src/main/java/com/posthub/gateway/model/entity/RefreshToken.java b/gateway-service/src/main/java/com/posthub/gateway/model/entity/RefreshToken.java new file mode 100644 index 0000000..e98c9a2 --- /dev/null +++ b/gateway-service/src/main/java/com/posthub/gateway/model/entity/RefreshToken.java @@ -0,0 +1,30 @@ +package com.posthub.gateway.model.entity; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; + +import java.time.LocalDateTime; + +@Table("refresh_token") +@Getter +@Setter +@NoArgsConstructor +public class RefreshToken { + + @Id + private Integer id; + + private String token; + + private LocalDateTime created; + + @Column("session_id") + private String sessionId; + + @Column("user_id") + private Integer userId; +} \ No newline at end of file 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 new file mode 100644 index 0000000..0b99db9 --- /dev/null +++ b/gateway-service/src/main/java/com/posthub/gateway/model/entity/User.java @@ -0,0 +1,43 @@ +package com.posthub.gateway.model.entity; + +import com.posthub.gateway.model.enums.RegistrationStatus; +import com.posthub.gateway.model.enums.UserRole; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; + +import java.time.LocalDateTime; + +@Table("users") +@Getter +@Setter +@NoArgsConstructor +public class User { + + @Id + private Integer id; + + private String username; + + private String password; + + private String email; + + private LocalDateTime created; + + private LocalDateTime updated; + + @Column("last_login") + private LocalDateTime lastLogin; + + private Boolean deleted; + + @Column("registration_status") + private RegistrationStatus registrationStatus; + + @Column("role") + private UserRole role; +} \ No newline at end of file diff --git a/gateway-service/src/main/java/com/posthub/gateway/model/enums/RegistrationStatus.java b/gateway-service/src/main/java/com/posthub/gateway/model/enums/RegistrationStatus.java new file mode 100644 index 0000000..3dd2fec --- /dev/null +++ b/gateway-service/src/main/java/com/posthub/gateway/model/enums/RegistrationStatus.java @@ -0,0 +1,7 @@ +package com.posthub.gateway.model.enums; + +public enum RegistrationStatus { + ACTIVE, + BLOCKED, + DELETED +} \ No newline at end of file diff --git a/gateway-service/src/main/java/com/posthub/gateway/model/enums/UserRole.java b/gateway-service/src/main/java/com/posthub/gateway/model/enums/UserRole.java new file mode 100644 index 0000000..88104e9 --- /dev/null +++ b/gateway-service/src/main/java/com/posthub/gateway/model/enums/UserRole.java @@ -0,0 +1,6 @@ +package com.posthub.gateway.model.enums; + +public enum UserRole { + USER, + ADMIN +} \ No newline at end of file diff --git a/gateway-service/src/main/java/com/posthub/gateway/model/request/LoginRequest.java b/gateway-service/src/main/java/com/posthub/gateway/model/request/LoginRequest.java new file mode 100644 index 0000000..1fb3fc9 --- /dev/null +++ b/gateway-service/src/main/java/com/posthub/gateway/model/request/LoginRequest.java @@ -0,0 +1,19 @@ +package com.posthub.gateway.model.request; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class LoginRequest { + + @Email + @NotNull + private String email; + + @NotEmpty + private String password; +} \ No newline at end of file diff --git a/gateway-service/src/main/java/com/posthub/gateway/model/request/RegistrationUserRequest.java b/gateway-service/src/main/java/com/posthub/gateway/model/request/RegistrationUserRequest.java new file mode 100644 index 0000000..d138928 --- /dev/null +++ b/gateway-service/src/main/java/com/posthub/gateway/model/request/RegistrationUserRequest.java @@ -0,0 +1,28 @@ +package com.posthub.gateway.model.request; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class RegistrationUserRequest { + + @NotBlank + @Size(min = 2, max = 30) + private String username; + + @NotBlank + @Email + @Size(max = 50) + private String email; + + @NotBlank + @Size(min = 6, max = 80) + private String password; + + @NotBlank + private String confirmPassword; +} \ No newline at end of file diff --git a/gateway-service/src/main/java/com/posthub/gateway/model/response/RagResponse.java b/gateway-service/src/main/java/com/posthub/gateway/model/response/RagResponse.java new file mode 100644 index 0000000..2618109 --- /dev/null +++ b/gateway-service/src/main/java/com/posthub/gateway/model/response/RagResponse.java @@ -0,0 +1,25 @@ +package com.posthub.gateway.model.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class RagResponse

{ + + private String message; + private P payload; + private boolean success; + + public static

RagResponse

createSuccessful(P payload) { + return new RagResponse<>("", payload, true); + } + + public static

RagResponse

createSuccessfulWithNewToken(P payload) { + return new RagResponse<>("Token created or updated", payload, true); + } +} \ No newline at end of file diff --git a/gateway-service/src/main/java/com/posthub/gateway/model/response/UserProfileDTO.java b/gateway-service/src/main/java/com/posthub/gateway/model/response/UserProfileDTO.java new file mode 100644 index 0000000..cdd099e --- /dev/null +++ b/gateway-service/src/main/java/com/posthub/gateway/model/response/UserProfileDTO.java @@ -0,0 +1,22 @@ +package com.posthub.gateway.model.response; + +import com.posthub.gateway.model.enums.RegistrationStatus; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +@AllArgsConstructor +public class UserProfileDTO { + + private Integer id; + private String username; + private String email; + private RegistrationStatus registrationStatus; + private LocalDateTime lastLogin; + private String token; + private String refreshToken; +} \ No newline at end of file diff --git a/gateway-service/src/main/java/com/posthub/gateway/repository/RefreshTokenRepository.java b/gateway-service/src/main/java/com/posthub/gateway/repository/RefreshTokenRepository.java new file mode 100644 index 0000000..4a9b32a --- /dev/null +++ b/gateway-service/src/main/java/com/posthub/gateway/repository/RefreshTokenRepository.java @@ -0,0 +1,12 @@ +package com.posthub.gateway.repository; + +import com.posthub.gateway.model.entity.RefreshToken; +import org.springframework.data.repository.reactive.ReactiveCrudRepository; +import reactor.core.publisher.Mono; + +public interface RefreshTokenRepository extends ReactiveCrudRepository { + + Mono findByToken(String token); + + Mono findByUserId(Integer userId); +} \ 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 new file mode 100644 index 0000000..871eda4 --- /dev/null +++ b/gateway-service/src/main/java/com/posthub/gateway/repository/UserRepository.java @@ -0,0 +1,14 @@ +package com.posthub.gateway.repository; + +import com.posthub.gateway.model.entity.User; +import org.springframework.data.repository.reactive.ReactiveCrudRepository; +import reactor.core.publisher.Mono; + +public interface UserRepository extends ReactiveCrudRepository { + + Mono findByEmail(String email); + + Mono findByUsername(String username); + + Mono existsByEmail(String email); +} \ No newline at end of file diff --git a/gateway-service/src/main/java/com/posthub/gateway/security/JwtTokenProvider.java b/gateway-service/src/main/java/com/posthub/gateway/security/JwtTokenProvider.java new file mode 100644 index 0000000..8c71a21 --- /dev/null +++ b/gateway-service/src/main/java/com/posthub/gateway/security/JwtTokenProvider.java @@ -0,0 +1,111 @@ +package com.posthub.gateway.security; + +import com.posthub.gateway.model.entity.User; +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Component +public class JwtTokenProvider { + + private static final String USER_ID = "userId"; + private static final String USERNAME = "username"; + private static final String USER_EMAIL = "email"; + private static final String USER_ROLE = "role"; + private static final String USER_REGISTRATION_STATUS = "registrationStatus"; + private static final String SESSION_ID = "sessionId"; + private static final String LAST_UPDATE = "lastUpdate"; + + private final SecretKey secretKey; + private final long jwtValidityInMilliseconds; + + public JwtTokenProvider(@Value("${jwt.secret}") String secret, + @Value("${jwt.expiration:103600000}") long jwtValidityInMilliseconds) { + byte[] decoded = Decoders.BASE64.decode(secret); + this.secretKey = Keys.hmacShaKeyFor(decoded); + this.jwtValidityInMilliseconds = jwtValidityInMilliseconds; + } + + public String generateToken(@NonNull User user, String sessionId) { + Map claims = new HashMap<>(); + claims.put(USER_ID, user.getId()); + claims.put(USERNAME, user.getUsername()); + claims.put(USER_EMAIL, user.getEmail()); + claims.put(USER_ROLE, user.getRole().name()); + claims.put(USER_REGISTRATION_STATUS, user.getRegistrationStatus().name()); + claims.put(SESSION_ID, sessionId); + claims.put(LAST_UPDATE, LocalDateTime.now().toString()); + return createToken(claims, user.getEmail()); + } + + public String refreshToken(String token) { + Claims claims = getAllClaimsFromToken(token); + return createToken(claims, claims.getSubject()); + } + + public boolean validateToken(String token) { + try { + Jws claims = Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(token); + return !claims.getPayload().getExpiration().before(new Date()); + } catch (JwtException | IllegalArgumentException e) { + log.warn("Invalid JWT token: {}", e.getMessage()); + return false; + } + } + + public String getUsername(String token) { + return getAllClaimsFromToken(token).get(USERNAME, String.class); + } + + public String getUserId(String token) { + return String.valueOf(getAllClaimsFromToken(token).get(USER_ID)); + } + + public String getUserEmail(String token) { + return getAllClaimsFromToken(token).get(USER_EMAIL, String.class); + } + + public String getUserRole(String token) { + return getAllClaimsFromToken(token).get(USER_ROLE, String.class); + } + + public String getSessionId(String token) { + return getAllClaimsFromToken(token).get(SESSION_ID, String.class); + } + + private Claims getAllClaimsFromToken(String token) { + try { + return Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(token) + .getPayload(); + } catch (ExpiredJwtException e) { + return e.getClaims(); + } + } + + private String createToken(Map claims, String subject) { + return Jwts.builder() + .claims(claims) + .subject(subject) + .issuedAt(new Date()) + .expiration(new Date(System.currentTimeMillis() + jwtValidityInMilliseconds)) + .signWith(secretKey) + .compact(); + } +} \ No newline at end of file diff --git a/gateway-service/src/main/java/com/posthub/gateway/service/AuthService.java b/gateway-service/src/main/java/com/posthub/gateway/service/AuthService.java new file mode 100644 index 0000000..f8fa9c2 --- /dev/null +++ b/gateway-service/src/main/java/com/posthub/gateway/service/AuthService.java @@ -0,0 +1,146 @@ +package com.posthub.gateway.service; + +import com.posthub.gateway.model.constants.ApiErrorMessage; +import com.posthub.gateway.model.entity.RefreshToken; +import com.posthub.gateway.model.entity.User; +import com.posthub.gateway.model.enums.RegistrationStatus; +import com.posthub.gateway.model.enums.UserRole; +import com.posthub.gateway.model.request.LoginRequest; +import com.posthub.gateway.model.request.RegistrationUserRequest; +import com.posthub.gateway.model.response.RagResponse; +import com.posthub.gateway.model.response.UserProfileDTO; +import com.posthub.gateway.repository.RefreshTokenRepository; +import com.posthub.gateway.repository.UserRepository; +import com.posthub.gateway.security.JwtTokenProvider; +import com.posthub.gateway.util.PasswordUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; +import reactor.core.publisher.Mono; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AuthService { + + private final UserRepository userRepository; + private final RefreshTokenRepository refreshTokenRepository; + private final JwtTokenProvider jwtTokenProvider; + private final PasswordEncoder passwordEncoder; + + public Mono> login(LoginRequest request) { + return userRepository.findByEmail(request.getEmail()) + .switchIfEmpty(Mono.error(new ResponseStatusException( + HttpStatus.UNAUTHORIZED, ApiErrorMessage.INVALID_USER_OR_PASSWORD.getMessage()))) + .flatMap(user -> { + if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { + return Mono.error(new ResponseStatusException( + HttpStatus.UNAUTHORIZED, ApiErrorMessage.INVALID_USER_OR_PASSWORD.getMessage())); + } + if (user.getRegistrationStatus() != RegistrationStatus.ACTIVE) { + return Mono.error(new ResponseStatusException( + HttpStatus.FORBIDDEN, ApiErrorMessage.ACCOUNT_NOT_ACTIVE.getMessage())); + } + user.setLastLogin(LocalDateTime.now()); + user.setUpdated(LocalDateTime.now()); + return userRepository.save(user); + }) + .flatMap(this::generateTokensAndBuildResponse); + } + + public Mono> register(RegistrationUserRequest request) { + if (!request.getPassword().equals(request.getConfirmPassword())) { + return Mono.error(new ResponseStatusException( + HttpStatus.BAD_REQUEST, ApiErrorMessage.MISMATCH_PASSWORDS.getMessage())); + } + if (PasswordUtils.isNotValidPassword(request.getPassword())) { + return Mono.error(new ResponseStatusException( + HttpStatus.BAD_REQUEST, ApiErrorMessage.INVALID_PASSWORD.getMessage())); + } + + return userRepository.findByUsername(request.getUsername()) + .flatMap(existing -> Mono.error(new ResponseStatusException( + HttpStatus.CONFLICT, ApiErrorMessage.USERNAME_ALREADY_EXISTS.getMessage(request.getUsername())))) + .switchIfEmpty(userRepository.existsByEmail(request.getEmail()) + .flatMap(exists -> { + if (exists) { + return Mono.error(new ResponseStatusException( + HttpStatus.CONFLICT, ApiErrorMessage.EMAIL_ALREADY_EXISTS.getMessage(request.getEmail()))); + } + User user = new User(); + user.setUsername(request.getUsername()); + user.setEmail(request.getEmail()); + user.setPassword(passwordEncoder.encode(request.getPassword())); + user.setRegistrationStatus(RegistrationStatus.ACTIVE); + user.setRole(UserRole.USER); + user.setCreated(LocalDateTime.now()); + user.setUpdated(LocalDateTime.now()); + user.setDeleted(false); + return userRepository.save(user); + })) + .flatMap(this::generateTokensAndBuildResponse); + } + + public Mono> refreshAccessToken(String refreshTokenValue) { + return refreshTokenRepository.findByToken(refreshTokenValue) + .switchIfEmpty(Mono.error(new ResponseStatusException( + HttpStatus.UNAUTHORIZED, ApiErrorMessage.NOT_FOUND_REFRESH_TOKEN.getMessage()))) + .flatMap(refreshToken -> { + refreshToken.setCreated(LocalDateTime.now()); + refreshToken.setToken(generateUuid()); + return refreshTokenRepository.save(refreshToken) + .flatMap(saved -> userRepository.findById(saved.getUserId()) + .flatMap(user -> { + String accessToken = jwtTokenProvider.generateToken(user, saved.getSessionId()); + UserProfileDTO dto = toUserProfileDto(user, accessToken, saved.getToken()); + return Mono.just(RagResponse.createSuccessfulWithNewToken(dto)); + })); + }); + } + + private Mono> generateTokensAndBuildResponse(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()); + UserProfileDTO dto = toUserProfileDto(user, accessToken, refreshToken.getToken()); + log.info("Auth success for user: {}", user.getEmail()); + return RagResponse.createSuccessfulWithNewToken(dto); + }); + } + + private UserProfileDTO toUserProfileDto(User user, String token, String refreshToken) { + return new UserProfileDTO( + user.getId(), + user.getUsername(), + user.getEmail(), + user.getRegistrationStatus(), + user.getLastLogin(), + token, + refreshToken + ); + } + + private String generateUuid() { + return UUID.randomUUID().toString().replace("-", ""); + } +} \ No newline at end of file diff --git a/gateway-service/src/main/java/com/posthub/gateway/util/PasswordUtils.java b/gateway-service/src/main/java/com/posthub/gateway/util/PasswordUtils.java new file mode 100644 index 0000000..a86d8a8 --- /dev/null +++ b/gateway-service/src/main/java/com/posthub/gateway/util/PasswordUtils.java @@ -0,0 +1,34 @@ +package com.posthub.gateway.util; + +import com.posthub.gateway.model.constants.ApiConstants; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class PasswordUtils { + + public static boolean isNotValidPassword(String password) { + if (password == null || password.isEmpty() || password.trim().isEmpty()) { + return true; + } + String trim = password.trim(); + if (trim.length() < ApiConstants.REQUIRED_MIN_PASSWORD_LENGTH) { + return true; + } + int charactersNumber = ApiConstants.REQUIRED_MIN_CHARACTERS_NUMBER_IN_PASSWORD; + int lettersUCaseNumber = ApiConstants.REQUIRED_MIN_LETTERS_NUMBER_EVERY_CASE_IN_PASSWORD; + int lettersLCaseNumber = ApiConstants.REQUIRED_MIN_LETTERS_NUMBER_EVERY_CASE_IN_PASSWORD; + int digitsNumber = ApiConstants.REQUIRED_MIN_DIGITS_NUMBER_IN_PASSWORD; + for (int i = 0; i < trim.length(); i++) { + String currentLetter = String.valueOf(trim.charAt(i)); + if (!ApiConstants.PASSWORD_ALL_CHARACTERS.contains(currentLetter)) { + return true; + } + charactersNumber -= ApiConstants.PASSWORD_CHARACTERS.contains(currentLetter) ? 1 : 0; + lettersUCaseNumber -= ApiConstants.PASSWORD_LETTERS_UPPER_CASE.contains(currentLetter) ? 1 : 0; + lettersLCaseNumber -= ApiConstants.PASSWORD_LETTERS_LOWER_CASE.contains(currentLetter) ? 1 : 0; + digitsNumber -= ApiConstants.PASSWORD_DIGITS.contains(currentLetter) ? 1 : 0; + } + return ((charactersNumber > 0) || (lettersUCaseNumber > 0) || (lettersLCaseNumber > 0) || (digitsNumber > 0)); + } +} \ 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 75b9c48..0ff4bd1 100644 --- a/gateway-service/src/main/resources/application.yml +++ b/gateway-service/src/main/resources/application.yml @@ -6,6 +6,16 @@ spring: application: name: gateway-service + # ---- R2DBC (reactive DB) ---- + r2dbc: + url: r2dbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:appdb} + username: ${DB_USERNAME:app} + password: ${DB_PASSWORD:} + pool: + initial-size: 2 + max-size: 10 + max-idle-time: 30m + cloud: consul: host: ${CONSUL_HOST:localhost} @@ -18,27 +28,21 @@ spring: prefer-ip-address: true instance-id: ${spring.application.name}:${random.value} - # Spring Cloud Gateway 2025.0 — new prefix: spring.cloud.gateway.server.webflux gateway: server: webflux: - # Trust Nginx reverse proxy for forwarded headers trusted-proxies: 127\.0\.0\.1|10\.0\.0\..*|172\.1[6-9]\..*|172\.2[0-9]\..*|172\.3[0-1]\..*|192\.168\..* - discovery: locator: enabled: true lower-case-service-id: true - httpclient: connect-timeout: 5000 response-timeout: 60s - default-filters: - DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_FIRST - routes: - # RAG Service - actuator (health, info) + # RAG Service - actuator - id: rag-service-actuator uri: lb://rag-service predicates: @@ -57,19 +61,29 @@ spring: - RewritePath=/api/rag(?/?.*), ${segment} - AddRequestHeader=X-Forwarded-Prefix, /api/rag - # Analytics Service (will be added later) - # - id: analytics-service-api - # uri: lb://analytics-service - # predicates: - # - Path=/api/analytics/** - # - Method=GET,POST - # filters: - # - RewritePath=/api/analytics(?/?.*), ${segment} +# ---- JWT ---- +jwt: + secret: ${JWT_SECRET:} + expiration: ${JWT_EXPIRATION:103600000} +# ---- Auth path config ---- +auth: + public-paths: + - /api/auth/login + - /api/auth/register + - /api/auth/refresh/token + - /actuator/** + - /api/*/v3/api-docs/** + - /api/*/swagger-ui/** + admin-paths: + - /api/*/admin/** + +# ---- CORS ---- gateway: cors: allowed-origins: ${CORS_ORIGINS:*} +# ---- Actuator ---- management: endpoints: web: @@ -81,8 +95,10 @@ management: gateway: enabled: true +# ---- Logging ---- logging: level: root: INFO com.posthub.gateway: DEBUG org.springframework.cloud.gateway: INFO + org.springframework.r2dbc: INFO \ No newline at end of file diff --git a/rag-service/pom.xml b/rag-service/pom.xml index 96d3c12..ceef7bc 100644 --- a/rag-service/pom.xml +++ b/rag-service/pom.xml @@ -128,23 +128,6 @@ spring-security-test test - - io.jsonwebtoken - jjwt-api - 0.11.5 - - - io.jsonwebtoken - jjwt-impl - 0.11.5 - runtime - - - io.jsonwebtoken - jjwt-jackson - 0.11.5 - runtime - org.springdoc springdoc-openapi-starter-webmvc-ui diff --git a/rag-service/src/main/java/com/balex/rag/config/SecurityConfig.java b/rag-service/src/main/java/com/balex/rag/config/SecurityConfig.java index c196351..1186a96 100644 --- a/rag-service/src/main/java/com/balex/rag/config/SecurityConfig.java +++ b/rag-service/src/main/java/com/balex/rag/config/SecurityConfig.java @@ -1,99 +1,26 @@ package com.balex.rag.config; -import com.balex.rag.security.filter.JwtRequestFilter; -import com.balex.rag.security.handler.AccessRestrictionHandler; -import jakarta.servlet.DispatcherType; -import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.dao.DaoAuthenticationProvider; -import org.springframework.security.config.Customizer; -import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; -import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.HttpStatusEntryPoint; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; - -import java.util.List; @Configuration -@EnableWebSecurity -@EnableMethodSecurity -@RequiredArgsConstructor public class SecurityConfig { - private final JwtRequestFilter jwtRequestFilter; - private final AccessRestrictionHandler accessRestrictionHandler; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - .cors(Customizer.withDefaults()) + return http .csrf(AbstractHttpConfigurer::disable) - .authorizeHttpRequests(auth -> auth - .dispatcherTypeMatchers(DispatcherType.ASYNC).permitAll() - .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() - .requestMatchers(HttpMethod.GET, "/auth/refresh/token").permitAll() - .requestMatchers(HttpMethod.POST, "/auth/login", "/auth/register").permitAll() - .requestMatchers( - "/v3/api-docs/**", - "/swagger-ui/**", - "/swagger-ui.html", - "/webjars/**", - "/actuator/**" - ).permitAll() - .anyRequest().authenticated() - ) - .exceptionHandling(exceptions -> exceptions - .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)) - .accessDeniedHandler(accessRestrictionHandler) - ) - .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); - - return http.build(); + .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) + .build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } - - @Bean - public DaoAuthenticationProvider daoAuthenticationProvider(UserDetailsService userDetailsService) { - DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(userDetailsService); - daoAuthenticationProvider.setPasswordEncoder(passwordEncoder()); - return daoAuthenticationProvider; - } - - @Bean - public AuthenticationManager authenticationManagerBean(AuthenticationConfiguration authenticationConfiguration) throws Exception { - return authenticationConfiguration.getAuthenticationManager(); - } - - @Bean - public CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration config = new CorsConfiguration(); - //config.setAllowedOrigins(List.of("http://localhost:5173")); - config.addAllowedOriginPattern("*"); - config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); - config.setAllowedHeaders(List.of("*")); - config.setAllowCredentials(true); - - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", config); - return source; - } - -} - +} \ No newline at end of file diff --git a/rag-service/src/main/java/com/balex/rag/controller/AuthController.java b/rag-service/src/main/java/com/balex/rag/controller/AuthController.java deleted file mode 100644 index 058a03f..0000000 --- a/rag-service/src/main/java/com/balex/rag/controller/AuthController.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.balex.rag.controller; - -import com.balex.rag.model.constants.ApiLogMessage; -import com.balex.rag.model.dto.UserProfileDTO; -import com.balex.rag.model.request.user.LoginRequest; -import com.balex.rag.model.request.user.RegistrationUserRequest; -import com.balex.rag.model.response.RagResponse; -import com.balex.rag.service.AuthService; -import com.balex.rag.service.EventPublisher; -import com.balex.rag.utils.ApiUtils; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.ExampleObject; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; - -@Slf4j -@RestController -@Validated -@RequiredArgsConstructor -@RequestMapping("${end.points.auth}") -public class AuthController { - private final AuthService authService; - private final EventPublisher eventPublisher; - - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Successful authorization", - content = @Content(mediaType = "application/json", - examples = @ExampleObject(value = "{ \"token\": \"eyJhbGcIoIJIuz...\" }"))) - }) - @PostMapping("${end.points.login}") - @Operation(summary = "User login", description = "Authenticates the user and returns an access/refresh token" - ) - public ResponseEntity login( - @RequestBody @Valid LoginRequest request, - HttpServletResponse response) { - log.trace(ApiLogMessage.NAME_OF_CURRENT_METHOD.getValue(), ApiUtils.getMethodName()); - - RagResponse result = authService.login(request); - Cookie authorizationCookie = ApiUtils.createAuthCookie(result.getPayload().getToken()); - response.addCookie(authorizationCookie); - - return ResponseEntity.ok(result); - } - - @GetMapping("${end.points.refresh.token}") - @Operation(summary = "Refresh access token", description = "Generates new access token using provided refresh token" - ) - public ResponseEntity> refreshToken( - @RequestParam(name = "token") String refreshToken, - HttpServletResponse response) { - log.trace(ApiLogMessage.NAME_OF_CURRENT_METHOD.getValue(), ApiUtils.getMethodName()); - - RagResponse result = authService.refreshAccessToken(refreshToken); - Cookie authorizationCookie = ApiUtils.createAuthCookie(result.getPayload().getToken()); - response.addCookie(authorizationCookie); - - return ResponseEntity.ok(result); - } - - @PostMapping("${end.points.register}") - @Operation(summary = "Register a new user", description = "Creates new user and returns authentication details" - ) - public ResponseEntity register( - @RequestBody @Valid RegistrationUserRequest request, - HttpServletResponse response) { - log.trace(ApiLogMessage.NAME_OF_CURRENT_METHOD.getValue(), ApiUtils.getMethodName()); - - RagResponse result = authService.registerUser(request); - Cookie authorizationCookie = ApiUtils.createAuthCookie(result.getPayload().getToken()); - response.addCookie(authorizationCookie); - - eventPublisher.publishUserCreated(result.getPayload().getId().toString()); - - return ResponseEntity.ok(result); - } - -} \ No newline at end of file diff --git a/rag-service/src/main/java/com/balex/rag/controller/ChatController.java b/rag-service/src/main/java/com/balex/rag/controller/ChatController.java index abf792c..56ef1aa 100644 --- a/rag-service/src/main/java/com/balex/rag/controller/ChatController.java +++ b/rag-service/src/main/java/com/balex/rag/controller/ChatController.java @@ -1,10 +1,10 @@ package com.balex.rag.controller; -import com.balex.rag.model.constants.ApiLogMessage; import com.balex.rag.model.entity.Chat; import com.balex.rag.service.ChatService; import com.balex.rag.service.EventPublisher; -import com.balex.rag.utils.ApiUtils; +import com.balex.rag.utils.UserContext; +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; @@ -22,25 +22,25 @@ public class ChatController { private final ChatService chatService; private final EventPublisher eventPublisher; - private final ApiUtils apiUtils; @GetMapping("") - public ResponseEntity> mainPage() { - Long ownerId = apiUtils.getUserIdFromAuthentication().longValue(); + public ResponseEntity> mainPage(HttpServletRequest request) { + Long ownerId = UserContext.getUserId(request).longValue(); List response = chatService.getAllChatsByOwner(ownerId); return ResponseEntity.ok(response); } @GetMapping("/{chatId}") - public ResponseEntity showChat(@PathVariable Long chatId) { - Long ownerId = apiUtils.getUserIdFromAuthentication().longValue(); + public ResponseEntity showChat(@PathVariable Long chatId, HttpServletRequest request) { + Long ownerId = UserContext.getUserId(request).longValue(); Chat response = chatService.getChat(chatId, ownerId); return ResponseEntity.ok(response); } @PostMapping("/new") - public ResponseEntity newChat(@RequestParam String title) { - Chat chat = chatService.createNewChat(title); + public ResponseEntity newChat(@RequestParam String title, HttpServletRequest request) { + Long ownerId = UserContext.getUserId(request).longValue(); + Chat chat = chatService.createNewChat(title, ownerId); eventPublisher.publishChatCreated( chat.getIdOwner().toString(), @@ -50,8 +50,8 @@ public class ChatController { } @DeleteMapping("/{chatId}") - public ResponseEntity deleteChat(@PathVariable Long chatId) { - Long ownerId = apiUtils.getUserIdFromAuthentication().longValue(); + public ResponseEntity deleteChat(@PathVariable Long chatId, HttpServletRequest request) { + Long ownerId = UserContext.getUserId(request).longValue(); Chat chat = chatService.getChat(chatId, ownerId); chatService.deleteChat(chatId, ownerId); diff --git a/rag-service/src/main/java/com/balex/rag/controller/ChatEntryController.java b/rag-service/src/main/java/com/balex/rag/controller/ChatEntryController.java index 82b9486..6ff1045 100644 --- a/rag-service/src/main/java/com/balex/rag/controller/ChatEntryController.java +++ b/rag-service/src/main/java/com/balex/rag/controller/ChatEntryController.java @@ -9,6 +9,8 @@ import com.balex.rag.service.ChatEntryService; import com.balex.rag.service.ChatService; import com.balex.rag.service.EventPublisher; import com.balex.rag.utils.ApiUtils; +import com.balex.rag.utils.UserContext; +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; @@ -26,15 +28,15 @@ public class ChatEntryController { private final ChatService chatService; private final RagDefaultsProperties ragDefaults; private final EventPublisher eventPublisher; - private final ApiUtils apiUtils; @PostMapping("/{chatId}") public ResponseEntity addUserEntry( @PathVariable Long chatId, - @RequestBody UserEntryRequest request) { + @RequestBody UserEntryRequest request, + HttpServletRequest httpRequest) { log.trace(ApiLogMessage.NAME_OF_CURRENT_METHOD.getValue(), ApiUtils.getMethodName()); - Long ownerId = apiUtils.getUserIdFromAuthentication().longValue(); + Long ownerId = UserContext.getUserId(httpRequest).longValue(); boolean onlyContext = request.onlyContext() != null ? request.onlyContext() : ragDefaults.onlyContext(); double topP = request.topP() != null ? request.topP() : ragDefaults.topP(); diff --git a/rag-service/src/main/java/com/balex/rag/controller/DocumentUploadStreamController.java b/rag-service/src/main/java/com/balex/rag/controller/DocumentUploadStreamController.java index 9f3f607..ae82b95 100644 --- a/rag-service/src/main/java/com/balex/rag/controller/DocumentUploadStreamController.java +++ b/rag-service/src/main/java/com/balex/rag/controller/DocumentUploadStreamController.java @@ -1,11 +1,12 @@ package com.balex.rag.controller; import com.balex.rag.service.UserDocumentService; -import com.balex.rag.utils.ApiUtils; +import com.balex.rag.utils.UserContext; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -28,7 +29,6 @@ import java.util.List; public class DocumentUploadStreamController { private final UserDocumentService userDocumentService; - private final ApiUtils apiUtils; @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Document processing progress stream", @@ -39,9 +39,9 @@ public class DocumentUploadStreamController { }) @PostMapping(value = "/upload-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter uploadDocumentsWithProgress( - @RequestPart("files") @Valid List files - ) { - Integer userId = apiUtils.getUserIdFromAuthentication(); + @RequestPart("files") @Valid List files, + HttpServletRequest request) { + Integer userId = UserContext.getUserId(request); return userDocumentService.processUploadedFilesWithSse(files, userId.longValue()); } } \ No newline at end of file diff --git a/rag-service/src/main/java/com/balex/rag/controller/UserController.java b/rag-service/src/main/java/com/balex/rag/controller/UserController.java index 1dba786..9b40e5e 100644 --- a/rag-service/src/main/java/com/balex/rag/controller/UserController.java +++ b/rag-service/src/main/java/com/balex/rag/controller/UserController.java @@ -11,6 +11,8 @@ import com.balex.rag.model.response.RagResponse; import com.balex.rag.service.EventPublisher; import com.balex.rag.service.UserService; import com.balex.rag.utils.ApiUtils; +import com.balex.rag.utils.UserContext; +import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -51,26 +53,25 @@ public class UserController { } @GetMapping("${end.points.userinfo}") - public ResponseEntity> getUserInfo( - @RequestHeader("Authorization") String token) { + public ResponseEntity> getUserInfo(HttpServletRequest request) { log.trace(ApiLogMessage.NAME_OF_CURRENT_METHOD.getValue(), ApiUtils.getMethodName()); - RagResponse userInfo = userService.getUserInfo(token); + Integer userId = UserContext.getUserId(request); + RagResponse userInfo = userService.getUserInfo(userId); return ResponseEntity.ok(userInfo); } @DeleteMapping("${end.points.userinfo}") - public ResponseEntity> deleteUserDocuments( - @RequestHeader("Authorization") String token) { + public ResponseEntity> deleteUserDocuments(HttpServletRequest request) { log.trace(ApiLogMessage.NAME_OF_CURRENT_METHOD.getValue(), ApiUtils.getMethodName()); - RagResponse deletedCount = userService.deleteUserDocuments(token); + Integer userId = UserContext.getUserId(request); + RagResponse deletedCount = userService.deleteUserDocuments(userId); return ResponseEntity.ok(deletedCount); } - @PutMapping("${end.points.id}") public ResponseEntity> updateUserById( @PathVariable(name = "id") Integer userId, @@ -80,14 +81,14 @@ public class UserController { RagResponse updatedPost = userService.updateUser(userId, request); return ResponseEntity.ok(updatedPost); } - @DeleteMapping("${end.points.id}") public ResponseEntity softDeleteUser( - @PathVariable(name = "id") Integer userId - ) { + @PathVariable(name = "id") Integer userId, + HttpServletRequest request) { log.trace(ApiLogMessage.NAME_OF_CURRENT_METHOD.getValue(), ApiUtils.getMethodName()); - userService.softDeleteUser(userId); + Integer currentUserId = UserContext.getUserId(request); + userService.softDeleteUser(userId, currentUserId); return ResponseEntity.ok().build(); } diff --git a/rag-service/src/main/java/com/balex/rag/filter/GatewayAuthFilter.java b/rag-service/src/main/java/com/balex/rag/filter/GatewayAuthFilter.java new file mode 100644 index 0000000..94e4ace --- /dev/null +++ b/rag-service/src/main/java/com/balex/rag/filter/GatewayAuthFilter.java @@ -0,0 +1,37 @@ +package com.balex.rag.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Slf4j +@Component +public class GatewayAuthFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + String userId = request.getHeader("X-User-Id"); + String email = request.getHeader("X-User-Email"); + String username = request.getHeader("X-User-Name"); + String role = request.getHeader("X-User-Role"); + + if (userId != null) { + request.setAttribute("userId", userId); + request.setAttribute("userEmail", email); + request.setAttribute("userName", username); + request.setAttribute("userRole", role); + log.debug("Gateway user: id={}, email={}, role={}", userId, email, role); + } + + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/rag-service/src/main/java/com/balex/rag/mapper/UserMapper.java b/rag-service/src/main/java/com/balex/rag/mapper/UserMapper.java index 83911ec..605dda5 100644 --- a/rag-service/src/main/java/com/balex/rag/mapper/UserMapper.java +++ b/rag-service/src/main/java/com/balex/rag/mapper/UserMapper.java @@ -1,14 +1,11 @@ package com.balex.rag.mapper; import com.balex.rag.model.dto.UserDTO; -import com.balex.rag.model.dto.UserProfileDTO; import com.balex.rag.model.dto.UserSearchDTO; import com.balex.rag.model.entity.User; import com.balex.rag.model.enums.RegistrationStatus; import com.balex.rag.model.request.user.NewUserRequest; -import com.balex.rag.model.request.user.RegistrationUserRequest; import com.balex.rag.model.request.user.UpdateUserRequest; -import org.hibernate.type.descriptor.DateTimeUtils; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.MappingTarget; @@ -19,7 +16,7 @@ import java.util.Objects; @Mapper( componentModel = "spring", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE, - imports = {RegistrationStatus.class, Objects.class, DateTimeUtils.class} + imports = {RegistrationStatus.class, Objects.class} ) public interface UserMapper { @@ -36,16 +33,4 @@ public interface UserMapper { @Mapping(source = "deleted", target = "isDeleted") UserSearchDTO toUserSearchDto(User user); - - @Mapping(target = "username", source = "user.username") - @Mapping(target = "email", source = "user.email") - @Mapping(target = "token", source = "token") - @Mapping(target = "refreshToken", source = "refreshToken") - UserProfileDTO toUserProfileDto(User user, String token, String refreshToken); - - @Mapping(target = "password", ignore = true) - @Mapping(target = "registrationStatus", expression = "java(RegistrationStatus.ACTIVE)") - User fromDto(RegistrationUserRequest request); - -} - +} \ No newline at end of file diff --git a/rag-service/src/main/java/com/balex/rag/model/constants/ApiConstants.java b/rag-service/src/main/java/com/balex/rag/model/constants/ApiConstants.java index 6cb3949..ce506cc 100644 --- a/rag-service/src/main/java/com/balex/rag/model/constants/ApiConstants.java +++ b/rag-service/src/main/java/com/balex/rag/model/constants/ApiConstants.java @@ -8,8 +8,6 @@ public final class ApiConstants { public static final String UNDEFINED = "undefined"; public static final String EMPTY_FILENAME = "unknown"; - public static final String NO_NEW_DOCUMENTS_UPLOADED = "No new documents uploaded"; - public static final String DOCUMENTS_UPLOADED = "Documents uploaded: "; public static final String ANSI_RED = "\u001B[31m"; public static final String ANSI_WHITE = "\u001B[37m"; public static final String BREAK_LINE = "\n"; diff --git a/rag-service/src/main/java/com/balex/rag/model/entity/RefreshToken.java b/rag-service/src/main/java/com/balex/rag/model/entity/RefreshToken.java deleted file mode 100644 index c15f9af..0000000 --- a/rag-service/src/main/java/com/balex/rag/model/entity/RefreshToken.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.balex.rag.model.entity; - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; - -import java.time.LocalDateTime; - -@Entity -@Getter -@Setter -@Table(name = "refresh_token") -public class RefreshToken { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Integer id; - - @Column(nullable = false, unique = true) - private String token; - - @Column(nullable = false) - private LocalDateTime created; - - @Column(name = "session_id") - private String sessionId; - - @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) - @JoinColumn(name = "user_id", nullable = false) - private User user; -} \ No newline at end of file diff --git a/rag-service/src/main/java/com/balex/rag/model/exception/InvalidTokenException.java b/rag-service/src/main/java/com/balex/rag/model/exception/InvalidTokenException.java deleted file mode 100644 index cf2b2d2..0000000 --- a/rag-service/src/main/java/com/balex/rag/model/exception/InvalidTokenException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.balex.rag.model.exception; - -public class InvalidTokenException extends RuntimeException { - - public InvalidTokenException(String message) { - super(message); - } - -} diff --git a/rag-service/src/main/java/com/balex/rag/model/request/user/LoginRequest.java b/rag-service/src/main/java/com/balex/rag/model/request/user/LoginRequest.java deleted file mode 100644 index 8e0bbf4..0000000 --- a/rag-service/src/main/java/com/balex/rag/model/request/user/LoginRequest.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.balex.rag.model.request.user; - -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.io.Serializable; - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class LoginRequest implements Serializable { - - @Email - @NotNull - private String email; - - @NotEmpty - private String password; - -} - diff --git a/rag-service/src/main/java/com/balex/rag/model/request/user/RegistrationUserRequest.java b/rag-service/src/main/java/com/balex/rag/model/request/user/RegistrationUserRequest.java deleted file mode 100644 index 4a5a031..0000000 --- a/rag-service/src/main/java/com/balex/rag/model/request/user/RegistrationUserRequest.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.balex.rag.model.request.user; - -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.io.Serializable; - -@Data -@AllArgsConstructor -@NoArgsConstructor -public class RegistrationUserRequest implements Serializable { - - @NotBlank - private String username; - - @Email - @NotNull - private String email; - - @NotEmpty - private String password; - - @NotEmpty - private String confirmPassword; - -} diff --git a/rag-service/src/main/java/com/balex/rag/repo/RefreshTokenRepository.java b/rag-service/src/main/java/com/balex/rag/repo/RefreshTokenRepository.java deleted file mode 100644 index f90d1f1..0000000 --- a/rag-service/src/main/java/com/balex/rag/repo/RefreshTokenRepository.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.balex.rag.repo; - -import com.balex.rag.model.entity.RefreshToken; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; - -public interface RefreshTokenRepository extends JpaRepository { - - Optional findByToken(String token); - - Optional findByUserId(Integer userId); - - Optional findByUserEmail(String email); -} \ No newline at end of file diff --git a/rag-service/src/main/java/com/balex/rag/security/JwtTokenProvider.java b/rag-service/src/main/java/com/balex/rag/security/JwtTokenProvider.java deleted file mode 100644 index ffb0129..0000000 --- a/rag-service/src/main/java/com/balex/rag/security/JwtTokenProvider.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.balex.rag.security; - -import com.balex.rag.model.entity.User; -import com.balex.rag.service.model.AuthenticationConstants; -import io.jsonwebtoken.*; -import io.jsonwebtoken.io.Decoders; -import io.jsonwebtoken.security.Keys; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.lang.NonNull; -import org.springframework.stereotype.Component; - -import javax.crypto.SecretKey; -import java.time.LocalDateTime; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; - -@Slf4j -@Component -public class JwtTokenProvider { - private final SecretKey secretKey; - private final Long jwtValidityInMilliseconds; - - public JwtTokenProvider(@Value("${jwt.secret}") String secret, - @Value("${jwt.expiration:3600000}") long jwtValidityInMilliseconds) { - this.secretKey = getKey(secret); - this.jwtValidityInMilliseconds = jwtValidityInMilliseconds; - } - - public String generateToken(@NonNull User user, String sessionId) { - Map claims = new HashMap<>(); - claims.put(AuthenticationConstants.USER_ID, user.getId()); - claims.put(AuthenticationConstants.USERNAME, user.getUsername()); - claims.put(AuthenticationConstants.USER_EMAIL, user.getEmail()); - claims.put(AuthenticationConstants.USER_REGISTRATION_STATUS, user.getRegistrationStatus().name()); - claims.put(AuthenticationConstants.SESSION_ID, sessionId); - claims.put(AuthenticationConstants.LAST_UPDATE, LocalDateTime.now().toString()); - - return createToken(claims, user.getEmail()); - } - - public String refreshToken(String token) { - Claims claims = getAllClaimsFromToken(token); - return createToken(claims, claims.getSubject()); - } - - public boolean validateToken(String token) { - try { - Jws claims = Jwts.parserBuilder() - .setSigningKey(secretKey) - .build() - .parseClaimsJws(token); - return !claims.getBody().getExpiration().before(new Date()); - } catch (JwtException | IllegalArgumentException e) { - return false; - } - } - - public String getUsername(String token) { - Claims claims = getAllClaimsFromToken(token); - return claims.get(AuthenticationConstants.USERNAME).toString(); - } - - public String getUserId(String token) { - Claims claims = getAllClaimsFromToken(token); - return String.valueOf(claims.get(AuthenticationConstants.USER_ID)); - } - - - private Claims getAllClaimsFromToken(String token) { - try { - return Jwts.parserBuilder() - .setSigningKey(secretKey) - .build() - .parseClaimsJws(token) - .getBody(); - } catch (ExpiredJwtException e) { - return e.getClaims(); - } - } - - private SecretKey getKey(String secretKey64) { - byte[] decode64 = Decoders.BASE64.decode(secretKey64); - return Keys.hmacShaKeyFor(decode64); - } - - private String createToken(Map claims, String subject) { - return Jwts.builder() - .setClaims(claims) - .setSubject(subject) - .setIssuedAt(new Date()) - .setExpiration(new Date(System.currentTimeMillis() + jwtValidityInMilliseconds)) - .signWith(secretKey, SignatureAlgorithm.HS512) - .compact(); - } - - public String getSessionId(String token) { - Claims claims = getAllClaimsFromToken(token); - return claims.get(AuthenticationConstants.SESSION_ID, String.class); - } - -} - diff --git a/rag-service/src/main/java/com/balex/rag/security/filter/JwtRequestFilter.java b/rag-service/src/main/java/com/balex/rag/security/filter/JwtRequestFilter.java deleted file mode 100644 index a1b57ed..0000000 --- a/rag-service/src/main/java/com/balex/rag/security/filter/JwtRequestFilter.java +++ /dev/null @@ -1,130 +0,0 @@ -package com.balex.rag.security.filter; - -import com.balex.rag.model.constants.ApiErrorMessage; -import com.balex.rag.security.JwtTokenProvider; -import com.balex.rag.service.impl.RefreshTokenServiceImpl; -import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.MalformedJwtException; -import io.jsonwebtoken.SignatureException; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.validation.constraints.NotNull; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -import static com.balex.rag.model.constants.ApiConstants.USER_ROLE; - -@Slf4j -@Component -@RequiredArgsConstructor -public class JwtRequestFilter extends OncePerRequestFilter { - private static final String AUTHORIZATION_HEADER = "Authorization"; - private static final String BEARER_PREFIX = "Bearer "; - private static final String LOGIN_PATH = "/auth/login"; - private static final String REGISTER_PATH = "/auth/register"; - - private final JwtTokenProvider jwtTokenProvider; - private final RefreshTokenServiceImpl refreshTokenService; - - @Override - protected void doFilterInternal( - @NotNull HttpServletRequest request, - @NotNull HttpServletResponse response, - @NotNull FilterChain filterChain) - throws ServletException, IOException { - - log.info("JWT filter: method={}, uri={}, header={}", - request.getMethod(), - request.getRequestURI(), - request.getHeader(AUTHORIZATION_HEADER)); - - Optional authHeader = Optional.ofNullable(request.getHeader(AUTHORIZATION_HEADER)); - String requestURI = request.getRequestURI(); - - if (authHeader.isPresent() && authHeader.get().startsWith(BEARER_PREFIX)) { - String jwt = authHeader.get().substring(BEARER_PREFIX.length()); - try { - if (!jwtTokenProvider.validateToken(jwt)) { - throw new ExpiredJwtException(null, null, ApiErrorMessage.TOKEN_EXPIRED.getMessage()); - } - - Optional emailOpt = Optional.ofNullable(jwtTokenProvider.getUsername(jwt)); - Optional userIdOpt = Optional.ofNullable(jwtTokenProvider.getUserId(jwt)); - String sessionId = jwtTokenProvider.getSessionId(jwt); - - if (emailOpt.isPresent() && userIdOpt.isPresent()) { - // Check session validity - Optional activeSessionId = refreshTokenService.getSessionIdByEmail(emailOpt.get()); -// if (activeSessionId.isEmpty() || !activeSessionId.get().equals(sessionId)) { -// sendErrorResponse(response, HttpStatus.UNAUTHORIZED, "SESSION_INVALIDATED"); -// return; -// } - - if (SecurityContextHolder.getContext().getAuthentication() == null) { - List authorities = Collections.singletonList(new SimpleGrantedAuthority(USER_ROLE)); - - UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( - emailOpt.get(), - jwt, - authorities - ); - SecurityContextHolder.getContext().setAuthentication(authenticationToken); - } - } - - } catch (ExpiredJwtException e) { - handleTokenExpiration(requestURI, jwt, response); - return; - } catch (SignatureException | MalformedJwtException e) { - handleSignatureException(response); - return; - } catch (Exception e) { - handleUnexpectedException(response, e); - return; - } - } - filterChain.doFilter(request, response); - } - - private void handleTokenExpiration(String requestURI, String jwt, HttpServletResponse response) throws IOException { - if (isAuthEndpoint(requestURI)) { - String refreshedToken = jwtTokenProvider.refreshToken(jwt); - response.setHeader(AUTHORIZATION_HEADER, BEARER_PREFIX + refreshedToken); - } else { - sendErrorResponse(response, HttpStatus.UNAUTHORIZED, ApiErrorMessage.TOKEN_EXPIRED.getMessage()); - } - } - - private void handleSignatureException(HttpServletResponse response) throws IOException { - sendErrorResponse(response, HttpStatus.UNAUTHORIZED, ApiErrorMessage.INVALID_TOKEN_SIGNATURE.getMessage()); - } - - private void handleUnexpectedException(HttpServletResponse response, Exception e) throws IOException { - log.error(ApiErrorMessage.ERROR_DURING_JWT_PROCESSING.getMessage(), e); - sendErrorResponse(response, HttpStatus.INTERNAL_SERVER_ERROR, ApiErrorMessage.UNEXPECTED_ERROR_OCCURRED.getMessage()); - } - - private void sendErrorResponse(HttpServletResponse response, HttpStatus status, String message) throws IOException { - response.setStatus(status.value()); - response.getWriter().write(message); - } - - private boolean isAuthEndpoint(String uri) { - return uri.equals(LOGIN_PATH) || uri.equals(REGISTER_PATH); - } -} - - diff --git a/rag-service/src/main/java/com/balex/rag/security/handler/AccessRestrictionHandler.java b/rag-service/src/main/java/com/balex/rag/security/handler/AccessRestrictionHandler.java deleted file mode 100644 index 4bfeb02..0000000 --- a/rag-service/src/main/java/com/balex/rag/security/handler/AccessRestrictionHandler.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.balex.rag.security.handler; - -import com.balex.rag.model.constants.ApiErrorMessage; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.SneakyThrows; -import org.springframework.http.HttpStatus; -import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.web.access.AccessDeniedHandler; -import org.springframework.stereotype.Component; - -@Component -public class AccessRestrictionHandler implements AccessDeniedHandler { - - @Override - @SneakyThrows - public void handle(HttpServletRequest request, - HttpServletResponse response, - AccessDeniedException accessDeniedException) { - response.setStatus(HttpStatus.FORBIDDEN.value()); - response.getWriter().write(ApiErrorMessage.HAVE_NO_ACCESS.getMessage()); - } - -} - diff --git a/rag-service/src/main/java/com/balex/rag/security/validation/AccessValidator.java b/rag-service/src/main/java/com/balex/rag/security/validation/AccessValidator.java index 90b692d..c035964 100644 --- a/rag-service/src/main/java/com/balex/rag/security/validation/AccessValidator.java +++ b/rag-service/src/main/java/com/balex/rag/security/validation/AccessValidator.java @@ -1,14 +1,7 @@ package com.balex.rag.security.validation; import com.balex.rag.model.constants.ApiErrorMessage; -import com.balex.rag.model.entity.User; -import com.balex.rag.model.exception.InvalidDataException; -import com.balex.rag.model.exception.InvalidPasswordException; -import com.balex.rag.model.exception.NotFoundException; import com.balex.rag.repo.UserRepository; -import com.balex.rag.service.model.exception.DataExistException; -import com.balex.rag.utils.ApiUtils; -import com.balex.rag.utils.PasswordUtils; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import org.springframework.stereotype.Component; @@ -19,34 +12,11 @@ import java.nio.file.AccessDeniedException; @RequiredArgsConstructor public class AccessValidator { private final UserRepository userRepository; - private final ApiUtils apiUtils; - - public void validateNewUser(String username, String email, String password, String confirmPassword) { - userRepository.findByUsername(username).ifPresent(existingUser -> { - throw new DataExistException(ApiErrorMessage.USERNAME_ALREADY_EXISTS.getMessage(username)); - }); - - userRepository.findByEmail(email).ifPresent(existingUser -> { - throw new DataExistException(ApiErrorMessage.EMAIL_ALREADY_EXISTS.getMessage(email)); - }); - - if (!password.equals(confirmPassword)) { - throw new InvalidDataException(ApiErrorMessage.MISMATCH_PASSWORDS.getMessage()); - } - - if (PasswordUtils.isNotValidPassword(password)) { - throw new InvalidPasswordException(ApiErrorMessage.INVALID_PASSWORD.getMessage()); - } - } @SneakyThrows - public void validateOwnerAccess(Integer ownerId) { - Integer currentUserId = apiUtils.getUserIdFromAuthentication(); - + public void validateOwnerAccess(Integer ownerId, Integer currentUserId) { if (!currentUserId.equals(ownerId)) { throw new AccessDeniedException(ApiErrorMessage.HAVE_NO_ACCESS.getMessage()); } } - -} - +} \ No newline at end of file diff --git a/rag-service/src/main/java/com/balex/rag/security/validation/PasswordMatchesValidator.java b/rag-service/src/main/java/com/balex/rag/security/validation/PasswordMatchesValidator.java deleted file mode 100644 index 7b58c2c..0000000 --- a/rag-service/src/main/java/com/balex/rag/security/validation/PasswordMatchesValidator.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.balex.rag.security.validation; - -import com.balex.rag.model.request.user.RegistrationUserRequest; -import com.balex.rag.utils.PasswordMatches; -import jakarta.validation.ConstraintValidator; -import jakarta.validation.ConstraintValidatorContext; - -public class PasswordMatchesValidator implements ConstraintValidator { - - @Override - public boolean isValid(RegistrationUserRequest request, ConstraintValidatorContext constraintValidatorContext) { - return request.getPassword().equals(request.getConfirmPassword()); - } - -} - diff --git a/rag-service/src/main/java/com/balex/rag/service/AuthService.java b/rag-service/src/main/java/com/balex/rag/service/AuthService.java deleted file mode 100644 index bb88578..0000000 --- a/rag-service/src/main/java/com/balex/rag/service/AuthService.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.balex.rag.service; - -import com.balex.rag.model.dto.UserProfileDTO; -import com.balex.rag.model.request.user.LoginRequest; -import com.balex.rag.model.request.user.RegistrationUserRequest; -import com.balex.rag.model.response.RagResponse; - -public interface AuthService { - - RagResponse login(LoginRequest request); - - RagResponse refreshAccessToken(String refreshToken); - - RagResponse registerUser(RegistrationUserRequest request); - -} diff --git a/rag-service/src/main/java/com/balex/rag/service/ChatService.java b/rag-service/src/main/java/com/balex/rag/service/ChatService.java index b4473c8..e07fc5f 100644 --- a/rag-service/src/main/java/com/balex/rag/service/ChatService.java +++ b/rag-service/src/main/java/com/balex/rag/service/ChatService.java @@ -7,7 +7,7 @@ import java.util.List; public interface ChatService { - Chat createNewChat(String title); + Chat createNewChat(String title, Long ownerId); List getAllChatsByOwner(Long ownerId); diff --git a/rag-service/src/main/java/com/balex/rag/service/RefreshTokenService.java b/rag-service/src/main/java/com/balex/rag/service/RefreshTokenService.java deleted file mode 100644 index 1e7b71c..0000000 --- a/rag-service/src/main/java/com/balex/rag/service/RefreshTokenService.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.balex.rag.service; - -import com.balex.rag.model.entity.RefreshToken; -import com.balex.rag.model.entity.User; - -public interface RefreshTokenService { - - RefreshToken generateOrUpdateRefreshToken(User user); - - RefreshToken validateAndRefreshToken(String refreshToken); - -} diff --git a/rag-service/src/main/java/com/balex/rag/service/UserService.java b/rag-service/src/main/java/com/balex/rag/service/UserService.java index cd2ab88..87a8db1 100644 --- a/rag-service/src/main/java/com/balex/rag/service/UserService.java +++ b/rag-service/src/main/java/com/balex/rag/service/UserService.java @@ -9,9 +9,8 @@ import com.balex.rag.model.response.PaginationResponse; import com.balex.rag.model.response.RagResponse; import jakarta.validation.constraints.NotNull; import org.springframework.data.domain.Pageable; -import org.springframework.security.core.userdetails.UserDetailsService; -public interface UserService extends UserDetailsService { +public interface UserService { RagResponse getById(@NotNull Integer userId); @@ -19,15 +18,11 @@ public interface UserService extends UserDetailsService { RagResponse updateUser(@NotNull Integer postId, @NotNull UpdateUserRequest request); - void softDeleteUser(Integer userId); + void softDeleteUser(Integer userId, Integer currentUserId); RagResponse> findAllUsers(Pageable pageable); - RagResponse getUserInfo(String token); - - RagResponse deleteUserDocuments(String token); - -} - - + RagResponse getUserInfo(Integer userId); + RagResponse deleteUserDocuments(Integer userId); +} \ No newline at end of file diff --git a/rag-service/src/main/java/com/balex/rag/service/impl/AuthServiceImpl.java b/rag-service/src/main/java/com/balex/rag/service/impl/AuthServiceImpl.java deleted file mode 100644 index cc04ba9..0000000 --- a/rag-service/src/main/java/com/balex/rag/service/impl/AuthServiceImpl.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.balex.rag.service.impl; - -import com.balex.rag.mapper.UserMapper; -import com.balex.rag.model.constants.ApiErrorMessage; -import com.balex.rag.model.dto.UserProfileDTO; -import com.balex.rag.model.entity.RefreshToken; -import com.balex.rag.model.entity.User; -import com.balex.rag.model.exception.InvalidDataException; -import com.balex.rag.model.request.user.LoginRequest; -import com.balex.rag.model.request.user.RegistrationUserRequest; -import com.balex.rag.model.response.RagResponse; -import com.balex.rag.repo.UserRepository; -import com.balex.rag.security.JwtTokenProvider; -import com.balex.rag.security.validation.AccessValidator; -import com.balex.rag.service.AuthService; -import com.balex.rag.service.RefreshTokenService; -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import java.util.HashSet; -import java.util.Set; - -@Slf4j -@Service -@AllArgsConstructor -public class AuthServiceImpl implements AuthService { - private final UserRepository userRepository; - private final UserMapper userMapper; - private final JwtTokenProvider jwtTokenProvider; - private final AuthenticationManager authenticationManager; - private final RefreshTokenService refreshTokenService; - private final PasswordEncoder passwordEncoder; - private final AccessValidator accessValidator; - - - @Override - public RagResponse login(@NotNull LoginRequest request) { - try { - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword()) - ); - } catch (BadCredentialsException e) { - throw new InvalidDataException(ApiErrorMessage.INVALID_USER_OR_PASSWORD.getMessage()); - } - - User user = userRepository.findUserByEmailAndDeletedFalse(request.getEmail()) - .orElseThrow(() -> new InvalidDataException(ApiErrorMessage.INVALID_USER_OR_PASSWORD.getMessage())); - - RefreshToken refreshToken = refreshTokenService.generateOrUpdateRefreshToken(user); - String token = jwtTokenProvider.generateToken(user, refreshToken.getSessionId()); - UserProfileDTO userProfileDTO = userMapper.toUserProfileDto(user, token, refreshToken.getToken()); - userProfileDTO.setToken(token); - - return RagResponse.createSuccessfulWithNewToken(userProfileDTO); - } - - @Override - public RagResponse refreshAccessToken(String refreshTokenValue) { - RefreshToken refreshToken = refreshTokenService.validateAndRefreshToken(refreshTokenValue); - User user = refreshToken.getUser(); - - String accessToken = jwtTokenProvider.generateToken(user, refreshToken.getSessionId()); - - return RagResponse.createSuccessfulWithNewToken( - userMapper.toUserProfileDto(user, accessToken, refreshToken.getToken()) - ); - } - - @Override - public RagResponse registerUser(@NotNull RegistrationUserRequest request) { - accessValidator.validateNewUser( - request.getUsername(), - request.getEmail(), - request.getPassword(), - request.getConfirmPassword() - ); - - User newUser = userMapper.fromDto(request); - newUser.setPassword(passwordEncoder.encode(request.getPassword())); - userRepository.save(newUser); - - RefreshToken refreshToken = refreshTokenService.generateOrUpdateRefreshToken(newUser); - String token = jwtTokenProvider.generateToken(newUser, refreshToken.getSessionId()); - UserProfileDTO userProfileDTO = userMapper.toUserProfileDto(newUser, token, refreshToken.getToken()); - userProfileDTO.setToken(token); - - return RagResponse.createSuccessfulWithNewToken(userProfileDTO); - } - -} - diff --git a/rag-service/src/main/java/com/balex/rag/service/impl/ChatServiceImpl.java b/rag-service/src/main/java/com/balex/rag/service/impl/ChatServiceImpl.java index 92941cf..cde046f 100644 --- a/rag-service/src/main/java/com/balex/rag/service/impl/ChatServiceImpl.java +++ b/rag-service/src/main/java/com/balex/rag/service/impl/ChatServiceImpl.java @@ -3,13 +3,11 @@ package com.balex.rag.service.impl; import com.balex.rag.model.entity.Chat; import com.balex.rag.repo.ChatRepository; import com.balex.rag.service.ChatService; -import com.balex.rag.utils.ApiUtils; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.data.domain.Sort; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.web.server.ResponseStatusException; @@ -23,25 +21,19 @@ public class ChatServiceImpl implements ChatService { private final ChatRepository chatRepo; private final ChatClient chatClient; - private final ApiUtils apiUtils; public List getAllChatsByOwner(Long ownerId) { return chatRepo.findByIdOwnerOrderByCreatedAtDesc(ownerId); } - public Chat createNewChat(String title) { - Long ownerId = apiUtils.getUserIdFromAuthentication().longValue(); - Chat chat = Chat.builder() - .title(title) - .idOwner(ownerId) - .build(); + public Chat createNewChat(String title, Long ownerId) { + Chat chat = Chat.builder().title(title).idOwner(ownerId).build(); chatRepo.save(chat); return chat; } public Chat getChat(Long chatId, Long ownerId) { - Chat chat = chatRepo.findById(chatId).orElseThrow( - () -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Chat not found")); + Chat chat = chatRepo.findById(chatId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Chat not found")); if (!chat.getIdOwner().equals(ownerId)) { throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Access denied"); } @@ -57,15 +49,7 @@ public class ChatServiceImpl implements ChatService { SseEmitter sseEmitter = new SseEmitter(0L); final StringBuilder answer = new StringBuilder(); - chatClient - .prompt(userPrompt) - .advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, chatId)) - .stream() - .chatResponse() - .subscribe( - (ChatResponse response) -> processToken(response, sseEmitter, answer), - sseEmitter::completeWithError, - sseEmitter::complete); + chatClient.prompt(userPrompt).advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, chatId)).stream().chatResponse().subscribe((ChatResponse response) -> processToken(response, sseEmitter, answer), sseEmitter::completeWithError, sseEmitter::complete); return sseEmitter; } diff --git a/rag-service/src/main/java/com/balex/rag/service/impl/RefreshTokenServiceImpl.java b/rag-service/src/main/java/com/balex/rag/service/impl/RefreshTokenServiceImpl.java deleted file mode 100644 index f5fb107..0000000 --- a/rag-service/src/main/java/com/balex/rag/service/impl/RefreshTokenServiceImpl.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.balex.rag.service.impl; - -import com.balex.rag.model.constants.ApiErrorMessage; -import com.balex.rag.model.entity.RefreshToken; -import com.balex.rag.model.entity.User; -import com.balex.rag.model.exception.NotFoundException; -import com.balex.rag.repo.RefreshTokenRepository; -import com.balex.rag.service.RefreshTokenService; -import com.balex.rag.utils.ApiUtils; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import java.time.LocalDateTime; -import java.util.Optional; - -@Slf4j -@Service -@RequiredArgsConstructor -public class RefreshTokenServiceImpl implements RefreshTokenService { - private final RefreshTokenRepository refreshTokenRepository; - - @Override - public RefreshToken generateOrUpdateRefreshToken(User user) { - String sessionId = ApiUtils.generateUuidWithoutDash(); - return refreshTokenRepository.findByUserId(user.getId()) - .map(refreshToken -> { - refreshToken.setCreated(LocalDateTime.now()); - refreshToken.setToken(ApiUtils.generateUuidWithoutDash()); - refreshToken.setSessionId(sessionId); - return refreshTokenRepository.save(refreshToken); - }) - .orElseGet(() -> { - RefreshToken newToken = new RefreshToken(); - newToken.setUser(user); - newToken.setCreated(LocalDateTime.now()); - newToken.setToken(ApiUtils.generateUuidWithoutDash()); - newToken.setSessionId(sessionId); - return refreshTokenRepository.save(newToken); - }); - } - - @Override - public RefreshToken validateAndRefreshToken(String requestRefreshToken) { - RefreshToken refreshToken = refreshTokenRepository.findByToken(requestRefreshToken) - .orElseThrow(() -> new NotFoundException(ApiErrorMessage.NOT_FOUND_REFRESH_TOKEN.getMessage())); - - refreshToken.setCreated(LocalDateTime.now()); - refreshToken.setToken(ApiUtils.generateUuidWithoutDash()); - return refreshTokenRepository.save(refreshToken); - } - - public Optional getSessionIdByEmail(String email) { - return refreshTokenRepository.findByUserEmail(email) - .map(RefreshToken::getSessionId); - } -} \ No newline at end of file diff --git a/rag-service/src/main/java/com/balex/rag/service/impl/UserServiceImpl.java b/rag-service/src/main/java/com/balex/rag/service/impl/UserServiceImpl.java index 24aed75..cba4559 100644 --- a/rag-service/src/main/java/com/balex/rag/service/impl/UserServiceImpl.java +++ b/rag-service/src/main/java/com/balex/rag/service/impl/UserServiceImpl.java @@ -8,7 +8,6 @@ import com.balex.rag.model.dto.UserSearchDTO; import com.balex.rag.model.entity.LoadedDocumentInfo; import com.balex.rag.model.entity.User; import com.balex.rag.model.entity.UserInfo; -import com.balex.rag.model.exception.InvalidTokenException; import com.balex.rag.model.exception.NotFoundException; import com.balex.rag.model.request.user.NewUserRequest; import com.balex.rag.model.request.user.UpdateUserRequest; @@ -17,7 +16,6 @@ import com.balex.rag.model.response.RagResponse; import com.balex.rag.repo.DocumentRepository; import com.balex.rag.repo.UserRepository; import com.balex.rag.repo.VectorStoreRepository; -import com.balex.rag.security.JwtTokenProvider; import com.balex.rag.security.validation.AccessValidator; import com.balex.rag.service.UserService; import com.balex.rag.service.model.exception.DataExistException; @@ -25,19 +23,12 @@ import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDateTime; -import java.util.Collections; import java.util.List; -import static com.balex.rag.model.constants.ApiConstants.USER_ROLE; - @Service @RequiredArgsConstructor public class UserServiceImpl implements UserService { @@ -45,7 +36,6 @@ public class UserServiceImpl implements UserService { private final UserMapper userMapper; private final PasswordEncoder passwordEncoder; private final AccessValidator accessValidator; - private final JwtTokenProvider jwtTokenProvider; private final DocumentRepository documentRepository; private final VectorStoreRepository vectorStoreRepository; @@ -98,15 +88,14 @@ public class UserServiceImpl implements UserService { @Override @Transactional - public void softDeleteUser(Integer userId) { + public void softDeleteUser(Integer userId, Integer currentUserId) { User user = userRepository.findByIdAndDeletedFalse(userId) .orElseThrow(() -> new NotFoundException(ApiErrorMessage.USER_NOT_FOUND_BY_ID.getMessage(userId))); - accessValidator.validateOwnerAccess(userId); + accessValidator.validateOwnerAccess(userId, currentUserId); user.setDeleted(true); userRepository.save(user); - } @Override @@ -128,28 +117,11 @@ public class UserServiceImpl implements UserService { return RagResponse.createSuccessful(paginationResponse); } - @Override - public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { - return getUserDetails(email, userRepository); - } - - static UserDetails getUserDetails(String email, UserRepository userRepository) { - User user = userRepository.findByEmail(email) - .orElseThrow(() -> new NotFoundException(ApiErrorMessage.EMAIL_NOT_FOUND.getMessage(email))); - - user.setLastLogin(LocalDateTime.now()); - userRepository.save(user); - return new org.springframework.security.core.userdetails.User( - user.getEmail(), - user.getPassword(), - Collections.singletonList(new SimpleGrantedAuthority(USER_ROLE)) - ); - } - @Override @Transactional(readOnly = true) - public RagResponse getUserInfo(String token) { - User user = getUserInfoFromToken(token); + public RagResponse getUserInfo(Integer userId) { + User user = userRepository.findByIdAndDeletedFalse(userId) + .orElseThrow(() -> new NotFoundException(ApiErrorMessage.USER_NOT_FOUND_BY_ID.getMessage(userId))); List loadedFiles = documentRepository .findByUserId(user.getId()) @@ -167,9 +139,9 @@ public class UserServiceImpl implements UserService { @Override @Transactional - public RagResponse deleteUserDocuments(String token) { - getUserInfoFromToken(token); - User user = getUserInfoFromToken(token); + public RagResponse deleteUserDocuments(Integer userId) { + User user = userRepository.findByIdAndDeletedFalse(userId) + .orElseThrow(() -> new NotFoundException(ApiErrorMessage.USER_NOT_FOUND_BY_ID.getMessage(userId))); List documents = documentRepository.findByUserId(user.getId()); @@ -177,28 +149,9 @@ public class UserServiceImpl implements UserService { return RagResponse.createSuccessful(0); } - // Удаляем чанки по user_id vectorStoreRepository.deleteByUserId(user.getId().longValue()); - - // Удаляем записи из loaded_document documentRepository.deleteAll(documents); return RagResponse.createSuccessful(documents.size()); } - - private User getUserInfoFromToken(String token) { - if (token == null || token.isBlank()) { - throw new InvalidTokenException("Token is empty or null"); - } - - String cleanToken = token.startsWith("Bearer ") - ? token.substring(7) - : token; - - String username = jwtTokenProvider.getUsername(cleanToken); - - return userRepository.findByUsername(username) - .orElseThrow(() -> new InvalidTokenException("User not found: " + username)); - } -} - +} \ No newline at end of file diff --git a/rag-service/src/main/java/com/balex/rag/utils/ApiUtils.java b/rag-service/src/main/java/com/balex/rag/utils/ApiUtils.java index a6ef40b..a87affb 100644 --- a/rag-service/src/main/java/com/balex/rag/utils/ApiUtils.java +++ b/rag-service/src/main/java/com/balex/rag/utils/ApiUtils.java @@ -1,53 +1,16 @@ package com.balex.rag.utils; -import com.balex.rag.model.constants.ApiConstants; -import com.balex.rag.security.JwtTokenProvider; -import jakarta.servlet.http.Cookie; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.springframework.http.HttpHeaders; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Component; - -import java.util.UUID; - -@Component -@RequiredArgsConstructor -public class ApiUtils { - - private final JwtTokenProvider jwtTokenProvider; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class ApiUtils { public static String getMethodName() { try { return new Throwable().getStackTrace()[1].getMethodName(); } catch (Exception cause) { - return ApiConstants.UNDEFINED; + return "undefined"; } } - - public static Cookie createAuthCookie(String value) { - Cookie authorizationCookie = new Cookie(HttpHeaders.AUTHORIZATION, value); - authorizationCookie.setHttpOnly(true); - authorizationCookie.setSecure(true); - authorizationCookie.setPath("/"); - authorizationCookie.setMaxAge(300); - return authorizationCookie; - } - - public static String generateUuidWithoutDash() { - return UUID.randomUUID().toString().replace(ApiConstants.DASH, StringUtils.EMPTY); - } - - - public static String getCurrentUsername() { - return SecurityContextHolder.getContext().getAuthentication().getName(); - } - - public Integer getUserIdFromAuthentication() { - String jwtToken = SecurityContextHolder.getContext().getAuthentication().getCredentials().toString(); - return Integer.parseInt(jwtTokenProvider.getUserId(jwtToken)); - } - -} - +} \ No newline at end of file diff --git a/rag-service/src/main/java/com/balex/rag/utils/PasswordMatches.java b/rag-service/src/main/java/com/balex/rag/utils/PasswordMatches.java deleted file mode 100644 index 1f1ae9e..0000000 --- a/rag-service/src/main/java/com/balex/rag/utils/PasswordMatches.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.balex.rag.utils; - -import com.balex.rag.security.validation.PasswordMatchesValidator; -import jakarta.validation.Constraint; -import jakarta.validation.Payload; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Constraint(validatedBy = PasswordMatchesValidator.class) -@Target({ ElementType.TYPE }) -@Retention(RetentionPolicy.RUNTIME) -public @interface PasswordMatches { - String message() default "Passwords do not match"; - - Class[] groups() default {}; - - Class[] payload() default {}; -} - - diff --git a/rag-service/src/main/java/com/balex/rag/utils/PasswordUtils.java b/rag-service/src/main/java/com/balex/rag/utils/PasswordUtils.java index ebc73df..4c9602c 100644 --- a/rag-service/src/main/java/com/balex/rag/utils/PasswordUtils.java +++ b/rag-service/src/main/java/com/balex/rag/utils/PasswordUtils.java @@ -54,10 +54,6 @@ public final class PasswordUtils { int lettersUCaseNumber = ApiConstants.REQUIRED_MIN_LETTERS_NUMBER_EVERY_CASE_IN_PASSWORD; int lettersLCaseNumber = ApiConstants.REQUIRED_MIN_PASSWORD_LENGTH - charactersNumber - digitsNumber - lettersUCaseNumber; -// String characters = RandomStringUtils.random(charactersNumber, ApiConstants.PASSWORD_CHARACTERS); -// String digits = RandomStringUtils.random(digitsNumber, ApiConstants.PASSWORD_DIGITS); -// String lettersUCase = RandomStringUtils.random(lettersUCaseNumber, ApiConstants.PASSWORD_LETTERS_UPPER_CASE); -// String lettersLCase = RandomStringUtils.random(lettersLCaseNumber, ApiConstants.PASSWORD_LETTERS_LOWER_CASE); String characters = randomFromChars(charactersNumber, ApiConstants.PASSWORD_CHARACTERS); String digits = randomFromChars(digitsNumber, ApiConstants.PASSWORD_DIGITS); String lettersUCase = randomFromChars(lettersUCaseNumber, ApiConstants.PASSWORD_LETTERS_UPPER_CASE); diff --git a/rag-service/src/main/java/com/balex/rag/utils/UserContext.java b/rag-service/src/main/java/com/balex/rag/utils/UserContext.java new file mode 100644 index 0000000..a6f8392 --- /dev/null +++ b/rag-service/src/main/java/com/balex/rag/utils/UserContext.java @@ -0,0 +1,25 @@ +package com.balex.rag.utils; + +import jakarta.servlet.http.HttpServletRequest; + +public final class UserContext { + + private UserContext() {} + + public static Integer getUserId(HttpServletRequest request) { + String userId = (String) request.getAttribute("userId"); + return userId != null ? Integer.parseInt(userId) : null; + } + + public static String getUserEmail(HttpServletRequest request) { + return (String) request.getAttribute("userEmail"); + } + + public static String getUserName(HttpServletRequest request) { + return (String) request.getAttribute("userName"); + } + + public static String getUserRole(HttpServletRequest request) { + return (String) request.getAttribute("userRole"); + } +} \ No newline at end of file diff --git a/rag-service/src/main/resources/application.properties b/rag-service/src/main/resources/application.properties index 00befa7..91b6e94 100644 --- a/rag-service/src/main/resources/application.properties +++ b/rag-service/src/main/resources/application.properties @@ -23,8 +23,6 @@ spring.cloud.consul.discovery.health-check-interval=10s management.endpoints.web.exposure.include=health,info management.endpoint.health.show-details=always -jwt.secret=${JWT_SECRET:ywfI6dBznYmHbokihB/OBzZz6E0Fj+6PiqrM8dQ5c3t0HeYarblCbOGM8vQtOt472AtQ+MsCH7OVIKHOzjrPsQ==} -jwt.expiration=103600000 spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/ragdb} spring.datasource.username=${SPRING_DATASOURCE_USERNAME:postgres} spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:postgres}