add shared computer feature

This commit is contained in:
JCEEE
2026-09-14 15:33:07 +01:00
parent 7003609afe
commit e0dd4c0721
15 changed files with 1137 additions and 7 deletions
+123
View File
@@ -0,0 +1,123 @@
# Shared-device switching (PIN)
Design decisions for children sharing a family computer/tablet — recorded
2026-09-11. Implements: multi-session cookies, `pins` collection, profile
picker, and the idle lock.
## Model
- **The PIN is NOT a login.** It selects among sessions that already exist on
the device. A PIN alone mints nothing on a fresh device; a stolen cookie
alone selects nothing without the PIN. Threat model: sibling mischief on a
family device (devtools-level bypass is explicitly accepted — data is
family-scoped and actions are reversible).
- Per-device state lives in cookies:
- `pb_token_<userId>` — one httpOnly cookie per child with a session on
this device (the "device is logged into both accounts" property).
- `pb_active` — which child session is currently active.
- `pb_token` — unchanged single session for parents (and legacy children).
Parent login supersedes kid mode by clearing `pb_active`; a kid switch
shadows (but does not delete) a parent session; resolving order is
`pb_active` first, then `pb_token`.
- Sessions are never the security boundary: the OTP gate still guards each
child's first-ever join on a device, and switching re-mints an expired token
server-side via the derived password (never exposed to the client).
## `pins` collection
- Superuser-only rules (like `otp`); all reads/writes via server endpoints
with session-role checks. Plaintext 3-digit PIN — deliberately recoverable
so a parent can read it out (Q1: View, not reset). Child changes their own
PIN in Preferences (requires current PIN).
- Fields: `famId` (rel), `userId` (rel), `pin` (text).
## Switch flow
`POST /api/switch-user { userId, pin }`:
1. Request must carry `pb_token_<userId>` (the profile must be on this
device). No session required — the picker is reachable with no active user.
2. PIN verified server-side via superuser (children must NOT be able to read
each other's pins, so collection rules can't be the check).
3. In-memory rate limit: 5 fails → 30s lockout per userId (3-digit space).
4. Refresh the device cookie's token; if expired, re-auth with the derived
password (heals sessions on switch).
5. Set `pb_token_<userId>` + `pb_active`; client full-reloads to that child's
dashboard URL (`/famSlug/handle`).
Per-profile removal: `POST /api/device/remove { userId }` — deletes that
cookie (+ `pb_active` if it was active). No session required (device-local
cookie surgery only).
## Picker
One component (`SharedPicker`) used everywhere:
- TopNav switcher button (always visible when the device has child sessions).
- Idle-lock return (overlay).
- `/{famSlug}/switch` — standalone landing when the device has child sessions
but no active session (e.g. after per-profile logout). The fam layout
redirects unauthenticated device requests to it. Links: "Add a child with a
code" → `/{famSlug}/join`, "Parent login" → `/login`.
Every pick (including "resume" as the current kid) requires the PIN.
## Idle lock
- Default 10 min; parent-adjustable in Family Settings: `Off / 2 / 10`
(`settings.lockMins`, 0 = off; `shared-device.md` settled default 10).
- Client-side only, child sessions only (parent sessions never lock).
- **Only fires when shared-computer mode is ON for the device** (see below) —
a personal device never locks, even with lockMins set.
- `localStorage['fam_last_active']` updated on click/key/touch (throttled
10s) and force-written on `pagehide` — survives browser close, sleep and
restarts. Next launch compares elapsed time; past the timeout → picker.
Live timer (15s interval) does the same mid-session.
- Resets: join wizard and picker switch write a fresh timestamp before
navigating, so freshly-activated sessions don't instantly lock.
## Shared-computer toggle (TopNav)
- **Storage: localStorage, NOT the DB.** Shared-ness is a property of *this
browser* — a DB flag on `fams`/`settings` would force PIN mode on every
device including a parent's phone. `localStorage['fam_shared_device']`
is the source of truth; a plain `fam_shared_device=1` cookie mirrors it
so server loads see it (`data.sharedDevice`). Neither is a security
boundary — the PIN + device session cookies remain the gate.
- **Location:** monitor icon in the TopNav (fam layout children slot, next
to the profile switcher). Rendered for any role, even logged out —
whoever holds the device can mark it shared. Active state: indigo fill.
- **What it gates:**
- Profile switcher (`kid-switch`) button — only shown when shared mode
is ON and the device holds child sessions.
- Idle lock — only fires for child sessions on shared devices.
- (Server `/switch` redirect unchanged — the picker is harmless on
- non-shared devices and the server can't do better without the cookie.)
- **PIN reminders on enable** (`GET /api/pins/status`, set/unset only —
values never leave the server except via the settings `revealPin`
action):
- Parent toggling sees the roster: who still needs a PIN (→ Settings →
Members) vs who is ready (→ read the PINs out to them).
- A child toggling sees only their own state: "remember your PIN" or
- "ask a parent to set one up" (no PIN → picker shows "ask a parent").
- Client flag helpers: `lib/client/shared-device.ts`
(`isSharedDevice` / `setSharedDevice`, writes the cookie mirror too).
## Join flow (child)
After OTP redeem:
1. If the device already has other child sessions → PIN setup (required).
2. Else ask "Will this computer be shared with a sibling?" — yes → PIN setup;
no → skip (PIN can be added later in Preferences; parents can set it in
Family Settings).
3. Land on the child's dashboard.
Parents keep the existing join (set own password, single session).
## Edge cases
- Two tabs share the timestamp → the active tab keeps the other from locking.
- Tab switches don't lock (kids alt-tab constantly).
- Expired sessions: the picker still shows the profile; switching re-mints.
- No PIN set on a profile → picker shows "ask a parent to set it up".