v1.1.0: session guard on all game routes + register + sign-in screen (ISS-107)
CI / build + unit + e2e (pull_request) Failing after 26m20s

This commit is contained in:
2026-09-26 14:40:56 +00:00
parent 21dd37f88c
commit b0f1bd8b67
54 changed files with 1002 additions and 91 deletions
+47 -1
View File
@@ -4,8 +4,10 @@ import dynamic from 'next/dynamic';
import { useEffect, useRef, useState } from 'react';
import LeftPane from '@/components/LeftPane';
import RightPane from '@/components/RightPane';
import SignInScreen from '@/components/SignInScreen';
import ToastStack from '@/components/Toasts';
import type { HoverTarget } from '@/components/hoverTypes';
import { clearStoredSession, sessionHeaders, storedSession } from '@/lib/authClient';
import { useGameState } from '@/lib/useGameState';
import { useToasts } from '@/lib/useToasts';
import { diffSnapshots } from '@/lib/deriveEvents';
@@ -13,6 +15,50 @@ import { diffSnapshots } from '@/lib/deriveEvents';
const Map = dynamic(() => import('@/components/Map'), { ssr: false });
export default function Home() {
// v1.1.0 (ISS-107): auth gate — null while checking the stored session on mount, then either
// the sign-in screen or the game shell. The shell only mounts once authenticated, so its state
// feed never polls an unauthenticated /api/state.
const [authed, setAuthed] = useState<boolean | null>(null);
useEffect(() => {
const sid = storedSession();
if (!sid) {
setAuthed(false);
return;
}
let cancelled = false;
fetch('/api/me', { headers: sessionHeaders(sid), cache: 'no-store' })
.then((res) => {
if (cancelled) return;
if (!res.ok) clearStoredSession(); // stale/expired id — drop it and show sign-in
setAuthed(res.ok);
})
.catch(() => {
if (!cancelled) setAuthed(false); // network error → let the user try to sign in
});
return () => {
cancelled = true;
};
}, []);
if (authed === null) {
return (
<div className="app-shell">
<p data-testid="auth-checking" className="sign-in-loading">
Loading…
</p>
</div>
);
}
if (!authed) {
return <SignInScreen onAuthed={() => setAuthed(true)} />;
}
// The admin panel's Log out cleared the session — drop back to sign-in (ISS-107).
return <AppShell onLogout={() => setAuthed(false)} />;
}
function AppShell({ onLogout }: { onLogout: () => void }) {
// Single polling source for the whole shell (v0.9.0): one /api/state loop, snapshot passed
// down as props so later panes can share it without extra poll loops. applySnapshot (v0.10.0)
// lets a pane push an action endpoint's full response snapshot in immediately.
@@ -49,7 +95,7 @@ export default function Home() {
{/* v0.11.0 — the same polled snapshot drives the map's plane markers + route lines;
ISS-010 (v0.18.0) — applySnapshot lets the airport popup's Relocate row push its
action response into the shared state, exactly like RightPane does */}
<Map state={state} hovered={hovered} applySnapshot={applySnapshot} />
<Map state={state} hovered={hovered} applySnapshot={applySnapshot} onLogout={onLogout} />
<RightPane state={state} loading={loading} applySnapshot={applySnapshot} onHover={setHovered} onError={onError} />
<ToastStack toasts={toasts} onDismiss={dismiss} />
</div>