Restored dev

This commit is contained in:
Jose
2026-02-15 06:26:40 +01:00
parent de98ed5cc5
commit 3796e6f190
191 changed files with 7581 additions and 883 deletions

View File

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

View File

@@ -1,31 +1,62 @@
package net.miarma.backend.core.config;
import net.miarma.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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfigurationSource;
import java.util.Optional;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
private final JwtFilter jwtFilter;
private final JwtFilter jwtFilter;
private final RestAuthEntryPoint authEntryPoint;
private final RestAccessDeniedHandler accessDeniedHandler;
private final CorsConfigurationSource corsConfigurationSource;
public SecurityConfig(JwtFilter jwtFilter) {
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/**").permitAll()
.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()
);
@@ -33,9 +64,4 @@ public class SecurityConfig {
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
}

View File

@@ -0,0 +1,13 @@
package net.miarma.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

@@ -1,60 +1,122 @@
package net.miarma.backend.core.controller;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import net.miarma.backlib.dto.*;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
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.security.core.Authentication;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import net.miarma.backend.core.dto.LoginRequest;
import net.miarma.backend.core.dto.LoginResponse;
import net.miarma.backend.core.model.Credential;
import net.miarma.backend.core.service.AuthService;
import net.miarma.backend.core.service.JwtService;
import net.miarma.backend.core.service.CredentialService;
import net.miarma.backlib.security.JwtService;
@RestController
@RequestMapping("/auth")
public class AuthController {
private final AuthService authService;
private final JwtService jwtService;
private final AuthService authService;
public AuthController(AuthService authService, JwtService jwtService) {
this.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(response);
return ResponseEntity.ok(
new LoginResponse(response.token(), response.user(), response.account())
);
}
@PostMapping("/refresh")
@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("Token missing");
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("Invalid token");
return ResponseEntity.status(401).body(
new ApiErrorDto(
401,
"Unauthorized",
"Invalid token",
"/v2/core/auth/change-password"
)
);
}
UUID userId = jwtService.getUserId(token);
short serviceId = jwtService.getServiceId(token);
Byte serviceId = jwtService.getServiceId(token);
String newToken = jwtService.generateToken(userId, serviceId);
return ResponseEntity.ok(Map.of(
"token", newToken,
"userId", userId,
"serviceId", serviceId
"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

@@ -1,5 +1,97 @@
package net.miarma.backend.core.controller;
import java.util.List;
import java.util.UUID;
import net.miarma.backend.core.dto.UpdateCredentialDto;
import net.miarma.backend.core.mapper.CredentialMapper;
import net.miarma.backend.core.mapper.UserMapper;
import net.miarma.backend.core.security.CorePrincipal;
import net.miarma.backend.core.service.UserService;
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 net.miarma.backend.core.model.Credential;
import net.miarma.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("/test/{userId}")
public ResponseEntity<String> test(@PathVariable("userId") UUID userId) {
CorePrincipal principal = (CorePrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return ResponseEntity.ok(
userId.equals(principal.getUserId()) ? "OK" : "NO"
);
}
@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))
)
);
}
@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

@@ -1,5 +1,90 @@
package net.miarma.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 net.miarma.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.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import net.miarma.backend.core.model.File;
import net.miarma.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

@@ -1,10 +1,174 @@
package net.miarma.backend.core.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.UUID;
import net.miarma.backend.core.mapper.CredentialMapper;
import net.miarma.backend.core.model.Credential;
import net.miarma.backend.core.service.CredentialService;
import net.miarma.backlib.dto.*;
import org.apache.coyote.Response;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import net.miarma.backend.core.mapper.UserMapper;
import net.miarma.backend.core.model.User;
import net.miarma.backlib.security.JwtService;
import net.miarma.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

@@ -1,37 +0,0 @@
package net.miarma.backend.core.dto;
import jakarta.validation.constraints.NotBlank;
public class LoginRequest {
@NotBlank
private String username;
@NotBlank
private String password;
private short serviceId;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public short getServiceId() {
return serviceId;
}
public void setServiceId(short serviceId) {
this.serviceId = serviceId;
}
}

View File

@@ -1,37 +0,0 @@
package net.miarma.backend.core.dto;
public class LoginResponse {
private String token;
private short serviceId;
private UserDto user;
public LoginResponse(String token, short serviceId, UserDto user) {
this.token = token;
this.serviceId = serviceId;
this.user = user;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public short getServiceId() {
return serviceId;
}
public void setServiceId(short serviceId) {
this.serviceId = serviceId;
}
public UserDto getUser() {
return user;
}
public void setUser(UserDto user) {
this.user = user;
}
}

View File

@@ -0,0 +1,41 @@
package net.miarma.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

@@ -1,81 +0,0 @@
package net.miarma.backend.core.dto;
import java.time.Instant;
import java.util.UUID;
public class UserDto {
private UUID userId;
private String displayName;
private String avatar;
private Byte globalStatus;
private Byte globalRole;
private Instant createdAt;
private Instant updatedAt;
public UserDto(UUID userId, String displayName, String avatar, Byte globalStatus, Byte globalRole,
Instant createdAt, Instant updatedAt) {
this.userId = userId;
this.displayName = displayName;
this.avatar = avatar;
this.globalStatus = globalStatus;
this.globalRole = globalRole;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
public UUID getUserId() {
return userId;
}
public void setUserId(UUID userId) {
this.userId = userId;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public String 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,81 @@
package net.miarma.backend.core.mapper;
import net.miarma.backend.core.dto.UpdateCredentialDto;
import net.miarma.backend.core.model.Credential;
import net.miarma.backlib.dto.CreateCredentialDto;
import net.miarma.backlib.dto.CredentialDto;
import org.hibernate.sql.Update;
import java.util.UUID;
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,38 @@
package net.miarma.backend.core.mapper;
import net.miarma.backend.core.model.File;
import net.miarma.backlib.dto.FileDto;
import java.time.Instant;
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 net.miarma.backend.core.mapper;
import net.miarma.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

@@ -19,79 +19,66 @@ 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"})
}
)
@Table(name = "credentials",
uniqueConstraints = {
@UniqueConstraint(columnNames = { "service_id", "username" }),
@UniqueConstraint(columnNames = { "service_id", "email" })
})
public class Credential {
@Id
@Column(columnDefinition = "BINARY(16)")
private byte[] credentialIdBin;
@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;
@Column(name = "user_id", columnDefinition = "BINARY(16)")
private byte[] userIdBin;
@Transient
private UUID credentialId;
@CreationTimestamp
private Instant createdAt;
@UpdateTimestamp
private Instant updatedAt;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", insertable = false, updatable = false)
private User user;
@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
@PrePersist
@PreUpdate
private void prePersist() {
if (credentialId != null) {
ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
bb.putLong(credentialId.getMostSignificantBits());
bb.putLong(credentialId.getLeastSignificantBits());
credentialIdBin = bb.array();
credentialIdBin = UuidUtil.uuidToBin(credentialId);
}
if (userId != null) {
ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
bb.putLong(userId.getMostSignificantBits());
bb.putLong(userId.getLeastSignificantBits());
userIdBin = bb.array();
userIdBin = UuidUtil.uuidToBin(userId);
}
}
@PostLoad
private void postLoad() {
if (credentialIdBin != null) {
ByteBuffer bb = ByteBuffer.wrap(credentialIdBin);
long high = bb.getLong();
long low = bb.getLong();
credentialId = new UUID(high, low);
credentialId = UuidUtil.binToUUID(credentialIdBin);
}
if (userIdBin != null) {
ByteBuffer bb = ByteBuffer.wrap(userIdBin);
long high = bb.getLong();
long low = bb.getLong();
userId = new UUID(high, low);
userId = UuidUtil.binToUUID(userIdBin);
}
}
@@ -174,4 +161,4 @@ public class Credential {
public void setUser(User user) {
this.user = user;
}
}
}

View File

@@ -6,9 +6,12 @@ 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")
@@ -40,15 +43,28 @@ public class File {
private Instant uploadedAt;
@Column(name = "context", nullable = false)
private Short context;
private Byte context;
@PrePersist
public void prePersist() {
if (fileId == null) {
fileId = UUID.randomUUID();
@PreUpdate
private void prePersist() {
if (fileId != null) {
fileIdBin = UuidUtil.uuidToBin(fileId);
}
if (uploadedAt == null) {
uploadedAt = Instant.now();
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);
}
}
@@ -96,11 +112,11 @@ public class File {
return uploadedAt;
}
public Short getContext() {
public Byte getContext() {
return context;
}
public void setContext(Short context) {
public void setContext(Byte context) {
this.context = context;
}
}

View File

@@ -1,6 +1,5 @@
package net.miarma.backend.core.model;
import java.nio.ByteBuffer;
import java.time.Instant;
import java.util.UUID;
@@ -15,6 +14,7 @@ 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")
@@ -48,20 +48,14 @@ public class User {
@PreUpdate
private void prePersist() {
if (userId != null) {
ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
bb.putLong(userId.getMostSignificantBits());
bb.putLong(userId.getLeastSignificantBits());
userIdBin = bb.array();
userIdBin = UuidUtil.uuidToBin(userId);
}
}
@PostLoad
private void postLoad() {
if (userIdBin != null) {
ByteBuffer bb = ByteBuffer.wrap(userIdBin);
long high = bb.getLong();
long low = bb.getLong();
userId = new UUID(high, low);
userId = UuidUtil.binToUUID(userIdBin);
}
}

View File

@@ -4,19 +4,59 @@ import java.util.List;
import java.util.Optional;
import java.util.UUID;
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 net.miarma.backend.core.model.Credential;
public interface CredentialRepository extends JpaRepository<Credential, UUID> {
public interface CredentialRepository extends JpaRepository<Credential, byte[]> {
Optional<Credential> findByServiceIdAndUsername(short serviceId, String username);
@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);
Optional<Credential> findByServiceIdAndEmail(short serviceId, String email);
List<Credential> findAllByServiceId(Byte serviceId);
Optional<Credential> findByUserIdAndServiceId(UUID userId, short 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);
List<Credential> findByUserId(UUID userId);
Optional<Credential> findByUsernameAndServiceId(String username, Byte serviceId);
@Query("SELECT c FROM Credential c WHERE c.userIdBin = :userIdBin")
List<Credential> findByUserId(@Param("userIdBin") byte[] userIdBin);
List<Credential> findByEmail(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

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

View File

@@ -1,11 +1,9 @@
package net.miarma.backend.core.repository;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
import net.miarma.backend.core.model.User;
public interface UserRepository extends JpaRepository<User, UUID> {
public interface UserRepository extends JpaRepository<User, byte[]> {
}

View File

@@ -0,0 +1,46 @@
package net.miarma.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

@@ -1,4 +1,4 @@
package net.miarma.backend.core.config;
package net.miarma.backend.core.security;
import java.io.IOException;
import java.util.List;
@@ -8,7 +8,6 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
@@ -17,7 +16,7 @@ import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import net.miarma.backend.core.model.User;
import net.miarma.backend.core.service.JwtService;
import net.miarma.backlib.security.JwtService;
import net.miarma.backend.core.service.UserService;
@Component
@@ -36,7 +35,6 @@ public class JwtFilter extends OncePerRequestFilter {
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);
@@ -44,17 +42,11 @@ public class JwtFilter extends OncePerRequestFilter {
try {
if (jwtService.validateToken(token)) {
UUID userId = jwtService.getUserId(token);
short serviceId = jwtService.getServiceId(token);
Byte serviceId = jwtService.getServiceId(token);
User user = userService.getById(userId);
List<GrantedAuthority> authorities = List.of(
new SimpleGrantedAuthority("ROLE_" + user.getGlobalRole())
);
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(user, null, authorities);
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
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();
@@ -68,7 +60,7 @@ public class JwtFilter extends OncePerRequestFilter {
return;
}
}
filterChain.doFilter(request, response);
}
}

View File

@@ -1,46 +1,112 @@
package net.miarma.backend.core.service;
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 net.miarma.backend.core.dto.LoginRequest;
import net.miarma.backend.core.dto.LoginResponse;
import net.miarma.backend.core.dto.UserDto;
import net.miarma.backend.core.mapper.CredentialMapper;
import net.miarma.backend.core.mapper.UserMapper;
import net.miarma.backend.core.model.Credential;
import net.miarma.backend.core.model.User;
import tools.jackson.databind.JsonNode;
@Service
public class AuthService {
private final CredentialService credentialService;
private final JwtService jwtService;
private final UserService userService;
private final net.miarma.backlib.security.JwtService jwtService;
private final PasswordEncoder passwordEncoder;
public AuthService(CredentialService credentialService, JwtService jwtService,
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.getByUserIdAndService(request.getServiceId(), request.getUsername());
Credential cred = credentialService.getForLogin(request.serviceId(), request.username());
if (!passwordEncoder.matches(request.getPassword(), cred.getPassword())) {
throw new RuntimeException("Invalid credentials");
if (!passwordEncoder.matches(request.password(), cred.getPassword())) {
throw new UnauthorizedException("Credenciales no válidas");
}
String token = jwtService.generateToken(cred.getUserId(), request.getServiceId());
if (cred.getStatus() == 0) {
throw new ForbiddenException("Esa cuenta está desactivada");
}
UserDto userDto = new UserDto(
cred.getUser().getUserId(),
cred.getUser().getDisplayName(),
cred.getUser().getAvatar(),
cred.getUser().getGlobalStatus(),
cred.getUser().getGlobalRole(),
cred.getUser().getCreatedAt(),
cred.getUser().getUpdatedAt()
);
String token = jwtService.generateToken(cred.getUserId(), request.serviceId());
UserDto userDto = UserMapper.toDto(cred.getUser());
CredentialDto credentialDto = CredentialMapper.toDto(cred);
return new LoginResponse(token, request.getServiceId(), userDto);
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();
}
try {
userIdByEmail = credentialService.getByEmail(request.email()).get(0).getUserId();
} catch (NotFoundException e) { }
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

@@ -3,47 +3,164 @@ package net.miarma.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 net.miarma.backend.core.model.Credential;
import net.miarma.backend.core.model.User;
import net.miarma.backend.core.repository.CredentialRepository;
import net.miarma.backlib.dto.ChangePasswordRequest;
import net.miarma.backlib.dto.CredentialDto;
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) {
public CredentialService(CredentialRepository credentialRepository, UserService userService, PasswordEncoder passwordEncoder) {
this.credentialRepository = credentialRepository;
this.userService = userService;
this.passwordEncoder = passwordEncoder;
}
public Credential getById(UUID id) {
return credentialRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Credential not found"));
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) {
// TODO: validate duplicates here
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(userId);
List<Credential> creds = credentialRepository.findByUserId(UuidUtil.uuidToBin(userId));
if (creds.isEmpty()) {
throw new RuntimeException("User has no credentials");
throw new NotFoundException("El usuario no tiene cuenta");
}
return creds;
}
public List<Credential> getByUsername(@NotBlank String username) {
return credentialRepository.findByUsername(username);
}
public Credential getByUserIdAndService(short serviceId, String username) {
return credentialRepository.findByServiceIdAndUsername(serviceId, username)
.orElseThrow(() -> new RuntimeException("Credential not found in this site"));
public List<Credential> getByEmail(String email) {
return credentialRepository.findByEmail(email).stream()
.toList();
}
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(short serviceId, String username) {
public Credential getForLogin(Byte serviceId, String username) {
return credentialRepository.findByServiceIdAndUsername(serviceId, username)
.orElseThrow(() -> new RuntimeException("Invalid credentials"));
.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

@@ -1,13 +1,24 @@
package net.miarma.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.Map;
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;
import net.miarma.backend.core.model.File;
import net.miarma.backend.core.repository.FileRepository;
import net.miarma.backlib.util.UuidUtil;
@Service
@Transactional
@@ -15,32 +26,65 @@ public class FileService {
private final FileRepository fileRepository;
@Value("${filesDir}")
private String filesDir;
public FileService(FileRepository fileRepository) {
this.fileRepository = fileRepository;
}
public File get(UUID fileId) {
return fileRepository.findById(fileId)
.orElseThrow(() -> new RuntimeException("File not found"));
public File getById(UUID fileId) {
byte[] idBytes = UuidUtil.uuidToBin(fileId);
return fileRepository.findById(idBytes)
.orElseThrow(() -> new NotFoundException("Archivo no encontrado"));
}
public List<File> listByUser(UUID userId) {
public List<File> getAll() {
return fileRepository.findAll();
}
public List<File> getByUserId(UUID userId) {
return fileRepository.findByUploadedBy(userId);
}
public List<File> listByContext(short context) {
return fileRepository.findByContext(context);
}
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());
public File create(File file) {
return fileRepository.save(file);
}
public void delete(UUID fileId) {
if (!fileRepository.existsById(fileId)) {
throw new RuntimeException("File not found");
public File update(UUID fileId, File file) {
byte[] idBytes = UuidUtil.uuidToBin(fileId);
if (!fileRepository.existsById(idBytes)) {
throw new NotFoundException("Archivo no encontrado");
}
fileRepository.deleteById(fileId);
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

@@ -1,56 +0,0 @@
package net.miarma.backend.core.service;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Service;
import java.security.Key;
import java.util.Date;
import java.util.UUID;
@Service
public class JwtService {
private final String secret = "miarma-esto-es-un-secreto-super-largo-para-jwt-1234567890";
private final Key key = Keys.hmacShaKeyFor(secret.getBytes());
private final long expiration = 3600_000;
public String generateToken(UUID userId, short serviceId) {
Date now = new Date();
Date exp = new Date(now.getTime() + expiration);
return Jwts.builder()
.setSubject(userId.toString())
.claim("service", serviceId)
.setIssuedAt(now)
.setExpiration(exp)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
public UUID getUserId(String token) {
Claims claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
return UUID.fromString(claims.getSubject());
}
public short getServiceId(String token) {
Claims claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
return ((Number) claims.get("service")).shortValue();
}
public Date getExpiration(String token) {
Claims claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
return claims.getExpiration();
}
}

View File

@@ -1,12 +1,21 @@
package net.miarma.backend.core.service;
import java.util.List;
import java.util.UUID;
import net.miarma.backend.core.mapper.UserMapper;
import net.miarma.backend.core.security.CorePrincipal;
import net.miarma.backlib.dto.ChangeAvatarRequest;
import net.miarma.backlib.exception.NotFoundException;
import net.miarma.backlib.exception.ValidationException;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import jakarta.transaction.Transactional;
import net.miarma.backend.core.model.User;
import net.miarma.backend.core.repository.UserRepository;
import net.miarma.backlib.dto.UserDto;
import net.miarma.backlib.util.UuidUtil;
@Service
@Transactional
@@ -17,25 +26,88 @@ public class UserService {
this.userRepository = userRepository;
}
public List<User> getAll() {
return userRepository.findAll();
}
public User getById(UUID userId) {
return userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("User not found"));
byte[] idBytes = UuidUtil.uuidToBin(userId);
return userRepository.findById(idBytes)
.orElseThrow(() -> new NotFoundException("Usuario no encontrado"));
}
public User create(User user) {
// TODO: basic validation
return userRepository.save(user);
}
public User update(User user) {
if(!userRepository.existsById(user.getUserId()))
throw new RuntimeException("User not found");
return userRepository.save(user);
}
public void delete(UUID userId) {
if(!userRepository.existsById(userId))
throw new RuntimeException("User not found");
userRepository.deleteById(userId);
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/miarma_v2
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/miarma_v2
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

@@ -1,12 +1,25 @@
spring:
datasource:
url: jdbc:mariadb://localhost:3306/miarma_v2
username: USER
password: PASS
application:
name: core-service
jpa:
open-in-view: false
hibernate:
ddl-auto: update
show-sql: true
ddl-auto: validate
properties:
hibernate:
format_sql: true
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