Add: missing create methods in controllers. Fix: SYSTEM token gets the refresh now. Add: CORS. Add: HTTP to core.

This commit is contained in:
Jose
2026-01-25 23:42:50 +01:00
parent 2d255a7f0b
commit e4461f7790
47 changed files with 709 additions and 198 deletions

View File

@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<artifactId>backlib</artifactId>
<groupId>net.miarma</groupId>
<version>1.0.0</version>
<version>1.0.1</version>
<properties>
<java.version>25</java.version>

View File

@@ -0,0 +1,37 @@
package net.miarma.backlib.dto;
import tools.jackson.databind.ObjectMapper;
import java.time.Instant;
import java.util.List;
import java.util.Map;
public class ApiValidationErrorDto {
private Map<String,String> errors;
private Instant timestamp;
public ApiValidationErrorDto(Map<String,String> errors) {
this.errors = errors;
this.timestamp = Instant.now();
}
public Map<String,String> getErrors() {
return errors;
}
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);
}
}

View File

@@ -1,7 +1,21 @@
package net.miarma.backlib.exception;
public class ValidationException extends RuntimeException {
public ValidationException(String message) {
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;
}
}

View File

@@ -2,12 +2,15 @@ package net.miarma.backlib.http;
import jakarta.servlet.http.HttpServletRequest;
import net.miarma.backlib.dto.ApiErrorDto;
import net.miarma.backlib.dto.ApiValidationErrorDto;
import net.miarma.backlib.exception.*;
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)
@@ -81,17 +84,9 @@ public class GlobalExceptionHandler {
}
@ExceptionHandler(ValidationException.class)
public ResponseEntity<ApiErrorDto> handleValidation(
ValidationException ex, HttpServletRequest req) {
ApiErrorDto error = new ApiErrorDto(
HttpStatus.UNPROCESSABLE_CONTENT.value(),
HttpStatus.UNPROCESSABLE_CONTENT.getReasonPhrase(),
ex.getMessage(),
req.getRequestURI()
);
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_CONTENT).body(error);
public ResponseEntity<ApiValidationErrorDto> handleValidation(ValidationException ex) {
Map<String, String> errors = Map.of(ex.getField(), ex.getMessage());
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_CONTENT).body(new ApiValidationErrorDto(errors));
}
@ExceptionHandler(Exception.class)

View File

@@ -2,15 +2,23 @@ package net.miarma.backlib.security;
import org.springframework.stereotype.Component;
import java.time.Instant;
@Component
public class CoreAuthTokenHolder {
private volatile String token;
private volatile Instant expiresAt;
public String getToken() {
return token;
}
public void setToken(String token) {
public boolean isExpired() {
return expiresAt == null || Instant.now().isAfter(expiresAt.minusSeconds(30));
}
public void setToken(String token, Instant expiresAt) {
this.token = token;
this.expiresAt = expiresAt;
}
}

View File

@@ -83,7 +83,7 @@
<dependency>
<groupId>net.miarma</groupId>
<artifactId>backlib</artifactId>
<version>1.0.0</version>
<version>1.0.1</version>
</dependency>
</dependencies>

View File

@@ -0,0 +1,28 @@
package net.miarma.backend.core.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins(
"http://localhost:3000",
"http://localhost:8081",
"http://huertos:8081"
)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true);
}
};
}
}

View File

@@ -6,6 +6,7 @@ import net.miarma.backlib.http.RestAuthEntryPoint;
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;
@@ -15,6 +16,11 @@ 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.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List;
@Configuration
@EnableWebSecurity
@@ -34,9 +40,23 @@ public class SecurityConfig {
this.accessDeniedHandler = accessDeniedHandler;
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:3000"));
config.setAllowedMethods(List.of("GET","POST","PUT","DELETE","OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(Customizer.withDefaults())
.csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(ex -> ex

View File

@@ -1,15 +1,13 @@
package net.miarma.backend.core.controller;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import net.miarma.backend.core.model.Credential;
@@ -106,4 +104,10 @@ public class AuthController {
return ResponseEntity.ok(Map.of("message", "Password changed successfully"));
}
@GetMapping("/validate")
public ResponseEntity<Boolean> validate(@RequestHeader("Authorization") String authHeader) {
String token = authHeader.substring(7);
return ResponseEntity.ok(jwtService.validateToken(token));
}
}

View File

@@ -72,7 +72,7 @@ public class FileController {
@PutMapping("/{fileId}")
@PreAuthorize("hasRole('ADMIN') or @fileService.isOwner(#fileId, authentication.principal.userId)")
public ResponseEntity<File> update(@PathVariable("file_id") UUID fileId, @RequestBody File file) {
public ResponseEntity<File> update(@PathVariable("fileId") UUID fileId, @RequestBody File file) {
file.setFileId(fileId);
File updated = fileService.update(file);
return ResponseEntity.ok(updated);
@@ -80,7 +80,7 @@ public class FileController {
@DeleteMapping("/{fileId}")
@PreAuthorize("hasRole('ADMIN') or @fileService.isOwner(#fileId, authentication.principal.userId)")
public ResponseEntity<Void> delete(@PathVariable("file_id") UUID fileId, @RequestBody Map<String,String> body) throws IOException {
public ResponseEntity<Void> delete(@PathVariable("fileId") UUID fileId, @RequestBody Map<String,String> body) throws IOException {
String filePath = body.get("file_path");
Files.deleteIfExists(Paths.get(filePath));
fileService.delete(fileId);

View File

@@ -28,7 +28,8 @@ public interface CredentialRepository extends JpaRepository<Credential, byte[]>
Optional<Credential> findByServiceIdAndEmail(Byte serviceId, String email);
Optional<Credential> findByUserIdAndServiceId(UUID userId, Byte serviceId);
@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, int serviceId);

View File

@@ -38,16 +38,16 @@ public class CredentialService {
public Credential create(Credential credential) {
if (credential.getUsername() == null || credential.getUsername().isBlank()) {
throw new ValidationException("Username cannot be blank");
throw new ValidationException("userName", "Username cannot be blank");
}
if (credential.getEmail() == null || !credential.getEmail().matches("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$")) {
throw new ValidationException("Invalid email format");
throw new ValidationException("email", "Invalid email format");
}
if (credential.getPassword() == null || credential.getPassword().length() < 6) {
throw new ValidationException("Password must be at least 6 characters");
throw new ValidationException("password", "Password must be at least 6 characters");
}
if (credential.getServiceId() == null || credential.getServiceId() < 0) {
throw new ValidationException("ServiceId must be positive");
throw new ValidationException("serviceId", "ServiceId must be positive");
}
boolean existsUsername = credentialRepository.existsByUsernameAndServiceId(
@@ -90,7 +90,7 @@ public class CredentialService {
}
public Credential getByUserIdAndService(UUID userId, Byte serviceId) {
return credentialRepository.findByUserIdAndServiceId(userId, serviceId)
return credentialRepository.findByUserIdAndServiceId(UuidUtil.uuidToBin(userId), serviceId)
.orElseThrow(() -> new NotFoundException("Credential not found in this site"));
}
@@ -117,13 +117,13 @@ public class CredentialService {
.orElseThrow(() -> new NotFoundException("Credential not found"));
if (dto.getUsername() != null && dto.getUsername().isBlank()) {
throw new ValidationException("Username cannot be blank");
throw new ValidationException("userName", "Username cannot be blank");
}
if (dto.getEmail() != null && !dto.getEmail().matches("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$")) {
throw new ValidationException("Invalid email format");
throw new ValidationException("email", "Invalid email format");
}
if (dto.getServiceId() != null && dto.getServiceId() < 0) {
throw new ValidationException("ServiceId must be positive");
throw new ValidationException("serviceId", "ServiceId must be positive");
}
if (dto.getUsername() != null) cred.setUsername(dto.getUsername());
@@ -141,7 +141,7 @@ public class CredentialService {
.orElseThrow(() -> new NotFoundException("Credential not found"));
if (!passwordEncoder.matches(request.oldPassword(), cred.getPassword())) {
throw new ValidationException("Old password is incorrect");
throw new ValidationException("oldPassword", "Old password is incorrect");
}
cred.setPassword(passwordEncoder.encode(request.newPassword()));

View File

@@ -12,6 +12,7 @@ import java.util.UUID;
import net.miarma.backend.core.mapper.FileMapper;
import net.miarma.backlib.dto.FileDto;
import net.miarma.backlib.exception.NotFoundException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -24,7 +25,9 @@ import net.miarma.backlib.util.UuidUtil;
public class FileService {
private final FileRepository fileRepository;
private final String basePath = "/var/www/files";
@Value("${filesDir}")
private String filesDir;
public FileService(FileRepository fileRepository) {
this.fileRepository = fileRepository;
@@ -45,7 +48,7 @@ public class FileService {
}
public File create(FileDto.Request dto, byte[] fileBinary) throws IOException {
Path dirPath = Paths.get(basePath, String.valueOf(dto.getContext()));
Path dirPath = Paths.get(filesDir, String.valueOf(dto.getContext()));
if (!Files.exists(dirPath)) {
Files.createDirectories(dirPath);
}

View File

@@ -36,7 +36,7 @@ public class UserService {
public User create(UserDto dto) {
if(dto.getDisplayName() == null || dto.getDisplayName().isBlank()) {
throw new ValidationException("Display name is required");
throw new ValidationException("displayName", "Display name is required");
}
User user = new User();

View File

@@ -16,6 +16,8 @@ logging:
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

@@ -15,6 +15,8 @@ logging:
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

@@ -67,7 +67,7 @@
<dependency>
<groupId>net.miarma</groupId>
<artifactId>backlib</artifactId>
<version>1.0.0</version>
<version>1.0.1</version>
<scope>compile</scope>
</dependency>
</dependencies>

View File

@@ -2,6 +2,7 @@ package net.miarma.backend.huertos;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication(scanBasePackages = {
"net.miarma.backend.huertos",

View File

@@ -0,0 +1,34 @@
package net.miarma.backend.huertos.client;
import net.miarma.backlib.dto.LoginRequest;
import net.miarma.backlib.dto.LoginResponse;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.RestTemplate;
@Component
public class CoreAuthClient {
private final RestTemplate restTemplate;
private final String coreUrl;
public CoreAuthClient(
@Qualifier("authRestTemplate") RestTemplate restTemplate,
@Value("${core.url}") String coreUrl
) {
this.restTemplate = restTemplate;
this.coreUrl = coreUrl;
}
public LoginResponse login(LoginRequest req) {
return restTemplate.postForObject(
coreUrl + "/auth/login",
req,
LoginResponse.class
);
}
}

View File

@@ -3,6 +3,7 @@ package net.miarma.backend.huertos.client;
import net.miarma.backlib.dto.LoginRequest;
import net.miarma.backlib.dto.LoginResponse;
import net.miarma.backlib.dto.UserWithCredentialDto;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
@@ -17,22 +18,12 @@ public class HuertosWebClient {
private final RestTemplate restTemplate;
private final String coreUrl;
public HuertosWebClient(RestTemplate restTemplate,
public HuertosWebClient(@Qualifier("secureRestTemplate") RestTemplate restTemplate,
@Value("${core.url}") String coreUrl) {
this.restTemplate = restTemplate;
this.coreUrl = coreUrl;
}
public LoginResponse login(String username, String password) {
LoginRequest req = new LoginRequest(username, password, (byte) 1);
LoginResponse resp = restTemplate.postForObject(
coreUrl + "/auth/login",
req,
LoginResponse.class
);
return resp;
}
public UserWithCredentialDto getUserWithCredential(UUID userId, Byte serviceId) {
return restTemplate.getForObject(
coreUrl + "/users/{user_id}/service/{service_id}",

View File

@@ -0,0 +1,27 @@
package net.miarma.backend.huertos.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins(
"http://localhost:3000"
)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true);
}
};
}
}

View File

@@ -1,37 +0,0 @@
package net.miarma.backend.huertos.config;
import net.miarma.backend.huertos.client.HuertosWebClient;
import net.miarma.backlib.security.CoreAuthTokenHolder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
@Component
public class HuertosStartup implements ApplicationRunner {
private final HuertosWebClient client;
private final CoreAuthTokenHolder tokenHolder;
private final String user;
private final String pass;
public HuertosStartup(
HuertosWebClient client,
CoreAuthTokenHolder tokenHolder,
@Value("${huertos.user}") String user,
@Value("${huertos.password}") String pass
) {
this.client = client;
this.tokenHolder = tokenHolder;
this.user = user;
this.pass = pass;
}
@Override
public void run(ApplicationArguments args) {
String token = client.login(user, pass).token();
tokenHolder.setToken(token);
System.out.println("TOKEN recibido: " + token);
System.out.println("HUERTOS CLIENT has been authenticated in CORE");
}
}

View File

@@ -1,5 +1,6 @@
package net.miarma.backend.huertos.config;
import net.miarma.backend.huertos.service.CoreAuthService;
import net.miarma.backlib.security.CoreAuthTokenHolder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -13,23 +14,21 @@ import java.util.List;
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(CoreAuthTokenHolder tokenHolder) {
RestTemplate restTemplate = new RestTemplate();
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
interceptors.add(authInterceptor(tokenHolder));
restTemplate.setInterceptors(interceptors);
return restTemplate;
public RestTemplate authRestTemplate() {
return new RestTemplate();
}
private ClientHttpRequestInterceptor authInterceptor(CoreAuthTokenHolder tokenHolder) {
return (request, body, execution) -> {
String token = tokenHolder.getToken();
if (token != null) {
request.getHeaders().add("Authorization", "Bearer " + token);
}
@Bean
public RestTemplate secureRestTemplate(CoreAuthService coreAuthService) {
RestTemplate rt = new RestTemplate();
rt.getInterceptors().add((request, body, execution) -> {
String token = coreAuthService.getToken();
request.getHeaders().setBearerAuth(token);
return execution.execute(request, body);
};
});
return rt;
}
}

View File

@@ -5,12 +5,18 @@ 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.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List;
@Configuration
@EnableWebSecurity
@@ -31,9 +37,23 @@ public class SecurityConfig {
this.accessDeniedHandler = accessDeniedHandler;
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:3000"));
config.setAllowedMethods(List.of("GET","POST","PUT","DELETE","OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(Customizer.withDefaults())
.csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(ex -> ex
@@ -43,10 +63,9 @@ public class SecurityConfig {
.authorizeHttpRequests(auth -> auth
// PUBLICAS
.requestMatchers("/auth/login").permitAll()
.requestMatchers("/announcements").permitAll()
.requestMatchers("/requests/mine").permitAll()
.requestMatchers("/users/waitlist/limited").permitAll()
.requestMatchers("/users/latest-number").permitAll()
.requestMatchers("/pre-users/validate").permitAll()
// PRIVADAS
.anyRequest().authenticated()
);

View File

@@ -50,6 +50,16 @@ public class AnnouncementController {
);
}
@PostMapping
@PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')")
public ResponseEntity<AnnouncementDto.Response> create(@RequestBody AnnouncementDto.Request dto) {
return ResponseEntity.ok(
AnnouncementMapper.toResponse(
announcementService.create(
AnnouncementMapper.toEntity(dto)
)));
}
@DeleteMapping("/{announce_id}")
@PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')")
public ResponseEntity<Map<String, String>> delete(@PathVariable("announce_id") UUID announcementId) {

View File

@@ -7,6 +7,7 @@ import net.miarma.backend.huertos.service.BalanceService;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -25,4 +26,14 @@ public class BalanceController {
Balance balance = balanceService.get();
return ResponseEntity.ok(BalanceMapper.toDto(balance));
}
@PostMapping
@PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')")
public ResponseEntity<BalanceDto> setBalance(BalanceDto dto) {
return ResponseEntity.ok(
BalanceMapper.toDto(
balanceService.create(BalanceMapper.toEntity(dto))
)
);
}
}

View File

@@ -32,6 +32,16 @@ public class ExpenseController {
);
}
@PostMapping
@PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')")
public ResponseEntity<ExpenseDto.Response> create(ExpenseDto.Request dto) {
return ResponseEntity.ok(
ExpenseMapper.toResponse(
expenseService.create(
ExpenseMapper.toEntity(dto)
)));
}
@GetMapping("/{expense_id}")
@PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')")
public ResponseEntity<ExpenseDto.Response> getById(@PathVariable("expense_id") UUID expenseId) {

View File

@@ -1,5 +1,6 @@
package net.miarma.backend.huertos.controller;
import net.miarma.backend.huertos.client.CoreAuthClient;
import net.miarma.backend.huertos.client.HuertosWebClient;
import net.miarma.backend.huertos.dto.HuertosLoginResponse;
import net.miarma.backend.huertos.dto.HuertosUserMetadataDto;
@@ -18,17 +19,17 @@ import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/auth")
public class HuertosAuthController {
private final HuertosUserMetadataService metadataService;
private final HuertosWebClient webClient;
private final CoreAuthClient authClient;
public HuertosAuthController(HuertosUserMetadataService metadataService,
HuertosWebClient webClient) {
CoreAuthClient authClient) {
this.metadataService = metadataService;
this.webClient = webClient;
this.authClient = authClient;
}
@PostMapping("/login")
public ResponseEntity<HuertosLoginResponse> login(@RequestBody LoginRequest req) {
LoginResponse coreResponse = webClient.login(req.username(), req.password());
LoginResponse coreResponse = authClient.login(req);
HuertosUserMetadata metadata = metadataService.getById(coreResponse.user().getUserId());
return ResponseEntity.ok(
new HuertosLoginResponse(

View File

@@ -1,7 +1,9 @@
package net.miarma.backend.huertos.controller;
import net.miarma.backend.huertos.dto.ExpenseDto;
import net.miarma.backend.huertos.dto.IncomeDto;
import net.miarma.backend.huertos.dto.view.VIncomesWithFullNamesDto;
import net.miarma.backend.huertos.mapper.ExpenseMapper;
import net.miarma.backend.huertos.mapper.IncomeMapper;
import net.miarma.backend.huertos.mapper.view.VIncomesWithFullNamesMapper;
import net.miarma.backend.huertos.model.Income;
@@ -75,6 +77,16 @@ public class IncomeController {
);
}
@PostMapping
@PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')")
public ResponseEntity<IncomeDto.Response> create(IncomeDto.Request dto) {
return ResponseEntity.ok(
IncomeMapper.toResponse(
incomeService.create(
IncomeMapper.toEntity(dto)
)));
}
@GetMapping("/{income_id}")
@PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')")
public ResponseEntity<IncomeDto.Response> getById(@PathVariable("income_id") UUID incomeId) {

View File

@@ -1,11 +1,17 @@
package net.miarma.backend.huertos.controller;
import net.miarma.backend.huertos.dto.MemberDto;
import net.miarma.backend.huertos.dto.WaitlistCensoredDto;
import net.miarma.backend.huertos.dto.*;
import net.miarma.backend.huertos.mapper.IncomeMapper;
import net.miarma.backend.huertos.mapper.RequestMapper;
import net.miarma.backend.huertos.security.HuertosPrincipal;
import net.miarma.backend.huertos.service.IncomeService;
import net.miarma.backend.huertos.service.MemberService;
import net.miarma.backend.huertos.service.RequestService;
import net.miarma.backlib.security.JwtService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
@@ -17,106 +23,130 @@ import java.util.UUID;
public class MemberController {
private final MemberService memberService;
private final RequestService requestService;
private final IncomeService incomeService;
private final JwtService jwtService;
public MemberController(MemberService memberService) {
public MemberController(MemberService memberService, RequestService requestService, IncomeService incomeService, JwtService jwtService) {
this.memberService = memberService;
}
private Byte getServiceIdFromToken() {
var auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || !(auth.getPrincipal() instanceof HuertosPrincipal principal)) {
return null;
}
return principal.getServiceId();
this.requestService = requestService;
this.incomeService = incomeService;
this.jwtService = jwtService;
}
@GetMapping
@PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')")
public List<MemberDto> getAll() {
return memberService.getAll(getServiceIdFromToken());
}
@GetMapping("/{user_id}")
@PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')")
public MemberDto getById(@PathVariable("user_id") UUID userId) {
return memberService.getById(userId, getServiceIdFromToken());
public ResponseEntity<List<MemberDto>> getAll() {
return ResponseEntity.ok(memberService.getAll((byte)1));
}
@GetMapping("/me")
public MemberDto getMe() {
HuertosPrincipal principal =
(HuertosPrincipal) SecurityContextHolder
.getContext()
.getAuthentication()
.getPrincipal();
public ResponseEntity<MemberProfileDto> getMe() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return memberService.getById(principal.getUserId(), getServiceIdFromToken());
if (!(authentication.getPrincipal() instanceof HuertosPrincipal principal)) {
throw new IllegalStateException("Invalid principal type");
}
UUID userId = principal.getUserId();
Byte serviceId = principal.getServiceId();
if (serviceId == null) {
throw new IllegalStateException("ServiceId missing in token");
}
MemberDto member = memberService.getById(userId);
Integer memberNumber = member.metadata().getMemberNumber();
List<RequestDto.Response> requests = requestService.getByUserId(userId).stream()
.map(RequestMapper::toResponse)
.toList();
List<IncomeDto.Response> payments = incomeService.getByMemberNumber(memberNumber).stream()
.map(IncomeMapper::toResponse)
.toList();
return ResponseEntity.ok(
new MemberProfileDto(
member.user(),
member.account(),
member.metadata(),
requests,
payments,
memberService.hasCollaborator(memberNumber),
memberService.hasGreenhouse(memberNumber),
memberService.hasCollaboratorRequest(memberNumber),
memberService.hasGreenhouseRequest(memberNumber)
)
);
}
@GetMapping("/{user_id:[0-9a-fA-F\\-]{36}}")
@PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')")
public ResponseEntity<MemberDto> getById(@PathVariable("user_id") UUID userId) {
return ResponseEntity.ok(memberService.getById(userId));
}
@GetMapping("/latest-number")
public Integer getLatestNumber() {
return memberService.getLatestMemberNumber();
public ResponseEntity<Integer> getLatestNumber() {
return ResponseEntity.ok(memberService.getLatestMemberNumber());
}
@GetMapping("/waitlist")
@PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')")
public List<MemberDto> getWaitlist() {
return memberService.getWaitlist();
public ResponseEntity<List<MemberDto>> getWaitlist() {
return ResponseEntity.ok(memberService.getWaitlist());
}
@GetMapping("/waitlist/limited")
public List<WaitlistCensoredDto> getWaitlistLimited() {
return memberService.getWaitlistLimited();
public ResponseEntity<List<WaitlistCensoredDto>> getWaitlistLimited() {
return ResponseEntity.ok(memberService.getWaitlistLimited());
}
@GetMapping("/number/{member_number}")
@PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')")
public MemberDto getByMemberNumber(@PathVariable("member_number") Integer memberNumber) {
return memberService.getByMemberNumber(memberNumber);
public ResponseEntity<MemberDto> getByMemberNumber(@PathVariable("member_number") Integer memberNumber) {
return ResponseEntity.ok(memberService.getByMemberNumber(memberNumber));
}
@GetMapping("/number/{member_number}/incomes")
@PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')")
public Boolean getMemberIncomes(@PathVariable("member_number") Integer memberNumber) {
return memberService.hasIncomes(memberNumber);
public ResponseEntity<List<IncomeDto.Response>> getMemberIncomes(@PathVariable("member_number") Integer memberNumber) {
return ResponseEntity.ok(memberService.getIncomes(memberNumber));
}
@GetMapping("/number/{member_number}/has-paid")
public Boolean getMemberHasPaid(@PathVariable("member_number") Integer memberNumber) {
return memberService.hasPaid(memberNumber);
public ResponseEntity<Boolean> getMemberHasPaid(@PathVariable("member_number") Integer memberNumber) {
return ResponseEntity.ok(memberService.hasPaid(memberNumber));
}
@GetMapping("/number/{member_number}/has-collaborator")
public Boolean getMemberHasCollaborator(@PathVariable("member_number") Integer memberNumber) {
return memberService.hasCollaborator(memberNumber);
public ResponseEntity<Boolean> getMemberHasCollaborator(@PathVariable("member_number") Integer memberNumber) {
return ResponseEntity.ok(memberService.hasCollaborator(memberNumber));
}
@GetMapping("/number/{member_number}/has-greenhouse")
public Boolean getMemberHasGreenhouse(@PathVariable("member_number") Integer memberNumber) {
return memberService.hasGreenhouse(memberNumber);
public ResponseEntity<Boolean> getMemberHasGreenhouse(@PathVariable("member_number") Integer memberNumber) {
return ResponseEntity.ok(memberService.hasGreenhouse(memberNumber));
}
@GetMapping("/number/{member_number}/has-collaborator-request")
public Boolean getMemberHasCollaboratorRequest(@PathVariable("member_number") Integer memberNumber) {
return memberService.hasCollaboratorRequest(memberNumber);
public ResponseEntity<Boolean> getMemberHasCollaboratorRequest(@PathVariable("member_number") Integer memberNumber) {
return ResponseEntity.ok(memberService.hasCollaboratorRequest(memberNumber));
}
@GetMapping("/number/{member_number}/has-greenhouse-request")
public Boolean getMemberHasGreenhouseRequest(@PathVariable("member_number") Integer memberNumber) {
return memberService.hasGreenhouseRequest(memberNumber);
public ResponseEntity<Boolean> getMemberHasGreenhouseRequest(@PathVariable("member_number") Integer memberNumber) {
return ResponseEntity.ok(memberService.hasGreenhouseRequest(memberNumber));
}
@GetMapping("/plot/{plot_number}")
@PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')")
public MemberDto getByPlotNumber(@PathVariable("plot_number") Integer plotNumber) {
return memberService.getByPlotNumber(plotNumber);
public ResponseEntity<MemberDto> getByPlotNumber(@PathVariable("plot_number") Integer plotNumber) {
return ResponseEntity.ok(memberService.getByPlotNumber(plotNumber));
}
@GetMapping("/dni/{dni}")
@PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')")
public MemberDto getByDni(@PathVariable("dni") String dni) {
return memberService.getByDni(dni);
public ResponseEntity<MemberDto> getByDni(@PathVariable("dni") String dni) {
return ResponseEntity.ok(memberService.getByDni(dni));
}
}

View File

@@ -4,6 +4,8 @@ import net.miarma.backend.huertos.dto.PreUserDto;
import net.miarma.backend.huertos.mapper.PreUserMapper;
import net.miarma.backend.huertos.model.PreUser;
import net.miarma.backend.huertos.service.PreUserService;
import net.miarma.backlib.dto.ApiValidationErrorDto;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@@ -13,7 +15,7 @@ import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/pre_users")
@RequestMapping("/pre-users")
public class PreUserController {
private final PreUserService preUserService;
@@ -40,6 +42,14 @@ public class PreUserController {
return ResponseEntity.ok(PreUserMapper.toResponse(preUser));
}
@PostMapping("/validate")
public ResponseEntity<ApiValidationErrorDto> validate(@RequestBody PreUserDto.Request request) {
Map<String, String> errors = preUserService.validate(request);
if(!errors.isEmpty())
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_CONTENT).body(new ApiValidationErrorDto(errors));
return ResponseEntity.ok(new ApiValidationErrorDto(Map.of()));
}
@PutMapping("/{pre_user_id}")
@PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')")
public ResponseEntity<PreUserDto.Response> update(

View File

@@ -1,16 +1,10 @@
package net.miarma.backend.huertos.controller;
import jakarta.transaction.Transactional;
import net.miarma.backend.huertos.dto.CreateWaitlistDto;
import net.miarma.backend.huertos.dto.PreUserDto;
import net.miarma.backend.huertos.dto.RequestCountDto;
import net.miarma.backend.huertos.dto.RequestDto;
import net.miarma.backend.huertos.dto.*;
import net.miarma.backend.huertos.dto.view.VRequestsWithPreUsersDto;
import net.miarma.backend.huertos.mapper.PreUserMapper;
import net.miarma.backend.huertos.mapper.RequestMapper;
import net.miarma.backend.huertos.mapper.view.VIncomesWithFullNamesMapper;
import net.miarma.backend.huertos.mapper.view.VRequestsWithPreUsersMapper;
import net.miarma.backend.huertos.model.PreUser;
import net.miarma.backend.huertos.model.Request;
import net.miarma.backend.huertos.model.view.VRequestsWithPreUsers;
import net.miarma.backend.huertos.service.PreUserService;
@@ -57,6 +51,23 @@ public class RequestController {
);
}
@PostMapping
public ResponseEntity<RequestDto.Response> create(RequestDto.Request dto) {
return ResponseEntity.ok(
RequestMapper.toResponse(
requestService.createWaitlist(
RequestMapper.toEntity(dto)
)));
}
@PostMapping("/waitlist")
@Transactional
public ResponseEntity<RequestDto.Response> createWaitlist(
@RequestBody CreateWaitlistDto body) {
RequestDto.Response response = requestService.createWaitlist(body.request(), body.preUser());
return ResponseEntity.ok(response);
}
@GetMapping("/count")
@PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')")
public ResponseEntity<RequestCountDto> getRequestCount() {
@@ -119,14 +130,6 @@ public class RequestController {
return ResponseEntity.ok(RequestMapper.toResponse(request));
}
@PostMapping
@Transactional
public ResponseEntity<RequestDto.Response> create(
@RequestBody CreateWaitlistDto body) {
RequestDto.Response response = requestService.create(body.request(), body.preUser());
return ResponseEntity.ok(response);
}
@PutMapping("/{request_id}")
@PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')")
public ResponseEntity<RequestDto.Response> update(

View File

@@ -39,6 +39,7 @@ public class AnnouncementDto {
private String body;
private Byte priority;
private UUID publishedBy;
private String publishedByName;
private Instant createdAt;
public UUID getAnnounceId() {
@@ -73,6 +74,10 @@ public class AnnouncementDto {
this.publishedBy = publishedBy;
}
public String getPublishedByName() { return publishedByName; }
public void setPublishedByName(String publishedByName) { this.publishedByName = publishedByName; }
public Instant getCreatedAt() {
return createdAt;
}

View File

@@ -0,0 +1,19 @@
package net.miarma.backend.huertos.dto;
import net.miarma.backlib.dto.CredentialDto;
import net.miarma.backlib.dto.UserDto;
import java.util.List;
public record MemberProfileDto(
UserDto user,
CredentialDto account,
HuertosUserMetadataDto metadata,
List<RequestDto.Response> requests,
List<IncomeDto.Response> payments,
boolean hasCollaborator,
boolean hasGreenhouse,
boolean hasCollaboratorRequest,
boolean hasGreenhouseRequest
) {
}

View File

@@ -1,5 +1,7 @@
package net.miarma.backend.huertos.dto;
import jakarta.annotation.Nullable;
import java.time.Instant;
import java.util.UUID;
@@ -33,15 +35,15 @@ public class RequestDto {
this.requestedBy = requestedBy;
}
public UUID getTargetUserId() {
public @Nullable UUID getTargetUserId() {
return targetUserId;
}
public void setTargetUserId(UUID targetUserId) {
public void setTargetUserId(@Nullable UUID targetUserId) {
this.targetUserId = targetUserId;
}
private UUID targetUserId;
@Nullable private UUID targetUserId;
}
public static class Response {

View File

@@ -14,6 +14,7 @@ public class AnnouncementMapper {
dto.setBody(entity.getBody());
dto.setPriority(entity.getPriority());
dto.setPublishedBy(entity.getPublishedBy());
dto.setPublishedByName(entity.getPublishedByName());
dto.setCreatedAt(entity.getCreatedAt());
return dto;
}

View File

@@ -23,8 +23,14 @@ public class Announcement {
@Column(name = "priority", nullable = false)
private Byte priority;
@Column(name = "published_by", columnDefinition = "BINARY(16)", nullable = false)
private UUID publishedBy;
@Column(name = "published_by", columnDefinition = "BINARY(16)", nullable = false)
private byte[] publishedByBin;
@Transient
private UUID publishedBy;
@Column(name = "published_by_name", nullable = false)
private String publishedByName;
@Column(name = "created_at", nullable = false)
private Instant createdAt;
@@ -35,6 +41,10 @@ public class Announcement {
if (announceId != null) {
announceIdBin = UuidUtil.uuidToBin(announceId);
}
if (publishedBy != null) {
publishedByBin = UuidUtil.uuidToBin(publishedBy);
}
}
@PostLoad
@@ -42,6 +52,10 @@ public class Announcement {
if (announceIdBin != null) {
announceId = UuidUtil.binToUUID(announceIdBin);
}
if (publishedByBin != null) {
publishedBy = UuidUtil.binToUUID(publishedByBin);
}
}
public UUID getAnnounceId() {
@@ -76,6 +90,14 @@ public class Announcement {
this.publishedBy = publishedBy;
}
public String getPublishedByName() {
return publishedByName;
}
public void setPublishedByName(String publishedByName) {
this.publishedByName = publishedByName;
}
public Instant getCreatedAt() {
return createdAt;
}

View File

@@ -4,4 +4,7 @@ import net.miarma.backend.huertos.model.PreUser;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PreUserRepository extends JpaRepository<PreUser, byte[]> {
boolean existsByDni(String dni);
boolean existsByEmail(String email);
}

View File

@@ -17,9 +17,11 @@ import java.util.UUID;
public class AnnouncementService {
private final AnnouncementRepository announcementRepository;
private final MemberService memberService;
public AnnouncementService(AnnouncementRepository announcementRepository) {
public AnnouncementService(AnnouncementRepository announcementRepository, MemberService memberService) {
this.announcementRepository = announcementRepository;
this.memberService = memberService;
}
public List<Announcement> getAll() {
@@ -36,6 +38,7 @@ public class AnnouncementService {
if (announcement.getAnnounceId() == null) {
announcement.setAnnounceId(UUID.randomUUID());
}
announcement.setPublishedByName(memberService.getById(announcement.getPublishedBy()).user().getDisplayName());
announcement.setCreatedAt(Instant.now());
return announcementRepository.save(announcement);
}

View File

@@ -0,0 +1,60 @@
package net.miarma.backend.huertos.service;
import net.miarma.backend.huertos.client.HuertosWebClient;
import net.miarma.backlib.dto.LoginRequest;
import net.miarma.backlib.dto.LoginResponse;
import net.miarma.backlib.security.CoreAuthTokenHolder;
import net.miarma.backlib.security.JwtService;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import tools.jackson.databind.ObjectMapper;
import java.time.Instant;
@Service
public class CoreAuthService {
private final RestTemplate authRestTemplate;
private final CoreAuthTokenHolder tokenHolder;
private final JwtService jwtService;
@Value("${huertos.user}")
private String username;
@Value("${huertos.password}")
private String password;
@Value("${core.url}")
private String coreUrl;
public CoreAuthService(@Qualifier("authRestTemplate") RestTemplate authRestTemplate,
CoreAuthTokenHolder tokenHolder,
JwtService jwtService) {
this.authRestTemplate = authRestTemplate;
this.tokenHolder = tokenHolder;
this.jwtService = jwtService;
}
public synchronized String getToken() {
if (tokenHolder.getToken() == null || tokenHolder.isExpired()) {
refreshToken();
}
return tokenHolder.getToken();
}
private void refreshToken() {
var req = new LoginRequest(username, password, (byte) 1);
LoginResponse resp = authRestTemplate.postForObject(
coreUrl + "/auth/login",
req,
LoginResponse.class
);
String token = resp.token();
Instant exp = jwtService.getExpiration(token).toInstant();
tokenHolder.setToken(token, exp);
}
}

View File

@@ -35,16 +35,16 @@ public class ExpenseService {
public Expense create(Expense expense) {
if (expense.getConcept() == null || expense.getConcept().isBlank()) {
throw new ValidationException("Concept is required");
throw new ValidationException("concept", "Concept is required");
}
if (expense.getAmount() == null) {
throw new ValidationException("Amount is required");
throw new ValidationException("amount", "Amount is required");
}
if (expense.getSupplier() == null || expense.getSupplier().isBlank()) {
throw new ValidationException("Supplier is required");
throw new ValidationException("supplier", "Supplier is required");
}
if (expense.getInvoice() == null || expense.getInvoice().isBlank()) {
throw new ValidationException("Invoice is required");
throw new ValidationException("invoice", "Invoice is required");
}
expense.setExpenseId(UUID.randomUUID());

View File

@@ -54,7 +54,7 @@ public class IncomeService {
throw new BadRequestException("concept is required");
}
if (income.getAmount() == null || income.getAmount().signum() <= 0) {
throw new ValidationException("amount must be positive");
throw new ValidationException("amount", "amount must be positive");
}
income.setIncomeId(UUID.randomUUID());
@@ -72,7 +72,7 @@ public class IncomeService {
if (dto.getConcept() != null) income.setConcept(dto.getConcept());
if (dto.getAmount() != null) {
if (dto.getAmount().signum() <= 0) {
throw new ValidationException("amount must be positive");
throw new ValidationException("amount", "amount must be positive");
}
income.setAmount(dto.getAmount());
}
@@ -106,4 +106,9 @@ public class IncomeService {
List<Income> incomes = getByUserId(userId);
return !incomes.isEmpty() && incomes.stream().allMatch(Income::isPaid);
}
public List<Income> getByMemberNumber(Integer memberNumber) {
UUID userId = metadataService.getByMemberNumber(memberNumber).getUserId();
return getByUserId(userId);
}
}

View File

@@ -1,9 +1,11 @@
package net.miarma.backend.huertos.service;
import net.miarma.backend.huertos.client.HuertosWebClient;
import net.miarma.backend.huertos.dto.IncomeDto;
import net.miarma.backend.huertos.dto.MemberDto;
import net.miarma.backend.huertos.dto.WaitlistCensoredDto;
import net.miarma.backend.huertos.mapper.HuertosUserMetadataMapper;
import net.miarma.backend.huertos.mapper.IncomeMapper;
import net.miarma.backend.huertos.security.NameCensorer;
import net.miarma.backlib.dto.UserWithCredentialDto;
import net.miarma.backlib.exception.NotFoundException;
@@ -31,12 +33,22 @@ public class MemberService {
this.metadataService = metadataService;
}
public MemberDto getById(UUID userId, Byte serviceId) {
var uwc = huertosWebClient.getUserWithCredential(userId, serviceId);
var meta = metadataService.getById(userId);
public MemberDto getById(UUID userId) {
var uwc = huertosWebClient.getUserWithCredential(userId, (byte)1);
if (uwc == null) {
throw new NotFoundException("User not found in core");
}
return new MemberDto(uwc.user(), uwc.account(),
HuertosUserMetadataMapper.toDto(meta));
var meta = metadataService.getById(userId);
if (meta == null) {
throw new NotFoundException("User metadata not found");
}
return new MemberDto(
uwc.user(),
uwc.account(),
HuertosUserMetadataMapper.toDto(meta)
);
}
public List<MemberDto> getAll(Byte serviceId) {
@@ -106,8 +118,10 @@ public class MemberService {
.orElseThrow(() -> new NotFoundException("Member not found"));
}
public Boolean hasIncomes(Integer memberNumber) {
return incomeService.existsByMemberNumber(memberNumber);
public List<IncomeDto.Response> getIncomes(Integer memberNumber) {
return incomeService.getByMemberNumber(memberNumber).stream()
.map(IncomeMapper::toResponse)
.toList();
}
public Boolean hasPaid(Integer memberNumber) {
@@ -132,11 +146,11 @@ public class MemberService {
.toList();
return plotMembers.stream()
.anyMatch(dto -> dto.metadata().getType().equals((byte)2));
.anyMatch(dto -> dto.metadata().getType().equals((byte)3));
}
public Boolean hasGreenhouse(Integer memberNumber) {
return metadataService.existsByMemberNumber(memberNumber);
return metadataService.getByMemberNumber(memberNumber).getType().equals((byte)2);
}
public Boolean hasCollaboratorRequest(Integer memberNumber) {

View File

@@ -2,9 +2,11 @@ package net.miarma.backend.huertos.service;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import net.miarma.backend.huertos.dto.PreUserDto;
import net.miarma.backend.huertos.validation.PreUserValidator;
import net.miarma.backlib.exception.BadRequestException;
import net.miarma.backlib.exception.NotFoundException;
import org.springframework.stereotype.Service;
@@ -100,4 +102,18 @@ public class PreUserService {
repository.deleteById(idBytes);
}
public Map<String, String> validate(PreUserDto.Request request) {
Map<String, String> errors = PreUserValidator.validate(request);
if (request.getDni() != null && repository.existsByDni(request.getDni())) {
errors.put("dni", "There is already an user with that NIF/NIE");
}
if (request.getEmail() != null && repository.existsByEmail(request.getEmail())) {
errors.put("email", "There is already an user with that email");
}
return errors;
}
}

View File

@@ -8,7 +8,6 @@ import net.miarma.backend.huertos.dto.PreUserDto;
import net.miarma.backend.huertos.dto.RequestDto;
import net.miarma.backend.huertos.mapper.PreUserMapper;
import net.miarma.backend.huertos.mapper.RequestMapper;
import net.miarma.backend.huertos.model.HuertosUserMetadata;
import net.miarma.backend.huertos.model.PreUser;
import net.miarma.backend.huertos.repository.PreUserRepository;
import net.miarma.backlib.exception.BadRequestException;
@@ -64,7 +63,7 @@ public class RequestService {
.toList();
}
public Request create(Request request) {
public Request createWaitlist(Request request) {
if (request.getType() == null) {
throw new BadRequestException("type is required");
}
@@ -82,8 +81,8 @@ public class RequestService {
}
@Transactional
public RequestDto.Response create(RequestDto.Request requestDto,
PreUserDto.Request preUserDto) {
public RequestDto.Response createWaitlist(RequestDto.Request requestDto,
PreUserDto.Request preUserDto) {
PreUser preUser = preUserRepository.save(PreUserMapper.toEntity(preUserDto));
Request request = requestRepository.save(RequestMapper.toEntity(requestDto));
return RequestMapper.toResponse(request);

View File

@@ -0,0 +1,66 @@
package net.miarma.backend.huertos.validation;
/**
* Validador de DNI/NIE español.
* <p>
* Este validador comprueba si un DNI o NIE es válido según las reglas establecidas por la legislación española.
* Un DNI debe tener 8 dígitos seguidos de una letra, y un NIE debe comenzar con X, Y o Z seguido de 7 dígitos y una letra.
*
* @author José Manuel Amador Gallardo
*/
public class DniValidator {
/**
* Valida un DNI o NIE español.
*
* @param id El DNI o NIE a validar.
* @return true si el DNI/NIE es válido, false en caso contrario.
*/
public static boolean isValid(String id) {
if (id == null || id.length() != 9) {
return false;
}
id = id.toUpperCase(); // Pa evitar problemas con minúsculas
String numberPart;
char letterPart = id.charAt(8);
if (id.startsWith("X") || id.startsWith("Y") || id.startsWith("Z")) {
// NIE
char prefix = id.charAt(0);
String numericPrefix = switch (prefix) {
case 'X' -> "0";
case 'Y' -> "1";
case 'Z' -> "2";
default -> null;
};
if (numericPrefix == null) return false;
numberPart = numericPrefix + id.substring(1, 8);
} else {
// DNI
numberPart = id.substring(0, 8);
}
if (!numberPart.matches("\\d{8}")) {
return false;
}
int number = Integer.parseInt(numberPart);
char expectedLetter = calculateLetter(number);
return letterPart == expectedLetter;
}
/**
* Calcula la letra correspondiente a un número de DNI.
*
* @param number El número del DNI (8 dígitos).
* @return La letra correspondiente.
*/
private static char calculateLetter(int number) {
String letters = "TRWAGMYFPDXBNJZSQVHLCKE";
return letters.charAt(number % 23);
}
}

View File

@@ -0,0 +1,61 @@
package net.miarma.backend.huertos.validation;
import net.miarma.backend.huertos.dto.PreUserDto;
import java.util.HashMap;
import java.util.Map;
public class PreUserValidator {
public static Map<String, String> validate(PreUserDto.Request dto) {
Map<String, String> errors = new HashMap<>();
if (dto.getUserName() == null || dto.getUserName().isBlank()) {
errors.put("userName", "El nombre de usuario es obligatorio");
} else if (dto.getUserName().length() < 3) {
errors.put("userName", "El nombre de usuario debe tener al menos 3 caracteres");
}
if (dto.getDisplayName() == null || dto.getDisplayName().isBlank()) {
errors.put("displayName", "El nombre visible es obligatorio");
}
if (dto.getDni() == null || dto.getDni().isBlank()) {
errors.put("dni", "El DNI es obligatorio");
} else if (!dto.getDni().matches("^[0-9]{8}[A-Za-z]$")) {
errors.put("dni", "DNI inválido");
}
if (dto.getEmail() == null || dto.getEmail().isBlank()) {
errors.put("email", "El email es obligatorio");
} else if (!dto.getEmail().matches("^[\\w-.]+@([\\w-]+\\.)+[\\w-]{2,4}$")) {
errors.put("email", "Email inválido");
}
if (dto.getPhone() == null || dto.getPhone().isBlank()) {
errors.put("phone", "El teléfono es obligatorio");
} else if (!dto.getPhone().matches("^[0-9]{9}$")) {
errors.put("phone", "Teléfono inválido");
}
if (dto.getAddress() == null || dto.getAddress().isBlank()) {
errors.put("address", "La dirección es obligatoria");
}
if (dto.getZipCode() == null || dto.getZipCode().isBlank()) {
errors.put("zipCode", "El código postal es obligatorio");
}
if (dto.getCity() == null || dto.getCity().isBlank()) {
errors.put("city", "La ciudad es obligatoria");
}
if (dto.getPassword() == null || dto.getPassword().isBlank()) {
errors.put("password", "La contraseña es obligatoria");
} else if (dto.getPassword().length() < 6) {
errors.put("password", "La contraseña debe tener al menos 6 caracteres");
}
return errors;
}
}