Add: CORS handling for dev profile. Fix: custom dates on socios, ingresos and gastos. Add: new endpoint for socios dropdown on ingresos

This commit is contained in:
Jose
2026-02-01 14:43:39 +01:00
parent d3c19a6186
commit 6a5a4b6871
18 changed files with 188 additions and 17 deletions

5
TODO
View File

@@ -1,4 +1,5 @@
POR HACER -------------------------------- POR HACER --------------------------------
- implementar urlParams para filtros
- documentación - documentación
- mail wrapper - mail wrapper
@@ -8,7 +9,9 @@ RESUELTO ---------------------------------
- aceptar solicitudes LE/Colab (sobre todo por crear preusers) - aceptar solicitudes LE/Colab (sobre todo por crear preusers)
- mejorar queries para no filtrar en memoria -> IMPOSIBLE CON ENDPOINTS INTERNOS DE CORE: RESUELTO CON CACHING - mejorar queries para no filtrar en memoria -> IMPOSIBLE CON ENDPOINTS INTERNOS DE CORE: RESUELTO CON CACHING
- normalizar el uso de services y repositories desde otros services y repositories - normalizar el uso de services y repositories desde otros services y repositories
- implementar urlParams para filtros -> NO RESUELTO (DEPRECATED)
- sistema comun de errores en back & front - sistema comun de errores en back & front
- nombre del requester - nombre del requester
- cambiar contraseña (?) - cambiar contraseña (?)
- todos los socios en dropdown ingresos
- validacion LE/COlab
- createdAt custom en ing,gastos

View File

@@ -0,0 +1,41 @@
package net.miarma.backlib.filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class RequestLoggingFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(RequestLoggingFilter.class);
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
long start = System.currentTimeMillis();
filterChain.doFilter(request, response);
long duration = System.currentTimeMillis() - start;
log.info("({}) {} {} -> {} ({} ms)",
request.getRemoteAddr(),
request.getMethod(),
request.getRequestURI(),
response.getStatus(),
duration
);
}
}

View File

@@ -0,0 +1,27 @@
package net.miarma.backlib.http;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@Profile("dev") // esto asegura que solo se cargue en dev
public class DevCorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000") // tu frontend React
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true)
.allowedHeaders("*");
}
};
}
}

View File

@@ -21,6 +21,8 @@
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/exception/NotFoundException.java /home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/exception/NotFoundException.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/exception/UnauthorizedException.java /home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/exception/UnauthorizedException.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/exception/ValidationException.java /home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/exception/ValidationException.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/filter/RequestLoggingFilter.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/http/DevCorsConfig.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/http/GlobalExceptionHandler.java /home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/http/GlobalExceptionHandler.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/http/RestAccessDeniedHandler.java /home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/http/RestAccessDeniedHandler.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/http/RestAuthEntryPoint.java /home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/http/RestAuthEntryPoint.java

10
build-upload.sh Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/bash
cd core/
mvn clean package
cd ..
cd huertos/
mvn clean package
cd ..
scp core/target/core-1.0.0.jar jomaa@10.0.0.254:/home/jomaa/transfer
scp huertos/target/huertos-1.0.0.jar jomaa@10.0.0.254:/home/jomaa/transfer

View File

@@ -12,6 +12,9 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfigurationSource;
import java.util.Optional;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@@ -20,19 +23,26 @@ public class SecurityConfig {
private final JwtFilter jwtFilter; private final JwtFilter jwtFilter;
private final RestAuthEntryPoint authEntryPoint; private final RestAuthEntryPoint authEntryPoint;
private final RestAccessDeniedHandler accessDeniedHandler; private final RestAccessDeniedHandler accessDeniedHandler;
private final CorsConfigurationSource corsConfigurationSource;
public SecurityConfig( public SecurityConfig(
JwtFilter jwtFilter, JwtFilter jwtFilter,
RestAuthEntryPoint authEntryPoint, RestAuthEntryPoint authEntryPoint,
RestAccessDeniedHandler accessDeniedHandler RestAccessDeniedHandler accessDeniedHandler,
Optional<CorsConfigurationSource> corsConfigurationSource
) { ) {
this.jwtFilter = jwtFilter; this.jwtFilter = jwtFilter;
this.authEntryPoint = authEntryPoint; this.authEntryPoint = authEntryPoint;
this.accessDeniedHandler = accessDeniedHandler; this.accessDeniedHandler = accessDeniedHandler;
this.corsConfigurationSource = corsConfigurationSource.orElse(null);
} }
@Bean @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
if (corsConfigurationSource != null) {
http.cors(Customizer.withDefaults());
}
http http
.csrf(csrf -> csrf.disable()) .csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

View File

@@ -5,12 +5,16 @@ import net.miarma.backlib.http.RestAccessDeniedHandler;
import net.miarma.backlib.http.RestAuthEntryPoint; import net.miarma.backlib.http.RestAuthEntryPoint;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfigurationSource;
import java.util.Optional;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@@ -20,19 +24,26 @@ public class SecurityConfig {
private final HuertosJwtFilter jwtFilter; private final HuertosJwtFilter jwtFilter;
private final RestAuthEntryPoint authEntryPoint; private final RestAuthEntryPoint authEntryPoint;
private final RestAccessDeniedHandler accessDeniedHandler; private final RestAccessDeniedHandler accessDeniedHandler;
private final CorsConfigurationSource corsConfigurationSource;
public SecurityConfig( public SecurityConfig(
HuertosJwtFilter jwtFilter, HuertosJwtFilter jwtFilter,
RestAuthEntryPoint authEntryPoint, RestAuthEntryPoint authEntryPoint,
RestAccessDeniedHandler accessDeniedHandler RestAccessDeniedHandler accessDeniedHandler,
Optional<CorsConfigurationSource> corsConfigurationSource
) { ) {
this.jwtFilter = jwtFilter; this.jwtFilter = jwtFilter;
this.authEntryPoint = authEntryPoint; this.authEntryPoint = authEntryPoint;
this.accessDeniedHandler = accessDeniedHandler; this.accessDeniedHandler = accessDeniedHandler;
this.corsConfigurationSource = corsConfigurationSource.orElse(null);
} }
@Bean @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
if (corsConfigurationSource != null) {
http.cors(Customizer.withDefaults());
}
http http
.csrf(csrf -> csrf.disable()) .csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

View File

@@ -31,7 +31,7 @@ public class MemberController {
@GetMapping @GetMapping
@PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')") @PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')")
public ResponseEntity<List<MemberDto>> getAll() { public ResponseEntity<List<MemberDto>> getAll() {
return ResponseEntity.ok(memberService.getAll((byte)1)); return ResponseEntity.ok(memberService.getAll());
} }
@GetMapping("/me") @GetMapping("/me")
@@ -45,6 +45,12 @@ public class MemberController {
); );
} }
@GetMapping("/dropdown")
@PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')")
public ResponseEntity<List<DropdownDto>> getDropdown() {
return ResponseEntity.ok(memberService.getDropdown());
}
@GetMapping("/{user_id:[0-9a-fA-F\\-]{36}}") @GetMapping("/{user_id:[0-9a-fA-F\\-]{36}}")
@PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')") @PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')")
public ResponseEntity<MemberDto> getById(@PathVariable("user_id") UUID userId) { public ResponseEntity<MemberDto> getById(@PathVariable("user_id") UUID userId) {

View File

@@ -0,0 +1,6 @@
package net.miarma.backend.huertos.dto;
import java.util.UUID;
public record DropdownDto(UUID userId, Integer memberNumber, String displayName) {
}

View File

@@ -11,6 +11,7 @@ public class ExpenseDto {
private String supplier; private String supplier;
private String invoice; private String invoice;
private Byte type; private Byte type;
private Instant createdAt;
public String getConcept() { public String getConcept() {
return concept; return concept;
@@ -51,6 +52,14 @@ public class ExpenseDto {
public void setType(Byte type) { public void setType(Byte type) {
this.type = type; this.type = type;
} }
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
} }
public static class Response { public static class Response {
@@ -59,6 +68,8 @@ public class ExpenseDto {
private BigDecimal amount; private BigDecimal amount;
private String supplier; private String supplier;
private String invoice; private String invoice;
private Byte type;
private Instant createdAt;
public UUID getExpenseId() { public UUID getExpenseId() {
return expenseId; return expenseId;
@@ -115,8 +126,5 @@ public class ExpenseDto {
public void setCreatedAt(Instant createdAt) { public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt; this.createdAt = createdAt;
} }
private Byte type;
private Instant createdAt;
} }
} }

View File

@@ -12,6 +12,7 @@ public class IncomeDto {
private BigDecimal amount; private BigDecimal amount;
private Byte type; private Byte type;
private Byte frequency; private Byte frequency;
private Instant createdAt;
public Integer getMemberNumber() { public Integer getMemberNumber() {
return memberNumber; return memberNumber;
@@ -60,6 +61,14 @@ public class IncomeDto {
public void setFrequency(Byte frequency) { public void setFrequency(Byte frequency) {
this.frequency = frequency; this.frequency = frequency;
} }
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
} }
public static class Response { public static class Response {

View File

@@ -0,0 +1,10 @@
package net.miarma.backend.huertos.mapper;
import net.miarma.backend.huertos.dto.DropdownDto;
import net.miarma.backend.huertos.dto.MemberDto;
public class DropdownDtoMapper {
public static DropdownDto toDto(MemberDto dto) {
return new DropdownDto(dto.user().getUserId(), dto.metadata().getMemberNumber(), dto.user().getDisplayName());
}
}

View File

@@ -28,6 +28,7 @@ public class ExpenseMapper {
entity.setSupplier(dto.getSupplier()); entity.setSupplier(dto.getSupplier());
entity.setInvoice(dto.getInvoice()); entity.setInvoice(dto.getInvoice());
entity.setType(dto.getType()); entity.setType(dto.getType());
entity.setCreatedAt(dto.getCreatedAt());
return entity; return entity;
} }
} }

View File

@@ -28,6 +28,7 @@ public class IncomeMapper {
entity.setAmount(dto.getAmount()); entity.setAmount(dto.getAmount());
entity.setType(dto.getType()); entity.setType(dto.getType());
entity.setFrequency(dto.getFrequency()); entity.setFrequency(dto.getFrequency());
entity.setCreatedAt(dto.getCreatedAt());
return entity; return entity;
} }
} }

View File

@@ -46,9 +46,11 @@ public class ExpenseService {
if (expense.getInvoice() == null || expense.getInvoice().isBlank()) { if (expense.getInvoice() == null || expense.getInvoice().isBlank()) {
throw new ValidationException("invoice", "La factura es obligatoria"); throw new ValidationException("invoice", "La factura es obligatoria");
} }
if (expense.getCreatedAt() == null) {
expense.setCreatedAt(Instant.now());
}
expense.setExpenseId(UUID.randomUUID()); expense.setExpenseId(UUID.randomUUID());
expense.setCreatedAt(Instant.now());
return expenseRepository.save(expense); return expenseRepository.save(expense);
} }

View File

@@ -1,6 +1,7 @@
package net.miarma.backend.huertos.service; package net.miarma.backend.huertos.service;
import java.time.Instant; import java.time.Instant;
import java.time.temporal.TemporalAmount;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@@ -56,9 +57,11 @@ public class IncomeService {
if (income.getAmount() == null || income.getAmount().signum() <= 0) { if (income.getAmount() == null || income.getAmount().signum() <= 0) {
throw new ValidationException("amount", "La cantidad debe ser positiva"); throw new ValidationException("amount", "La cantidad debe ser positiva");
} }
if (income.getCreatedAt() == null) {
income.setCreatedAt(Instant.now());
}
income.setIncomeId(UUID.randomUUID()); income.setIncomeId(UUID.randomUUID());
income.setCreatedAt(Instant.now());
return incomeRepository.save(income); return incomeRepository.save(income);
} }

View File

@@ -2,6 +2,7 @@
import net.miarma.backend.huertos.client.HuertosWebClient; import net.miarma.backend.huertos.client.HuertosWebClient;
import net.miarma.backend.huertos.dto.*; import net.miarma.backend.huertos.dto.*;
import net.miarma.backend.huertos.mapper.DropdownDtoMapper;
import net.miarma.backend.huertos.mapper.RequestMapper; import net.miarma.backend.huertos.mapper.RequestMapper;
import net.miarma.backend.huertos.mapper.UserMetadataMapper; import net.miarma.backend.huertos.mapper.UserMetadataMapper;
import net.miarma.backend.huertos.mapper.IncomeMapper; import net.miarma.backend.huertos.mapper.IncomeMapper;
@@ -52,8 +53,8 @@
} }
@Cacheable("members") @Cacheable("members")
public List<MemberDto> getAll(Byte serviceId) { public List<MemberDto> getAll() {
List<UserWithCredentialDto> all = huertosWebClient.getAllUsersWithCredentials(serviceId); List<UserWithCredentialDto> all = huertosWebClient.getAllUsersWithCredentials((byte)1);
return all.stream() return all.stream()
.filter(uwc -> metadataService.existsById(uwc.user().getUserId())) .filter(uwc -> metadataService.existsById(uwc.user().getUserId()))
@@ -125,21 +126,21 @@
} }
public MemberDto getByMemberNumber(Integer memberNumber) { public MemberDto getByMemberNumber(Integer memberNumber) {
return getAll((byte)1).stream() return getAll().stream()
.filter(dto -> dto.metadata().getMemberNumber().equals(memberNumber)) .filter(dto -> dto.metadata().getMemberNumber().equals(memberNumber))
.findFirst() .findFirst()
.orElseThrow(() -> new NotFoundException("No hay socio con ese número")); .orElseThrow(() -> new NotFoundException("No hay socio con ese número"));
} }
public MemberDto getByPlotNumber(Integer plotNumber) { public MemberDto getByPlotNumber(Integer plotNumber) {
return getAll((byte)1).stream() return getAll().stream()
.filter(dto -> dto.metadata().getPlotNumber().equals(plotNumber)) .filter(dto -> dto.metadata().getPlotNumber().equals(plotNumber))
.findFirst() .findFirst()
.orElseThrow(() -> new NotFoundException("No hay socio con ese huerto")); .orElseThrow(() -> new NotFoundException("No hay socio con ese huerto"));
} }
public MemberDto getByDni(String dni) { public MemberDto getByDni(String dni) {
return getAll((byte)1).stream() return getAll().stream()
.filter(dto -> dni.equals(dto.metadata().getDni())) .filter(dto -> dni.equals(dto.metadata().getDni()))
.findFirst() .findFirst()
.orElseThrow(() -> new NotFoundException("No hay socio con ese DNI")); .orElseThrow(() -> new NotFoundException("No hay socio con ese DNI"));
@@ -156,7 +157,7 @@
} }
public Boolean hasCollaborator(Integer memberNumber) { public Boolean hasCollaborator(Integer memberNumber) {
List<MemberDto> all = getAll((byte)1); List<MemberDto> all = getAll();
var member = all.stream() var member = all.stream()
.filter(dto -> dto.metadata().getMemberNumber().equals(memberNumber)) .filter(dto -> dto.metadata().getMemberNumber().equals(memberNumber))
@@ -189,4 +190,10 @@
UUID userId = metadataService.getByMemberNumber(memberNumber).getUserId(); UUID userId = metadataService.getByMemberNumber(memberNumber).getUserId();
return requestService.hasGreenhouseRequest(userId); return requestService.hasGreenhouseRequest(userId);
} }
public List<DropdownDto> getDropdown() {
return getAll().stream()
.map(DropdownDtoMapper::toDto)
.toList();
}
} }

View File

@@ -49,7 +49,7 @@ public class RequestValidator {
if (requestType == 2) { if (requestType == 2) {
if (metadata.getPlotNumber() == null) { if (metadata.getPlotNumber() == null) {
throw new BadRequestException("El colaborador debe tener parcela"); throw new BadRequestException("El colaborador debe tener huerto");
} }
} }
@@ -65,8 +65,22 @@ public class RequestValidator {
} }
} }
if (requestType == 0) {
if (isBlank(metadata.getAddress())) {
throw new ValidationException("address", "La dirección es obligatoria");
}
if (isBlank(metadata.getZipCode())) {
throw new ValidationException("zipCode", "El código postal es obligatorio");
} else if(metadata.getZipCode().length() < 5) {
throw new ValidationException("zipCode", "El código postal debe tener 5 dígitos");
}
if (isBlank(metadata.getCity())) {
throw new ValidationException("city", "La ciudad es obligatoria");
}
}
if (metadata.getPhone() != null && !PHONE_PATTERN.matcher(metadata.getPhone()).matches()) { if (metadata.getPhone() != null && !PHONE_PATTERN.matcher(metadata.getPhone()).matches()) {
throw new ValidationException("phone", "Teléfono inválido (debe tener entre 9 y 15 dígitos, opcional +)"); throw new ValidationException("phone", "Teléfono inválido (debe tener 9 dígitos)");
} }
} }