xpeditis2.0/apps/frontend/app/dashboard/profile/page.tsx
David d9868dd49f
All checks were successful
CI/CD Pipeline / Backend - Build, Test & Push (push) Successful in 2m42s
CI/CD Pipeline / Frontend - Build, Test & Push (push) Successful in 27m20s
CI/CD Pipeline / Integration Tests (push) Has been skipped
CI/CD Pipeline / Deployment Summary (push) Successful in 1s
CI/CD Pipeline / Deploy to Portainer (push) Successful in 12s
CI/CD Pipeline / Discord Notification (Failure) (push) Has been skipped
CI/CD Pipeline / Discord Notification (Success) (push) Successful in 2s
fix: prevent password fields from being pre-filled in profile page
Fixed issue where password form fields (especially "New Password")
were being pre-filled with values, either from browser autocomplete
or residual form state.

Changes:
1. Added explicit empty defaultValues to password form
   - currentPassword: ''
   - newPassword: ''
   - confirmPassword: ''

2. Added autoComplete attributes to prevent browser pre-fill:
   - currentPassword: autoComplete="current-password"
   - newPassword: autoComplete="new-password"
   - confirmPassword: autoComplete="new-password"

3. Added useEffect to reset password form when switching tabs:
   - Ensures clean state when navigating to "Change Password" tab
   - Prevents stale values from persisting

4. Explicit reset values on successful password change:
   - Previously used passwordForm.reset() without values
   - Now explicitly sets all fields to empty strings

This ensures password fields are always empty and never pre-filled
by the browser or by residual form state.

Refs: apps/frontend/app/dashboard/profile/page.tsx:64-70,85-95

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-12 18:24:13 +01:00

429 lines
16 KiB
TypeScript

/**
* User Profile Page
*
* Allows users to view and update their profile information
*/
'use client';
import { useState, useEffect } from 'react';
import { useAuth } from '@/lib/context/auth-context';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { updateUser, changePassword } from '@/lib/api';
// Password update schema
const passwordSchema = z
.object({
currentPassword: z.string().min(1, 'Current password is required'),
newPassword: z
.string()
.min(12, 'Password must be at least 12 characters')
.regex(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/,
'Password must contain uppercase, lowercase, number, and special character'
),
confirmPassword: z.string().min(1, 'Please confirm your password'),
})
.refine(data => data.newPassword === data.confirmPassword, {
message: "Passwords don't match",
path: ['confirmPassword'],
});
type PasswordFormData = z.infer<typeof passwordSchema>;
// Profile update schema
const profileSchema = z.object({
firstName: z.string().min(2, 'First name must be at least 2 characters'),
lastName: z.string().min(2, 'Last name must be at least 2 characters'),
email: z.string().email('Invalid email address'),
});
type ProfileFormData = z.infer<typeof profileSchema>;
export default function ProfilePage() {
const { user, refreshUser, loading } = useAuth();
const queryClient = useQueryClient();
const [activeTab, setActiveTab] = useState<'profile' | 'password'>('profile');
const [successMessage, setSuccessMessage] = useState('');
const [errorMessage, setErrorMessage] = useState('');
// Profile form
const profileForm = useForm<ProfileFormData>({
resolver: zodResolver(profileSchema),
defaultValues: {
firstName: user?.firstName || '',
lastName: user?.lastName || '',
email: user?.email || '',
},
});
// Password form
const passwordForm = useForm<PasswordFormData>({
resolver: zodResolver(passwordSchema),
defaultValues: {
currentPassword: '',
newPassword: '',
confirmPassword: '',
},
});
// Update form values when user data loads
useEffect(() => {
if (user) {
profileForm.reset({
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user]);
// Reset password form when switching to password tab
useEffect(() => {
if (activeTab === 'password') {
passwordForm.reset({
currentPassword: '',
newPassword: '',
confirmPassword: '',
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeTab]);
// Update profile mutation
const updateProfileMutation = useMutation({
mutationFn: (data: ProfileFormData) => {
if (!user?.id) throw new Error('User ID not found');
return updateUser(user.id, data);
},
onSuccess: () => {
setSuccessMessage('Profile updated successfully!');
setErrorMessage('');
refreshUser();
queryClient.invalidateQueries({ queryKey: ['user'] });
setTimeout(() => setSuccessMessage(''), 3000);
},
onError: (error: any) => {
setErrorMessage(error.message || 'Failed to update profile');
setSuccessMessage('');
},
});
// Update password mutation
const updatePasswordMutation = useMutation({
mutationFn: async (data: PasswordFormData) => {
return changePassword({
currentPassword: data.currentPassword,
newPassword: data.newPassword,
});
},
onSuccess: () => {
setSuccessMessage('Password updated successfully!');
setErrorMessage('');
passwordForm.reset({
currentPassword: '',
newPassword: '',
confirmPassword: '',
});
setTimeout(() => setSuccessMessage(''), 3000);
},
onError: (error: any) => {
setErrorMessage(error.message || 'Failed to update password');
setSuccessMessage('');
},
});
const handleProfileSubmit = (data: ProfileFormData) => {
updateProfileMutation.mutate(data);
};
const handlePasswordSubmit = (data: PasswordFormData) => {
updatePasswordMutation.mutate(data);
};
// Show loading state while user data is being fetched
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">Loading profile...</p>
</div>
</div>
);
}
// Show error if user is not found after loading
if (!loading && !user) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<p className="text-red-600 mb-4">Unable to load user profile</p>
<button
onClick={() => window.location.reload()}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Retry
</button>
</div>
</div>
);
}
return (
<div className="max-w-4xl mx-auto space-y-6">
{/* Header */}
<div className="bg-gradient-to-r from-blue-600 to-blue-700 rounded-lg shadow-lg p-6 text-white">
<h1 className="text-3xl font-bold mb-2">My Profile</h1>
<p className="text-blue-100">Manage your account settings and preferences</p>
</div>
{/* Success/Error Messages */}
{successMessage && (
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="flex items-center">
<svg className="w-5 h-5 text-green-600 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm font-medium text-green-800">{successMessage}</span>
</div>
</div>
)}
{errorMessage && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-center">
<svg className="w-5 h-5 text-red-600 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clipRule="evenodd"
/>
</svg>
<span className="text-sm font-medium text-red-800">{errorMessage}</span>
</div>
</div>
)}
{/* User Info Card */}
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center space-x-4">
<div className="w-16 h-16 bg-blue-600 rounded-full flex items-center justify-center text-white text-2xl font-bold">
{user?.firstName?.[0]}
{user?.lastName?.[0]}
</div>
<div>
<h2 className="text-2xl font-bold text-gray-900">
{user?.firstName} {user?.lastName}
</h2>
<p className="text-gray-600">{user?.email}</p>
<div className="flex items-center space-x-2 mt-2">
<span className="px-3 py-1 text-xs font-medium text-blue-800 bg-blue-100 rounded-full">
{user?.role}
</span>
<span className="px-3 py-1 text-xs font-medium text-green-800 bg-green-100 rounded-full">
Active
</span>
</div>
</div>
</div>
</div>
{/* Tabs */}
<div className="bg-white rounded-lg shadow">
<div className="border-b">
<nav className="flex space-x-8 px-6" aria-label="Tabs">
<button
onClick={() => setActiveTab('profile')}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab === 'profile'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Profile Information
</button>
<button
onClick={() => setActiveTab('password')}
className={`py-4 px-1 border-b-2 font-medium text-sm ${
activeTab === 'password'
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Change Password
</button>
</nav>
</div>
<div className="p-6">
{activeTab === 'profile' ? (
<form onSubmit={profileForm.handleSubmit(handleProfileSubmit)} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* First Name */}
<div>
<label
htmlFor="firstName"
className="block text-sm font-medium text-gray-700 mb-2"
>
First Name
</label>
<input
{...profileForm.register('firstName')}
type="text"
id="firstName"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
{profileForm.formState.errors.firstName && (
<p className="mt-1 text-sm text-red-600">
{profileForm.formState.errors.firstName.message}
</p>
)}
</div>
{/* Last Name */}
<div>
<label
htmlFor="lastName"
className="block text-sm font-medium text-gray-700 mb-2"
>
Last Name
</label>
<input
{...profileForm.register('lastName')}
type="text"
id="lastName"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
{profileForm.formState.errors.lastName && (
<p className="mt-1 text-sm text-red-600">
{profileForm.formState.errors.lastName.message}
</p>
)}
</div>
</div>
{/* Email */}
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2">
Email Address
</label>
<input
{...profileForm.register('email')}
type="email"
id="email"
disabled
className="w-full px-4 py-2 border border-gray-300 rounded-lg bg-gray-50 text-gray-500 cursor-not-allowed"
/>
<p className="mt-1 text-xs text-gray-500">Email cannot be changed</p>
</div>
{/* Submit Button */}
<div className="flex justify-end">
<button
type="submit"
disabled={updateProfileMutation.isPending}
className="px-6 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
{updateProfileMutation.isPending ? 'Saving...' : 'Save Changes'}
</button>
</div>
</form>
) : (
<form onSubmit={passwordForm.handleSubmit(handlePasswordSubmit)} className="space-y-6">
{/* Current Password */}
<div>
<label
htmlFor="currentPassword"
className="block text-sm font-medium text-gray-700 mb-2"
>
Current Password
</label>
<input
{...passwordForm.register('currentPassword')}
type="password"
id="currentPassword"
autoComplete="current-password"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
{passwordForm.formState.errors.currentPassword && (
<p className="mt-1 text-sm text-red-600">
{passwordForm.formState.errors.currentPassword.message}
</p>
)}
</div>
{/* New Password */}
<div>
<label
htmlFor="newPassword"
className="block text-sm font-medium text-gray-700 mb-2"
>
New Password
</label>
<input
{...passwordForm.register('newPassword')}
type="password"
id="newPassword"
autoComplete="new-password"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
{passwordForm.formState.errors.newPassword && (
<p className="mt-1 text-sm text-red-600">
{passwordForm.formState.errors.newPassword.message}
</p>
)}
<p className="mt-1 text-xs text-gray-500">
Must be at least 12 characters with uppercase, lowercase, number, and special
character
</p>
</div>
{/* Confirm Password */}
<div>
<label
htmlFor="confirmPassword"
className="block text-sm font-medium text-gray-700 mb-2"
>
Confirm New Password
</label>
<input
{...passwordForm.register('confirmPassword')}
type="password"
id="confirmPassword"
autoComplete="new-password"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
{passwordForm.formState.errors.confirmPassword && (
<p className="mt-1 text-sm text-red-600">
{passwordForm.formState.errors.confirmPassword.message}
</p>
)}
</div>
{/* Submit Button */}
<div className="flex justify-end">
<button
type="submit"
disabled={updatePasswordMutation.isPending}
className="px-6 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
{updatePasswordMutation.isPending ? 'Updating...' : 'Update Password'}
</button>
</div>
</form>
)}
</div>
</div>
</div>
);
}