import { useCallback, useEffect, useState } from 'react' import { Loading, ErrorState } from '../../../components/PageState.jsx' import ProviderIcon from '../../../components/ProviderIcon.jsx' import RecoveryCodesDisplay from '../../../components/security/RecoveryCodesDisplay.jsx' import TrustedDevicesPanel from '../../../components/security/TrustedDevicesPanel.jsx' import RecoveryCodesPanel from '../../../components/security/RecoveryCodesPanel.jsx' import { api } from '../../../api/client.js' // Link/unlink external SSO identities to this account. Linking redirects through // the provider's OAuth flow (/auth/sso/:id/link) and returns here with ?linked // or ?link_error. Only providers that are enabled + valid can be linked. function LinkedAccounts() { const [linked, setLinked] = useState(null) const [available, setAvailable] = useState([]) const [error, setError] = useState('') const banner = (() => { const q = new URLSearchParams(window.location.search) if (q.get('linked')) return { ok: true, text: 'Account linked.' } if (q.get('link_error') === 'in_use') return { ok: false, text: 'That external account is already linked to another user.' } if (q.get('link_error')) return { ok: false, text: 'Could not link that account. Please try again.' } return null })() const load = useCallback(async () => { try { const [ids, avail] = await Promise.all([ api.admin.linkedIdentities(), api.authProviders().catch(() => []), ]) setLinked(ids) setAvailable(Array.isArray(avail) ? avail : []) } catch { setError('Could not load linked accounts.') } }, []) useEffect(() => { load() }, [load]) const nameFor = (id) => available.find((p) => p.id === id)?.name || id.charAt(0).toUpperCase() + id.slice(1) const iconFor = (id) => (id === 'google' || id === 'discord' ? id : 'oidc') async function unlink(provider) { if (!window.confirm(`Unlink ${nameFor(provider)} from your account?`)) return try { await api.admin.unlinkIdentity(provider) await load() } catch (err) { setError(err.message || 'Could not unlink.') } } if (error) return if (!linked) return null const linkedIds = new Set(linked.map((i) => i.provider)) const linkable = available.filter((p) => !linkedIds.has(p.id)) return (

Linked accounts

Link a Google, Discord, or other SSO account so you can sign in with it. SSO can only sign in to an account it is linked to — linking here is what grants that access.

{banner && (

{banner.text}

)} {linked.length > 0 && (
{linked.map((i) => (
{nameFor(i.provider)}
{i.email &&
{i.email}
}
))}
)} {linkable.length > 0 && (
{linkable.map((p) => ( ))}
)} {linked.length === 0 && linkable.length === 0 && (

No SSO providers are enabled. Configure them under Authentication.

)}
) } // Self-service account security: enable / disable optional TOTP two-factor. export default function AccountAdmin() { const [account, setAccount] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState('') // Enrollment state. const [setup, setSetup] = useState(null) // fields qr and otpauthUrl once enrolling const [code, setCode] = useState('') const [busy, setBusy] = useState(false) const [msg, setMsg] = useState('') const [newCodes, setNewCodes] = useState(null) // one-time recovery codes shown after enabling async function load() { try { setAccount(await api.admin.getAccount()) } catch { setError('Could not load your account.') } finally { setLoading(false) } } useEffect(() => { load() }, []) if (loading) return if (error) return async function beginSetup() { setBusy(true) setMsg('') setError('') try { setSetup(await api.admin.totpSetup()) setCode('') } catch (err) { setError(err.message || 'Could not start setup.') } finally { setBusy(false) } } async function confirmEnable() { setBusy(true) setMsg('') setError('') try { const res = await api.admin.totpEnable(code.trim()) setSetup(null) setCode('') setNewCodes(res?.recoveryCodes || null) setMsg('Two-factor authentication is now enabled.') await load() } catch (err) { setError(err.message || 'Could not enable two-factor.') } finally { setBusy(false) } } async function disable() { setBusy(true) setMsg('') setError('') try { await api.admin.totpDisable(code.trim()) setCode('') setMsg('Two-factor authentication has been disabled.') await load() } catch (err) { setError(err.message || 'Could not disable two-factor.') } finally { setBusy(false) } } const enabled = account?.totp_enabled return (

Two-factor authentication

Add a time-based one-time code (TOTP) from an authenticator app as a second step at login. Optional, and only affects your own account.

{enabled ? 'Enabled' : 'Not enabled'}
{/* Enable flow */} {!enabled && !setup && (
)} {!enabled && setup && (

1. Scan this QR code with your authenticator app, then enter the current 6-digit code to confirm.

TOTP QR code
)} {/* Disable flow */} {enabled && (

Enter a current code from your authenticator to turn two-factor off.

)} {msg &&

{msg}

} {error &&

{error}

} {/* One-time recovery codes shown right after enabling 2FA. */} {newCodes && (
setNewCodes(null)} />
)} {/* Trusted devices + recovery-code management, only relevant with 2FA on. */} {enabled && ( <> )}
) }