Changeset 484dc3f


Ignore:
Timestamp:
01/26/26 19:03:37 (6 months ago)
Author:
Filip Gavrilovski <filipgavrilovski28@…>
Branches:
main
Children:
d47e225
Parents:
3727852
Message:

add support for profile pictures

Files:
9 edited

Legend:

Unmodified
Added
Removed
  • finkwave/.gitignore

    r3727852 r484dc3f  
    3333.vscode/
    3434application.properties
     35
     36uploads/profile-pictures/*
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/controller/AuthController.java

    r3727852 r484dc3f  
    55import com.ukim.finki.develop.finkwave.config.AuthProperties;
    66import org.springframework.http.HttpStatus;
     7import org.springframework.http.MediaType;
    78import org.springframework.http.ResponseEntity;
    89import org.springframework.security.core.Authentication;
    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.PostMapping;
    14 import org.springframework.web.bind.annotation.RequestBody;
    15 import org.springframework.web.bind.annotation.RequestMapping;
    16 import org.springframework.web.bind.annotation.RestController;
     12import org.springframework.web.bind.annotation.*;
    1713import org.springframework.web.server.ResponseStatusException;
    1814
     
    3228    private final AuthService authService;
    3329
    34     @PostMapping("/register")
     30    @PostMapping(value = "/register", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    3531    public ResponseEntity<Map<String, Object>> register(
    36             @RequestBody AuthRequestDto authRequestDto,
     32            @ModelAttribute AuthRequestDto authRequestDto,
    3733            HttpServletResponse httpServletResponse){
    3834        try {
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/dto/AuthRequestDto.java

    r3727852 r484dc3f  
    22
    33import jakarta.annotation.Nullable;
     4import org.springframework.web.multipart.MultipartFile;
    45
    56public record AuthRequestDto(
     
    89        String email,
    910        String password,
    10         @Nullable String profilePhoto
     11        @Nullable MultipartFile profilePhoto
    1112){
    1213}
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/filters/JwtFilter.java

    r3727852 r484dc3f  
    3333            @NonNull HttpServletResponse response,
    3434            @NonNull FilterChain filterChain) throws ServletException, IOException {
     35       
    3536        String token = null;
    3637        if (request.getCookies() != null){
     
    5859            } catch (JwtException e){
    5960                System.out.println("Invalid jwt token.");
     61                response.setStatus(HttpStatus.UNAUTHORIZED.value());
    6062                return;
    6163            } catch (Exception e){
    6264                System.out.println(e.getMessage());
     65                response.setStatus(HttpStatus.UNAUTHORIZED.value());
    6366                return;
    6467            }
  • finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/AuthService.java

    r3727852 r484dc3f  
    1919import jakarta.transaction.Transactional;
    2020import lombok.RequiredArgsConstructor;
     21import org.springframework.web.multipart.MultipartFile;
     22
     23import java.io.IOException;
     24import java.nio.file.Files;
     25import java.nio.file.Path;
     26import java.nio.file.Paths;
     27import java.util.UUID;
    2128
    2229@Service
     
    3239
    3340    @Transactional
    34     public UserResponseDto registerAndLogIn(HttpServletResponse response, AuthRequestDto authRequestDto){
     41    public UserResponseDto registerAndLogIn(HttpServletResponse response, AuthRequestDto authRequestDto)
     42    throws IOException {
    3543        if (userRepository.findByUsername(authRequestDto.username()).isPresent()){
    3644            throw new RuntimeException("User already exists");
     
    9098    }
    9199
    92     private User createNonAdminUser(AuthRequestDto authRequestDto){
    93         User user = User.builder()
     100    private User createNonAdminUser(AuthRequestDto authRequestDto) throws IOException {
     101        User.UserBuilder userBuilder = User.builder()
    94102                .fullName(authRequestDto.fullname())
    95103                .username(authRequestDto.username())
    96104                .email(authRequestDto.email())
    97                 .profilePhoto(authRequestDto.profilePhoto())
    98105                .password(passwordEncoder.encode(authRequestDto.password()))
    99                 .role(Role.NONADMIN)
    100                 .build();
     106                .role(Role.NONADMIN);
     107
     108
     109        MultipartFile profilePhoto = authRequestDto.profilePhoto();
     110        if (profilePhoto != null) {
     111            if (profilePhoto.getContentType() != null && !profilePhoto.getContentType().startsWith("image/"))
     112                throw new IllegalArgumentException("Invalid fyle type");
     113
     114            if (profilePhoto.getSize() > 5_000_000) {
     115                throw new IllegalArgumentException(("File too large."));
     116            }
     117
     118            String filename = UUID.randomUUID() + "-" + profilePhoto.getOriginalFilename();
     119            Path path = Paths.get("uploads/profile-pictures", filename);
     120            Files.copy(profilePhoto.getInputStream(), path);
     121            userBuilder.profilePhoto("profile-pictures/" + filename);
     122        }
     123
     124        User user = userBuilder.build();
    101125
    102126        NonAdminUser nonAdminUser = new NonAdminUser();
  • frontend/src/Login.tsx

    r3727852 r484dc3f  
    2020                        }>("/auth/login", { username, password });
    2121                        scheduleTokenRefresh(response.data.tokenExpiresIn);
    22                         setUser({
    23                                 username: response.data.user.username,
    24                                 fullName: "demo",
    25                                 email: "demo@demo.com",
    26                                 role: response.data.user.role,
    27                         });
     22                        setUser(response.data.user);
    2823                        navigate("/");
    2924                } catch (error) {
  • frontend/src/Nav.tsx

    r3727852 r484dc3f  
    11import { Link } from "react-router-dom";
    2 import axiosInstance from "./api/axiosInstance";
     2import axiosInstance, { baseURL } from "./api/axiosInstance";
    33import Logo from "./assets/logo-finkwave.png";
    44import { useAuth } from "./context/authContext";
     
    2929                                                        <div className="flex items-center space-x-3">
    3030                                                                <div className="flex items-center space-x-2 bg-slate-700 rounded-lg px-3 py-2">
    31                                                                         <div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center">
    32                                                                                 <span className="text-white text-sm font-semibold">
    33                                                                                         {user.username.charAt(0).toUpperCase()}
    34                                                                                 </span>
    35                                                                         </div>
     31                                                                        {user.profilePhoto ? (
     32                                                                                <img
     33                                                                                        src={`${baseURL}/${user.profilePhoto}`}
     34                                                                                        alt={`${user.username}'s profile`}
     35                                                                                        className="w-8 h-8 rounded-full object-cover"
     36                                                                                />
     37                                                                        ) : (
     38                                                                                <div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center">
     39                                                                                        <span className="text-white text-sm font-semibold">
     40                                                                                                {user.username.charAt(0).toUpperCase()}
     41                                                                                        </span>
     42                                                                                </div>
     43                                                                        )}
     44
    3645                                                                        <div className="text-white">
    3746                                                                                <p className="text-sm font-medium">{user.username}</p>
     
    4251                                                                </div>
    4352                                                                <button
    44                                                                         onClick={(e: React.MouseEvent<HTMLButtonElement>) =>
    45                                                                                 handleLogout(e)
    46                                                                         }
     53                                                                        onClick={handleLogout}
    4754                                                                        className="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-lg text-sm
    4855                                    font-medium transition-colors duration-200 flex items-center space-x-1 cursor-pointer"
  • frontend/src/Register.tsx

    r3727852 r484dc3f  
    1 import { useState } from "react";
     1import { useEffect, useState } from "react";
    22import { useNavigate } from "react-router";
    33import axiosInstance, { scheduleTokenRefresh } from "./api/axiosInstance";
     
    1111        const [fullname, setFullname] = useState("");
    1212        const [email, setEmail] = useState("");
    13         const [profilePhoto, setProfilePhoto] = useState<string | null>(null);
     13        const [profilePhotoFile, setProfilePhotoFile] = useState<File | null>(null);
     14        const [previewProfilePhoto, setPreviewProfilePhoto] = useState<string | null>(
     15                null,
     16        );
    1417
    1518        const navigate = useNavigate();
    1619
     20        useEffect(() => {
     21                return () => {
     22                        if (previewProfilePhoto) {
     23                                URL.revokeObjectURL(previewProfilePhoto);
     24                        }
     25                };
     26        }, [previewProfilePhoto]);
     27
    1728        const handleRegister = async (e: React.FormEvent) => {
    1829                e.preventDefault();
     30                // todo: add proper error handling
     31                if (profilePhotoFile && profilePhotoFile.size > 5 * 1024 * 1024) {
     32                        alert("Max file size is 5MB");
     33                        return;
     34                }
     35
     36                if (profilePhotoFile && !profilePhotoFile.type.startsWith("image/")) {
     37                        alert("Only images allowed");
     38                        return;
     39                }
     40                const formData = new FormData();
     41                if (profilePhotoFile) formData.append("profilePhoto", profilePhotoFile);
     42                formData.append("username", username);
     43                formData.append("fullname", fullname);
     44                formData.append("email", email);
     45                formData.append("password", password);
     46
    1947                try {
    2048                        const response = await axiosInstance.post<{
    2149                                user: User;
    2250                                tokenExpiresIn: number;
    23                         }>("/auth/register", {
    24                                 username,
    25                                 fullname,
    26                                 email,
    27                                 password,
    28                                 profilePhoto,
    29                         });
     51                        }>("/auth/register", formData);
    3052                        scheduleTokenRefresh(response.data.tokenExpiresIn);
    3153                        setUser(response.data.user);
     
    89111                                </div>
    90112                                <div className="mb-4">
    91                                         <label className="block text-gray-700 mb-2" htmlFor="profilePhoto">
    92                                                 Profile Photo URL
    93                                         </label>
     113                                        <label className="block text-gray-700 mb-2">Profile Photo</label>
     114                                        <div className="flex items-center gap-3">
     115                                                <label
     116                                                        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"
     118                                                >
     119                                                        Choose File
     120                                                </label>
     121                                                <span className="text-gray-600 text-sm">
     122                                                        {profilePhotoFile ? profilePhotoFile.name : "No file chosen"}
     123                                                </span>
     124                                        </div>
    94125                                        <input
    95                                                 placeholder="todo"
    96                                                 type="text"
     126                                                type="file"
     127                                                accept="image/*"
    97128                                                id="profilePhoto"
    98                                                 className="w-full p-2 border border-gray-300 rounded"
    99                                                 value={profilePhoto || ""}
    100                                                 onChange={(e) => setProfilePhoto(e.target.value)}
     129                                                className="hidden"
     130                                                onChange={(e) => {
     131                                                        if (e.target.files && e.target.files[0]) {
     132                                                                setProfilePhotoFile(e.target.files[0]);
     133                                                                setPreviewProfilePhoto(URL.createObjectURL(e.target.files[0]));
     134                                                        }
     135                                                }}
    101136                                        />
    102137                                </div>
     138                                {previewProfilePhoto && (
     139                                        <div className="mb-4">
     140                                                <p className="block text-gray-700 mb-2">Profile Photo Preview:</p>
     141                                                <img
     142                                                        src={previewProfilePhoto}
     143                                                        alt="Profile Preview"
     144                                                        className="w-20 h-20 object-cover rounded-full"
     145                                                />
     146                                        </div>
     147                                )}
    103148                                <button
    104149                                        type="submit"
    105150                                        className="w-full bg-blue-500 text-white p-2 rounded hover:bg-blue-600 cursor-pointer"
    106                                         onClick={(e: React.MouseEvent<HTMLButtonElement>) =>
    107                                                 handleRegister(e)
    108                                         }
     151                                        onClick={handleRegister}
    109152                                >
    110153                                        Register
  • frontend/src/api/axiosInstance.ts

    r3727852 r484dc3f  
    55export const axiosInstance = axios.create({
    66        baseURL,
    7         headers: {
    8                 "Content-Type": "application/json",
    9         },
    107        withCredentials: true,
     8});
     9
     10// set content-type to application/json unless sending FormData
     11axiosInstance.interceptors.request.use((config) => {
     12        if (!(config.data instanceof FormData)) {
     13                config.headers["Content-Type"] = "application/json";
     14        }
     15        return config;
    1116});
    1217
     
    2328export const scheduleTokenRefresh = (expiryTimeSeconds: number) => {
    2429        clearRefreshTimeout();
     30
     31        if (!expiryTimeSeconds || expiryTimeSeconds <= 0) {
     32                return;
     33        }
    2534
    2635        const refreshTime = (expiryTimeSeconds - 60) * 1000;
     
    4453                        scheduleTokenRefresh(refreshResponse.data.tokenExpiresIn);
    4554                } catch (err) {
     55                        clearRefreshTimeout();
    4656                        console.error(err);
    4757                        throw err;
     
    5363};
    5464
    55 axios.interceptors.response.use(
     65axiosInstance.interceptors.response.use(
    5666        (response) => response,
    5767        async (error) => {
Note: See TracChangeset for help on using the changeset viewer.