import { useState, useEffect } from 'react' import { useNavigate } from 'react-router-dom' import { Box, Button, Card, CardContent, Chip, Container, Dialog, DialogActions, DialogContent, DialogTitle, Snackbar, Alert, TextField, Typography, InputAdornment, IconButton, List, ListItem, ListItemIcon, ListItemText, } from '@mui/material' import { Person as PersonIcon, Lock as LockIcon, Visibility, VisibilityOff, VpnKey as MfaIcon, Save as SaveIcon, Check as CheckIcon, Close as CloseIcon, } from '@mui/icons-material' import { useAuthStore } from '../store/authStore' import { usersApi } from '../api/client' import type { User } from '../types' /** Password strength checker */ function checkPasswordStrength(password: string) { return { length: password.length >= 8, uppercase: /[A-Z]/.test(password), lowercase: /[a-z]/.test(password), digit: /[0-9]/.test(password), special: /[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]/.test(password), } } function isPasswordValid(checks: ReturnType) { return checks.length && checks.uppercase && checks.lowercase && checks.digit && checks.special } export default function ProfilePage() { const navigate = useNavigate() const { user, setUser } = useAuthStore() // ── Profile state ──────────────────────────────────────────────────────── const [me, setMe] = useState(null) const [displayName, setDisplayName] = useState('') const [email, setEmail] = useState('') const [loadingProfile, setLoadingProfile] = useState(true) const [savingProfile, setSavingProfile] = useState(false) // ── Password state ────────────────────────────────────────────────────── const [currentPw, setCurrentPw] = useState('') const [newPw, setNewPw] = useState('') const [confirmPw, setConfirmPw] = useState('') const [showCurrentPw, setShowCurrentPw] = useState(false) const [showNewPw, setShowNewPw] = useState(false) const [showConfirmPw, setShowConfirmPw] = useState(false) const [changingPw, setChangingPw] = useState(false) // ── MFA state ──────────────────────────────────────────────────────────── const [mfaDisableOpen, setMfaDisableOpen] = useState(false) const [mfaDisablePw, setMfaDisablePw] = useState('') const [disablingMfa, setDisablingMfa] = useState(false) // ── Snackbar state ────────────────────────────────────────────────────── const [snack, setSnack] = useState<{ open: boolean; severity: 'success' | 'error'; message: string }>({ open: false, severity: 'success', message: '', }) const showSnack = (severity: 'success' | 'error', message: string) => setSnack({ open: true, severity, message }) const pwChecks = checkPasswordStrength(newPw) const pwValid = isPasswordValid(pwChecks) const pwMismatch = !!(newPw && confirmPw && newPw !== confirmPw) // ── Load current user on mount ────────────────────────────────────────── useEffect(() => { ;(async () => { try { const { data } = await usersApi.getMe() setMe(data) setDisplayName(data.display_name || '') setEmail(data.email || '') } catch { showSnack('error', 'Failed to load profile') } finally { setLoadingProfile(false) } })() }, []) // ── Save profile ──────────────────────────────────────────────────────── const handleSaveProfile = async () => { if (!me) return setSavingProfile(true) try { const { data } = await usersApi.update(me.id, { display_name: displayName, email }) setMe(data) setUser(data) showSnack('success', 'Profile updated') } catch { showSnack('error', 'Failed to update profile') } finally { setSavingProfile(false) } } // ── Change password ──────────────────────────────────────────────────── const handleChangePassword = async () => { if (newPw !== confirmPw) { showSnack('error', 'New passwords do not match') return } if (!pwValid) { showSnack('error', 'Password does not meet strength requirements') return } setChangingPw(true) try { await usersApi.changePassword({ current_password: currentPw, new_password: newPw }) setCurrentPw('') setNewPw('') setConfirmPw('') showSnack('success', 'Password changed successfully') } catch { showSnack('error', 'Failed to change password') } finally { setChangingPw(false) } } // ── Disable MFA ───────────────────────────────────────────────────────── const handleDisableMfa = async () => { setDisablingMfa(true) try { await usersApi.disableMfa(mfaDisablePw) if (me) setMe({ ...me, mfa_enabled: false }) // Also update authStore user if (user) setUser({ ...user, mfa_enabled: false }) setMfaDisablePw('') setMfaDisableOpen(false) showSnack('success', 'MFA disabled') } catch { showSnack('error', 'Failed to disable MFA') } finally { setDisablingMfa(false) } } if (loadingProfile) { return ( Loading profile… ) } return ( My Profile {/* ── Profile Section ──────────────────────────────────────────────── */} Profile Information setDisplayName(e.target.value)} /> setEmail(e.target.value)} /> Role: {/* ── Password Section ─────────────────────────────────────────────── */} Change Password setCurrentPw(e.target.value)} InputProps={{ endAdornment: ( setShowCurrentPw(!showCurrentPw)} edge="end"> {showCurrentPw ? : } ), }} /> setNewPw(e.target.value)} error={pwMismatch} InputProps={{ endAdornment: ( setShowNewPw(!showNewPw)} edge="end"> {showNewPw ? : } ), }} /> {newPw && ( {pwChecks.length ? : } {pwChecks.uppercase ? : } {pwChecks.lowercase ? : } {pwChecks.digit ? : } {pwChecks.special ? : } )} setConfirmPw(e.target.value)} error={pwMismatch} helperText={pwMismatch ? 'Passwords do not match' : ''} InputProps={{ endAdornment: ( setShowConfirmPw(!showConfirmPw)} edge="end"> {showConfirmPw ? : } ), }} /> {/* ── MFA Section ──────────────────────────────────────────────────── */} Multi-Factor Authentication Status: {me?.mfa_enabled ? ( ) : ( )} {/* ── Disable MFA Confirmation Dialog ─────────────────────────────── */} setMfaDisableOpen(false)} maxWidth="xs" fullWidth> Disable MFA Are you sure you want to disable multi-factor authentication? This will make your account less secure. setMfaDisablePw(e.target.value)} autoFocus /> {/* ── Snackbar ─────────────────────────────────────────────────────── */} setSnack((s) => ({ ...s, open: false }))} anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} > setSnack((s) => ({ ...s, open: false }))} variant="filled" > {snack.message} ) }