Add: custom Exception for fine-grain error handling. Add: env variables for deploying in prod.

This commit is contained in:
Jose
2026-01-22 13:02:24 +01:00
parent e95d5a0793
commit 01b7425237
42 changed files with 665 additions and 205 deletions

View File

@@ -1,6 +1,8 @@
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.authentication.AuthenticationManager;
@@ -18,10 +20,18 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
private final JwtFilter jwtFilter;
private final JwtFilter jwtFilter;
private final RestAuthEntryPoint authEntryPoint;
private final RestAccessDeniedHandler accessDeniedHandler;
public SecurityConfig(JwtFilter jwtFilter) {
public SecurityConfig(
JwtFilter jwtFilter,
RestAuthEntryPoint authEntryPoint,
RestAccessDeniedHandler accessDeniedHandler
) {
this.jwtFilter = jwtFilter;
this.authEntryPoint = authEntryPoint;
this.accessDeniedHandler = accessDeniedHandler;
}
@Bean
@@ -29,6 +39,10 @@ public class SecurityConfig {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(ex -> ex
.authenticationEntryPoint(authEntryPoint)
.accessDeniedHandler(accessDeniedHandler)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**", "/screenshot").permitAll()
.anyRequest().authenticated()

View File

@@ -2,6 +2,9 @@ package net.miarma.backend.core.service;
import java.util.UUID;
import net.miarma.backlib.exception.ConflictException;
import net.miarma.backlib.exception.ForbiddenException;
import net.miarma.backlib.exception.UnauthorizedException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@@ -34,9 +37,13 @@ public class AuthService {
public LoginResponse login(LoginRequest request) {
Credential cred = credentialService.getForLogin(request.serviceId(), request.username());
if (!passwordEncoder.matches(request.password(), cred.getPassword())) {
throw new RuntimeException("Invalid credentials");
throw new UnauthorizedException("Invalid credentials");
}
if (cred.getStatus() == 0) {
throw new ForbiddenException("This account is inactive");
}
String token = jwtService.generateToken(cred.getUserId(), request.serviceId());
@@ -48,13 +55,13 @@ public class AuthService {
public LoginResponse register(RegisterRequest request) {
if (credentialService.existsByUsernameAndService(request.username(), request.serviceId())) {
throw new RuntimeException("Username already taken");
throw new ConflictException("Username already taken");
}
User user;
try {
user = credentialService.getByEmail(request.email());
} catch (RuntimeException e) {
} catch (Exception e) {
UserDto dto = new UserDto();
dto.setUserId(UUID.randomUUID());
dto.setDisplayName(request.displayName());

View File

@@ -3,6 +3,10 @@ package net.miarma.backend.core.service;
import java.util.List;
import java.util.UUID;
import net.miarma.backlib.exception.BadRequestException;
import net.miarma.backlib.exception.ConflictException;
import net.miarma.backlib.exception.NotFoundException;
import net.miarma.backlib.exception.ValidationException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -29,30 +33,30 @@ public class CredentialService {
public Credential getById(UUID credentialId) {
byte[] idBytes = UuidUtil.uuidToBin(credentialId);
return credentialRepository.findById(idBytes)
.orElseThrow(() -> new RuntimeException("Credential not found"));
.orElseThrow(() -> new NotFoundException("Credential not found"));
}
public Credential create(Credential credential) {
if (credential.getUsername() == null || credential.getUsername().isBlank()) {
throw new IllegalArgumentException("Username cannot be blank");
throw new ValidationException("Username cannot be blank");
}
if (credential.getEmail() == null || !credential.getEmail().matches("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$")) {
throw new IllegalArgumentException("Invalid email format");
throw new ValidationException("Invalid email format");
}
if (credential.getPassword() == null || credential.getPassword().length() < 6) {
throw new IllegalArgumentException("Password must be at least 6 characters");
throw new ValidationException("Password must be at least 6 characters");
}
if (credential.getServiceId() == null || credential.getServiceId() < 0) {
throw new IllegalArgumentException("ServiceId must be positive");
throw new ValidationException("ServiceId must be positive");
}
boolean existsUsername = credentialRepository.existsByUsernameAndServiceId(
credential.getUsername(), credential.getServiceId());
if (existsUsername) throw new IllegalArgumentException("Username already exists for this service");
if (existsUsername) throw new ConflictException("Username already exists for this service");
boolean existsEmail = credentialRepository.existsByEmailAndServiceId(
credential.getEmail(), credential.getServiceId());
if (existsEmail) throw new IllegalArgumentException("Email already exists for this service");
if (existsEmail) throw new ConflictException("Email already exists for this service");
credential.setCredentialId(UUID.randomUUID());
credential.setPassword(passwordEncoder.encode(credential.getPassword()));
@@ -74,25 +78,25 @@ public class CredentialService {
public List<Credential> getByUserId(UUID userId) {
List<Credential> creds = credentialRepository.findByUserId(UuidUtil.uuidToBin(userId));
if (creds.isEmpty()) {
throw new RuntimeException("User has no credentials");
throw new NotFoundException("User has no credentials");
}
return creds;
}
public User getByEmail(String email) {
return credentialRepository.findByEmail(email)
.orElseThrow(() -> new RuntimeException("No credential found for email"))
.orElseThrow(() -> new NotFoundException("No credential found for email"))
.getUser();
}
public Credential getByUserIdAndService(UUID userId, Byte serviceId) {
return credentialRepository.findByUserIdAndServiceId(userId, serviceId)
.orElseThrow(() -> new RuntimeException("Credential not found in this site"));
.orElseThrow(() -> new NotFoundException("Credential not found in this site"));
}
public Credential getForLogin(Byte serviceId, String username) {
return credentialRepository.findByServiceIdAndUsername(serviceId, username)
.orElseThrow(() -> new RuntimeException("Invalid credentials"));
.orElseThrow(() -> new BadRequestException("Invalid credentials"));
}
public boolean existsByUsernameAndService(String username, int serviceId) {
@@ -102,7 +106,7 @@ public class CredentialService {
public boolean isOwner(UUID credentialId, UUID userId) {
byte[] idBytes = UuidUtil.uuidToBin(credentialId);
Credential c = credentialRepository.findById(idBytes)
.orElseThrow(() -> new RuntimeException("Credential not found"));
.orElseThrow(() -> new NotFoundException("Credential not found"));
return c.getUserId().equals(userId);
}
@@ -110,16 +114,16 @@ public class CredentialService {
byte[] idBytes = UuidUtil.uuidToBin(credentialId);
Credential cred = credentialRepository.findById(idBytes)
.orElseThrow(() -> new RuntimeException("Credential not found"));
.orElseThrow(() -> new NotFoundException("Credential not found"));
if (dto.getUsername() != null && dto.getUsername().isBlank()) {
throw new IllegalArgumentException("Username cannot be blank");
throw new ValidationException("Username cannot be blank");
}
if (dto.getEmail() != null && !dto.getEmail().matches("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$")) {
throw new IllegalArgumentException("Invalid email format");
throw new ValidationException("Invalid email format");
}
if (dto.getServiceId() != null && dto.getServiceId() < 0) {
throw new IllegalArgumentException("ServiceId must be positive");
throw new ValidationException("ServiceId must be positive");
}
if (dto.getUsername() != null) cred.setUsername(dto.getUsername());
@@ -134,10 +138,10 @@ public class CredentialService {
byte[] idBytes = UuidUtil.uuidToBin(credentialId);
Credential cred = credentialRepository.findById(idBytes)
.orElseThrow(() -> new RuntimeException("Credential not found"));
.orElseThrow(() -> new NotFoundException("Credential not found"));
if (!passwordEncoder.matches(request.oldPassword(), cred.getPassword())) {
throw new IllegalArgumentException("Old password is incorrect");
throw new ValidationException("Old password is incorrect");
}
cred.setPassword(passwordEncoder.encode(request.newPassword()));
@@ -147,19 +151,19 @@ public class CredentialService {
public void delete(UUID credentialId) {
byte[] idBytes = UuidUtil.uuidToBin(credentialId);
if(!credentialRepository.existsById(idBytes))
throw new RuntimeException("Credential not found");
throw new NotFoundException("Credential not found");
credentialRepository.deleteById(idBytes);
}
public Byte getStatus(UUID credentialId) {
Credential credential = credentialRepository.findById(UuidUtil.uuidToBin(credentialId))
.orElseThrow(() -> new RuntimeException("User not found"));;
.orElseThrow(() -> new NotFoundException("User not found"));;
return credential.getStatus();
}
public void updateStatus(UUID credentialId, Byte status) {
Credential credential = credentialRepository.findById(UuidUtil.uuidToBin(credentialId))
.orElseThrow(() -> new RuntimeException("User not found"));;
.orElseThrow(() -> new NotFoundException("User not found"));;
credential.setStatus(status);
credentialRepository.save(credential);
}

View File

@@ -11,6 +11,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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -32,7 +33,7 @@ public class FileService {
public File getById(UUID fileId) {
byte[] idBytes = UuidUtil.uuidToBin(fileId);
return fileRepository.findById(idBytes)
.orElseThrow(() -> new RuntimeException("File not found"));
.orElseThrow(() -> new NotFoundException("File not found"));
}
public List<File> getAll() {
@@ -62,7 +63,7 @@ public class FileService {
public File update(File file) {
byte[] idBytes = UuidUtil.uuidToBin(file.getFileId());
if (!fileRepository.existsById(idBytes)) {
throw new RuntimeException("File not found");
throw new NotFoundException("File not found");
}
return fileRepository.save(file);
}
@@ -70,7 +71,7 @@ public class FileService {
public void delete(UUID fileId) {
byte[] idBytes = UuidUtil.uuidToBin(fileId);
if (!fileRepository.existsById(idBytes)) {
throw new RuntimeException("File not found");
throw new NotFoundException("File not found");
}
fileRepository.deleteById(idBytes);
}

View File

@@ -5,6 +5,8 @@ import java.util.UUID;
import net.miarma.backend.core.mapper.UserMapper;
import net.miarma.backlib.dto.ChangeAvatarRequest;
import net.miarma.backlib.exception.NotFoundException;
import net.miarma.backlib.exception.ValidationException;
import org.springframework.stereotype.Service;
import jakarta.transaction.Transactional;
@@ -29,12 +31,12 @@ public class UserService {
public User getById(UUID userId) {
byte[] idBytes = UuidUtil.uuidToBin(userId);
return userRepository.findById(idBytes)
.orElseThrow(() -> new RuntimeException("User not found"));
.orElseThrow(() -> new NotFoundException("User not found"));
}
public User create(UserDto dto) {
if(dto.getDisplayName() == null || dto.getDisplayName().isBlank()) {
throw new RuntimeException("Display name is required");
throw new ValidationException("Display name is required");
}
User user = new User();
@@ -50,7 +52,7 @@ public class UserService {
public User update(UUID userId, UserDto dto) {
byte[] idBytes = UuidUtil.uuidToBin(userId);
User user = userRepository.findById(idBytes)
.orElseThrow(() -> new RuntimeException("User not found"));
.orElseThrow(() -> new NotFoundException("User not found"));
if (dto.getDisplayName() != null) {
String displayName = dto.getDisplayName().trim();
@@ -74,13 +76,13 @@ public class UserService {
public void delete(UUID userId) {
byte[] idBytes = UuidUtil.uuidToBin(userId);
if(!userRepository.existsById(idBytes))
throw new RuntimeException("User not found");
throw new NotFoundException("User not found");
userRepository.deleteById(idBytes);
}
public UserDto updateAvatar(UUID userId, ChangeAvatarRequest req) {
User user = userRepository.findById(UuidUtil.uuidToBin(userId))
.orElseThrow(() -> new RuntimeException("User not found"));
.orElseThrow(() -> new NotFoundException("User not found"));
user.setAvatar(req.avatar());
userRepository.save(user);
return UserMapper.toDto(user);
@@ -88,26 +90,26 @@ public class UserService {
public Byte getStatus(UUID userId) {
User user = userRepository.findById(UuidUtil.uuidToBin(userId))
.orElseThrow(() -> new RuntimeException("User not found"));;
.orElseThrow(() -> new NotFoundException("User not found"));;
return user.getGlobalStatus();
}
public void updateStatus(UUID userId, Byte status) {
User user = userRepository.findById(UuidUtil.uuidToBin(userId))
.orElseThrow(() -> new RuntimeException("User not found"));;
.orElseThrow(() -> new NotFoundException("User not found"));;
user.setGlobalStatus(status);
userRepository.save(user);
}
public Byte getRole(UUID userId) {
User user = userRepository.findById(UuidUtil.uuidToBin(userId))
.orElseThrow(() -> new RuntimeException("User not found"));;
.orElseThrow(() -> new NotFoundException("User not found"));;
return user.getGlobalRole();
}
public void updateRole(UUID userId, Byte role) {
User user = userRepository.findById(UuidUtil.uuidToBin(userId))
.orElseThrow(() -> new RuntimeException("User not found"));;
.orElseThrow(() -> new NotFoundException("User not found"));;
user.setGlobalRole(role);
userRepository.save(user);
}

View File

@@ -0,0 +1,21 @@
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
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,20 @@
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
jwt:
private-key-path: ${JWT_PRIVATE_KEY}
public-key-path: ${JWT_PUBLIC_KEY}

View File

@@ -1,43 +1,21 @@
server:
port: 8080
servlet:
context-path: /v2/core
spring:
application:
name: core-service
datasource:
url: jdbc:mariadb://localhost:3306/miarma_v2
username: admin
password: ositovito
driver-class-name: org.mariadb.jdbc.Driver
jpa:
open-in-view: false
hibernate:
ddl-auto: validate
properties:
hibernate:
format_sql: true
jdbc:
time_zone: UTC
jackson:
serialization:
indent-output: true
default-property-inclusion: non_null
time-zone: Europe/Madrid
logging:
level:
org.hibernate.SQL: DEBUG
org.hibernate.orm.jdbc.bind: TRACE
org.springframework.security: INFO
jwt:
private-key-path: /home/jomaa/.config/miarma-backend/private.pem
public-key-path: /home/jomaa/.config/miarma-backend/public.pem
expiration-ms: 3600000
management: