feat: PKCE auth + CI/CD deploy
Some checks failed
CI/CD — Build & Deploy / Build & Deploy (push) Failing after 25s
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:
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user