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