generated from Gallardo7761/miarma-template-full
Compare commits
1 Commits
fdc3120aa7
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf19607a07 |
@@ -23,13 +23,6 @@
|
|||||||
<java.version>25</java.version>
|
<java.version>25</java.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<repositories>
|
|
||||||
<repository>
|
|
||||||
<id>gitea</id>
|
|
||||||
<url>https://git.miarma.net/api/packages/Gallardo7761/maven</url>
|
|
||||||
</repository>
|
|
||||||
</repositories>
|
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<!-- Spring Boot -->
|
<!-- Spring Boot -->
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -49,6 +42,22 @@
|
|||||||
<artifactId>mariadb-java-client</artifactId>
|
<artifactId>mariadb-java-client</artifactId>
|
||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>jakarta.validation</groupId>
|
||||||
|
<artifactId>jakarta.validation-api</artifactId>
|
||||||
|
<version>3.1.1</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.hibernate.validator</groupId>
|
||||||
|
<artifactId>hibernate-validator</artifactId>
|
||||||
|
<version>8.0.0.Final</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>1.18.42</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
<!-- JWT -->
|
<!-- JWT -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.jsonwebtoken</groupId>
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
@@ -67,12 +76,6 @@
|
|||||||
<version>0.11.5</version>
|
<version>0.11.5</version>
|
||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>net.miarma</groupId>
|
|
||||||
<artifactId>backlib</artifactId>
|
|
||||||
<version>1.1.0</version>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@@ -1,36 +1,67 @@
|
|||||||
package es.adeptusminiaturium.backend.config;
|
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.CustomUserDetailsService;
|
||||||
|
import es.adeptusminiaturium.backend.service.UserService;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.security.authentication.AuthenticationManager;
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
import org.springframework.security.config.Customizer;
|
import org.springframework.security.config.Customizer;
|
||||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
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.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.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
|
import org.springframework.web.cors.CorsConfigurationSource;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
@EnableMethodSecurity(prePostEnabled = true)
|
||||||
public class SecurityConfig {
|
public class SecurityConfig {
|
||||||
|
private final RestAuthEntryPoint authEntryPoint;
|
||||||
|
private final RestAccessDeniedHandler accessDeniedHandler;
|
||||||
|
private final CorsConfigurationSource corsConfigurationSource;
|
||||||
private final CustomUserDetailsService userDetailsService;
|
private final CustomUserDetailsService userDetailsService;
|
||||||
|
|
||||||
public SecurityConfig(CustomUserDetailsService userDetailsService) {
|
public SecurityConfig(CustomUserDetailsService userDetailsService,
|
||||||
|
RestAuthEntryPoint authEntryPoint,
|
||||||
|
RestAccessDeniedHandler accessDeniedHandler,
|
||||||
|
Optional<CorsConfigurationSource> corsConfigurationSource) {
|
||||||
|
this.authEntryPoint = authEntryPoint;
|
||||||
|
this.accessDeniedHandler = accessDeniedHandler;
|
||||||
|
this.corsConfigurationSource = corsConfigurationSource.orElse(null);
|
||||||
this.userDetailsService = userDetailsService;
|
this.userDetailsService = userDetailsService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@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
|
http
|
||||||
.csrf(Customizer.withDefaults())
|
.csrf(csrf -> csrf.disable())
|
||||||
.authorizeHttpRequests(auth -> auth
|
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
//.requestMatchers("/admin/**").hasRole("ADMIN")
|
.exceptionHandling(ex -> ex
|
||||||
//.requestMatchers("/posts/**").authenticated()
|
.authenticationEntryPoint(authEntryPoint)
|
||||||
.anyRequest().permitAll()
|
.accessDeniedHandler(accessDeniedHandler)
|
||||||
)
|
)
|
||||||
.formLogin(Customizer.withDefaults())
|
.authorizeHttpRequests(auth -> auth
|
||||||
.httpBasic(Customizer.withDefaults());
|
.requestMatchers("/auth/login").permitAll()
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
);
|
||||||
|
|
||||||
|
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
@@ -50,4 +81,9 @@ public class SecurityConfig {
|
|||||||
|
|
||||||
return authBuilder.build();
|
return authBuilder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public JwtService jwtService() {
|
||||||
|
return new JwtService();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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<LoginResponse> 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<Boolean> validate(@RequestHeader("Authorization") String authHeader) {
|
||||||
|
String token = authHeader.substring(7);
|
||||||
|
return ResponseEntity.ok(jwtService.validateToken(token));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,42 +1,62 @@
|
|||||||
package es.adeptusminiaturium.backend.controller;
|
package es.adeptusminiaturium.backend.controller;
|
||||||
|
|
||||||
|
import es.adeptusminiaturium.backend.mapper.UserMapper;
|
||||||
|
import es.adeptusminiaturium.backend.model.User;
|
||||||
import es.adeptusminiaturium.backend.service.UserService;
|
import es.adeptusminiaturium.backend.service.UserService;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import es.adeptusminiaturium.backend.dto.UserDto;
|
import es.adeptusminiaturium.backend.dto.UserDto;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/users")
|
@RequestMapping("/api/users")
|
||||||
public class UserController {
|
public class UserController {
|
||||||
|
|
||||||
private final UserService service;
|
private final UserService service;
|
||||||
|
|
||||||
public UserController(UserService service) { this.service = service; }
|
public UserController(UserService service) {
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public ResponseEntity<List<UserDto.Response>> getAll() {
|
public ResponseEntity<List<UserDto.Response>> getAll() {
|
||||||
return ResponseEntity.ok(service.getAllUsers());
|
List<UserDto.Response> users = service.getAllUsers()
|
||||||
|
.stream()
|
||||||
|
.map(UserMapper::toResponse)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return ResponseEntity.ok(users);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
public ResponseEntity<UserDto.Response> getOne(@PathVariable UUID id) {
|
public ResponseEntity<UserDto.Response> getOne(@PathVariable UUID id) {
|
||||||
return ResponseEntity.ofNullable(service.getUser(id));
|
User user = service.getUser(id);
|
||||||
|
return ResponseEntity.ok(UserMapper.toResponse(user));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public ResponseEntity<UserDto.Response> create(@RequestBody UserDto.Request dto) {
|
public ResponseEntity<UserDto.Response> 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}")
|
@PutMapping("/{id}")
|
||||||
public ResponseEntity<UserDto.Response> update(@PathVariable UUID id, @RequestBody UserDto.Request dto) {
|
public ResponseEntity<UserDto.Response> update(
|
||||||
return ResponseEntity.ofNullable(service.updateUser(id, dto));
|
@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}")
|
@DeleteMapping("/{id}")
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String,String> errors;
|
||||||
|
private String path;
|
||||||
|
private Instant timestamp;
|
||||||
|
|
||||||
|
public ApiValidationErrorDto(Map<String,String> errors, String path) {
|
||||||
|
this.status = 422;
|
||||||
|
this.errors = errors;
|
||||||
|
this.path = path;
|
||||||
|
this.timestamp = Instant.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String,String> 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<String,String> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package es.adeptusminiaturium.backend.dto;
|
||||||
|
|
||||||
|
public record ChangeAvatarRequest(String avatarUrl) {}
|
||||||
@@ -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) {}
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package es.adeptusminiaturium.backend.dto;
|
||||||
|
|
||||||
|
import es.adeptusminiaturium.backend.enums.UserRole;
|
||||||
|
|
||||||
|
public record ChangeRoleRequest(UserRole role) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package es.adeptusminiaturium.backend.dto;
|
||||||
|
|
||||||
|
import es.adeptusminiaturium.backend.enums.UserStatus;
|
||||||
|
|
||||||
|
public record ChangeStatusRequest(UserStatus status) {}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package es.adeptusminiaturium.backend.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
public record LoginRequest(@NotBlank String userName,
|
||||||
|
@NotBlank String password) {}
|
||||||
@@ -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) {}
|
||||||
@@ -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) {}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package es.adeptusminiaturium.backend.exception;
|
||||||
|
|
||||||
|
public class BadRequestException extends RuntimeException {
|
||||||
|
public BadRequestException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package es.adeptusminiaturium.backend.exception;
|
||||||
|
|
||||||
|
public class ConflictException extends RuntimeException {
|
||||||
|
public ConflictException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package es.adeptusminiaturium.backend.exception;
|
||||||
|
|
||||||
|
public class ForbiddenException extends RuntimeException {
|
||||||
|
public ForbiddenException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package es.adeptusminiaturium.backend.exception;
|
||||||
|
|
||||||
|
public class NotFoundException extends RuntimeException {
|
||||||
|
public NotFoundException(String message) { super(message); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package es.adeptusminiaturium.backend.exception;
|
||||||
|
|
||||||
|
public class UnauthorizedException extends RuntimeException {
|
||||||
|
public UnauthorizedException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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("*");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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<ApiErrorDto> 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<ApiErrorDto> 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<ApiErrorDto> 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<ApiErrorDto> 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<ApiErrorDto> 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<ApiValidationErrorDto> handleValidation(
|
||||||
|
ValidationException ex, HttpServletRequest req) {
|
||||||
|
Map<String, String> errors = Map.of(ex.getField(), ex.getMessage());
|
||||||
|
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_CONTENT).body(new ApiValidationErrorDto(errors, req.getRequestURI()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public ResponseEntity<ApiErrorDto> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -2,7 +2,6 @@ package es.adeptusminiaturium.backend.security;
|
|||||||
|
|
||||||
import es.adeptusminiaturium.backend.enums.UserStatus;
|
import es.adeptusminiaturium.backend.enums.UserStatus;
|
||||||
import es.adeptusminiaturium.backend.model.User;
|
import es.adeptusminiaturium.backend.model.User;
|
||||||
import es.adeptusminiaturium.backend.enums.UserRole;
|
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
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.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class CustomUserDetails implements UserDetails {
|
public class CustomPrincipal implements UserDetails {
|
||||||
|
|
||||||
private final User user;
|
private final User user;
|
||||||
|
|
||||||
public CustomUserDetails(User user) {
|
public CustomPrincipal(User user) {
|
||||||
this.user = user;
|
this.user = user;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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<Character> 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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package es.adeptusminiaturium.backend.service;
|
package es.adeptusminiaturium.backend.service;
|
||||||
|
|
||||||
import es.adeptusminiaturium.backend.repository.UserRepository;
|
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.UserDetails;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
@@ -19,7 +19,7 @@ public class CustomUserDetailsService implements UserDetailsService {
|
|||||||
@Override
|
@Override
|
||||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||||
return userRepo.findByUserName(username)
|
return userRepo.findByUserName(username)
|
||||||
.map(CustomUserDetails::new)
|
.map(CustomPrincipal::new)
|
||||||
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
|
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
package es.adeptusminiaturium.backend.service;
|
package es.adeptusminiaturium.backend.service;
|
||||||
|
|
||||||
|
import es.adeptusminiaturium.backend.dto.ChangePasswordRequest;
|
||||||
import es.adeptusminiaturium.backend.dto.UserDto;
|
import es.adeptusminiaturium.backend.exception.NotFoundException;
|
||||||
import es.adeptusminiaturium.backend.mapper.UserMapper;
|
|
||||||
import es.adeptusminiaturium.backend.model.User;
|
import es.adeptusminiaturium.backend.model.User;
|
||||||
import es.adeptusminiaturium.backend.repository.UserRepository;
|
import es.adeptusminiaturium.backend.repository.UserRepository;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -10,39 +9,58 @@ import org.springframework.stereotype.Service;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class UserService {
|
public class UserService {
|
||||||
|
|
||||||
private final UserRepository repo;
|
private final UserRepository repo;
|
||||||
|
|
||||||
public UserService(UserRepository repo) { this.repo = repo; }
|
public UserService(UserRepository repo) {
|
||||||
|
this.repo = repo;
|
||||||
public List<UserDto.Response> getAllUsers() {
|
|
||||||
return repo.findAll().stream().map(UserMapper::toResponse).collect(Collectors.toList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public UserDto.Response getUser(UUID userId) {
|
public List<User> getAllUsers() {
|
||||||
return repo.findById(userId).map(UserMapper::toResponse).orElse(null);
|
return repo.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
public UserDto.Response createUser(UserDto.Request dto) {
|
public User getUser(UUID userId) {
|
||||||
User user = UserMapper.toEntity(dto);
|
return repo.findById(userId)
|
||||||
return UserMapper.toResponse(repo.save(user));
|
.orElseThrow(() -> new NotFoundException("User not found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public UserDto.Response updateUser(UUID userId, UserDto.Request dto) {
|
public User createUser(User user) {
|
||||||
return repo.findById(userId).map(user -> {
|
user.setCreatedAt(LocalDateTime.now());
|
||||||
user.setDisplayName(dto.getDisplayName());
|
|
||||||
user.setUserName(dto.getUserName());
|
|
||||||
user.setPassword(dto.getPassword());
|
|
||||||
user.setRole(dto.getRole());
|
|
||||||
user.setStatus(dto.getStatus());
|
|
||||||
user.setUpdatedAt(LocalDateTime.now());
|
user.setUpdatedAt(LocalDateTime.now());
|
||||||
return UserMapper.toResponse(repo.save(user));
|
return repo.save(user);
|
||||||
}).orElse(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteUser(UUID userId) { repo.deleteById(userId); }
|
public User updateUser(UUID userId, User updatedUser) {
|
||||||
|
return repo.findById(userId).map(user -> {
|
||||||
|
|
||||||
|
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 repo.save(user);
|
||||||
|
|
||||||
|
}).orElseThrow(() -> new NotFoundException("User not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"apiConfig": {
|
"apiConfig": {
|
||||||
"baseUrl": "http://localhost:3000",
|
"baseUrl": "http://localhost:3000/api",
|
||||||
"endpoints": {
|
"endpoints": {
|
||||||
"auth": {
|
"auth": {
|
||||||
"login": "/auth/login",
|
"login": "/auth/login",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"apiConfig": {
|
"apiConfig": {
|
||||||
"baseUrl": "http://localhost:3000",
|
"baseUrl": "https://adeptusminiaturium.es/api",
|
||||||
"endpoints": {
|
"endpoints": {
|
||||||
"auth": {
|
"auth": {
|
||||||
"login": "/auth/login",
|
"login": "/auth/login",
|
||||||
|
|||||||
@@ -6,12 +6,6 @@ const Header = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="imperial-header py-5 text-center position-relative">
|
<header className="imperial-header py-5 text-center position-relative">
|
||||||
<img
|
|
||||||
src="/images/purity.png"
|
|
||||||
alt="Purity Seal"
|
|
||||||
className="purity-seal left"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<h1 className="mb-2">Adeptus Miniaturium</h1>
|
<h1 className="mb-2">Adeptus Miniaturium</h1>
|
||||||
<p className="m-0">{t("header.subtitle")}</p>
|
<p className="m-0">{t("header.subtitle")}</p>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -6,28 +6,6 @@ header {
|
|||||||
box-shadow: 0 10px 30px rgba(0,0,0,0.9);
|
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 {
|
header h1 {
|
||||||
font-family: 'Cinzel', serif;
|
font-family: 'Cinzel', serif;
|
||||||
font-size: 3.5rem;
|
font-size: 3.5rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user