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 (
-
-
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;