generated from Gallardo7761/miarma-template-full
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf19607a07 | ||
|
|
fdc3120aa7 | ||
|
|
af65e3b51e | ||
|
|
5ef4d0f2e0 | ||
|
|
dcc1d55db6 | ||
|
|
95dd13595e | ||
|
|
aec029b670 | ||
|
|
859d5b88bc |
19
.gitignore
vendored
19
.gitignore
vendored
@@ -1,4 +1,15 @@
|
|||||||
.env
|
frontend/.env
|
||||||
node_modules/
|
frontend/node_modules/
|
||||||
dist/
|
frontend/dist/
|
||||||
package-lock.json
|
frontend/package-lock.json
|
||||||
|
|
||||||
|
backend/target/
|
||||||
|
backend/.mvn/wrapper/maven-wrapper.jar
|
||||||
|
backend/!**/src/main/**/target/
|
||||||
|
backend/!**/src/test/**/target/
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
backend/.idea
|
||||||
|
backend/*.iws
|
||||||
|
backend/*.iml
|
||||||
|
backend/*.ipr
|
||||||
|
|||||||
3
backend/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
3
backend/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
wrapperVersion=3.3.4
|
||||||
|
distributionType=only-script
|
||||||
|
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip
|
||||||
98
backend/pom.xml
Normal file
98
backend/pom.xml
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>4.0.2</version>
|
||||||
|
<relativePath/>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<groupId>es.adeptusminiaturium</groupId>
|
||||||
|
<artifactId>backend</artifactId>
|
||||||
|
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<name>backend</name>
|
||||||
|
<description>Adeptus Miniaturium's online site</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>25</maven.compiler.source>
|
||||||
|
<maven.compiler.target>25</maven.compiler.target>
|
||||||
|
<java.version>25</java.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- Spring Boot -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mariadb.jdbc</groupId>
|
||||||
|
<artifactId>mariadb-java-client</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</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 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-api</artifactId>
|
||||||
|
<version>0.11.5</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-impl</artifactId>
|
||||||
|
<version>0.11.5</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-jackson</artifactId>
|
||||||
|
<version>0.11.5</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<goals>
|
||||||
|
<goal>repackage</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package es.adeptusminiaturium.backend;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class BackendApplication {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(BackendApplication.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
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,
|
||||||
|
RestAuthEntryPoint authEntryPoint,
|
||||||
|
RestAccessDeniedHandler accessDeniedHandler,
|
||||||
|
Optional<CorsConfigurationSource> corsConfigurationSource) {
|
||||||
|
this.authEntryPoint = authEntryPoint;
|
||||||
|
this.accessDeniedHandler = accessDeniedHandler;
|
||||||
|
this.corsConfigurationSource = corsConfigurationSource.orElse(null);
|
||||||
|
this.userDetailsService = userDetailsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public JwtFilter jwtFilter(JwtService jwtService, UserService userService) {
|
||||||
|
return new JwtFilter(jwtService, userService);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http, JwtFilter jwtFilter) throws Exception {
|
||||||
|
http
|
||||||
|
.csrf(csrf -> csrf.disable())
|
||||||
|
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
|
.exceptionHandling(ex -> ex
|
||||||
|
.authenticationEntryPoint(authEntryPoint)
|
||||||
|
.accessDeniedHandler(accessDeniedHandler)
|
||||||
|
)
|
||||||
|
.authorizeHttpRequests(auth -> auth
|
||||||
|
.requestMatchers("/auth/login").permitAll()
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
);
|
||||||
|
|
||||||
|
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PasswordEncoder passwordEncoder() {
|
||||||
|
return new BCryptPasswordEncoder(12);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public AuthenticationManager authManager(HttpSecurity http) {
|
||||||
|
AuthenticationManagerBuilder authBuilder =
|
||||||
|
http.getSharedObject(AuthenticationManagerBuilder.class);
|
||||||
|
|
||||||
|
authBuilder.userDetailsService(userDetailsService)
|
||||||
|
.passwordEncoder(passwordEncoder());
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package es.adeptusminiaturium.backend.controller;
|
||||||
|
|
||||||
|
import es.adeptusminiaturium.backend.dto.MediaDto;
|
||||||
|
import es.adeptusminiaturium.backend.service.MediaService;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/media")
|
||||||
|
public class MediaController {
|
||||||
|
|
||||||
|
private final MediaService service;
|
||||||
|
|
||||||
|
public MediaController(MediaService service) { this.service = service; }
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<MediaDto.Response>> getAll() {
|
||||||
|
return ResponseEntity.ok(service.getAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity<MediaDto.Response> getOne(@PathVariable Long id) {
|
||||||
|
return ResponseEntity.ofNullable(service.getById(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<MediaDto.Response> create(@RequestBody MediaDto.Request dto) {
|
||||||
|
return ResponseEntity.ok(service.create(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ResponseEntity<MediaDto.Response> update(@PathVariable Long id, @RequestBody MediaDto.Request dto) {
|
||||||
|
return ResponseEntity.ofNullable(service.update(id, dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ResponseEntity<Void> delete(@PathVariable Long id) {
|
||||||
|
service.delete(id);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package es.adeptusminiaturium.backend.controller;
|
||||||
|
|
||||||
|
import es.adeptusminiaturium.backend.service.PostService;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import es.adeptusminiaturium.backend.dto.PostDto;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/posts")
|
||||||
|
public class PostController {
|
||||||
|
|
||||||
|
private final PostService postService;
|
||||||
|
|
||||||
|
public PostController(PostService postService) {
|
||||||
|
this.postService = postService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<PostDto.Response>> getAll() {
|
||||||
|
return ResponseEntity.ok(postService.getAllPosts());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity<PostDto.Response> getById(@PathVariable Long id) {
|
||||||
|
return ResponseEntity.ok(postService.getPostById(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<PostDto.Response> create(@RequestBody PostDto.Request dto) {
|
||||||
|
return ResponseEntity.ok(postService.createPost(dto));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package es.adeptusminiaturium.backend.controller;
|
||||||
|
|
||||||
|
import es.adeptusminiaturium.backend.dto.PublicationDto;
|
||||||
|
import es.adeptusminiaturium.backend.service.PublicationService;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/publications")
|
||||||
|
public class PublicationController {
|
||||||
|
|
||||||
|
private final PublicationService service;
|
||||||
|
|
||||||
|
public PublicationController(PublicationService service) { this.service = service; }
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<PublicationDto.Response>> getAll() {
|
||||||
|
return ResponseEntity.ok(service.getAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity<PublicationDto.Response> getOne(@PathVariable Long id) {
|
||||||
|
return ResponseEntity.ofNullable(service.getById(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<PublicationDto.Response> create(@RequestBody PublicationDto.Request dto) {
|
||||||
|
return ResponseEntity.ok(service.create(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ResponseEntity<PublicationDto.Response> update(@PathVariable Long id, @RequestBody PublicationDto.Request dto) {
|
||||||
|
return ResponseEntity.ofNullable(service.update(id, dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ResponseEntity<Void> delete(@PathVariable Long id) {
|
||||||
|
service.delete(id);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<List<UserDto.Response>> getAll() {
|
||||||
|
List<UserDto.Response> users = service.getAllUsers()
|
||||||
|
.stream()
|
||||||
|
.map(UserMapper::toResponse)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return ResponseEntity.ok(users);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity<UserDto.Response> getOne(@PathVariable UUID id) {
|
||||||
|
User user = service.getUser(id);
|
||||||
|
return ResponseEntity.ok(UserMapper.toResponse(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<UserDto.Response> create(@RequestBody UserDto.Request dto) {
|
||||||
|
|
||||||
|
User user = UserMapper.toEntity(dto);
|
||||||
|
User saved = service.createUser(user);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(UserMapper.toResponse(saved));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ResponseEntity<UserDto.Response> 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}")
|
||||||
|
public ResponseEntity<Void> delete(@PathVariable UUID id) {
|
||||||
|
service.deleteUser(id);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,46 @@
|
|||||||
|
package es.adeptusminiaturium.backend.dto;
|
||||||
|
|
||||||
|
import es.adeptusminiaturium.backend.enums.MediaType;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
public class MediaDto {
|
||||||
|
|
||||||
|
public static class Request {
|
||||||
|
private Long postId;
|
||||||
|
private MediaType mediaType;
|
||||||
|
private String url;
|
||||||
|
private Integer position;
|
||||||
|
|
||||||
|
public Long getPostId() { return postId; }
|
||||||
|
public void setPostId(Long postId) { this.postId = postId; }
|
||||||
|
public MediaType getMediaType() { return mediaType; }
|
||||||
|
public void setMediaType(MediaType mediaType) { this.mediaType = mediaType; }
|
||||||
|
public String getUrl() { return url; }
|
||||||
|
public void setUrl(String url) { this.url = url; }
|
||||||
|
public Integer getPosition() { return position; }
|
||||||
|
public void setPosition(Integer position) { this.position = position; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Response {
|
||||||
|
private Long mediaId;
|
||||||
|
private PostDto.Response post;
|
||||||
|
private MediaType mediaType;
|
||||||
|
private String url;
|
||||||
|
private Integer position;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
public Long getMediaId() { return mediaId; }
|
||||||
|
public void setMediaId(Long mediaId) { this.mediaId = mediaId; }
|
||||||
|
public PostDto.Response getPost() { return post; }
|
||||||
|
public void setPost(PostDto.Response post) { this.post = post; }
|
||||||
|
public MediaType getMediaType() { return mediaType; }
|
||||||
|
public void setMediaType(MediaType mediaType) { this.mediaType = mediaType; }
|
||||||
|
public String getUrl() { return url; }
|
||||||
|
public void setUrl(String url) { this.url = url; }
|
||||||
|
public Integer getPosition() { return position; }
|
||||||
|
public void setPosition(Integer position) { this.position = position; }
|
||||||
|
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package es.adeptusminiaturium.backend.dto;
|
||||||
|
|
||||||
|
import es.adeptusminiaturium.backend.enums.PostStatus;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class PostDto {
|
||||||
|
|
||||||
|
public static class Request {
|
||||||
|
private UUID authorId;
|
||||||
|
private String title;
|
||||||
|
private String body;
|
||||||
|
private String hashtags;
|
||||||
|
private PostStatus status;
|
||||||
|
|
||||||
|
public UUID getAuthorId() { return authorId; }
|
||||||
|
public void setAuthorId(UUID authorId) { this.authorId = authorId; }
|
||||||
|
public String getTitle() { return title; }
|
||||||
|
public void setTitle(String title) { this.title = title; }
|
||||||
|
public String getBody() { return body; }
|
||||||
|
public void setBody(String body) { this.body = body; }
|
||||||
|
public String getHashtags() { return hashtags; }
|
||||||
|
public void setHashtags(String hashtags) { this.hashtags = hashtags; }
|
||||||
|
public PostStatus getStatus() { return status; }
|
||||||
|
public void setStatus(PostStatus status) { this.status = status; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Response {
|
||||||
|
private Long postId;
|
||||||
|
private UserDto.Response author;
|
||||||
|
private String title;
|
||||||
|
private String body;
|
||||||
|
private String hashtags;
|
||||||
|
private PostStatus status;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
private LocalDateTime publishedAt;
|
||||||
|
|
||||||
|
public Long getPostId() { return postId; }
|
||||||
|
public void setPostId(Long postId) { this.postId = postId; }
|
||||||
|
public UserDto.Response getAuthor() { return author; }
|
||||||
|
public void setAuthor(UserDto.Response author) { this.author = author; }
|
||||||
|
public String getTitle() { return title; }
|
||||||
|
public void setTitle(String title) { this.title = title; }
|
||||||
|
public String getBody() { return body; }
|
||||||
|
public void setBody(String body) { this.body = body; }
|
||||||
|
public String getHashtags() { return hashtags; }
|
||||||
|
public void setHashtags(String hashtags) { this.hashtags = hashtags; }
|
||||||
|
public PostStatus getStatus() { return status; }
|
||||||
|
public void setStatus(PostStatus status) { this.status = status; }
|
||||||
|
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||||
|
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||||
|
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||||
|
public LocalDateTime getPublishedAt() { return publishedAt; }
|
||||||
|
public void setPublishedAt(LocalDateTime publishedAt) { this.publishedAt = publishedAt; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package es.adeptusminiaturium.backend.dto;
|
||||||
|
|
||||||
|
import es.adeptusminiaturium.backend.enums.Platform;
|
||||||
|
import es.adeptusminiaturium.backend.enums.PublicationStatus;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
public class PublicationDto {
|
||||||
|
|
||||||
|
public static class Request {
|
||||||
|
private Long postId;
|
||||||
|
private Platform platform;
|
||||||
|
private String externalId;
|
||||||
|
private PublicationStatus status;
|
||||||
|
|
||||||
|
// getters y setters
|
||||||
|
public Long getPostId() { return postId; }
|
||||||
|
public void setPostId(Long postId) { this.postId = postId; }
|
||||||
|
public Platform getPlatform() { return platform; }
|
||||||
|
public void setPlatform(Platform platform) { this.platform = platform; }
|
||||||
|
public String getExternalId() { return externalId; }
|
||||||
|
public void setExternalId(String externalId) { this.externalId = externalId; }
|
||||||
|
public PublicationStatus getStatus() { return status; }
|
||||||
|
public void setStatus(PublicationStatus status) { this.status = status; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Response {
|
||||||
|
private Long publicationId;
|
||||||
|
private PostDto.Response post;
|
||||||
|
private Platform platform;
|
||||||
|
private String externalId;
|
||||||
|
private PublicationStatus status;
|
||||||
|
private LocalDateTime publishedAt;
|
||||||
|
private String errorMessage;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
// getters y setters
|
||||||
|
public Long getPublicationId() { return publicationId; }
|
||||||
|
public void setPublicationId(Long publicationId) { this.publicationId = publicationId; }
|
||||||
|
public PostDto.Response getPost() { return post; }
|
||||||
|
public void setPost(PostDto.Response post) { this.post = post; }
|
||||||
|
public Platform getPlatform() { return platform; }
|
||||||
|
public void setPlatform(Platform platform) { this.platform = platform; }
|
||||||
|
public String getExternalId() { return externalId; }
|
||||||
|
public void setExternalId(String externalId) { this.externalId = externalId; }
|
||||||
|
public PublicationStatus getStatus() { return status; }
|
||||||
|
public void setStatus(PublicationStatus status) { this.status = status; }
|
||||||
|
public LocalDateTime getPublishedAt() { return publishedAt; }
|
||||||
|
public void setPublishedAt(LocalDateTime publishedAt) { this.publishedAt = publishedAt; }
|
||||||
|
public String getErrorMessage() { return errorMessage; }
|
||||||
|
public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; }
|
||||||
|
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,63 @@
|
|||||||
|
package es.adeptusminiaturium.backend.dto;
|
||||||
|
|
||||||
|
import es.adeptusminiaturium.backend.enums.UserRole;
|
||||||
|
import es.adeptusminiaturium.backend.enums.UserStatus;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class UserDto {
|
||||||
|
|
||||||
|
public static class Request {
|
||||||
|
private String displayName;
|
||||||
|
private String userName;
|
||||||
|
private String password;
|
||||||
|
private String avatar;
|
||||||
|
private UserRole role;
|
||||||
|
private UserStatus status;
|
||||||
|
|
||||||
|
public String getDisplayName() { return displayName; }
|
||||||
|
public void setDisplayName(String displayName) { this.displayName = displayName; }
|
||||||
|
public String getUserName() { return userName; }
|
||||||
|
public void setUserName(String userName) { this.userName = userName; }
|
||||||
|
public String getPassword() { return password; }
|
||||||
|
public void setPassword(String password) { this.password = password; }
|
||||||
|
public String getAvatar() { return avatar; }
|
||||||
|
public void setAvatar(String avatar) { this.avatar = avatar; }
|
||||||
|
public UserRole getRole() { return role; }
|
||||||
|
public void setRole(UserRole role) { this.role = role; }
|
||||||
|
public UserStatus getStatus() { return status; }
|
||||||
|
public void setStatus(UserStatus status) { this.status = status; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Response {
|
||||||
|
private UUID userId;
|
||||||
|
private String displayName;
|
||||||
|
private String userName;
|
||||||
|
private String avatar;
|
||||||
|
private UserRole role;
|
||||||
|
private UserStatus status;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
public UUID getUserId() { return userId; }
|
||||||
|
public void setUserId(UUID userId) { this.userId = userId; }
|
||||||
|
public String getDisplayName() { return displayName; }
|
||||||
|
public void setDisplayName(String displayName) { this.displayName = displayName; }
|
||||||
|
public String getUserName() { return userName; }
|
||||||
|
public void setUserName(String userName) { this.userName = userName; }
|
||||||
|
public String getAvatar() { return avatar; }
|
||||||
|
public void setAvatar(String avatar) { this.avatar = avatar; }
|
||||||
|
public UserRole getRole() { return role; }
|
||||||
|
public void setRole(UserRole role) { this.role = role; }
|
||||||
|
public UserStatus getStatus() { return status; }
|
||||||
|
public void setStatus(UserStatus status) { this.status = status; }
|
||||||
|
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||||
|
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||||
|
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package es.adeptusminiaturium.backend.enums;
|
||||||
|
|
||||||
|
public enum MediaType {
|
||||||
|
IMAGE, // 0
|
||||||
|
VIDEO // 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package es.adeptusminiaturium.backend.enums;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public enum Platform {
|
||||||
|
WEB, // 0
|
||||||
|
INSTAGRAM, // 1
|
||||||
|
TIKTOK, // 2
|
||||||
|
TWITTER; // 3
|
||||||
|
|
||||||
|
public static List<Platform> valuesList() {
|
||||||
|
return List.of(values());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package es.adeptusminiaturium.backend.enums;
|
||||||
|
|
||||||
|
public enum PostStatus {
|
||||||
|
DRAFT, // 0
|
||||||
|
PUBLISHED, // 1
|
||||||
|
ARCHIVED // 2
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package es.adeptusminiaturium.backend.enums;
|
||||||
|
|
||||||
|
public enum PublicationStatus {
|
||||||
|
PENDING, // 0
|
||||||
|
PUBLISHED, // 1
|
||||||
|
FAILED // 2
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package es.adeptusminiaturium.backend.enums;
|
||||||
|
|
||||||
|
public enum UserRole {
|
||||||
|
USER, // 0
|
||||||
|
ADMIN // 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package es.adeptusminiaturium.backend.enums;
|
||||||
|
|
||||||
|
public enum UserStatus {
|
||||||
|
INACTIVE, // 0
|
||||||
|
ACTIVE // 1
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package es.adeptusminiaturium.backend.mapper;
|
||||||
|
|
||||||
|
import es.adeptusminiaturium.backend.dto.MediaDto;
|
||||||
|
import es.adeptusminiaturium.backend.model.Media;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
public class MediaMapper {
|
||||||
|
|
||||||
|
public static MediaDto.Response toResponse(Media entity) {
|
||||||
|
MediaDto.Response dto = new MediaDto.Response();
|
||||||
|
dto.setMediaId(entity.getMediaId());
|
||||||
|
dto.setPost(PostMapper.toResponse(entity.getPost()));
|
||||||
|
dto.setMediaType(entity.getMediaType());
|
||||||
|
dto.setUrl(entity.getUrl());
|
||||||
|
dto.setPosition(entity.getPosition());
|
||||||
|
dto.setCreatedAt(entity.getCreatedAt());
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Media toEntity(MediaDto.Request dto) {
|
||||||
|
Media entity = new Media();
|
||||||
|
entity.setMediaId(null); // autoincrement
|
||||||
|
entity.setPost(null); // se setea después con repo.findById(dto.getPostId())
|
||||||
|
entity.setMediaType(dto.getMediaType());
|
||||||
|
entity.setUrl(dto.getUrl());
|
||||||
|
entity.setPosition(dto.getPosition());
|
||||||
|
entity.setCreatedAt(LocalDateTime.now());
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package es.adeptusminiaturium.backend.mapper;
|
||||||
|
|
||||||
|
import es.adeptusminiaturium.backend.dto.PostDto;
|
||||||
|
import es.adeptusminiaturium.backend.dto.UserDto;
|
||||||
|
import es.adeptusminiaturium.backend.model.Post;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
public class PostMapper {
|
||||||
|
|
||||||
|
public static PostDto.Response toResponse(Post entity) {
|
||||||
|
PostDto.Response dto = new PostDto.Response();
|
||||||
|
dto.setPostId(entity.getPostId());
|
||||||
|
dto.setAuthor(UserMapper.toResponse(entity.getAuthor()));
|
||||||
|
dto.setTitle(entity.getTitle());
|
||||||
|
dto.setBody(entity.getBody());
|
||||||
|
dto.setHashtags(entity.getHashtags());
|
||||||
|
dto.setStatus(entity.getStatus());
|
||||||
|
dto.setCreatedAt(entity.getCreatedAt());
|
||||||
|
dto.setUpdatedAt(entity.getUpdatedAt());
|
||||||
|
dto.setPublishedAt(entity.getPublishedAt());
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Post toEntity(PostDto.Request dto) {
|
||||||
|
Post entity = new Post();
|
||||||
|
entity.setPostId(null);
|
||||||
|
entity.setAuthor(null);
|
||||||
|
entity.setTitle(dto.getTitle());
|
||||||
|
entity.setBody(dto.getBody());
|
||||||
|
entity.setHashtags(dto.getHashtags());
|
||||||
|
entity.setStatus(dto.getStatus());
|
||||||
|
entity.setCreatedAt(LocalDateTime.now());
|
||||||
|
entity.setUpdatedAt(LocalDateTime.now());
|
||||||
|
entity.setPublishedAt(null);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package es.adeptusminiaturium.backend.mapper;
|
||||||
|
|
||||||
|
import es.adeptusminiaturium.backend.dto.PublicationDto;
|
||||||
|
import es.adeptusminiaturium.backend.model.Publication;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
public class PublicationMapper {
|
||||||
|
|
||||||
|
public static PublicationDto.Response toResponse(Publication entity) {
|
||||||
|
PublicationDto.Response dto = new PublicationDto.Response();
|
||||||
|
dto.setPublicationId(entity.getPublicationId());
|
||||||
|
dto.setPost(PostMapper.toResponse(entity.getPost()));
|
||||||
|
dto.setPlatform(entity.getPlatform());
|
||||||
|
dto.setExternalId(entity.getExternalId());
|
||||||
|
dto.setStatus(entity.getStatus());
|
||||||
|
dto.setPublishedAt(entity.getPublishedAt());
|
||||||
|
dto.setErrorMessage(entity.getErrorMessage());
|
||||||
|
dto.setCreatedAt(entity.getCreatedAt());
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Publication toEntity(PublicationDto.Request dto) {
|
||||||
|
Publication entity = new Publication();
|
||||||
|
entity.setPublicationId(null);
|
||||||
|
entity.setPost(null);
|
||||||
|
entity.setPlatform(dto.getPlatform());
|
||||||
|
entity.setExternalId(dto.getExternalId());
|
||||||
|
entity.setStatus(dto.getStatus());
|
||||||
|
entity.setPublishedAt(null);
|
||||||
|
entity.setErrorMessage(null);
|
||||||
|
entity.setCreatedAt(LocalDateTime.now());
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package es.adeptusminiaturium.backend.mapper;
|
||||||
|
|
||||||
|
import es.adeptusminiaturium.backend.dto.UserDto;
|
||||||
|
import es.adeptusminiaturium.backend.model.User;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class UserMapper {
|
||||||
|
|
||||||
|
public static UserDto.Response toResponse(User entity) {
|
||||||
|
UserDto.Response dto = new UserDto.Response();
|
||||||
|
dto.setUserId(entity.getUserId());
|
||||||
|
dto.setDisplayName(entity.getDisplayName());
|
||||||
|
dto.setUserName(entity.getUserName());
|
||||||
|
dto.setAvatar(entity.getAvatar());
|
||||||
|
dto.setRole(entity.getRole());
|
||||||
|
dto.setStatus(entity.getStatus());
|
||||||
|
dto.setCreatedAt(entity.getCreatedAt());
|
||||||
|
dto.setUpdatedAt(entity.getUpdatedAt());
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static User toEntity(UserDto.Request dto) {
|
||||||
|
User entity = new User();
|
||||||
|
entity.setUserId(UUID.randomUUID());
|
||||||
|
entity.setDisplayName(dto.getDisplayName());
|
||||||
|
entity.setUserName(dto.getUserName());
|
||||||
|
entity.setPassword(dto.getPassword());
|
||||||
|
entity.setAvatar(dto.getAvatar());
|
||||||
|
entity.setRole(dto.getRole());
|
||||||
|
entity.setStatus(dto.getStatus());
|
||||||
|
entity.setCreatedAt(LocalDateTime.now());
|
||||||
|
entity.setUpdatedAt(LocalDateTime.now());
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package es.adeptusminiaturium.backend.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonManagedReference;
|
||||||
|
import es.adeptusminiaturium.backend.enums.MediaType;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "media")
|
||||||
|
public class Media {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "media_id")
|
||||||
|
private Long mediaId;
|
||||||
|
|
||||||
|
@JsonManagedReference
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "post_id", nullable = false)
|
||||||
|
private Post post;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.ORDINAL)
|
||||||
|
@Column(name = "media_type", nullable = false)
|
||||||
|
private MediaType mediaType = MediaType.IMAGE;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 512)
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private Integer position = 0;
|
||||||
|
|
||||||
|
@CreationTimestamp
|
||||||
|
@Column(name = "created_at", insertable = false, updatable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
public Long getMediaId() {
|
||||||
|
return mediaId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMediaId(Long mediaId) {
|
||||||
|
this.mediaId = mediaId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Post getPost() {
|
||||||
|
return post;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPost(Post post) {
|
||||||
|
this.post = post;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MediaType getMediaType() {
|
||||||
|
return mediaType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMediaType(MediaType mediaType) {
|
||||||
|
this.mediaType = mediaType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUrl() {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUrl(String url) {
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getPosition() {
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPosition(Integer position) {
|
||||||
|
this.position = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package es.adeptusminiaturium.backend.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonManagedReference;
|
||||||
|
import es.adeptusminiaturium.backend.enums.PostStatus;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
import org.hibernate.annotations.UpdateTimestamp;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "posts")
|
||||||
|
public class Post {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "post_id")
|
||||||
|
private Long postId;
|
||||||
|
|
||||||
|
@JsonManagedReference
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "author_id", nullable = false)
|
||||||
|
private User author;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Column(columnDefinition = "TEXT")
|
||||||
|
private String body;
|
||||||
|
|
||||||
|
@Column(columnDefinition = "TEXT")
|
||||||
|
private String hashtags;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.ORDINAL)
|
||||||
|
@Column(nullable = false)
|
||||||
|
private PostStatus status = PostStatus.DRAFT;
|
||||||
|
|
||||||
|
@Column(name = "published_at")
|
||||||
|
private LocalDateTime publishedAt;
|
||||||
|
|
||||||
|
@CreationTimestamp
|
||||||
|
@Column(name = "created_at", insertable = false, updatable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@UpdateTimestamp
|
||||||
|
@Column(name = "updated_at", insertable = false, updatable = false)
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
|
private List<Media> media;
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
|
private List<Publication> publications;
|
||||||
|
|
||||||
|
public Long getPostId() {
|
||||||
|
return postId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPostId(Long postId) {
|
||||||
|
this.postId = postId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public User getAuthor() {
|
||||||
|
return author;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAuthor(User author) {
|
||||||
|
this.author = author;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBody() {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBody(String body) {
|
||||||
|
this.body = body;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getHashtags() {
|
||||||
|
return hashtags;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHashtags(String hashtags) {
|
||||||
|
this.hashtags = hashtags;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PostStatus getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(PostStatus status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getPublishedAt() {
|
||||||
|
return publishedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPublishedAt(LocalDateTime publishedAt) {
|
||||||
|
this.publishedAt = publishedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getUpdatedAt() {
|
||||||
|
return updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||||
|
this.updatedAt = updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Media> getMedia() {
|
||||||
|
return media;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMedia(List<Media> media) {
|
||||||
|
this.media = media;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Publication> getPublications() {
|
||||||
|
return publications;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPublications(List<Publication> publications) {
|
||||||
|
this.publications = publications;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package es.adeptusminiaturium.backend.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonManagedReference;
|
||||||
|
import es.adeptusminiaturium.backend.enums.Platform;
|
||||||
|
import es.adeptusminiaturium.backend.enums.PublicationStatus;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(
|
||||||
|
name = "publications",
|
||||||
|
uniqueConstraints = @UniqueConstraint(columnNames = {"post_id", "platform"})
|
||||||
|
)
|
||||||
|
public class Publication {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "publication_id")
|
||||||
|
private Long publicationId;
|
||||||
|
|
||||||
|
@JsonManagedReference
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "post_id", nullable = false)
|
||||||
|
private Post post;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.ORDINAL)
|
||||||
|
@Column(nullable = false)
|
||||||
|
private Platform platform;
|
||||||
|
|
||||||
|
private String externalId;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.ORDINAL)
|
||||||
|
@Column(nullable = false)
|
||||||
|
private PublicationStatus status = PublicationStatus.PENDING;
|
||||||
|
|
||||||
|
@Column(name = "published_at")
|
||||||
|
private LocalDateTime publishedAt;
|
||||||
|
|
||||||
|
@Column(columnDefinition = "TEXT")
|
||||||
|
private String errorMessage;
|
||||||
|
|
||||||
|
@CreationTimestamp
|
||||||
|
@Column(name = "created_at", insertable = false, updatable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
public Long getPublicationId() {
|
||||||
|
return publicationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPublicationId(Long publicationId) {
|
||||||
|
this.publicationId = publicationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Post getPost() {
|
||||||
|
return post;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPost(Post post) {
|
||||||
|
this.post = post;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Platform getPlatform() {
|
||||||
|
return platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlatform(Platform platform) {
|
||||||
|
this.platform = platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getExternalId() {
|
||||||
|
return externalId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExternalId(String externalId) {
|
||||||
|
this.externalId = externalId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PublicationStatus getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(PublicationStatus status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getPublishedAt() {
|
||||||
|
return publishedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPublishedAt(LocalDateTime publishedAt) {
|
||||||
|
this.publishedAt = publishedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getErrorMessage() {
|
||||||
|
return errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setErrorMessage(String errorMessage) {
|
||||||
|
this.errorMessage = errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package es.adeptusminiaturium.backend.model;
|
||||||
|
|
||||||
|
import es.adeptusminiaturium.backend.enums.UserRole;
|
||||||
|
import es.adeptusminiaturium.backend.enums.UserStatus;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
import org.hibernate.annotations.UpdateTimestamp;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "users")
|
||||||
|
public class User {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@Column(name = "user_id", nullable = false, updatable = false)
|
||||||
|
private UUID userId;
|
||||||
|
|
||||||
|
@Column(name = "display_name")
|
||||||
|
private String displayName;
|
||||||
|
|
||||||
|
@Column(name = "user_name", nullable = false, unique = true)
|
||||||
|
private String userName;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
private String avatar;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.ORDINAL)
|
||||||
|
@Column(nullable = false)
|
||||||
|
private UserRole role = UserRole.USER;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.ORDINAL)
|
||||||
|
@Column(nullable = false)
|
||||||
|
private UserStatus status = UserStatus.ACTIVE;
|
||||||
|
|
||||||
|
@CreationTimestamp
|
||||||
|
@Column(name = "created_at", insertable = false, updatable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@UpdateTimestamp
|
||||||
|
@Column(name = "updated_at", insertable = false, updatable = false)
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
public UUID getUserId() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserId(UUID userId) {
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDisplayName(String displayName) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserName() {
|
||||||
|
return userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserName(String userName) {
|
||||||
|
this.userName = userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPassword(String password) {
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAvatar() {
|
||||||
|
return avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAvatar(String avatar) {
|
||||||
|
this.avatar = avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserRole getRole() {
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRole(UserRole role) {
|
||||||
|
this.role = role;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserStatus getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(UserStatus status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getUpdatedAt() {
|
||||||
|
return updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||||
|
this.updatedAt = updatedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package es.adeptusminiaturium.backend.repository;
|
||||||
|
|
||||||
|
import es.adeptusminiaturium.backend.model.Media;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface MediaRepository extends JpaRepository<Media, Long> {
|
||||||
|
List<Media> findByPostPostId(Long postId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package es.adeptusminiaturium.backend.repository;
|
||||||
|
|
||||||
|
import es.adeptusminiaturium.backend.model.Post;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface PostRepository extends JpaRepository<Post, Long> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package es.adeptusminiaturium.backend.repository;
|
||||||
|
|
||||||
|
import es.adeptusminiaturium.backend.model.Publication;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface PublicationRepository extends JpaRepository<Publication, Long> {
|
||||||
|
List<Publication> findByPostPostId(Long postId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package es.adeptusminiaturium.backend.repository;
|
||||||
|
|
||||||
|
import es.adeptusminiaturium.backend.model.User;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface UserRepository extends JpaRepository<User, UUID> {
|
||||||
|
Optional<User> findByUserName(String userName);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package es.adeptusminiaturium.backend.security;
|
||||||
|
|
||||||
|
import es.adeptusminiaturium.backend.enums.UserStatus;
|
||||||
|
import es.adeptusminiaturium.backend.model.User;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class CustomPrincipal implements UserDetails {
|
||||||
|
|
||||||
|
private final User user;
|
||||||
|
|
||||||
|
public CustomPrincipal(User user) {
|
||||||
|
this.user = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||||
|
return List.of(new SimpleGrantedAuthority("ROLE_" + user.getRole().name()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPassword() {
|
||||||
|
return user.getPassword();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsername() {
|
||||||
|
return user.getUserName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return user.getStatus().equals(UserStatus.ACTIVE);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package es.adeptusminiaturium.backend.service;
|
||||||
|
|
||||||
|
import es.adeptusminiaturium.backend.repository.UserRepository;
|
||||||
|
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;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class CustomUserDetailsService implements UserDetailsService {
|
||||||
|
|
||||||
|
private final UserRepository userRepo;
|
||||||
|
|
||||||
|
public CustomUserDetailsService(UserRepository userRepo) {
|
||||||
|
this.userRepo = userRepo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||||
|
return userRepo.findByUserName(username)
|
||||||
|
.map(CustomPrincipal::new)
|
||||||
|
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package es.adeptusminiaturium.backend.service;
|
||||||
|
|
||||||
|
import es.adeptusminiaturium.backend.dto.MediaDto;
|
||||||
|
import es.adeptusminiaturium.backend.mapper.MediaMapper;
|
||||||
|
import es.adeptusminiaturium.backend.model.Media;
|
||||||
|
import es.adeptusminiaturium.backend.repository.MediaRepository;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class MediaService {
|
||||||
|
|
||||||
|
private final MediaRepository repo;
|
||||||
|
|
||||||
|
public MediaService(MediaRepository repo) { this.repo = repo; }
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public List<MediaDto.Response> getAll() {
|
||||||
|
return repo.findAll().stream().map(MediaMapper::toResponse).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public MediaDto.Response getById(Long id) {
|
||||||
|
return repo.findById(id).map(MediaMapper::toResponse).orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MediaDto.Response create(MediaDto.Request dto) {
|
||||||
|
Media media = MediaMapper.toEntity(dto);
|
||||||
|
return MediaMapper.toResponse(repo.save(media));
|
||||||
|
}
|
||||||
|
|
||||||
|
public MediaDto.Response update(Long id, MediaDto.Request dto) {
|
||||||
|
return repo.findById(id).map(media -> {
|
||||||
|
media.setMediaType(dto.getMediaType());
|
||||||
|
media.setUrl(dto.getUrl());
|
||||||
|
media.setPosition(dto.getPosition());
|
||||||
|
return MediaMapper.toResponse(repo.save(media));
|
||||||
|
}).orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(Long id) { repo.deleteById(id); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package es.adeptusminiaturium.backend.service;
|
||||||
|
|
||||||
|
import es.adeptusminiaturium.backend.dto.PostDto;
|
||||||
|
import es.adeptusminiaturium.backend.mapper.PostMapper;
|
||||||
|
import es.adeptusminiaturium.backend.model.Post;
|
||||||
|
import es.adeptusminiaturium.backend.repository.PostRepository;
|
||||||
|
import es.adeptusminiaturium.backend.repository.UserRepository;
|
||||||
|
import es.adeptusminiaturium.backend.validator.PostValidator;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class PostService {
|
||||||
|
|
||||||
|
private final PostRepository postRepository;
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
|
public PostService(PostRepository postRepository, UserRepository userRepository) {
|
||||||
|
this.postRepository = postRepository;
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public List<PostDto.Response> getAllPosts() {
|
||||||
|
return postRepository.findAll().stream()
|
||||||
|
.map(PostMapper::toResponse)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public PostDto.Response createPost(PostDto.Request dto) {
|
||||||
|
Post post = PostMapper.toEntity(dto);
|
||||||
|
post.setAuthor(userRepository.findById(dto.getAuthorId()).orElseThrow());
|
||||||
|
Post saved = postRepository.save(post);
|
||||||
|
return PostMapper.toResponse(saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PostDto.Response getPostById(Long postId) {
|
||||||
|
Post post = postRepository.findById(postId).orElseThrow();
|
||||||
|
return PostMapper.toResponse(post);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package es.adeptusminiaturium.backend.service;
|
||||||
|
|
||||||
|
import es.adeptusminiaturium.backend.dto.PublicationDto;
|
||||||
|
import es.adeptusminiaturium.backend.mapper.PublicationMapper;
|
||||||
|
import es.adeptusminiaturium.backend.model.Publication;
|
||||||
|
import es.adeptusminiaturium.backend.repository.PublicationRepository;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class PublicationService {
|
||||||
|
|
||||||
|
private final PublicationRepository repo;
|
||||||
|
|
||||||
|
public PublicationService(PublicationRepository repo) { this.repo = repo; }
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public List<PublicationDto.Response> getAll() {
|
||||||
|
return repo.findAll().stream().map(PublicationMapper::toResponse).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public PublicationDto.Response getById(Long id) {
|
||||||
|
return repo.findById(id).map(PublicationMapper::toResponse).orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PublicationDto.Response create(PublicationDto.Request dto) {
|
||||||
|
Publication pub = PublicationMapper.toEntity(dto);
|
||||||
|
return PublicationMapper.toResponse(repo.save(pub));
|
||||||
|
}
|
||||||
|
|
||||||
|
public PublicationDto.Response update(Long id, PublicationDto.Request dto) {
|
||||||
|
return repo.findById(id).map(pub -> {
|
||||||
|
pub.setPlatform(dto.getPlatform());
|
||||||
|
pub.setExternalId(dto.getExternalId());
|
||||||
|
pub.setStatus(dto.getStatus());
|
||||||
|
return PublicationMapper.toResponse(repo.save(pub));
|
||||||
|
}).orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(Long id) { repo.deleteById(id); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package es.adeptusminiaturium.backend.service;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class UserService {
|
||||||
|
|
||||||
|
private final UserRepository repo;
|
||||||
|
|
||||||
|
public UserService(UserRepository repo) {
|
||||||
|
this.repo = repo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<User> getAllUsers() {
|
||||||
|
return repo.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public User getUser(UUID userId) {
|
||||||
|
return repo.findById(userId)
|
||||||
|
.orElseThrow(() -> new NotFoundException("User not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
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(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package es.adeptusminiaturium.backend.validator;
|
||||||
|
|
||||||
|
import es.adeptusminiaturium.backend.model.Media;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class MediaValidator {
|
||||||
|
|
||||||
|
public static List<String> validate(Media media) {
|
||||||
|
List<String> errors = new ArrayList<>();
|
||||||
|
|
||||||
|
if (media.getPost() == null) {
|
||||||
|
errors.add("Media must belong to a post");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (media.getUrl() == null || media.getUrl().isBlank()) {
|
||||||
|
errors.add("URL cannot be empty");
|
||||||
|
} else if (media.getUrl().length() > 512) {
|
||||||
|
errors.add("URL too long");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (media.getPosition() == null || media.getPosition() < 0) {
|
||||||
|
errors.add("Position cannot be negative");
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void validateOrThrow(Media media) {
|
||||||
|
List<String> errors = validate(media);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException(String.join(", ", errors));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package es.adeptusminiaturium.backend.validator;
|
||||||
|
|
||||||
|
import es.adeptusminiaturium.backend.model.Post;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class PostValidator {
|
||||||
|
|
||||||
|
public static List<String> validate(Post post) {
|
||||||
|
List<String> errors = new ArrayList<>();
|
||||||
|
|
||||||
|
if (post.getAuthor() == null) {
|
||||||
|
errors.add("Post must have an author");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (post.getTitle() == null || post.getTitle().isBlank()) {
|
||||||
|
errors.add("Title cannot be empty");
|
||||||
|
} else if (post.getTitle().length() > 255) {
|
||||||
|
errors.add("Title too long");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (post.getBody() != null && post.getBody().length() > 10000) {
|
||||||
|
errors.add("Body too long");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (post.getHashtags() != null && post.getHashtags().length() > 1000) {
|
||||||
|
errors.add("Hashtags too long");
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void validateOrThrow(Post post) {
|
||||||
|
List<String> errors = validate(post);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException(String.join(", ", errors));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package es.adeptusminiaturium.backend.validator;
|
||||||
|
|
||||||
|
import es.adeptusminiaturium.backend.enums.Platform;
|
||||||
|
import es.adeptusminiaturium.backend.model.Publication;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class PublicationValidator {
|
||||||
|
|
||||||
|
public static List<String> validate(Publication pub) {
|
||||||
|
List<String> errors = new ArrayList<>();
|
||||||
|
|
||||||
|
if (pub.getPost() == null) {
|
||||||
|
errors.add("Publication must belong to a post");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pub.getPlatform() == null) {
|
||||||
|
errors.add("Platform cannot be null");
|
||||||
|
} else if (!Platform.valuesList().contains(pub.getPlatform())) {
|
||||||
|
errors.add("Invalid platform");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pub.getStatus() == null) {
|
||||||
|
errors.add("Status cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pub.getExternalId() != null && pub.getExternalId().length() > 255) {
|
||||||
|
errors.add("ExternalId too long");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pub.getErrorMessage() != null && pub.getErrorMessage().length() > 1000) {
|
||||||
|
errors.add("ErrorMessage too long");
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void validateOrThrow(Publication pub) {
|
||||||
|
List<String> errors = validate(pub);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException(String.join(", ", errors));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package es.adeptusminiaturium.backend.validator;
|
||||||
|
|
||||||
|
import es.adeptusminiaturium.backend.model.User;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class UserValidator {
|
||||||
|
|
||||||
|
public static List<String> validate(User user) {
|
||||||
|
List<String> errors = new ArrayList<>();
|
||||||
|
|
||||||
|
if (user.getUserName() == null || user.getUserName().isBlank()) {
|
||||||
|
errors.add("Username cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.getPassword() == null || user.getPassword().length() < 8) {
|
||||||
|
errors.add("Password must have at least 8 characters");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.getDisplayName() != null && user.getDisplayName().length() > 255) {
|
||||||
|
errors.add("DisplayName too long");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.getAvatar() != null && user.getAvatar().length() > 255) {
|
||||||
|
errors.add("Avatar URL too long");
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void validateOrThrow(User user) {
|
||||||
|
List<String> errors = validate(user);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException(String.join(", ", errors));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
backend/src/main/resources/application-dev.yaml
Normal file
16
backend/src/main/resources/application-dev.yaml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
server:
|
||||||
|
port: 8080
|
||||||
|
|
||||||
|
spring:
|
||||||
|
jpa:
|
||||||
|
show-sql: true
|
||||||
|
datasource:
|
||||||
|
url: jdbc:mariadb://localhost:3306/miniaturium
|
||||||
|
username: admin
|
||||||
|
password: ${DB_PASS}
|
||||||
|
driver-class-name: org.mariadb.jdbc.Driver
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
org.hibernate.SQL: DEBUG
|
||||||
|
org.hibernate.orm.jdbc.bind: TRACE
|
||||||
13
backend/src/main/resources/application-prod.yaml
Normal file
13
backend/src/main/resources/application-prod.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
server:
|
||||||
|
port: 8080
|
||||||
|
|
||||||
|
spring:
|
||||||
|
datasource:
|
||||||
|
url: jdbc:mariadb://mariadb:3306/miniaturium
|
||||||
|
username: ${DB_USER}
|
||||||
|
password: ${DB_PASS}
|
||||||
|
driver-class-name: org.mariadb.jdbc.Driver
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
org.hibernate.SQL: WARN
|
||||||
25
backend/src/main/resources/application.yaml
Normal file
25
backend/src/main/resources/application.yaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: backend
|
||||||
|
|
||||||
|
jpa:
|
||||||
|
open-in-view: false
|
||||||
|
hibernate:
|
||||||
|
ddl-auto: validate
|
||||||
|
properties:
|
||||||
|
hibernate:
|
||||||
|
jdbc:
|
||||||
|
time_zone: UTC
|
||||||
|
|
||||||
|
jackson:
|
||||||
|
default-property-inclusion: non_null
|
||||||
|
time-zone: Europe/Madrid
|
||||||
|
|
||||||
|
jwt:
|
||||||
|
expiration-ms: 3600000
|
||||||
|
|
||||||
|
management:
|
||||||
|
endpoints:
|
||||||
|
web:
|
||||||
|
exposure:
|
||||||
|
include: health,info
|
||||||
52
backend/src/main/resources/bd.sql
Normal file
52
backend/src/main/resources/bd.sql
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
CREATE TABLE users(
|
||||||
|
user_id UUID PRIMARY KEY,
|
||||||
|
display_name VARCHAR(255),
|
||||||
|
user_name VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
avatar VARCHAR(255) DEFAULT NULL,
|
||||||
|
role TINYINT NOT NULL DEFAULT 0, -- 0 = user, 1 = admin
|
||||||
|
status TINYINT NOT NULL DEFAULT 1, -- 0 = inactivo, 1 = activo
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP(),
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP() ON UPDATE CURRENT_TIMESTAMP()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE posts(
|
||||||
|
post_id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
author_id UUID NOT NULL,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
body TEXT,
|
||||||
|
hashtags TEXT,
|
||||||
|
status TINYINT NOT NULL DEFAULT 0, -- 0 = draft, 1 = published, 2 = archived
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP(),
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP() ON UPDATE CURRENT_TIMESTAMP(),
|
||||||
|
published_at TIMESTAMP NULL,
|
||||||
|
FOREIGN KEY (author_id) REFERENCES users(user_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE media(
|
||||||
|
media_id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
post_id BIGINT NOT NULL,
|
||||||
|
media_type TINYINT NOT NULL DEFAULT 0, -- 0 = image, 1 = video
|
||||||
|
url VARCHAR(512) NOT NULL,
|
||||||
|
position BIGINT NOT NULL DEFAULT 0,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP(),
|
||||||
|
FOREIGN KEY (post_id) REFERENCES posts(post_id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE publications(
|
||||||
|
publication_id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
post_id BIGINT NOT NULL,
|
||||||
|
platform TINYINT NOT NULL, -- 0 = web, 1 = instagram, 2 = tiktok, 3 = twitter
|
||||||
|
external_id VARCHAR(255) DEFAULT NULL,
|
||||||
|
status TINYINT NOT NULL DEFAULT 0, -- 0 = pending, 1 = published, 2 = failed
|
||||||
|
published_at TIMESTAMP NULL,
|
||||||
|
error_message TEXT DEFAULT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP(),
|
||||||
|
UNIQUE (post_id, platform),
|
||||||
|
FOREIGN KEY (post_id) REFERENCES posts(post_id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_posts_author ON posts(author_id);
|
||||||
|
CREATE INDEX idx_posts_status_published ON posts(status, published_at);
|
||||||
|
CREATE INDEX idx_media_post_position ON media(post_id, position);
|
||||||
|
CREATE INDEX idx_publications_status ON publications(status);
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title></title>
|
<title>Adeptus Miniaturium</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root" class="p-0 m-0"></div>
|
<div id="root" class="p-0 m-0"></div>
|
||||||
@@ -19,10 +19,13 @@
|
|||||||
"bootstrap": "^5.3.7",
|
"bootstrap": "^5.3.7",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"framer-motion": "^12.23.12",
|
"framer-motion": "^12.23.12",
|
||||||
|
"i18next": "^25.8.10",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-bootstrap": "^2.10.10",
|
"react-bootstrap": "^2.10.10",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
|
"react-i18next": "^16.5.4",
|
||||||
"react-router-dom": "^7.7.1",
|
"react-router-dom": "^7.7.1",
|
||||||
|
"react-router-hash-link": "^2.4.3",
|
||||||
"react-slick": "^0.30.3",
|
"react-slick": "^0.30.3",
|
||||||
"slick-carousel": "^1.8.1"
|
"slick-carousel": "^1.8.1"
|
||||||
},
|
},
|
||||||
@@ -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",
|
||||||
BIN
frontend/public/images/mini_1.jpeg
Normal file
BIN
frontend/public/images/mini_1.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 237 KiB |
BIN
frontend/public/images/mini_2.jpeg
Normal file
BIN
frontend/public/images/mini_2.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 156 KiB |
BIN
frontend/public/images/mini_3.jpeg
Normal file
BIN
frontend/public/images/mini_3.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
BIN
frontend/public/images/mini_4.jpeg
Normal file
BIN
frontend/public/images/mini_4.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 163 KiB |
BIN
frontend/public/images/mini_5.jpeg
Normal file
BIN
frontend/public/images/mini_5.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 233 KiB |
BIN
frontend/public/images/pfp.jpg
Normal file
BIN
frontend/public/images/pfp.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
26
frontend/src/App.jsx
Normal file
26
frontend/src/App.jsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import Footer from '@/components/Footer'
|
||||||
|
import Header from '@/components/Header'
|
||||||
|
import NavBar from '@/components/NavBar/NavBar'
|
||||||
|
import Home from '@/pages/Home'
|
||||||
|
import { Route, Routes, useLocation } from 'react-router-dom'
|
||||||
|
import Login from './pages/Login'
|
||||||
|
import "./i18n";
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
const withoutFooter = ["/login"]
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header profilePic={"/images/pfp.jpg"}/>
|
||||||
|
<NavBar />
|
||||||
|
<Routes>
|
||||||
|
<Route path='/' element={<Home />} />
|
||||||
|
<Route path='/login' element={<Login />} />
|
||||||
|
</Routes>
|
||||||
|
{!withoutFooter.includes(location.pathname) && <Footer />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
@@ -11,10 +11,12 @@ import CustomContainer from '@/components/CustomContainer.jsx';
|
|||||||
import ContentWrapper from '@/components/ContentWrapper.jsx';
|
import ContentWrapper from '@/components/ContentWrapper.jsx';
|
||||||
|
|
||||||
import '@/css/LoginForm.css';
|
import '@/css/LoginForm.css';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const LoginForm = () => {
|
const LoginForm = () => {
|
||||||
const { login, error } = useContext(AuthContext);
|
const { login, error } = useContext(AuthContext);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [formState, setFormState] = useState({
|
const [formState, setFormState] = useState({
|
||||||
emailOrUserName: "",
|
emailOrUserName: "",
|
||||||
@@ -47,24 +49,21 @@ const LoginForm = () => {
|
|||||||
await login(loginBody);
|
await login(loginBody);
|
||||||
navigate("/");
|
navigate("/");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error de login:", err.message);
|
console.error("Error:", err.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomContainer>
|
<CustomContainer>
|
||||||
<ContentWrapper>
|
<ContentWrapper>
|
||||||
<div className="login-card card shadow p-5 rounded-5 mx-auto col-12 col-md-8 col-lg-6 col-xl-5 d-flex flex-column gap-4">
|
<div className="login-card card shadow rounded-0 mx-auto col-12 col-md-8 col-lg-6 col-xl-5 d-flex flex-column gap-4">
|
||||||
<h1 className="text-center">Inicio de sesión</h1>
|
<h1 className="text-center">{t("login.title")}</h1>
|
||||||
<Form className="d-flex flex-column gap-5" onSubmit={handleSubmit}>
|
<Form className="d-flex flex-column gap-5" onSubmit={handleSubmit}>
|
||||||
<div className="d-flex flex-column gap-3">
|
<div className="d-flex flex-column gap-3">
|
||||||
<FloatingLabel
|
<FloatingLabel
|
||||||
controlId="floatingUsuario"
|
controlId="floatingUsuario"
|
||||||
label={
|
label={
|
||||||
<>
|
t("login.user_label")
|
||||||
<FontAwesomeIcon icon={faUser} className="me-2" />
|
|
||||||
Usuario o Email
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
@@ -73,7 +72,7 @@ const LoginForm = () => {
|
|||||||
name="emailOrUserName"
|
name="emailOrUserName"
|
||||||
value={formState.emailOrUserName}
|
value={formState.emailOrUserName}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="rounded-4"
|
className="rounded-0"
|
||||||
/>
|
/>
|
||||||
</FloatingLabel>
|
</FloatingLabel>
|
||||||
|
|
||||||
@@ -82,20 +81,6 @@ const LoginForm = () => {
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
name="password"
|
name="password"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="d-flex flex-column flex-sm-row justify-content-between align-items-center gap-2">
|
|
||||||
<Form.Check
|
|
||||||
type="checkbox"
|
|
||||||
name="keepLoggedIn"
|
|
||||||
label="Mantener sesión iniciada"
|
|
||||||
className="text-secondary"
|
|
||||||
value={formState.keepLoggedIn}
|
|
||||||
onChange={(e) => { formState.keepLoggedIn = e.target.checked; setFormState({ ...formState }) }}
|
|
||||||
/>
|
|
||||||
{/*<Link disabled to="#" className="muted">
|
|
||||||
Olvidé mi contraseña
|
|
||||||
</Link>*/}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
@@ -105,8 +90,8 @@ const LoginForm = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Button type="submit" className="w-75 padding-4 rounded-4 border-0 shadow-sm login-button">
|
<Button type="submit" className="border-0 w-75 padding-4 rounded-0 shadow-sm login-button">
|
||||||
Iniciar sesión
|
{t("login.button")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -4,9 +4,11 @@ import '@/css/PasswordInput.css';
|
|||||||
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faEye, faEyeSlash, faKey } from '@fortawesome/free-solid-svg-icons';
|
import { faEye, faEyeSlash, faKey } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const PasswordInput = ({ value, onChange, name = "password" }) => {
|
const PasswordInput = ({ value, onChange, name = "password" }) => {
|
||||||
const [show, setShow] = useState(false);
|
const [show, setShow] = useState(false);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const toggleShow = () => setShow(prev => !prev);
|
const toggleShow = () => setShow(prev => !prev);
|
||||||
|
|
||||||
@@ -15,10 +17,7 @@ const PasswordInput = ({ value, onChange, name = "password" }) => {
|
|||||||
<FloatingLabel
|
<FloatingLabel
|
||||||
controlId="passwordInput"
|
controlId="passwordInput"
|
||||||
label={
|
label={
|
||||||
<>
|
t("login.password_label")
|
||||||
<FontAwesomeIcon icon={faKey} className="me-2" />
|
|
||||||
Contraseña
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
@@ -27,7 +26,7 @@ const PasswordInput = ({ value, onChange, name = "password" }) => {
|
|||||||
value={value}
|
value={value}
|
||||||
placeholder=""
|
placeholder=""
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
className="rounded-4 pe-5"
|
className="rounded-0 pe-5"
|
||||||
/>
|
/>
|
||||||
</FloatingLabel>
|
</FloatingLabel>
|
||||||
|
|
||||||
@@ -2,8 +2,10 @@ import PropTypes from 'prop-types';
|
|||||||
|
|
||||||
const CustomContainer = ({ children }) => {
|
const CustomContainer = ({ children }) => {
|
||||||
return (
|
return (
|
||||||
<main className="px-4 py-5">
|
<main className="container my-5">
|
||||||
|
<div className="d-flex flex-column gap-5">
|
||||||
{children}
|
{children}
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user