Add Auth and Database#1
Merged
Merged
Conversation
Phase 18a adds real login/signup via Supabase Auth + JWKS-verified
backend middleware. Phase 18b moves preferences, course/lesson
progress, and editor project out of localStorage into Supabase
Postgres with RLS policies scoping reads/writes by auth.uid().
Chunk 1 post-audit fixes rolled into the same commit to avoid
shipping auth half-done.
Backend
- NEW middleware/authMiddleware.ts — jose JWKS verification, attaches
req.userId from sub; 401 on missing/invalid/expired. Mounted after
csrfGuard + rate-limit, before route handlers on every protected
route.
- NEW middleware/adminGuard.ts — placeholder that 403s until a role
system lands. Reserves a slot for future catalog-delete endpoints
without blocking user-owned DELETE /api/user/courses/:courseId.
- services/session/sessionManager.ts — SessionRecord gains userId;
rebind no longer 403s when another user owns the requested id
(oracle collapse: silently mints a fresh nanoid, same as the not-
found path). endSession / ping / status still enforce ownership.
- routes/execution.ts / project.ts / ai.ts — every getSession(sid)
swap for requireOwnedSession(sid, req.userId). Cross-user sid access
returns 403.
- middleware/aiRateLimit.ts + mutationRateLimit.ts — bucket key now
user:<userId> when authenticated, sid|ip fallback otherwise.
- NEW db/{client,preferences,courseProgress,lessonProgress,
editorProject}.ts — postgres.js connection + typed upserts against
the Phase 18b tables.
- NEW routes/userData.ts — GET/PATCH /api/user/preferences, courses,
lessons; GET/PUT /api/user/editor-project. Zod bounds on every
input: slug regex on courseId/lessonId, 200 KB cap on lastCode +
practiceExerciseCode jsonb, 500 KB cap on editor files, path regex
blocks .. traversal.
- config.ts — supabase.{url,jwksUrl,audience,issuer} section; startup
fails fast if required fields missing in production.
Frontend
- NEW auth/{supabaseClient,authStore,RequireAuth,HydrationGate,
generation}.ts/tsx — singleton client, Zustand store subscribing
to onAuthStateChange, route guard, hydration gate with Retry +
Sign-out escape, and a module-level generation counter that guards
stores against late writes across fast sign-out-then-sign-in.
- NEW pages/{LoginPage,SignupPage,ResetPasswordPage,AuthCallbackPage}
.tsx — hand-rolled auth UI with strength meter, live requirements
checklist, magic-link fallback, Google + GitHub OAuth buttons.
- api/client.ts — attaches Authorization: Bearer <access_token>;
global 401 handler signs out + redirects to /login.
- state/preferencesStore.ts + features/learning/stores/progressStore
.ts + state/projectStore.ts — hydrate(gen?) signature with auth-
generation guard; leaves hydrated=false + surfaces hydrateError on
failure (HydrationGate renders the Retry branch).
- features/learning/stores/progressStore.ts — savePracticeCode action
persists per-exercise WIP code via practiceExerciseCode jsonb
column; LessonPage restore prefers saved code over starter.
- hooks/useEditorProjectPersistence.ts — debounced PUT of the editor
project payload on file/tab/stdin changes.
- components/SettingsPanel.tsx — Account tab with email, sign-out,
deletion placeholder.
- Dropped the __dev__/* profiles shortcut; real auth supplants it.
Schema
- supabase/migrations/20260420000000_phase18b_user_data.sql — four
tables (user_preferences, course_progress, lesson_progress,
editor_project) keyed by user_id uuid REFERENCES auth.users ON
DELETE CASCADE. RLS policy auth.uid() = user_id on all four.
- supabase/migrations/20260420120000_practice_exercise_code.sql —
jsonb column for per-exercise WIP code.
E2E
- NEW fixtures/auth.ts — per-worker admin-create + programmatic
session inject. listAllUsers() walks paginated /admin/users so
accumulated stale test users don't silently pile past 1000.
- NEW specs/auth.spec.ts — signup → login → persistence-on-reload →
signout; /api/session 401 without Authorization; cross-user session
access returns 403 (with rebind oracle-collapse asserted separately
as a 200 with a fresh sid).
- NEW specs/cross-device.spec.ts — server-backed progress survives a
localStorage wipe on a second "device".
- Every existing spec: imports test from fixtures/auth so the
per-worker user is auto-logged-in.
Docker + dev flow
- docker-compose.yml — backend gains SUPABASE_URL, SUPABASE_JWT_*,
DATABASE_URL env vars.
- Root package.json — scripts/dev.sh wraps supabase start + docker
compose up. supabase/config.toml pins a local stack with realtime /
storage / edge-runtime / imgproxy / smtp excluded.
Coverage
- Backend: 302 passed, 1 skipped (from 255).
- Frontend: 168 passed (from 216 with dev-profile drops; net +
auth/progress/practice tests).
- E2E: 104 passed, 6 skipped.
- Typecheck clean on both packages.
Docs
- ARCHITECTURE.md + DEVELOPMENT.md sections refreshed for the auth +
user-data surface, env model (local stack dev, cloud project prod),
and Supabase CLI first-time setup.
Introduce a shared AuthLoader so RequireAuth and HydrationGate render the same DOM during the auth-resolve → store-hydrate handoff, killing the ~200ms flicker between two separate skeletons. Add a 600ms minimum display to prevent a one-frame flash on warm caches. HydrationGate now computes a 4-step progress (auth/prefs/progress/editor), surfaces which dependency is loading, and exposes a 3s soft-retry button plus an 8s stuck branch that offers Retry + Sign out. A11y sweep: UserMenu gets keyboard focus return on Escape and a per-user aria-label; auth submit buttons get aria-busy + descriptive labels; PasswordField announces a debounced strength summary to screen readers instead of firing on every keystroke.
…r shape Remove localStorage surfaces left over from 18b's move to Postgres: learnerStore, ownedStorage, progressSnapshot, ProgressIOControls, and the LearnerIdentity type. Three lesson pages now read user.id from useAuthStore directly instead of mirroring it into a separate key. Auth store drops the adopt/reset identity dance for the same reason. Session-ownership checks collapse to one shape: requireActiveSession throws HttpError(403/404/409), and callers in execution, project, and executeTests routes drop the res-writing + early-return ceremony. authMiddleware moves to the same shape — propagates HttpError(401/503) via next(err) — and the centralized errorHandler suppresses console noise for routine 401s. Extract per-language editor starter files out of projectStore.ts into a util/starters.ts module. Bump AuthLoader MIN_VISIBLE_MS 600 -> 1000 so the initial hydration gate reads as deliberate rather than a blip.
- Collect First/Last name at signup; editable in Account settings via supabase.auth.updateUser metadata; drives 2-char initials + display name in UserMenu. - Single toolbar entry point: remove SettingsButton + inline gear from all 5 pages; UserMenu → Settings is the one way in. - Settings modal → 3-tab layout (Account / AI / Appearance) with side-nav; Account adds Profile, Session, Danger zone with a timer-backed "Changes saved" toast that survives USER_UPDATED. - Misc UX polish: AuthShell footer, OAuth "Connecting…" state, PasswordField sr-only per-item announcement, AuthCallback pulseDot, WelcomeOverlay click-propagation guard, EditorPage effect-deps note. - Backend: userData.ts → HttpError; authMiddleware JWKS timeout 5s→2s. - Postgres: composite (user_id, updated_at DESC) indexes on course_progress + lesson_progress for Recent Activity reads. - E2E: tabbed-Settings aware `openSettings(page, tab)` helper; signup spec fills First/Last; 104/105 passing (1 SERVICE_KEY-gated).
Replaces the dual-target model (local CLI stack + one cloud project)
with two cloud projects on a single account: codetutor-dev for dev +
CI/E2E, codetutor-prod reserved for deploy. No users exist yet, so
the dual-path config was paying maintenance cost for no benefit.
- Delete local-CLI scaffolding: supabase/config.toml, .gitignore.
Keep supabase/migrations/ as the schema source of truth; apply to
both cloud projects via `npx supabase db push`.
- Delete committed frontend/.env.development (local defaults). Real
creds move to gitignored .env / .env.local / frontend/.env.*.local
siblings, documented by committed .example templates.
- Drop SUPABASE_AUTH_ISSUER env override + cached-issuer branch in
authMiddleware — cloud-only means issuer is always ${SUPABASE_URL}
/auth/v1. Strip host.docker.internal / 127.0.0.1:54321 fallbacks
from docker-compose, e2e fixtures, auth.spec, supabaseClient.
- Delete root package.json + lockfile + node_modules (only held the
Supabase CLI devDep); invoke via `npx supabase` on demand.
- Rewrite docs/DEVELOPMENT.md first-time setup; prune README stale
"no account needed" line + add .env copy steps.
Transaction-pooler compatibility fixes surfaced by the cloud-only
e2e pass:
- backend/src/db/client.ts: set `prepare: false` on the postgres.js
pool. Supabase's transaction pooler (port 6543) recycles conns
between txns and doesn't support prepared statements; the local
session pooler did.
- docker-compose.yml: surface VITE_SUPABASE_URL + VITE_SUPABASE_ANON
_KEY as runtime env on the frontend service so Vite's dev server
picks them up from the compose .env file (previous indirection
through frontend/.env.development is gone).
Verification: backend 302/302, frontend 168/168, e2e 103 passed,
1 skipped, 1 flake (Supabase cloud signup email rate-limit) across
15 specs run one-by-one against codetutor-dev.
Supabase free tier caps "emails sent" at 2/hour and the setting is greyed out without custom SMTP. GoTrue increments that counter on every signup even with email confirmation off, so the signup spec was a reliable flake on the second run within an hour. Poll for either the homepage redirect or the "email rate limit exceeded" banner; on the banner, test.skip() with a clear reason. Login spec below still exercises the full auth pipeline via admin-create + signInWithPassword, so coverage on real signup drops from "always asserts" to "asserts when the project isn't rate-capped" — acceptable given the hard free-tier ceiling.
Two Phase 18d follow-ups landed in `98ac457` that weren't yet reflected in the config table: VITE_SUPABASE_URL now flows from the compose `.env` to the frontend container's runtime env (rather than `frontend/.env.development.local`), and the backend's postgres.js pool runs with `prepare: false` because Supabase's transaction pooler on port 6543 doesn't support prepared statements.
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 30524964 | Triggered | Supabase Service Role JWT | 6aec192 | .env.example | View secret |
| 30524965 | Triggered | Generic Password | 98ac457 | .env.example | View secret |
| 30524966 | Triggered | JSON Web Token | 6aec192 | .env.example | View secret |
| 30524965 | Triggered | Generic Password | 98ac457 | .env.production.example | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secrets safely. Learn here the best practices.
- Revoke and rotate these secrets.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
msrivas-7
added a commit
that referenced
this pull request
Apr 21, 2026
Three defensive P0 items bundled into one deploy-affecting commit so the prod stack lands them together rather than in drips. #1 Container resource limits (docker-compose.prod.yml) - backend: 1.5 GiB mem / 1 CPU (+ 512 MiB reservation) - caddy: 128 MiB / 0.5 CPU - socket-proxy: 64 MiB / 0.25 CPU Caps the blast radius of a runaway Node process on the 4 GB B2s — without these, one OOM wedges the whole stack. Runner containers already have per-session caps via LocalDockerBackend. #2 Azure Backup (infra/azure/modules/backup.bicep + main.bicep wiring) Recovery Services Vault (RS0/Standard, LRS, 14d soft-delete) + V2 weekly policy (Sun 02:00 UTC, 4 weekly retention, 2d instant RP). VM enrollment is a one-time `az backup protection enable-for-vm` step documented in infra/azure/README.md — the protected-item resource name is too brittle in Bicep to be worth automating. Before this, `az snapshot list` was empty and a VM wipe would have lost Caddy's LE account key (rate-limited re-issuance). #3 Deploy rollback + no-more-systemctl-restart (infra/scripts/ vm-deploy-backend.sh + deploy.yml) The deploy workflow now captures PREV_SHA before `git reset --hard origin/main`, then delegates to the new VM script. The script does `docker compose up -d backend` instead of `systemctl restart codetutor-ai.service`, so Caddy stays up and TLS connections aren't dropped across a deploy. On /api/health failure it reverse-resets git, retags :rollback back to :latest, and brings backend up on the old image. Added infra/scripts/** to the deploy workflow's path trigger + backend filter so future script-only edits still deploy. First deploy on this new path runs under the OLD inline script (no rollback coverage); the new script lands as part of the same reset and protects every subsequent deploy.
msrivas-7
added a commit
that referenced
this pull request
Apr 23, 2026
Covers all 10 E2E gaps from the adversarial audit plus an axe-core a11y fence across /learn, /editor, and lesson pages. While writing the SSE drop spec, uncovered that a stream EOF without a done/error frame silently cleared the asking-indicator — fixed by surfacing a retry error so the user can recover. axe-core also surfaced missing aria-valuenow on the focusable splitter role; added the WAI-ARIA slider range attrs. New / extended specs: #1 multi-tab.spec.ts — distinct sessionIds, no output bleed #2 ai-tutor.spec.ts — SSE drop surfaces retry, not silence #3 free-tier.spec.ts — provider_auth_failed → platform recovery #4 multi-tab.spec.ts — 5 concurrent snapshots stay coherent #5 ai-tutor.spec.ts — hint counter rollback + success control #6 credential.test.ts — UTC rollover, resetAtUtc, clock-spoof #7 multi-tab.spec.ts — visibility wake + 404 → rebind #8 lesson-edge.spec.ts — prereq bounce writes no in_progress row #9 editor.spec.ts — non-ASCII stdin + source round-trip #10 learning.spec.ts — browser back/forward restores caches ++ a11y.spec.ts — axe fence on 3 pages, wcag2aa
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.