Magic-link sign in UI and APIs

This commit is contained in:
adilallo
2026-04-06 16:37:15 -06:00
parent 331ed40234
commit 7218947df3
74 changed files with 1582 additions and 363 deletions
+54 -36
View File
@@ -7,7 +7,7 @@ Temporary working notes for building the backend. Safe to delete once the stack
## 1. Where we are
- **Next.js 16** single repo ([`package.json`](package.json)).
- **PostgreSQL + Prisma**: schema and migrations under `prisma/`; product APIs under `app/api/*` (health, auth/OTP, session, drafts, rules, templates, web-vitals).
- **PostgreSQL + Prisma**: schema and migrations under `prisma/`; product APIs under `app/api/*` (health, auth/magic-link, session, drafts, rules, templates, web-vitals).
- **Server modules** in `lib/server/` (db, session, mail, rate limiting, etc.).
- **Create flow** persists in the browser (`localStorage`); optional **server draft sync** when `NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true` and the user is signed in ([`app/create/context/CreateFlowBackendSync.tsx`](app/create/context/CreateFlowBackendSync.tsx)).
- **Web vitals** [`app/api/web-vitals/route.ts`](app/api/web-vitals/route.ts) still use **file-based** storage under `.next` (not suitable for multi-instance production).
@@ -17,19 +17,31 @@ Temporary working notes for building the backend. Safe to delete once the stack
Mirrors [CONTRIBUTING.md](../CONTRIBUTING.md) **API routes** table; handlers live under `app/api/*/route.ts`.
| Method | Path | Purpose |
| ---------- | ----------------------- | --------------------------------------------- |
| GET | `/api/health` | Liveness / DB check |
| GET | `/api/auth/session` | Current user or null |
| POST | `/api/auth/otp/request` | Send email OTP |
| POST | `/api/auth/otp/verify` | Verify OTP, set session cookie |
| POST | `/api/auth/logout` | Clear session |
| GET / PUT | `/api/drafts/me` | Load or save create-flow JSON (authenticated) |
| GET / POST | `/api/rules` | List or publish rules |
| GET | `/api/templates` | List curated templates |
| Method | Path | Purpose |
| ---------- | ------------------------------ | --------------------------------------------- |
| GET | `/api/health` | Liveness / DB check |
| GET | `/api/auth/session` | Current user or null |
| POST | `/api/auth/magic-link/request` | Send sign-in link email |
| GET | `/api/auth/magic-link/verify` | Validate token, set session cookie, redirect |
| POST | `/api/auth/logout` | Clear session |
| GET / PUT | `/api/drafts/me` | Load or save create-flow JSON (authenticated) |
| GET / POST | `/api/rules` | List or publish rules |
| GET | `/api/templates` | List curated templates |
**Product sign-in** uses **magic link** (`/api/auth/magic-link/*`).
**Also present (not in CONTRIBUTING table):** `POST` / `GET` [`/api/web-vitals`](../app/api/web-vitals/route.ts) — file-based store today; production path TBD (§7).
### HTTP API (profile / account — not implemented yet)
Planned for the signed-in profile/dashboard ([Figma profile frame](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22143-900069); [docs/backend-linear-tickets.md](backend-linear-tickets.md) Ticket 15; Linear **[CR-86](https://linear.app/community-rule/issue/CR-86/backend-profile-dashboard-account-figma-profile)**):
- Authenticated list of **own** `PublishedRule` rows (e.g. `GET /api/rules/me` or a strictly scoped query—**not** the same as public `GET /api/rules`).
- Owner-only **delete** and **duplicate** (clone) for published rules.
- **Delete account** (authenticated), with an explicit policy for drafts, sessions, and linked rules.
**Future (separate ticket):** **Change email** with verification (e.g. magic link to a new address, conflict handling)—**out of scope** for the profile milestone above.
---
## 2. What were building
@@ -41,7 +53,7 @@ Mirrors [CONTRIBUTING.md](../CONTRIBUTING.md) **API routes** table; handlers liv
- HTTP handlers under `app/api/…`
- Shared server code under `lib/server/…`
**Step 3.** Use the old backend only as a **product hint** (email OTP, saving rules, listing rules). Do **not** copy its Express layout or MySQL schema.
**Step 3.** Use the old backend only as a **product hint** (passwordless email sign-in, saving rules, listing rules). Do **not** copy its Express layout or MySQL schema.
---
@@ -51,9 +63,9 @@ Mirrors [CONTRIBUTING.md](../CONTRIBUTING.md) **API routes** table; handlers liv
**Step 2.** Use **Prisma**`schema.prisma`, `npx prisma migrate dev` / `migrate deploy`.
**Step 3.** Add **SMTP** (or Mailhog locally) for email OTP in deployed environments; dev can log OTP to the console when `SMTP_URL` is unset.
**Step 3.** Add **SMTP** (or Mailhog locally) for **magic-link** sign-in email in deployed environments; when `SMTP_URL` is unset in dev, the app can log the **verify URL** to the console (same pattern as [`lib/server/mail.ts`](lib/server/mail.ts)).
**Step 4.** **Redis / queues / Kubernetes** — not required for v1. **Exception:** before running **multiple app instances**, plan a **shared rate-limit store** (often Redis) for OTP endpoints; the current limiter is in-memory per process ([`lib/server/rateLimit.ts`](lib/server/rateLimit.ts)).
**Step 4.** **Redis / queues / Kubernetes** — not required for v1. **Exception:** before running **multiple app instances**, plan a **shared rate-limit store** (often Redis) for **passwordless email (magic-link request)**; the current limiter is in-memory per process ([`lib/server/rateLimit.ts`](lib/server/rateLimit.ts)).
---
@@ -61,14 +73,14 @@ Mirrors [CONTRIBUTING.md](../CONTRIBUTING.md) **API routes** table; handlers liv
Plain-English entities (names can evolve):
| Area | Purpose |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| **User** | Identified by email after OTP verification. |
| **Session** | **Custom v1:** HttpOnly cookie; opaque token; **hash** stored in DB ([`lib/server/session.ts`](lib/server/session.ts)). Not NextAuth/Lucia. |
| **OtpChallenge** | Short-lived email codes (hashed). |
| **RuleDraft** | **One** JSON blob per user (create-flow state). Schema already has **`updatedAt`**; no draft **versioning** or **multiple named drafts** in v1. |
| **PublishedRule** | Saved rule after publish (title, summary, document JSON). |
| **RuleTemplate** | Curated templates (slug, category, ordering). |
| Area | Purpose |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **User** | Identified by email after **magic link verification** (primary v1 path). An optional **display name** (or preferred name) could be added later for richer greetings; it does **not** block the profile page—no schema commitment in this roadmap pass alone. |
| **Session** | **Custom v1:** HttpOnly cookie; opaque token; **hash** stored in DB ([`lib/server/session.ts`](lib/server/session.ts)). Not NextAuth/Lucia. |
| **MagicLinkToken** | Short-lived **hashed** token for email sign-in links; optional `nextPath` for post-login redirect. |
| **RuleDraft** | **One** JSON blob per user (create-flow state). Schema already has **`updatedAt`**; no draft **versioning** or **multiple named drafts** in v1. |
| **PublishedRule** | Saved rule after publish (title, summary, document JSON). Profile UI badges such as **IN PROGRESS** may be **derived from `document` JSON**, a future `status` column, or UI-only—product decision when implementing Ticket 15. |
| **RuleTemplate** | Curated templates (slug, category, ordering). |
**Session follow-ups to implement or decide:** token **rotation** on sensitive events, whether **new login invalidates other sessions**, and **cleanup** of expired `Session` rows (job or lazy delete). Revisit a small auth library (e.g. Auth.js, Lucia) only if maintaining custom code becomes costly.
@@ -80,8 +92,8 @@ Align JSON shapes with `app/create/types.ts` as it matures.
## 5. Session and authentication (v1)
- **Decision:** **Custom** database-backed sessions + email OTP; cookies are **httpOnly**; tokens are hashed at rest.
- **OTP rate limiting:** **In-memory** is acceptable for a **single Node process**. It does **not** coordinate across instances—**add a shared limiter (e.g. Redis)** before horizontal scaling or serious abuse exposure.
- **Decision:** **Custom** database-backed sessions + **email magic link**; cookies are **httpOnly**; session and magic-link tokens are hashed at rest.
- **Rate limiting (magic-link request):** **In-memory** is acceptable for a **single Node process**. It does **not** coordinate across instances—**add a shared limiter (e.g. Redis)** before horizontal scaling or serious abuse exposure.
- Do **not** treat “switch to NextAuth/Lucia” as required for v1; document the custom lifecycle above instead.
---
@@ -92,8 +104,10 @@ Match the current API behavior; tighten as product evolves:
- **`GET /api/drafts/me` / `PUT /api/drafts/me`:** Authenticated user only; draft is **scoped to that user** (`userId`).
- **`POST /api/rules`:** Authenticated user only; rule is stored with **`userId`** (owner).
- **`GET /api/rules`:** **Public list** of published rules (metadata: id, title, summary, timestamps)—no auth required today. **Not** a private “my rules” feed unless you add a separate route later.
- **v1:** No **editing** or **deleting** published rules via API in the shipped handlers; no **sharing** or **collaborative ownership**—treat each rule as **owned by one user** until product defines more.
- **`GET /api/rules`:** **Public list** of published rules (metadata: id, title, summary, timestamps)—no auth required today. **Not** a private “my rules” feed unless you add a separate route later (see §1 “profile / account — not implemented yet” and Ticket 15).
- **Profile / owner scope (planned):** Authenticated **list own rules**, **delete own rule**, **duplicate own rule**—required for the signed-in dashboard in design; **v1 shipped handlers** may not include these until that work lands.
- **Delete account (planned):** Authenticated endpoint + UX to remove the user record per policy (cascade vs orphan `PublishedRule`, drafts, sessions)—Ticket 15. **Change email** is **not** part of that milestone; plan a **future ticket** for verified email updates.
- **v1 (shipped today):** No **editing** or **deleting** published rules via API in current handlers; no **sharing** or **collaborative ownership**—treat each rule as **owned by one user** until product defines more.
---
@@ -122,7 +136,7 @@ Match the current API behavior; tighten as product evolves:
**Backend behavior already in the repo:** Steps **510** match implemented Route Handlers and middleware (`lib/server/*`). **Step 11** (web vitals) is **not** production-ready (files under `.next`); treat as follow-up work aligned with §7.
**Product / frontend still open (not only “backend exists”):** Sign-in UI, wiring publish from the create flow, template seed + UI consumptionsee §12 and [docs/backend-linear-tickets.md](backend-linear-tickets.md).
**Product / frontend still open (not only “backend exists”):** Sign-in UI, wiring publish from the create flow, template seed + UI consumption, **profile / my rules dashboard** (Ticket 15)—see §12 and [docs/backend-linear-tickets.md](backend-linear-tickets.md).
---
@@ -149,12 +163,14 @@ npm run dev
**Step 5.** Confirm **health**: `GET /api/health` should return JSON.
**Step 6.** **OTP login** (happy path):
**Step 6.** **Magic-link sign-in** (happy path):
1. `POST /api/auth/otp/request` with `{ "email": "you@example.com" }`
2. Read the code from your mail catcher or server logs (dev).
3. `POST /api/auth/otp/verify` with `{ "email": "...", "code": "..." }`
4. `GET /api/auth/session` should show your user.
1. `POST /api/auth/magic-link/request` with `{ "email": "you@example.com" }` (optional `"next"` for redirect after verify).
2. Open the link from email, Mailhog, or **server logs** when `SMTP_URL` is unset (dev).
3. Browser hits `GET /api/auth/magic-link/verify?token=...` (and optional `next=...`); response sets the session cookie and redirects.
4. `GET /api/auth/session` should show your user in the same browser.
**Before wiring create-flow session UI:** Confirm the same browser that completed verify gets `user` from `GET /api/auth/session` (cookie + same-site). On **staging/production**, magic-link emails embed the app origin—misconfigured **`Host`** or TLS termination can produce broken links; align reverse proxy with the public site URL.
**Step 7.** **Drafts**: With a session, `GET /api/drafts/me` and `PUT /api/drafts/me` with `{ "payload": { ... } }` (create flow state object).
@@ -171,8 +187,8 @@ npm run dev
## 10. Security checklist
- **HTTPS** in staging/production; session cookie **Secure**.
- **Rate-limit** OTP (in-memory OK for one instance; **shared store before multi-instance**see §5).
- **Hash** OTP codes and session tokens before storing; short OTP expiry.
- **Rate-limit** magic-link **request**in-memory OK for one instance; **shared store before multi-instance** (see §5).
- **Hash** magic-link tokens and session tokens before storing; short **magic-link** TTL (align with implementation, e.g. 15 minutes).
- **Secrets** only in env / secret store — never commit `.env` with real values.
- **CORS:** prefer **same-origin** `/api/*`; if cross-origin, configure CORS and CSRF carefully.
@@ -204,16 +220,18 @@ npm run dev
**Step 2.** Set `NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true` to opt in to server drafts when logged in.
**Step 3.** Implement sign-in UI when you are ready: call the OTP routes, then rely on the browser cookie for `/api/drafts/me`.
**Step 3.** Sign-in UI: **`/login`** (and **Log in** in the site header) uses **magic link** (modal / page flow: request link → open verify URL); after verify, rely on the browser cookie for `/api/drafts/me`.
**Step 4.** On publish, call `POST /api/rules` from the completed step when the backend is required (wire when the final review UI is ready).
**Step 5.** **Profile / dashboard** (`/profile` or agreed path): signed-in hub for **my rules** (after Ticket 15 APIs exist), **duplicate** / **delete** rule actions, **logout**, **delete account**—aligned with [Figma profile](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22143-900069). **Change email** in design is **deferred** (hide, “coming soon,” or backlog) until a future account ticket; greeting copy can stay **static** or use **email local-part in UI only**—no `displayName` field required for MVP.
---
## 13. Optional later
- **Session library** spike (Auth.js, Lucia) if custom lifecycle cost grows.
- **Redis** (or similar) for **shared OTP rate limits** and horizontal scale.
- **Redis** (or similar) for **shared magic-link rate limits** and horizontal scale.
- **RuleDraft** versioning or multiple drafts per user.
- Standalone **API service** (Fastify/Hono) if scaling or workers demand it.
- **OpenAPI** if external API clients appear.