v1.1.0: session guard on all game routes + register + sign-in screen (ISS-107)
CI / build + unit + e2e (pull_request) Failing after 26m20s
CI / build + unit + e2e (pull_request) Failing after 26m20s
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
'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" name="username" autoComplete="username" required />
|
||||
<label htmlFor="sign-in-password">Password</label>
|
||||
<input id="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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user