Merge branch 'auth-to-gateway' into 'main'

auth-to-gateway

See merge request mirage74-group/post-hub-platform!1
This commit is contained in:
2026-03-09 13:06:11 +01:00
56 changed files with 1070 additions and 1066 deletions

View File

@@ -43,6 +43,50 @@
<artifactId>spring-cloud-starter-gateway</artifactId> <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency> </dependency>
<!-- Reactive Security (WebFlux auto-detected) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- R2DBC — reactive DB access -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<!-- R2DBC PostgreSQL driver (version managed by Spring Boot BOM) -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>r2dbc-postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.13.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.13.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.13.0</version>
<scope>runtime</scope>
</dependency>
<!-- Validation (@Valid, @NotBlank, @Email) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Consul service discovery --> <!-- Consul service discovery -->
<dependency> <dependency>
<groupId>org.springframework.cloud</groupId> <groupId>org.springframework.cloud</groupId>

View File

@@ -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<String> publicPaths = new ArrayList<>();
private List<String> adminPaths = new ArrayList<>();
}

View File

@@ -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<String, RegistrationStatus> {
@Override
public RegistrationStatus convert(@NonNull String source) {
return RegistrationStatus.valueOf(source);
}
}
@WritingConverter
static class RegistrationStatusWriteConverter implements Converter<RegistrationStatus, String> {
@Override
public String convert(@NonNull RegistrationStatus source) {
return source.name();
}
}
@ReadingConverter
static class UserRoleReadConverter implements Converter<String, UserRole> {
@Override
public UserRole convert(@NonNull String source) {
return UserRole.valueOf(source);
}
}
@WritingConverter
static class UserRoleWriteConverter implements Converter<UserRole, String> {
@Override
public String convert(@NonNull UserRole source) {
return source.name();
}
}
}

View File

@@ -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();
}
}

View File

@@ -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<ResponseEntity<RagResponse<UserProfileDTO>>> 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<ResponseEntity<RagResponse<UserProfileDTO>>> 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<ResponseEntity<RagResponse<UserProfileDTO>>> 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);
}
}

View File

@@ -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<AuthFilter.Config> {
public AuthFilter() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
List<String> 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<Void> unauthorized(org.springframework.web.server.ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
public static class Config {
}
}

View File

@@ -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<Void> 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<String> 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<Void> unauthorized(ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
private Mono<Void> 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;
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,7 @@
package com.posthub.gateway.model.enums;
public enum RegistrationStatus {
ACTIVE,
BLOCKED,
DELETED
}

View File

@@ -0,0 +1,6 @@
package com.posthub.gateway.model.enums;
public enum UserRole {
USER,
ADMIN
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<P> {
private String message;
private P payload;
private boolean success;
public static <P> RagResponse<P> createSuccessful(P payload) {
return new RagResponse<>("", payload, true);
}
public static <P> RagResponse<P> createSuccessfulWithNewToken(P payload) {
return new RagResponse<>("Token created or updated", payload, true);
}
}

View File

@@ -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;
}

View File

@@ -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<RefreshToken, Integer> {
Mono<RefreshToken> findByToken(String token);
Mono<RefreshToken> findByUserId(Integer userId);
}

View File

@@ -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<User, Integer> {
Mono<User> findByEmail(String email);
Mono<User> findByUsername(String username);
Mono<Boolean> existsByEmail(String email);
}

View File

@@ -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<String, Object> 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> 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<String, Object> claims, String subject) {
return Jwts.builder()
.claims(claims)
.subject(subject)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + jwtValidityInMilliseconds))
.signWith(secretKey)
.compact();
}
}

View File

@@ -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<RagResponse<UserProfileDTO>> 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<RagResponse<UserProfileDTO>> 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.<User>error(new ResponseStatusException(
HttpStatus.CONFLICT, ApiErrorMessage.USERNAME_ALREADY_EXISTS.getMessage(request.getUsername()))))
.switchIfEmpty(userRepository.existsByEmail(request.getEmail())
.flatMap(exists -> {
if (exists) {
return Mono.<User>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<RagResponse<UserProfileDTO>> 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<RagResponse<UserProfileDTO>> 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("-", "");
}
}

View File

@@ -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));
}
}

View File

@@ -6,6 +6,16 @@ spring:
application: application:
name: gateway-service 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: cloud:
consul: consul:
host: ${CONSUL_HOST:localhost} host: ${CONSUL_HOST:localhost}
@@ -18,27 +28,21 @@ spring:
prefer-ip-address: true prefer-ip-address: true
instance-id: ${spring.application.name}:${random.value} instance-id: ${spring.application.name}:${random.value}
# Spring Cloud Gateway 2025.0 — new prefix: spring.cloud.gateway.server.webflux
gateway: gateway:
server: server:
webflux: 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\..* trusted-proxies: 127\.0\.0\.1|10\.0\.0\..*|172\.1[6-9]\..*|172\.2[0-9]\..*|172\.3[0-1]\..*|192\.168\..*
discovery: discovery:
locator: locator:
enabled: true enabled: true
lower-case-service-id: true lower-case-service-id: true
httpclient: httpclient:
connect-timeout: 5000 connect-timeout: 5000
response-timeout: 60s response-timeout: 60s
default-filters: default-filters:
- DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_FIRST - DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_FIRST
routes: routes:
# RAG Service - actuator (health, info) # RAG Service - actuator
- id: rag-service-actuator - id: rag-service-actuator
uri: lb://rag-service uri: lb://rag-service
predicates: predicates:
@@ -57,19 +61,29 @@ spring:
- RewritePath=/api/rag(?<segment>/?.*), ${segment} - RewritePath=/api/rag(?<segment>/?.*), ${segment}
- AddRequestHeader=X-Forwarded-Prefix, /api/rag - AddRequestHeader=X-Forwarded-Prefix, /api/rag
# Analytics Service (will be added later) # ---- JWT ----
# - id: analytics-service-api jwt:
# uri: lb://analytics-service secret: ${JWT_SECRET:}
# predicates: expiration: ${JWT_EXPIRATION:103600000}
# - Path=/api/analytics/**
# - Method=GET,POST
# filters:
# - RewritePath=/api/analytics(?<segment>/?.*), ${segment}
# ---- 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: gateway:
cors: cors:
allowed-origins: ${CORS_ORIGINS:*} allowed-origins: ${CORS_ORIGINS:*}
# ---- Actuator ----
management: management:
endpoints: endpoints:
web: web:
@@ -81,8 +95,10 @@ management:
gateway: gateway:
enabled: true enabled: true
# ---- Logging ----
logging: logging:
level: level:
root: INFO root: INFO
com.posthub.gateway: DEBUG com.posthub.gateway: DEBUG
org.springframework.cloud.gateway: INFO org.springframework.cloud.gateway: INFO
org.springframework.r2dbc: INFO

View File

@@ -128,23 +128,6 @@
<artifactId>spring-security-test</artifactId> <artifactId>spring-security-test</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency> <dependency>
<groupId>org.springdoc</groupId> <groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>

View File

@@ -1,99 +1,26 @@
package com.balex.rag.config; 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.Bean;
import org.springframework.context.annotation.Configuration; 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.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; 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.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain; 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 @Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig { public class SecurityConfig {
private final JwtRequestFilter jwtRequestFilter;
private final AccessRestrictionHandler accessRestrictionHandler;
@Bean @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http return http
.cors(Customizer.withDefaults())
.csrf(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
.dispatcherTypeMatchers(DispatcherType.ASYNC).permitAll() .build();
.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();
} }
@Bean @Bean
public PasswordEncoder passwordEncoder() { public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); 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;
}
}

View File

@@ -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<UserProfileDTO> 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<RagResponse<UserProfileDTO>> refreshToken(
@RequestParam(name = "token") String refreshToken,
HttpServletResponse response) {
log.trace(ApiLogMessage.NAME_OF_CURRENT_METHOD.getValue(), ApiUtils.getMethodName());
RagResponse<UserProfileDTO> 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<UserProfileDTO> result = authService.registerUser(request);
Cookie authorizationCookie = ApiUtils.createAuthCookie(result.getPayload().getToken());
response.addCookie(authorizationCookie);
eventPublisher.publishUserCreated(result.getPayload().getId().toString());
return ResponseEntity.ok(result);
}
}

View File

@@ -1,10 +1,10 @@
package com.balex.rag.controller; package com.balex.rag.controller;
import com.balex.rag.model.constants.ApiLogMessage;
import com.balex.rag.model.entity.Chat; import com.balex.rag.model.entity.Chat;
import com.balex.rag.service.ChatService; import com.balex.rag.service.ChatService;
import com.balex.rag.service.EventPublisher; 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.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -22,25 +22,25 @@ public class ChatController {
private final ChatService chatService; private final ChatService chatService;
private final EventPublisher eventPublisher; private final EventPublisher eventPublisher;
private final ApiUtils apiUtils;
@GetMapping("") @GetMapping("")
public ResponseEntity<List<Chat>> mainPage() { public ResponseEntity<List<Chat>> mainPage(HttpServletRequest request) {
Long ownerId = apiUtils.getUserIdFromAuthentication().longValue(); Long ownerId = UserContext.getUserId(request).longValue();
List<Chat> response = chatService.getAllChatsByOwner(ownerId); List<Chat> response = chatService.getAllChatsByOwner(ownerId);
return ResponseEntity.ok(response); return ResponseEntity.ok(response);
} }
@GetMapping("/{chatId}") @GetMapping("/{chatId}")
public ResponseEntity<Chat> showChat(@PathVariable Long chatId) { public ResponseEntity<Chat> showChat(@PathVariable Long chatId, HttpServletRequest request) {
Long ownerId = apiUtils.getUserIdFromAuthentication().longValue(); Long ownerId = UserContext.getUserId(request).longValue();
Chat response = chatService.getChat(chatId, ownerId); Chat response = chatService.getChat(chatId, ownerId);
return ResponseEntity.ok(response); return ResponseEntity.ok(response);
} }
@PostMapping("/new") @PostMapping("/new")
public ResponseEntity<Chat> newChat(@RequestParam String title) { public ResponseEntity<Chat> newChat(@RequestParam String title, HttpServletRequest request) {
Chat chat = chatService.createNewChat(title); Long ownerId = UserContext.getUserId(request).longValue();
Chat chat = chatService.createNewChat(title, ownerId);
eventPublisher.publishChatCreated( eventPublisher.publishChatCreated(
chat.getIdOwner().toString(), chat.getIdOwner().toString(),
@@ -50,8 +50,8 @@ public class ChatController {
} }
@DeleteMapping("/{chatId}") @DeleteMapping("/{chatId}")
public ResponseEntity<Void> deleteChat(@PathVariable Long chatId) { public ResponseEntity<Void> deleteChat(@PathVariable Long chatId, HttpServletRequest request) {
Long ownerId = apiUtils.getUserIdFromAuthentication().longValue(); Long ownerId = UserContext.getUserId(request).longValue();
Chat chat = chatService.getChat(chatId, ownerId); Chat chat = chatService.getChat(chatId, ownerId);
chatService.deleteChat(chatId, ownerId); chatService.deleteChat(chatId, ownerId);

View File

@@ -9,6 +9,8 @@ import com.balex.rag.service.ChatEntryService;
import com.balex.rag.service.ChatService; import com.balex.rag.service.ChatService;
import com.balex.rag.service.EventPublisher; import com.balex.rag.service.EventPublisher;
import com.balex.rag.utils.ApiUtils; import com.balex.rag.utils.ApiUtils;
import com.balex.rag.utils.UserContext;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -26,15 +28,15 @@ public class ChatEntryController {
private final ChatService chatService; private final ChatService chatService;
private final RagDefaultsProperties ragDefaults; private final RagDefaultsProperties ragDefaults;
private final EventPublisher eventPublisher; private final EventPublisher eventPublisher;
private final ApiUtils apiUtils;
@PostMapping("/{chatId}") @PostMapping("/{chatId}")
public ResponseEntity<ChatEntry> addUserEntry( public ResponseEntity<ChatEntry> addUserEntry(
@PathVariable Long chatId, @PathVariable Long chatId,
@RequestBody UserEntryRequest request) { @RequestBody UserEntryRequest request,
HttpServletRequest httpRequest) {
log.trace(ApiLogMessage.NAME_OF_CURRENT_METHOD.getValue(), ApiUtils.getMethodName()); 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(); boolean onlyContext = request.onlyContext() != null ? request.onlyContext() : ragDefaults.onlyContext();
double topP = request.topP() != null ? request.topP() : ragDefaults.topP(); double topP = request.topP() != null ? request.topP() : ragDefaults.topP();

View File

@@ -1,11 +1,12 @@
package com.balex.rag.controller; package com.balex.rag.controller;
import com.balex.rag.service.UserDocumentService; 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.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -28,7 +29,6 @@ import java.util.List;
public class DocumentUploadStreamController { public class DocumentUploadStreamController {
private final UserDocumentService userDocumentService; private final UserDocumentService userDocumentService;
private final ApiUtils apiUtils;
@ApiResponses(value = { @ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Document processing progress stream", @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) @PostMapping(value = "/upload-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter uploadDocumentsWithProgress( public SseEmitter uploadDocumentsWithProgress(
@RequestPart("files") @Valid List<MultipartFile> files @RequestPart("files") @Valid List<MultipartFile> files,
) { HttpServletRequest request) {
Integer userId = apiUtils.getUserIdFromAuthentication(); Integer userId = UserContext.getUserId(request);
return userDocumentService.processUploadedFilesWithSse(files, userId.longValue()); return userDocumentService.processUploadedFilesWithSse(files, userId.longValue());
} }
} }

View File

@@ -11,6 +11,8 @@ import com.balex.rag.model.response.RagResponse;
import com.balex.rag.service.EventPublisher; import com.balex.rag.service.EventPublisher;
import com.balex.rag.service.UserService; import com.balex.rag.service.UserService;
import com.balex.rag.utils.ApiUtils; import com.balex.rag.utils.ApiUtils;
import com.balex.rag.utils.UserContext;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -51,26 +53,25 @@ public class UserController {
} }
@GetMapping("${end.points.userinfo}") @GetMapping("${end.points.userinfo}")
public ResponseEntity<RagResponse<UserInfo>> getUserInfo( public ResponseEntity<RagResponse<UserInfo>> getUserInfo(HttpServletRequest request) {
@RequestHeader("Authorization") String token) {
log.trace(ApiLogMessage.NAME_OF_CURRENT_METHOD.getValue(), ApiUtils.getMethodName()); log.trace(ApiLogMessage.NAME_OF_CURRENT_METHOD.getValue(), ApiUtils.getMethodName());
RagResponse<UserInfo> userInfo = userService.getUserInfo(token); Integer userId = UserContext.getUserId(request);
RagResponse<UserInfo> userInfo = userService.getUserInfo(userId);
return ResponseEntity.ok(userInfo); return ResponseEntity.ok(userInfo);
} }
@DeleteMapping("${end.points.userinfo}") @DeleteMapping("${end.points.userinfo}")
public ResponseEntity<RagResponse<Integer>> deleteUserDocuments( public ResponseEntity<RagResponse<Integer>> deleteUserDocuments(HttpServletRequest request) {
@RequestHeader("Authorization") String token) {
log.trace(ApiLogMessage.NAME_OF_CURRENT_METHOD.getValue(), ApiUtils.getMethodName()); log.trace(ApiLogMessage.NAME_OF_CURRENT_METHOD.getValue(), ApiUtils.getMethodName());
RagResponse<Integer> deletedCount = userService.deleteUserDocuments(token); Integer userId = UserContext.getUserId(request);
RagResponse<Integer> deletedCount = userService.deleteUserDocuments(userId);
return ResponseEntity.ok(deletedCount); return ResponseEntity.ok(deletedCount);
} }
@PutMapping("${end.points.id}") @PutMapping("${end.points.id}")
public ResponseEntity<RagResponse<UserDTO>> updateUserById( public ResponseEntity<RagResponse<UserDTO>> updateUserById(
@PathVariable(name = "id") Integer userId, @PathVariable(name = "id") Integer userId,
@@ -80,14 +81,14 @@ public class UserController {
RagResponse<UserDTO> updatedPost = userService.updateUser(userId, request); RagResponse<UserDTO> updatedPost = userService.updateUser(userId, request);
return ResponseEntity.ok(updatedPost); return ResponseEntity.ok(updatedPost);
} }
@DeleteMapping("${end.points.id}") @DeleteMapping("${end.points.id}")
public ResponseEntity<Void> softDeleteUser( public ResponseEntity<Void> softDeleteUser(
@PathVariable(name = "id") Integer userId @PathVariable(name = "id") Integer userId,
) { HttpServletRequest request) {
log.trace(ApiLogMessage.NAME_OF_CURRENT_METHOD.getValue(), ApiUtils.getMethodName()); 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(); return ResponseEntity.ok().build();
} }

View File

@@ -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);
}
}

View File

@@ -1,14 +1,11 @@
package com.balex.rag.mapper; package com.balex.rag.mapper;
import com.balex.rag.model.dto.UserDTO; 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.dto.UserSearchDTO;
import com.balex.rag.model.entity.User; import com.balex.rag.model.entity.User;
import com.balex.rag.model.enums.RegistrationStatus; import com.balex.rag.model.enums.RegistrationStatus;
import com.balex.rag.model.request.user.NewUserRequest; 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 com.balex.rag.model.request.user.UpdateUserRequest;
import org.hibernate.type.descriptor.DateTimeUtils;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget; import org.mapstruct.MappingTarget;
@@ -19,7 +16,7 @@ import java.util.Objects;
@Mapper( @Mapper(
componentModel = "spring", componentModel = "spring",
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE, nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE,
imports = {RegistrationStatus.class, Objects.class, DateTimeUtils.class} imports = {RegistrationStatus.class, Objects.class}
) )
public interface UserMapper { public interface UserMapper {
@@ -36,16 +33,4 @@ public interface UserMapper {
@Mapping(source = "deleted", target = "isDeleted") @Mapping(source = "deleted", target = "isDeleted")
UserSearchDTO toUserSearchDto(User user); 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);
} }

View File

@@ -8,8 +8,6 @@ public final class ApiConstants {
public static final String UNDEFINED = "undefined"; public static final String UNDEFINED = "undefined";
public static final String EMPTY_FILENAME = "unknown"; 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_RED = "\u001B[31m";
public static final String ANSI_WHITE = "\u001B[37m"; public static final String ANSI_WHITE = "\u001B[37m";
public static final String BREAK_LINE = "\n"; public static final String BREAK_LINE = "\n";

View File

@@ -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;
}

View File

@@ -1,9 +0,0 @@
package com.balex.rag.model.exception;
public class InvalidTokenException extends RuntimeException {
public InvalidTokenException(String message) {
super(message);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<RefreshToken, Integer> {
Optional<RefreshToken> findByToken(String token);
Optional<RefreshToken> findByUserId(Integer userId);
Optional<RefreshToken> findByUserEmail(String email);
}

View File

@@ -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<String, Object> 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> 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<String, Object> 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);
}
}

View File

@@ -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<String> 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<String> emailOpt = Optional.ofNullable(jwtTokenProvider.getUsername(jwt));
Optional<String> userIdOpt = Optional.ofNullable(jwtTokenProvider.getUserId(jwt));
String sessionId = jwtTokenProvider.getSessionId(jwt);
if (emailOpt.isPresent() && userIdOpt.isPresent()) {
// Check session validity
Optional<String> 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<SimpleGrantedAuthority> 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);
}
}

View File

@@ -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());
}
}

View File

@@ -1,14 +1,7 @@
package com.balex.rag.security.validation; package com.balex.rag.security.validation;
import com.balex.rag.model.constants.ApiErrorMessage; 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.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.RequiredArgsConstructor;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -19,34 +12,11 @@ import java.nio.file.AccessDeniedException;
@RequiredArgsConstructor @RequiredArgsConstructor
public class AccessValidator { public class AccessValidator {
private final UserRepository userRepository; 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 @SneakyThrows
public void validateOwnerAccess(Integer ownerId) { public void validateOwnerAccess(Integer ownerId, Integer currentUserId) {
Integer currentUserId = apiUtils.getUserIdFromAuthentication();
if (!currentUserId.equals(ownerId)) { if (!currentUserId.equals(ownerId)) {
throw new AccessDeniedException(ApiErrorMessage.HAVE_NO_ACCESS.getMessage()); throw new AccessDeniedException(ApiErrorMessage.HAVE_NO_ACCESS.getMessage());
} }
} }
} }

View File

@@ -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<PasswordMatches, RegistrationUserRequest> {
@Override
public boolean isValid(RegistrationUserRequest request, ConstraintValidatorContext constraintValidatorContext) {
return request.getPassword().equals(request.getConfirmPassword());
}
}

View File

@@ -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<UserProfileDTO> login(LoginRequest request);
RagResponse<UserProfileDTO> refreshAccessToken(String refreshToken);
RagResponse<UserProfileDTO> registerUser(RegistrationUserRequest request);
}

View File

@@ -7,7 +7,7 @@ import java.util.List;
public interface ChatService { public interface ChatService {
Chat createNewChat(String title); Chat createNewChat(String title, Long ownerId);
List<Chat> getAllChatsByOwner(Long ownerId); List<Chat> getAllChatsByOwner(Long ownerId);

View File

@@ -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);
}

View File

@@ -9,9 +9,8 @@ import com.balex.rag.model.response.PaginationResponse;
import com.balex.rag.model.response.RagResponse; import com.balex.rag.model.response.RagResponse;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.security.core.userdetails.UserDetailsService;
public interface UserService extends UserDetailsService { public interface UserService {
RagResponse<UserDTO> getById(@NotNull Integer userId); RagResponse<UserDTO> getById(@NotNull Integer userId);
@@ -19,15 +18,11 @@ public interface UserService extends UserDetailsService {
RagResponse<UserDTO> updateUser(@NotNull Integer postId, @NotNull UpdateUserRequest request); RagResponse<UserDTO> updateUser(@NotNull Integer postId, @NotNull UpdateUserRequest request);
void softDeleteUser(Integer userId); void softDeleteUser(Integer userId, Integer currentUserId);
RagResponse<PaginationResponse<UserSearchDTO>> findAllUsers(Pageable pageable); RagResponse<PaginationResponse<UserSearchDTO>> findAllUsers(Pageable pageable);
RagResponse<UserInfo> getUserInfo(String token); RagResponse<UserInfo> getUserInfo(Integer userId);
RagResponse<Integer> deleteUserDocuments(String token);
RagResponse<Integer> deleteUserDocuments(Integer userId);
} }

View File

@@ -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<UserProfileDTO> 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<UserProfileDTO> 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<UserProfileDTO> 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);
}
}

View File

@@ -3,13 +3,11 @@ package com.balex.rag.service.impl;
import com.balex.rag.model.entity.Chat; import com.balex.rag.model.entity.Chat;
import com.balex.rag.repo.ChatRepository; import com.balex.rag.repo.ChatRepository;
import com.balex.rag.service.ChatService; import com.balex.rag.service.ChatService;
import com.balex.rag.utils.ApiUtils;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
@@ -23,25 +21,19 @@ public class ChatServiceImpl implements ChatService {
private final ChatRepository chatRepo; private final ChatRepository chatRepo;
private final ChatClient chatClient; private final ChatClient chatClient;
private final ApiUtils apiUtils;
public List<Chat> getAllChatsByOwner(Long ownerId) { public List<Chat> getAllChatsByOwner(Long ownerId) {
return chatRepo.findByIdOwnerOrderByCreatedAtDesc(ownerId); return chatRepo.findByIdOwnerOrderByCreatedAtDesc(ownerId);
} }
public Chat createNewChat(String title) { public Chat createNewChat(String title, Long ownerId) {
Long ownerId = apiUtils.getUserIdFromAuthentication().longValue(); Chat chat = Chat.builder().title(title).idOwner(ownerId).build();
Chat chat = Chat.builder()
.title(title)
.idOwner(ownerId)
.build();
chatRepo.save(chat); chatRepo.save(chat);
return chat; return chat;
} }
public Chat getChat(Long chatId, Long ownerId) { public Chat getChat(Long chatId, Long ownerId) {
Chat chat = chatRepo.findById(chatId).orElseThrow( Chat chat = chatRepo.findById(chatId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Chat not found"));
() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Chat not found"));
if (!chat.getIdOwner().equals(ownerId)) { if (!chat.getIdOwner().equals(ownerId)) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Access denied"); throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Access denied");
} }
@@ -57,15 +49,7 @@ public class ChatServiceImpl implements ChatService {
SseEmitter sseEmitter = new SseEmitter(0L); SseEmitter sseEmitter = new SseEmitter(0L);
final StringBuilder answer = new StringBuilder(); final StringBuilder answer = new StringBuilder();
chatClient chatClient.prompt(userPrompt).advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, chatId)).stream().chatResponse().subscribe((ChatResponse response) -> processToken(response, sseEmitter, answer), sseEmitter::completeWithError, sseEmitter::complete);
.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; return sseEmitter;
} }

View File

@@ -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<String> getSessionIdByEmail(String email) {
return refreshTokenRepository.findByUserEmail(email)
.map(RefreshToken::getSessionId);
}
}

View File

@@ -8,7 +8,6 @@ import com.balex.rag.model.dto.UserSearchDTO;
import com.balex.rag.model.entity.LoadedDocumentInfo; import com.balex.rag.model.entity.LoadedDocumentInfo;
import com.balex.rag.model.entity.User; import com.balex.rag.model.entity.User;
import com.balex.rag.model.entity.UserInfo; 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.exception.NotFoundException;
import com.balex.rag.model.request.user.NewUserRequest; import com.balex.rag.model.request.user.NewUserRequest;
import com.balex.rag.model.request.user.UpdateUserRequest; 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.DocumentRepository;
import com.balex.rag.repo.UserRepository; import com.balex.rag.repo.UserRepository;
import com.balex.rag.repo.VectorStoreRepository; import com.balex.rag.repo.VectorStoreRepository;
import com.balex.rag.security.JwtTokenProvider;
import com.balex.rag.security.validation.AccessValidator; import com.balex.rag.security.validation.AccessValidator;
import com.balex.rag.service.UserService; import com.balex.rag.service.UserService;
import com.balex.rag.service.model.exception.DataExistException; import com.balex.rag.service.model.exception.DataExistException;
@@ -25,19 +23,12 @@ import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; 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.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List; import java.util.List;
import static com.balex.rag.model.constants.ApiConstants.USER_ROLE;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class UserServiceImpl implements UserService { public class UserServiceImpl implements UserService {
@@ -45,7 +36,6 @@ public class UserServiceImpl implements UserService {
private final UserMapper userMapper; private final UserMapper userMapper;
private final PasswordEncoder passwordEncoder; private final PasswordEncoder passwordEncoder;
private final AccessValidator accessValidator; private final AccessValidator accessValidator;
private final JwtTokenProvider jwtTokenProvider;
private final DocumentRepository documentRepository; private final DocumentRepository documentRepository;
private final VectorStoreRepository vectorStoreRepository; private final VectorStoreRepository vectorStoreRepository;
@@ -98,15 +88,14 @@ public class UserServiceImpl implements UserService {
@Override @Override
@Transactional @Transactional
public void softDeleteUser(Integer userId) { public void softDeleteUser(Integer userId, Integer currentUserId) {
User user = userRepository.findByIdAndDeletedFalse(userId) User user = userRepository.findByIdAndDeletedFalse(userId)
.orElseThrow(() -> new NotFoundException(ApiErrorMessage.USER_NOT_FOUND_BY_ID.getMessage(userId))); .orElseThrow(() -> new NotFoundException(ApiErrorMessage.USER_NOT_FOUND_BY_ID.getMessage(userId)));
accessValidator.validateOwnerAccess(userId); accessValidator.validateOwnerAccess(userId, currentUserId);
user.setDeleted(true); user.setDeleted(true);
userRepository.save(user); userRepository.save(user);
} }
@Override @Override
@@ -128,28 +117,11 @@ public class UserServiceImpl implements UserService {
return RagResponse.createSuccessful(paginationResponse); 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 @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public RagResponse<UserInfo> getUserInfo(String token) { public RagResponse<UserInfo> getUserInfo(Integer userId) {
User user = getUserInfoFromToken(token); User user = userRepository.findByIdAndDeletedFalse(userId)
.orElseThrow(() -> new NotFoundException(ApiErrorMessage.USER_NOT_FOUND_BY_ID.getMessage(userId)));
List<LoadedDocumentInfo> loadedFiles = documentRepository List<LoadedDocumentInfo> loadedFiles = documentRepository
.findByUserId(user.getId()) .findByUserId(user.getId())
@@ -167,9 +139,9 @@ public class UserServiceImpl implements UserService {
@Override @Override
@Transactional @Transactional
public RagResponse<Integer> deleteUserDocuments(String token) { public RagResponse<Integer> deleteUserDocuments(Integer userId) {
getUserInfoFromToken(token); User user = userRepository.findByIdAndDeletedFalse(userId)
User user = getUserInfoFromToken(token); .orElseThrow(() -> new NotFoundException(ApiErrorMessage.USER_NOT_FOUND_BY_ID.getMessage(userId)));
List<LoadedDocument> documents = documentRepository.findByUserId(user.getId()); List<LoadedDocument> documents = documentRepository.findByUserId(user.getId());
@@ -177,28 +149,9 @@ public class UserServiceImpl implements UserService {
return RagResponse.createSuccessful(0); return RagResponse.createSuccessful(0);
} }
// Удаляем чанки по user_id
vectorStoreRepository.deleteByUserId(user.getId().longValue()); vectorStoreRepository.deleteByUserId(user.getId().longValue());
// Удаляем записи из loaded_document
documentRepository.deleteAll(documents); documentRepository.deleteAll(documents);
return RagResponse.createSuccessful(documents.size()); 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));
}
}

View File

@@ -1,53 +1,16 @@
package com.balex.rag.utils; package com.balex.rag.utils;
import com.balex.rag.model.constants.ApiConstants; import lombok.AccessLevel;
import com.balex.rag.security.JwtTokenProvider; import lombok.NoArgsConstructor;
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;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class ApiUtils {
public static String getMethodName() { public static String getMethodName() {
try { try {
return new Throwable().getStackTrace()[1].getMethodName(); return new Throwable().getStackTrace()[1].getMethodName();
} catch (Exception cause) { } 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));
}
}

View File

@@ -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<? extends Payload>[] payload() default {};
}

View File

@@ -54,10 +54,6 @@ public final class PasswordUtils {
int lettersUCaseNumber = ApiConstants.REQUIRED_MIN_LETTERS_NUMBER_EVERY_CASE_IN_PASSWORD; int lettersUCaseNumber = ApiConstants.REQUIRED_MIN_LETTERS_NUMBER_EVERY_CASE_IN_PASSWORD;
int lettersLCaseNumber = ApiConstants.REQUIRED_MIN_PASSWORD_LENGTH int lettersLCaseNumber = ApiConstants.REQUIRED_MIN_PASSWORD_LENGTH
- charactersNumber - digitsNumber - lettersUCaseNumber; - 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 characters = randomFromChars(charactersNumber, ApiConstants.PASSWORD_CHARACTERS);
String digits = randomFromChars(digitsNumber, ApiConstants.PASSWORD_DIGITS); String digits = randomFromChars(digitsNumber, ApiConstants.PASSWORD_DIGITS);
String lettersUCase = randomFromChars(lettersUCaseNumber, ApiConstants.PASSWORD_LETTERS_UPPER_CASE); String lettersUCase = randomFromChars(lettersUCaseNumber, ApiConstants.PASSWORD_LETTERS_UPPER_CASE);

View File

@@ -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");
}
}

View File

@@ -23,8 +23,6 @@ spring.cloud.consul.discovery.health-check-interval=10s
management.endpoints.web.exposure.include=health,info management.endpoints.web.exposure.include=health,info
management.endpoint.health.show-details=always 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.url=${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/ragdb}
spring.datasource.username=${SPRING_DATASOURCE_USERNAME:postgres} spring.datasource.username=${SPRING_DATASOURCE_USERNAME:postgres}
spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:postgres} spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:postgres}