90 lines
4.6 KiB
Markdown
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.
|