79 lines
3.1 KiB
TypeScript
79 lines
3.1 KiB
TypeScript
'use client';
|
||
|
||
import { useState } from 'react';
|
||
import { storeSession } from '@/lib/authClient';
|
||
|
||
// v1.1.0 (ISS-107) — full-screen sign-in / create-account screen shown when no valid session is
|
||
// stored. Both forms POST the same shape ({username, password}); login mints a session for an
|
||
// existing account, register creates one first and then mints it. On success the sessionId goes
|
||
// to localStorage (via storeSession) and onAuthed() flips the shell back in.
|
||
|
||
interface SignInScreenProps {
|
||
onAuthed: () => void;
|
||
}
|
||
|
||
export default function SignInScreen({ onAuthed }: SignInScreenProps) {
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [busy, setBusy] = useState(false);
|
||
|
||
async function submit(e: React.FormEvent<HTMLFormElement>, path: '/api/login' | '/api/register') {
|
||
e.preventDefault();
|
||
if (busy) return;
|
||
setBusy(true);
|
||
setError(null);
|
||
try {
|
||
const form = new FormData(e.currentTarget);
|
||
const res = await fetch(path, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
username: String(form.get('username') ?? ''),
|
||
password: String(form.get('password') ?? ''),
|
||
}),
|
||
cache: 'no-store',
|
||
});
|
||
const data = (await res.json()) as { sessionId?: string; error?: string };
|
||
if (!res.ok || !data.sessionId) throw new Error(data.error ?? `HTTP ${res.status}`);
|
||
storeSession(data.sessionId);
|
||
onAuthed();
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : String(err));
|
||
} finally {
|
||
setBusy(false);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="sign-in">
|
||
<h1 data-testid="app-title">Flight Ops</h1>
|
||
{error !== null && (
|
||
<p data-testid="auth-error" role="alert" className="sign-in-error">
|
||
{error}
|
||
</p>
|
||
)}
|
||
|
||
<form data-testid="sign-in-form" onSubmit={(e) => void submit(e, '/api/login')}>
|
||
<label htmlFor="sign-in-username">Username</label>
|
||
<input id="sign-in-username" data-testid="sign-in-username" name="username" autoComplete="username" required />
|
||
<label htmlFor="sign-in-password">Password</label>
|
||
<input id="sign-in-password" data-testid="sign-in-password" name="password" type="password" autoComplete="current-password" required />
|
||
<button type="submit" data-testid="sign-in-submit" className="pane-btn" disabled={busy}>
|
||
Sign in
|
||
</button>
|
||
</form>
|
||
|
||
<p className="sign-in-divider">or</p>
|
||
|
||
<form data-testid="create-account-form" onSubmit={(e) => void submit(e, '/api/register')}>
|
||
<label htmlFor="register-username">Username (3–20 chars: a-z 0-9 _ -)</label>
|
||
<input id="register-username" name="username" autoComplete="off" required />
|
||
<label htmlFor="register-password">Password (at least 8 characters)</label>
|
||
<input id="register-password" name="password" type="password" autoComplete="new-password" required minLength={8} />
|
||
<button type="submit" data-testid="register-submit" className="pane-btn" disabled={busy}>
|
||
Create account
|
||
</button>
|
||
</form>
|
||
</div>
|
||
);
|
||
}
|