From bf19607a07054efbb0aa379ab83ea2b8752743ce Mon Sep 17 00:00:00 2001 From: Jose Date: Sat, 7 Mar 2026 21:46:28 +0100 Subject: [PATCH] feat: Refactor UserController to use UserMapper for DTO conversions feat: Add ApiErrorDto and ApiValidationErrorDto for standardized error responses feat: Implement ChangeAvatarRequest, ChangePasswordRequest, ChangeRoleRequest, ChangeStatusRequest, LoginRequest, LoginResponse, and RegisterRequest DTOs feat: Create custom exceptions for better error handling: BadRequestException, ConflictException, ForbiddenException, NotFoundException, UnauthorizedException, and ValidationException feat: Add RequestLoggingFilter for logging incoming requests feat: Implement DevCorsConfig for CORS settings in development environment feat: Create GlobalExceptionHandler for centralized exception handling feat: Implement RestAccessDeniedHandler and RestAuthEntryPoint for handling access denied and unauthorized requests feat: Create CustomPrincipal for Spring Security user details feat: Implement JwtFilter and JwtService for JWT authentication feat: Add PasswordGenerator for secure password generation feat: Implement ServiceAuthFilter for service-level authentication feat: Create AuthService for handling authentication and password changes refactor: Update CustomUserDetailsService to use CustomPrincipal refactor: Update UserService to improve user management and exception handling fix: Update frontend API base URL in development and production settings refactor: Clean up Header component and associated CSS --- backend/pom.xml | 29 ++--- .../backend/config/SecurityConfig.java | 56 +++++++-- .../backend/controller/AuthController.java | 108 ++++++++++++++++++ .../backend/controller/UserController.java | 34 ++++-- .../backend/dto/ApiErrorDto.java | 65 +++++++++++ .../backend/dto/ApiValidationErrorDto.java | 57 +++++++++ .../backend/dto/ChangeAvatarRequest.java | 3 + .../backend/dto/ChangePasswordRequest.java | 9 ++ .../backend/dto/ChangeRoleRequest.java | 6 + .../backend/dto/ChangeStatusRequest.java | 5 + .../backend/dto/LoginRequest.java | 6 + .../backend/dto/LoginResponse.java | 6 + .../backend/dto/RegisterRequest.java | 8 ++ .../exception/BadRequestException.java | 7 ++ .../backend/exception/ConflictException.java | 7 ++ .../backend/exception/ForbiddenException.java | 7 ++ .../backend/exception/NotFoundException.java | 5 + .../exception/UnauthorizedException.java | 7 ++ .../exception/ValidationException.java | 21 ++++ .../backend/filter/RequestLoggingFilter.java | 41 +++++++ .../backend/http/DevCorsConfig.java | 27 +++++ .../backend/http/GlobalExceptionHandler.java | 106 +++++++++++++++++ .../backend/http/RestAccessDeniedHandler.java | 31 +++++ .../backend/http/RestAuthEntryPoint.java | 34 ++++++ ...mUserDetails.java => CustomPrincipal.java} | 5 +- .../backend/security/JwtFilter.java | 59 ++++++++++ .../backend/security/JwtService.java | 101 ++++++++++++++++ .../backend/security/PasswordGenerator.java | 45 ++++++++ .../backend/security/ServiceAuthFilter.java | 45 ++++++++ .../backend/service/AuthService.java | 59 ++++++++++ .../service/CustomUserDetailsService.java | 4 +- .../backend/service/UserService.java | 62 ++++++---- frontend/public/config/settings.dev.json | 2 +- frontend/public/config/settings.prod.json | 2 +- frontend/src/components/Header.jsx | 6 - frontend/src/css/Header.css | 22 ---- 36 files changed, 1010 insertions(+), 87 deletions(-) create mode 100644 backend/src/main/java/es/adeptusminiaturium/backend/controller/AuthController.java create mode 100644 backend/src/main/java/es/adeptusminiaturium/backend/dto/ApiErrorDto.java create mode 100644 backend/src/main/java/es/adeptusminiaturium/backend/dto/ApiValidationErrorDto.java create mode 100644 backend/src/main/java/es/adeptusminiaturium/backend/dto/ChangeAvatarRequest.java create mode 100644 backend/src/main/java/es/adeptusminiaturium/backend/dto/ChangePasswordRequest.java create mode 100644 backend/src/main/java/es/adeptusminiaturium/backend/dto/ChangeRoleRequest.java create mode 100644 backend/src/main/java/es/adeptusminiaturium/backend/dto/ChangeStatusRequest.java create mode 100644 backend/src/main/java/es/adeptusminiaturium/backend/dto/LoginRequest.java create mode 100644 backend/src/main/java/es/adeptusminiaturium/backend/dto/LoginResponse.java create mode 100644 backend/src/main/java/es/adeptusminiaturium/backend/dto/RegisterRequest.java create mode 100644 backend/src/main/java/es/adeptusminiaturium/backend/exception/BadRequestException.java create mode 100644 backend/src/main/java/es/adeptusminiaturium/backend/exception/ConflictException.java create mode 100644 backend/src/main/java/es/adeptusminiaturium/backend/exception/ForbiddenException.java create mode 100644 backend/src/main/java/es/adeptusminiaturium/backend/exception/NotFoundException.java create mode 100644 backend/src/main/java/es/adeptusminiaturium/backend/exception/UnauthorizedException.java create mode 100644 backend/src/main/java/es/adeptusminiaturium/backend/exception/ValidationException.java create mode 100644 backend/src/main/java/es/adeptusminiaturium/backend/filter/RequestLoggingFilter.java create mode 100644 backend/src/main/java/es/adeptusminiaturium/backend/http/DevCorsConfig.java create mode 100644 backend/src/main/java/es/adeptusminiaturium/backend/http/GlobalExceptionHandler.java create mode 100644 backend/src/main/java/es/adeptusminiaturium/backend/http/RestAccessDeniedHandler.java create mode 100644 backend/src/main/java/es/adeptusminiaturium/backend/http/RestAuthEntryPoint.java rename backend/src/main/java/es/adeptusminiaturium/backend/security/{CustomUserDetails.java => CustomPrincipal.java} (86%) create mode 100644 backend/src/main/java/es/adeptusminiaturium/backend/security/JwtFilter.java create mode 100644 backend/src/main/java/es/adeptusminiaturium/backend/security/JwtService.java create mode 100644 backend/src/main/java/es/adeptusminiaturium/backend/security/PasswordGenerator.java create mode 100644 backend/src/main/java/es/adeptusminiaturium/backend/security/ServiceAuthFilter.java create mode 100644 backend/src/main/java/es/adeptusminiaturium/backend/service/AuthService.java diff --git a/backend/pom.xml b/backend/pom.xml index 6145bb8..4414ffa 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -23,13 +23,6 @@ 25 - - - gitea - https://git.miarma.net/api/packages/Gallardo7761/maven - - - @@ -49,6 +42,22 @@ mariadb-java-client runtime + + jakarta.validation + jakarta.validation-api + 3.1.1 + + + org.hibernate.validator + hibernate-validator + 8.0.0.Final + + + org.projectlombok + lombok + 1.18.42 + compile + io.jsonwebtoken @@ -67,12 +76,6 @@ 0.11.5 runtime - - net.miarma - backlib - 1.1.0 - compile - diff --git a/backend/src/main/java/es/adeptusminiaturium/backend/config/SecurityConfig.java b/backend/src/main/java/es/adeptusminiaturium/backend/config/SecurityConfig.java index 5513e38..c9b5ac7 100644 --- a/backend/src/main/java/es/adeptusminiaturium/backend/config/SecurityConfig.java +++ b/backend/src/main/java/es/adeptusminiaturium/backend/config/SecurityConfig.java @@ -1,36 +1,67 @@ package es.adeptusminiaturium.backend.config; +import es.adeptusminiaturium.backend.http.RestAccessDeniedHandler; +import es.adeptusminiaturium.backend.http.RestAuthEntryPoint; +import es.adeptusminiaturium.backend.security.JwtFilter; +import es.adeptusminiaturium.backend.security.JwtService; import es.adeptusminiaturium.backend.service.CustomUserDetailsService; +import es.adeptusminiaturium.backend.service.UserService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.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.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfigurationSource; + +import java.util.Optional; @Configuration +@EnableWebSecurity +@EnableMethodSecurity(prePostEnabled = true) public class SecurityConfig { - + private final RestAuthEntryPoint authEntryPoint; + private final RestAccessDeniedHandler accessDeniedHandler; + private final CorsConfigurationSource corsConfigurationSource; private final CustomUserDetailsService userDetailsService; - public SecurityConfig(CustomUserDetailsService userDetailsService) { + public SecurityConfig(CustomUserDetailsService userDetailsService, + RestAuthEntryPoint authEntryPoint, + RestAccessDeniedHandler accessDeniedHandler, + Optional corsConfigurationSource) { + this.authEntryPoint = authEntryPoint; + this.accessDeniedHandler = accessDeniedHandler; + this.corsConfigurationSource = corsConfigurationSource.orElse(null); this.userDetailsService = userDetailsService; } @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + public JwtFilter jwtFilter(JwtService jwtService, UserService userService) { + return new JwtFilter(jwtService, userService); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http, JwtFilter jwtFilter) throws Exception { http - .csrf(Customizer.withDefaults()) - .authorizeHttpRequests(auth -> auth - //.requestMatchers("/admin/**").hasRole("ADMIN") - //.requestMatchers("/posts/**").authenticated() - .anyRequest().permitAll() + .csrf(csrf -> csrf.disable()) + .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .exceptionHandling(ex -> ex + .authenticationEntryPoint(authEntryPoint) + .accessDeniedHandler(accessDeniedHandler) ) - .formLogin(Customizer.withDefaults()) - .httpBasic(Customizer.withDefaults()); + .authorizeHttpRequests(auth -> auth + .requestMatchers("/auth/login").permitAll() + .anyRequest().authenticated() + ); + + http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } @@ -50,4 +81,9 @@ public class SecurityConfig { return authBuilder.build(); } + + @Bean + public JwtService jwtService() { + return new JwtService(); + } } \ No newline at end of file diff --git a/backend/src/main/java/es/adeptusminiaturium/backend/controller/AuthController.java b/backend/src/main/java/es/adeptusminiaturium/backend/controller/AuthController.java new file mode 100644 index 0000000..f784311 --- /dev/null +++ b/backend/src/main/java/es/adeptusminiaturium/backend/controller/AuthController.java @@ -0,0 +1,108 @@ +package es.adeptusminiaturium.backend.controller; + +import es.adeptusminiaturium.backend.dto.ApiErrorDto; +import es.adeptusminiaturium.backend.dto.ChangePasswordRequest; +import es.adeptusminiaturium.backend.dto.LoginRequest; +import es.adeptusminiaturium.backend.dto.LoginResponse; +import es.adeptusminiaturium.backend.security.JwtService; +import es.adeptusminiaturium.backend.service.AuthService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; +import java.util.UUID; + +@RestController +@RequestMapping("/auth") +public class AuthController { + private final JwtService jwtService; + private final AuthService authService; + + public AuthController(JwtService jwtService, AuthService authService) { + this.jwtService = jwtService; + this.authService = authService; + } + + @PostMapping("/login") + public ResponseEntity login(@RequestBody LoginRequest request) { + LoginResponse response = authService.login(request); + return ResponseEntity.ok( + new LoginResponse(response.token(), response.user()) + ); + } + + @GetMapping("/refresh") + public ResponseEntity refreshToken(@RequestHeader("Authorization") String authHeader) { + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + return ResponseEntity.status(401).body( + new ApiErrorDto( + 401, + "Unauthorized", + "No token", + "/api/auth/change-password" + ) + ); + } + + String token = authHeader.substring(7); + if (!jwtService.validateToken(token)) { + return ResponseEntity.status(401).body( + new ApiErrorDto( + 401, + "Unauthorized", + "Invalid token", + "/api/auth/change-password" + ) + ); + } + + UUID userId = jwtService.getUserId(token); + String newToken = jwtService.generateToken(userId); + + return ResponseEntity.ok(Map.of( + "token", newToken, + "userId", userId + )); + } + + @PostMapping("/change-password") + public ResponseEntity changePassword( + @RequestHeader("Authorization") String authHeader, + @RequestBody ChangePasswordRequest request + ) { + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + return ResponseEntity.status(401).body( + new ApiErrorDto( + 401, + "Unauthorized", + "No hay token", + "/api/auth/change-password" + ) + ); + } + + String token = authHeader.substring(7); + if (!jwtService.validateToken(token)) { + return ResponseEntity.status(401).body( + new ApiErrorDto( + 401, + "Unauthorized", + "Invalid token", + "/api/auth/change-password" + ) + ); + } + + UUID userId = jwtService.getUserId(token); + + authService.changePassword(userId, request); + return ResponseEntity.ok(Map.of("message", "Contraseña cambiada correctamente")); + } + + + @GetMapping("/validate") + public ResponseEntity validate(@RequestHeader("Authorization") String authHeader) { + String token = authHeader.substring(7); + return ResponseEntity.ok(jwtService.validateToken(token)); + } +} diff --git a/backend/src/main/java/es/adeptusminiaturium/backend/controller/UserController.java b/backend/src/main/java/es/adeptusminiaturium/backend/controller/UserController.java index 1ec4f15..bee79ad 100644 --- a/backend/src/main/java/es/adeptusminiaturium/backend/controller/UserController.java +++ b/backend/src/main/java/es/adeptusminiaturium/backend/controller/UserController.java @@ -1,42 +1,62 @@ package es.adeptusminiaturium.backend.controller; +import es.adeptusminiaturium.backend.mapper.UserMapper; +import es.adeptusminiaturium.backend.model.User; import es.adeptusminiaturium.backend.service.UserService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.UUID; +import java.util.stream.Collectors; import es.adeptusminiaturium.backend.dto.UserDto; import org.springframework.web.bind.annotation.*; - @RestController @RequestMapping("/api/users") public class UserController { private final UserService service; - public UserController(UserService service) { this.service = service; } + public UserController(UserService service) { + this.service = service; + } @GetMapping public ResponseEntity> getAll() { - return ResponseEntity.ok(service.getAllUsers()); + List users = service.getAllUsers() + .stream() + .map(UserMapper::toResponse) + .collect(Collectors.toList()); + + return ResponseEntity.ok(users); } @GetMapping("/{id}") public ResponseEntity getOne(@PathVariable UUID id) { - return ResponseEntity.ofNullable(service.getUser(id)); + User user = service.getUser(id); + return ResponseEntity.ok(UserMapper.toResponse(user)); } @PostMapping public ResponseEntity create(@RequestBody UserDto.Request dto) { - return ResponseEntity.ok(service.createUser(dto)); + + User user = UserMapper.toEntity(dto); + User saved = service.createUser(user); + + return ResponseEntity.ok(UserMapper.toResponse(saved)); } @PutMapping("/{id}") - public ResponseEntity update(@PathVariable UUID id, @RequestBody UserDto.Request dto) { - return ResponseEntity.ofNullable(service.updateUser(id, dto)); + public ResponseEntity update( + @PathVariable UUID id, + @RequestBody UserDto.Request dto) { + + User user = UserMapper.toEntity(dto); + User updated = service.updateUser(id, user); + + return ResponseEntity.ok(UserMapper.toResponse(updated)); } @DeleteMapping("/{id}") diff --git a/backend/src/main/java/es/adeptusminiaturium/backend/dto/ApiErrorDto.java b/backend/src/main/java/es/adeptusminiaturium/backend/dto/ApiErrorDto.java new file mode 100644 index 0000000..1479244 --- /dev/null +++ b/backend/src/main/java/es/adeptusminiaturium/backend/dto/ApiErrorDto.java @@ -0,0 +1,65 @@ +package es.adeptusminiaturium.backend.dto; + +import tools.jackson.databind.ObjectMapper; + +import java.time.Instant; + +public class ApiErrorDto { + private int status; + private String error; + private String message; + private String path; + private Instant timestamp; + + public ApiErrorDto(int status, String error, String message, String path) { + this.status = status; + this.error = error; + this.message = message; + this.path = path; + this.timestamp = Instant.now(); + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public Instant getTimestamp() { + return timestamp; + } + + public void setTimestamp(Instant timestamp) { + this.timestamp = timestamp; + } + + public String toJson() { + return new ObjectMapper().writeValueAsString(this); + } +} diff --git a/backend/src/main/java/es/adeptusminiaturium/backend/dto/ApiValidationErrorDto.java b/backend/src/main/java/es/adeptusminiaturium/backend/dto/ApiValidationErrorDto.java new file mode 100644 index 0000000..0d687f9 --- /dev/null +++ b/backend/src/main/java/es/adeptusminiaturium/backend/dto/ApiValidationErrorDto.java @@ -0,0 +1,57 @@ +package es.adeptusminiaturium.backend.dto; + +import tools.jackson.databind.ObjectMapper; + +import java.time.Instant; +import java.util.List; +import java.util.Map; + +public class ApiValidationErrorDto { + private int status; + private Map errors; + private String path; + private Instant timestamp; + + public ApiValidationErrorDto(Map errors, String path) { + this.status = 422; + this.errors = errors; + this.path = path; + this.timestamp = Instant.now(); + } + + public Map getErrors() { + return errors; + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public void setErrors(Map errors) { + this.errors = errors; + } + + public Instant getTimestamp() { + return timestamp; + } + + public void setTimestamp(Instant timestamp) { + this.timestamp = timestamp; + } + + public String toJson() { + return new ObjectMapper().writeValueAsString(this); + } +} diff --git a/backend/src/main/java/es/adeptusminiaturium/backend/dto/ChangeAvatarRequest.java b/backend/src/main/java/es/adeptusminiaturium/backend/dto/ChangeAvatarRequest.java new file mode 100644 index 0000000..a91fd70 --- /dev/null +++ b/backend/src/main/java/es/adeptusminiaturium/backend/dto/ChangeAvatarRequest.java @@ -0,0 +1,3 @@ +package es.adeptusminiaturium.backend.dto; + +public record ChangeAvatarRequest(String avatarUrl) {} diff --git a/backend/src/main/java/es/adeptusminiaturium/backend/dto/ChangePasswordRequest.java b/backend/src/main/java/es/adeptusminiaturium/backend/dto/ChangePasswordRequest.java new file mode 100644 index 0000000..2e8db27 --- /dev/null +++ b/backend/src/main/java/es/adeptusminiaturium/backend/dto/ChangePasswordRequest.java @@ -0,0 +1,9 @@ +package es.adeptusminiaturium.backend.dto; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record ChangePasswordRequest(@NotBlank String oldPassword, + @NotBlank String newPassword) {} + diff --git a/backend/src/main/java/es/adeptusminiaturium/backend/dto/ChangeRoleRequest.java b/backend/src/main/java/es/adeptusminiaturium/backend/dto/ChangeRoleRequest.java new file mode 100644 index 0000000..d0a831a --- /dev/null +++ b/backend/src/main/java/es/adeptusminiaturium/backend/dto/ChangeRoleRequest.java @@ -0,0 +1,6 @@ +package es.adeptusminiaturium.backend.dto; + +import es.adeptusminiaturium.backend.enums.UserRole; + +public record ChangeRoleRequest(UserRole role) { +} diff --git a/backend/src/main/java/es/adeptusminiaturium/backend/dto/ChangeStatusRequest.java b/backend/src/main/java/es/adeptusminiaturium/backend/dto/ChangeStatusRequest.java new file mode 100644 index 0000000..86d4570 --- /dev/null +++ b/backend/src/main/java/es/adeptusminiaturium/backend/dto/ChangeStatusRequest.java @@ -0,0 +1,5 @@ +package es.adeptusminiaturium.backend.dto; + +import es.adeptusminiaturium.backend.enums.UserStatus; + +public record ChangeStatusRequest(UserStatus status) {} diff --git a/backend/src/main/java/es/adeptusminiaturium/backend/dto/LoginRequest.java b/backend/src/main/java/es/adeptusminiaturium/backend/dto/LoginRequest.java new file mode 100644 index 0000000..a36bc70 --- /dev/null +++ b/backend/src/main/java/es/adeptusminiaturium/backend/dto/LoginRequest.java @@ -0,0 +1,6 @@ +package es.adeptusminiaturium.backend.dto; + +import jakarta.validation.constraints.NotBlank; + +public record LoginRequest(@NotBlank String userName, + @NotBlank String password) {} diff --git a/backend/src/main/java/es/adeptusminiaturium/backend/dto/LoginResponse.java b/backend/src/main/java/es/adeptusminiaturium/backend/dto/LoginResponse.java new file mode 100644 index 0000000..d8c46e5 --- /dev/null +++ b/backend/src/main/java/es/adeptusminiaturium/backend/dto/LoginResponse.java @@ -0,0 +1,6 @@ +package es.adeptusminiaturium.backend.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record LoginResponse(@JsonProperty("token") String token, + UserDto.Response user) {} \ No newline at end of file diff --git a/backend/src/main/java/es/adeptusminiaturium/backend/dto/RegisterRequest.java b/backend/src/main/java/es/adeptusminiaturium/backend/dto/RegisterRequest.java new file mode 100644 index 0000000..a7fb876 --- /dev/null +++ b/backend/src/main/java/es/adeptusminiaturium/backend/dto/RegisterRequest.java @@ -0,0 +1,8 @@ +package es.adeptusminiaturium.backend.dto; + +import jakarta.validation.constraints.NotBlank; + +public record RegisterRequest(String displayName, + @NotBlank String username, + @NotBlank String email, + @NotBlank String password) {} \ No newline at end of file diff --git a/backend/src/main/java/es/adeptusminiaturium/backend/exception/BadRequestException.java b/backend/src/main/java/es/adeptusminiaturium/backend/exception/BadRequestException.java new file mode 100644 index 0000000..da98ae9 --- /dev/null +++ b/backend/src/main/java/es/adeptusminiaturium/backend/exception/BadRequestException.java @@ -0,0 +1,7 @@ +package es.adeptusminiaturium.backend.exception; + +public class BadRequestException extends RuntimeException { + public BadRequestException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/es/adeptusminiaturium/backend/exception/ConflictException.java b/backend/src/main/java/es/adeptusminiaturium/backend/exception/ConflictException.java new file mode 100644 index 0000000..4c32dae --- /dev/null +++ b/backend/src/main/java/es/adeptusminiaturium/backend/exception/ConflictException.java @@ -0,0 +1,7 @@ +package es.adeptusminiaturium.backend.exception; + +public class ConflictException extends RuntimeException { + public ConflictException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/es/adeptusminiaturium/backend/exception/ForbiddenException.java b/backend/src/main/java/es/adeptusminiaturium/backend/exception/ForbiddenException.java new file mode 100644 index 0000000..b41e560 --- /dev/null +++ b/backend/src/main/java/es/adeptusminiaturium/backend/exception/ForbiddenException.java @@ -0,0 +1,7 @@ +package es.adeptusminiaturium.backend.exception; + +public class ForbiddenException extends RuntimeException { + public ForbiddenException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/es/adeptusminiaturium/backend/exception/NotFoundException.java b/backend/src/main/java/es/adeptusminiaturium/backend/exception/NotFoundException.java new file mode 100644 index 0000000..7d6211b --- /dev/null +++ b/backend/src/main/java/es/adeptusminiaturium/backend/exception/NotFoundException.java @@ -0,0 +1,5 @@ +package es.adeptusminiaturium.backend.exception; + +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { super(message); } +} diff --git a/backend/src/main/java/es/adeptusminiaturium/backend/exception/UnauthorizedException.java b/backend/src/main/java/es/adeptusminiaturium/backend/exception/UnauthorizedException.java new file mode 100644 index 0000000..abd1de6 --- /dev/null +++ b/backend/src/main/java/es/adeptusminiaturium/backend/exception/UnauthorizedException.java @@ -0,0 +1,7 @@ +package es.adeptusminiaturium.backend.exception; + +public class UnauthorizedException extends RuntimeException { + public UnauthorizedException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/es/adeptusminiaturium/backend/exception/ValidationException.java b/backend/src/main/java/es/adeptusminiaturium/backend/exception/ValidationException.java new file mode 100644 index 0000000..d1b3e80 --- /dev/null +++ b/backend/src/main/java/es/adeptusminiaturium/backend/exception/ValidationException.java @@ -0,0 +1,21 @@ +package es.adeptusminiaturium.backend.exception; + +public class ValidationException extends RuntimeException { + private final String field; + private final String message; + + public ValidationException(String field, String message) { + super(message); + this.field = field; + this.message = message; + } + + public String getField() { + return field; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/backend/src/main/java/es/adeptusminiaturium/backend/filter/RequestLoggingFilter.java b/backend/src/main/java/es/adeptusminiaturium/backend/filter/RequestLoggingFilter.java new file mode 100644 index 0000000..259fa81 --- /dev/null +++ b/backend/src/main/java/es/adeptusminiaturium/backend/filter/RequestLoggingFilter.java @@ -0,0 +1,41 @@ +package es.adeptusminiaturium.backend.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +public class RequestLoggingFilter extends OncePerRequestFilter { + + private static final Logger log = LoggerFactory.getLogger(RequestLoggingFilter.class); + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + + long start = System.currentTimeMillis(); + + filterChain.doFilter(request, response); + + long duration = System.currentTimeMillis() - start; + + log.info("({}) {} {} -> {} ({} ms)", + request.getRemoteAddr(), + request.getMethod(), + request.getRequestURI(), + response.getStatus(), + duration + ); + } +} diff --git a/backend/src/main/java/es/adeptusminiaturium/backend/http/DevCorsConfig.java b/backend/src/main/java/es/adeptusminiaturium/backend/http/DevCorsConfig.java new file mode 100644 index 0000000..072089f --- /dev/null +++ b/backend/src/main/java/es/adeptusminiaturium/backend/http/DevCorsConfig.java @@ -0,0 +1,27 @@ +package es.adeptusminiaturium.backend.http; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@Profile("dev") // esto asegura que solo se cargue en dev +public class DevCorsConfig { + + @Bean + public WebMvcConfigurer corsConfigurer() { + return new WebMvcConfigurer() { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("http://localhost:3000") // tu frontend React + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowCredentials(true) + .allowedHeaders("*"); + } + }; + } +} + diff --git a/backend/src/main/java/es/adeptusminiaturium/backend/http/GlobalExceptionHandler.java b/backend/src/main/java/es/adeptusminiaturium/backend/http/GlobalExceptionHandler.java new file mode 100644 index 0000000..09cc851 --- /dev/null +++ b/backend/src/main/java/es/adeptusminiaturium/backend/http/GlobalExceptionHandler.java @@ -0,0 +1,106 @@ +package es.adeptusminiaturium.backend.http; + +import es.adeptusminiaturium.backend.dto.ApiErrorDto; +import es.adeptusminiaturium.backend.dto.ApiValidationErrorDto; +import es.adeptusminiaturium.backend.exception.*; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.Map; + +@RestControllerAdvice +public class GlobalExceptionHandler { + @ExceptionHandler(NotFoundException.class) + public ResponseEntity handleNotFound( + NotFoundException ex, HttpServletRequest req) { + + ApiErrorDto error = new ApiErrorDto( + HttpStatus.NOT_FOUND.value(), + HttpStatus.NOT_FOUND.getReasonPhrase(), + ex.getMessage(), + req.getRequestURI() + ); + + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error); + } + + @ExceptionHandler(BadRequestException.class) + public ResponseEntity handleBadRequest( + BadRequestException ex, HttpServletRequest req) { + + ApiErrorDto error = new ApiErrorDto( + HttpStatus.BAD_REQUEST.value(), + HttpStatus.BAD_REQUEST.getReasonPhrase(), + ex.getMessage(), + req.getRequestURI() + ); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error); + } + + @ExceptionHandler(UnauthorizedException.class) + public ResponseEntity handleUnauthorized( + UnauthorizedException ex, HttpServletRequest req) { + + ApiErrorDto error = new ApiErrorDto( + HttpStatus.UNAUTHORIZED.value(), + HttpStatus.UNAUTHORIZED.getReasonPhrase(), + ex.getMessage(), + req.getRequestURI() + ); + + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error); + } + + @ExceptionHandler(ForbiddenException.class) + public ResponseEntity handleForbidden( + ForbiddenException ex, HttpServletRequest req) { + + ApiErrorDto error = new ApiErrorDto( + HttpStatus.FORBIDDEN.value(), + HttpStatus.FORBIDDEN.getReasonPhrase(), + ex.getMessage(), + req.getRequestURI() + ); + + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error); + } + + @ExceptionHandler(ConflictException.class) + public ResponseEntity handleConflict( + ConflictException ex, HttpServletRequest req) { + + ApiErrorDto error = new ApiErrorDto( + HttpStatus.CONFLICT.value(), + HttpStatus.CONFLICT.getReasonPhrase(), + ex.getMessage(), + req.getRequestURI() + ); + + return ResponseEntity.status(HttpStatus.CONFLICT).body(error); + } + + @ExceptionHandler(ValidationException.class) + public ResponseEntity handleValidation( + ValidationException ex, HttpServletRequest req) { + Map errors = Map.of(ex.getField(), ex.getMessage()); + return ResponseEntity.status(HttpStatus.UNPROCESSABLE_CONTENT).body(new ApiValidationErrorDto(errors, req.getRequestURI())); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleAll( + Exception ex, HttpServletRequest req) { + + ApiErrorDto error = new ApiErrorDto( + HttpStatus.INTERNAL_SERVER_ERROR.value(), + HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), + "Internal server error", + req.getRequestURI() + ); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); + } +} diff --git a/backend/src/main/java/es/adeptusminiaturium/backend/http/RestAccessDeniedHandler.java b/backend/src/main/java/es/adeptusminiaturium/backend/http/RestAccessDeniedHandler.java new file mode 100644 index 0000000..346e628 --- /dev/null +++ b/backend/src/main/java/es/adeptusminiaturium/backend/http/RestAccessDeniedHandler.java @@ -0,0 +1,31 @@ +package es.adeptusminiaturium.backend.http; + +import es.adeptusminiaturium.backend.dto.ApiErrorDto; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class RestAccessDeniedHandler implements AccessDeniedHandler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, + AccessDeniedException accessDeniedException) throws IOException { + + ApiErrorDto error = new ApiErrorDto( + HttpStatus.FORBIDDEN.value(), + HttpStatus.FORBIDDEN.getReasonPhrase(), + "Forbidden", + request.getRequestURI() + ); + + response.setStatus(HttpStatus.FORBIDDEN.value()); + response.setContentType("application/json"); + response.getWriter().write(error.toJson()); + } +} \ No newline at end of file diff --git a/backend/src/main/java/es/adeptusminiaturium/backend/http/RestAuthEntryPoint.java b/backend/src/main/java/es/adeptusminiaturium/backend/http/RestAuthEntryPoint.java new file mode 100644 index 0000000..48dfacf --- /dev/null +++ b/backend/src/main/java/es/adeptusminiaturium/backend/http/RestAuthEntryPoint.java @@ -0,0 +1,34 @@ +package es.adeptusminiaturium.backend.http; + +import es.adeptusminiaturium.backend.dto.ApiErrorDto; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class RestAuthEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) throws IOException { + + ApiErrorDto error = new ApiErrorDto( + HttpStatus.UNAUTHORIZED.value(), + HttpStatus.UNAUTHORIZED.getReasonPhrase(), + "Unauthorized", + request.getRequestURI() + ); + + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.setContentType("application/json"); + response.getWriter().write(error.toJson()); + } +} + diff --git a/backend/src/main/java/es/adeptusminiaturium/backend/security/CustomUserDetails.java b/backend/src/main/java/es/adeptusminiaturium/backend/security/CustomPrincipal.java similarity index 86% rename from backend/src/main/java/es/adeptusminiaturium/backend/security/CustomUserDetails.java rename to backend/src/main/java/es/adeptusminiaturium/backend/security/CustomPrincipal.java index ef6a0ad..673398c 100644 --- a/backend/src/main/java/es/adeptusminiaturium/backend/security/CustomUserDetails.java +++ b/backend/src/main/java/es/adeptusminiaturium/backend/security/CustomPrincipal.java @@ -2,7 +2,6 @@ package es.adeptusminiaturium.backend.security; import es.adeptusminiaturium.backend.enums.UserStatus; import es.adeptusminiaturium.backend.model.User; -import es.adeptusminiaturium.backend.enums.UserRole; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; @@ -10,11 +9,11 @@ import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.List; -public class CustomUserDetails implements UserDetails { +public class CustomPrincipal implements UserDetails { private final User user; - public CustomUserDetails(User user) { + public CustomPrincipal(User user) { this.user = user; } diff --git a/backend/src/main/java/es/adeptusminiaturium/backend/security/JwtFilter.java b/backend/src/main/java/es/adeptusminiaturium/backend/security/JwtFilter.java new file mode 100644 index 0000000..85cfa34 --- /dev/null +++ b/backend/src/main/java/es/adeptusminiaturium/backend/security/JwtFilter.java @@ -0,0 +1,59 @@ +package es.adeptusminiaturium.backend.security; + +import es.adeptusminiaturium.backend.model.User; +import es.adeptusminiaturium.backend.service.UserService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.UUID; + +public class JwtFilter extends OncePerRequestFilter { + + private final JwtService jwtService; + private final UserService userService; + private final long refreshThreshold = 300_000; + + public JwtFilter(JwtService jwtService, UserService userService) { + this.jwtService = jwtService; + this.userService = userService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + String authHeader = request.getHeader("Authorization"); + if (authHeader != null && authHeader.startsWith("Bearer ")) { + String token = authHeader.substring(7); + + try { + if (jwtService.validateToken(token)) { + UUID userId = jwtService.getUserId(token); + Byte serviceId = jwtService.getServiceId(token); + + User user = userService.getUser(userId); + CustomPrincipal principal = new CustomPrincipal(user); + UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(principal, null, principal.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(auth); + + long timeLeft = jwtService.getExpiration(token).getTime() - System.currentTimeMillis(); + if (timeLeft < refreshThreshold) { + String newToken = jwtService.generateToken(userId); + response.setHeader("X-Refresh-Token", newToken); + } + } + } catch (Exception e) { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid or expired token"); + return; + } + } + + filterChain.doFilter(request, response); + } +} diff --git a/backend/src/main/java/es/adeptusminiaturium/backend/security/JwtService.java b/backend/src/main/java/es/adeptusminiaturium/backend/security/JwtService.java new file mode 100644 index 0000000..9ff695b --- /dev/null +++ b/backend/src/main/java/es/adeptusminiaturium/backend/security/JwtService.java @@ -0,0 +1,101 @@ +package es.adeptusminiaturium.backend.security; + +import io.jsonwebtoken.*; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +@Service +public class JwtService { + + @Value("${jwt.private-key-path}") + private String privateKeyPath; + + @Value("${jwt.public-key-path}") + private String publicKeyPath; + + @Value("${jwt.expiration-ms}") + private long expiration; + + private PrivateKey privateKey; + private PublicKey publicKey; + + @PostConstruct + public void init() throws Exception { + this.privateKey = loadPrivateKey(privateKeyPath); + this.publicKey = loadPublicKey(publicKeyPath); + } + + private PrivateKey loadPrivateKey(String path) throws Exception { + String pem = Files.readString(Path.of(path)); + pem = pem.replace("-----BEGIN PRIVATE KEY-----", "") + .replace("-----END PRIVATE KEY-----", "") + .replaceAll("\\s", ""); + + byte[] decoded = Base64.getDecoder().decode(pem); + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(decoded); + KeyFactory kf = KeyFactory.getInstance("RSA"); + return kf.generatePrivate(spec); + } + + private PublicKey loadPublicKey(String path) throws Exception { + String pem = Files.readString(Path.of(path)); + pem = pem.replace("-----BEGIN PUBLIC KEY-----", "") + .replace("-----END PUBLIC KEY-----", "") + .replaceAll("\\s", ""); + + byte[] decoded = Base64.getDecoder().decode(pem); + X509EncodedKeySpec spec = new X509EncodedKeySpec(decoded); + KeyFactory kf = KeyFactory.getInstance("RSA"); + return kf.generatePublic(spec); + } + + public String generateToken(UUID userId) { + Date now = new Date(); + Date exp = new Date(now.getTime() + expiration); + + return Jwts.builder() + .setSubject(userId.toString()) + .setIssuedAt(now) + .setExpiration(exp) + .signWith(privateKey, SignatureAlgorithm.RS256) + .compact(); + } + + public boolean validateToken(String token) { + try { + Jwts.parserBuilder().setSigningKey(publicKey).build().parseClaimsJws(token); + return true; + } catch (JwtException | IllegalArgumentException e) { + return false; + } + } + + public UUID getUserId(String token) { + Claims claims = Jwts.parserBuilder().setSigningKey(publicKey).build().parseClaimsJws(token).getBody(); + return UUID.fromString(claims.getSubject()); + } + + public Byte getServiceId(String token) { + Claims claims = Jwts.parserBuilder().setSigningKey(publicKey).build().parseClaimsJws(token).getBody(); + return ((Number) claims.get("service")).byteValue(); + } + + public Date getExpiration(String token) { + Claims claims = Jwts.parserBuilder().setSigningKey(publicKey).build().parseClaimsJws(token).getBody(); + return claims.getExpiration(); + } +} + diff --git a/backend/src/main/java/es/adeptusminiaturium/backend/security/PasswordGenerator.java b/backend/src/main/java/es/adeptusminiaturium/backend/security/PasswordGenerator.java new file mode 100644 index 0000000..5983494 --- /dev/null +++ b/backend/src/main/java/es/adeptusminiaturium/backend/security/PasswordGenerator.java @@ -0,0 +1,45 @@ +package es.adeptusminiaturium.backend.security; + +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class PasswordGenerator { + + private static final String UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + private static final String LOWER = "abcdefghijklmnopqrstuvwxyz"; + private static final String DIGITS = "0123456789"; + private static final String SYMBOLS = "!@#$%^&*"; // compatibles con bcrypt + private static final String ALL = UPPER + LOWER + DIGITS + SYMBOLS; + + private static final SecureRandom random = new SecureRandom(); + + public static String generate(int length) { + if (length < 8) length = 8; + + List password = new ArrayList<>(); + + password.add(getRandChar(UPPER)); + password.add(getRandChar(LOWER)); + password.add(getRandChar(DIGITS)); + password.add(getRandChar(SYMBOLS)); + + while (password.size() < length) { + password.add(getRandChar(ALL)); + } + + Collections.shuffle(password, random); + + StringBuilder sb = new StringBuilder(); + for (char c : password) { + sb.append(c); + } + + return sb.toString(); + } + + private static char getRandChar(String chars) { + return chars.charAt(random.nextInt(chars.length())); + } +} diff --git a/backend/src/main/java/es/adeptusminiaturium/backend/security/ServiceAuthFilter.java b/backend/src/main/java/es/adeptusminiaturium/backend/security/ServiceAuthFilter.java new file mode 100644 index 0000000..37d9b2b --- /dev/null +++ b/backend/src/main/java/es/adeptusminiaturium/backend/security/ServiceAuthFilter.java @@ -0,0 +1,45 @@ +package es.adeptusminiaturium.backend.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +public class ServiceAuthFilter extends OncePerRequestFilter { + + private final JwtService jwtService; + + public ServiceAuthFilter(JwtService jwtService) { + this.jwtService = jwtService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) + throws ServletException, IOException { + + String path = request.getRequestURI(); + + if (path.startsWith("/users/service")) { + String authHeader = request.getHeader("Authorization"); + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + + String token = authHeader.substring(7); + if (!jwtService.validateToken(token)) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + } + + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/backend/src/main/java/es/adeptusminiaturium/backend/service/AuthService.java b/backend/src/main/java/es/adeptusminiaturium/backend/service/AuthService.java new file mode 100644 index 0000000..7474f40 --- /dev/null +++ b/backend/src/main/java/es/adeptusminiaturium/backend/service/AuthService.java @@ -0,0 +1,59 @@ +package es.adeptusminiaturium.backend.service; + +import es.adeptusminiaturium.backend.dto.ChangePasswordRequest; +import es.adeptusminiaturium.backend.dto.LoginRequest; +import es.adeptusminiaturium.backend.dto.LoginResponse; +import es.adeptusminiaturium.backend.dto.UserDto; +import es.adeptusminiaturium.backend.enums.UserStatus; +import es.adeptusminiaturium.backend.exception.ForbiddenException; +import es.adeptusminiaturium.backend.exception.UnauthorizedException; +import es.adeptusminiaturium.backend.exception.ValidationException; +import es.adeptusminiaturium.backend.mapper.UserMapper; +import es.adeptusminiaturium.backend.model.User; +import es.adeptusminiaturium.backend.security.JwtService; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +@Service +public class AuthService { + private final UserService userService; + private final JwtService jwtService; + private final PasswordEncoder passwordEncoder; + + public AuthService(UserService userService, JwtService jwtService, PasswordEncoder passwordEncoder) { + this.userService = userService; + this.jwtService = jwtService; + this.passwordEncoder = passwordEncoder; + } + + public LoginResponse login(LoginRequest request) { + User user = userService.getByUsername(request.userName()); + + if (!passwordEncoder.matches(request.password(), user.getPassword())) + throw new UnauthorizedException("Invalid credentials"); + + if (user.getStatus() == UserStatus.INACTIVE) + throw new ForbiddenException("User is inactive"); + + String token = jwtService.generateToken(user.getUserId()); + UserDto.Response userDto = UserMapper.toResponse(user); + + return new LoginResponse(token, userDto); + } + + public void changePassword(UUID userId, ChangePasswordRequest request) { + User user = userService.getUser(userId); + + if (!passwordEncoder.matches(request.oldPassword(), user.getPassword())) { + throw new ValidationException("oldPassword", "La contraseña actual es incorrecta"); + } + + if (request.newPassword().length() < 8) { + throw new ValidationException("newPassword", "La nueva contraseña debe tener al menos 8 caracteres"); + } + + userService.changePassword(userId, request.newPassword()); + } +} diff --git a/backend/src/main/java/es/adeptusminiaturium/backend/service/CustomUserDetailsService.java b/backend/src/main/java/es/adeptusminiaturium/backend/service/CustomUserDetailsService.java index ece8ceb..21ecbb8 100644 --- a/backend/src/main/java/es/adeptusminiaturium/backend/service/CustomUserDetailsService.java +++ b/backend/src/main/java/es/adeptusminiaturium/backend/service/CustomUserDetailsService.java @@ -1,7 +1,7 @@ package es.adeptusminiaturium.backend.service; import es.adeptusminiaturium.backend.repository.UserRepository; -import es.adeptusminiaturium.backend.security.CustomUserDetails; +import es.adeptusminiaturium.backend.security.CustomPrincipal; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; @@ -19,7 +19,7 @@ public class CustomUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return userRepo.findByUserName(username) - .map(CustomUserDetails::new) + .map(CustomPrincipal::new) .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); } } \ No newline at end of file diff --git a/backend/src/main/java/es/adeptusminiaturium/backend/service/UserService.java b/backend/src/main/java/es/adeptusminiaturium/backend/service/UserService.java index 54fdfe8..4b56227 100644 --- a/backend/src/main/java/es/adeptusminiaturium/backend/service/UserService.java +++ b/backend/src/main/java/es/adeptusminiaturium/backend/service/UserService.java @@ -1,8 +1,7 @@ package es.adeptusminiaturium.backend.service; - -import es.adeptusminiaturium.backend.dto.UserDto; -import es.adeptusminiaturium.backend.mapper.UserMapper; +import es.adeptusminiaturium.backend.dto.ChangePasswordRequest; +import es.adeptusminiaturium.backend.exception.NotFoundException; import es.adeptusminiaturium.backend.model.User; import es.adeptusminiaturium.backend.repository.UserRepository; import org.springframework.stereotype.Service; @@ -10,39 +9,58 @@ import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.util.List; import java.util.UUID; -import java.util.stream.Collectors; @Service public class UserService { private final UserRepository repo; - public UserService(UserRepository repo) { this.repo = repo; } - - public List getAllUsers() { - return repo.findAll().stream().map(UserMapper::toResponse).collect(Collectors.toList()); + public UserService(UserRepository repo) { + this.repo = repo; } - public UserDto.Response getUser(UUID userId) { - return repo.findById(userId).map(UserMapper::toResponse).orElse(null); + public List getAllUsers() { + return repo.findAll(); } - public UserDto.Response createUser(UserDto.Request dto) { - User user = UserMapper.toEntity(dto); - return UserMapper.toResponse(repo.save(user)); + public User getUser(UUID userId) { + return repo.findById(userId) + .orElseThrow(() -> new NotFoundException("User not found")); } - public UserDto.Response updateUser(UUID userId, UserDto.Request dto) { + public User createUser(User user) { + user.setCreatedAt(LocalDateTime.now()); + user.setUpdatedAt(LocalDateTime.now()); + return repo.save(user); + } + + public User updateUser(UUID userId, User updatedUser) { return repo.findById(userId).map(user -> { - user.setDisplayName(dto.getDisplayName()); - user.setUserName(dto.getUserName()); - user.setPassword(dto.getPassword()); - user.setRole(dto.getRole()); - user.setStatus(dto.getStatus()); + + user.setDisplayName(updatedUser.getDisplayName()); + user.setUserName(updatedUser.getUserName()); + user.setPassword(updatedUser.getPassword()); + user.setRole(updatedUser.getRole()); + user.setStatus(updatedUser.getStatus()); user.setUpdatedAt(LocalDateTime.now()); - return UserMapper.toResponse(repo.save(user)); - }).orElse(null); + + return repo.save(user); + + }).orElseThrow(() -> new NotFoundException("User not found")); } - public void deleteUser(UUID userId) { repo.deleteById(userId); } + public void deleteUser(UUID userId) { + repo.deleteById(userId); + } + + public User getByUsername(String userName) { + return repo.findByUserName(userName) + .orElseThrow(() -> new NotFoundException("User not found")); + } + + public void changePassword(UUID userId, String password) { + repo.findById(userId) + .orElseThrow(() -> new NotFoundException("User not found")) + .setPassword(password); + } } \ No newline at end of file diff --git a/frontend/public/config/settings.dev.json b/frontend/public/config/settings.dev.json index 741c641..479275e 100644 --- a/frontend/public/config/settings.dev.json +++ b/frontend/public/config/settings.dev.json @@ -1,6 +1,6 @@ { "apiConfig": { - "baseUrl": "http://localhost:3000", + "baseUrl": "http://localhost:3000/api", "endpoints": { "auth": { "login": "/auth/login", diff --git a/frontend/public/config/settings.prod.json b/frontend/public/config/settings.prod.json index 741c641..dca4995 100644 --- a/frontend/public/config/settings.prod.json +++ b/frontend/public/config/settings.prod.json @@ -1,6 +1,6 @@ { "apiConfig": { - "baseUrl": "http://localhost:3000", + "baseUrl": "https://adeptusminiaturium.es/api", "endpoints": { "auth": { "login": "/auth/login", diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx index 8186c49..753a068 100644 --- a/frontend/src/components/Header.jsx +++ b/frontend/src/components/Header.jsx @@ -6,12 +6,6 @@ const Header = () => { return (
- Purity Seal -

Adeptus Miniaturium

{t("header.subtitle")}

diff --git a/frontend/src/css/Header.css b/frontend/src/css/Header.css index 8a8e410..21bdc65 100644 --- a/frontend/src/css/Header.css +++ b/frontend/src/css/Header.css @@ -6,28 +6,6 @@ header { box-shadow: 0 10px 30px rgba(0,0,0,0.9); } -.imperial-header { - overflow: visible; - z-index: 1000; -} - -.purity-seal { - position: absolute; - top: 5px; - width: 196px; - z-index: 10; - pointer-events: none; -} - -.purity-seal.left { - left: 8%; -} - -.purity-seal.right { - right: 5%; - transform: rotate(10deg); -} - header h1 { font-family: 'Cinzel', serif; font-size: 3.5rem;