unify error handling

This commit is contained in:
2026-04-17 13:31:00 -06:00
parent 46bdd5c3d7
commit 4162e34a5f
9 changed files with 85 additions and 98 deletions

View File

@@ -13,6 +13,9 @@ import com.petshop.backend.dto.auth.ResetPasswordResponse;
import com.petshop.backend.dto.auth.UserInfoResponse;
import com.petshop.backend.entity.StoreLocation;
import com.petshop.backend.entity.User;
import com.petshop.backend.exception.BusinessException;
import com.petshop.backend.exception.ConflictException;
import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.security.JwtUtil;
import com.petshop.backend.security.UserAuthCacheService;
@@ -74,7 +77,7 @@ public class AuthController {
}
@PostMapping("/register")
public ResponseEntity<?> register(@Valid @RequestBody RegisterRequest request) {
public ResponseEntity<RegisterResponse> register(@Valid @RequestBody RegisterRequest request) {
String username = trimToNull(request.getUsername());
String email = trimToNull(request.getEmail());
String firstName = trimToNull(request.getFirstName());
@@ -83,23 +86,17 @@ public class AuthController {
if (userRepository.findByUsername(username).isPresent()) {
log.warn("Registration rejected: username already exists ({})", username);
Map<String, String> error = new HashMap<>();
error.put("message", "Username already exists");
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
throw new ConflictException("Username already exists");
}
if (userRepository.findByEmail(email).isPresent()) {
log.warn("Registration rejected: email already exists");
Map<String, String> error = new HashMap<>();
error.put("message", "Email already exists");
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
throw new ConflictException("Email already exists");
}
if (phone != null && userRepository.findByPhone(phone).isPresent()) {
log.warn("Registration rejected: phone already exists");
Map<String, String> error = new HashMap<>();
error.put("message", "Phone already exists");
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
throw new ConflictException("Phone already exists");
}
User user = new User();
@@ -117,9 +114,7 @@ public class AuthController {
try {
savedUser = userRepository.save(user);
} catch (DataIntegrityViolationException e) {
Map<String, String> error = new HashMap<>();
error.put("message", "Username, email, or phone already exists");
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
throw new ConflictException("Username, email, or phone already exists");
}
emailService.sendWelcome(savedUser);
@@ -137,7 +132,7 @@ public class AuthController {
}
@PostMapping("/login")
public ResponseEntity<?> login(@Valid @RequestBody LoginRequest request) {
public ResponseEntity<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
@@ -159,21 +154,15 @@ public class AuthController {
} catch (BadCredentialsException e) {
log.warn("Login failed for username {}", request.getUsername());
Map<String, String> error = new HashMap<>();
error.put("message", "Invalid username or password");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error);
throw e;
} catch (InternalAuthenticationServiceException e) {
if (e.getCause() instanceof DisabledException disabledException) {
log.warn("Login denied for disabled user {}", request.getUsername());
Map<String, String> error = new HashMap<>();
error.put("message", disabledException.getMessage());
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error);
throw disabledException;
}
throw e;
} catch (DisabledException e) {
Map<String, String> error = new HashMap<>();
error.put("message", e.getMessage());
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error);
throw e;
}
}
@@ -196,7 +185,7 @@ public class AuthController {
@Transactional
@PutMapping("/me")
public ResponseEntity<?> updateProfile(@Valid @RequestBody ProfileUpdateRequest request) {
public ResponseEntity<UserInfoResponse> updateProfile(@Valid @RequestBody ProfileUpdateRequest request) {
Long userId = AuthenticationHelper.getAuthenticatedUserId();
User user = userRepository.findByIdForUpdate(userId)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
@@ -205,9 +194,7 @@ public class AuthController {
String username = trimToNull(request.getUsername());
if (username != null && !username.equals(user.getUsername())) {
if (userRepository.findByUsername(username).isPresent()) {
Map<String, String> error = new HashMap<>();
error.put("message", "Username already exists");
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
throw new ConflictException("Username already exists");
}
user.setUsername(username);
invalidateToken = true;
@@ -216,9 +203,7 @@ public class AuthController {
String email = trimToNull(request.getEmail());
if (email != null && !email.equals(user.getEmail())) {
if (userRepository.findByEmail(email).isPresent()) {
Map<String, String> error = new HashMap<>();
error.put("message", "Email already exists");
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
throw new ConflictException("Email already exists");
}
user.setEmail(email);
}
@@ -241,9 +226,7 @@ public class AuthController {
if (phone != null && userRepository.findByPhone(phone)
.filter(existing -> !existing.getId().equals(user.getId()))
.isPresent()) {
Map<String, String> error = new HashMap<>();
error.put("message", "Phone already exists");
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
throw new ConflictException("Phone already exists");
}
user.setPhone(phone);
}
@@ -262,9 +245,7 @@ public class AuthController {
try {
updatedUser = userRepository.save(user);
} catch (DataIntegrityViolationException e) {
Map<String, String> error = new HashMap<>();
error.put("message", "Username, email, or phone already exists");
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
throw new ConflictException("Username, email, or phone already exists");
}
userAuthCacheService.evict(updatedUser.getId());
return ResponseEntity.ok(toUserInfoResponse(updatedUser));
@@ -306,17 +287,6 @@ public class AuthController {
return trimToNull(PhoneUtils.normalize(trimToNull(value)));
}
private NameParts splitFullName(String value) {
String normalized = trimToNull(value);
if (normalized == null) {
throw new IllegalArgumentException("Full name is required");
}
String[] parts = normalized.split("\\s+", 2);
String firstName = parts[0];
String lastName = parts.length > 1 ? parts[1] : "";
return new NameParts(firstName, lastName, joinFullName(firstName, lastName));
}
private String joinFullName(String firstName, String lastName) {
String first = trimToNull(firstName);
String last = trimToNull(lastName);
@@ -329,30 +299,21 @@ public class AuthController {
return first + " " + last;
}
private record NameParts(String firstName, String lastName, String fullName) {
}
@PostMapping("/me/avatar")
public ResponseEntity<?> uploadAvatar(@RequestParam("avatar") MultipartFile file) {
public ResponseEntity<AvatarUploadResponse> uploadAvatar(@RequestParam("avatar") MultipartFile file) {
User user = getAuthenticatedUser();
if (file.isEmpty()) {
Map<String, String> error = new HashMap<>();
error.put("message", "Please select a file to upload");
return ResponseEntity.badRequest().body(error);
throw new BusinessException("Please select a file to upload");
}
if (file.getSize() > 5 * 1024 * 1024) {
Map<String, String> error = new HashMap<>();
error.put("message", "File size must not exceed 5MB");
return ResponseEntity.badRequest().body(error);
throw new BusinessException("File size must not exceed 5MB");
}
String contentType = file.getContentType();
if (contentType == null || (!contentType.equals("image/jpeg") && !contentType.equals("image/png") && !contentType.equals("image/gif"))) {
Map<String, String> error = new HashMap<>();
error.put("message", "Only JPG, PNG, and GIF images are allowed");
return ResponseEntity.badRequest().body(error);
throw new BusinessException("Only JPG, PNG, and GIF images are allowed");
}
try {
@@ -364,9 +325,7 @@ public class AuthController {
return ResponseEntity.ok(new AvatarUploadResponse(avatarStorageService.toOwnerAvatarUrl(user), "Avatar uploaded successfully"));
} catch (IOException e) {
Map<String, String> error = new HashMap<>();
error.put("message", "Failed to upload avatar: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
throw new BusinessException("Failed to upload avatar: " + e.getMessage());
}
}
@@ -375,9 +334,7 @@ public class AuthController {
User user = getAuthenticatedUser();
if (!avatarStorageService.hasAvatar(user)) {
Map<String, String> error = new HashMap<>();
error.put("message", "No avatar uploaded");
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
throw new ResourceNotFoundException("No avatar uploaded");
}
Map<String, String> response = new HashMap<>();

View File

@@ -0,0 +1,7 @@
package com.petshop.backend.exception;
public class ConflictException extends RuntimeException {
public ConflictException(String message) {
super(message);
}
}

View File

@@ -5,6 +5,8 @@ import jakarta.servlet.http.HttpServletRequest;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.core.PropertyReferenceException;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
@@ -33,6 +35,11 @@ public class GlobalExceptionHandler {
return buildErrorResponse(HttpStatus.BAD_REQUEST, ex.getMessage(), ex, request);
}
@ExceptionHandler(ConflictException.class)
public ResponseEntity<ApiErrorResponse> handleConflictException(ConflictException ex, HttpServletRequest request) {
return buildErrorResponse(HttpStatus.CONFLICT, ex.getMessage(), ex, request);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleValidationExceptions(MethodArgumentNotValidException ex, HttpServletRequest request) {
Map<String, String> errors = new HashMap<>();
@@ -54,6 +61,16 @@ public class GlobalExceptionHandler {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
@ExceptionHandler(BadCredentialsException.class)
public ResponseEntity<ApiErrorResponse> handleBadCredentials(BadCredentialsException ex, HttpServletRequest request) {
return buildErrorResponse(HttpStatus.UNAUTHORIZED, "Invalid username or password", ex, request);
}
@ExceptionHandler(DisabledException.class)
public ResponseEntity<ApiErrorResponse> handleDisabledException(DisabledException ex, HttpServletRequest request) {
return buildErrorResponse(HttpStatus.FORBIDDEN, ex.getMessage(), ex, request);
}
@ExceptionHandler(org.springframework.security.access.AccessDeniedException.class)
public ResponseEntity<ApiErrorResponse> handleAccessDeniedException(org.springframework.security.access.AccessDeniedException ex, HttpServletRequest request) {
return buildErrorResponse(HttpStatus.FORBIDDEN, ex.getMessage(), ex, request);

View File

@@ -8,6 +8,7 @@ import com.petshop.backend.entity.Pet;
import com.petshop.backend.entity.Sale;
import com.petshop.backend.entity.StoreLocation;
import com.petshop.backend.entity.User;
import com.petshop.backend.exception.BusinessException;
import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.event.AdoptionConfirmedEvent;
import com.petshop.backend.event.SaleReceiptEvent;
@@ -96,7 +97,7 @@ public class AdoptionService {
String adoptionStatus = normalizeAdoptionStatus(request.getAdoptionStatus());
validatePetAvailability(pet, null, null);
if (ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus) && request.getAdoptionDate() != null && request.getAdoptionDate().isAfter(LocalDate.now())) {
throw new IllegalArgumentException("Cannot mark a future-dated adoption as Completed");
throw new BusinessException("Cannot mark a future-dated adoption as Completed");
}
Adoption adoption = new Adoption();
@@ -139,7 +140,7 @@ public class AdoptionService {
Long currentPetId = adoption.getPet() != null ? adoption.getPet().getPetId() : null;
validatePetAvailability(pet, adoption.getAdoptionId(), currentPetId);
if (ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus) && request.getAdoptionDate() != null && request.getAdoptionDate().isAfter(LocalDate.now())) {
throw new IllegalArgumentException("Cannot mark a future-dated adoption as Completed");
throw new BusinessException("Cannot mark a future-dated adoption as Completed");
}
adoption.setPet(pet);
@@ -168,7 +169,7 @@ public class AdoptionService {
// Verify the pet is actually located at the claimed store
if (pet.getStore() == null || !pet.getStore().getStoreId().equals(sourceStoreId)) {
throw new IllegalArgumentException("The specified pet is not located at the selected store.");
throw new BusinessException("The specified pet is not located at the selected store.");
}
// Verify the pet is available for adoption
@@ -203,7 +204,7 @@ public class AdoptionService {
}
if (!ADOPTION_STATUS_PENDING.equalsIgnoreCase(adoption.getAdoptionStatus())) {
throw new IllegalArgumentException("Only pending adoptions can be cancelled");
throw new BusinessException("Only pending adoptions can be cancelled");
}
adoption.setAdoptionStatus(ADOPTION_STATUS_CANCELLED);
@@ -280,7 +281,7 @@ public class AdoptionService {
User employee = userRepository.findById(requestedEmployeeId)
.orElseThrow(() -> new ResourceNotFoundException("Employee not found with id: " + requestedEmployeeId));
if (!isAssignableUser(employee)) {
throw new IllegalArgumentException("Selected employee is not assignable for adoption work");
throw new BusinessException("Selected employee is not assignable for adoption work");
}
return employee;
}
@@ -295,7 +296,7 @@ public class AdoptionService {
private String normalizeAdoptionStatus(String adoptionStatus) {
if (adoptionStatus == null) {
throw new IllegalArgumentException("Adoption status is required");
throw new BusinessException("Adoption status is required");
}
String trimmedStatus = adoptionStatus.trim();
if (ADOPTION_STATUS_PENDING.equalsIgnoreCase(trimmedStatus)) {
@@ -310,7 +311,7 @@ public class AdoptionService {
if (ADOPTION_STATUS_MISSED.equalsIgnoreCase(trimmedStatus)) {
return ADOPTION_STATUS_MISSED;
}
throw new IllegalArgumentException("Adoption status must be Pending, Completed, Cancelled, or Missed");
throw new BusinessException("Adoption status must be Pending, Completed, Cancelled, or Missed");
}
private void validatePetAvailability(Pet pet, Long adoptionId, Long currentPetId) {
@@ -319,11 +320,11 @@ public class AdoptionService {
? adoptionRepository.existsByPet_IdAndAdoptionStatusIgnoreCase(pet.getPetId(), ADOPTION_STATUS_COMPLETED)
: adoptionRepository.existsByPet_IdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(pet.getPetId(), ADOPTION_STATUS_COMPLETED, adoptionId);
if (adoptedElsewhere) {
throw new IllegalArgumentException("Selected pet has already been adopted");
throw new BusinessException("Selected pet has already been adopted");
}
if (!samePetAsCurrentAdoption && !PET_STATUS_AVAILABLE.equalsIgnoreCase(pet.getPetStatus())) {
throw new IllegalArgumentException("Selected pet is not available for adoption");
throw new BusinessException("Selected pet is not available for adoption");
}
}

View File

@@ -116,14 +116,14 @@ public class AppointmentService {
// Customers must supply a pet that is Adopted and owned by them
if (User.Role.CUSTOMER.equals(authenticatedUser.getRole())) {
if (pet == null) {
throw new IllegalArgumentException("A pet must be selected for your appointment");
throw new BusinessException("A pet must be selected for your appointment");
}
if (pet.getOwner() == null || !pet.getOwner().getId().equals(authenticatedUser.getId())) {
throw new IllegalArgumentException("The selected pet does not belong to your account");
throw new BusinessException("The selected pet does not belong to your account");
}
String petStatus = pet.getPetStatus();
if (!"Owned".equalsIgnoreCase(petStatus) && !"Adopted".equalsIgnoreCase(petStatus)) {
throw new IllegalArgumentException("Only your own pets can be booked for appointments");
throw new BusinessException("Only your own pets can be booked for appointments");
}
}
@@ -198,7 +198,7 @@ public class AppointmentService {
String status = appointment.getAppointmentStatus();
if (!"Booked".equalsIgnoreCase(status) && !"Scheduled".equalsIgnoreCase(status)) {
throw new IllegalArgumentException("Only booked or scheduled appointments can be cancelled");
throw new BusinessException("Only booked or scheduled appointments can be cancelled");
}
appointment.setAppointmentStatus("Cancelled");
@@ -311,7 +311,7 @@ public class AppointmentService {
if ("Booked".equalsIgnoreCase(request.getAppointmentStatus())) {
LocalDateTime appointmentDateTime = LocalDateTime.of(request.getAppointmentDate(), request.getAppointmentTime());
if (appointmentDateTime.isBefore(LocalDateTime.now())) {
throw new IllegalArgumentException("Booked appointments must be scheduled in the future");
throw new BusinessException("Booked appointments must be scheduled in the future");
}
}
}
@@ -363,7 +363,7 @@ public class AppointmentService {
boolean assignedToStore = assignableUsers.stream()
.anyMatch(u -> u.getId().equals(requestedEmployeeId));
if (!assignedToStore) {
throw new IllegalArgumentException("Selected employee is not assignable for the selected store");
throw new BusinessException("Selected employee is not assignable for the selected store");
}
return employee;
}
@@ -377,7 +377,7 @@ public class AppointmentService {
List<Appointment> existingAppointments = appointmentRepository
.findByEmployeeIdAndAppointmentDate(employee.getId(), date);
if (!isSlotAvailable(existingAppointments, service, time, appointmentIdToIgnore)) {
throw new IllegalArgumentException("The selected employee is already booked for this time slot");
throw new BusinessException("The selected employee is already booked for this time slot");
}
}
@@ -401,7 +401,7 @@ public class AppointmentService {
List<Appointment> existingAppointments = appointmentRepository
.findByPetIdAndAppointmentDate(pet.getPetId(), date);
if (!isSlotAvailable(existingAppointments, service, time, appointmentIdToIgnore)) {
throw new IllegalArgumentException("This pet already has an appointment during this time slot");
throw new BusinessException("This pet already has an appointment during this time slot");
}
}

View File

@@ -4,6 +4,7 @@ import com.petshop.backend.dto.common.BulkDeleteRequest;
import com.petshop.backend.dto.common.CouponRequest;
import com.petshop.backend.dto.common.CouponResponse;
import com.petshop.backend.entity.Coupon;
import com.petshop.backend.exception.BusinessException;
import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.repository.CouponRepository;
import org.springframework.data.domain.Page;
@@ -39,7 +40,7 @@ public class CouponService {
@Transactional
public CouponResponse createCoupon(CouponRequest request) {
if (couponRepository.findByCouponCode(request.getCouponCode()).isPresent()) {
throw new IllegalArgumentException("Coupon code already exists: " + request.getCouponCode());
throw new BusinessException("Coupon code already exists: " + request.getCouponCode());
}
Coupon coupon = new Coupon();
@@ -55,7 +56,7 @@ public class CouponService {
couponRepository.findByCouponCode(request.getCouponCode()).ifPresent(existing -> {
if (!existing.getCouponId().equals(id)) {
throw new IllegalArgumentException("Coupon code already exists: " + request.getCouponCode());
throw new BusinessException("Coupon code already exists: " + request.getCouponCode());
}
});

View File

@@ -9,6 +9,7 @@ import com.petshop.backend.entity.Adoption;
import com.petshop.backend.entity.Pet;
import com.petshop.backend.entity.StoreLocation;
import com.petshop.backend.entity.User;
import com.petshop.backend.exception.BusinessException;
import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.security.AppPrincipal;
import com.petshop.backend.repository.AdoptionRepository;
@@ -125,7 +126,7 @@ public class PetService {
boolean hasBooked = linkedAppointments.stream()
.anyMatch(a -> "Booked".equalsIgnoreCase(a.getAppointmentStatus()));
if (hasBooked) {
throw new IllegalArgumentException(
throw new BusinessException(
"Your pet has a booked appointment. Please cancel the appointment before removing your pet from our database.");
}
// Nullify the pet reference on non-booked appointments to avoid FK constraint violations
@@ -280,18 +281,18 @@ public class PetService {
private void validateImageFile(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("Please select an image to upload");
throw new BusinessException("Please select an image to upload");
}
if (file.getSize() > 5 * 1024 * 1024) {
throw new IllegalArgumentException("Image file size must be less than 5MB");
throw new BusinessException("Image file size must be less than 5MB");
}
String contentType = file.getContentType();
if (contentType == null) {
throw new IllegalArgumentException("Only JPG, PNG, and GIF images are allowed");
throw new BusinessException("Only JPG, PNG, and GIF images are allowed");
}
String normalized = contentType.toLowerCase(Locale.ROOT);
if (!normalized.equals("image/jpeg") && !normalized.equals("image/png") && !normalized.equals("image/gif")) {
throw new IllegalArgumentException("Only JPG, PNG, and GIF images are allowed");
throw new BusinessException("Only JPG, PNG, and GIF images are allowed");
}
}

View File

@@ -135,18 +135,18 @@ public class ProductService {
private void validateImageFile(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("Please select an image to upload");
throw new BusinessException("Please select an image to upload");
}
if (file.getSize() > 5 * 1024 * 1024) {
throw new IllegalArgumentException("Image file size must be less than 5MB");
throw new BusinessException("Image file size must be less than 5MB");
}
String contentType = file.getContentType();
if (contentType == null) {
throw new IllegalArgumentException("Only JPG, PNG, and GIF images are allowed");
throw new BusinessException("Only JPG, PNG, and GIF images are allowed");
}
String normalized = contentType.toLowerCase(Locale.ROOT);
if (!normalized.equals("image/jpeg") && !normalized.equals("image/png") && !normalized.equals("image/gif")) {
throw new IllegalArgumentException("Only JPG, PNG, and GIF images are allowed");
throw new BusinessException("Only JPG, PNG, and GIF images are allowed");
}
}

View File

@@ -3,8 +3,11 @@ package com.petshop.backend.util;
import com.petshop.backend.entity.User;
import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.security.AppPrincipal;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
@Component
@@ -13,7 +16,7 @@ public class AuthenticationHelper {
public static Authentication getAuthentication() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
throw new RuntimeException("No authenticated user found");
throw new AuthenticationCredentialsNotFoundException("No authenticated user found");
}
return authentication;
}
@@ -23,7 +26,7 @@ public class AuthenticationHelper {
if (principal instanceof AppPrincipal appPrincipal) {
return appPrincipal;
}
throw new RuntimeException("Authenticated principal is not supported");
throw new AuthenticationServiceException("Authenticated principal is not supported");
}
public static Long getAuthenticatedUserId() {
@@ -36,11 +39,11 @@ public class AuthenticationHelper {
if (principal instanceof AppPrincipal appPrincipal) {
return userRepository.findById(appPrincipal.getUserId())
.orElseThrow(() -> new RuntimeException("User not found: " + appPrincipal.getUserId()));
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + appPrincipal.getUserId()));
}
String username = authentication.getName();
return userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("User not found: " + username));
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
}
}