Changeset 484dc3f
- Timestamp:
- 01/26/26 19:03:37 (6 months ago)
- Branches:
- main
- Children:
- d47e225
- Parents:
- 3727852
- Files:
-
- 9 edited
-
finkwave/.gitignore (modified) (1 diff)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/controller/AuthController.java (modified) (2 diffs)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/dto/AuthRequestDto.java (modified) (2 diffs)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/filters/JwtFilter.java (modified) (2 diffs)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/AuthService.java (modified) (3 diffs)
-
frontend/src/Login.tsx (modified) (1 diff)
-
frontend/src/Nav.tsx (modified) (3 diffs)
-
frontend/src/Register.tsx (modified) (3 diffs)
-
frontend/src/api/axiosInstance.ts (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
finkwave/.gitignore
r3727852 r484dc3f 33 33 .vscode/ 34 34 application.properties 35 36 uploads/profile-pictures/* -
finkwave/src/main/java/com/ukim/finki/develop/finkwave/controller/AuthController.java
r3727852 r484dc3f 5 5 import com.ukim.finki.develop.finkwave.config.AuthProperties; 6 6 import org.springframework.http.HttpStatus; 7 import org.springframework.http.MediaType; 7 8 import org.springframework.http.ResponseEntity; 8 9 import org.springframework.security.core.Authentication; 9 10 import org.springframework.security.core.context.SecurityContextHolder; 10 11 import 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; 12 import org.springframework.web.bind.annotation.*; 17 13 import org.springframework.web.server.ResponseStatusException; 18 14 … … 32 28 private final AuthService authService; 33 29 34 @PostMapping( "/register")30 @PostMapping(value = "/register", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) 35 31 public ResponseEntity<Map<String, Object>> register( 36 @ RequestBodyAuthRequestDto authRequestDto,32 @ModelAttribute AuthRequestDto authRequestDto, 37 33 HttpServletResponse httpServletResponse){ 38 34 try { -
finkwave/src/main/java/com/ukim/finki/develop/finkwave/dto/AuthRequestDto.java
r3727852 r484dc3f 2 2 3 3 import jakarta.annotation.Nullable; 4 import org.springframework.web.multipart.MultipartFile; 4 5 5 6 public record AuthRequestDto( … … 8 9 String email, 9 10 String password, 10 @Nullable StringprofilePhoto11 @Nullable MultipartFile profilePhoto 11 12 ){ 12 13 } -
finkwave/src/main/java/com/ukim/finki/develop/finkwave/filters/JwtFilter.java
r3727852 r484dc3f 33 33 @NonNull HttpServletResponse response, 34 34 @NonNull FilterChain filterChain) throws ServletException, IOException { 35 35 36 String token = null; 36 37 if (request.getCookies() != null){ … … 58 59 } catch (JwtException e){ 59 60 System.out.println("Invalid jwt token."); 61 response.setStatus(HttpStatus.UNAUTHORIZED.value()); 60 62 return; 61 63 } catch (Exception e){ 62 64 System.out.println(e.getMessage()); 65 response.setStatus(HttpStatus.UNAUTHORIZED.value()); 63 66 return; 64 67 } -
finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/AuthService.java
r3727852 r484dc3f 19 19 import jakarta.transaction.Transactional; 20 20 import lombok.RequiredArgsConstructor; 21 import org.springframework.web.multipart.MultipartFile; 22 23 import java.io.IOException; 24 import java.nio.file.Files; 25 import java.nio.file.Path; 26 import java.nio.file.Paths; 27 import java.util.UUID; 21 28 22 29 @Service … … 32 39 33 40 @Transactional 34 public UserResponseDto registerAndLogIn(HttpServletResponse response, AuthRequestDto authRequestDto){ 41 public UserResponseDto registerAndLogIn(HttpServletResponse response, AuthRequestDto authRequestDto) 42 throws IOException { 35 43 if (userRepository.findByUsername(authRequestDto.username()).isPresent()){ 36 44 throw new RuntimeException("User already exists"); … … 90 98 } 91 99 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() 94 102 .fullName(authRequestDto.fullname()) 95 103 .username(authRequestDto.username()) 96 104 .email(authRequestDto.email()) 97 .profilePhoto(authRequestDto.profilePhoto())98 105 .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(); 101 125 102 126 NonAdminUser nonAdminUser = new NonAdminUser(); -
frontend/src/Login.tsx
r3727852 r484dc3f 20 20 }>("/auth/login", { username, password }); 21 21 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); 28 23 navigate("/"); 29 24 } catch (error) { -
frontend/src/Nav.tsx
r3727852 r484dc3f 1 1 import { Link } from "react-router-dom"; 2 import axiosInstance from "./api/axiosInstance";2 import axiosInstance, { baseURL } from "./api/axiosInstance"; 3 3 import Logo from "./assets/logo-finkwave.png"; 4 4 import { useAuth } from "./context/authContext"; … … 29 29 <div className="flex items-center space-x-3"> 30 30 <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 36 45 <div className="text-white"> 37 46 <p className="text-sm font-medium">{user.username}</p> … … 42 51 </div> 43 52 <button 44 onClick={(e: React.MouseEvent<HTMLButtonElement>) => 45 handleLogout(e) 46 } 53 onClick={handleLogout} 47 54 className="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-lg text-sm 48 55 font-medium transition-colors duration-200 flex items-center space-x-1 cursor-pointer" -
frontend/src/Register.tsx
r3727852 r484dc3f 1 import { use State } from "react";1 import { useEffect, useState } from "react"; 2 2 import { useNavigate } from "react-router"; 3 3 import axiosInstance, { scheduleTokenRefresh } from "./api/axiosInstance"; … … 11 11 const [fullname, setFullname] = useState(""); 12 12 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 ); 14 17 15 18 const navigate = useNavigate(); 16 19 20 useEffect(() => { 21 return () => { 22 if (previewProfilePhoto) { 23 URL.revokeObjectURL(previewProfilePhoto); 24 } 25 }; 26 }, [previewProfilePhoto]); 27 17 28 const handleRegister = async (e: React.FormEvent) => { 18 29 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 19 47 try { 20 48 const response = await axiosInstance.post<{ 21 49 user: User; 22 50 tokenExpiresIn: number; 23 }>("/auth/register", { 24 username, 25 fullname, 26 email, 27 password, 28 profilePhoto, 29 }); 51 }>("/auth/register", formData); 30 52 scheduleTokenRefresh(response.data.tokenExpiresIn); 31 53 setUser(response.data.user); … … 89 111 </div> 90 112 <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> 94 125 <input 95 placeholder="todo"96 type="text"126 type="file" 127 accept="image/*" 97 128 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 }} 101 136 /> 102 137 </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 )} 103 148 <button 104 149 type="submit" 105 150 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} 109 152 > 110 153 Register -
frontend/src/api/axiosInstance.ts
r3727852 r484dc3f 5 5 export const axiosInstance = axios.create({ 6 6 baseURL, 7 headers: {8 "Content-Type": "application/json",9 },10 7 withCredentials: true, 8 }); 9 10 // set content-type to application/json unless sending FormData 11 axiosInstance.interceptors.request.use((config) => { 12 if (!(config.data instanceof FormData)) { 13 config.headers["Content-Type"] = "application/json"; 14 } 15 return config; 11 16 }); 12 17 … … 23 28 export const scheduleTokenRefresh = (expiryTimeSeconds: number) => { 24 29 clearRefreshTimeout(); 30 31 if (!expiryTimeSeconds || expiryTimeSeconds <= 0) { 32 return; 33 } 25 34 26 35 const refreshTime = (expiryTimeSeconds - 60) * 1000; … … 44 53 scheduleTokenRefresh(refreshResponse.data.tokenExpiresIn); 45 54 } catch (err) { 55 clearRefreshTimeout(); 46 56 console.error(err); 47 57 throw err; … … 53 63 }; 54 64 55 axios .interceptors.response.use(65 axiosInstance.interceptors.response.use( 56 66 (response) => response, 57 67 async (error) => {
Note:
See TracChangeset
for help on using the changeset viewer.
