feat(20-03): unify member editor + declutter admin members panel

- playwright-cli verified: Members tab shows tappable rows, no retired buttons
- Row tap opens 'Edit member' sheet; per-section saves keep sheet open
- 'Add member' trigger opens 'Add member' sheet in create mode
- Profile save fires 'Profile saved.' toast; sheet stays open (D-05)
- eslint + prettier + typecheck + vitest (275 tests) all pass
- Fix pre-existing prettier drift in docs/*, CLAUDE.md, README.md, api/admin.ts
This commit is contained in:
Lucas Berger
2026-06-18 17:39:00 -04:00
parent 9e6b004541
commit 9b62887f0f
17 changed files with 334 additions and 322 deletions
+98 -97
View File
@@ -25,54 +25,54 @@ No API key or `Authorization` header is used. Credentials are never included in
## Endpoints Overview
| Method | Path | Auth | Description |
| ------ | ------------------------------------- | --------- | -------------------------------------------------- |
| GET | `/health` | None | DB liveness check |
| GET | `/callback` | None | OIDC authorization-code exchange |
| GET | `/api/setup/status` | None | Setup wizard completion status |
| POST | `/api/setup/config` | None | Store OIDC and VAPID config (wizard step 1) |
| POST | `/api/setup/validate/db` | None | Validate DB connectivity (wizard step) |
| POST | `/api/setup/validate/oidc` | None | Validate OIDC issuer discovery (wizard step) |
| POST | `/api/setup/validate/vapid` | None | Validate VAPID key pair (wizard step) |
| POST | `/api/setup/credential` | None | Store first admin's Fastmail credential (wizard) |
| POST | `/api/setup/complete` | None | Lock the setup wizard |
| GET | `/api/auth/mode` | None | Auth mode discovery (local vs OIDC enabled) |
| POST | `/api/auth/local/login` | None | Local username+password login |
| POST | `/api/auth/local/logout` | None | Clear local session cookie |
| GET | `/api/auth/local/logout` | None | Clear local session cookie (browser redirect alias)|
| GET | `/api/login` | OIDC | OIDC login entry point, redirects to `/` |
| GET | `/api/me` | Session | Current user identity, role, and setup status |
| POST | `/api/me/credential` | Session | Member self-service Fastmail credential update |
| POST | `/api/me/password` | Session | Member self-change local password |
| POST | `/api/me/link-oidc` | Session | Initiate OIDC identity link for a local user |
| GET | `/api/events` | Session | Windowed calendar occurrences |
| POST | `/api/events/create` | Session | Enqueue a new event write |
| PATCH | `/api/events/:uid/edit` | Session | Enqueue an event update |
| DELETE | `/api/events/:uid` | Session | Enqueue an event delete |
| GET | `/api/events/sync-status` | Session | Outbox status for a UID |
| GET | `/api/events/writable-calendars` | Session | Calendars the member can write to |
| GET | `/api/lists` | Session | All lists accessible to the member |
| POST | `/api/lists` | Session | Create a list |
| PATCH | `/api/lists/:id` | Session | Update list name or sharing |
| DELETE | `/api/lists/:id` | Session | Delete a list (owner only) |
| GET | `/api/lists/:id/items` | Session | All items in a list |
| POST | `/api/lists/:id/items` | Session | Add an item to a list |
| PATCH | `/api/list-items/:itemId` | Session | Update a single list item field |
| DELETE | `/api/list-items/:itemId` | Session | Delete a list item |
| GET | `/api/sse/heartbeat` | Session | SSE heartbeat stream |
| GET | `/api/sse/lists` | Session | Scoped live-list SSE stream |
| GET | `/api/push/vapid-public-key` | Session | VAPID public key for push subscribe |
| POST | `/api/push/subscription` | Session | Register a push subscription |
| DELETE | `/api/push/subscription` | Session | Remove push subscriptions for caller |
| GET | `/api/admin/members` | Admin | List members with credential status |
| POST | `/api/admin/members` | Admin | Create a new local member |
| POST | `/api/admin/members/:id/password` | Admin | Reset a local member's password |
| POST | `/api/admin/credentials` | Admin | Validate and store a member's Fastmail credential |
| GET | `/api/admin/calendars` | Admin | List synced calendars |
| PUT | `/api/admin/calendars/:id/shared` | Admin | Designate the shared family calendar |
| GET | `/api/admin/config/timezone` | Admin | Get household timezone |
| PUT | `/api/admin/config/timezone` | Admin | Set household timezone |
| POST | `/api/admin/config/timezone/seed` | Admin | Seed household timezone if not yet set |
| Method | Path | Auth | Description |
| ------ | --------------------------------- | ------- | --------------------------------------------------- |
| GET | `/health` | None | DB liveness check |
| GET | `/callback` | None | OIDC authorization-code exchange |
| GET | `/api/setup/status` | None | Setup wizard completion status |
| POST | `/api/setup/config` | None | Store OIDC and VAPID config (wizard step 1) |
| POST | `/api/setup/validate/db` | None | Validate DB connectivity (wizard step) |
| POST | `/api/setup/validate/oidc` | None | Validate OIDC issuer discovery (wizard step) |
| POST | `/api/setup/validate/vapid` | None | Validate VAPID key pair (wizard step) |
| POST | `/api/setup/credential` | None | Store first admin's Fastmail credential (wizard) |
| POST | `/api/setup/complete` | None | Lock the setup wizard |
| GET | `/api/auth/mode` | None | Auth mode discovery (local vs OIDC enabled) |
| POST | `/api/auth/local/login` | None | Local username+password login |
| POST | `/api/auth/local/logout` | None | Clear local session cookie |
| GET | `/api/auth/local/logout` | None | Clear local session cookie (browser redirect alias) |
| GET | `/api/login` | OIDC | OIDC login entry point, redirects to `/` |
| GET | `/api/me` | Session | Current user identity, role, and setup status |
| POST | `/api/me/credential` | Session | Member self-service Fastmail credential update |
| POST | `/api/me/password` | Session | Member self-change local password |
| POST | `/api/me/link-oidc` | Session | Initiate OIDC identity link for a local user |
| GET | `/api/events` | Session | Windowed calendar occurrences |
| POST | `/api/events/create` | Session | Enqueue a new event write |
| PATCH | `/api/events/:uid/edit` | Session | Enqueue an event update |
| DELETE | `/api/events/:uid` | Session | Enqueue an event delete |
| GET | `/api/events/sync-status` | Session | Outbox status for a UID |
| GET | `/api/events/writable-calendars` | Session | Calendars the member can write to |
| GET | `/api/lists` | Session | All lists accessible to the member |
| POST | `/api/lists` | Session | Create a list |
| PATCH | `/api/lists/:id` | Session | Update list name or sharing |
| DELETE | `/api/lists/:id` | Session | Delete a list (owner only) |
| GET | `/api/lists/:id/items` | Session | All items in a list |
| POST | `/api/lists/:id/items` | Session | Add an item to a list |
| PATCH | `/api/list-items/:itemId` | Session | Update a single list item field |
| DELETE | `/api/list-items/:itemId` | Session | Delete a list item |
| GET | `/api/sse/heartbeat` | Session | SSE heartbeat stream |
| GET | `/api/sse/lists` | Session | Scoped live-list SSE stream |
| GET | `/api/push/vapid-public-key` | Session | VAPID public key for push subscribe |
| POST | `/api/push/subscription` | Session | Register a push subscription |
| DELETE | `/api/push/subscription` | Session | Remove push subscriptions for caller |
| GET | `/api/admin/members` | Admin | List members with credential status |
| POST | `/api/admin/members` | Admin | Create a new local member |
| POST | `/api/admin/members/:id/password` | Admin | Reset a local member's password |
| POST | `/api/admin/credentials` | Admin | Validate and store a member's Fastmail credential |
| GET | `/api/admin/calendars` | Admin | List synced calendars |
| PUT | `/api/admin/calendars/:id/shared` | Admin | Designate the shared family calendar |
| GET | `/api/admin/config/timezone` | Admin | Get household timezone |
| PUT | `/api/admin/config/timezone` | Admin | Set household timezone |
| POST | `/api/admin/config/timezone/seed` | Admin | Seed household timezone if not yet set |
---
@@ -129,12 +129,12 @@ Stores non-secret OIDC and VAPID configuration into `app_config`. Returns `423`
}
```
| Field | Type | Required | Constraints |
| ---------------- | ------ | -------- | ------------------------------ |
| `oidcIssuer` | string | Yes | HTTPS URL |
| `oidcClientId` | string | Yes | 1256 characters |
| `vapidPublicKey` | string | Yes | 1512 characters |
| `appExternalUrl` | string | Yes | HTTPS URL, max 512 characters |
| Field | Type | Required | Constraints |
| ---------------- | ------ | -------- | ----------------------------- |
| `oidcIssuer` | string | Yes | HTTPS URL |
| `oidcClientId` | string | Yes | 1256 characters |
| `vapidPublicKey` | string | Yes | 1512 characters |
| `appExternalUrl` | string | Yes | HTTPS URL, max 512 characters |
**Response 200**
@@ -187,10 +187,10 @@ Creates the first admin user (no OIDC identity yet, `claimed: false`) and valida
}
```
| Field | Type | Required | Constraints |
| --------------- | ------ | -------- | ---------------- |
| Field | Type | Required | Constraints |
| --------------- | ------ | -------- | ------------------------------- |
| `fastmailEmail` | string | Yes | Valid email, max 256 characters |
| `appPassword` | string | Yes | 1500 characters |
| `appPassword` | string | Yes | 1500 characters |
Zod validation errors for this route never echo received values (the app password is never included in error responses).
@@ -233,6 +233,7 @@ Pre-auth endpoint. Returns which authentication methods are currently enabled. U
Validates a username + password against `local_credentials` and issues a signed `local-session` JWT cookie. Pre-auth — reachable without a session.
Rate limiting is per-username (not per-IP):
- 5 failures within 60 seconds → `429 Too Many Requests`
- 10 cumulative failures → `423 Account Locked` (auto-expires after 15 minutes or on admin password reset)
@@ -244,10 +245,10 @@ Timing-oracle defense: `verifyPassword` (scrypt) is always called, even for unkn
{ "username": "alice", "password": "hunter2" }
```
| Field | Type | Required | Constraints |
| ---------- | ------ | -------- | ---------------- |
| Field | Type | Required | Constraints |
| ---------- | ------ | -------- | -------------------------- |
| `username` | string | Yes | 1128 characters (trimmed) |
| `password` | string | Yes | 11000 characters |
| `password` | string | Yes | 11000 characters |
Zod validation errors never echo received values.
@@ -296,14 +297,14 @@ Display name is derived from OIDC claims in priority order: `name` → `preferre
}
```
| Field | Type | Description |
| -------------------- | ------- | ---------------------------------------------------------------- |
| `id` | integer | Stable member ID |
| `displayName` | string | Derived from OIDC claims or set by admin |
| `color` | string | Member's assigned color (hex) |
| `isAdmin` | boolean | Whether the member has the admin role |
| `needsProviderSetup` | boolean | True when no Fastmail credential is stored for this member |
| `hasLocalCredential` | boolean | True when a local username/password credential exists |
| Field | Type | Description |
| -------------------- | ------- | ---------------------------------------------------------- |
| `id` | integer | Stable member ID |
| `displayName` | string | Derived from OIDC claims or set by admin |
| `color` | string | Member's assigned color (hex) |
| `isAdmin` | boolean | Whether the member has the admin role |
| `needsProviderSetup` | boolean | True when no Fastmail credential is stored for this member |
| `hasLocalCredential` | boolean | True when a local username/password credential exists |
**Error responses:** `401` if the session is invalid.
@@ -323,11 +324,11 @@ Member self-service endpoint to set or rotate their own Fastmail CalDAV app pass
}
```
| Field | Type | Required | Constraints |
| --------------- | ------ | -------- | ---------------- |
| `providerType` | string | Yes | Must be `"caldav"` |
| Field | Type | Required | Constraints |
| --------------- | ------ | -------- | ------------------------------- |
| `providerType` | string | Yes | Must be `"caldav"` |
| `fastmailEmail` | string | Yes | Valid email, max 256 characters |
| `appPassword` | string | Yes | 1500 characters |
| `appPassword` | string | Yes | 1500 characters |
**Response 200**`{ "ok": true }`
@@ -348,9 +349,9 @@ Self-service password change for local-auth members. Requires the current passwo
}
```
| Field | Type | Required | Constraints |
| ----------------- | ------ | -------- | ---------------- |
| `currentPassword` | string | Yes | 1+ characters |
| Field | Type | Required | Constraints |
| ----------------- | ------ | -------- | -------------------- |
| `currentPassword` | string | Yes | 1+ characters |
| `newPassword` | string | Yes | Minimum 8 characters |
**Response 200**`{ "ok": true }`
@@ -942,10 +943,10 @@ Creates a new local-auth member: inserts a `users` row and a `local_credentials`
}
```
| Field | Type | Required | Constraints |
| ----------------- | ------ | -------- | ---------------- |
| `displayName` | string | Yes | 1256 characters |
| `username` | string | Yes | 1128 characters |
| Field | Type | Required | Constraints |
| ----------------- | ------ | -------- | -------------------- |
| `displayName` | string | Yes | 1256 characters |
| `username` | string | Yes | 1128 characters |
| `initialPassword` | string | Yes | Minimum 8 characters |
Zod validation errors never echo received values (the initial password is never included in error responses).
@@ -997,12 +998,12 @@ Validates and stores a Fastmail CalDAV app password for any household member. Pe
}
```
| Field | Type | Required | Constraints |
| --------------- | ------- | -------- | ---------------- |
| `userId` | integer | Yes | Positive integer |
| `providerType` | string | Yes | Must be `"caldav"` |
| Field | Type | Required | Constraints |
| --------------- | ------- | -------- | ------------------------------- |
| `userId` | integer | Yes | Positive integer |
| `providerType` | string | Yes | Must be `"caldav"` |
| `fastmailEmail` | string | Yes | Valid email, max 256 characters |
| `appPassword` | string | Yes | 1500 characters |
| `appPassword` | string | Yes | 1500 characters |
Zod validation errors and CalDAV validation failures return `400` with `{ "error": "Invalid request" }` — the app password is never echoed.
@@ -1022,7 +1023,7 @@ Lists all synced calendars with their shared-calendar designation.
{
"calendars": [
{ "id": 1, "displayName": "Personal", "isShared": false },
{ "id": 2, "displayName": "Family", "isShared": true }
{ "id": 2, "displayName": "Family", "isShared": true }
]
}
```
@@ -1065,8 +1066,8 @@ Validates and upserts the household IANA timezone into `app_config`.
{ "timezone": "America/Toronto" }
```
| Field | Type | Required | Constraints |
| ---------- | ------ | -------- | -------------------------- |
| Field | Type | Required | Constraints |
| ---------- | ------ | -------- | ------------------------------------ |
| `timezone` | string | Yes | Valid IANA timezone, 164 characters |
**Response 200**`{ "ok": true }`
@@ -1105,17 +1106,17 @@ All error responses use a consistent JSON envelope.
{ "error": "Human-readable message" }
```
| HTTP Status | Meaning |
| ----------- | ------------------------------------------------------------------------------ |
| `400` | Invalid request parameters (e.g., malformed date window) |
| `401` | Session missing or invalid |
| HTTP Status | Meaning |
| ----------- | -------------------------------------------------------------------------------------------------------- |
| `400` | Invalid request parameters (e.g., malformed date window) |
| `401` | Session missing or invalid |
| `403` | Authenticated but not authorized (wrong owner, sharee attempted owner-only op, non-admin on admin route) |
| `404` | Resource not found |
| `409` | Conflict (e.g., duplicate username) |
| `422` | Valid request but cannot be fulfilled (e.g., user has no calendar configured) |
| `423` | Locked (setup already complete, or account locked after too many failed logins)|
| `429` | Too many requests (login rate limit exceeded for this username) |
| `503` | DB or downstream service unavailable |
| `404` | Resource not found |
| `409` | Conflict (e.g., duplicate username) |
| `422` | Valid request but cannot be fulfilled (e.g., user has no calendar configured) |
| `423` | Locked (setup already complete, or account locked after too many failed logins) |
| `429` | Too many requests (login rate limit exceeded for this username) |
| `503` | DB or downstream service unavailable |
Zod validation failures return `400` with a structured body from `@hono/zod-validator` rather than the `{ "error": "..." }` envelope. Exception: credential and password routes use a `noEchoHook` that always returns `{ "error": "Invalid request" }` to prevent echoing submitted secrets in error details.