created: monorepo with front & back

This commit is contained in:
Jose
2026-03-07 00:00:09 +01:00
commit 76b30df1ba
272 changed files with 23425 additions and 0 deletions

1
backend/core/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target/

9
backend/core/core.iml Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="AdditionalModuleElements">
<content url="file://$MODULE_DIR$" dumb="true">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
</content>
</component>
</module>

107
backend/core/pom.xml Normal file
View File

@@ -0,0 +1,107 @@
<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>es.huertosbellavista</groupId>
<artifactId>backend</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>core</artifactId>
<properties>
<maven.compiler.source>25</maven.compiler.source>
<maven.compiler.target>25</maven.compiler.target>
</properties>
<repositories>
<repository>
<id>gitea</id>
<url>https://git.miarma.net/api/packages/Gallardo7761/maven</url>
</repository>
</repositories>
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</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>
<dependency>
<groupId>net.miarma</groupId>
<artifactId>backlib</artifactId>
<version>1.1.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,14 @@
package es.huertosbellavista.backend.core;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages = {
"es.huertosbellavista.backend.core",
"net.miarma.backlib"
})
public class CoreApplication {
public static void main(String[] args) {
SpringApplication.run(CoreApplication.class, args);
}
}

View File

@@ -0,0 +1,67 @@
package es.huertosbellavista.backend.core.config;
import es.huertosbellavista.backend.core.security.JwtFilter;
import net.miarma.backlib.http.RestAccessDeniedHandler;
import net.miarma.backlib.http.RestAuthEntryPoint;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
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.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 JwtFilter jwtFilter;
private final RestAuthEntryPoint authEntryPoint;
private final RestAccessDeniedHandler accessDeniedHandler;
private final CorsConfigurationSource corsConfigurationSource;
public SecurityConfig(
JwtFilter jwtFilter,
RestAuthEntryPoint authEntryPoint,
RestAccessDeniedHandler accessDeniedHandler,
Optional<CorsConfigurationSource> corsConfigurationSource
) {
this.jwtFilter = jwtFilter;
this.authEntryPoint = authEntryPoint;
this.accessDeniedHandler = accessDeniedHandler;
this.corsConfigurationSource = corsConfigurationSource.orElse(null);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
if (corsConfigurationSource != null) {
http.cors(Customizer.withDefaults());
}
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()
.requestMatchers("/auth/refresh").permitAll()
.requestMatchers("/auth/change-password").permitAll()
.requestMatchers("/auth/register").permitAll()
.requestMatchers("/test").permitAll()
.requestMatchers("/screenshot").permitAll()
.anyRequest().authenticated()
);
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}

View File

@@ -0,0 +1,13 @@
package es.huertosbellavista.backend.core.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
public class WebClientConfig {
@Bean
public WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
}

View File

@@ -0,0 +1,116 @@
package es.huertosbellavista.backend.core.controller;
import java.util.Map;
import java.util.UUID;
import net.miarma.backlib.dto.*;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import es.huertosbellavista.backend.core.service.AuthService;
import net.miarma.backlib.security.JwtService;
@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(@Valid @RequestBody LoginRequest request) {
LoginResponse response = authService.login(request);
return ResponseEntity.ok(
new LoginResponse(response.token(), response.user(), response.account())
);
}
@PostMapping("/register")
public ResponseEntity<LoginResponse> register(@RequestBody RegisterRequest request) {
return ResponseEntity.ok(authService.register(request));
}
@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 hay token",
"/v2/core/auth/change-password"
)
);
}
String token = authHeader.substring(7);
if (!jwtService.validateToken(token)) {
return ResponseEntity.status(401).body(
new ApiErrorDto(
401,
"Unauthorized",
"Invalid token",
"/v2/core/auth/change-password"
)
);
}
UUID userId = jwtService.getUserId(token);
Byte serviceId = jwtService.getServiceId(token);
String newToken = jwtService.generateToken(userId, serviceId);
return ResponseEntity.ok(Map.of(
"token", newToken,
"userId", userId,
"serviceId", serviceId
));
}
@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",
"/v2/core/auth/change-password"
)
);
}
String token = authHeader.substring(7);
if (!jwtService.validateToken(token)) {
return ResponseEntity.status(401).body(
new ApiErrorDto(
401,
"Unauthorized",
"Invalid token",
"/v2/core/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));
}
}

View File

@@ -0,0 +1,100 @@
package es.huertosbellavista.backend.core.controller;
import java.util.List;
import java.util.UUID;
import es.huertosbellavista.backend.core.dto.UpdateCredentialDto;
import es.huertosbellavista.backend.core.mapper.CredentialMapper;
import es.huertosbellavista.backend.core.security.CorePrincipal;
import net.miarma.backlib.dto.*;
import net.miarma.backlib.exception.ForbiddenException;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import es.huertosbellavista.backend.core.model.Credential;
import es.huertosbellavista.backend.core.service.CredentialService;
@RestController
@RequestMapping("/credentials")
public class CredentialController {
private final CredentialService credentialService;
public CredentialController(CredentialService credentialService) {
this.credentialService = credentialService;
}
@GetMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<List<Credential>> getAll() {
return ResponseEntity.ok(credentialService.getAll());
}
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<CredentialDto> create(@RequestBody CreateCredentialDto dto) {
return ResponseEntity.ok(
CredentialMapper.toDto(
credentialService.create(
CredentialMapper.toEntity(dto)))
);
}
@GetMapping("/user/{userId}")
public ResponseEntity<List<CredentialDto>> getByUserId(@PathVariable("userId") UUID userId) {
CorePrincipal principal = (CorePrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (!principal.getRole().equals((byte)1) && !principal.getUserId().equals(userId)) {
throw new ForbiddenException("No tienes permiso");
}
return ResponseEntity.ok(credentialService.getByUserId(userId).stream()
.map(CredentialMapper::toDto)
.toList());
}
@GetMapping("/{credential_id}")
public ResponseEntity<CredentialDto> getById(@PathVariable("credential_id") UUID credentialId) {
CorePrincipal principal = (CorePrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Credential cred = credentialService.getById(credentialId);
if (!principal.getRole().equals((byte)1) && !cred.getUserId().equals(principal.getUserId())) {
throw new ForbiddenException("No tienes permiso");
}
return ResponseEntity.ok(CredentialMapper.toDto(cred));
}
@PutMapping("/{credential_id}")
public ResponseEntity<CredentialDto> update(
@PathVariable("credential_id") UUID credentialId,
@RequestBody UpdateCredentialDto dto
) {
CorePrincipal principal = (CorePrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Credential cred = credentialService.getById(credentialId);
if (!principal.getRole().equals((byte)1) && !cred.getUserId().equals(principal.getUserId())) {
throw new ForbiddenException("No tienes permiso");
}
return ResponseEntity.ok(
CredentialMapper.toDto(
credentialService.update(credentialId, CredentialMapper.toEntity(dto))
)
);
}
@PutMapping("/{credential_id}/full")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<CredentialDto> updateFull(
@PathVariable("credential_id") UUID credentialId,
@RequestBody CredentialDto dto
) {
return ResponseEntity.ok(
CredentialMapper.toDto(
credentialService.update(credentialId, CredentialMapper.toEntity(dto))
)
);
}
@GetMapping("/{service_id}/{user_id}/status")
public ResponseEntity<Byte> getStatus(@PathVariable("user_id") UUID userId, @PathVariable("service_id") Byte serviceId) {
return ResponseEntity.ok(credentialService.getStatus(userId, serviceId));
}
}

View File

@@ -0,0 +1,89 @@
package es.huertosbellavista.backend.core.controller;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import es.huertosbellavista.backend.core.mapper.FileMapper;
import net.miarma.backlib.dto.FileDto;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import es.huertosbellavista.backend.core.model.File;
import es.huertosbellavista.backend.core.service.FileService;
@RestController
@RequestMapping("/files")
public class FileController {
private final FileService fileService;
public FileController(FileService fileService) {
this.fileService = fileService;
}
@GetMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<List<File>> getAll() {
List<File> files = fileService.getAll();
return ResponseEntity.ok(files);
}
@GetMapping("/{file_id}")
@PreAuthorize("hasRole('ADMIN') or @fileService.isOwner(#file_id, authentication.principal.userId)")
public ResponseEntity<File> getById(@PathVariable("file_id") UUID fileId) {
File file = fileService.getById(fileId);
return ResponseEntity.ok(file);
}
@PostMapping(consumes = "multipart/form-data")
@PreAuthorize("hasRole('ADMIN') or #uploadedBy == authentication.principal.userId")
public ResponseEntity<FileDto.Response> create(
@RequestPart("file") MultipartFile file,
@RequestPart("fileName") String fileName,
@RequestPart("mimeType") String mimeType,
@RequestPart("uploadedBy") UUID uploadedBy,
@RequestPart("context") Integer context
) throws IOException {
File entity = new File();
entity.setFileName(fileName);
entity.setMimeType(mimeType);
entity.setUploadedBy(uploadedBy);
entity.setContext(context.byteValue());
File created = fileService.create(entity, file.getBytes());
return ResponseEntity.status(HttpStatus.CREATED)
.body(FileMapper.toResponse(created));
}
@PutMapping("/{file_id}")
@PreAuthorize("hasRole('ADMIN') or @fileService.isOwner(#file_id, authentication.principal.userId)")
public ResponseEntity<File> update(@PathVariable("file_id") UUID fileId, @RequestBody FileDto.Request request) {
File updated = fileService.update(fileId, FileMapper.toEntity(request));
return ResponseEntity.ok(updated);
}
@DeleteMapping("/{file_id}")
@PreAuthorize("hasRole('ADMIN') or @fileService.isOwner(#file_id, authentication.principal.userId)")
public ResponseEntity<Void> delete(@PathVariable("file_id") UUID fileId, @RequestBody Map<String,String> body) throws IOException {
String filePath = body.get("file_path");
Files.deleteIfExists(Paths.get(filePath));
fileService.delete(fileId);
return ResponseEntity.ok().build();
}
}

View File

@@ -0,0 +1,173 @@
package es.huertosbellavista.backend.core.controller;
import java.util.List;
import java.util.UUID;
import es.huertosbellavista.backend.core.mapper.CredentialMapper;
import es.huertosbellavista.backend.core.model.Credential;
import es.huertosbellavista.backend.core.service.CredentialService;
import net.miarma.backlib.dto.*;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import es.huertosbellavista.backend.core.mapper.UserMapper;
import es.huertosbellavista.backend.core.model.User;
import net.miarma.backlib.security.JwtService;
import es.huertosbellavista.backend.core.service.UserService;
@RestController
@RequestMapping("/users")
public class UserController {
private UserService userService;
private CredentialService credentialService;
private JwtService jwtService;
public UserController(UserService userService, CredentialService credentialService, JwtService jwtService) {
this.userService = userService;
this.credentialService = credentialService;
this.jwtService = jwtService;
}
@GetMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<List<UserDto>> getAll() {
return ResponseEntity.ok(
userService.getAll()
.stream()
.map(UserMapper::toDto)
.toList()
);
}
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<UserDto> create(@RequestBody CreateUserDto dto) {
return ResponseEntity.ok(
UserMapper.toDto(
userService.create(
UserMapper.fromCreateDto(dto)))
);
}
@GetMapping("/{user_id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<UserDto> getById(@PathVariable("user_id") UUID userId) {
User user = userService.getById(userId);
return ResponseEntity.ok(UserMapper.toDto(user));
}
@GetMapping("/service/{service_id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<List<UserWithCredentialDto>> getAllWithCredentials(
@PathVariable("service_id") Byte serviceId
) {
List<Credential> credentials = credentialService.getByServiceIdFetchUser(serviceId);
List<UserWithCredentialDto> result = credentials.stream()
.map(cred -> new UserWithCredentialDto(
UserMapper.toDto(cred.getUser()),
CredentialMapper.toDto(cred)
))
.toList();
return ResponseEntity.ok(result);
}
@GetMapping("/{user_id}/service/{service_id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<UserWithCredentialDto> getByIdWithCredentials(
@PathVariable("user_id") UUID userId,
@PathVariable("service_id") Byte serviceId
) {
User user = userService.getById(userId);
Credential credential = credentialService.getByUserIdAndService(userId, serviceId);
return ResponseEntity.ok(
UserMapper.toDtoWithCredentials(
UserMapper.toDto(user),
CredentialMapper.toDto(credential)
)
);
}
@PutMapping("/{user_id}")
@PreAuthorize("hasRole('ADMIN') or #userId == principal.userId")
public ResponseEntity<UserDto> update(
@PathVariable("user_id") UUID userId,
@RequestBody UserDto dto
) {
User updated = userService.update(userId, UserMapper.fromDto(dto));
return ResponseEntity.ok(UserMapper.toDto(updated));
}
@GetMapping("/{user_id}/avatar")
public ResponseEntity<String> getAvatar(@PathVariable("user_id") UUID userId) {
return ResponseEntity.ok(userService.getById(userId).getAvatar());
}
@PutMapping("/{user_id}/avatar")
public ResponseEntity<UserDto> updateAvatar(@PathVariable("user_id") UUID userId, @RequestBody ChangeAvatarRequest avatar) {
return ResponseEntity.ok(userService.updateAvatar(userId, avatar));
}
@GetMapping("/{user_id}/status")
public ResponseEntity<Byte> getStatus(@PathVariable("user_id") UUID userId) {
return ResponseEntity.ok(userService.getStatus(userId));
}
@PutMapping("/{user_id}/status")
public ResponseEntity<Void> updateStatus(
@PathVariable("user_id") UUID userId,
@RequestBody ChangeStatusRequest req
) {
userService.updateStatus(userId, req.status());
return ResponseEntity.noContent().build();
}
@GetMapping("/{user_id}/role")
public ResponseEntity<Byte> getRole(@PathVariable("user_id") UUID userId) {
return ResponseEntity.ok(userService.getRole(userId));
}
@PutMapping("/{user_id}/role")
public ResponseEntity<Void> updateRole(
@PathVariable("user_id") UUID userid,
@RequestBody ChangeRoleRequest req
) {
userService.updateRole(userid, req.role());
return ResponseEntity.noContent().build();
}
@GetMapping("/{user_id}/exists")
public ResponseEntity<UserExistsResponse> exists(@PathVariable("user_id") UUID userId) {
boolean exists = userService.exists(userId);
return ResponseEntity.ok(new UserExistsResponse(exists));
}
@GetMapping("/me")
public ResponseEntity<UserDto> getMe(@RequestHeader("Authorization") String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
String token = authHeader.substring(7);
UUID userId;
try {
userId = jwtService.getUserId(token);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
User user = userService.getById(userId);
return ResponseEntity.ok(UserMapper.toDto(user));
}
@DeleteMapping("/{user_id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Void> delete(@PathVariable("user_id") UUID userId) {
userService.delete(userId);
return ResponseEntity.ok().build();
}
}

View File

@@ -0,0 +1,41 @@
package es.huertosbellavista.backend.core.dto;
import java.util.UUID;
public class UpdateCredentialDto {
private String email;
private String username;
private Byte status;
public UpdateCredentialDto() { }
public UpdateCredentialDto(String email, String username, Byte status) {
this.email = email;
this.username = username;
this.status = status;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Byte getStatus() {
return status;
}
public void setStatus(Byte status) {
this.status = status;
}
}

View File

@@ -0,0 +1,78 @@
package es.huertosbellavista.backend.core.mapper;
import es.huertosbellavista.backend.core.dto.UpdateCredentialDto;
import es.huertosbellavista.backend.core.model.Credential;
import net.miarma.backlib.dto.CreateCredentialDto;
import net.miarma.backlib.dto.CredentialDto;
public class CredentialMapper {
public static CredentialDto toDto(Credential c) {
if (c == null) return null;
return new CredentialDto(
c.getCredentialId(),
c.getUserId(),
c.getServiceId(),
c.getUsername(),
c.getEmail(),
c.getStatus(),
c.getCreatedAt(),
c.getUpdatedAt()
);
}
public static CreateCredentialDto toCreateDto(Credential c) {
if (c == null) return null;
return new CreateCredentialDto(
c.getUserId(),
c.getServiceId(),
c.getUsername(),
c.getEmail(),
c.getPassword(),
c.getStatus()
);
}
public static Credential toEntity(CredentialDto dto) {
if (dto == null) return null;
Credential c = new Credential();
c.setCredentialId(dto.getCredentialId());
c.setUserId(dto.getUserId());
c.setServiceId(dto.getServiceId());
c.setUsername(dto.getUsername());
c.setEmail(dto.getEmail());
c.setStatus(dto.getStatus());
c.setCreatedAt(dto.getCreatedAt());
c.setUpdatedAt(dto.getUpdatedAt());
return c;
}
public static Credential toEntity(CreateCredentialDto dto) {
if (dto == null) return null;
Credential c = new Credential();
c.setUserId(dto.getUserId());
c.setServiceId(dto.getServiceId());
c.setUsername(dto.getUsername());
c.setEmail(dto.getEmail());
c.setPassword(dto.getPassword());
c.setStatus(dto.getStatus());
return c;
}
public static Credential toEntity(UpdateCredentialDto dto) {
if (dto == null) return null;
Credential c = new Credential();
c.setEmail(dto.getEmail());
c.setUsername(dto.getUsername());
c.setStatus(dto.getStatus());
return c;
}
}

View File

@@ -0,0 +1,37 @@
package es.huertosbellavista.backend.core.mapper;
import es.huertosbellavista.backend.core.model.File;
import net.miarma.backlib.dto.FileDto;
import java.util.UUID;
public class FileMapper {
private FileMapper() {}
public static FileDto.Response toResponse(File file) {
FileDto.Response res = new FileDto.Response();
res.setFileId(file.getFileId());
res.setFileName(file.getFileName());
res.setFilePath(file.getFilePath());
res.setMimeType(file.getMimeType());
res.setUploadedBy(file.getUploadedBy());
res.setUploadedAt(file.getUploadedAt());
res.setContext(file.getContext());
return res;
}
public static File toEntity(FileDto.Request req) {
File file = new File();
file.setFileId(UUID.randomUUID());
file.setFileName(req.getFileName());
file.setFilePath(req.getFilePath());
file.setMimeType(req.getMimeType());
file.setUploadedBy(req.getUploadedBy());
file.setContext(req.getContext());
return file;
}
}

View File

@@ -0,0 +1,52 @@
package es.huertosbellavista.backend.core.mapper;
import es.huertosbellavista.backend.core.model.User;
import net.miarma.backlib.dto.CreateUserDto;
import net.miarma.backlib.dto.CredentialDto;
import net.miarma.backlib.dto.UserDto;
import net.miarma.backlib.dto.UserWithCredentialDto;
import java.util.UUID;
public class UserMapper {
public static User fromDto(UserDto dto) {
if (dto == null) return null;
User user = new User();
user.setDisplayName(dto.getDisplayName());
user.setAvatar(dto.getAvatar());
user.setGlobalRole(dto.getGlobalRole());
user.setGlobalStatus(dto.getGlobalStatus());
return user;
}
public static User fromCreateDto(CreateUserDto dto) {
User user = new User();
user.setUserId(UUID.randomUUID());
user.setDisplayName(dto.displayName());
user.setAvatar(dto.avatar() != null ? dto.avatar() : null);
user.setGlobalRole((byte)0);
user.setGlobalStatus((byte)1);
return user;
}
public static UserDto toDto(User u) {
if (u == null) return null;
return new UserDto(
u.getUserId(),
u.getDisplayName(),
u.getAvatar(),
u.getGlobalStatus(),
u.getGlobalRole(),
u.getCreatedAt(),
u.getUpdatedAt()
);
}
public static UserWithCredentialDto toDtoWithCredentials(UserDto user, CredentialDto account){
if (user == null || account == null) return null;
return new UserWithCredentialDto(user, account);
}
}

View File

@@ -0,0 +1,163 @@
package es.huertosbellavista.backend.core.model;
import java.time.Instant;
import java.util.UUID;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.PostLoad;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import jakarta.persistence.Table;
import jakarta.persistence.Transient;
import jakarta.persistence.UniqueConstraint;
import net.miarma.backlib.util.UuidUtil;
@Entity
@Table(name = "credentials",
uniqueConstraints = {
@UniqueConstraint(columnNames = { "service_id", "username" }),
@UniqueConstraint(columnNames = { "service_id", "email" })
})
public class Credential {
@Id
@Column(name = "credential_id", columnDefinition = "BINARY(16)")
private byte[] credentialIdBin;
@Column(name = "user_id", columnDefinition = "BINARY(16)")
private byte[] userIdBin;
@Transient
private UUID credentialId;
@Transient
private UUID userId;
@Column(name = "service_id")
private Byte serviceId;
private String username;
private String email;
private String password;
private Byte status;
@CreationTimestamp
private Instant createdAt;
@UpdateTimestamp
private Instant updatedAt;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", insertable = false, updatable = false)
private User user;
@PrePersist
@PreUpdate
private void prePersist() {
if (credentialId != null) {
credentialIdBin = UuidUtil.uuidToBin(credentialId);
}
if (userId != null) {
userIdBin = UuidUtil.uuidToBin(userId);
}
}
@PostLoad
private void postLoad() {
if (credentialIdBin != null) {
credentialId = UuidUtil.binToUUID(credentialIdBin);
}
if (userIdBin != null) {
userId = UuidUtil.binToUUID(userIdBin);
}
}
public UUID getCredentialId() {
return credentialId;
}
public void setCredentialId(UUID credentialId) {
this.credentialId = credentialId;
}
public UUID getUserId() {
return userId;
}
public void setUserId(UUID userId) {
this.userId = userId;
}
public Byte getServiceId() {
return serviceId;
}
public void setServiceId(Byte serviceId) {
this.serviceId = serviceId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Byte getStatus() {
return status;
}
public void setStatus(Byte status) {
this.status = status;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
public Instant getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Instant updatedAt) {
this.updatedAt = updatedAt;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}

View File

@@ -0,0 +1,122 @@
package es.huertosbellavista.backend.core.model;
import java.time.Instant;
import java.util.UUID;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.PostLoad;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import jakarta.persistence.Table;
import jakarta.persistence.Transient;
import net.miarma.backlib.util.UuidUtil;
@Entity
@Table(name = "files")
public class File {
@Id
@Column(name = "file_id", columnDefinition = "BINARY(16)")
private byte[] fileIdBin;
@Transient
private UUID fileId;
@Column(name = "file_name", nullable = false, length = 128)
private String fileName;
@Column(name = "file_path", nullable = false, length = 256)
private String filePath;
@Column(name = "mime_type", nullable = false, length = 64)
private String mimeType;
@Column(name = "uploaded_by", columnDefinition = "BINARY(16)", nullable = false)
private byte[] uploadedByBin;
@Transient
private UUID uploadedBy;
@Column(name = "uploaded_at", nullable = false, updatable = false)
private Instant uploadedAt;
@Column(name = "context", nullable = false)
private Byte context;
@PrePersist
@PreUpdate
private void prePersist() {
if (fileId != null) {
fileIdBin = UuidUtil.uuidToBin(fileId);
}
if (uploadedBy != null) {
uploadedByBin = UuidUtil.uuidToBin(uploadedBy);
}
}
@PostLoad
private void postLoad() {
if (fileIdBin != null) {
fileId = UuidUtil.binToUUID(fileIdBin);
}
if (uploadedByBin != null) {
uploadedBy = UuidUtil.binToUUID(uploadedByBin);
}
}
public UUID getFileId() {
return fileId;
}
public void setFileId(UUID fileId) {
this.fileId = fileId;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public String getFilePath() {
return filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public String getMimeType() {
return mimeType;
}
public void setMimeType(String mimeType) {
this.mimeType = mimeType;
}
public UUID getUploadedBy() {
return uploadedBy;
}
public void setUploadedBy(UUID uploadedBy) {
this.uploadedBy = uploadedBy;
}
public Instant getUploadedAt() {
return uploadedAt;
}
public Byte getContext() {
return context;
}
public void setContext(Byte context) {
this.context = context;
}
}

View File

@@ -0,0 +1,117 @@
package es.huertosbellavista.backend.core.model;
import java.time.Instant;
import java.util.UUID;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.PostLoad;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import jakarta.persistence.Table;
import jakarta.persistence.Transient;
import net.miarma.backlib.util.UuidUtil;
@Entity
@Table(name = "users")
public class User {
@Id
@Column(name = "user_id", columnDefinition = "BINARY(16)")
private byte[] userIdBin;
@Transient
private UUID userId;
@Column(name = "display_name", nullable = false)
private String displayName;
private String avatar;
@Column(name = "global_status")
private Byte globalStatus;
@Column(name = "global_role")
private Byte globalRole;
@CreationTimestamp
private Instant createdAt;
@UpdateTimestamp
private Instant updatedAt;
@PrePersist
@PreUpdate
private void prePersist() {
if (userId != null) {
userIdBin = UuidUtil.uuidToBin(userId);
}
}
@PostLoad
private void postLoad() {
if (userIdBin != null) {
userId = UuidUtil.binToUUID(userIdBin);
}
}
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 getAvatar() {
return avatar;
}
public void setAvatar(String avatar) {
this.avatar = avatar;
}
public Byte getGlobalStatus() {
return globalStatus;
}
public void setGlobalStatus(Byte globalStatus) {
this.globalStatus = globalStatus;
}
public Byte getGlobalRole() {
return globalRole;
}
public void setGlobalRole(Byte globalRole) {
this.globalRole = globalRole;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
public Instant getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Instant updatedAt) {
this.updatedAt = updatedAt;
}
}

View File

@@ -0,0 +1,62 @@
package es.huertosbellavista.backend.core.repository;
import java.util.List;
import java.util.Optional;
import jakarta.validation.constraints.NotBlank;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import es.huertosbellavista.backend.core.model.Credential;
public interface CredentialRepository extends JpaRepository<Credential, byte[]> {
@Query("""
SELECT c FROM Credential c
JOIN FETCH c.user
WHERE c.serviceId = :serviceId
AND c.username = :username
""")
Optional<Credential> findByServiceIdAndUsername(@Param("serviceId") Byte serviceId,
@Param("username") String username);
List<Credential> findAllByServiceId(Byte serviceId);
@Query("SELECT c FROM Credential c JOIN FETCH c.user WHERE c.serviceId = :serviceId")
List<Credential> getByServiceIdFetchUser(@Param("serviceId") Byte serviceId);
Optional<Credential> findByServiceIdAndEmail(Byte serviceId, String email);
@Query("SELECT c FROM Credential c WHERE c.userIdBin = :userIdBin AND c.serviceId = :serviceId")
Optional<Credential> findByUserIdAndServiceId(@Param("userIdBin") byte[] userIdBin, @Param("serviceId") Byte serviceId);
Optional<Credential> findByUsernameAndServiceId(String username, Byte serviceId);
@Query("SELECT c FROM Credential c WHERE c.userIdBin = :userIdBin")
List<Credential> findByUserId(@Param("userIdBin") byte[] userIdBin);
@Query("SELECT c FROM Credential c WHERE c.email = :email")
List<Credential> findByEmail(@Param("email") String email);
boolean existsByUsernameAndServiceId(String username, Byte serviceId);
boolean existsByEmailAndServiceId(String email, Byte serviceId);
@Modifying
@Query("""
UPDATE Credential c
SET c.username = :username,
c.email = :email,
c.status = :status
WHERE c.credentialIdBin = :credentialIdBin
""")
int update(@Param("credentialIdBin") byte[] credentialIdBin,
@Param("username") String username,
@Param("email") String email,
@Param("status") Byte status);
List<Credential> findByUsername(@NotBlank String username);
}

View File

@@ -0,0 +1,16 @@
package es.huertosbellavista.backend.core.repository;
import java.util.List;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
import es.huertosbellavista.backend.core.model.File;
public interface FileRepository extends JpaRepository<File, byte[]> {
List<File> findByUploadedBy(UUID uploadedBy);
List<File> findByContext(Byte context);
List<File> findByUploadedByAndContext(UUID uploadedBy, Byte context);
}

View File

@@ -0,0 +1,9 @@
package es.huertosbellavista.backend.core.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import es.huertosbellavista.backend.core.model.User;
public interface UserRepository extends JpaRepository<User, byte[]> {
}

View File

@@ -0,0 +1,46 @@
package es.huertosbellavista.backend.core.security;
import net.miarma.backlib.security.CoreAuthTokenHolder;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
public class CorePrincipal implements UserDetails {
private final UUID userId;
private final Byte role;
public CorePrincipal(UUID userId, Byte role) {
this.userId = userId;
this.role = role;
}
public UUID getUserId() { return userId; }
public Byte getRole() { return role; }
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> auth = new ArrayList<>();
String roleName = switch(role) {
case 0 -> "USER";
case 1 -> "ADMIN";
default -> "GUEST";
};
auth.add(new SimpleGrantedAuthority("ROLE_" + roleName));
return auth;
}
@Override public String getPassword() { return ""; }
@Override public String getUsername() { return userId.toString(); }
@Override public boolean isAccountNonExpired() { return true; }
@Override public boolean isAccountNonLocked() { return true; }
@Override public boolean isCredentialsNonExpired() { return true; }
@Override public boolean isEnabled() { return true; }
}

View File

@@ -0,0 +1,63 @@
package es.huertosbellavista.backend.core.security;
import java.io.IOException;
import java.util.UUID;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import es.huertosbellavista.backend.core.model.User;
import net.miarma.backlib.security.JwtService;
import es.huertosbellavista.backend.core.service.UserService;
@Component
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.getById(userId);
CorePrincipal principal = new CorePrincipal(userId, user.getGlobalRole());
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, serviceId);
response.setHeader("X-Refresh-Token", newToken);
}
}
} catch (Exception e) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid or expired token");
return;
}
}
filterChain.doFilter(request, response);
}
}

View File

@@ -0,0 +1,113 @@
package es.huertosbellavista.backend.core.service;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import net.miarma.backlib.dto.*;
import net.miarma.backlib.exception.*;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import es.huertosbellavista.backend.core.mapper.CredentialMapper;
import es.huertosbellavista.backend.core.mapper.UserMapper;
import es.huertosbellavista.backend.core.model.Credential;
import es.huertosbellavista.backend.core.model.User;
@Service
public class AuthService {
private final CredentialService credentialService;
private final UserService userService;
private final net.miarma.backlib.security.JwtService jwtService;
private final PasswordEncoder passwordEncoder;
public AuthService(CredentialService credentialService, UserService userService,
net.miarma.backlib.security.JwtService jwtService, PasswordEncoder passwordEncoder) {
this.credentialService = credentialService;
this.userService = userService;
this.jwtService = jwtService;
this.passwordEncoder = passwordEncoder;
}
public LoginResponse login(LoginRequest request) {
Credential cred = credentialService.getForLogin(request.serviceId(), request.username());
if (!passwordEncoder.matches(request.password(), cred.getPassword())) {
throw new UnauthorizedException("Credenciales no válidas");
}
if (cred.getStatus() == 0) {
throw new ForbiddenException("Esa cuenta está desactivada");
}
String token = jwtService.generateToken(cred.getUserId(), request.serviceId());
UserDto userDto = UserMapper.toDto(cred.getUser());
CredentialDto credentialDto = CredentialMapper.toDto(cred);
return new LoginResponse(token, userDto, credentialDto);
}
public LoginResponse register(RegisterRequest request) {
UUID userIdByUsername = null;
UUID userIdByEmail = null;
Optional<Credential> credByUsername = credentialService.getByUsername(request.username()).stream().findFirst();
if(credByUsername.isPresent()) {
userIdByUsername = credByUsername.get().getUserId();
}
List<Credential> accountsByEmail = credentialService.getByEmail(request.email());
if (!accountsByEmail.isEmpty()) {
userIdByEmail = accountsByEmail.getFirst().getUserId();
}
User user;
if (userIdByUsername != null && userIdByEmail != null) {
if (!userIdByUsername.equals(userIdByEmail)) {
throw new ConflictException("Username y email ya existen pero pertenecen a usuarios distintos");
}
user = userService.getById(userIdByUsername);
} else if (userIdByUsername != null) {
user = userService.getById(userIdByUsername);
} else if (userIdByEmail != null) {
user = userService.getById(userIdByEmail);
} else {
CreateUserDto dto = new CreateUserDto(request.displayName(), null);
user = userService.create(UserMapper.fromCreateDto(dto));
}
Credential cred = new Credential();
cred.setCredentialId(UUID.randomUUID());
cred.setUserId(user.getUserId());
cred.setUser(user);
cred.setServiceId(request.serviceId());
cred.setUsername(request.username());
cred.setEmail(request.email());
cred.setPassword(request.password());
cred.setStatus((byte)1);
credentialService.create(cred);
String token = jwtService.generateToken(user.getUserId(), request.serviceId());
return new LoginResponse(token, UserMapper.toDto(user), CredentialMapper.toDto(cred));
}
public void changePassword(UUID userId, ChangePasswordRequest request) {
Credential cred = credentialService.getByUserId(userId)
.stream()
.filter(c -> c.getServiceId().equals(request.serviceId()))
.findFirst()
.orElseThrow(() -> new NotFoundException("Cuenta no encontrada"));
if (!passwordEncoder.matches(request.oldPassword(), cred.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");
}
credentialService.updatePassword(cred.getCredentialId(), request);
}
}

View File

@@ -0,0 +1,163 @@
package es.huertosbellavista.backend.core.service;
import java.util.List;
import java.util.UUID;
import jakarta.validation.constraints.NotBlank;
import net.miarma.backlib.exception.*;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import es.huertosbellavista.backend.core.model.Credential;
import es.huertosbellavista.backend.core.repository.CredentialRepository;
import net.miarma.backlib.dto.ChangePasswordRequest;
import net.miarma.backlib.util.UuidUtil;
@Service
@Transactional
public class CredentialService {
private final CredentialRepository credentialRepository;
private final UserService userService;
private final PasswordEncoder passwordEncoder;
public CredentialService(CredentialRepository credentialRepository, UserService userService, PasswordEncoder passwordEncoder) {
this.credentialRepository = credentialRepository;
this.userService = userService;
this.passwordEncoder = passwordEncoder;
}
public Credential getById(UUID credentialId) {
byte[] idBytes = UuidUtil.uuidToBin(credentialId);
return credentialRepository.findById(idBytes)
.orElseThrow(() -> new NotFoundException("Cuenta no encontrada"));
}
public Credential create(Credential credential) {
if (credential.getUsername() == null || credential.getUsername().isBlank()) {
throw new ValidationException("userName", "El usuario no puede estar vacío");
}
if (credential.getEmail() == null || !credential.getEmail().matches("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$")) {
throw new ValidationException("email", "Formato de email no válido");
}
if (credential.getPassword() == null || credential.getPassword().length() < 6) {
throw new ValidationException("password", "La contraseña tiene que tener al menos 6 caracteres");
}
if (credential.getServiceId() == null || credential.getServiceId() < 0) {
throw new ValidationException("serviceId", "El identificador de servicio debe ser positivo");
}
boolean existsUsername = credentialRepository.existsByUsernameAndServiceId(
credential.getUsername(), credential.getServiceId());
if (existsUsername) throw new ConflictException("El usuario ya existe para este servicio");
boolean existsEmail = credentialRepository.existsByEmailAndServiceId(
credential.getEmail(), credential.getServiceId());
if (existsEmail) throw new ConflictException("El email ya existe para este servicio");
credential.setCredentialId(UUID.randomUUID());
credential.setPassword(passwordEncoder.encode(credential.getPassword()));
return credentialRepository.save(credential);
}
public List<Credential> getAll() {
return credentialRepository.findAll();
}
public List<Credential> getByServiceId(Byte serviceId) {
return credentialRepository.findAllByServiceId(serviceId);
}
public List<Credential> getByServiceIdFetchUser(Byte serviceId) {
return credentialRepository.getByServiceIdFetchUser(serviceId);
}
public List<Credential> getByUserId(UUID userId) {
List<Credential> creds = credentialRepository.findByUserId(UuidUtil.uuidToBin(userId));
if (creds.isEmpty()) {
throw new NotFoundException("El usuario no tiene cuenta");
}
return creds;
}
public List<Credential> getByUsername(@NotBlank String username) {
return credentialRepository.findByUsername(username);
}
public List<Credential> getByEmail(String email) {
return credentialRepository.findByEmail(email);
}
public Credential getByUserIdAndService(UUID userId, Byte serviceId) {
return credentialRepository.findByUserIdAndServiceId(UuidUtil.uuidToBin(userId), serviceId)
.orElseThrow(() -> new NotFoundException("El usuario no tiene cuenta en este sitio"));
}
public Credential getForLogin(Byte serviceId, String username) {
return credentialRepository.findByServiceIdAndUsername(serviceId, username)
.orElseThrow(() -> new BadRequestException("Credenciales no válidas"));
}
public boolean existsByUsernameAndService(String username, Byte serviceId) {
return credentialRepository.findByUsernameAndServiceId(username, serviceId).isPresent();
}
public boolean isOwner(UUID credentialId, UUID userId) {
byte[] idBytes = UuidUtil.uuidToBin(credentialId);
return credentialRepository.findById(idBytes)
.map(c -> c.getUserId().equals(userId))
.orElse(false);
}
public Credential update(UUID credentialId, Credential changes) {
Credential cred = getById(credentialId);
if (cred.getStatus() == (byte)0) {
throw new ForbiddenException("La cuenta está inactiva, contacta con un administrador");
}
if (changes.getUsername() != null) cred.setUsername(changes.getUsername());
if (changes.getEmail() != null) cred.setEmail(changes.getEmail());
if (changes.getStatus() != null) cred.setStatus(changes.getStatus());
int updated = credentialRepository.update(
UuidUtil.uuidToBin(cred.getCredentialId()),
cred.getUsername(),
cred.getEmail(),
cred.getStatus()
);
if (updated == 0)
throw new RuntimeException("No se pudo actualizar la cuenta");
return getById(credentialId);
}
public Credential updatePassword(UUID credentialId, ChangePasswordRequest request) {
byte[] idBytes = UuidUtil.uuidToBin(credentialId);
Credential cred = credentialRepository.findById(idBytes)
.orElseThrow(() -> new NotFoundException("Cuenta no encontrada"));
if (!passwordEncoder.matches(request.oldPassword(), cred.getPassword())) {
throw new ValidationException("oldPassword", "La contraseña actual es incorrecta");
}
cred.setPassword(passwordEncoder.encode(request.newPassword()));
return credentialRepository.save(cred);
}
public void delete(UUID credentialId) {
byte[] idBytes = UuidUtil.uuidToBin(credentialId);
if(!credentialRepository.existsById(idBytes))
throw new NotFoundException("Cuenta no encontrada");
credentialRepository.deleteById(idBytes);
}
public Byte getStatus(UUID userId, Byte serviceId) {
Credential credential = getByUserIdAndService(userId, serviceId);
return credential.getStatus();
}
}

View File

@@ -0,0 +1,87 @@
package es.huertosbellavista.backend.core.service;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.UUID;
import net.miarma.backlib.exception.NotFoundException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import es.huertosbellavista.backend.core.model.File;
import es.huertosbellavista.backend.core.repository.FileRepository;
import net.miarma.backlib.util.UuidUtil;
@Service
@Transactional
public class FileService {
private final FileRepository fileRepository;
@Value("${filesDir}")
private String filesDir;
public FileService(FileRepository fileRepository) {
this.fileRepository = fileRepository;
}
public File getById(UUID fileId) {
byte[] idBytes = UuidUtil.uuidToBin(fileId);
return fileRepository.findById(idBytes)
.orElseThrow(() -> new NotFoundException("Archivo no encontrado"));
}
public List<File> getAll() {
return fileRepository.findAll();
}
public List<File> getByUserId(UUID userId) {
return fileRepository.findByUploadedBy(userId);
}
public File create(File file, byte[] fileBinary) throws IOException {
Path dirPath = Paths.get(filesDir, String.valueOf(file.getContext()));
if (!Files.exists(dirPath)) {
Files.createDirectories(dirPath);
}
Path filePath = dirPath.resolve(file.getFileName());
try (FileOutputStream fos = new FileOutputStream(filePath.toFile())) {
fos.write(fileBinary);
}
file.setFilePath(filePath.toString());
return fileRepository.save(file);
}
public File update(UUID fileId, File file) {
byte[] idBytes = UuidUtil.uuidToBin(fileId);
if (!fileRepository.existsById(idBytes)) {
throw new NotFoundException("Archivo no encontrado");
}
return fileRepository.save(file);
}
public void delete(UUID fileId) {
byte[] idBytes = UuidUtil.uuidToBin(fileId);
if (!fileRepository.existsById(idBytes)) {
throw new NotFoundException("Archivo no encontrado");
}
fileRepository.deleteById(idBytes);
}
public boolean isOwner(UUID fileId, UUID userId) {
byte[] fileBytes = UuidUtil.uuidToBin(fileId);
return fileRepository.findById(fileBytes)
.map(f -> f.getUploadedBy().equals(userId))
.orElse(false);
}
}

View File

@@ -0,0 +1,111 @@
package es.huertosbellavista.backend.core.service;
import java.util.List;
import java.util.UUID;
import es.huertosbellavista.backend.core.mapper.UserMapper;
import net.miarma.backlib.dto.ChangeAvatarRequest;
import net.miarma.backlib.exception.NotFoundException;
import net.miarma.backlib.exception.ValidationException;
import org.springframework.stereotype.Service;
import jakarta.transaction.Transactional;
import es.huertosbellavista.backend.core.model.User;
import es.huertosbellavista.backend.core.repository.UserRepository;
import net.miarma.backlib.dto.UserDto;
import net.miarma.backlib.util.UuidUtil;
@Service
@Transactional
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public List<User> getAll() {
return userRepository.findAll();
}
public User getById(UUID userId) {
byte[] idBytes = UuidUtil.uuidToBin(userId);
return userRepository.findById(idBytes)
.orElseThrow(() -> new NotFoundException("Usuario no encontrado"));
}
public User create(User user) {
if(user.getDisplayName() == null || user.getDisplayName().isBlank()) {
throw new ValidationException("displayName", "El nombre a mostrar es necesario");
}
return userRepository.save(user);
}
public User update(UUID userId, User changes) {
User user = userRepository.findById(UuidUtil.uuidToBin(userId))
.orElseThrow(() -> new NotFoundException("Usuario no encontrado"));
if (changes.getDisplayName() != null) {
String dn = changes.getDisplayName().trim();
if (dn.isEmpty()) throw new ValidationException("displayName", "No puede estar vacío");
if (dn.length() > 50) throw new ValidationException("displayName", "Máx 50 caracteres");
user.setDisplayName(dn);
}
if (changes.getAvatar() != null)
user.setAvatar(changes.getAvatar());
if (changes.getGlobalRole() != null)
user.setGlobalRole(changes.getGlobalRole());
if (changes.getGlobalStatus() != null)
user.setGlobalStatus(changes.getGlobalStatus());
return userRepository.save(user);
}
public void delete(UUID userId) {
byte[] idBytes = UuidUtil.uuidToBin(userId);
if(!userRepository.existsById(idBytes))
throw new NotFoundException("Usuario no encontrado");
userRepository.deleteById(idBytes);
}
public UserDto updateAvatar(UUID userId, ChangeAvatarRequest req) {
User user = userRepository.findById(UuidUtil.uuidToBin(userId))
.orElseThrow(() -> new NotFoundException("Usuario no encontrado"));
user.setAvatar(req.avatar());
userRepository.save(user);
return UserMapper.toDto(user);
}
public Byte getStatus(UUID userId) {
User user = userRepository.findById(UuidUtil.uuidToBin(userId))
.orElseThrow(() -> new NotFoundException("Usuario no encontrado"));;
return user.getGlobalStatus();
}
public void updateStatus(UUID userId, Byte status) {
User user = userRepository.findById(UuidUtil.uuidToBin(userId))
.orElseThrow(() -> new NotFoundException("Usuario no encontrado"));;
user.setGlobalStatus(status);
userRepository.save(user);
}
public Byte getRole(UUID userId) {
User user = userRepository.findById(UuidUtil.uuidToBin(userId))
.orElseThrow(() -> new NotFoundException("Usuario no encontrado"));;
return user.getGlobalRole();
}
public void updateRole(UUID userId, Byte role) {
User user = userRepository.findById(UuidUtil.uuidToBin(userId))
.orElseThrow(() -> new NotFoundException("Usuario no encontrado"));;
user.setGlobalRole(role);
userRepository.save(user);
}
public boolean exists(UUID userId) {
return userRepository.existsById(UuidUtil.uuidToBin(userId));
}
}

View File

@@ -0,0 +1,23 @@
server:
port: 8080
servlet:
context-path: /v2/core
spring:
datasource:
url: jdbc:mariadb://localhost:3306/huertos
username: admin
password: ${DB_PASS}
driver-class-name: org.mariadb.jdbc.Driver
logging:
level:
org.hibernate.SQL: DEBUG
org.hibernate.orm.jdbc.bind: TRACE
org.springframework.security: DEBUG
filesDir: "/home/jomaa/.config/miarma-backend/files"
jwt:
private-key-path: /home/jomaa/.config/miarma-backend/private.pem
public-key-path: /home/jomaa/.config/miarma-backend/public.pem

View File

@@ -0,0 +1,22 @@
server:
port: 8080
servlet:
context-path: /v2/core
spring:
datasource:
url: jdbc:mariadb://mariadb:3306/huertos
username: ${DB_USER}
password: ${DB_PASS}
driver-class-name: org.mariadb.jdbc.Driver
logging:
level:
org.springframework.security: INFO
org.hibernate.SQL: WARN
filesDir: "/files"
jwt:
private-key-path: ${JWT_PRIVATE_KEY}
public-key-path: ${JWT_PUBLIC_KEY}

View File

@@ -0,0 +1,25 @@
spring:
application:
name: core-service
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