feat: PKCE auth + CI/CD deploy
Some checks failed
CI/CD — Build & Deploy / Build & Deploy (push) Failing after 25s

- Frontend: PKCE flow (oauth.js, api.js centralized, cookie-based AuthContext)
- Backend: token introspection, cookies httpOnly, refresh endpoint
- Replaced localStorage JWT with httpOnly session cookies
- useSaveSync migrated to cookie auth
- cookie-parser added
- Gitea CI workflow (vps-runner pattern)
This commit is contained in:
2026-03-24 13:01:15 +01:00
parent 39f683a31e
commit 91d1616dd7
15 changed files with 548 additions and 393 deletions

View File

@@ -1,8 +1,8 @@
// useSaveSync.ts — Auto-save game state to backend every 30s
// Requires JWT token in localStorage (set by auth flow)
// Falls back silently if no token (guest mode)
// Cookie-based auth — credentials sent automatically
import { useEffect, useRef, useCallback } from "react";
import { useAuth } from "../context/AuthContext";
import type { GameState } from "../core/economy";
const SAVE_INTERVAL_MS = 30_000; // 30 seconds
@@ -15,16 +15,13 @@ interface SaveSyncOptions {
}
async function apiRequest(path: string, options: RequestInit = {}) {
const token = localStorage.getItem("token");
if (!token) return null;
const res = await fetch(`${BACKEND_URL}/api${path}`, {
...options,
credentials: "include",
headers: {
"Content-Type": "application/json",
"x-auth-token": token,
...options.headers,
},
...options,
});
if (!res.ok) {
@@ -37,17 +34,15 @@ async function apiRequest(path: string, options: RequestInit = {}) {
}
export function useSaveSync({ getGameState, onLoad, playTimeSeconds }: SaveSyncOptions) {
const { user } = useAuth();
const lastSaveRef = useRef<string | null>(null);
const loadedRef = useRef(false);
// Load save on mount (once)
useEffect(() => {
if (loadedRef.current) return;
if (loadedRef.current || !user) return;
loadedRef.current = true;
const token = localStorage.getItem("token");
if (!token) return;
apiRequest("/save").then((data) => {
if (data?.gameState) {
onLoad(data.gameState);
@@ -55,12 +50,11 @@ export function useSaveSync({ getGameState, onLoad, playTimeSeconds }: SaveSyncO
console.info("[SaveSync] Loaded save from server");
}
});
}, [onLoad]);
}, [onLoad, user]);
// Save function
const saveToServer = useCallback(async () => {
const token = localStorage.getItem("token");
if (!token) return;
if (!user) return;
const gameState = getGameState();
const result = await apiRequest("/save", {
@@ -71,37 +65,31 @@ export function useSaveSync({ getGameState, onLoad, playTimeSeconds }: SaveSyncO
if (result?.lastSave) {
lastSaveRef.current = result.lastSave;
}
}, [getGameState, playTimeSeconds]);
}, [getGameState, playTimeSeconds, user]);
// Auto-save interval
useEffect(() => {
const token = localStorage.getItem("token");
if (!token) return undefined;
if (!user) return undefined;
const interval = setInterval(() => {
saveToServer();
}, SAVE_INTERVAL_MS);
return () => clearInterval(interval);
}, [saveToServer]);
}, [saveToServer, user]);
// Save on page unload
useEffect(() => {
const handleUnload = () => {
const token = localStorage.getItem("token");
if (!token) return;
if (!user) return;
const gameState = getGameState();
const payload = JSON.stringify({ gameState, playTimeSeconds });
// Use fetch with keepalive for reliable save on tab close
// (sendBeacon doesn't support custom headers)
fetch(`${BACKEND_URL}/api/save`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-auth-token": token,
},
credentials: "include",
headers: { "Content-Type": "application/json" },
body: payload,
keepalive: true,
}).catch(() => {});
@@ -109,7 +97,7 @@ export function useSaveSync({ getGameState, onLoad, playTimeSeconds }: SaveSyncO
window.addEventListener("beforeunload", handleUnload);
return () => window.removeEventListener("beforeunload", handleUnload);
}, [getGameState, playTimeSeconds]);
}, [getGameState, playTimeSeconds, user]);
return { saveToServer, lastSave: lastSaveRef.current };
}