import { useCallback, useEffect, useState } from 'react' import ProviderIcon from '../../components/ProviderIcon.jsx' import { Loading, ErrorState } from '../../components/PageState.jsx' import { useAuth } from '../../contexts/AuthContext.jsx' import { api } from '../../api/client.js' // ── Change username ──────────────────────────────────────────────────────── function ChangeUsername({ account, onChanged }) { const [username, setUsername] = useState(account.username) const [busy, setBusy] = useState(false) const [msg, setMsg] = useState('') const [error, setError] = useState('') async function save(e) { e.preventDefault() setMsg('') setError('') if (username.trim().length < 3) return setError('Username must be at least 3 characters.') setBusy(true) try { const { username: next } = await api.player.changeUsername(username.trim()) setMsg('Username updated.') await onChanged(next) } catch (err) { if (err.status === 409) setError('That username is already taken.') else setError(err.message || 'Could not change your username.') } finally { setBusy(false) } } return (
) } // ── Change / set password ────────────────────────────────────────────────── function ChangePassword({ account }) { const hasPassword = account.has_password const [current, setCurrent] = useState('') const [next, setNext] = useState('') const [busy, setBusy] = useState(false) const [msg, setMsg] = useState('') const [error, setError] = useState('') async function save(e) { e.preventDefault() setMsg('') setError('') if (next.length < 8) return setError('New password must be at least 8 characters.') if (hasPassword && !current) return setError('Enter your current password.') setBusy(true) try { await api.player.changePassword(next, hasPassword ? current : undefined) setMsg(hasPassword ? 'Password changed.' : 'Password set. You can now sign in with it.') setCurrent('') setNext('') } catch (err) { setError(err.message || 'Could not change your password.') } finally { setBusy(false) } } return (
{!hasPassword && (

Your account was created through a linked provider and has no password yet. Set one to also be able to sign in with a username and password.

)}
{hasPassword && ( )}
) } // ── Two-factor (TOTP) ────────────────────────────────────────────────────── function TwoFactor({ account, reload }) { const enabled = account.totp_enabled const [setup, setSetup] = useState(null) const [code, setCode] = useState('') const [busy, setBusy] = useState(false) const [msg, setMsg] = useState('') const [error, setError] = useState('') async function begin() { setBusy(true); setMsg(''); setError('') try { setSetup(await api.player.totpSetup()) setCode('') } catch (err) { setError(err.message || 'Could not start setup.') } finally { setBusy(false) } } async function confirm() { setBusy(true); setMsg(''); setError('') try { await api.player.totpEnable(code.trim()) setSetup(null); setCode(''); setMsg('Two-factor is now enabled.') await reload() } catch (err) { setError(err.message || 'Could not enable two-factor.') } finally { setBusy(false) } } async function disable() { setBusy(true); setMsg(''); setError('') try { await api.player.totpDisable(code.trim()) setCode(''); setMsg('Two-factor has been disabled.') await reload() } catch (err) { setError(err.message || 'Could not disable two-factor.') } finally { setBusy(false) } } return (
{enabled ? 'Enabled' : 'Not enabled'}
{!enabled && !setup && (
)} {!enabled && setup && (

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

TOTP QR code
)} {enabled && (

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

)}
) } // ── Linked SSO identities ────────────────────────────────────────────────── 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.player.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.player.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 (

Link a Google, Discord, or other provider so you can sign in with it.

{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.

)}
) } // ── Shared bits ──────────────────────────────────────────────────────────── function Section({ title, children }) { return (

{title}

{children}
) } function Note({ msg, error }) { if (!msg && !error) return null return

{error || msg}

} // ── Page ─────────────────────────────────────────────────────────────────── export default function PlayerAccount() { const { refresh } = useAuth() const [account, setAccount] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState('') const load = useCallback(async () => { try { setAccount(await api.player.getAccount()) } catch { setError('Could not load your account.') } finally { setLoading(false) } }, []) useEffect(() => { load() }, [load]) // After a username change: reload local account + refresh the auth context so // the header reflects the new name. const onUsernameChanged = useCallback(async () => { await Promise.all([load(), refresh()]) }, [load, refresh]) return (
{loading && } {error && } {!loading && !error && account && ( <>

Signed in as {account.username} {account.email ? ` · ${account.email}` : ''}

)}
) }