Changes in / [c1d2f07:77e572b]
- Files:
-
- 7 deleted
- 12 edited
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/controller/AuthController.java (modified) (6 diffs)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/controller/GlobalExceptionHandler.java (modified) (1 diff)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/exceptions/AuthException.java (deleted)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/exceptions/InvalidCredentialsException.java (deleted)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/exceptions/InvalidFileException.java (deleted)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/exceptions/InvalidTokenException.java (deleted)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/exceptions/UnauthenticatedException.java (deleted)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/exceptions/UserAlreadyExistsException.java (deleted)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/exceptions/UserNotFoundException.java (deleted)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/model/RefreshToken.java (modified) (1 diff)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/AuthService.java (modified) (9 diffs)
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/RefreshTokenService.java (modified) (3 diffs)
-
frontend/package-lock.json (modified) (3 diffs)
-
frontend/package.json (modified) (1 diff)
-
frontend/src/App.tsx (modified) (2 diffs)
-
frontend/src/api/axiosInstance.ts (modified) (1 diff)
-
frontend/src/pages/Login.tsx (modified) (2 diffs)
-
frontend/src/pages/Nav.tsx (modified) (2 diffs)
-
frontend/src/pages/Register.tsx (modified) (6 diffs)
Legend:
- Unmodified
- Added
- Removed
-
finkwave/src/main/java/com/ukim/finki/develop/finkwave/controller/AuthController.java
rc1d2f07 r77e572b 1 1 package com.ukim.finki.develop.finkwave.controller; 2 2 3 import java.io.IOException;4 3 import java.util.Map; 5 4 5 import com.ukim.finki.develop.finkwave.config.AuthProperties; 6 import org.springframework.http.HttpStatus; 6 7 import org.springframework.http.MediaType; 7 8 import org.springframework.http.ResponseEntity; … … 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.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; 12 import org.springframework.web.bind.annotation.*; 13 import org.springframework.web.server.ResponseStatusException; 18 14 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;22 15 import com.ukim.finki.develop.finkwave.model.dto.AuthRequestDto; 23 16 import com.ukim.finki.develop.finkwave.model.dto.LoginRequestDto; … … 38 31 public ResponseEntity<Map<String, Object>> register( 39 32 @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 } 46 45 } 47 46 … … 49 48 public ResponseEntity<Map<String, Object>> login( 50 49 @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 } 57 63 } 64 58 65 59 66 @PostMapping("/refresh") 60 67 public ResponseEntity<Map<String, Integer>> refresh( 61 68 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"); 65 72 } 66 73 authService.refreshAccessTokenAndAddCookie(httpServletResponse, refreshToken); … … 73 80 public ResponseEntity<Map<String, String>> logout( 74 81 HttpServletResponse httpServletResponse, 75 @CookieValue(name = "refreshToken", required = false) String refreshToken) {82 @CookieValue(name = "refreshToken", required = false) String refreshToken){ 76 83 authService.clearCookies(httpServletResponse, refreshToken); 77 84 SecurityContextHolder.clearContext(); … … 80 87 81 88 @GetMapping("/user") 82 public ResponseEntity<Map<String, Object>> getUser() {89 public ResponseEntity<Map<String, Object>> getUser(){ 83 90 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"); 86 93 } 87 94 Object principal = authentication.getPrincipal(); -
finkwave/src/main/java/com/ukim/finki/develop/finkwave/controller/GlobalExceptionHandler.java
rc1d2f07 r77e572b 1 1 package com.ukim.finki.develop.finkwave.controller; 2 2 3 import java.util.Map;4 5 import org.slf4j.Logger;6 import org.slf4j.LoggerFactory;7 3 import org.springframework.http.HttpStatus; 8 4 import org.springframework.http.ResponseEntity; 9 import org.springframework.security.core.userdetails.UsernameNotFoundException;10 5 import org.springframework.web.bind.annotation.ExceptionHandler; 11 6 import org.springframework.web.bind.annotation.RestControllerAdvice; 12 import org.springframework.web.multipart.MaxUploadSizeExceededException;13 import org.springframework.web.server.ResponseStatusException;14 7 15 import com.ukim.finki.develop.finkwave.exceptions.AuthException; 8 import java.util.HashMap; 9 import java.util.Map; 16 10 17 11 @RestControllerAdvice 18 12 public class GlobalExceptionHandler { 19 private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);20 13 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()); 27 18 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); 60 20 } 61 21 } -
finkwave/src/main/java/com/ukim/finki/develop/finkwave/model/RefreshToken.java
rc1d2f07 r77e572b 9 9 @Entity 10 10 @Getter @Setter 11 @Table(name = "refresh_token")12 11 public class RefreshToken { 13 12 @Id -
finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/AuthService.java
rc1d2f07 r77e572b 1 1 package 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;8 2 9 3 import org.springframework.security.core.Authentication; … … 11 5 import org.springframework.security.crypto.password.PasswordEncoder; 12 6 import org.springframework.stereotype.Service; 13 import org.springframework.web.multipart.MultipartFile;14 7 15 8 import 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; 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; 21 12 import com.ukim.finki.develop.finkwave.model.NonAdminUser; 22 13 import com.ukim.finki.develop.finkwave.model.RefreshToken; 23 14 import com.ukim.finki.develop.finkwave.model.Role; 24 15 import 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;28 16 import com.ukim.finki.develop.finkwave.repository.NonAdminUserRepository; 29 17 import com.ukim.finki.develop.finkwave.repository.UserRepository; … … 33 21 import jakarta.transaction.Transactional; 34 22 import 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; 35 30 36 31 @Service … … 44 39 private final AuthProperties authProperties; 45 40 46 private static final long MAX_FILE_SIZE = 2_000_000;47 41 48 42 @Transactional … … 50 44 throws IOException { 51 45 if (userRepository.findByUsername(authRequestDto.username()).isPresent()){ 52 throw new UserAlreadyExistsException();46 throw new RuntimeException("User already exists"); 53 47 } 54 48 User user = createNonAdminUser(authRequestDto); … … 58 52 public UserResponseDto login(HttpServletResponse response, LoginRequestDto loginRequestDto){ 59 53 User user = userRepository.findByUsername(loginRequestDto.username()) 60 .orElseThrow( InvalidCredentialsException::new);54 .orElseThrow(() -> new RuntimeException("Invalid credentials")); 61 55 62 56 if (!passwordEncoder.matches(loginRequestDto.password(), user.getPassword())){ 63 throw new InvalidCredentialsException();57 throw new RuntimeException("Invalid credentials"); 64 58 } 65 59 … … 80 74 public UserResponseDto getUserDtoByUsername(String username){ 81 75 User user = userRepository.findByUsername(username) 82 .orElseThrow( UserNotFoundException::new);76 .orElseThrow(() -> new RuntimeException("Invalid credentials")); 83 77 84 78 return userResponseFromUser(user); … … 109 103 Authentication authentication= SecurityContextHolder.getContext().getAuthentication(); 110 104 if (authentication == null || !authentication.isAuthenticated()) { 111 throw new UnauthenticatedException();105 throw new RuntimeException("User not authenticated"); 112 106 } 113 107 114 108 String username=authentication.getName(); 115 109 110 116 111 return userRepository.findByUsername(username).map(User::getId) 117 .orElseThrow( UserNotFoundException::new);112 .orElseThrow(()->new RuntimeException("User not found")); 118 113 } 119 114 … … 129 124 130 125 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"); 136 129 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.")); 139 132 } 140 133 -
finkwave/src/main/java/com/ukim/finki/develop/finkwave/service/RefreshTokenService.java
rc1d2f07 r77e572b 1 1 package 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; 2 9 3 10 import java.time.Instant; … … 5 12 import java.util.Optional; 6 13 import 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;17 14 18 15 @Service … … 35 32 36 33 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"); 44 37 } 45 38 return refreshToken; -
frontend/package-lock.json
rc1d2f07 r77e572b 15 15 "react-dom": "^19.2.0", 16 16 "react-router-dom": "^7.13.0", 17 "react-toastify": "^11.0.5",18 17 "tailwindcss": "^4.1.18" 19 18 }, … … 2153 2152 "funding": { 2154 2153 "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"2164 2154 } 2165 2155 }, … … 3693 3683 } 3694 3684 }, 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 },3708 3685 "node_modules/resolve-from": { 3709 3686 "version": "4.0.0", -
frontend/package.json
rc1d2f07 r77e572b 17 17 "react-dom": "^19.2.0", 18 18 "react-router-dom": "^7.13.0", 19 "react-toastify": "^11.0.5",20 19 "tailwindcss": "^4.1.18" 21 20 }, -
frontend/src/App.tsx
rc1d2f07 r77e572b 1 1 import { createBrowserRouter, Outlet, RouterProvider } from "react-router-dom"; 2 import { ToastContainer } from "react-toastify";3 import "react-toastify/dist/ReactToastify.css";4 2 import AllUsers from "./pages/AllUsers"; 5 3 import LandingPage from "./pages/LandingPage"; … … 13 11 <div className="flex flex-col min-h-screen"> 14 12 <Nav /> 15 <ToastContainer16 position="top-right"17 autoClose={2000}18 hideProgressBar={false}19 newestOnTop={false}20 closeOnClick21 rtl={false}22 pauseOnFocusLoss23 draggable24 pauseOnHover25 theme="light"26 />27 13 <main className="grow"> 28 14 <Outlet /> -
frontend/src/api/axiosInstance.ts
rc1d2f07 r77e572b 70 70 [401, 403].includes(error.response?.status) && 71 71 !originalConfig._retry && 72 originalConfig.url !== "/auth/refresh" && 73 originalConfig.url !== "/auth/login" && 74 originalConfig.url !== "/auth/register" 72 originalConfig.url !== "/auth/refresh" 75 73 ) { 76 74 originalConfig._retry = true; -
frontend/src/pages/Login.tsx
rc1d2f07 r77e572b 1 1 import { useState } from "react"; 2 2 import { useNavigate } from "react-router-dom"; 3 import { toast } from "react-toastify";4 3 import axiosInstance, { scheduleTokenRefresh } from "../api/axiosInstance"; 5 4 import { useAuth } from "../context/authContext"; … … 23 22 setUser(response.data.user); 24 23 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."); 31 26 } 32 27 }; -
frontend/src/pages/Nav.tsx
rc1d2f07 r77e572b 1 1 import { Link } from "react-router-dom"; 2 import { toast } from "react-toastify";3 2 import axiosInstance, { baseURL } from "../api/axiosInstance"; 4 3 import Logo from "../assets/logo-finkwave.png"; … … 13 12 await axiosInstance.post("/auth/logout"); 14 13 setUser(undefined); 15 toast.success("Logout successful!");16 14 } catch (error) { 17 15 console.error("Logout failed:", error); 18 toast.error("Logout failed!");19 16 } 20 17 }; -
frontend/src/pages/Register.tsx
rc1d2f07 r77e572b 1 1 import { useEffect, useState } from "react"; 2 2 import { useNavigate } from "react-router"; 3 import { toast } from "react-toastify";4 3 import axiosInstance, { scheduleTokenRefresh } from "../api/axiosInstance"; 5 4 import { useAuth } from "../context/authContext"; … … 16 15 null, 17 16 ); 18 const [error, setError] = useState<string | null>(null);19 17 20 18 const navigate = useNavigate(); … … 28 26 }, [previewProfilePhoto]); 29 27 30 const handleRemoveFile = () => {31 if (previewProfilePhoto) {32 URL.revokeObjectURL(previewProfilePhoto);33 }34 setProfilePhotoFile(null);35 setPreviewProfilePhoto(null);36 };37 38 28 const handleRegister = async (e: React.FormEvent) => { 39 29 e.preventDefault(); 30 // todo: add proper error handling 40 31 if (profilePhotoFile && profilePhotoFile.size > 5 * 1024 * 1024) { 41 32 alert("Max file size is 5MB"); 42 return;43 }44 45 if (username === "" || password === "" || fullname === "" || email === "") {46 setError("Please fill in all required fields.");47 33 return; 48 34 } … … 67 53 setUser(response.data.user); 68 54 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); 73 57 } 74 58 }; … … 77 61 <div className="flex flex-col items-center justify-center min-h-[90vh] bg-gray-100"> 78 62 <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"> 81 64 <div className="mb-4"> 82 65 <label className="block text-gray-700 mb-2" htmlFor="username"> … … 132 115 <label 133 116 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" 135 118 > 136 119 Choose File 137 120 </label> 138 <span className="text-gray-600 text-sm flex-1">121 <span className="text-gray-600 text-sm"> 139 122 {profilePhotoFile ? profilePhotoFile.name : "No file chosen"} 140 123 </span> 141 {profilePhotoFile && (142 <button143 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 Remove148 </button>149 )}150 124 </div> 151 125 <input
Note:
See TracChangeset
for help on using the changeset viewer.
