Changeset 3238007


Ignore:
Timestamp:
01/27/26 14:25:53 (6 months ago)
Author:
Filip Gavrilovski <filipgavrilovski28@…>
Branches:
main
Children:
16aed54
Parents:
b70fbeb
Message:

add custom exceptions and proper server side error handling

Files:
7 added
6 edited

Legend:

Unmodified
Added
Removed
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/controller/AuthController.java

    rb70fbeb r3238007  
    11package com.ukim.finki.develop.finkwave.controller;
    22
     3import java.io.IOException;
    34import java.util.Map;
    45
    5 import com.ukim.finki.develop.finkwave.config.AuthProperties;
    6 import org.springframework.http.HttpStatus;
    76import org.springframework.http.MediaType;
    87import org.springframework.http.ResponseEntity;
     
    109import org.springframework.security.core.context.SecurityContextHolder;
    1110import org.springframework.security.core.userdetails.UserDetails;
    12 import org.springframework.web.bind.annotation.*;
    13 import org.springframework.web.server.ResponseStatusException;
     11import org.springframework.web.bind.annotation.CookieValue;
     12import org.springframework.web.bind.annotation.GetMapping;
     13import org.springframework.web.bind.annotation.ModelAttribute;
     14import org.springframework.web.bind.annotation.PostMapping;
     15import org.springframework.web.bind.annotation.RequestBody;
     16import org.springframework.web.bind.annotation.RequestMapping;
     17import org.springframework.web.bind.annotation.RestController;
    1418
     19import com.ukim.finki.develop.finkwave.config.AuthProperties;
     20import com.ukim.finki.develop.finkwave.exceptions.InvalidTokenException;
     21import com.ukim.finki.develop.finkwave.exceptions.UnauthenticatedException;
    1522import com.ukim.finki.develop.finkwave.model.dto.AuthRequestDto;
    1623import com.ukim.finki.develop.finkwave.model.dto.LoginRequestDto;
     
    3138    public ResponseEntity<Map<String, Object>> register(
    3239            @ModelAttribute AuthRequestDto authRequestDto,
    33             HttpServletResponse httpServletResponse){
    34         try {
    35             UserResponseDto userResponseDto = authService.registerAndLogIn(httpServletResponse, authRequestDto);
    36             return ResponseEntity.ok(Map.of(
    37                     "user", userResponseDto,
    38                     "tokenExpiresIn", authProperties.getAccessTokenMaxAge()
    39             ));
    40         } catch (Exception e){
    41             return ResponseEntity
    42                     .status(HttpStatus.BAD_REQUEST)
    43                     .body(Map.of("error", e.getMessage()));
    44         }
     40            HttpServletResponse httpServletResponse) throws IOException {
     41        UserResponseDto userResponseDto = authService.registerAndLogIn(httpServletResponse, authRequestDto);
     42        return ResponseEntity.ok(Map.of(
     43                "user", userResponseDto,
     44                "tokenExpiresIn", authProperties.getAccessTokenMaxAge()
     45        ));
    4546    }
    4647
     
    4849    public ResponseEntity<Map<String, Object>> login(
    4950            @RequestBody LoginRequestDto loginRequestDto,
    50             HttpServletResponse httpServletResponse){
    51         try {
    52             UserResponseDto userResponseDto = authService.login(httpServletResponse, loginRequestDto);
    53             return ResponseEntity.ok(Map.of(
    54                     "user", userResponseDto,
    55                     "tokenExpiresIn", authProperties.getAccessTokenMaxAge()
    56                     )
    57             );
    58         } catch (Exception e){
    59             return ResponseEntity
    60                     .status(HttpStatus.BAD_REQUEST)
    61                     .body(Map.of("error", e.getMessage()));
    62         }
     51            HttpServletResponse httpServletResponse) {
     52        UserResponseDto userResponseDto = authService.login(httpServletResponse, loginRequestDto);
     53        return ResponseEntity.ok(Map.of(
     54                "user", userResponseDto,
     55                "tokenExpiresIn", authProperties.getAccessTokenMaxAge()
     56        ));
    6357    }
    64 
    6558
    6659    @PostMapping("/refresh")
    6760    public ResponseEntity<Map<String, Integer>> refresh(
    6861            HttpServletResponse httpServletResponse,
    69             @CookieValue(name = "refreshToken", required = false) String refreshToken){
    70         if (refreshToken == null){
    71             throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "No credentials");
     62            @CookieValue(name = "refreshToken", required = false) String refreshToken) {
     63        if (refreshToken == null) {
     64            throw new InvalidTokenException("No refresh token provided.");
    7265        }
    7366        authService.refreshAccessTokenAndAddCookie(httpServletResponse, refreshToken);
     
    8073    public ResponseEntity<Map<String, String>> logout(
    8174            HttpServletResponse httpServletResponse,
    82             @CookieValue(name = "refreshToken", required = false) String refreshToken){
     75            @CookieValue(name = "refreshToken", required = false) String refreshToken) {
    8376        authService.clearCookies(httpServletResponse, refreshToken);
    8477        SecurityContextHolder.clearContext();
     
    8780
    8881    @GetMapping("/user")
    89     public ResponseEntity<Map<String, Object>> getUser(){
     82    public ResponseEntity<Map<String, Object>> getUser() {
    9083        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    91         if (authentication == null || !authentication.isAuthenticated()){
    92             throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Not authenticated");
     84        if (authentication == null || !authentication.isAuthenticated()) {
     85            throw new UnauthenticatedException();
    9386        }
    9487        Object principal = authentication.getPrincipal();
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/controller/GlobalExceptionHandler.java

    rb70fbeb r3238007  
    11package com.ukim.finki.develop.finkwave.controller;
    22
     3import java.util.Map;
     4
     5import org.slf4j.Logger;
     6import org.slf4j.LoggerFactory;
    37import org.springframework.http.HttpStatus;
    48import org.springframework.http.ResponseEntity;
     9import org.springframework.security.core.userdetails.UsernameNotFoundException;
    510import org.springframework.web.bind.annotation.ExceptionHandler;
    611import org.springframework.web.bind.annotation.RestControllerAdvice;
     12import org.springframework.web.multipart.MaxUploadSizeExceededException;
     13import org.springframework.web.server.ResponseStatusException;
    714
    8 import java.util.HashMap;
    9 import java.util.Map;
     15import com.ukim.finki.develop.finkwave.exceptions.AuthException;
    1016
    1117@RestControllerAdvice
    1218public class GlobalExceptionHandler {
     19    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    1320
    14     @ExceptionHandler(RuntimeException.class)
    15     public ResponseEntity<Map<String, String>> handleRuntimeException(RuntimeException ex) {
    16         Map<String, String> errorMap = new HashMap<>();
    17         errorMap.put("error", ex.getMessage());
     21    @ExceptionHandler(AuthException.class)
     22    public ResponseEntity<Map<String, String>> handleAuthException(AuthException ex) {
     23        return ResponseEntity
     24                .status(ex.getStatus())
     25                .body(Map.of("error", ex.getMessage()));
     26    }
    1827
    19         return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorMap);
     28    @ExceptionHandler(UsernameNotFoundException.class)
     29    public ResponseEntity<Map<String, String>> handleUsernameNotFound(UsernameNotFoundException ex) {
     30        return ResponseEntity
     31                .status(HttpStatus.UNAUTHORIZED)
     32                .body(Map.of("error", "Invalid credentials."));
     33    }
     34
     35    @ExceptionHandler(ResponseStatusException.class)
     36    public ResponseEntity<Map<String, String>> handleResponseStatusException(ResponseStatusException ex) {
     37        logger.error("Response status:", ex);
     38        return ResponseEntity
     39                .status(ex.getStatusCode())
     40                .body(Map.of("error", ex.getReason() != null ? ex.getReason() : "An error occurred."));
     41    }
     42
     43    @ExceptionHandler(MaxUploadSizeExceededException.class)
     44    public ResponseEntity<Map<String, Object>> handleMaxSize(MaxUploadSizeExceededException ex) {
     45        logger.error("Max upload size error:", ex);
     46        return ResponseEntity
     47                .status(HttpStatus.PAYLOAD_TOO_LARGE)
     48                .body(Map.of(
     49                        "error", "File too large.",
     50                        "maxSize", "2MB"
     51                ));
     52    }
     53
     54    @ExceptionHandler(Exception.class)
     55    public ResponseEntity<Map<String, String>> handleGenericException(Exception ex) {
     56        logger.error("Unexpected error occurred:", ex);
     57        return ResponseEntity
     58                .status(HttpStatus.INTERNAL_SERVER_ERROR)
     59                .body(Map.of("error", "An unexpected error occurred."));
    2060    }
    2161}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/AuthService.java

    rb70fbeb r3238007  
    11package com.ukim.finki.develop.finkwave.service;
     2
     3import java.io.IOException;
     4import java.nio.file.Files;
     5import java.nio.file.Path;
     6import java.nio.file.Paths;
     7import java.util.UUID;
    28
    39import org.springframework.security.core.Authentication;
     
    511import org.springframework.security.crypto.password.PasswordEncoder;
    612import org.springframework.stereotype.Service;
     13import org.springframework.web.multipart.MultipartFile;
    714
    815import com.ukim.finki.develop.finkwave.config.AuthProperties;
    9 import com.ukim.finki.develop.finkwave.model.dto.AuthRequestDto;
    10 import com.ukim.finki.develop.finkwave.model.dto.LoginRequestDto;
    11 import com.ukim.finki.develop.finkwave.model.dto.UserResponseDto;
     16import com.ukim.finki.develop.finkwave.exceptions.InvalidCredentialsException;
     17import com.ukim.finki.develop.finkwave.exceptions.InvalidFileException;
     18import com.ukim.finki.develop.finkwave.exceptions.UnauthenticatedException;
     19import com.ukim.finki.develop.finkwave.exceptions.UserAlreadyExistsException;
     20import com.ukim.finki.develop.finkwave.exceptions.UserNotFoundException;
    1221import com.ukim.finki.develop.finkwave.model.NonAdminUser;
    1322import com.ukim.finki.develop.finkwave.model.RefreshToken;
    1423import com.ukim.finki.develop.finkwave.model.Role;
    1524import com.ukim.finki.develop.finkwave.model.User;
     25import com.ukim.finki.develop.finkwave.model.dto.AuthRequestDto;
     26import com.ukim.finki.develop.finkwave.model.dto.LoginRequestDto;
     27import com.ukim.finki.develop.finkwave.model.dto.UserResponseDto;
    1628import com.ukim.finki.develop.finkwave.repository.NonAdminUserRepository;
    1729import com.ukim.finki.develop.finkwave.repository.UserRepository;
     
    2133import jakarta.transaction.Transactional;
    2234import lombok.RequiredArgsConstructor;
    23 import org.springframework.web.multipart.MultipartFile;
    24 
    25 import java.io.IOException;
    26 import java.nio.file.Files;
    27 import java.nio.file.Path;
    28 import java.nio.file.Paths;
    29 import java.util.UUID;
    3035
    3136@Service
     
    3944    private final AuthProperties authProperties;
    4045
     46    private static final long MAX_FILE_SIZE = 2_000_000;
    4147
    4248    @Transactional
     
    4450    throws IOException {
    4551        if (userRepository.findByUsername(authRequestDto.username()).isPresent()){
    46             throw new RuntimeException("Error creating user.");
     52            throw new UserAlreadyExistsException();
    4753        }
    4854        User user = createNonAdminUser(authRequestDto);
     
    5258    public UserResponseDto login(HttpServletResponse response, LoginRequestDto loginRequestDto){
    5359        User user = userRepository.findByUsername(loginRequestDto.username())
    54                 .orElseThrow(() -> new RuntimeException("Invalid credentials."));
     60                .orElseThrow(InvalidCredentialsException::new);
    5561
    5662        if (!passwordEncoder.matches(loginRequestDto.password(), user.getPassword())){
    57             throw new RuntimeException("Invalid credentials.");
     63            throw new InvalidCredentialsException();
    5864        }
    5965
     
    7480    public UserResponseDto getUserDtoByUsername(String username){
    7581        User user = userRepository.findByUsername(username)
    76                 .orElseThrow(() -> new RuntimeException("Invalid credentials."));
     82                .orElseThrow(UserNotFoundException::new);
    7783
    7884        return userResponseFromUser(user);
     
    103109        Authentication authentication= SecurityContextHolder.getContext().getAuthentication();
    104110        if (authentication == null || !authentication.isAuthenticated()) {
    105             throw new RuntimeException("User not authenticated.");
     111            throw new UnauthenticatedException();
    106112        }
    107113
    108114        String username=authentication.getName();
    109115
    110 
    111116        return userRepository.findByUsername(username).map(User::getId)
    112                 .orElseThrow(()->new RuntimeException("User not found."));
     117                .orElseThrow(UserNotFoundException::new);
    113118    }
    114119
     
    124129
    125130        MultipartFile profilePhoto = authRequestDto.profilePhoto();
    126         if (profilePhoto != null) {
    127             if (profilePhoto.getContentType() != null && !profilePhoto.getContentType().startsWith("image/"))
    128                 throw new IllegalArgumentException("Invalid fyle type.");
     131        if (profilePhoto != null && !profilePhoto.isEmpty()) {
     132            String contentType = profilePhoto.getContentType();
     133            if (contentType == null || !contentType.startsWith("image/")) {
     134                throw InvalidFileException.invalidType();
     135            }
    129136
    130             if (profilePhoto.getSize() > 2_000_000) {
    131                 throw new IllegalArgumentException(("File too large."));
     137            if (profilePhoto.getSize() > MAX_FILE_SIZE) {
     138                throw InvalidFileException.tooLarge(MAX_FILE_SIZE);
    132139            }
    133140
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/RefreshTokenService.java

    rb70fbeb r3238007  
    11package com.ukim.finki.develop.finkwave.service;
    2 
    3 import com.ukim.finki.develop.finkwave.config.AuthProperties;
    4 import com.ukim.finki.develop.finkwave.model.RefreshToken;
    5 import com.ukim.finki.develop.finkwave.model.User;
    6 import com.ukim.finki.develop.finkwave.repository.RefreshTokenRepository;
    7 import lombok.RequiredArgsConstructor;
    8 import org.springframework.stereotype.Service;
    92
    103import java.time.Instant;
     
    125import java.util.Optional;
    136import java.util.UUID;
     7
     8import org.springframework.stereotype.Service;
     9
     10import com.ukim.finki.develop.finkwave.config.AuthProperties;
     11import com.ukim.finki.develop.finkwave.exceptions.InvalidTokenException;
     12import com.ukim.finki.develop.finkwave.model.RefreshToken;
     13import com.ukim.finki.develop.finkwave.model.User;
     14import com.ukim.finki.develop.finkwave.repository.RefreshTokenRepository;
     15
     16import lombok.RequiredArgsConstructor;
    1417
    1518@Service
     
    3235
    3336    public RefreshToken validateRefreshToken(String token){
    34         RefreshToken refreshToken = findByToken(token).orElseThrow(() -> new RuntimeException("Invalid token"));
    35         if (refreshToken.isRevoked() || refreshToken.getExpiresAt().isBefore(Instant.now())){
    36             throw new RuntimeException("Invalid refresh token");
     37        RefreshToken refreshToken = findByToken(token)
     38                .orElseThrow(() -> new InvalidTokenException("Invalid refresh token."));
     39        if (refreshToken.isRevoked()){
     40            throw new InvalidTokenException("Refresh token has been revoked.");
     41        }
     42        if (refreshToken.getExpiresAt().isBefore(Instant.now())){
     43            throw new InvalidTokenException("Refresh token has expired.");
    3744        }
    3845        return refreshToken;
  • frontend/src/pages/Login.tsx

    rb70fbeb r3238007  
    2828                                error.response?.data?.error ||
    2929                                "Login failed. Please check your credentials.";
    30                         setError(errorMessage);
    31                         toast.error(errorMessage);
     30                        setError(`Login failed: ${errorMessage}`);
    3231                }
    3332        };
  • frontend/src/pages/Register.tsx

    rb70fbeb r3238007  
    5858                        const errorMessage =
    5959                                error.response?.data?.error || "Registration failed. Please try again.";
    60                         setError(errorMessage);
    61                         toast.error(errorMessage);
     60                        setError(`Registration failed: ${errorMessage}`);
    6261                }
    6362        };
Note: See TracChangeset for help on using the changeset viewer.