From c0f547d8d3445cf48f74e37de88acc8899385fba Mon Sep 17 00:00:00 2001 From: Daniel Ntege Date: Sun, 2 Feb 2025 10:06:10 +0300 Subject: [PATCH] Implement password change functionality with validation and strength indicator; enhance user feedback with loading states and error handling --- .../profile/components/PasswordEdit.tsx | 245 +++++++++++++++--- netmanager-app/core/apis/settings.ts | 14 +- 2 files changed, 218 insertions(+), 41 deletions(-) diff --git a/netmanager-app/app/(authenticated)/profile/components/PasswordEdit.tsx b/netmanager-app/app/(authenticated)/profile/components/PasswordEdit.tsx index e4a6c33b5c..881be832fc 100644 --- a/netmanager-app/app/(authenticated)/profile/components/PasswordEdit.tsx +++ b/netmanager-app/app/(authenticated)/profile/components/PasswordEdit.tsx @@ -4,61 +4,236 @@ import { useState } from "react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" +import { useAppSelector } from "@/core/redux/hooks" +import { updateUserPasswordApi } from "@/core/apis/settings" +import { useToast } from "@/components/ui/use-toast" +import { Progress } from "@/components/ui/progress" +import { Eye, EyeOff } from "lucide-react" export default function PasswordEdit() { + const currentUser = useAppSelector((state) => state.user.userDetails) + const { toast } = useToast() + const [isLoading, setIsLoading] = useState(false) + const [showPassword, setShowPassword] = useState({ + current: false, + new: false, + confirm: false, + }) + + const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/ + const [passwords, setPasswords] = useState({ - current: "", - new: "", - confirm: "", + currentPassword: "", + newPassword: "", + confirmNewPassword: "", + }) + + const [errors, setErrors] = useState({ + currentPassword: "", + newPassword: "", + confirmNewPassword: "", }) const handleInputChange = (e: React.ChangeEvent) => { - setPasswords({ ...passwords, [e.target.name]: e.target.value }) + const { name, value } = e.target + setPasswords({ ...passwords, [name]: value }) + validateField(name, value) + } + + const validateField = (name: string, value: string) => { + let error = "" + switch (name) { + case "currentPassword": + if (!value) error = "Current password is required" + break + case "newPassword": + if (!value) { + error = "New password is required" + } else if (!passwordRegex.test(value)) { + error = + "Password must be at least 8 characters long and include uppercase, lowercase, number, and special character" + } + break + case "confirmNewPassword": + if (!value) { + error = "Please confirm your new password" + } else if (value !== passwords.newPassword) { + error = "Passwords do not match" + } + break + } + setErrors((prev) => ({ ...prev, [name]: error })) + } + + const calculatePasswordStrength = (password: string) => { + let strength = 0 + if (password.length >= 8) strength += 25 + if (password.match(/[A-Z]/)) strength += 25 + if (password.match(/[a-z]/)) strength += 25 + if (password.match(/[0-9]/)) strength += 25 + return strength } - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() - setPasswords({ current: "", new: "", confirm: "" }) + if (!currentUser) { + toast({ + title: "Error", + description: "User not found.", + variant: "destructive", + }) + return + } + + const userId = currentUser._id + const { currentPassword, newPassword } = passwords + + Object.entries(passwords).forEach(([key, value]) => validateField(key, value)) + + if (Object.values(errors).some((error) => error)) { + toast({ + title: "Error", + description: "Please correct the errors in the form.", + variant: "destructive", + }) + return + } + + const pwdData = { + password: newPassword, + old_password: currentPassword, + } + + try { + setIsLoading(true) + const response = await updateUserPasswordApi(userId, pwdData) + + if (response) { + setPasswords({ + currentPassword: "", + newPassword: "", + confirmNewPassword: "", + }) + toast({ + title: "Success", + description: "Password updated successfully.", + }) + } else { + throw new Error(response.message) + } + } catch (error) { + toast({ + title: "Error", + description: (error as Error).message || "An error occurred.", + variant: "destructive", + }) + } finally { + setIsLoading(false) + } + } + + const togglePasswordVisibility = (field: "current" | "new" | "confirm") => { + setShowPassword((prev) => ({ ...prev, [field]: !prev[field] })) } return ( -
+

Change Password

- - + +
+ + +
+ {errors.currentPassword && ( +

+ {errors.currentPassword} +

+ )}
- - + +
+ + +
+ {errors.newPassword && ( +

+ {errors.newPassword} +

+ )} + +

+ Password strength: {calculatePasswordStrength(passwords.newPassword)}% +

- - + +
+ + +
+ {errors.confirmNewPassword && ( +

+ {errors.confirmNewPassword} +

+ )}
- +
) diff --git a/netmanager-app/core/apis/settings.ts b/netmanager-app/core/apis/settings.ts index 6671f02673..f36a9a91a5 100644 --- a/netmanager-app/core/apis/settings.ts +++ b/netmanager-app/core/apis/settings.ts @@ -12,6 +12,11 @@ interface CreateClientData { user_id: string; } +interface PasswordData { + password: string; + old_password: string; +} + const axiosInstance = createAxiosInstance(); export const getUserClientsApi = async (userID: string): Promise => { @@ -33,12 +38,9 @@ export const createClientApi = async (data: CreateClientData): Promise = export const updateUserPasswordApi = async ( userId: string, - tenant: string, - userData: UserDetails -): Promise => { - return await axiosInstance.put(`${USERS_MGT_URL}/updatePassword`, userData, { - params: { tenant, id: userId }, - }) + userData: PasswordData +): Promise => { + return await axiosInstance.put(`${USERS_MGT_URL}/updatePassword/${userId}`, userData) .then((response) => response.data); };