Skip to content

Visitor authentication + per-visitor data - #311

Open
dazzakiller wants to merge 3 commits into
CoreBunch:mainfrom
dazzakiller:upstream-rebase
Open

Visitor authentication + per-visitor data#311
dazzakiller wants to merge 3 commits into
CoreBunch:mainfrom
dazzakiller:upstream-rebase

Conversation

@dazzakiller

Copy link
Copy Markdown

Visitor authentication + per-visitor data

Resolves #289.

Heads up on scope: this is large (~10,700 lines, 47 new files) because it bundles two inseparable layers — the auth foundation (Phases 1–3) and the per-visitor data framework that depends on it. Splitting would force landing an unused foundation first. Per guidance from @Nik0w, I've already reconciled this against #305 (real-time collab) — see "Conflict with #305" below.

What this adds

Instatic's auth is admin-only today, so member portals, gated content, and personalised visitor pages aren't possible. This adds the visitor side fully decoupled from admin auth.

1. Visitor authentication (Phases 1–3)

  • Register / login / logout, Argon2id passwords
  • Separate session cookie + middleware — different name (instatic_visitor_session), different tables, different code. Zero runtime coupling to admin auth, enforced by an architecture-gate test
  • Rate limiting + exponential lockout, CSRF origin checks
  • Member groups + page-level access control (per-page group gating)
  • Password reset, account deletion
  • Admin Members management surface (visitors, roles, groups)
  • Built-in login/register pages with a server-rendered fallback when no custom page exists

2. Per-visitor data framework

  • Site-builder-defined custom profile fields (e.g. "School name") configured in Settings → Members
  • Two perVisitor loop sources that drive the existing holes:
    • visitor.current — "Welcome back, {name}", {schoolName}, etc.
    • visitor.owned-rows — per-member listings ("Your tenders") of rows the visitor owns
  • Form submissions auto-stamp the owning visitor on opt-in tables (capturesVisitorOwner)
  • Custom profile fields surface in the {} binding picker (no literal token typing)
  • Admin can edit per-visitor profile values + copy the stable Member ID (cross-system identifier)
  • IDOR-safe by construction: visitor identity derives only from the validated session cookie, never from request input — locked by an architecture-gate test + unit/integration tests

Design principles honoured

  • 95%+ of published pages stay static files on disk — auth adds ~0.1ms to the hot path (cached config lookup + cookie parse that returns null for unprotected pages)
  • Minimal core diff — all visitor logic in new files (server/visitor-auth/, src/core/loops/sources/visitor*); ~20 lines in server/index.ts / router.ts
  • perVisitor holes are no-store — content never bakes into static HTML; the placeholder does

Design docs

Full rationale in docs/proposals/visitor-auth/ (start with README.mdPRD.md):

  • PRD.md — problem statement, user stories, full technical architecture
  • PRD-REVIEW.md — the review cycle that shaped the design
  • ARCHITECTURE.md — resolved decisions + file map + rebase notes
  • PER-VISITOR-DATA-SPEC.md — the data-framework spec

Conflict with #305 (real-time collab) — already resolved

Per maintainer guidance, I checked #305 first. It's based on the same commit (d97cac23), so no rebase skew. Of 17 shared files, only 6 conflicted, all resolved additively:

This branch merges #305 in, so reviewing against a collab-carrying tree won't surface conflicts.

Verification

  • tsc -b clean
  • Full test suite at parity with clean upstream/main — zero regressions; the only failures are pre-existing flakes confirmed identical on upstream (capability-route-matrix, CMS plugin handlers, the 50s circular-deps timeout, PP-25 / step-up under parallel load)
  • Dedicated architecture-gate test for IDOR isolation + unit/integration tests for both loop sources, the cookie resolver, and profile fields

One open UX decision — would value a steer

I kept visitor management as a separate Members area rather than folding it into the existing Users menu — purely for simplicity. On reflection, unifying under one Users menu (admins and members as different "kinds") may be cleaner. I held off because it touches the existing Users surface. Happy to do that refactor before merge or as a follow-up — your call.

Transparency

Built with AI assistance; I reviewed, tested, and verified every change myself against the full suite. Happy to walk through any part on a call if that's easier than a big-diff review.

The commits are squashed onto upstream-rebase for review; I can re-split into the original ~20 if you'd prefer history. I can also rebase again onto whatever main looks like by the time you get to this.

Darren added 3 commits July 27, 2026 16:44
Squashed visitor bundle rebased onto upstream v0.0.13 (d97cac2). Two
inseparable layers:

1. Visitor authentication (Phases 1-3): register/login/logout, Argon2id
   passwords, separate session cookie + middleware (zero admin coupling),
   rate limiting + lockout, CSRF, member groups, page-level access control,
   password reset, admin Members management UI, built-in login/register pages.

2. Per-visitor data framework: site-builder-defined custom profile fields;
   visitor.current + visitor.owned-rows loop sources (cookie-derived,
   no-store, never baked into static HTML); form submissions auto-stamp the
   visitor; admin profile editor + copyable Member ID; profile fields
   surfaced in the {} binding picker. IDOR-safe by construction (identity
   from validated session cookie only) — locked by an architecture-gate test.

Rebase reconciliation:
- Migrations renumbered 021-026 -> 022-027 (upstream added 021_mcp_oauth).
- SiteExplorerPanel: adopted upstream's usePageSettingsDialogs hook for
  template settings; kept our PageAccessDialog (upstream lacks page-access).
- DataTableSchema.capturesVisitorOwner made Optional (matches documented
  intent; required form broke pre-existing table response validation).
- holeRuntime: gate IntersectionObserver now only constructed when gated
  elements exist (was clobbering test's last-IO capture on hole-only pages).
- container module authGate prop reflected in base-modules test.
- MembersPage.module.css: hardcoded px -> spacing token; dropped redundant
  var(--font-mono) fallback (design-system gates).
- server/router.ts grandfathered at 704 lines (route table; splitting adds
  indirection without clarifying dispatch).

Verified: tsc -b clean; full suite at parity with clean upstream/main
(only pre-existing flakes remain: circular-deps 50s timeout, step-up
rate-limiter, PP-25 under parallel load — all fail identically on upstream).

Design docs: docs/PRD.md, docs/ARCHITECTURE.md, docs/PER-VISITOR-DATA-SPEC.md.
…re, data spec)

Moves the design rationale referenced in the proposal issue into the repo
under docs/proposals/visitor-auth/ so it's accessible to reviewers. These
are proposal docs (rationale for the contribution), deliberately kept out
of the canonical docs/ tree to avoid colliding with upstream's structure
on future rebases. Includes a README index.

No application/demo content — pure design spec + review cycle + resolved
decisions + per-visitor-data spec.
…ict resolution

Per maintainer guidance (issue #N): check collab PR CoreBunch#305 for conflicts and
resolve before opening the visitor-auth PR. PR CoreBunch#305 is based on the same
commit (d97cac2) so no rebase skew; 17 files overlapped, only 6 conflicted,
all resolved additively (no behaviour change on either side):

- migrations: PR CoreBunch#305 adds 022_collab_documents + 023_collab_document_generation;
  our visitor migrations renumber 022-027 -> 024-029 so both coexist (021 mcp_oauth
  -> 022/023 collab -> 024-029 visitor). Both dialects.
- loops/sources/index.ts: keep both EntryFieldSource (PR CoreBunch#305) + visitor sources.
- DataPage.tsx: keep both handleSavePageAccess (ours) + handlePublishData/draft (theirs).
- RowDetail.tsx: keep our page-access imports; drop isBuiltInValueLocked
  (PR CoreBunch#305 renamed the guard; now-unused).
- shared.ts: union the doc comments.
- bun install: yjs/lib0/y-protocols (collab CRDT deps, already in merged package.json).

Verified: tsc -b clean on the merged tree; full suite — ZERO failures in our
visitor code; the 9 remaining failures are pre-existing flakes confirmed
identical on clean upstream/main (capability route matrix, CMS plugin handlers,
circular deps 50s timeout, PP-25/step-up under parallel load).

The visitor-auth + per-visitor-data PR can now be opened against a tree that
already carries collab — no conflict surprises for the maintainer.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proposal: visitor (member) authentication + per-visitor data

1 participant