Changes in / [c1d2f07:77e572b]


Ignore:
Files:
7 deleted
12 edited

Legend:

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

    rc1d2f07 r77e572b  
    11package com.ukim.finki.develop.finkwave.controller;
    22
    3 import java.io.IOException;
    43import java.util.Map;
    54
     5import com.ukim.finki.develop.finkwave.config.AuthProperties;
     6import org.springframework.http.HttpStatus;
    67import org.springframework.http.MediaType;
    78import org.springframework.http.ResponseEntity;
     
    910import org.springframework.security.core.context.SecurityContextHolder;
    1011import org.springframework.security.core.userdetails.UserDetails;
    11 import org.springframework.web.bind.annotation.CookieValue;
    12 import org.springframework.web.bind.annotation.GetMapping;
    13 import org.springframework.web.bind.annotation.ModelAttribute;
    14 import org.springframework.web.bind.annotation.PostMapping;
    15 import org.springframework.web.bind.annotation.RequestBody;
    16 import org.springframework.web.bind.annotation.RequestMapping;
    17 import org.springframework.web.bind.annotation.RestController;
     12import org.springframework.web.bind.annotation.*;
     13import org.springframework.web.server.ResponseStatusException;
    1814
    19 import com.ukim.finki.develop.finkwave.config.AuthProperties;
    20 import com.ukim.finki.develop.finkwave.exceptions.InvalidTokenException;
    21 import com.ukim.finki.develop.finkwave.exceptions.UnauthenticatedException;
    2215import com.ukim.finki.develop.finkwave.model.dto.AuthRequestDto;
    2316import com.ukim.finki.develop.finkwave.model.dto.LoginRequestDto;
     
    3831    public ResponseEntity<Map<String, Object>> register(
    3932            @ModelAttribute AuthRequestDto authRequestDto,
    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         ));
     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        }
    4645    }
    4746
     
    4948    public ResponseEntity<Map<String, Object>> login(
    5049            @RequestBody LoginRequestDto loginRequestDto,
    51             HttpServletResponse httpServletResponse) {
    52         UserResponseDto userResponseDto = authService.login(httpServletResponse, loginRequestDto);
    53         return ResponseEntity.ok(Map.of(
    54                 "user", userResponseDto,
    55                 "tokenExpiresIn", authProperties.getAccessTokenMaxAge()
    56         ));
     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        }
    5763    }
     64
    5865
    5966    @PostMapping("/refresh")
    6067    public ResponseEntity<Map<String, Integer>> refresh(
    6168            HttpServletResponse httpServletResponse,
    62             @CookieValue(name = "refreshToken", required = false) String refreshToken) {
    63         if (refreshToken == null) {
    64             throw new InvalidTokenException("No refresh token provided.");
     69            @CookieValue(name = "refreshToken", required = false) String refreshToken){
     70        if (refreshToken == null){
     71            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "No credentials");
    6572        }
    6673        authService.refreshAccessTokenAndAddCookie(httpServletResponse, refreshToken);
     
    7380    public ResponseEntity<Map<String, String>> logout(
    7481            HttpServletResponse httpServletResponse,
    75             @CookieValue(name = "refreshToken", required = false) String refreshToken) {
     82            @CookieValue(name = "refreshToken", required = false) String refreshToken){
    7683        authService.clearCookies(httpServletResponse, refreshToken);
    7784        SecurityContextHolder.clearContext();
     
    8087
    8188    @GetMapping("/user")
    82     public ResponseEntity<Map<String, Object>> getUser() {
     89    public ResponseEntity<Map<String, Object>> getUser(){
    8390        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    84         if (authentication == null || !authentication.isAuthenticated()) {
    85             throw new UnauthenticatedException();
     91        if (authentication == null || !authentication.isAuthenticated()){
     92            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Not authenticated");
    8693        }
    8794        Object principal = authentication.getPrincipal();
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/controller/GlobalExceptionHandler.java

    rc1d2f07 r77e572b  
    11package com.ukim.finki.develop.finkwave.controller;
    22
    3 import java.util.Map;
    4 
    5 import org.slf4j.Logger;
    6 import org.slf4j.LoggerFactory;
    73import org.springframework.http.HttpStatus;
    84import org.springframework.http.ResponseEntity;
    9 import org.springframework.security.core.userdetails.UsernameNotFoundException;
    105import org.springframework.web.bind.annotation.ExceptionHandler;
    116import org.springframework.web.bind.annotation.RestControllerAdvice;
    12 import org.springframework.web.multipart.MaxUploadSizeExceededException;
    13 import org.springframework.web.server.ResponseStatusException;
    147
    15 import com.ukim.finki.develop.finkwave.exceptions.AuthException;
     8import java.util.HashMap;
     9import java.util.Map;
    1610
    1711@RestControllerAdvice
    1812public class GlobalExceptionHandler {
    19     private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    2013
    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     }
     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());
    2718
    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 error: {}", ex.getMessage());
    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.getMessage());
    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.getMessage());
    57         return ResponseEntity
    58                 .status(HttpStatus.INTERNAL_SERVER_ERROR)
    59                 .body(Map.of("error", "An unexpected error occurred."));
     19        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorMap);
    6020    }
    6121}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/model/RefreshToken.java

    rc1d2f07 r77e572b  
    99@Entity
    1010@Getter @Setter
    11 @Table(name = "refresh_token")
    1211public class RefreshToken {
    1312    @Id
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/AuthService.java

    rc1d2f07 r77e572b  
    11package com.ukim.finki.develop.finkwave.service;
    2 
    3 import java.io.IOException;
    4 import java.nio.file.Files;
    5 import java.nio.file.Path;
    6 import java.nio.file.Paths;
    7 import java.util.UUID;
    82
    93import org.springframework.security.core.Authentication;
     
    115import org.springframework.security.crypto.password.PasswordEncoder;
    126import org.springframework.stereotype.Service;
    13 import org.springframework.web.multipart.MultipartFile;
    147
    158import com.ukim.finki.develop.finkwave.config.AuthProperties;
    16 import com.ukim.finki.develop.finkwave.exceptions.InvalidCredentialsException;
    17 import com.ukim.finki.develop.finkwave.exceptions.InvalidFileException;
    18 import com.ukim.finki.develop.finkwave.exceptions.UnauthenticatedException;
    19 import com.ukim.finki.develop.finkwave.exceptions.UserAlreadyExistsException;
    20 import com.ukim.finki.develop.finkwave.exceptions.UserNotFoundException;
     9import com.ukim.finki.develop.finkwave.model.dto.AuthRequestDto;
     10import com.ukim.finki.develop.finkwave.model.dto.LoginRequestDto;
     11import com.ukim.finki.develop.finkwave.model.dto.UserResponseDto;
    2112import com.ukim.finki.develop.finkwave.model.NonAdminUser;
    2213import com.ukim.finki.develop.finkwave.model.RefreshToken;
    2314import com.ukim.finki.develop.finkwave.model.Role;
    2415import com.ukim.finki.develop.finkwave.model.User;
    25 import com.ukim.finki.develop.finkwave.model.dto.AuthRequestDto;
    26 import com.ukim.finki.develop.finkwave.model.dto.LoginRequestDto;
    27 import com.ukim.finki.develop.finkwave.model.dto.UserResponseDto;
    2816import com.ukim.finki.develop.finkwave.repository.NonAdminUserRepository;
    2917import com.ukim.finki.develop.finkwave.repository.UserRepository;
     
    3321import jakarta.transaction.Transactional;
    3422import lombok.RequiredArgsConstructor;
     23import org.springframework.web.multipart.MultipartFile;
     24
     25import java.io.IOException;
     26import java.nio.file.Files;
     27import java.nio.file.Path;
     28import java.nio.file.Paths;
     29import java.util.UUID;
    3530
    3631@Service
     
    4439    private final AuthProperties authProperties;
    4540
    46     private static final long MAX_FILE_SIZE = 2_000_000;
    4741
    4842    @Transactional
     
    5044    throws IOException {
    5145        if (userRepository.findByUsername(authRequestDto.username()).isPresent()){
    52             throw new UserAlreadyExistsException();
     46            throw new RuntimeException("User already exists");
    5347        }
    5448        User user = createNonAdminUser(authRequestDto);
     
    5852    public UserResponseDto login(HttpServletResponse response, LoginRequestDto loginRequestDto){
    5953        User user = userRepository.findByUsername(loginRequestDto.username())
    60                 .orElseThrow(InvalidCredentialsException::new);
     54                .orElseThrow(() -> new RuntimeException("Invalid credentials"));
    6155
    6256        if (!passwordEncoder.matches(loginRequestDto.password(), user.getPassword())){
    63             throw new InvalidCredentialsException();
     57            throw new RuntimeException("Invalid credentials");
    6458        }
    6559
     
    8074    public UserResponseDto getUserDtoByUsername(String username){
    8175        User user = userRepository.findByUsername(username)
    82                 .orElseThrow(UserNotFoundException::new);
     76                .orElseThrow(() -> new RuntimeException("Invalid credentials"));
    8377
    8478        return userResponseFromUser(user);
     
    109103        Authentication authentication= SecurityContextHolder.getContext().getAuthentication();
    110104        if (authentication == null || !authentication.isAuthenticated()) {
    111             throw new UnauthenticatedException();
     105            throw new RuntimeException("User not authenticated");
    112106        }
    113107
    114108        String username=authentication.getName();
    115109
     110
    116111        return userRepository.findByUsername(username).map(User::getId)
    117                 .orElseThrow(UserNotFoundException::new);
     112                .orElseThrow(()->new RuntimeException("User not found"));
    118113    }
    119114
     
    129124
    130125        MultipartFile profilePhoto = authRequestDto.profilePhoto();
    131         if (profilePhoto != null && !profilePhoto.isEmpty()) {
    132             String contentType = profilePhoto.getContentType();
    133             if (contentType == null || !contentType.startsWith("image/")) {
    134                 throw InvalidFileException.invalidType();
    135             }
     126        if (profilePhoto != null) {
     127            if (profilePhoto.getContentType() != null && !profilePhoto.getContentType().startsWith("image/"))
     128                throw new IllegalArgumentException("Invalid fyle type");
    136129
    137             if (profilePhoto.getSize() > MAX_FILE_SIZE) {
    138                 throw InvalidFileException.tooLarge(MAX_FILE_SIZE);
     130            if (profilePhoto.getSize() > 5_000_000) {
     131                throw new IllegalArgumentException(("File too large."));
    139132            }
    140133
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/RefreshTokenService.java

    rc1d2f07 r77e572b  
    11package com.ukim.finki.develop.finkwave.service;
     2
     3import com.ukim.finki.develop.finkwave.config.AuthProperties;
     4import com.ukim.finki.develop.finkwave.model.RefreshToken;
     5import com.ukim.finki.develop.finkwave.model.User;
     6import com.ukim.finki.develop.finkwave.repository.RefreshTokenRepository;
     7import lombok.RequiredArgsConstructor;
     8import org.springframework.stereotype.Service;
    29
    310import java.time.Instant;
     
    512import java.util.Optional;
    613import java.util.UUID;
    7 
    8 import org.springframework.stereotype.Service;
    9 
    10 import com.ukim.finki.develop.finkwave.config.AuthProperties;
    11 import com.ukim.finki.develop.finkwave.exceptions.InvalidTokenException;
    12 import com.ukim.finki.develop.finkwave.model.RefreshToken;
    13 import com.ukim.finki.develop.finkwave.model.User;
    14 import com.ukim.finki.develop.finkwave.repository.RefreshTokenRepository;
    15 
    16 import lombok.RequiredArgsConstructor;
    1714
    1815@Service
     
    3532
    3633    public RefreshToken validateRefreshToken(String 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.");
     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");
    4437        }
    4538        return refreshToken;
  • frontend/package-lock.json

    rc1d2f07 r77e572b  
    1515                                "react-dom": "^19.2.0",
    1616                                "react-router-dom": "^7.13.0",
    17                                 "react-toastify": "^11.0.5",
    1817                                "tailwindcss": "^4.1.18"
    1918                        },
     
    21532152                        "funding": {
    21542153                                "url": "https://github.com/chalk/chalk?sponsor=1"
    2155                         }
    2156                 },
    2157                 "node_modules/clsx": {
    2158                         "version": "2.1.1",
    2159                         "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
    2160                         "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
    2161                         "license": "MIT",
    2162                         "engines": {
    2163                                 "node": ">=6"
    21642154                        }
    21652155                },
     
    36933683                        }
    36943684                },
    3695                 "node_modules/react-toastify": {
    3696                         "version": "11.0.5",
    3697                         "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz",
    3698                         "integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==",
    3699                         "license": "MIT",
    3700                         "dependencies": {
    3701                                 "clsx": "^2.1.1"
    3702                         },
    3703                         "peerDependencies": {
    3704                                 "react": "^18 || ^19",
    3705                                 "react-dom": "^18 || ^19"
    3706                         }
    3707                 },
    37083685                "node_modules/resolve-from": {
    37093686                        "version": "4.0.0",
  • frontend/package.json

    rc1d2f07 r77e572b  
    1717                "react-dom": "^19.2.0",
    1818                "react-router-dom": "^7.13.0",
    19                 "react-toastify": "^11.0.5",
    2019                "tailwindcss": "^4.1.18"
    2120        },
  • frontend/src/App.tsx

    rc1d2f07 r77e572b  
    11import { createBrowserRouter, Outlet, RouterProvider } from "react-router-dom";
    2 import { ToastContainer } from "react-toastify";
    3 import "react-toastify/dist/ReactToastify.css";
    42import AllUsers from "./pages/AllUsers";
    53import LandingPage from "./pages/LandingPage";
     
    1311                <div className="flex flex-col min-h-screen">
    1412                        <Nav />
    15                         <ToastContainer
    16                                 position="top-right"
    17                                 autoClose={2000}
    18                                 hideProgressBar={false}
    19                                 newestOnTop={false}
    20                                 closeOnClick
    21                                 rtl={false}
    22                                 pauseOnFocusLoss
    23                                 draggable
    24                                 pauseOnHover
    25                                 theme="light"
    26                         />
    2713                        <main className="grow">
    2814                                <Outlet />
  • frontend/src/api/axiosInstance.ts

    rc1d2f07 r77e572b  
    7070                        [401, 403].includes(error.response?.status) &&
    7171                        !originalConfig._retry &&
    72                         originalConfig.url !== "/auth/refresh" &&
    73                         originalConfig.url !== "/auth/login" &&
    74                         originalConfig.url !== "/auth/register"
     72                        originalConfig.url !== "/auth/refresh"
    7573                ) {
    7674                        originalConfig._retry = true;
  • frontend/src/pages/Login.tsx

    rc1d2f07 r77e572b  
    11import { useState } from "react";
    22import { useNavigate } from "react-router-dom";
    3 import { toast } from "react-toastify";
    43import axiosInstance, { scheduleTokenRefresh } from "../api/axiosInstance";
    54import { useAuth } from "../context/authContext";
     
    2322                        setUser(response.data.user);
    2423                        navigate("/");
    25                         toast.success("Login successful!");
    26                 } catch (error: any) {
    27                         const errorMessage =
    28                                 error.response?.data?.error ||
    29                                 "Login failed. Please check your credentials.";
    30                         setError(`Login failed: ${errorMessage}`);
     24                } catch (error) {
     25                        setError("Login failed. Please check your credentials.");
    3126                }
    3227        };
  • frontend/src/pages/Nav.tsx

    rc1d2f07 r77e572b  
    11import { Link } from "react-router-dom";
    2 import { toast } from "react-toastify";
    32import axiosInstance, { baseURL } from "../api/axiosInstance";
    43import Logo from "../assets/logo-finkwave.png";
     
    1312                        await axiosInstance.post("/auth/logout");
    1413                        setUser(undefined);
    15                         toast.success("Logout successful!");
    1614                } catch (error) {
    1715                        console.error("Logout failed:", error);
    18                         toast.error("Logout failed!");
    1916                }
    2017        };
  • frontend/src/pages/Register.tsx

    rc1d2f07 r77e572b  
    11import { useEffect, useState } from "react";
    22import { useNavigate } from "react-router";
    3 import { toast } from "react-toastify";
    43import axiosInstance, { scheduleTokenRefresh } from "../api/axiosInstance";
    54import { useAuth } from "../context/authContext";
     
    1615                null,
    1716        );
    18         const [error, setError] = useState<string | null>(null);
    1917
    2018        const navigate = useNavigate();
     
    2826        }, [previewProfilePhoto]);
    2927
    30         const handleRemoveFile = () => {
    31                 if (previewProfilePhoto) {
    32                         URL.revokeObjectURL(previewProfilePhoto);
    33                 }
    34                 setProfilePhotoFile(null);
    35                 setPreviewProfilePhoto(null);
    36         };
    37 
    3828        const handleRegister = async (e: React.FormEvent) => {
    3929                e.preventDefault();
     30                // todo: add proper error handling
    4031                if (profilePhotoFile && profilePhotoFile.size > 5 * 1024 * 1024) {
    4132                        alert("Max file size is 5MB");
    42                         return;
    43                 }
    44 
    45                 if (username === "" || password === "" || fullname === "" || email === "") {
    46                         setError("Please fill in all required fields.");
    4733                        return;
    4834                }
     
    6753                        setUser(response.data.user);
    6854                        navigate("/");
    69                         toast.success("Registration successful!");
    70                 } catch (error: any) {
    71                         const errorMessage = error.response?.data?.error || "Please try again.";
    72                         setError(`Registration failed: ${errorMessage}`);
     55                } catch (error) {
     56                        console.error("Registration failed:", error);
    7357                }
    7458        };
     
    7761                <div className="flex flex-col items-center justify-center min-h-[90vh] bg-gray-100">
    7862                        <h2 className="text-2xl mb-4">Register</h2>
    79                         <form className="bg-white p-6 rounded shadow-md w-full max-w-md">
    80                                 {error && <p className="text-red-500 mb-4">{error}</p>}
     63                        <form className="bg-white p-6 rounded shadow-md w-80">
    8164                                <div className="mb-4">
    8265                                        <label className="block text-gray-700 mb-2" htmlFor="username">
     
    132115                                                <label
    133116                                                        htmlFor="profilePhoto"
    134                                                         className="px-4 py-2 bg-blue-500 text-white rounded cursor-pointer hover:bg-blue-600 transition-colors text-sm font-medium text-center"
     117                                                        className="px-4 py-2 bg-blue-500 text-white rounded cursor-pointer hover:bg-blue-600 transition-colors text-sm font-medium"
    135118                                                >
    136119                                                        Choose File
    137120                                                </label>
    138                                                 <span className="text-gray-600 text-sm flex-1">
     121                                                <span className="text-gray-600 text-sm">
    139122                                                        {profilePhotoFile ? profilePhotoFile.name : "No file chosen"}
    140123                                                </span>
    141                                                 {profilePhotoFile && (
    142                                                         <button
    143                                                                 type="button"
    144                                                                 onClick={handleRemoveFile}
    145                                                                 className="px-3 py-2 bg-red-500 text-white rounded hover:bg-red-600 transition-colors text-sm font-medium"
    146                                                         >
    147                                                                 Remove
    148                                                         </button>
    149                                                 )}
    150124                                        </div>
    151125                                        <input
Note: See TracChangeset for help on using the changeset viewer.