Changeset c1d2f07


Ignore:
Timestamp:
01/27/26 15:56:00 (6 months ago)
Author:
Dimitar Arsov <dimitararsov04@…>
Branches:
main
Children:
6de2873
Parents:
77e572b (diff), 16aed54 (diff)
Note: this is a merge changeset, the changes displayed below correspond to the merge itself.
Use the (diff) links above to see all the changes relative to each parent.
Message:

Merge branch 'main' into follow-user

Files:
7 added
12 edited

Legend:

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

    r77e572b rc1d2f07  
    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

    r77e572b rc1d2f07  
    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 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."));
    2060    }
    2161}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/model/RefreshToken.java

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

    r77e572b rc1d2f07  
    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("User already exists");
     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() > 5_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

    r77e572b rc1d2f07  
    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/package-lock.json

    r77e572b rc1d2f07  
    1515                                "react-dom": "^19.2.0",
    1616                                "react-router-dom": "^7.13.0",
     17                                "react-toastify": "^11.0.5",
    1718                                "tailwindcss": "^4.1.18"
    1819                        },
     
    21522153                        "funding": {
    21532154                                "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"
    21542164                        }
    21552165                },
     
    36833693                        }
    36843694                },
     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                },
    36853708                "node_modules/resolve-from": {
    36863709                        "version": "4.0.0",
  • frontend/package.json

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

    r77e572b rc1d2f07  
    11import { createBrowserRouter, Outlet, RouterProvider } from "react-router-dom";
     2import { ToastContainer } from "react-toastify";
     3import "react-toastify/dist/ReactToastify.css";
    24import AllUsers from "./pages/AllUsers";
    35import LandingPage from "./pages/LandingPage";
     
    1113                <div className="flex flex-col min-h-screen">
    1214                        <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                        />
    1327                        <main className="grow">
    1428                                <Outlet />
  • frontend/src/api/axiosInstance.ts

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

    r77e572b rc1d2f07  
    11import { useState } from "react";
    22import { useNavigate } from "react-router-dom";
     3import { toast } from "react-toastify";
    34import axiosInstance, { scheduleTokenRefresh } from "../api/axiosInstance";
    45import { useAuth } from "../context/authContext";
     
    2223                        setUser(response.data.user);
    2324                        navigate("/");
    24                 } catch (error) {
    25                         setError("Login failed. Please check your credentials.");
     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}`);
    2631                }
    2732        };
  • frontend/src/pages/Nav.tsx

    r77e572b rc1d2f07  
    11import { Link } from "react-router-dom";
     2import { toast } from "react-toastify";
    23import axiosInstance, { baseURL } from "../api/axiosInstance";
    34import Logo from "../assets/logo-finkwave.png";
     
    1213                        await axiosInstance.post("/auth/logout");
    1314                        setUser(undefined);
     15                        toast.success("Logout successful!");
    1416                } catch (error) {
    1517                        console.error("Logout failed:", error);
     18                        toast.error("Logout failed!");
    1619                }
    1720        };
  • frontend/src/pages/Register.tsx

    r77e572b rc1d2f07  
    11import { useEffect, useState } from "react";
    22import { useNavigate } from "react-router";
     3import { toast } from "react-toastify";
    34import axiosInstance, { scheduleTokenRefresh } from "../api/axiosInstance";
    45import { useAuth } from "../context/authContext";
     
    1516                null,
    1617        );
     18        const [error, setError] = useState<string | null>(null);
    1719
    1820        const navigate = useNavigate();
     
    2628        }, [previewProfilePhoto]);
    2729
     30        const handleRemoveFile = () => {
     31                if (previewProfilePhoto) {
     32                        URL.revokeObjectURL(previewProfilePhoto);
     33                }
     34                setProfilePhotoFile(null);
     35                setPreviewProfilePhoto(null);
     36        };
     37
    2838        const handleRegister = async (e: React.FormEvent) => {
    2939                e.preventDefault();
    30                 // todo: add proper error handling
    3140                if (profilePhotoFile && profilePhotoFile.size > 5 * 1024 * 1024) {
    3241                        alert("Max file size is 5MB");
     42                        return;
     43                }
     44
     45                if (username === "" || password === "" || fullname === "" || email === "") {
     46                        setError("Please fill in all required fields.");
    3347                        return;
    3448                }
     
    5367                        setUser(response.data.user);
    5468                        navigate("/");
    55                 } catch (error) {
    56                         console.error("Registration failed:", error);
     69                        toast.success("Registration successful!");
     70                } catch (error: any) {
     71                        const errorMessage = error.response?.data?.error || "Please try again.";
     72                        setError(`Registration failed: ${errorMessage}`);
    5773                }
    5874        };
     
    6177                <div className="flex flex-col items-center justify-center min-h-[90vh] bg-gray-100">
    6278                        <h2 className="text-2xl mb-4">Register</h2>
    63                         <form className="bg-white p-6 rounded shadow-md w-80">
     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>}
    6481                                <div className="mb-4">
    6582                                        <label className="block text-gray-700 mb-2" htmlFor="username">
     
    115132                                                <label
    116133                                                        htmlFor="profilePhoto"
    117                                                         className="px-4 py-2 bg-blue-500 text-white rounded cursor-pointer hover:bg-blue-600 transition-colors text-sm font-medium"
     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"
    118135                                                >
    119136                                                        Choose File
    120137                                                </label>
    121                                                 <span className="text-gray-600 text-sm">
     138                                                <span className="text-gray-600 text-sm flex-1">
    122139                                                        {profilePhotoFile ? profilePhotoFile.name : "No file chosen"}
    123140                                                </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                                                )}
    124150                                        </div>
    125151                                        <input
Note: See TracChangeset for help on using the changeset viewer.