docs: Phase 3 portal — API_REFERENCE, ARCHITECTURE, DEPLOYMENT (email-change config), HANDOFF pass-48 (P3f)
This commit is contained in:
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
```
|
||||
<a href="{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email_change">Confirm this email change</a>
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user