'use client' import { useState, useEffect } from 'react' import { useAuth } from '@/lib/auth/context' import { motion, AnimatePresence } from 'framer-motion' import { PersonIcon, LockClosedIcon, EnvelopeClosedIcon, CheckIcon, ExclamationTriangleIcon, Cross2Icon, GearIcon, MobileIcon, FileTextIcon, CopyIcon } from '@radix-ui/react-icons' // @ts-ignore import { Button, Input } from '@ciphera-net/ui' import { PasswordInput } from '@ciphera-net/ui' import { toast } from 'sonner' import api from '@/lib/api/client' import { deriveAuthKey } from '@/lib/crypto/password' import { deleteAccount, getUserSessions, revokeSession, type Session } from '@/lib/api/user' import { setup2FA, verify2FA, disable2FA, regenerateRecoveryCodes, Setup2FAResponse } from '@/lib/api/2fa' import Image from 'next/image' export default function ProfileSettings() { const { user, refresh, logout } = useAuth() const [activeTab, setActiveTab] = useState<'profile' | 'security' | 'preferences'>('profile') // Profile State const [email, setEmail] = useState(user?.email || '') const [isEmailDirty, setIsEmailDirty] = useState(false) const [loadingProfile, setLoadingProfile] = useState(false) // Update email when user data loads useEffect(() => { if (user?.email) { setEmail(user.email) setIsEmailDirty(false) } }, [user?.email]) // Email Password Prompt State const [showEmailPasswordPrompt, setShowEmailPasswordPrompt] = useState(false) const [emailConfirmPassword, setEmailConfirmPassword] = useState('') // Security State const [currentPassword, setCurrentPassword] = useState('') const [newPassword, setNewPassword] = useState('') const [confirmPassword, setConfirmPassword] = useState('') const [loadingSecurity, setLoadingSecurity] = useState(false) const [securityError, setSecurityError] = useState(null) // 2FA State const [show2FASetup, setShow2FASetup] = useState(false) const [twoFAData, setTwoFAData] = useState(null) const [twoFACode, setTwoFACode] = useState('') const [loading2FA, setLoading2FA] = useState(false) // Recovery Codes State const [recoveryCodes, setRecoveryCodes] = useState([]) const [showRecoveryCodes, setShowRecoveryCodes] = useState(false) const [showRegenerateConfirm, setShowRegenerateConfirm] = useState(false) // Account Deletion State const [showDeletePrompt, setShowDeletePrompt] = useState(false) const [deletePassword, setDeletePassword] = useState('') const [loadingDelete, setLoadingDelete] = useState(false) const [deleteError, setDeleteError] = useState(null) // Sessions State const [sessions, setSessions] = useState([]) const [loadingSessions, setLoadingSessions] = useState(false) const [revokingSessionId, setRevokingSessionId] = useState(null) const handleUpdateProfile = async (e: React.FormEvent) => { e.preventDefault() if (!email || email === user?.email) return // Show password prompt if not visible if (!showEmailPasswordPrompt) { setShowEmailPasswordPrompt(true) return } // Actual submission with password setLoadingProfile(true) try { if (!user?.email) throw new Error('User email not found') const currentDerivedKey = await deriveAuthKey(emailConfirmPassword, user.email) const newDerivedKey = await deriveAuthKey(emailConfirmPassword, email) // Derive with NEW email await api('/auth/user/email', { method: 'PUT', body: JSON.stringify({ email: email, current_password: currentDerivedKey, new_derived_key: newDerivedKey }) }) toast.success('Profile updated successfully. Please verify your new email.') refresh() setShowEmailPasswordPrompt(false) setEmailConfirmPassword('') setIsEmailDirty(false) } catch (err: any) { toast.error(err.message || 'Failed to update profile') console.error(err) } finally { setLoadingProfile(false) } } const handleUpdatePassword = async (e: React.FormEvent) => { e.preventDefault() setSecurityError(null) if (newPassword !== confirmPassword) { setSecurityError('New passwords do not match') return } if (newPassword.length < 8) { setSecurityError('Password must be at least 8 characters') return } setLoadingSecurity(true) try { if (!user?.email) throw new Error('User email not found') const currentDerivedKey = await deriveAuthKey(currentPassword, user.email) const newDerivedKey = await deriveAuthKey(newPassword, user.email) await api('/auth/user/password', { method: 'PUT', body: JSON.stringify({ current_password: currentDerivedKey, new_password: newDerivedKey }) }) toast.success('Password updated successfully') setCurrentPassword('') setNewPassword('') setConfirmPassword('') } catch (err: any) { toast.error(err.message || 'Failed to update password') setSecurityError(err.message || 'Failed to update password') } finally { setLoadingSecurity(false) } } const handleDeleteAccount = async (e: React.FormEvent) => { e.preventDefault() setDeleteError(null) setLoadingDelete(true) try { if (!user?.email) throw new Error('User email not found') // 1. Derive key to verify ownership const derivedKey = await deriveAuthKey(deletePassword, user.email) // 2. Delete account await deleteAccount(derivedKey) toast.success('Account deleted successfully') // 3. Logout and redirect logout() } catch (err: any) { setDeleteError(err.message || 'Failed to delete account') toast.error(err.message || 'Failed to delete account') } finally { setLoadingDelete(false) } } const handleStart2FASetup = async () => { setLoading2FA(true) try { const data = await setup2FA() setTwoFAData(data) setShow2FASetup(true) } catch (err: any) { toast.error('Failed to start 2FA setup') } finally { setLoading2FA(false) } } const handleVerify2FA = async (e: React.FormEvent) => { e.preventDefault() setLoading2FA(true) try { const res = await verify2FA(twoFACode) toast.success('2FA enabled successfully') setShow2FASetup(false) setTwoFAData(null) setTwoFACode('') if (res.recovery_codes) { setRecoveryCodes(res.recovery_codes) setShowRecoveryCodes(true) } await refresh() } catch (err: any) { toast.error('Invalid code. Please try again.') } finally { setLoading2FA(false) } } const handleDisable2FA = async () => { setLoading2FA(true) try { await disable2FA() toast.success('2FA disabled successfully') await refresh() } catch (err: any) { toast.error('Failed to disable 2FA') } finally { setLoading2FA(false) } } const handleRegenerateCodes = async () => { setLoading2FA(true) try { const res = await regenerateRecoveryCodes() setRecoveryCodes(res.recovery_codes) setShowRecoveryCodes(true) setShowRegenerateConfirm(false) toast.success('Recovery codes regenerated') } catch (err: any) { toast.error('Failed to regenerate codes') } finally { setLoading2FA(false) } } const copyCodes = () => { navigator.clipboard.writeText(recoveryCodes.join('\n')) toast.success('Codes copied to clipboard') } // Load sessions const loadSessions = async () => { setLoadingSessions(true) try { const data = await getUserSessions() setSessions(data.sessions) } catch (err: any) { toast.error('Failed to load sessions') console.error(err) } finally { setLoadingSessions(false) } } // Load sessions when security tab is active useEffect(() => { if (activeTab === 'security') { loadSessions() } }, [activeTab]) const handleRevokeSession = async (sessionId: string) => { const session = sessions.find(s => s.id === sessionId) const isCurrentSession = session?.is_current if (isCurrentSession) { if (!confirm('This will log you out of this device. Continue?')) { return } } setRevokingSessionId(sessionId) try { await revokeSession(sessionId) toast.success('Session revoked successfully') // If current session was revoked, logout immediately if (isCurrentSession) { setTimeout(() => { logout() }, 1000) } else { // Reload sessions list for other sessions await loadSessions() } } catch (err: any) { toast.error('Failed to revoke session') console.error(err) } finally { setRevokingSessionId(null) } } // Parse user agent to get browser and device info const parseUserAgent = (ua: string) => { let browser = 'Unknown Browser' let device = 'Unknown Device' let os = 'Unknown OS' // Browser detection if (ua.includes('Chrome') && !ua.includes('Edg')) { browser = 'Chrome' } else if (ua.includes('Firefox')) { browser = 'Firefox' } else if (ua.includes('Safari') && !ua.includes('Chrome')) { browser = 'Safari' } else if (ua.includes('Edg')) { browser = 'Edge' } else if (ua.includes('Opera') || ua.includes('OPR')) { browser = 'Opera' } // OS detection if (ua.includes('Windows')) { os = 'Windows' } else if (ua.includes('Mac OS X') || ua.includes('Macintosh')) { os = 'macOS' } else if (ua.includes('Linux')) { os = 'Linux' } else if (ua.includes('Android')) { os = 'Android' } else if (ua.includes('iOS') || ua.includes('iPhone') || ua.includes('iPad')) { os = 'iOS' } // Device detection if (ua.includes('Mobile') || ua.includes('Android') || ua.includes('iPhone')) { device = 'Mobile' } else if (ua.includes('Tablet') || ua.includes('iPad')) { device = 'Tablet' } else { device = 'Desktop' } return { browser, os, device } } const formatDate = (dateString: string) => { const date = new Date(dateString) return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit', }).format(date) } if (!user) return null return (

Settings

Manage your account settings and preferences.

{/* Sidebar Navigation */} {/* Content Area */}
{activeTab === 'preferences' && (

Preferences are currently not available.

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

Profile Information

Update your account details.

{ setEmail(e.target.value) setIsEmailDirty(e.target.value !== user.email) }} className="w-full pl-11 pr-4 py-3 border border-neutral-200 dark:border-neutral-800 rounded-xl bg-neutral-50/50 dark:bg-neutral-900/50 focus:bg-white dark:focus:bg-neutral-900 focus:border-brand-orange focus:ring-4 focus:ring-brand-orange/10 outline-none transition-all duration-200 dark:text-white" />

Danger Zone

Irreversible actions for your account.

Delete Account

Permanently delete your account and all data.

)} {activeTab === 'security' && (
{/* 2FA Section */}

Two-Factor Authentication

Add an extra layer of security to your account.

Authenticator App

Use an app like Google Authenticator or Authy.

{user.totp_enabled ? ( ) : ( )}
{show2FASetup && twoFAData && (
{/* QR Code */} 2FA QR Code

1. Scan QR Code

Open your authenticator app and scan the image.

2. Enter Code

setTwoFACode(e.target.value.replace(/\D/g, ''))} placeholder="000000" className="w-32 px-3 py-2 text-center tracking-widest font-mono text-lg border border-neutral-200 dark:border-neutral-800 rounded-lg bg-white dark:bg-neutral-900 focus:ring-2 focus:ring-brand-orange/20 focus:border-brand-orange outline-none dark:text-white" />

Secret: {twoFAData.secret}

)}
{user.totp_enabled && (

Recovery Codes

Generate backup codes in case you lose access to your device.

)}

Password

Update your password to keep your account secure.

{securityError && (
{securityError}
)}
setCurrentPassword(e.target.value)} placeholder="Enter current password" required />
setNewPassword(e.target.value)} placeholder="Enter new password" required /> setConfirmPassword(e.target.value)} placeholder="Confirm new password" required />

{/* Active Sessions Section */}

Active Sessions

Manage devices that are currently signed in to your account.

{loadingSessions ? (
) : sessions.length === 0 ? (
No active sessions found.
) : (
{sessions.map((session) => { const { browser, os, device } = parseUserAgent(session.user_agent) const isCurrent = session.is_current return (
{browser} on {os} {isCurrent && ( Current Session )}
{device} • {session.client_ip || 'Unknown IP'}
Signed in {formatDate(session.created_at)}
{session.expires_at && (
Expires {formatDate(session.expires_at)}
)}
{!isCurrent && ( )}
) })}
)}
)} {/* Email Password Confirmation Modal */} {showEmailPasswordPrompt && (

Confirm Change

Please enter your password to confirm changing your email to {email}.

setEmailConfirmPassword(e.target.value)} placeholder="Enter your password" required className="mb-2" />
)}
{/* Account Deletion Modal */} {showDeletePrompt && (

Delete Account?

This action is irreversible. All your data will be permanently deleted.

{deleteError && (
{deleteError}
)}
setDeletePassword(e.target.value)} placeholder="Enter your password" required className="mb-2" />
)}
{/* Recovery Codes Modal */} {showRecoveryCodes && (

Recovery Codes

Save these codes securely!

They are the only way to access your account if you lose your device. We cannot display them again.

{recoveryCodes.map(code => (
{code}
))}
)}
{/* Regenerate Confirm Modal */} {showRegenerateConfirm && (

Regenerate Codes?

This will invalidate all existing codes. Make sure you have your new codes saved immediately.

)}
) }