diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index 77ca09f..0b3ce46 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -968,3 +968,8 @@ Outbox + dispatcher; the webhook never sends inline. Pure render + dedupe in `li - Dispatcher `POST /api/cron/notifications` (nodejs, force-dynamic; constant-time `CRON_SECRET` bearer; 503 if unset, 401 on mismatch; `proxy.ts` whitelists `/api/cron`). Drains ≤20 oldest `pending` email rows with `attempts < MAX`, sends with the row id as Resend `Idempotency-Key` (overlap-safe), ~4.5/s spacing. Success → `sent`+`sent_at`; failure → `attempts++`, `pending` if retryable and budget remains else `failed`. Returns `{ scanned, sent, failed, retried }`. Hit once a minute by the droplet crontab. - `getMyNotifications()` — the caller's own last 15 notifications (service client; `notifications` is admin-RLS, mirrors `getMyBilling`). Surfaced read-only on `/me`. - `getMyPayments()` — the caller's own last 50 payments (service client; `payments` SELECT is treasurer-scoped, so the self-view is strictly scoped to the caller's `space_id` + `member_id`, same convention as `getMyBilling`). Surfaced read-only in the `/me` Membership tab. + +### Member self-serve portal (`/me`, product spine Phase 3) +`/me` is a 3-tab portal (Profile / Membership / Activity); the server page fetches, a client component renders. Profile editing reuses `updateMyProfile` + `discloseAffiliations` (affiliations re-disclosed only when changed, so the COI timestamp is preserved). Class signups / reservations are cancellable inline via the existing `cancelMySignup` / `cancelReservation` (ownership + space scoping still enforced server-side). +- `requestEmailChange({ email })` — member-initiated login-email change. Validates with `emailChangeSchema`, requires an active member, calls `supabase.auth.updateUser({ email }, { emailRedirectTo: {origin}/auth/confirm })`. With "Secure email change" on (recommended), Supabase double-confirms (old + new); the change applies to `auth.users` only after both links. Returns `{ success }` / `{ error }` (error includes "email already in use"). NEVER writes the denormalized email here. +- `GET /auth/confirm` — email-change landing. Reads `token_hash` + `type`, calls `verifyOtp({ type: 'email_change', token_hash })`, and on success syncs `space_members.email` (service client, by `user_id`) to the now-authoritative `auth.users` email — idempotent, post-verification only. Whitelisted in `proxy.ts` (exact path; link is clicked with no session). Requires Supabase project config (see DEPLOYMENT). diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 1d972e6..469d08b 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -667,6 +667,10 @@ admin/board/treasurer, no client write policy; the member self-view (`getMyNotifications`, surfaced on `/me`) is a validated action. The droplet's crontab POSTs once a minute. No new permission code. +### Member self-serve portal (`/me`; product spine Phase 3) + +`/me` is a 3-tab portal (Profile / Membership / Activity). The server page does all data fetching and passes plain data to a single client portal component (`me-portal-client.tsx`) that owns tab state and rendering; the read-only sections were moved verbatim from the prior flat page (no behavior change in the shell). Profile editing and inline cancels reuse existing server actions (`updateMyProfile`, `discloseAffiliations`, `cancelMySignup`, `cancelReservation`) — no new mutation surface, server-side ownership/space scoping unchanged. `getMyPayments` follows the established service-client self-view convention (treasurer-scoped RLS, strictly scoped to the caller). Self-service email change goes through Supabase Auth (`updateUser` + `/auth/confirm` `verifyOtp`); the denormalized `space_members.email` is synced only post-verification. The email-change flow depends on Supabase project config (template + redirect allowlist; see DEPLOYMENT) and is inert until that is set. No new permission code, no schema change. + --- ## 13. Known Limitations diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 5be0b24..8afdde8 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -64,6 +64,20 @@ Transactional email is an outbox drained by `POST /api/cron/notifications`. Add It hits the app locally (bypassing Caddy), self-throttles under Resend's rate limit, and is idempotent, so a missed or overlapping minute is harmless. Migration `041_notifications.sql` is applied automatically by `deploy.sh` like every other `scripts/0*.sql`; no manual database step. +### Member email change (Supabase Auth config — required) + +The self-serve "change my login email" feature (`/me` Profile tab) needs Supabase project configuration that is NOT in code. Until this is set, requesting an email change will send a link that does not land correctly. + +1. Authentication → URL Configuration: add `{APP_URL}/auth/confirm` to the redirect allowlist (alongside the existing `/auth/callback`). +2. Authentication → Providers → Email: keep **"Secure email change"** enabled (double confirm: both old and new address must verify). +3. Authentication → Email Templates → "Change Email Address": point the link at the confirm route with the recovery-style token, not the OAuth code flow: + +``` +Confirm this email change +``` + +With Secure email change on, Supabase sends this to both the old and new address; each link hits `/auth/confirm`, which `verifyOtp`s and (post-verification only) syncs the denormalized `space_members.email`. Email delivery uses the project's configured SMTP (or Supabase built-in if none); production should use a real SMTP provider. + ## Operational checklist - Monitor `/var/log/hackerspace-deploy.log` for deploy outcomes. diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index f6dec58..e260eda 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -4,6 +4,30 @@ Append-only. Newest entries on top. Keep each entry to one screen. --- +## 2026-05-18 (pass 48) — Product spine Phase 3: member self-serve portal (LOCAL, awaiting one reviewed deploy) + +Branch `main`. Built Phase 3 (tabbed self-serve portal), gated per sub-phase. Suite 521, build clean. NOT deployed. + +### Shipped (commits 5a72832, ef210c9, 6189e20, 2c847be, 5edd778, + this docs) +- **P3a** `/me` restructured into a 3-tab portal (Profile / Membership / Activity). Server page fetches; `me-portal-client.tsx` renders. Read-only sections moved verbatim, no behavior change. +- **P3b** Inline profile editing (Profile tab) via existing `updateMyProfile` + `discloseAffiliations`; affiliations re-disclosed only when changed. Added `bio` to the `updateMyProfile` param type (schema/column already supported it). +- **P3c** Self-serve Cancel for registered/waitlisted class signups + reserved equipment reservations via existing actions (`CancelAction` = AlertDialog + toast + refresh). Server-side ownership/scoping unchanged. +- **P3d** `getMyPayments` (service-client self-view, treasurer-scoped RLS, strictly caller-scoped) + read-only list in Membership tab. +- **P3e** Self-service email change: `requestEmailChange` -> `supabase.auth.updateUser` (Secure email change = double confirm); new `GET /auth/confirm` `verifyOtp(email_change)` + post-verification sync of denormalized `space_members.email`; `proxy.ts` whitelists `/auth/confirm` (exact). Profile-tab UI. +- **P3f** Docs (API_REFERENCE, ARCHITECTURE Phase 3 section, DEPLOYMENT email-change config) + this entry. + +### Owner action required (P3e is inert until done) — Supabase project config +- Auth → URL Configuration: add `{APP_URL}/auth/confirm` to the redirect allowlist. +- Auth → Providers → Email: keep "Secure email change" ON. +- Auth → Email Templates → "Change Email Address": link to `{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email_change` (NOT the default code-flow link). Full steps in docs/DEPLOYMENT.md. + +### State +- **Not deployed.** P3a-f committed locally; awaiting one reviewed deploy. Still unpushed before this: pass-46 + this/earlier docs. UI not browser-tested from here (typecheck/build only). +- Behavior-preserving for existing read-only views; new: profile edit, cancels, my-payments, email change (email change needs the Supabase config above to function). +- Deferred (from the pass-47 audit, still open): P2 polish items + P3 doc cross-ref + optional CI guards. + +--- + ## 2026-05-18 (pass 47) — Full-codebase audit + P1 remediation pass (LOCAL, awaiting one reviewed deploy) Branch `main`. Ran a 4-agent read-only audit (server actions, DB/RLS/types, security, architecture/drift). Verdict: strong codebase, zero P0, no cross-tenant leaks/RLS gaps/injection/secret-to-client. Then fixed all P1s, gated per sub-phase. Suite 521, build clean.