Files
GEWU/web/src/shell/AccountLogin.tsx
T

66 lines
3.7 KiB
TypeScript
Raw Normal View History

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>;
}