Files
Hotel-Booking/Frontend/src/pages/customer/ProfilePage.tsx
Iliyan Angelov 9842cc3a4a updates
2025-11-21 19:44:42 +02:00

975 lines
43 KiB
TypeScript

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<typeof profileValidationSchema>;
type PasswordFormData = yup.InferType<typeof passwordValidationSchema>;
const ProfilePage: React.FC = () => {
const { userInfo, setUser } = useAuthStore();
const { setLoading } = useGlobalLoading();
const [activeTab, setActiveTab] = useState<'profile' | 'password' | 'mfa'>('profile');
const [avatarPreview, setAvatarPreview] = useState<string | null>(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<string | null>(null);
const [mfaQrCode, setMfaQrCode] = useState<string | null>(null);
const [mfaVerificationToken, setMfaVerificationToken] = useState<string>('');
const [showBackupCodes, setShowBackupCodes] = useState<string[] | null>(null);
const [showMfaSecret, setShowMfaSecret] = useState<boolean>(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<ProfileFormData>({
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<PasswordFormData>({
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<HTMLInputElement>) => {
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 <Loading fullScreen text="Loading profile..." />;
}
if (profileError && !userInfo) {
return (
<div className="container mx-auto px-3 sm:px-4 py-4 sm:py-8">
<EmptyState
title="Unable to Load Profile"
description={profileError.message || 'Something went wrong. Please try again.'}
action={{
label: 'Retry',
onClick: refetchProfile
}}
/>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-gray-100 to-gray-50 py-4 sm:py-6 lg:py-8 px-3 sm:px-4 lg:px-8">
<div className="container mx-auto max-w-5xl">
{}
<div className="mb-6 sm:mb-8 animate-fade-in">
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-serif font-bold text-gray-900 tracking-tight mb-2">
Profile Settings
</h1>
<p className="text-sm sm:text-base text-gray-600 font-light tracking-wide">
Manage your account information and security preferences
</p>
</div>
{}
<div className="mb-4 sm:mb-6 border-b border-gray-200 overflow-x-auto">
<div className="flex space-x-4 sm:space-x-8 min-w-max">
<button
onClick={() => setActiveTab('profile')}
className={`py-3 sm:py-4 px-2 sm:px-1 border-b-2 font-medium text-xs sm:text-sm transition-colors whitespace-nowrap ${
activeTab === 'profile'
? 'border-[#d4af37] text-[#d4af37]'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
<User className="w-4 h-4 sm:w-5 sm:h-5 inline mr-2" />
Profile Information
</button>
<button
onClick={() => setActiveTab('password')}
className={`py-3 sm:py-4 px-2 sm:px-1 border-b-2 font-medium text-xs sm:text-sm transition-colors whitespace-nowrap ${
activeTab === 'password'
? 'border-[#d4af37] text-[#d4af37]'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
<KeyRound className="w-4 h-4 sm:w-5 sm:h-5 inline mr-2" />
Change Password
</button>
<button
onClick={() => setActiveTab('mfa')}
className={`py-3 sm:py-4 px-2 sm:px-1 border-b-2 font-medium text-xs sm:text-sm transition-colors whitespace-nowrap ${
activeTab === 'mfa'
? 'border-[#d4af37] text-[#d4af37]'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
<Shield className="w-4 h-4 sm:w-5 sm:h-5 inline mr-2" />
Two-Factor Authentication
</button>
</div>
</div>
{}
{activeTab === 'profile' && (
<div className="luxury-glass rounded-sm p-4 sm:p-6 lg:p-8 border border-[#d4af37]/20 shadow-2xl animate-slide-up">
<form onSubmit={handleSubmitProfile(onSubmitProfile)} className="space-y-5 sm:space-y-6">
{}
<div className="flex flex-col sm:flex-row items-center sm:items-start gap-4 sm:gap-6 pb-5 sm:pb-6 border-b border-gray-200">
<div className="relative">
{avatarPreview || userInfo?.avatar ? (
<img
src={avatarPreview || normalizeImageUrl(userInfo?.avatar)}
alt="Profile"
className="w-20 h-20 sm:w-24 sm:h-24 rounded-full object-cover ring-4 ring-[#d4af37]/20 shadow-lg"
/>
) : (
<div className="w-20 h-20 sm:w-24 sm:h-24 rounded-full bg-gradient-to-br from-[#d4af37] to-[#c9a227] flex items-center justify-center ring-4 ring-[#d4af37]/20 shadow-lg">
<User className="w-10 h-10 sm:w-12 sm:h-12 text-white" />
</div>
)}
<label
htmlFor="avatar-upload"
className="absolute bottom-0 right-0 p-2 bg-gradient-to-br from-[#d4af37] to-[#c9a227] rounded-full cursor-pointer hover:from-[#f5d76e] hover:to-[#d4af37] transition-all duration-300 shadow-lg hover:shadow-xl hover:shadow-[#d4af37]/40 hover:-translate-y-0.5 active:translate-y-0"
>
<Camera className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-white" />
<input
id="avatar-upload"
type="file"
accept="image/*"
onChange={handleAvatarChange}
className="hidden"
/>
</label>
</div>
</div>
<div>
<label className="block text-sm sm:text-base font-medium text-gray-700 mb-2">
<User className="w-4 h-4 sm:w-5 sm:h-5 inline mr-2 text-[#d4af37]" />
Full Name
<span className="text-red-500 ml-1">*</span>
</label>
<input
{...registerProfile('name')}
type="text"
className={`luxury-input text-sm sm:text-base ${
profileErrors.name ? 'border-red-300 focus:ring-red-500 focus:border-red-500' : ''
}`}
placeholder="Enter your full name"
/>
{profileErrors.name && (
<p className="mt-1.5 text-xs sm:text-sm text-red-600 font-light flex items-center gap-1.5">
<AlertCircle className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
{profileErrors.name.message}
</p>
)}
</div>
{}
<div>
<label className="block text-sm sm:text-base font-medium text-gray-700 mb-2">
<Mail className="w-4 h-4 sm:w-5 sm:h-5 inline mr-2 text-[#d4af37]" />
Email Address
<span className="text-red-500 ml-1">*</span>
</label>
<input
{...registerProfile('email')}
type="email"
className={`luxury-input text-sm sm:text-base ${
profileErrors.email ? 'border-red-300 focus:ring-red-500 focus:border-red-500' : ''
}`}
placeholder="Enter your email"
/>
{profileErrors.email && (
<p className="mt-1.5 text-xs sm:text-sm text-red-600 font-light flex items-center gap-1.5">
<AlertCircle className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
{profileErrors.email.message}
</p>
)}
</div>
{}
<div>
<label className="block text-sm sm:text-base font-medium text-gray-700 mb-2">
<Phone className="w-4 h-4 sm:w-5 sm:h-5 inline mr-2 text-[#d4af37]" />
Phone Number
<span className="text-red-500 ml-1">*</span>
</label>
<input
{...registerProfile('phone')}
type="tel"
className={`luxury-input text-sm sm:text-base ${
profileErrors.phone ? 'border-red-300 focus:ring-red-500 focus:border-red-500' : ''
}`}
placeholder="Enter your phone number"
/>
{profileErrors.phone && (
<p className="mt-1.5 text-xs sm:text-sm text-red-600 font-light flex items-center gap-1.5">
<AlertCircle className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
{profileErrors.phone.message}
</p>
)}
</div>
{}
<div className="flex justify-end pt-4 sm:pt-5 border-t border-gray-200">
<button
type="submit"
className="btn-luxury-primary flex items-center space-x-2 px-4 sm:px-6 py-2.5 sm:py-3 rounded-sm font-semibold text-xs sm:text-sm relative"
>
<span className="absolute inset-0 bg-gradient-to-r from-white/0 via-white/20 to-white/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700"></span>
<Save className="w-4 h-4 sm:w-5 sm:h-5 relative z-10" />
<span className="relative z-10">Save Changes</span>
</button>
</div>
</form>
</div>
)}
{}
{activeTab === 'password' && (
<div className="luxury-glass rounded-sm p-4 sm:p-6 lg:p-8 border border-[#d4af37]/20 shadow-2xl animate-slide-up">
<form onSubmit={handleSubmitPassword(onSubmitPassword)} className="space-y-5 sm:space-y-6">
{}
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200/60 rounded-sm p-4 sm:p-5">
<div className="flex items-start gap-3">
<div className="p-1.5 sm:p-2 bg-blue-100 rounded-full flex-shrink-0">
<KeyRound className="w-4 h-4 sm:w-5 sm:h-5 text-blue-600" />
</div>
<div>
<h4 className="text-sm sm:text-base font-semibold text-blue-900 mb-1.5">
Password Requirements
</h4>
<ul className="text-xs sm:text-sm text-blue-700 space-y-1 font-light">
<li className="flex items-start gap-2">
<span className="text-blue-500 mt-0.5"></span>
<span>At least 6 characters long</span>
</li>
<li className="flex items-start gap-2">
<span className="text-blue-500 mt-0.5"></span>
<span>Use a combination of letters and numbers for better security</span>
</li>
</ul>
</div>
</div>
</div>
{}
<div>
<label className="block text-sm sm:text-base font-medium text-gray-700 mb-2">
<Lock className="w-4 h-4 sm:w-5 sm:h-5 inline mr-2 text-[#d4af37]" />
Current Password
<span className="text-red-500 ml-1">*</span>
</label>
<div className="relative">
<input
{...registerPassword('currentPassword')}
type={showPassword.current ? 'text' : 'password'}
className={`luxury-input text-sm sm:text-base pr-10 sm:pr-12 ${
passwordErrors.currentPassword ? 'border-red-300 focus:ring-red-500 focus:border-red-500' : ''
}`}
placeholder="Enter your current password"
/>
<button
type="button"
onClick={() => setShowPassword({ ...showPassword, current: !showPassword.current })}
className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-[#d4af37] transition-colors"
>
{showPassword.current ? (
<EyeOff className="w-4 h-4 sm:w-5 sm:h-5" />
) : (
<Eye className="w-4 h-4 sm:w-5 sm:h-5" />
)}
</button>
</div>
{passwordErrors.currentPassword && (
<p className="mt-1.5 text-xs sm:text-sm text-red-600 font-light flex items-center gap-1.5">
<AlertCircle className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
{passwordErrors.currentPassword.message}
</p>
)}
</div>
{}
<div>
<label className="block text-sm sm:text-base font-medium text-gray-700 mb-2">
<Lock className="w-4 h-4 sm:w-5 sm:h-5 inline mr-2 text-[#d4af37]" />
New Password
<span className="text-red-500 ml-1">*</span>
</label>
<div className="relative">
<input
{...registerPassword('newPassword')}
type={showPassword.new ? 'text' : 'password'}
className={`luxury-input text-sm sm:text-base pr-10 sm:pr-12 ${
passwordErrors.newPassword ? 'border-red-300 focus:ring-red-500 focus:border-red-500' : ''
}`}
placeholder="Enter your new password"
/>
<button
type="button"
onClick={() => setShowPassword({ ...showPassword, new: !showPassword.new })}
className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-[#d4af37] transition-colors"
>
{showPassword.new ? (
<EyeOff className="w-4 h-4 sm:w-5 sm:h-5" />
) : (
<Eye className="w-4 h-4 sm:w-5 sm:h-5" />
)}
</button>
</div>
{passwordErrors.newPassword && (
<p className="mt-1.5 text-xs sm:text-sm text-red-600 font-light flex items-center gap-1.5">
<AlertCircle className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
{passwordErrors.newPassword.message}
</p>
)}
</div>
{}
<div>
<label className="block text-sm sm:text-base font-medium text-gray-700 mb-2">
<Lock className="w-4 h-4 sm:w-5 sm:h-5 inline mr-2 text-[#d4af37]" />
Confirm New Password
<span className="text-red-500 ml-1">*</span>
</label>
<div className="relative">
<input
{...registerPassword('confirmPassword')}
type={showPassword.confirm ? 'text' : 'password'}
className={`luxury-input text-sm sm:text-base pr-10 sm:pr-12 ${
passwordErrors.confirmPassword ? 'border-red-300 focus:ring-red-500 focus:border-red-500' : ''
}`}
placeholder="Confirm your new password"
/>
<button
type="button"
onClick={() => setShowPassword({ ...showPassword, confirm: !showPassword.confirm })}
className="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-[#d4af37] transition-colors"
>
{showPassword.confirm ? (
<EyeOff className="w-4 h-4 sm:w-5 sm:h-5" />
) : (
<Eye className="w-4 h-4 sm:w-5 sm:h-5" />
)}
</button>
</div>
{passwordErrors.confirmPassword && (
<p className="mt-1.5 text-xs sm:text-sm text-red-600 font-light flex items-center gap-1.5">
<AlertCircle className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
{passwordErrors.confirmPassword.message}
</p>
)}
</div>
{}
<div className="flex justify-end pt-4 sm:pt-5 border-t border-gray-200">
<button
type="submit"
className="btn-luxury-primary flex items-center space-x-2 px-4 sm:px-6 py-2.5 sm:py-3 rounded-sm font-semibold text-xs sm:text-sm relative"
>
<span className="absolute inset-0 bg-gradient-to-r from-white/0 via-white/20 to-white/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700"></span>
<Lock className="w-4 h-4 sm:w-5 sm:h-5 relative z-10" />
<span className="relative z-10">Change Password</span>
</button>
</div>
</form>
</div>
)}
{}
{activeTab === 'mfa' && (
<div className="luxury-glass rounded-sm p-4 sm:p-6 lg:p-8 border border-[#d4af37]/20 shadow-2xl animate-slide-up space-y-5 sm:space-y-6">
{}
<div>
<h2 className="text-xl sm:text-2xl font-serif font-semibold text-gray-900 mb-2 flex items-center gap-2">
<Shield className="w-5 h-5 sm:w-6 sm:h-6 text-[#d4af37]" />
Two-Factor Authentication
</h2>
<p className="text-xs sm:text-sm text-gray-600 font-light leading-relaxed">
Add an extra layer of security to your account by requiring a verification code in addition to your password.
</p>
</div>
{mfaStatus?.mfa_enabled ? (
<div className="space-y-5 sm:space-y-6">
{}
<div className="bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200/60 rounded-sm p-4 sm:p-5 shadow-lg">
<div className="flex items-start sm:items-center gap-3 sm:gap-4">
<div className="p-2 sm:p-2.5 bg-green-100 rounded-full flex-shrink-0">
<ShieldCheck className="w-5 h-5 sm:w-6 sm:h-6 text-green-600" />
</div>
<div className="flex-1">
<h3 className="text-base sm:text-lg font-semibold text-green-900 mb-1">MFA is Enabled</h3>
<p className="text-xs sm:text-sm text-green-700 font-light mb-1.5">
Your account is protected with two-factor authentication.
</p>
<p className="text-xs text-green-600 font-medium">
Remaining backup codes: <span className="font-semibold">{mfaStatus.backup_codes_count}</span>
</p>
</div>
</div>
</div>
{}
{showBackupCodes && (
<div className="bg-gradient-to-r from-yellow-50 to-amber-50 border border-yellow-200/60 rounded-sm p-4 sm:p-5 shadow-lg">
<div className="mb-4">
<h3 className="font-semibold text-yellow-900 mb-2 flex items-center gap-2 text-sm sm:text-base">
<AlertCircle className="w-5 h-5 text-yellow-600" />
Save Your Backup Codes
</h3>
<p className="text-xs sm:text-sm text-yellow-700 font-light leading-relaxed">
Store these codes in a safe place. Each code can only be used once.
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 sm:gap-3 mb-4">
{showBackupCodes.map((code, index) => (
<div
key={index}
className="bg-white border border-yellow-300/60 rounded-sm px-3 sm:px-4 py-2 sm:py-2.5 font-mono text-xs sm:text-sm flex items-center justify-between shadow-sm hover:shadow-md transition-shadow"
>
<span className="text-gray-900 font-medium">{code}</span>
<button
onClick={() => {
navigator.clipboard.writeText(code);
toast.success('Code copied!');
}}
className="text-yellow-600 hover:text-yellow-800 transition-colors p-1 hover:bg-yellow-50 rounded"
>
<Copy className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
</button>
</div>
))}
</div>
<button
onClick={() => setShowBackupCodes(null)}
className="text-xs sm:text-sm text-yellow-700 hover:text-yellow-900 underline font-medium transition-colors"
>
I've saved these codes
</button>
</div>
)}
{}
<div className="bg-white border border-gray-200 rounded-sm p-4 sm:p-5">
<h3 className="text-sm sm:text-base font-semibold text-gray-900 mb-2 flex items-center gap-2">
<RefreshCw className="w-4 h-4 sm:w-5 sm:h-5 text-[#d4af37]" />
Backup Codes
</h3>
<p className="text-xs sm:text-sm text-gray-600 font-light mb-4 leading-relaxed">
If you lose access to your authenticator app, you can use backup codes to access your account.
</p>
<button
onClick={async () => {
try {
setLoading(true, 'Regenerating backup codes...');
const response = await authService.regenerateBackupCodes();
if (response.status === 'success' && response.data?.backup_codes) {
setShowBackupCodes(response.data.backup_codes);
await fetchMFAStatus();
toast.success('Backup codes regenerated. Please save them!');
}
} catch (error: any) {
toast.error(error.response?.data?.detail || 'Failed to regenerate backup codes');
} finally {
setLoading(false);
}
}}
className="flex items-center gap-2 px-4 sm:px-5 py-2.5 sm:py-3 bg-gradient-to-r from-yellow-50 to-amber-50 border border-yellow-200/60 rounded-sm text-sm sm:text-base font-medium text-yellow-900 hover:from-yellow-100 hover:to-amber-100 transition-all duration-300 shadow-sm hover:shadow-md hover:-translate-y-0.5 active:translate-y-0"
>
<RefreshCw className="w-4 h-4 sm:w-5 sm:h-5" />
Regenerate Backup Codes
</button>
</div>
{}
<div className="border-t border-gray-200 pt-5 sm:pt-6">
<h3 className="text-sm sm:text-base font-semibold text-red-900 mb-2 flex items-center gap-2">
<ShieldOff className="w-4 h-4 sm:w-5 sm:h-5 text-red-600" />
Disable MFA
</h3>
<p className="text-xs sm:text-sm text-gray-600 font-light mb-4 leading-relaxed">
Disabling MFA will reduce the security of your account. We recommend keeping it enabled.
</p>
<button
onClick={async () => {
if (!window.confirm('Are you sure you want to disable MFA? This will reduce your account security.')) {
return;
}
try {
setLoading(true, 'Disabling MFA...');
const response = await authService.disableMFA();
if (response.status === 'success') {
await fetchMFAStatus();
toast.success('MFA disabled successfully');
}
} catch (error: any) {
toast.error(error.response?.data?.detail || 'Failed to disable MFA');
} finally {
setLoading(false);
}
}}
className="flex items-center gap-2 px-4 sm:px-5 py-2.5 sm:py-3 bg-gradient-to-r from-red-50 to-rose-50 border border-red-200/60 rounded-sm text-sm sm:text-base font-medium text-red-900 hover:from-red-100 hover:to-rose-100 transition-all duration-300 shadow-sm hover:shadow-md hover:-translate-y-0.5 active:translate-y-0"
>
<ShieldOff className="w-4 h-4 sm:w-5 sm:h-5" />
Disable MFA
</button>
</div>
</div>
) : (
<div className="space-y-5 sm:space-y-6">
{!mfaSecret ? (
<div>
<button
onClick={async () => {
try {
setLoading(true, 'Initializing MFA...');
const response = await authService.initMFA();
if (response.status === 'success' && response.data) {
setMfaSecret(response.data.secret || '');
setMfaQrCode(response.data.qr_code || '');
}
} catch (error: any) {
toast.error(error.response?.data?.detail || 'Failed to initialize MFA');
} finally {
setLoading(false);
}
}}
className="btn-luxury-primary w-full sm:w-auto flex items-center justify-center gap-2 px-6 sm:px-8 py-3 sm:py-3.5 rounded-sm font-semibold text-sm sm:text-base relative"
>
<span className="absolute inset-0 bg-gradient-to-r from-white/0 via-white/20 to-white/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700"></span>
<Shield className="w-5 h-5 sm:w-6 sm:h-6 relative z-10" />
<span className="relative z-10">Enable Two-Factor Authentication</span>
</button>
</div>
) : (
<div className="space-y-5 sm:space-y-6">
{}
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200/60 rounded-sm p-4 sm:p-5 shadow-lg">
<h3 className="font-semibold text-blue-900 mb-2 text-sm sm:text-base flex items-center gap-2">
<CheckCircle className="w-4 h-4 sm:w-5 sm:h-5" />
Step 1: Scan QR Code
</h3>
<p className="text-xs sm:text-sm text-blue-700 font-light mb-4 leading-relaxed">
Use your authenticator app (Google Authenticator, Authy, etc.) to scan this QR code.
</p>
{mfaQrCode && (
<div className="flex justify-center mb-4">
<img
src={mfaQrCode}
alt="MFA QR Code"
className="border-4 border-white rounded-lg shadow-xl max-w-[200px] sm:max-w-[250px]"
/>
</div>
)}
<div className="bg-white border border-blue-200/60 rounded-sm p-3 sm:p-4 shadow-sm">
<div className="flex items-center justify-between mb-2">
<span className="text-xs sm:text-sm font-medium text-gray-700">Manual Entry Key:</span>
<button
onClick={() => setShowMfaSecret(!showMfaSecret)}
className="text-blue-600 hover:text-blue-800 transition-colors p-1 hover:bg-blue-50 rounded"
>
{showMfaSecret ? (
<EyeOff className="w-4 h-4 sm:w-5 sm:h-5" />
) : (
<Eye className="w-4 h-4 sm:w-5 sm:h-5" />
)}
</button>
</div>
<div className="flex items-center gap-2">
<code className="flex-1 font-mono text-xs sm:text-sm bg-gray-50 p-2 sm:p-2.5 rounded border border-gray-200 text-gray-900 break-all">
{showMfaSecret ? mfaSecret : ''}
</code>
<button
onClick={() => {
navigator.clipboard.writeText(mfaSecret || '');
toast.success('Secret key copied!');
}}
className="px-3 sm:px-4 py-2 bg-gradient-to-r from-blue-100 to-indigo-100 text-blue-700 rounded-sm hover:from-blue-200 hover:to-indigo-200 transition-all duration-300 shadow-sm hover:shadow-md hover:-translate-y-0.5 active:translate-y-0"
>
<Copy className="w-4 h-4 sm:w-5 sm:h-5" />
</button>
</div>
</div>
</div>
{}
<div className="bg-white border border-gray-200 rounded-sm p-4 sm:p-5">
<h3 className="font-semibold text-gray-900 mb-2 text-sm sm:text-base flex items-center gap-2">
<KeyRound className="w-4 h-4 sm:w-5 sm:h-5 text-[#d4af37]" />
Step 2: Verify Setup
</h3>
<p className="text-xs sm:text-sm text-gray-600 font-light mb-4 leading-relaxed">
Enter the 6-digit code from your authenticator app to complete the setup.
</p>
<div className="space-y-4">
<div>
<label className="block text-sm sm:text-base font-medium text-gray-700 mb-2">
Verification Code
</label>
<input
type="text"
value={mfaVerificationToken}
onChange={(e) => 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"
/>
</div>
<div className="flex flex-col sm:flex-row gap-3">
<button
onClick={async () => {
if (!mfaVerificationToken || mfaVerificationToken.length !== 6) {
toast.error('Please enter a valid 6-digit code');
return;
}
try {
setLoading(true, 'Enabling MFA...');
const response = await authService.enableMFA(mfaSecret || '', mfaVerificationToken);
if (response.status === 'success' && response.data?.backup_codes) {
setShowBackupCodes(response.data.backup_codes);
await fetchMFAStatus();
setMfaSecret(null);
setMfaQrCode(null);
setMfaVerificationToken('');
toast.success('MFA enabled successfully!');
}
} catch (error: any) {
toast.error(error.response?.data?.detail || 'Invalid verification code');
} finally {
setLoading(false);
}
}}
className="btn-luxury-primary flex-1 flex items-center justify-center gap-2 px-4 sm:px-6 py-2.5 sm:py-3 rounded-sm font-semibold text-sm sm:text-base relative"
>
<span className="absolute inset-0 bg-gradient-to-r from-white/0 via-white/20 to-white/0 translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700"></span>
<ShieldCheck className="w-4 h-4 sm:w-5 sm:h-5 relative z-10" />
<span className="relative z-10">Verify & Enable</span>
</button>
<button
onClick={() => {
setMfaSecret(null);
setMfaQrCode(null);
setMfaVerificationToken('');
setShowMfaSecret(false);
}}
className="px-4 sm:px-6 py-2.5 sm:py-3 border border-gray-300 text-gray-700 rounded-sm hover:bg-gray-50 transition-all duration-300 font-medium text-sm sm:text-base shadow-sm hover:shadow-md hover:-translate-y-0.5 active:translate-y-0"
>
Cancel
</button>
</div>
</div>
</div>
</div>
)}
</div>
)}
</div>
)}
</div>
</div>
);
};
export default ProfilePage;