import React, { useState, useEffect } from 'react' import { User, Save, Lock, KeyRound, AlertTriangle, Shield, Camera, Trash2, Copy, Download, Printer } from 'lucide-react' import { useNavigate, useSearchParams } from 'react-router-dom' import { useTranslation } from '../../i18n' import { useAuthStore } from '../../store/authStore' import { useToast } from '../shared/Toast' import { authApi, adminApi } from '../../api/client' import { getApiErrorMessage } from '../../types' import type { UserWithOidc } from '../../types' import Section from './Section' const MFA_BACKUP_SESSION_KEY = 'trek_mfa_backup_codes_pending' export default function AccountTab(): React.ReactElement { const { user, updateProfile, uploadAvatar, deleteAvatar, logout, loadUser, demoMode, appRequireMfa } = useAuthStore() const [searchParams] = useSearchParams() const navigate = useNavigate() const { t } = useTranslation() const toast = useToast() const avatarInputRef = React.useRef(null) const [saving, setSaving] = useState(false) const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) // Profile const [username, setUsername] = useState(user?.username || '') const [email, setEmail] = useState(user?.email || '') useEffect(() => { setUsername(user?.username || '') setEmail(user?.email || '') }, [user]) // Password const [currentPassword, setCurrentPassword] = useState('') const [newPassword, setNewPassword] = useState('') const [confirmPassword, setConfirmPassword] = useState('') const [oidcOnlyMode, setOidcOnlyMode] = useState(false) useEffect(() => { authApi.getAppConfig?.().then(config => { if (config?.oidc_only_mode) setOidcOnlyMode(true) }).catch(() => {}) }, []) // MFA const [mfaQr, setMfaQr] = useState(null) const [mfaSecret, setMfaSecret] = useState(null) const [mfaSetupCode, setMfaSetupCode] = useState('') const [mfaDisablePwd, setMfaDisablePwd] = useState('') const [mfaDisableCode, setMfaDisableCode] = useState('') const [mfaLoading, setMfaLoading] = useState(false) const [backupCodes, setBackupCodes] = useState(null) const mfaRequiredByPolicy = !demoMode && !user?.mfa_enabled && (searchParams.get('mfa') === 'required' || appRequireMfa) const backupCodesText = backupCodes?.join('\n') || '' useEffect(() => { if (!user?.mfa_enabled || backupCodes) return try { const raw = sessionStorage.getItem(MFA_BACKUP_SESSION_KEY) if (!raw) return const parsed = JSON.parse(raw) as unknown if (Array.isArray(parsed) && parsed.length > 0 && parsed.every(x => typeof x === 'string')) { setBackupCodes(parsed) } } catch { sessionStorage.removeItem(MFA_BACKUP_SESSION_KEY) } }, [user?.mfa_enabled, backupCodes]) const dismissBackupCodes = () => { sessionStorage.removeItem(MFA_BACKUP_SESSION_KEY) setBackupCodes(null) } const copyBackupCodes = async () => { if (!backupCodesText) return try { await navigator.clipboard.writeText(backupCodesText) toast.success(t('settings.mfa.backupCopied')) } catch { toast.error(t('common.error')) } } const downloadBackupCodes = () => { if (!backupCodesText) return const blob = new Blob([backupCodesText + '\n'], { type: 'text/plain;charset=utf-8' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = 'trek-mfa-backup-codes.txt' document.body.appendChild(a) a.click() a.remove() URL.revokeObjectURL(url) } const printBackupCodes = () => { if (!backupCodesText) return const html = `TREK MFA Backup Codes

TREK MFA Backup Codes

${new Date().toLocaleString()}

${backupCodesText}
` const w = window.open('', '_blank', 'width=900,height=700') if (!w) return w.document.open() w.document.write(html) w.document.close() w.focus() w.print() } const handleAvatarUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (!file) return try { await uploadAvatar(file) toast.success(t('settings.avatarUploaded')) } catch { toast.error(t('settings.avatarError')) } if (avatarInputRef.current) avatarInputRef.current.value = '' } const handleAvatarRemove = async () => { try { await deleteAvatar() toast.success(t('settings.avatarRemoved')) } catch { toast.error(t('settings.avatarError')) } } const saveProfile = async () => { setSaving(true) try { await updateProfile({ username, email }) toast.success(t('settings.toast.profileSaved')) } catch (err: unknown) { toast.error(err instanceof Error ? err.message : 'Error') } finally { setSaving(false) } } return ( <>
setUsername(e.target.value)} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent" />
setEmail(e.target.value)} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent" />
{/* Change Password */} {!oidcOnlyMode && (
setCurrentPassword(e.target.value)} placeholder={t('settings.currentPassword')} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent" /> setNewPassword(e.target.value)} placeholder={t('settings.newPassword')} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent" /> setConfirmPassword(e.target.value)} placeholder={t('settings.confirmPassword')} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent" />
)} {/* MFA */}

{t('settings.mfa.title')}

{mfaRequiredByPolicy && (

{t('settings.mfa.requiredByPolicy')}

)}

{t('settings.mfa.description')}

{demoMode ? (

{t('settings.mfa.demoBlocked')}

) : ( <>

{user?.mfa_enabled ? t('settings.mfa.enabled') : t('settings.mfa.disabled')}

{!user?.mfa_enabled && !mfaQr && ( )} {!user?.mfa_enabled && mfaQr && (

{t('settings.mfa.scanQr')}

{mfaSecret}
setMfaSetupCode(e.target.value.replace(/\D/g, '').slice(0, 8))} placeholder={t('settings.mfa.codePlaceholder')} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" />
)} {user?.mfa_enabled && (

{t('settings.mfa.disableTitle')}

{t('settings.mfa.disableHint')}

setMfaDisablePwd(e.target.value)} placeholder={t('settings.currentPassword')} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" /> setMfaDisableCode(e.target.value.replace(/\D/g, '').slice(0, 8))} placeholder={t('settings.mfa.codePlaceholder')} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" />
)} {backupCodes && backupCodes.length > 0 && (

{t('settings.mfa.backupTitle')}

{t('settings.mfa.backupDescription')}

{backupCodesText}

{t('settings.mfa.backupWarning')}

)} )}
{/* Avatar */}
{user?.avatar_url ? ( ) : (
{user?.username?.charAt(0).toUpperCase()}
)} {user?.avatar_url && ( )}
{user?.role === 'admin' ? <> {t('settings.roleAdmin')} : t('settings.roleUser')} {(user as UserWithOidc)?.oidc_issuer && ( SSO )}
{(user as UserWithOidc)?.oidc_issuer && (

{t('settings.oidcLinked')} {(user as UserWithOidc).oidc_issuer!.replace('https://', '').replace(/\/+$/, '')}

)}
{/* Delete Account Blocked */} {showDeleteConfirm === 'blocked' && (
setShowDeleteConfirm(false)}>
e.stopPropagation()}>

{t('settings.deleteBlockedTitle')}

{t('settings.deleteBlockedMessage')}

)} {/* Delete Account Confirm */} {showDeleteConfirm === true && (
setShowDeleteConfirm(false)}>
e.stopPropagation()}>

{t('settings.deleteAccountTitle')}

{t('settings.deleteAccountWarning')}

)} ) }