import React, { useState, useEffect } from 'react'; import { useForm } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import * as yup from 'yup'; import { User, Mail, Phone, Save, CheckCircle, AlertCircle, Lock, Camera, Shield, ShieldCheck, ShieldOff, Copy, Eye, EyeOff, RefreshCw, KeyRound } from 'lucide-react'; import { toast } from 'react-toastify'; import authService from '../../services/api/authService'; import useAuthStore from '../../store/useAuthStore'; import { Loading, EmptyState } from '../../components/common'; import { useAsync } from '../../hooks/useAsync'; import { useGlobalLoading } from '../../contexts/GlobalLoadingContext'; import { normalizeImageUrl } from '../../utils/imageUtils'; const profileValidationSchema = yup.object().shape({ name: yup .string() .required('Full name is required') .min(2, 'Full name must be at least 2 characters') .max(100, 'Full name cannot exceed 100 characters'), email: yup .string() .required('Email is required') .email('Invalid email address'), phone: yup .string() .required('Phone number is required') .matches( /^[\d\s\-\+\(\)]{5,}$/, 'Please enter a valid phone number' ), }); const passwordValidationSchema = yup.object().shape({ currentPassword: yup .string() .required('Current password is required'), newPassword: yup .string() .required('New password is required') .min(6, 'Password must be at least 6 characters'), confirmPassword: yup .string() .required('Please confirm your password') .oneOf([yup.ref('newPassword')], 'Passwords must match'), }); type ProfileFormData = yup.InferType; type PasswordFormData = yup.InferType; const ProfilePage: React.FC = () => { const { userInfo, setUser } = useAuthStore(); const { setLoading } = useGlobalLoading(); const [activeTab, setActiveTab] = useState<'profile' | 'password' | 'mfa'>('profile'); const [avatarPreview, setAvatarPreview] = useState(null); const [showPassword, setShowPassword] = useState<{ current: boolean; new: boolean; confirm: boolean; }>({ current: false, new: false, confirm: false, }); const [mfaStatus, setMfaStatus] = useState<{mfa_enabled: boolean; backup_codes_count: number} | null>(null); const [mfaSecret, setMfaSecret] = useState(null); const [mfaQrCode, setMfaQrCode] = useState(null); const [mfaVerificationToken, setMfaVerificationToken] = useState(''); const [showBackupCodes, setShowBackupCodes] = useState(null); const [showMfaSecret, setShowMfaSecret] = useState(false); const fetchProfile = async () => { const response = await authService.getProfile(); if (response.status === 'success' || response.success) { const user = response.data?.user || response.data; if (user) { setUser(user as any); return user; } } throw new Error('Failed to load profile'); }; const { data: profileData, loading: loadingProfile, error: profileError, execute: refetchProfile } = useAsync(fetchProfile, { immediate: true, onError: (error: any) => { toast.error(error.message || 'Unable to load profile'); } }); const { register: registerProfile, handleSubmit: handleSubmitProfile, formState: { errors: profileErrors }, reset: resetProfile, } = useForm({ resolver: yupResolver(profileValidationSchema), defaultValues: { name: ((userInfo && 'user' in userInfo ? userInfo.user : userInfo) as any)?.name || '', email: ((userInfo && 'user' in userInfo ? userInfo.user : userInfo) as any)?.email || '', phone: ((userInfo && 'user' in userInfo ? userInfo.user : userInfo) as any)?.phone || '', }, }); const { register: registerPassword, handleSubmit: handleSubmitPassword, formState: { errors: passwordErrors }, reset: resetPassword, } = useForm({ resolver: yupResolver(passwordValidationSchema), }); const fetchMFAStatus = async () => { try { const response = await authService.getMFAStatus(); if (response) { setMfaStatus({ mfa_enabled: response.mfa_enabled || false, backup_codes_count: response.backup_codes_count || 0, }); } } catch (error) { console.error('Failed to fetch MFA status:', error); } }; useEffect(() => { if (profileData || userInfo) { const data = profileData || (userInfo && 'user' in userInfo ? userInfo.user : userInfo); resetProfile({ name: (data as any)?.name || '', email: (data as any)?.email || '', phone: (data as any)?.phone || '', }); if ((data as any)?.avatar) { setAvatarPreview(normalizeImageUrl((data as any).avatar)); } else { setAvatarPreview(null); } } }, [profileData, userInfo, resetProfile]); useEffect(() => { if (activeTab === 'mfa') { fetchMFAStatus(); } }, [activeTab]); const onSubmitProfile = async (data: ProfileFormData) => { try { setLoading(true, 'Updating profile...'); if ('updateProfile' in authService) { const response = await (authService as any).updateProfile({ full_name: data.name, email: data.email, phone_number: data.phone, }); if (response.status === 'success' || response.success) { const updatedUser = response.data?.user || response.data; if (updatedUser) { setUser(updatedUser); toast.success('Profile updated successfully!'); refetchProfile(); } } } else { const { updateUser } = await import('../../services/api/userService'); const user = userInfo && 'user' in userInfo ? userInfo.user : userInfo; const response = await updateUser((user as any)?.id || (userInfo as any)?.id, { full_name: data.name, email: data.email, phone_number: data.phone, }); if ((response as any).success || (response as any).status === 'success') { const updatedUser = response.data?.user || response.data; if (updatedUser) { setUser({ id: updatedUser.id, name: (updatedUser as any).full_name || (updatedUser as any).name, email: updatedUser.email, phone: (updatedUser as any).phone_number || (updatedUser as any).phone, avatar: (updatedUser as any).avatar, role: updatedUser.role, }); toast.success('Profile updated successfully!'); refetchProfile(); } } } } catch (error: any) { const errorMessage = error.response?.data?.message || error.message || 'Failed to update profile'; toast.error(errorMessage); } finally { setLoading(false); } }; const onSubmitPassword = async (data: PasswordFormData) => { try { setLoading(true, 'Changing password...'); if ('updateProfile' in authService) { const response = await (authService as any).updateProfile({ currentPassword: data.currentPassword, password: data.newPassword, }); if (response.status === 'success' || response.success) { toast.success('Password changed successfully!'); resetPassword(); } } else { const { updateUser } = await import('../../services/api/userService'); const user = userInfo && 'user' in userInfo ? userInfo.user : userInfo; const response = await updateUser((user as any)?.id || (userInfo as any)?.id, { password: data.newPassword, }); if ((response as any).success || (response as any).status === 'success') { toast.success('Password changed successfully!'); resetPassword(); } } } catch (error: any) { const errorMessage = error.response?.data?.message || error.message || 'Failed to change password'; toast.error(errorMessage); } finally { setLoading(false); } }; const handleAvatarChange = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; if (!file.type.startsWith('image/')) { toast.error('Please select an image file'); return; } if (file.size > 2 * 1024 * 1024) { toast.error('Image size must be less than 2MB'); return; } const reader = new FileReader(); reader.onloadend = () => { setAvatarPreview(reader.result as string); }; reader.readAsDataURL(file); try { setLoading(true, 'Uploading avatar...'); const response = await authService.uploadAvatar(file); if (response.status === 'success' && response.data?.user) { const updatedUser = response.data.user; const avatarUrl = (response.data as any).full_url || normalizeImageUrl((updatedUser as any).avatar); setUser({ id: updatedUser.id, name: (updatedUser as any).name || (updatedUser as any).full_name, email: updatedUser.email, phone: (updatedUser as any).phone || (updatedUser as any).phone_number, avatar: avatarUrl, role: updatedUser.role, }); setAvatarPreview(avatarUrl || null); toast.success('Avatar uploaded successfully!'); refetchProfile(); } } catch (error: any) { const errorMessage = error.response?.data?.detail || error.message || 'Failed to upload avatar'; toast.error(errorMessage); setAvatarPreview(userInfo?.avatar ? normalizeImageUrl(userInfo.avatar) : null); } finally { setLoading(false); e.target.value = ''; } }; if (loadingProfile) { return ; } if (profileError && !userInfo) { return (
); } return (
{}

Profile Settings

Manage your account information and security preferences

{}
{} {activeTab === 'profile' && (
{}
{avatarPreview || userInfo?.avatar ? ( Profile ) : (
)}
{profileErrors.name && (

{profileErrors.name.message}

)}
{}
{profileErrors.email && (

{profileErrors.email.message}

)}
{}
{profileErrors.phone && (

{profileErrors.phone.message}

)}
{}
)} {} {activeTab === 'password' && (
{}

Password Requirements

  • At least 6 characters long
  • Use a combination of letters and numbers for better security
{}
{passwordErrors.currentPassword && (

{passwordErrors.currentPassword.message}

)}
{}
{passwordErrors.newPassword && (

{passwordErrors.newPassword.message}

)}
{}
{passwordErrors.confirmPassword && (

{passwordErrors.confirmPassword.message}

)}
{}
)} {} {activeTab === 'mfa' && (
{}

Two-Factor Authentication

Add an extra layer of security to your account by requiring a verification code in addition to your password.

{mfaStatus?.mfa_enabled ? (
{}

MFA is Enabled

Your account is protected with two-factor authentication.

Remaining backup codes: {mfaStatus.backup_codes_count}

{} {showBackupCodes && (

Save Your Backup Codes

Store these codes in a safe place. Each code can only be used once.

{showBackupCodes.map((code, index) => (
{code}
))}
)} {}

Backup Codes

If you lose access to your authenticator app, you can use backup codes to access your account.

{}

Disable MFA

Disabling MFA will reduce the security of your account. We recommend keeping it enabled.

) : (
{!mfaSecret ? (
) : (
{}

Step 1: Scan QR Code

Use your authenticator app (Google Authenticator, Authy, etc.) to scan this QR code.

{mfaQrCode && (
MFA QR Code
)}
Manual Entry Key:
{showMfaSecret ? mfaSecret : '••••••••••••••••'}
{}

Step 2: Verify Setup

Enter the 6-digit code from your authenticator app to complete the setup.

setMfaVerificationToken(e.target.value.replace(/\D/g, '').slice(0, 6))} placeholder="000000" maxLength={6} className="luxury-input text-center text-lg sm:text-xl tracking-widest font-mono" />
)}
)}
)}
); }; export default ProfilePage;