From fd647bcc2e4429357d1f853193b25c8ccef178da Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 20 Jun 2026 14:55:19 +1000 Subject: [PATCH] feat: add dev-only login for local admin access without OAuth - POST /api/auth/dev-login creates/finds user, promotes to admin, sets session cookie - POST /api/auth/dev-logout clears session cookie - Dev login page at #/dev-login in the SPA - Fix: getSessionCookieAttributes omits Secure over HTTP (local dev fix) - Guarded: only works when PUBLIC_HOST=localhost, returns 403 otherwise - Justfile: dev-admin and dev-logout commands - README: local admin login instructions --- README.md | 21 ++ justfile | 20 ++ src/App.tsx | 7 +- src/routes/DevLoginPage.tsx | 62 +++++ tests/auth.test.ts | 13 +- tests/dev-auth.test.ts | 424 ++++++++++++++++++++++++++++++++++ worker/auth.ts | 10 +- worker/index.ts | 5 + worker/routes/api.dev-auth.ts | 132 +++++++++++ 9 files changed, 687 insertions(+), 7 deletions(-) create mode 100644 src/routes/DevLoginPage.tsx create mode 100644 tests/dev-auth.test.ts create mode 100644 worker/routes/api.dev-auth.ts diff --git a/README.md b/README.md index 6296fde..0d8a5e3 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,27 @@ just dev # start Worker on http://localhost:8787 Open `http://localhost:8787` in your browser — the SPA and API are both served by the Worker. +### Sign in as admin locally + +OAuth isn't available in local dev. Use the dev-only login instead: + +1. Start the worker: `just dev` +2. Open `http://localhost:8787/#/dev-login` in your browser +3. Enter your email (pre-filled with `wahyd4@gmail.com`) and click "Sign in as admin" + +Or via CLI: + +```bash +just dev-admin # creates admin user + session +just dev-admin other@example.com # different email +just dev-logout # clears the session cookie +``` + +This endpoint is guarded: it only works when `PUBLIC_HOST=localhost` (or `APP_BASE_URL` +contains `localhost`). In dev/prod deployments it returns 403. The dev login bypasses +OAuth, finds-or-creates the user, promotes them to admin, and sets a `heygo_session` +cookie — exactly what a real OAuth callback would do, minus the provider. + ### Just commands Run `just` (no args) to list all recipes. The most common ones: diff --git a/justfile b/justfile index 7c32807..614aa83 100644 --- a/justfile +++ b/justfile @@ -46,6 +46,26 @@ smoke: build migrate-local echo "--- Root (SPA) ---"; curl -sI http://localhost:{{port}}/ | head -1; \ kill $$SERVER_PID 2>/dev/null +# ── Local dev auth ───────────────────────────────────────── + +# Create admin user and sign in locally (prints URL to open in browser) +# Requires the worker to be running: `just dev` +# Usage: just dev-admin wahyd4@gmail.com +dev-admin email="wahyd4@gmail.com": + @echo "=== Creating admin user via dev login API ===" + @curl -s -X POST http://localhost:{{port}}/api/auth/dev-login \ + -H 'content-type: application/json' \ + -d '{"email":"{{email}}"}' + @echo + @echo "Or open http://localhost:{{port}}/#/dev-login in your browser" + +# Sign out locally (clears session cookie) +# Requires the worker to be running: `just dev` +dev-logout: + @curl -s -X POST http://localhost:{{port}}/api/auth/dev-logout + @echo + @echo "Signed out." + # ── Local D1 helpers ─────────────────────────────────────── # Run an arbitrary SQL query against local D1 (quote the SQL) diff --git a/src/App.tsx b/src/App.tsx index f6714b6..3216d9b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,18 +3,20 @@ import LoginPage from './routes/LoginPage'; import PrivateLinksPage from './routes/PrivateLinksPage'; import PublicLinksPage from './routes/PublicLinksPage'; import AdminReviewPage from './routes/AdminReviewPage'; +import DevLoginPage from './routes/DevLoginPage'; -type RouteId = 'private' | 'public' | 'admin' | 'login'; +type RouteId = 'private' | 'public' | 'admin' | 'login' | 'dev-login'; const ROUTES: readonly { id: RouteId; label: string }[] = [ { id: 'private', label: 'Private Links' }, { id: 'public', label: 'Public Directory' }, { id: 'admin', label: 'Admin Review' }, { id: 'login', label: 'Login' }, + { id: 'dev-login', label: 'Dev Login' }, ]; function readRouteFromHash(): RouteId { - const match = window.location.hash.match(/^#\/(private|public|admin|login)/); + const match = window.location.hash.match(/^#\/(private|public|admin|login|dev-login)/); return (match?.[1] as RouteId) ?? 'private'; } @@ -58,6 +60,7 @@ export default function App() { {route === 'public' ? : null} {route === 'admin' ? : null} {route === 'login' ? : null} + {route === 'dev-login' ? : null}