inital commit
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
package kz.konturai.configuration;
|
||||
|
||||
import java.util.NoSuchElementException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
public static class ErrorResponse {
|
||||
public String message;
|
||||
|
||||
public ErrorResponse(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
public ResponseEntity<ErrorResponse> handleIllegalArgument(IllegalArgumentException ex) {
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorResponse(ex.getMessage()));
|
||||
}
|
||||
|
||||
@ExceptionHandler({ MethodArgumentNotValidException.class, BindException.class,
|
||||
HttpMessageNotReadableException.class })
|
||||
public ResponseEntity<ErrorResponse> handleValidation(Exception ex) {
|
||||
String msg = ex.getMessage();
|
||||
if (ex instanceof MethodArgumentNotValidException manv && manv.getBindingResult() != null) {
|
||||
msg = manv.getBindingResult().getAllErrors().stream()
|
||||
.findFirst()
|
||||
.map(e -> e.getDefaultMessage() != null ? e.getDefaultMessage() : e.toString())
|
||||
.orElse("Validation error");
|
||||
}
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorResponse(msg));
|
||||
}
|
||||
|
||||
@ExceptionHandler(NoSuchElementException.class)
|
||||
public ResponseEntity<ErrorResponse> handleNotFound(NoSuchElementException ex) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(ex.getMessage()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(AuthenticationException.class)
|
||||
public ResponseEntity<ErrorResponse> handleAuth(AuthenticationException ex) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse("Unauthorized"));
|
||||
}
|
||||
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
public ResponseEntity<ErrorResponse> handleAccessDenied(AccessDeniedException ex) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorResponse("Forbidden"));
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ErrorResponse> handleGeneric(Exception ex) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ErrorResponse("Internal server error"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package kz.konturai.configuration;
|
||||
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.info.Contact;
|
||||
import io.swagger.v3.oas.models.info.License;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.models.Components;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class OpenApiConfig {
|
||||
|
||||
@Bean
|
||||
public OpenAPI customOpenAPI() {
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("KonturAI API")
|
||||
.description("API для системы маркетинга и таргетинга KonturAI")
|
||||
.version("1.0.0")
|
||||
.contact(new Contact()
|
||||
.name("KonturAI Team")
|
||||
.email("support@konturai.kz"))
|
||||
.license(new License()
|
||||
.name("MIT License")
|
||||
.url("https://opensource.org/licenses/MIT")))
|
||||
.addSecurityItem(new SecurityRequirement().addList("Bearer Authentication"))
|
||||
.components(new Components()
|
||||
.addSecuritySchemes("Bearer Authentication", createAPIKeyScheme()));
|
||||
}
|
||||
|
||||
private SecurityScheme createAPIKeyScheme() {
|
||||
return new SecurityScheme()
|
||||
.type(SecurityScheme.Type.HTTP)
|
||||
.bearerFormat("JWT")
|
||||
.scheme("bearer");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package kz.konturai.configuration.security;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
@Component
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final JwtService jwtService;
|
||||
|
||||
public JwtAuthenticationFilter(JwtService jwtService) {
|
||||
this.jwtService = jwtService;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
if (header != null && header.startsWith("Bearer ")) {
|
||||
String token = header.substring(7);
|
||||
try {
|
||||
Claims claims = jwtService.parseAndValidate(token);
|
||||
String subject = claims.getSubject();
|
||||
Object rolesClaim = claims.get("roles");
|
||||
Collection<? extends GrantedAuthority> authorities = List.of();
|
||||
if (rolesClaim instanceof String s && !s.isEmpty()) {
|
||||
authorities = java.util.Arrays.stream(s.split(","))
|
||||
.map(String::trim)
|
||||
.filter(r -> !r.isEmpty())
|
||||
.map(SimpleGrantedAuthority::new)
|
||||
.toList();
|
||||
}
|
||||
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
|
||||
subject,
|
||||
null,
|
||||
authorities);
|
||||
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
SecurityContextHolder.getContext().setAuthentication(auth);
|
||||
} catch (Exception ignored) {
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package kz.konturai.configuration.security;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import io.jsonwebtoken.io.Decoders;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import java.security.Key;
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class JwtService {
|
||||
|
||||
private final Key signingKey;
|
||||
private final long accessTokenTtlSeconds;
|
||||
|
||||
public JwtService(
|
||||
@Value("${security.jwt.secret-base64:ZmFrZV9zZWNyZXRfMTIzNDU2Nzg5MGFiY2RlZmFrZV9zZWNyZXRfMTIzNDU2Nzg5MGFiY2Rl}") String base64Secret,
|
||||
@Value("${security.jwt.access-ttl-seconds:3600}") long accessTokenTtlSeconds) {
|
||||
this.signingKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(base64Secret));
|
||||
this.accessTokenTtlSeconds = accessTokenTtlSeconds;
|
||||
}
|
||||
|
||||
public String generateToken(String subject, Map<String, Object> claims) {
|
||||
Instant now = Instant.now();
|
||||
Instant expiry = now.plusSeconds(accessTokenTtlSeconds);
|
||||
return Jwts.builder()
|
||||
.setClaims(claims)
|
||||
.setSubject(subject)
|
||||
.setIssuedAt(Date.from(now))
|
||||
.setExpiration(Date.from(expiry))
|
||||
.signWith(signingKey, SignatureAlgorithm.HS256)
|
||||
.compact();
|
||||
}
|
||||
|
||||
public Claims parseAndValidate(String token) {
|
||||
return Jwts.parserBuilder()
|
||||
.setSigningKey(signingKey)
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package kz.konturai.configuration.security;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
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.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||
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;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
||||
@Configuration
|
||||
@EnableMethodSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
private final JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||
@Value("${app.cors.allowed-origin:http://localhost:5173}")
|
||||
private String allowedOrigin;
|
||||
|
||||
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
|
||||
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
AuthenticationEntryPoint entryPoint = (request, response, ex) -> {
|
||||
response.setStatus(401);
|
||||
response.setContentType("application/json");
|
||||
response.getWriter().write("{\"message\":\"Unauthorized\"}");
|
||||
};
|
||||
AccessDeniedHandler accessDeniedHandler = (request, response, ex) -> {
|
||||
response.setStatus(403);
|
||||
response.setContentType("application/json");
|
||||
response.getWriter().write("{\"message\":\"Forbidden\"}");
|
||||
};
|
||||
http
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers(org.springframework.http.HttpMethod.OPTIONS, "/**").permitAll()
|
||||
.requestMatchers("/api/auth/**").permitAll()
|
||||
.anyRequest().authenticated())
|
||||
.exceptionHandling(ex -> ex
|
||||
.authenticationEntryPoint(entryPoint)
|
||||
.accessDeniedHandler(accessDeniedHandler))
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
config.setAllowedOrigins(List.of(allowedOrigin));
|
||||
config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
|
||||
config.setAllowedHeaders(List.of("Authorization", "Content-Type", "Accept", "X-Requested-With"));
|
||||
config.setExposedHeaders(List.of("Authorization"));
|
||||
config.setAllowCredentials(true);
|
||||
config.setMaxAge(3600L);
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", config);
|
||||
return source;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
|
||||
return configuration.getAuthenticationManager();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package kz.konturai.controller;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import kz.konturai.domain.Role;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin")
|
||||
@Tag(name = "Admin", description = "API для административных функций")
|
||||
@SecurityRequirement(name = "Bearer Authentication")
|
||||
public class AdminController {
|
||||
|
||||
@Operation(summary = "Получить список ролей", description = "Возвращает список всех доступных ролей в системе")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "Список ролей успешно получен"),
|
||||
@ApiResponse(responseCode = "403", description = "Недостаточно прав доступа")
|
||||
})
|
||||
@GetMapping("/roles")
|
||||
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
|
||||
public ResponseEntity<List<String>> listRoles() {
|
||||
List<String> roles = Arrays.stream(Role.values())
|
||||
.map(Enum::name)
|
||||
.collect(Collectors.toList());
|
||||
return ResponseEntity.ok(roles);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package kz.konturai.controller;
|
||||
|
||||
import kz.konturai.dto.AuthDtos;
|
||||
import kz.konturai.service.spec.AuthService;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
@Tag(name = "Authentication", description = "API для аутентификации пользователей")
|
||||
public class AuthController {
|
||||
|
||||
private final AuthService authService;
|
||||
|
||||
public AuthController(AuthService authService) {
|
||||
this.authService = authService;
|
||||
}
|
||||
|
||||
// signup удалён — создание пользователя доступно только админам
|
||||
|
||||
@Operation(summary = "Вход в систему", description = "Аутентификация пользователя по email и паролю")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "Успешная аутентификация"),
|
||||
@ApiResponse(responseCode = "401", description = "Неверные учетные данные"),
|
||||
@ApiResponse(responseCode = "400", description = "Некорректные данные запроса")
|
||||
})
|
||||
@PostMapping("/signin")
|
||||
public ResponseEntity<AuthDtos.AuthResponse> signIn(@RequestBody AuthDtos.SignInRequest request) {
|
||||
return ResponseEntity.ok(authService.signIn(request));
|
||||
}
|
||||
|
||||
@Operation(summary = "Обновление токена", description = "Обновление access токена с помощью refresh токена")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "Токен успешно обновлен"),
|
||||
@ApiResponse(responseCode = "401", description = "Недействительный refresh токен"),
|
||||
@ApiResponse(responseCode = "400", description = "Некорректные данные запроса")
|
||||
})
|
||||
@PostMapping("/refresh")
|
||||
public ResponseEntity<AuthDtos.AuthResponse> refresh(@RequestBody AuthDtos.RefreshRequest request) {
|
||||
return ResponseEntity.ok(authService.refresh(request));
|
||||
}
|
||||
|
||||
@Operation(summary = "Выход из системы", description = "Выход пользователя из системы и инвалидация refresh токена")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "Успешный выход из системы"),
|
||||
@ApiResponse(responseCode = "400", description = "Некорректные данные запроса")
|
||||
})
|
||||
@PostMapping("/logout")
|
||||
public ResponseEntity<Void> logout(@RequestBody AuthDtos.RefreshRequest request) {
|
||||
authService.logout(request.refreshToken);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package kz.konturai.controller;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import kz.konturai.domain.User;
|
||||
import kz.konturai.repository.UserRepository;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/identity")
|
||||
@Tag(name = "Identity", description = "API для получения информации о текущем пользователе")
|
||||
@SecurityRequirement(name = "Bearer Authentication")
|
||||
public class IdentityController {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public IdentityController(UserRepository userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@Operation(summary = "Получить информацию о текущем пользователе", description = "Возвращает информацию о текущем аутентифицированном пользователе")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "Информация о пользователе успешно получена"),
|
||||
@ApiResponse(responseCode = "401", description = "Пользователь не аутентифицирован")
|
||||
})
|
||||
@GetMapping("/me")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ResponseEntity<MeResponse> me(Principal principal) {
|
||||
String email = principal.getName();
|
||||
User user = userRepository.findByEmail(email)
|
||||
.orElseThrow(() -> new IllegalArgumentException("User not found"));
|
||||
List<String> roles = user.getRoles() == null || user.getRoles().isBlank()
|
||||
? List.of()
|
||||
: Arrays.stream(user.getRoles().split(","))
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.collect(Collectors.toList());
|
||||
return ResponseEntity.ok(new MeResponse(user.getId(), user.getEmail(), roles));
|
||||
}
|
||||
|
||||
@Schema(description = "Информация о текущем пользователе")
|
||||
public static class MeResponse {
|
||||
@Schema(description = "ID пользователя", example = "1")
|
||||
public Long id;
|
||||
@Schema(description = "Email пользователя", example = "user@example.com")
|
||||
public String email;
|
||||
@Schema(description = "Список ролей пользователя", example = "[\"ROLE_USER\", \"ROLE_ADMIN\"]")
|
||||
public List<String> roles;
|
||||
|
||||
public MeResponse(Long id, String email, List<String> roles) {
|
||||
this.id = id;
|
||||
this.email = email;
|
||||
this.roles = roles;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package kz.konturai.controller;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import kz.konturai.dto.AuthDtos;
|
||||
import kz.konturai.dto.SetRolesRequest;
|
||||
import kz.konturai.dto.UpdateUserRequest;
|
||||
import kz.konturai.dto.UserSummary;
|
||||
import kz.konturai.domain.Role;
|
||||
import kz.konturai.service.spec.AuthService;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/users")
|
||||
@Tag(name = "User Management", description = "API для управления пользователями")
|
||||
@SecurityRequirement(name = "Bearer Authentication")
|
||||
public class UserAdminController {
|
||||
|
||||
private final AuthService authService;
|
||||
|
||||
public UserAdminController(AuthService authService) {
|
||||
this.authService = authService;
|
||||
}
|
||||
|
||||
@Operation(summary = "Создать пользователя", description = "Создание нового пользователя в системе")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "Пользователь успешно создан"),
|
||||
@ApiResponse(responseCode = "403", description = "Недостаточно прав доступа"),
|
||||
@ApiResponse(responseCode = "400", description = "Некорректные данные запроса")
|
||||
})
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
|
||||
public ResponseEntity<Void> createUser(@RequestBody AuthDtos.SignUpRequest request) {
|
||||
authService.signUp(request);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@Operation(summary = "Установить роли пользователя", description = "Установка ролей для существующего пользователя")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "Роли пользователя успешно обновлены"),
|
||||
@ApiResponse(responseCode = "403", description = "Недостаточно прав доступа"),
|
||||
@ApiResponse(responseCode = "400", description = "Некорректные данные запроса")
|
||||
})
|
||||
@PostMapping("/roles")
|
||||
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
|
||||
public ResponseEntity<Void> setUserRoles(@RequestBody SetRolesRequest request) {
|
||||
authService.setUserRoles(request.email(), request.roles());
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@Operation(summary = "Получить список ролей", description = "Возвращает список всех доступных ролей в системе")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "Список ролей успешно получен"),
|
||||
@ApiResponse(responseCode = "403", description = "Недостаточно прав доступа")
|
||||
})
|
||||
@GetMapping("/roles")
|
||||
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
|
||||
public ResponseEntity<List<String>> listRoles() {
|
||||
List<String> roles = Arrays.stream(Role.values()).map(Enum::name).collect(Collectors.toList());
|
||||
return ResponseEntity.ok(roles);
|
||||
}
|
||||
|
||||
@Operation(summary = "Получить список пользователей", description = "Возвращает пагинированный список всех пользователей в системе")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "Список пользователей успешно получен"),
|
||||
@ApiResponse(responseCode = "403", description = "Недостаточно прав доступа")
|
||||
})
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
|
||||
public ResponseEntity<kz.konturai.dto.PageResponse<UserSummary>> listUsers(
|
||||
@Parameter(description = "Номер страницы (начиная с 0)", example = "0") @RequestParam(defaultValue = "0") int page,
|
||||
@Parameter(description = "Размер страницы", example = "20") @RequestParam(defaultValue = "20") int size) {
|
||||
Pageable pageable = PageRequest.of(page, size);
|
||||
org.springframework.data.domain.Page<kz.konturai.domain.User> p = authService.listUsers(pageable);
|
||||
java.util.List<UserSummary> list = p.map(u -> new UserSummary(u.getId(), u.getEmail(), u.getRoles()))
|
||||
.getContent();
|
||||
kz.konturai.dto.PageResponse<UserSummary> resp = new kz.konturai.dto.PageResponse<>(
|
||||
list,
|
||||
p.getNumber(),
|
||||
p.getSize(),
|
||||
p.getTotalElements(),
|
||||
p.getTotalPages(),
|
||||
p.isFirst(),
|
||||
p.isLast(),
|
||||
p.getNumberOfElements());
|
||||
return ResponseEntity.ok(resp);
|
||||
}
|
||||
|
||||
@Operation(summary = "Обновить пользователя", description = "Обновление данных существующего пользователя")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "Пользователь успешно обновлен"),
|
||||
@ApiResponse(responseCode = "403", description = "Недостаточно прав доступа"),
|
||||
@ApiResponse(responseCode = "400", description = "Некорректные данные запроса")
|
||||
})
|
||||
@PutMapping("/update")
|
||||
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
|
||||
public ResponseEntity<Void> updateUser(@RequestBody UpdateUserRequest request) {
|
||||
authService.updateUser(request.id(), request.email(), request.password(), request.roles());
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@Operation(summary = "Удалить пользователя", description = "Удаление пользователя из системы по ID")
|
||||
@ApiResponses(value = {
|
||||
@ApiResponse(responseCode = "200", description = "Пользователь успешно удален"),
|
||||
@ApiResponse(responseCode = "403", description = "Недостаточно прав доступа"),
|
||||
@ApiResponse(responseCode = "400", description = "Некорректные данные запроса")
|
||||
})
|
||||
@DeleteMapping
|
||||
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
|
||||
public ResponseEntity<Void> deleteUser(
|
||||
@Parameter(description = "ID пользователя для удаления", example = "1") @RequestParam Long id) {
|
||||
authService.deleteUser(id);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
// DTOs moved to package dto as records
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package kz.konturai.domain;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.Instant;
|
||||
|
||||
@Entity
|
||||
@Table(name = "refresh_tokens")
|
||||
public class RefreshToken {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, unique = true, length = 512)
|
||||
private String token;
|
||||
|
||||
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "user_id")
|
||||
private User user;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Instant expiresAt;
|
||||
|
||||
@Column(nullable = false)
|
||||
private boolean revoked = false;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
public void setToken(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
public Instant getExpiresAt() {
|
||||
return expiresAt;
|
||||
}
|
||||
|
||||
public void setExpiresAt(Instant expiresAt) {
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
|
||||
public boolean isRevoked() {
|
||||
return revoked;
|
||||
}
|
||||
|
||||
public void setRevoked(boolean revoked) {
|
||||
this.revoked = revoked;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package kz.konturai.domain;
|
||||
|
||||
public enum Role {
|
||||
ROLE_ADMIN,
|
||||
ROLE_USER;
|
||||
|
||||
public static boolean isValid(String value) {
|
||||
for (Role r : values()) {
|
||||
if (r.name().equals(value))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package kz.konturai.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.Instant;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
public class User {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, unique = true)
|
||||
private String email;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String passwordHash;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String roles; // comma separated, e.g. "ROLE_USER"
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(nullable = false, updatable = false)
|
||||
private Instant createdAt;
|
||||
|
||||
@UpdateTimestamp
|
||||
@Column(nullable = false)
|
||||
private Instant updatedAt;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getPasswordHash() {
|
||||
return passwordHash;
|
||||
}
|
||||
|
||||
public void setPasswordHash(String passwordHash) {
|
||||
this.passwordHash = passwordHash;
|
||||
}
|
||||
|
||||
public String getRoles() {
|
||||
return roles;
|
||||
}
|
||||
|
||||
public void setRoles(String roles) {
|
||||
this.roles = roles;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public Instant getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package kz.konturai.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
public class AuthDtos {
|
||||
@Schema(description = "Запрос на регистрацию пользователя")
|
||||
public static class SignUpRequest {
|
||||
@Schema(description = "Email пользователя", example = "user@example.com")
|
||||
public String email;
|
||||
@Schema(description = "Пароль пользователя", example = "password123")
|
||||
public String password;
|
||||
}
|
||||
|
||||
@Schema(description = "Запрос на вход в систему")
|
||||
public static class SignInRequest {
|
||||
@Schema(description = "Email пользователя", example = "user@example.com")
|
||||
public String email;
|
||||
@Schema(description = "Пароль пользователя", example = "password123")
|
||||
public String password;
|
||||
}
|
||||
|
||||
@Schema(description = "Ответ с токенами аутентификации")
|
||||
public static class AuthResponse {
|
||||
@Schema(description = "Access токен для авторизации", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
|
||||
public String accessToken;
|
||||
@Schema(description = "Тип токена", example = "Bearer")
|
||||
public String tokenType = "Bearer";
|
||||
@Schema(description = "Refresh токен для обновления access токена", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
|
||||
public String refreshToken;
|
||||
|
||||
public AuthResponse(String accessToken, String refreshToken) {
|
||||
this.accessToken = accessToken;
|
||||
this.refreshToken = refreshToken;
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(description = "Запрос на обновление токена")
|
||||
public static class RefreshRequest {
|
||||
@Schema(description = "Refresh токен для обновления access токена", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
|
||||
public String refreshToken;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package kz.konturai.dto;
|
||||
|
||||
import java.util.List;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "Пагинированный ответ")
|
||||
public class PageResponse<T> {
|
||||
@Schema(description = "Содержимое страницы")
|
||||
public List<T> content;
|
||||
@Schema(description = "Номер текущей страницы", example = "0")
|
||||
public int pageNumber;
|
||||
@Schema(description = "Размер страницы", example = "20")
|
||||
public int pageSize;
|
||||
@Schema(description = "Общее количество элементов", example = "100")
|
||||
public long totalElements;
|
||||
@Schema(description = "Общее количество страниц", example = "5")
|
||||
public int totalPages;
|
||||
@Schema(description = "Является ли это первой страницей", example = "true")
|
||||
public boolean first;
|
||||
@Schema(description = "Является ли это последней страницей", example = "false")
|
||||
public boolean last;
|
||||
@Schema(description = "Количество элементов на текущей странице", example = "20")
|
||||
public int numberOfElements;
|
||||
|
||||
public PageResponse() {
|
||||
}
|
||||
|
||||
public PageResponse(List<T> content, int pageNumber, int pageSize, long totalElements, int totalPages,
|
||||
boolean first, boolean last, int numberOfElements) {
|
||||
this.content = content;
|
||||
this.pageNumber = pageNumber;
|
||||
this.pageSize = pageSize;
|
||||
this.totalElements = totalElements;
|
||||
this.totalPages = totalPages;
|
||||
this.first = first;
|
||||
this.last = last;
|
||||
this.numberOfElements = numberOfElements;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package kz.konturai.dto;
|
||||
|
||||
import java.util.List;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "Запрос на установку ролей пользователя")
|
||||
public record SetRolesRequest(
|
||||
@Schema(description = "Email пользователя", example = "user@example.com") String email,
|
||||
@Schema(description = "Список ролей для установки", example = "[\"ROLE_USER\", \"ROLE_ADMIN\"]") List<String> roles) {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package kz.konturai.dto;
|
||||
|
||||
import java.util.List;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "Запрос на обновление данных пользователя")
|
||||
public record UpdateUserRequest(
|
||||
@Schema(description = "ID пользователя", example = "1") Long id,
|
||||
@Schema(description = "Email пользователя", example = "user@example.com") String email,
|
||||
@Schema(description = "Новый пароль пользователя", example = "newpassword123") String password,
|
||||
@Schema(description = "Список ролей пользователя", example = "[\"ROLE_USER\", \"ROLE_ADMIN\"]") List<String> roles) {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package kz.konturai.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "Краткая информация о пользователе")
|
||||
public record UserSummary(
|
||||
@Schema(description = "ID пользователя", example = "1") Long id,
|
||||
@Schema(description = "Email пользователя", example = "user@example.com") String email,
|
||||
@Schema(description = "Роли пользователя", example = "ROLE_USER,ROLE_ADMIN") String roles) {
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package kz.konturai.repository;
|
||||
|
||||
import java.util.Optional;
|
||||
import kz.konturai.domain.RefreshToken;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
|
||||
Optional<RefreshToken> findByToken(String token);
|
||||
|
||||
long deleteByToken(String token);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package kz.konturai.repository;
|
||||
|
||||
import java.util.Optional;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
||||
import kz.konturai.domain.User;
|
||||
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
boolean existsByEmail(String email);
|
||||
|
||||
Optional<User> findByEmail(String email);
|
||||
|
||||
Page<User> findAll(Pageable pageable);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package kz.konturai.service.impl;
|
||||
|
||||
public interface IAuthService {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package kz.konturai.service.spec;
|
||||
|
||||
import kz.konturai.configuration.security.JwtService;
|
||||
import kz.konturai.domain.RefreshToken;
|
||||
import kz.konturai.domain.User;
|
||||
import kz.konturai.dto.AuthDtos;
|
||||
import kz.konturai.repository.RefreshTokenRepository;
|
||||
import kz.konturai.repository.UserRepository;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
public class AuthService {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final JwtService jwtService;
|
||||
private final RefreshTokenRepository refreshTokenRepository;
|
||||
|
||||
public AuthService(UserRepository userRepository, PasswordEncoder passwordEncoder, JwtService jwtService,
|
||||
RefreshTokenRepository refreshTokenRepository) {
|
||||
this.userRepository = userRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.jwtService = jwtService;
|
||||
this.refreshTokenRepository = refreshTokenRepository;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void signUp(AuthDtos.SignUpRequest request) {
|
||||
if (userRepository.existsByEmail(request.email)) {
|
||||
throw new IllegalArgumentException("Email already registered");
|
||||
}
|
||||
User user = new User();
|
||||
user.setEmail(request.email.trim().toLowerCase());
|
||||
user.setPasswordHash(passwordEncoder.encode(request.password));
|
||||
user.setRoles("ROLE_USER");
|
||||
userRepository.save(user);
|
||||
}
|
||||
|
||||
public AuthDtos.AuthResponse signIn(AuthDtos.SignInRequest request) {
|
||||
User user = userRepository.findByEmail(request.email.trim().toLowerCase())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid credentials"));
|
||||
if (!passwordEncoder.matches(request.password, user.getPasswordHash())) {
|
||||
throw new IllegalArgumentException("Invalid credentials");
|
||||
}
|
||||
String access = jwtService.generateToken(user.getEmail(),
|
||||
java.util.Map.of("uid", user.getId(), "roles", user.getRoles()));
|
||||
String refresh = issueRefreshToken(user);
|
||||
return new AuthDtos.AuthResponse(access, refresh);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public AuthDtos.AuthResponse refresh(AuthDtos.RefreshRequest request) {
|
||||
RefreshToken rt = refreshTokenRepository.findByToken(request.refreshToken)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid refresh token"));
|
||||
if (rt.isRevoked() || rt.getExpiresAt().isBefore(java.time.Instant.now())) {
|
||||
throw new IllegalArgumentException("Invalid refresh token");
|
||||
}
|
||||
User user = rt.getUser();
|
||||
// rotate
|
||||
rt.setRevoked(true);
|
||||
refreshTokenRepository.save(rt);
|
||||
String newRefresh = issueRefreshToken(user);
|
||||
String newAccess = jwtService.generateToken(user.getEmail(),
|
||||
java.util.Map.of("uid", user.getId(), "roles", user.getRoles()));
|
||||
return new AuthDtos.AuthResponse(newAccess, newRefresh);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void logout(String refreshToken) {
|
||||
refreshTokenRepository.findByToken(refreshToken)
|
||||
.ifPresent(rt -> {
|
||||
rt.setRevoked(true);
|
||||
refreshTokenRepository.save(rt);
|
||||
});
|
||||
}
|
||||
|
||||
private String issueRefreshToken(User user) {
|
||||
String token = java.util.UUID.randomUUID().toString();
|
||||
RefreshToken rt = new RefreshToken();
|
||||
rt.setUser(user);
|
||||
rt.setToken(token);
|
||||
long ttlDays = Long.parseLong(System.getProperty("security.refresh.ttl-days", "14"));
|
||||
rt.setExpiresAt(java.time.Instant.now().plus(java.time.Duration.ofDays(ttlDays)));
|
||||
refreshTokenRepository.save(rt);
|
||||
return token;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void setUserRoles(String email, java.util.List<String> roles) {
|
||||
for (String r : roles) {
|
||||
if (!kz.konturai.domain.Role.isValid(r)) {
|
||||
throw new IllegalArgumentException("Invalid role: " + r);
|
||||
}
|
||||
}
|
||||
User user = userRepository.findByEmail(email.trim().toLowerCase())
|
||||
.orElseThrow(() -> new IllegalArgumentException("User not found"));
|
||||
user.setRoles(String.join(",", roles));
|
||||
userRepository.save(user);
|
||||
}
|
||||
|
||||
public Page<User> listUsers(Pageable pageable) {
|
||||
return userRepository.findAll(pageable);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void updateUser(Long id, String email, String password, java.util.List<String> roles) {
|
||||
User user = userRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("User not found"));
|
||||
if (email != null && !email.isBlank()) {
|
||||
user.setEmail(email.trim().toLowerCase());
|
||||
}
|
||||
if (password != null && !password.isBlank()) {
|
||||
user.setPasswordHash(passwordEncoder.encode(password));
|
||||
}
|
||||
if (roles != null && !roles.isEmpty()) {
|
||||
for (String r : roles) {
|
||||
if (!kz.konturai.domain.Role.isValid(r)) {
|
||||
throw new IllegalArgumentException("Invalid role: " + r);
|
||||
}
|
||||
}
|
||||
user.setRoles(String.join(",", roles));
|
||||
}
|
||||
userRepository.save(user);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteUser(Long id) {
|
||||
userRepository.deleteById(id);
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
spring.application.name=konturai
|
||||
@@ -0,0 +1,18 @@
|
||||
spring:
|
||||
application:
|
||||
name: konturai
|
||||
datasource:
|
||||
url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://92.38.48.166:5433/konturai}
|
||||
username: ${POSTGRES_USER:postgres}
|
||||
password: ${POSTGRES_PASSWORD:password}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: ${SPRING_JPA_HIBERNATE_DDL_AUTO:none}
|
||||
show-sql: true
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
sql:
|
||||
init:
|
||||
mode: never
|
||||
@@ -0,0 +1,25 @@
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
roles VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||
|
||||
CREATE OR REPLACE FUNCTION set_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_users_updated_at ON users;
|
||||
CREATE TRIGGER trg_users_updated_at
|
||||
BEFORE UPDATE ON users
|
||||
FOR EACH ROW EXECUTE PROCEDURE set_updated_at();
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
CREATE TABLE IF NOT EXISTS refresh_tokens (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
token VARCHAR(512) NOT NULL UNIQUE,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
revoked BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_token ON refresh_tokens(token);
|
||||
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user ON refresh_tokens(user_id);
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
-- Seed a root admin user if not exists
|
||||
-- Uses pgcrypto to generate a bcrypt hash for the initial password
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM users WHERE email = 'root@konturai.local') THEN
|
||||
INSERT INTO users (email, password_hash, roles, created_at, updated_at)
|
||||
VALUES (
|
||||
'root@konturai.local',
|
||||
crypt('ChangeMe123!', gen_salt('bf', 10)),
|
||||
'ROLE_ADMIN,ROLE_USER',
|
||||
NOW(), NOW()
|
||||
);
|
||||
END IF;
|
||||
END$$;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user