66 lines
3.7 KiB
TypeScript
66 lines
3.7 KiB
TypeScript
import { useEffect, useRef, useState, type FormEvent } from 'react';
|
|||
|
|
|
||
|
|
export default function AccountLogin({ onLogin, busy, credentialLabel, onError }: {
|
||
|
|
onLogin: (session: { csrf_token: string }, passwordAccount: boolean, restored?: boolean) => Promise<void>;
|
||
|
|
busy: boolean;
|
||
|
|
credentialLabel: string;
|
||
|
|
onError: (message: string) => void;
|
||
|
|
}) {
|
||
|
|
const [checking, setChecking] = useState(true);
|
||
|
|
const callbacks = useRef({ onLogin, onError });
|
||
|
|
useEffect(() => { callbacks.current = { onLogin, onError }; }, [onLogin, onError]);
|
||
|
|
useEffect(() => {
|
||
|
|
const controller = new AbortController();
|
||
|
|
void (async () => {
|
||
|
|
try {
|
||
|
|
const response = await fetch('/v1/session', {
|
||
|
|
method: 'GET', credentials: 'same-origin', cache: 'no-store',
|
||
|
|
headers: { 'X-GEWU-Session': 'restore' }, signal: controller.signal,
|
||
|
|
});
|
||
|
|
if (response.status === 401) return;
|
||
|
|
if (!response.ok) throw new Error('Could not restore your session. Try signing in again.');
|
||
|
|
const session = await response.json() as { csrf_token: string; password_account: boolean };
|
||
|
|
if (!controller.signal.aborted) await callbacks.current.onLogin(session, session.password_account, true);
|
||
|
|
} catch (failure) {
|
||
|
|
if (!controller.signal.aborted) callbacks.current.onError(failure instanceof Error ? failure.message : 'Session restoration failed.');
|
||
|
|
} finally { if (!controller.signal.aborted) setChecking(false); }
|
||
|
|
})();
|
||
|
|
return () => controller.abort();
|
||
|
|
}, []);
|
||
|
|
const [legacy, setLegacy] = useState(false);
|
||
|
|
const [username, setUsername] = useState('');
|
||
|
|
const [password, setPassword] = useState('');
|
||
|
|
const [token, setToken] = useState('');
|
||
|
|
const [working, setWorking] = useState(false);
|
||
|
|
|
||
|
|
async function submit(event: FormEvent) {
|
||
|
|
event.preventDefault(); setWorking(true); onError('');
|
||
|
|
try {
|
||
|
|
const response = await fetch(legacy ? '/v1/session' : '/v1/users/session', {
|
||
|
|
method: 'POST', credentials: 'same-origin', cache: 'no-store',
|
||
|
|
headers: legacy ? { Authorization: `Bearer ${token.trim()}` } : { 'Content-Type': 'application/json' },
|
||
|
|
...(legacy ? {} : { body: JSON.stringify({ username, password }) }),
|
||
|
|
});
|
||
|
|
setPassword(''); setToken('');
|
||
|
|
if (!response.ok) {
|
||
|
|
const body = await response.json().catch(() => ({}));
|
||
|
|
throw new Error(typeof body.detail === 'string' ? body.detail : 'Sign-in failed.');
|
||
|
|
}
|
||
|
|
await onLogin(await response.json() as { csrf_token: string }, !legacy);
|
||
|
|
} catch (failure) { onError(failure instanceof Error ? failure.message : 'Sign-in failed.'); }
|
||
|
|
finally { setWorking(false); }
|
||
|
|
}
|
||
|
|
|
||
|
|
if (checking) return <p role="status">Restoring your session…</p>;
|
||
|
|
|
||
|
|
return <div>
|
||
|
|
<form className="connect-form" onSubmit={submit}>
|
||
|
|
{legacy ? <><label htmlFor="login-credential">{credentialLabel}</label><input id="login-credential" type="password" autoComplete="off" value={token} onChange={e => setToken(e.target.value)} required /></> : <>
|
||
|
|
<label htmlFor="login-username">Username</label><input id="login-username" autoComplete="username" value={username} onChange={e => setUsername(e.target.value)} required maxLength={64} pattern="[a-z0-9][a-z0-9._\-]*" />
|
||
|
|
<label htmlFor="login-password">Password</label><input id="login-password" type="password" autoComplete="current-password" value={password} onChange={e => setPassword(e.target.value)} required maxLength={1024} />
|
||
|
|
</>}
|
||
|
|
<button disabled={busy || working}>{working ? 'Signing in…' : legacy ? 'Connect' : 'Sign in'}</button>
|
||
|
|
</form>
|
||
|
|
<button className="text-button" disabled={busy || working} onClick={() => { setLegacy(!legacy); setPassword(''); setToken(''); }}>{legacy ? 'Use username and password' : 'Use an existing credential'}</button>
|
||
|
|
</div>;
|
||
|
|
}
|