MyKindlebox/memory-bank/FEATURE_User_Accounts.md

90 lines
4.6 KiB
Markdown

# Feature: User Accounts & Onboarding
Replaces the single shared-password auth (`AUTH_PASSWORD` env var, in-memory
session Set) with real user accounts. Decided with the user on 2026-07-02.
## Scope decisions (user-confirmed)
| Question | Decision |
|---|---|
| Data scope | **Shared herd** — users are separate logins to the same rabbitry. No ownership columns on rabbits/litters/shows; nothing about the data model changes. |
| Account creation | **In-app, no self-registration** — a fresh install shows a one-time "create your account" setup screen; after that, any signed-in user can add more users from Settings. No email/invite infrastructure. |
| Roles | **Everyone equal** — no admin flag. Any user can manage users and settings. (Trust model: family/partners on a LAN.) |
| Onboarding extras | **Per-user preferences only** — `theme` and `weight_unit` become per-user; `rabbitry_name`, `kit_prefix`, `default_breed` (and the internal `kit_counter`) stay global. No first-run settings wizard, no guided tour. |
## Consequences to be aware of
- **Auth becomes mandatory.** Today, unset `AUTH_PASSWORD` = open access. After
this feature, a fresh install requires creating an account, and every request
requires a session. The "no password = open" mode is retired.
- **`MYKINDLEBOX_PASSWORD` / `AUTH_PASSWORD` is retired.** Removed from
docker-compose and the README. An existing deployment that upgrades sees the
setup screen on next visit (its `users` table is empty); herd data is
untouched. The old env var is simply ignored.
- Because you must be signed in to delete a user and you **cannot delete
yourself**, the last account can never be deleted — no lockout state exists.
## Data model
```
users:
id integer PK autoincrement
username text NOT NULL UNIQUE COLLATE NOCASE
password_hash text NOT NULL -- "scrypt:<salt hex>:<hash hex>"
theme text -- NULL = inherit global settings value
weight_unit text -- NULL = inherit global settings value
created_at text NOT NULL default current_timestamp
auth_sessions:
token text PK -- randomUUID
user_id integer NOT NULL FK -> users.id ON DELETE CASCADE
expires_at text NOT NULL -- ISO; 30 days from login
created_at text NOT NULL default current_timestamp
```
Sessions move from the in-memory Set to SQLite so a server restart/redeploy no
longer signs everyone out. Expired rows are deleted lazily whenever a session
check touches them. Deleting a user cascades their sessions (they're signed out
everywhere immediately).
## Password hashing
`node:crypto` `scryptSync` (N=16384 default params), 16-byte random salt,
64-byte key, stored as `scrypt:<salt>:<hash>`; verified with `timingSafeEqual`.
Deliberately **not** bcrypt/argon2 — no new native dependency, so the Alpine
Docker build stays exactly as it is. Failed logins get a flat ~350 ms delay as
cheap brute-force damping.
## API
- `GET /api/auth/status` → `{ configured, authenticated, user? }` —
`configured` now means "at least one user exists". `user` = `{ id, username }`
when authenticated.
- `POST /api/auth/setup` `{ username, password }` — creates the **first** user
and signs them in; 409 once any user exists. This is the onboarding endpoint.
- `POST /api/auth/login` `{ username, password }` (username case-insensitive).
- `POST /api/auth/logout` — deletes the session row.
- `GET /api/users` → `[{ id, username, created_at }]` (no hashes).
- `POST /api/users` `{ username, password }`.
- `PATCH /api/users/:id` `{ password }` — reset any user's password (everyone
equal; family trust model — also the "someone forgot theirs" path).
- `DELETE /api/users/:id` — 400 when `id` is the caller.
- **`/api/settings` keeps its exact shape**: GET merges the global rows with the
current user's non-NULL `theme`/`weight_unit` overrides; PATCH routes those two
keys to the user row and everything else to the settings table. Client
consumers (ThemeSync, useWeightUnit, Settings page) need no changes for prefs.
## Client
- `AuthProvider` gains a third gate: `configured: false` → `SetupScreen`
(create first account), else unauthenticated → `LoginScreen` (now
username + password), else app. Context exposes `{ user, logout }`.
- Settings gets a **Users** card: list users, add user, reset password, delete
(self-delete disabled). The Account card shows who is signed in.
- Password minimum: 8 chars, enforced on both setup and user creation.
## Out of scope (deliberate)
Roles/permissions, invite links, email anything, per-user data visibility,
password-strength meters, 2FA, audit logs.