MyKindlebox/memory-bank/FEATURE_User_Accounts.md

4.6 KiB

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.