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
+9 -20
View File
@@ -1,13 +1,12 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
import { clearStoredSession, sessionHeaders, storedSession, storeSession } from '../lib/authClient';
// v0.71.0 — content of the Admin modal (opened by the top-bar "Admin" button). Three views:
// login form (no valid session), a "not an admin" notice, or the generic whole-database CRUD
// grid behind /api/admin/*. This is a dev tool surface — plain and functional on purpose.
const SESSION_STORAGE_KEY = 'flight.session';
interface Me {
username: string;
admin: boolean;
@@ -18,15 +17,7 @@ interface RowsData {
rows: Array<Record<string, unknown>>;
}
function readSession(): string | null {
try {
return window.localStorage.getItem(SESSION_STORAGE_KEY);
} catch {
return null;
}
}
export default function AdminPanel() {
export default function AdminPanel({ onLogout }: { onLogout?: () => void }) {
const [session, setSession] = useState<string | null>(null);
const [me, setMe] = useState<Me | null>(null); // null while checking /api/me on mount
const [checking, setChecking] = useState(true);
@@ -45,15 +36,12 @@ export default function AdminPanel() {
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const authHeaders = useCallback(
(s: string) => ({ 'X-Flight-Session': s }),
[],
);
const authHeaders = useCallback((s: string) => sessionHeaders(s), []);
// On mount: a stored session decides between the grid and the login form.
useEffect(() => {
let cancelled = false;
const stored = readSession();
const stored = storedSession();
if (!stored) {
setChecking(false);
return;
@@ -61,7 +49,7 @@ export default function AdminPanel() {
fetch('/api/me', { headers: authHeaders(stored) })
.then(async (res) => {
if (!res.ok || cancelled) {
window.localStorage.removeItem(SESSION_STORAGE_KEY);
clearStoredSession();
setSession(null);
return;
}
@@ -71,7 +59,7 @@ export default function AdminPanel() {
})
.catch(() => {
if (!cancelled) {
window.localStorage.removeItem(SESSION_STORAGE_KEY);
clearStoredSession();
setSession(null);
}
})
@@ -161,7 +149,7 @@ export default function AdminPanel() {
return;
}
const body = (await res.json()) as { sessionId: string; user: Me };
window.localStorage.setItem(SESSION_STORAGE_KEY, body.sessionId);
storeSession(body.sessionId);
setSession(body.sessionId);
setMe(body.user);
setUsername('');
@@ -170,7 +158,8 @@ export default function AdminPanel() {
const logout = async () => {
if (session) await fetch('/api/logout', { method: 'POST', headers: authHeaders(session) }).catch(() => undefined);
window.localStorage.removeItem(SESSION_STORAGE_KEY);
clearStoredSession();
onLogout?.(); // v1.1.0 (ISS-107): session gone → shell drops back to the sign-in screen
setSession(null);
setMe(null);
setTable(null);