migrate auth v2 code
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user