import { useState, useEffect } from 'react'; import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; import { Button } from '@/components/ui/button'; import { Item, ItemMedia, ItemContent, ItemTitle, ItemDescription, ItemActions, } from '@/components/ui/item'; import { httpClient } from '@/app/infra/http/HttpClient'; import { systemInfo } from '@/app/infra/http'; import { Loader2, ExternalLink, KeyRound, Layers, Fingerprint, Plus, Trash2, Pencil, } from 'lucide-react'; import { startRegistration } from '@simplewebauthn/browser'; import PasswordChangeDialog from '../password-change-dialog/PasswordChangeDialog'; import { PanelBody } from '../settings-dialog/panel-layout'; interface AccountSettingsPanelProps { // True when this panel is the active section and the dialog is open. active: boolean; onEmailResolved?: (email: string) => void; } interface PasskeyItem { uuid: string; name: string; aaguid?: string; transports?: string; backed_up?: boolean; created_at?: string; last_used_at?: string; } export default function AccountSettingsPanel({ active, onEmailResolved, }: AccountSettingsPanelProps) { const { t } = useTranslation(); const [accountType, setAccountType] = useState<'local' | 'space'>('local'); const [hasPassword, setHasPassword] = useState(false); const [userEmail, setUserEmail] = useState(''); const [loading, setLoading] = useState(true); const [spaceBindLoading, setSpaceBindLoading] = useState(false); const [passwordDialogOpen, setPasswordDialogOpen] = useState(false); const [passkeys, setPasskeys] = useState([]); const [passkeyLoading, setPasskeyLoading] = useState(false); const [registeringPasskey, setRegisteringPasskey] = useState(false); useEffect(() => { if (active) { loadUserInfo(); loadPasskeys(); } }, [active]); async function loadUserInfo() { setLoading(true); try { const info = await httpClient.getUserInfo(); setAccountType(info.account_type); setHasPassword(info.has_password); setUserEmail(info.user); onEmailResolved?.(info.user); } catch { toast.error(t('common.error')); } finally { setLoading(false); } } async function loadPasskeys() { setPasskeyLoading(true); try { const list = await httpClient.getPasskeys(); setPasskeys(list); } catch { // ignore } finally { setPasskeyLoading(false); } } const handleAddPasskey = async () => { setRegisteringPasskey(true); try { const { options, challenge_token } = await httpClient.getPasskeyRegisterOptions(window.location.origin); const regResp = await startRegistration({ optionsJSON: options }); const defaultName = prompt(t('account.passkeyNamePlaceholder')) || undefined; await httpClient.verifyPasskeyRegister( challenge_token, regResp, defaultName, ); toast.success(t('account.passkeyAddedSuccess')); await loadPasskeys(); } catch (error: any) { if (error?.name === 'NotAllowedError') { // User cancelled } else { toast.error(error?.message || t('common.error')); } } finally { setRegisteringPasskey(false); } }; const handleDeletePasskey = async (uuid: string) => { if (!confirm(t('account.deletePasskeyConfirm'))) return; try { await httpClient.deletePasskey(uuid); toast.success(t('account.passkeyDeleteSuccess')); await loadPasskeys(); } catch (error: any) { toast.error(error?.message || t('common.error')); } }; const handleRenamePasskey = async (uuid: string, currentName: string) => { const newName = prompt(t('account.passkeyName'), currentName); if (!newName || !newName.trim() || newName === currentName) return; try { await httpClient.renamePasskey(uuid, newName.trim()); toast.success(t('account.passkeyRenameSuccess')); await loadPasskeys(); } catch (error: any) { toast.error(error?.message || t('common.error')); } }; const handleBindSpace = async () => { setSpaceBindLoading(true); try { const currentOrigin = window.location.origin; const redirectUri = `${currentOrigin}/auth/space/callback?mode=bind`; const response = await httpClient.getSpaceBindAuthorizeUrl(redirectUri); window.location.href = response.authorize_url; } catch { toast.error(t('common.spaceLoginFailed')); setSpaceBindLoading(false); } }; const handlePasswordDialogClose = (dialogOpen: boolean) => { setPasswordDialogOpen(dialogOpen); if (!dialogOpen) { // Reload user info to update password status loadUserInfo(); } }; return ( {userEmail && (

{userEmail}

)} {loading ? (
) : (
{/* Password Item */} {t('account.passwordStatus')} {hasPassword ? t('account.passwordSetDescription') : t('account.setPasswordHint')} {/* Space Account Item */} {t('account.spaceStatus')} {accountType === 'space' ? t('account.spaceBoundDescription') : t('account.bindSpaceDescription')} {accountType === 'local' && ( )} {/* Passkey Section */}

{t('account.passkeySectionTitle')}

{t('account.passkeySectionDesc')}

{passkeyLoading ? (
) : passkeys.length === 0 ? (
{t('account.noPasskeys')}
) : (
{passkeys.map((pk) => ( {pk.name} {pk.created_at && ( {t('account.passkeyCreated', { date: new Date( pk.created_at, ).toLocaleDateString(), })} )} {pk.last_used_at && ( ยท{' '} {t('account.passkeyLastUsed', { date: new Date( pk.last_used_at, ).toLocaleDateString(), })} )} ))}
)}
)}
); }