migrate auth v2 code

This commit is contained in:
JCEEE
2026-08-16 10:11:39 +01:00
parent 3dd94b8a7c
commit c73ced7894
45 changed files with 1965 additions and 1519 deletions
+43 -41
View File
@@ -12,69 +12,71 @@
- SvelteKit (SSR frontend, internal :2080) + Hono proxy (internal :3456) + nginx (container :3001)
- PocketBase (separate Coolify service at `pb.chores.app.com`, :8090)
- Stripe one-time donations
- Coolify CRON → `GET /api/weekly-cron`
- Stripe one-time donations — **not implemented** (only `settings.webhookUrl` exists)
- Coolify CRON → `GET /api/weekly-cron` — **not implemented** (weekly settlement is manual via `complete-week`/`simulateEow`)
- Deployment: Coolify, Cloudflare DNS
## Auth
| Role | Auth | Session | Record in |
| -------------- | -------------------------- | -------------------------- | ------------ |
| Admin (parent) | PB email+pass | 24hr JWT | `fam_admins` |
| Member (child) | Invite code + device token | `device_token` cookie only | `members` |
| Role | Auth | Session | Record in |
| -------------- | --------------------------------------------- | -------------------------- | ------------------------- |
| Admin (parent) | PB email+pass | 24hr JWT `pb_token` cookie | `users` (role `parent`) |
| Member (child) | Invite OTP + server-derived password | httpOnly `pb_token` cookie | `users` (role `child`) |
| Superuser | PB `_superusers` (server-side only, `pb-admin`) | — | — |
- **Admins** (parents) have a PB auth record + `fam_admins` record. They authenticate via email/password login, get a session cookie (`session`) with `{ famId, userId, famSlug, memberName, role: "parent" }`.
- **Members** (children) exist only in the `members` collection. They authenticate via invite code + device token (SHA-256 hashed). The `device_token` cookie is set on join; no session cookie.
- **Platform superuser** (`_superusers`) used only server-side by `pb-admin.ts` for cross-family queries (e.g. `/admin` stats dashboard). Not an app role.
- The layout (`[fam]/+layout.server.ts`) derives `isParent` and `role` centrally from the session cookie — child pages use `page.data.isParent` or `page.data.role` from `$app/state`.
- **Admins** (parents) are `users` records (role `parent`). They authenticate via email/password login, get an httpOnly `pb_token` cookie with `{ id, name, username, role: "parent", famId, color }`.
- **Members** (children) are `users` records (role `child`); PB `username` = `{famSlug}:{handle}` (globally-unique auth identity; `handle` = whitespace-free lowercase name), URL segment = `handleOf(username)`, `name` = display name. Their PB password is **derived** server-side (`MEMBER_SECRET + famSlug + handle`); access is gated by a 20-min OTP in `user_configs`, then `authWithPassword`. They get the same httpOnly `pb_token` cookie. There is **no `members` collection**.
- **Platform superuser** (`_superusers`) used only server-side by `pb-admin.ts` for cross-family queries (e.g. `/admin` stats dashboard) and OTP/signup writes. Not an app role.
- The layout (`[fam]/+layout.server.ts`) derives `isParent` and `role` centrally from the session — child pages use `page.data.isParent` or `page.data.role` from `$app/state`.
- Because `pb_token` is httpOnly, the browser PB SDK is seeded from `page.data.pbToken` via `initPb(token)` in the layout `onMount` (not `document.cookie`).
## PB Collections (all scoped by `famId`)
## PB Collections (all scoped by `famId`; child/member = `users` row)
- `fams` — name, slug, inviteCode, stripeCustomerId, featureFlags
- `members` — famId, name, color, deviceToken(hashed), deviceTokenHint
- `users` — auth collection; famId, role (`parent`|`child`), username (`{famSlug}:{handle}`), name, color, email (admin only)
- `user_configs` — famId, userId, otp, colour, updatedAt (OTP gate for child join)
- `fams` — name, slug, stripeCustomerId, featureFlags
- `chore_templates` — famId, name, defaultValue, defaultFrequency
- `assigned_chores` — famId, memberId, templateId, frequency, value
- `completions` — famId, memberId, assignedChoreId, date
- `weekly_history` — famId, memberId, weekStart, pointsEarned, moneyEarned
- `rewards` — famId, memberId, source, label, value, claimed, claimedAt
- `monthly_bonuses` — famId, month, prizeType, prizeValue, winnerMemberId
- `assigned_chores` — famId, userId, templateId, frequency, value
- `completions` — famId, userId, assignedChoreId, date
- `weekly_history` — famId, userId, weekStart, pointsEarned, moneyEarned
- `rewards` — famId, userId, source, label, value, claimed, claimedAt, claimable, settleDate
- `monthly_bonuses` — famId, month, prizeType, prizeValue, winnerUserId
- `settings` — famId, pointsThreshold, weeklyBonus, webhookUrl
## Routes
```
/ Landing (SaaS marketing)
/admin Admin panel - statistic dashboard, and any donations made
/join/:code Member invite code
/join/:code/:member Member invite with pre-selected member
/admin Platform super-admin stats dashboard (and any donations)
/login · /logout Parent email/password login / logout
/signup Parent + family signup
/{famSlug}/join/{username} Member invite (OTP join), auto-fills from ?code=
/{fam} Fam dashboard
/{fam}/admin Admin panel
/{fam}/:username Member kanban & admin dashboard (role determined by session)
/{fam}/:username/preferences User preferences (admin→fam_admins, member→members)
/{fam}/:username/settings Family admin settings (session required)
/{fam}/:username/chores Chore templates & assignment grid
/{fam}/:username/rewards Rewards overview
/{fam}/:username/bonuses Bonus configs & evaluation
/api/* Hono proxy (webhooks, CRON)
/{fam}/{username} Parent → admin overview, Child → member kanban (role from session)
/{fam}/{username}/chores Chore templates & assignment grid
/{fam}/{username}/ledger Rewards / chores / todos ledger
/{fam}/{username}/bonuses Bonus configs & evaluation
/{fam}/{username}/preferences User preferences (parent→users, member→users)
/{fam}/{username}/settings Family admin settings (parent only)
/api/* Hono proxy (data layer; webhooks/CRON not implemented)
```
## Data Flow
### Reads (both roles)
- **Parent (admin):** `famStore.init()` fetches all collections via PB SDK (authenticated via `pb_token` cookie).
- **Child (member):** `famStore.init()` fetches all collections via PB SDK — **unauthenticated/anonymous**. All family-scoped collections have public `listRule` / `viewRule` (empty string = allow all), so reads work without any auth. PB SDK `.subscribe()` also works anonymously for public collections.
- **Parent (admin):** `famStore.init()` fetches all collections via PB SDK (authenticated via the `pb_token` cookie / seeded `initPb(token)`).
- **Child (member):** `famStore.init()` fetches all collections via PB SDK — authenticated via their `pb_token` (role `child`). Family-scoped collections have public `listRule` / `viewRule`, so reads work regardless; `.subscribe()` works for both roles.
- **TopNav season pills:** Read from `famStore.seasons` — reactive, no extra fetches needed.
### Writes (both roles go through Hono proxy)
### Writes
- **Chore toggle:** Browser → Hono proxy → PB (auth via device token or admin JWT)
- **Admin CRUD:** Form actions → Hono proxy → PB (admin JWT via `sessionHeaders`)
- **Member updates:** Browser → Hono proxy → PB (auth via `x-device-token` + `x-device-famid`)
- **Chore toggle:** Browser → Hono proxy → PB (member auth via `Authorization: Bearer <pb_token>`)
- **Admin CRUD:** Form actions / `hono.admin.*` → Hono proxy → PB (admin JWT via `sessionHeaders`)
- **Member updates:** Browser → Hono proxy → PB (auth via `Bearer <pb_token>`)
- **Reward creation:** After completion toggle, Hono proxy creates reward if threshold met
- **Weekly CRON:** Coolify → `GET /api/weekly-cron` on Hono → Hono queries PB, computes summaries, upserts weekly_history
- **Stripe donate:** Browser → Hono `/api/stripe/create-checkout` → Stripe → Hono webhook → update fam
- **WhatsApp:** Deferred — Hono CRON handler has pluggable notification interface
- **Weekly settlement:** NOT via CRON — manual `complete-week` action or `simulateEow` preview in settings. `/api/weekly-cron` (Coolify) is not implemented.
- **Stripe / WhatsApp:** not implemented — only the `settings.webhookUrl` field exists.
### UI reactivity
@@ -150,8 +152,8 @@ Two patterns based on who's acting:
| Pattern | Who | Frequency | Sensitivity | Optimistic? | Auth |
| ---------------------------- | ------ | -------------------- | ------------------------ | --------------------------------------------- | ------------------------- |
| Direct `fetch` + `memberApi` | Member | High (chore toggles) | None | Yes (instant UI, reconcile on response) | `x-device-token` header |
| Form action | Admin | Low (CRUD) | High (settings, members) | No — form is server-side, wait for round trip | httpOnly `session` cookie |
| Direct `fetch` + `memberApi` | Member | High (chore toggles) | None | Yes (instant UI, reconcile on response) | `Authorization: Bearer <pb_token>` |
| Form action | Admin | Low (CRUD) | High (settings, members) | No — form is server-side, wait for round trip | httpOnly `pb_token` cookie |
**Member direct fetch** — optimistic UI via local state mutation, reconciled on response:
@@ -195,7 +197,7 @@ All admin and member pages use the following pattern:
- Every collection query includes `famId = @request.auth.famId` filter
- Super admin bypasses famId filter (access via PB admin API)
- `deviceToken` stored as SHA-256 hash; never log raw tokens
- Child PB passwords are derived (`MEMBER_SECRET + famSlug + username`); the child join gate is a transient OTP in `user_configs`. No device tokens. Never log raw tokens/secrets.
- **Admin → Proxy**: `hono.admin.*` in `$lib/server/hono.ts` — uses `sessionHeaders(event)` (server-side only, requires `RequestEvent`)
- **Member → Proxy (server)**: `memberApi.*` in `$lib/client/api.ts` — use inside `+page.server.ts` load/actions; `BASE_URL` resolves to Hono port on server
- **Member → Proxy (browser)**: `memberApi.*` in `$lib/client/api.ts` — use inside `+page.svelte`; `BASE_URL` is empty, Vite proxies `/api/*` to Hono