diff --git a/auth-v2.md b/auth-v2.md new file mode 100644 index 0000000..b6c2767 --- /dev/null +++ b/auth-v2.md @@ -0,0 +1,138 @@ +# Auth architecture + +Current implementation: **SvelteKit directly + PocketBase**. Hono is **not** part of +auth today — it is reserved for future services (email, etc.). The `/api` proxy target +still points at it, but no auth traffic flows through it. + +``` + ┌────────────────────────────────────────────────────────┐ + │ BROWSER (Svelte) │ + │ /login /signup forms · $app/forms · use:enhance │ + └──────────────────────────┬─────────────────────────────┘ + │ 1. form action POST /login + │ /signup (returns result) + ▼ + ┌────────────────────────────────────────────────────────┐ + │ SVELTEKIT SERVER (Node) │ + │ │ + │ hooks.server.ts (runs once per request, first) │ + │ · read httpOnly cookie pb_session │ + │ · createPbClient(token) → pb.authRefresh() │ + │ · → { id, username, role, famId } │ + │ · event.locals.user / event.locals.pbToken │ + │ · route guards: public vs authed vs /admin │ + │ │ + │ +page.server.ts (actions / loads) │ + │ login.ts / signup.ts / session.ts │ + │ · createPbClient / createSuperPbClient │ + │ (ABSOLUTE PB URL - server-to-server, no proxy) │ + └──────────────────────────┬─────────────────────────────┘ + │ 2. PB API (REST) │ + ▼ + ┌────────────────────────────────────────────────────────┐ + │ POCKETBASE (100.103.22.104:8090) │ + │ collections: users (auth) · fams · members · ... │ + │ │ + │ PB is the SOURCE OF TRUTH: │ + │ · password hashing (bcrypt-style) │ + │ · JWT token issue + expiry (exp claim) │ + │ · authRefresh / token rotation │ + │ · per-collection API rules (authz boundary) │ + └────────────────────────────────────────────────────────┘ + + Future (NOT auth): SvelteKit → /api proxy → Hono → email & other services +``` + +## Request lifecycle (authenticated) + +1. Browser sends request; sends cookie `pb_session` (httpOnly, sameSite=lax, secure in prod). +2. `hooks.server.ts` extracts the token. +3. `createPbClient(token)` builds a PB client pre-authenticated as that user. +4. `pb.collection('users').authRefresh()`: + - validates the token (PB JWTs can't be checked offline), + - returns the fresh record → `event.locals.user = { id, username, role, famId }`, + - returns a fresh token; if it changed, the cookie is rolled forward. +5. Route guard runs (public / authed / admin-only). +6. `+page.server.ts` / `+server.ts` use `locals.user` for identity; use the token-backed + PB client for any CRUD so PB's collection rules apply. + +## Key decisions to replicate in another project + +- **Server-only PB client** lives in `src/lib/server/`; never imported by browser code. + Secrets (superuser creds) stay server-side. +- **Absolute PB base URL** in the SDK (e.g. `http://host:8090`), NOT a relative `/pb`. + All calls run in Node where relative URLs fail. The Vite `/pb` proxy is a browser-only + convenience and is unnecessary for server-side calls. +- **Env vars**: read via `$app/env/private` for private vars (declared in `src/env.ts`), + never `$app/env/public`. +- **`createSuperPbClient`** = anonymous client + `_superusers` auth, used for admin-style + creates (signup). Superusers bypass PB collection rules. +- **Session cookie**: PB JWT in `pb_session`; expiry governed by the token's `exp`, cookie + `maxAge` is just a ceiling; `authRefresh` rolls it forward. +- **Authz boundary** lives in PocketBase collection rules (famId scoping), not just app code. + +--- + +# Signup flow (`/signup`) + +Multi-step form on a single route. All steps run as **form actions** on the server; the +`use:enhance` handler only advances the step on a non-failure result. + +``` + ADMIN USER + +-----------------------------------------+ 1. POST ?/signup {familyName,email,password} + | +-----------+ +-----------+ +--------+ | ─────────────────────────────► + | | step 1 | | step 2 | | step 3 | | (use:enhance advances step on success) + | | family+ | | username | | child | | + | | email+pass| | | | name | | + | +-----------+ +-----------+ +--------+ | + +-----------------------------------------+ + ▲ 3. set httpOnly cookie pb_session + │ return {success:true} + ▼ + ┌────────────────────────────────────────────────────────────────────────┐ + │ /signup/+page.server.ts actions: signup · username · child │ + └────────────────────────────────────────────────────────────────────────┘ + │ + ?/signup │ ?/username ?/child + ─────────────────────┼───────────────────┬─────────────────────── + signupAdmin() │ setUsername() │ createChild() + ─────────────────────┼───────────────────┴─────────────────────── + │ ▼ (famId from locals.user) + ▼ + createSuperPbClient() ── superuser-authenticated PB client + │ + ├─(1) fams.create({ name, slug }) → family.id + ├─(2) users.create({ email,password,name, → user, role not set + │ famId: family.id }) + ├─(3) users.authWithPassword(email,password) → token + └─(4) setSessionCookie(cookies, token) + │ + ▼ + hooks.server.ts picks up the cookie next request + → authRefresh → event.locals.user populated → user is "logged in" + + Failure path: any PB error → throw SignupError(msg) + → action returns fail(status, { message }) + → FE renders form.message, does NOT advance step +``` + +## Signup step detail + +| Step | Action | Server fn | Writes | Returns | +| ---- | ------------ | ------------- | --------------------- | ---------------------------- | +| 1 | `?/signup` | `signupAdmin` | fams + users | cookie set, `{success:true}` | +| 2 | `?/username` | `setUsername` | users.name | `{success:true, username}` | +| 3 | `?/child` | `createChild` | members (name, famId) | `{success:true, code}` | + +## Notes / caveats in the current code + +- `users` collection has **no `role`/`username`** fields yet — `setUsername` writes to + `name`, and `role` isn't persisted (hooks reads it as `undefined`). Add `role` + + `username` to `users` if you need role-based guards (`/admin` relies on `role==='admin'`). +- `createChild` writes a `members` record with `inviteCode`, but `members` has no + `inviteCode` field (it's dropped). The member is created without a `users` auth record, + so it can't log in yet — see "Extending — Child / device accounts (Option C)" in the + root `readme.md`. +- `fams.createRule` is `null` (admin-only), `users.createRule` is `""` (public); signup + uses a superuser client, so both work regardless. diff --git a/frontend/src/routes/signup/new.page.svelte b/frontend/src/routes/signup/new.page.svelte new file mode 100644 index 0000000..16564e4 --- /dev/null +++ b/frontend/src/routes/signup/new.page.svelte @@ -0,0 +1,159 @@ + + + + {#if form?.message} +

{form.message}

+ {/if} + {#if step === 1} +
+ + + + +
+ +

+ Don't have a family yet? Create one +

+ {/if} + {#if step === 2} +

Step 2

+

Now for something more personal:

+
+ + +
+ {/if} + {#if step === 3} +

Step 3

+

Would you like to add a child device now?

+
+ + +
+

Or skip straight to admin

+ admin dashboard + {/if} + {#if step === 4} +

Step 4

+

Nice - now share this device login code with {step3.child}:

+
+ {form.code} +
+ {/if} +
+ +