From 596e949eef5f3e3ae292f98e09609f31e20e4859 Mon Sep 17 00:00:00 2001
From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com>
Date: Mon, 21 Sep 2026 22:50:07 +0000
Subject: [PATCH] fix(form): keep Google sign-in across page refresh (#204)
---
src/server/templates/form.js | 80 ++++++++++++++++++++--
tests/test_generate_form.py | 128 +++++++++++++++++++++++++++++++----
2 files changed, 189 insertions(+), 19 deletions(-)
diff --git a/src/server/templates/form.js b/src/server/templates/form.js
index f785e01..c108d3a 100644
--- a/src/server/templates/form.js
+++ b/src/server/templates/form.js
@@ -29,6 +29,7 @@ let countdownTimer = null;
let mealsListDelegationBound = false;
const GOOGLE_AUTH_MAX_ATTEMPTS = 200;
+const GOOGLE_SESSION_KEY = "seahaven.meals.googleIdToken";
window.googleCredential = null;
@@ -95,6 +96,68 @@ function waitForGoogleAuth(attempt = 0) {
}
}
+function decodeJwtPayload(token) {
+ if (!token || typeof token !== "string") return null;
+ const parts = token.split(".");
+ if (parts.length < 2) return null;
+ try {
+ const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
+ const padded = b64 + "=".repeat((4 - (b64.length % 4)) % 4);
+ const json = new TextDecoder().decode(
+ Uint8Array.from(atob(padded), (c) => c.charCodeAt(0)),
+ );
+ return JSON.parse(json);
+ } catch (e) {
+ return null;
+ }
+}
+
+function googleTokenIsUsable(token) {
+ const payload = decodeJwtPayload(token);
+ if (!payload || !payload.email) return false;
+ if (typeof payload.exp === "number" && payload.exp * 1000 <= Date.now()) {
+ return false;
+ }
+ if (GOOGLE_CLIENT_ID && payload.aud && payload.aud !== GOOGLE_CLIENT_ID) {
+ return false;
+ }
+ return true;
+}
+
+function readStoredGoogleToken() {
+ try {
+ return sessionStorage.getItem(GOOGLE_SESSION_KEY);
+ } catch (e) {
+ return null;
+ }
+}
+
+function persistGoogleSession(token) {
+ try {
+ if (googleTokenIsUsable(token)) {
+ sessionStorage.setItem(GOOGLE_SESSION_KEY, token);
+ } else {
+ sessionStorage.removeItem(GOOGLE_SESSION_KEY);
+ }
+ } catch (e) {}
+}
+
+function clearGoogleSession() {
+ try {
+ sessionStorage.removeItem(GOOGLE_SESSION_KEY);
+ } catch (e) {}
+}
+
+function restoreGoogleSession() {
+ const token = readStoredGoogleToken();
+ if (!googleTokenIsUsable(token)) {
+ if (token) clearGoogleSession();
+ return false;
+ }
+ handleCredentialResponse({ credential: token });
+ return true;
+}
+
function initGoogleAuth() {
google.accounts.id.initialize({
client_id: GOOGLE_CLIENT_ID,
@@ -107,17 +170,19 @@ function initGoogleAuth() {
text: "signin_with",
width: 300,
});
+ if (restoreGoogleSession()) return;
+ try {
+ google.accounts.id.prompt();
+ } catch (e) {}
}
function handleCredentialResponse(response) {
- googleCredential = response.credential;
+ const token = response && response.credential;
+ const payload = decodeJwtPayload(token);
+ if (!payload || !payload.email) return;
+ googleCredential = token;
syncGoogleCredential();
- const b64 = response.credential
- .split(".")[1]
- .replace(/-/g, "+")
- .replace(/_/g, "/");
- const payloadBytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
- const payload = JSON.parse(new TextDecoder().decode(payloadBytes));
+ persistGoogleSession(token);
googleUser = { name: payload.name, email: payload.email };
document.getElementById("auth-overlay").classList.add("is-hidden");
@@ -149,6 +214,7 @@ function signOut() {
googleCredential = null;
googleUser = null;
syncGoogleCredential();
+ clearGoogleSession();
if (
CONFIG.authMode === "google" &&
typeof google !== "undefined" &&
diff --git a/tests/test_generate_form.py b/tests/test_generate_form.py
index 9bcadbf..f54477b 100644
--- a/tests/test_generate_form.py
+++ b/tests/test_generate_form.py
@@ -75,18 +75,20 @@ def _render_for_browser(*, google: bool = False) -> str:
return html
-def _google_credential() -> str:
- payload = json.dumps(
- {
- "name": "Test Admin",
- "email": "test-admin@example.com",
- "picture": (
- "data:image/svg+xml,"
- ""
- ),
- }
- ).encode()
- encoded = base64.urlsafe_b64encode(payload).decode().rstrip("=")
+def _google_credential(extra: dict | None = None) -> str:
+ payload_obj = {
+ "name": "Test Admin",
+ "email": "test-admin@example.com",
+ "picture": (
+ "data:image/svg+xml,"
+ ""
+ ),
+ }
+ if extra:
+ payload_obj.update(extra)
+ encoded = (
+ base64.urlsafe_b64encode(json.dumps(payload_obj).encode()).decode().rstrip("=")
+ )
return f"e30.{encoded}.signature"
@@ -410,6 +412,7 @@ class TestGenerateFormPlaywright:
id: {
initialize() {},
renderButton() {},
+ prompt() {},
disableAutoSelect() {},
},
},
@@ -933,6 +936,7 @@ class TestGenerateFormGooglePlaywright:
id: {
initialize() {},
renderButton() {},
+ prompt() {},
disableAutoSelect() {},
},
},
@@ -1007,6 +1011,105 @@ class TestGenerateFormGooglePlaywright:
finally:
browser.close()
+ def test_refresh_restores_google_session(self, tmp_path):
+ sync_api = pytest.importorskip("playwright.sync_api")
+ sync_playwright = sync_api.sync_playwright
+
+ out = tmp_path / "google-session-form.html"
+ out.write_text(_render_for_browser(google=True))
+ credential = _google_credential({"exp": 2_000_000_000})
+
+ with sync_playwright() as p:
+ try:
+ browser = p.chromium.launch(headless=True)
+ except Exception as exc:
+ raise RuntimeError(
+ "Chromium is required for form Playwright tests. "
+ "Run: playwright install --with-deps chromium"
+ ) from exc
+ try:
+ page = browser.new_page()
+ page.add_init_script(
+ """window.google = {
+ accounts: {
+ id: {
+ initialize() {},
+ renderButton() {},
+ prompt() {},
+ disableAutoSelect() {},
+ },
+ },
+ };"""
+ )
+ page.route(
+ "https://accounts.google.com/gsi/client",
+ lambda route: route.abort(),
+ )
+ _mock_form_routes(page)
+ page.goto(out.as_uri(), wait_until="domcontentloaded")
+ page.evaluate(
+ "(token) => handleCredentialResponse({ credential: token })",
+ credential,
+ )
+ page.wait_for_function(
+ """() => {
+ const app = document.getElementById('app');
+ return app && !app.classList.contains('is-hidden');
+ }"""
+ )
+ assert page.evaluate(
+ "() => sessionStorage.getItem('seahaven.meals.googleIdToken')"
+ )
+
+ page.reload(wait_until="domcontentloaded")
+ _mock_form_routes(page)
+ page.wait_for_function(
+ """() => {
+ const app = document.getElementById('app');
+ const overlay = document.getElementById('auth-overlay');
+ const email = document.getElementById('user-email');
+ return app && !app.classList.contains('is-hidden')
+ && overlay && overlay.classList.contains('is-hidden')
+ && email && email.textContent === 'test-admin@example.com';
+ }"""
+ )
+
+ page.evaluate("signOut()")
+ assert (
+ page.evaluate(
+ "() => sessionStorage.getItem('seahaven.meals.googleIdToken')"
+ )
+ is None
+ )
+ page.reload(wait_until="domcontentloaded")
+ _mock_form_routes(page)
+ page.wait_for_function(
+ """() => {
+ const overlay = document.getElementById('auth-overlay');
+ const app = document.getElementById('app');
+ return overlay && !overlay.classList.contains('is-hidden')
+ && app && app.classList.contains('is-hidden');
+ }"""
+ )
+
+ page.evaluate(
+ "(token) => sessionStorage.setItem('seahaven.meals.googleIdToken', token)",
+ _google_credential({"exp": 1}),
+ )
+ page.reload(wait_until="domcontentloaded")
+ _mock_form_routes(page)
+ page.wait_for_function(
+ """() => {
+ const overlay = document.getElementById('auth-overlay');
+ const app = document.getElementById('app');
+ return overlay && !overlay.classList.contains('is-hidden')
+ && app && app.classList.contains('is-hidden')
+ && !sessionStorage.getItem('seahaven.meals.googleIdToken');
+ }"""
+ )
+ finally:
+ browser.close()
+
@pytest.fixture(scope="class")
@classmethod
def signed_in_admin_page(cls, tmp_path_factory):
@@ -1036,6 +1139,7 @@ class TestGenerateFormGooglePlaywright:
id: {
initialize() {},
renderButton() {},
+ prompt() {},
disableAutoSelect() {},
},
},