import { useEffect, useRef, useState } from 'react' import { api } from '../api.js' import { THEMES, useAuth } from '../auth.js' import { dateTime } from '../format.js' import { useT } from '../i18n.js' import { Avatar, ErrorNote, Field, Modal } from '../components/ui.jsx' export default function Profile() { const t = useT() const { user, setUser, setLang, theme, setTheme } = useAuth() const [name, setName] = useState(user.name) const [current, setCurrent] = useState('') const [next, setNext] = useState('') const [error, setError] = useState(null) const [done, setDone] = useState(false) const submit = async (e) => { e.preventDefault() try { const body = { name } if (next) Object.assign(body, { current_password: current, new_password: next }) setUser(await api.put('/api/auth/me', body)) setCurrent('') setNext('') setError(null) setDone(true) } catch (err) { setError(err); setDone(false) } } return (

{t('profile_title')}

setName(e.target.value)} />

{t('change_password')}

setCurrent(e.target.value)} /> setNext(e.target.value)} /> {done &&
{t('saved')}
}
) } function AvatarPicker() { const t = useT() const { user, setUser } = useAuth() const input = useRef(null) const [error, setError] = useState(null) const upload = async (e) => { const file = e.target.files[0] e.target.value = '' if (!file) return const form = new FormData() form.append('file', file) try { setUser(await api.put('/api/auth/me/avatar', form)); setError(null) } catch (err) { setError(err) } } const remove = async () => { try { setUser(await api.del('/api/auth/me/avatar')); setError(null) } catch (err) { setError(err) } } return (
{user.avatar_url && }
{t('avatar_hint')}
) } function MfaSection() { const t = useT() const { user, lang } = useAuth() const [dialog, setDialog] = useState(null) // setup | disable | codes return (

{t('mfa')} {user.mfa_enabled ? t('mfa_on') : t('mfa_off')}

{user.mfa_enabled ? <>

{t('mfa_on_since', { when: dateTime(user.mfa_enabled_at, lang), n: user.mfa_recovery_left })}

: <>

{t('mfa_off_text')}

} {dialog === 'setup' && setDialog(null)} />} {dialog === 'disable' && setDialog(null)} />} {dialog === 'codes' && setDialog(null)} />}
) } function RecoveryCodes({ codes, onDone }) { const t = useT() const [copied, setCopied] = useState(false) const copy = () => navigator.clipboard?.writeText(codes.join('\n')).then(() => setCopied(true)).catch(() => {}) return <>

{t('mfa_codes_text')}

{codes.map((c) => {c})}
} function MfaSetup({ onClose }) { const t = useT() const { setUser } = useAuth() const [setup, setSetup] = useState(null) const [code, setCode] = useState('') const [codes, setCodes] = useState(null) const [error, setError] = useState(null) useEffect(() => { api.post('/api/auth/mfa/setup').then(setSetup).catch(setError) }, []) const confirm = async (e) => { e.preventDefault() try { const r = await api.post('/api/auth/mfa/enable', { code }) setCodes(r.recovery_codes) setUser(r.user) } catch (err) { setError(err); setCode('') } } if (codes) return return ( {setup &&

{t('mfa_step1')}

{t('mfa_manual')}

{setup.secret.match(/.{1,4}/g).join(' ')}

{t('mfa_step2')}

setCode(e.target.value)} required autoFocus />
} {!setup && } ) } function MfaDisable({ onClose }) { const t = useT() const { setUser } = useAuth() const [password, setPassword] = useState('') const [code, setCode] = useState('') const [error, setError] = useState(null) const submit = async (e) => { e.preventDefault() try { setUser(await api.post('/api/auth/mfa/disable', { password, code })); onClose() } catch (err) { setError(err) } } return (

{t('mfa_disable_text')}

setPassword(e.target.value)} required /> setCode(e.target.value)} required />
) } function MfaNewCodes({ onClose }) { const t = useT() const { setUser } = useAuth() const [code, setCode] = useState('') const [codes, setCodes] = useState(null) const [error, setError] = useState(null) const submit = async (e) => { e.preventDefault() try { const r = await api.post('/api/auth/mfa/recovery-codes', { code }) setCodes(r.recovery_codes) setUser(r.user) } catch (err) { setError(err); setCode('') } } return ( {codes ? : (

{t('mfa_new_codes_text')}

setCode(e.target.value)} required autoFocus />
)}
) }