docs: refresh project documentation against current codebase
Publish / publish (push) Successful in 26s
Publish / publish (push) Successful in 26s
This commit is contained in:
+519
-34
@@ -2,16 +2,22 @@
|
||||
|
||||
# API Reference
|
||||
|
||||
FamilySync exposes a Hono HTTP API served on port 3000. All `/api/*` routes require an authenticated session (OIDC via Authelia, or `DEV_AUTH_BYPASS=true` for local development). Unauthenticated requests to protected routes receive a `302` redirect to Authelia's authorize endpoint, not a `401`, except where noted.
|
||||
FamilySync exposes a Hono HTTP API served on port 3000. All `/api/*` routes require an authenticated session (OIDC via Authelia, local username/password, or `DEV_AUTH_BYPASS=true` for local development). Unauthenticated requests to protected routes receive a `302` redirect to Authelia's authorize endpoint, not a `401`, except where noted.
|
||||
|
||||
## Authentication
|
||||
|
||||
The API uses OIDC session cookies managed by `@hono/oidc-auth`. The authorization-code + PKCE flow is handled transparently:
|
||||
The API supports two session mechanisms that coexist on the same `/api/*` guard chain:
|
||||
|
||||
**OIDC session (Authelia):** Managed by `@hono/oidc-auth`. The authorization-code + PKCE flow is handled transparently:
|
||||
|
||||
1. The PWA navigates to `GET /api/login`. This route sits under `/api/*`, where the `@hono/oidc-auth` guard is mounted.
|
||||
2. For a request with no valid session cookie, the OIDC middleware intercepts it before the route handler runs and 302-redirects to Authelia's authorize endpoint.
|
||||
3. Authelia posts the authorization code to `GET /callback`, which exchanges it for tokens and sets an `httpOnly; Secure; SameSite` session cookie, then continues back to `/api/login` — whose handler now redirects to `/` (the app shell).
|
||||
4. All subsequent `/api/*` requests carry the session cookie automatically. (The root `/` and static assets are served outside the `/api/*` guard.)
|
||||
4. All subsequent `/api/*` requests carry the session cookie automatically.
|
||||
|
||||
**Local session (username + password):** Available when OIDC is not configured or for members who have not linked an OIDC identity. `POST /api/auth/local/login` issues a signed JWT `local-session` cookie. The local-session middleware validates it on every `/api/*` request and sets the user context, so OIDC guard is bypassed for already-authenticated local users.
|
||||
|
||||
**Session precedence:** Local-session middleware runs first. If `c.get('user')` is already set (local session or dev bypass), the OIDC guard is skipped entirely.
|
||||
|
||||
**Dev bypass:** When `DEV_AUTH_BYPASS=true` and `NODE_ENV != production`, the OIDC guard is disabled and a fixed dev user (id `1`) is injected into every request. Never enable in production.
|
||||
|
||||
@@ -19,31 +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/login` | OIDC | Login entry point, redirects to `/` |
|
||||
| GET | `/api/me` | OIDC | Current user identity and color |
|
||||
| GET | `/api/events` | OIDC | Windowed calendar occurrences |
|
||||
| POST | `/api/events/create` | OIDC | Enqueue a new event write |
|
||||
| PATCH | `/api/events/:uid/edit` | OIDC | Enqueue an event update |
|
||||
| DELETE | `/api/events/:uid` | OIDC | Enqueue an event delete |
|
||||
| GET | `/api/events/sync-status` | OIDC | Outbox status for a UID |
|
||||
| GET | `/api/events/writable-calendars` | OIDC | Calendars the member can write to |
|
||||
| GET | `/api/lists` | OIDC | All lists accessible to the member |
|
||||
| POST | `/api/lists` | OIDC | Create a list |
|
||||
| PATCH | `/api/lists/:id` | OIDC | Update list name or sharing |
|
||||
| DELETE | `/api/lists/:id` | OIDC | Delete a list (owner only) |
|
||||
| GET | `/api/lists/:id/items` | OIDC | All items in a list |
|
||||
| POST | `/api/lists/:id/items` | OIDC | Add an item to a list |
|
||||
| PATCH | `/api/list-items/:itemId` | OIDC | Update a single list item field |
|
||||
| DELETE | `/api/list-items/:itemId` | OIDC | Delete a list item |
|
||||
| GET | `/api/sse/heartbeat` | OIDC | SSE heartbeat stream |
|
||||
| GET | `/api/sse/lists` | OIDC | Scoped live-list SSE stream |
|
||||
| GET | `/api/push/vapid-public-key` | OIDC | VAPID public key for push subscribe |
|
||||
| POST | `/api/push/subscription` | OIDC | Register a push subscription |
|
||||
| DELETE | `/api/push/subscription` | OIDC | Remove push subscriptions for caller |
|
||||
| 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 |
|
||||
|
||||
---
|
||||
|
||||
@@ -67,13 +96,190 @@ Unauthenticated. Performs a `SELECT 1` against MariaDB to prove connectivity.
|
||||
|
||||
---
|
||||
|
||||
## Setup Wizard
|
||||
|
||||
The setup wizard surface is reachable pre-authentication. All setup routes return `423` once `isSetupLocked()` returns true (i.e., after `POST /api/setup/complete` has been called or the database has an effective OIDC configuration).
|
||||
|
||||
### `GET /api/setup/status`
|
||||
|
||||
Returns whether the first-run wizard has been completed. Always reachable (no 423 guard — the PWA needs this to decide whether to show the wizard).
|
||||
|
||||
**Response 200**
|
||||
|
||||
```json
|
||||
{ "setupComplete": false, "dbName": "familysync" }
|
||||
```
|
||||
|
||||
`dbName` is the value of the `DB_NAME` environment variable (non-secret, for display in the wizard UI). `setupComplete: true` indicates the wizard is locked.
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/setup/config`
|
||||
|
||||
Stores non-secret OIDC and VAPID configuration into `app_config`. Returns `423` if setup is already locked.
|
||||
|
||||
**Request body**
|
||||
|
||||
```json
|
||||
{
|
||||
"oidcIssuer": "https://auth.example.com",
|
||||
"oidcClientId": "familysync",
|
||||
"vapidPublicKey": "BNF8dFt...",
|
||||
"appExternalUrl": "https://familysync.example.com"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Constraints |
|
||||
| ---------------- | ------ | -------- | ------------------------------ |
|
||||
| `oidcIssuer` | string | Yes | HTTPS URL |
|
||||
| `oidcClientId` | string | Yes | 1–256 characters |
|
||||
| `vapidPublicKey` | string | Yes | 1–512 characters |
|
||||
| `appExternalUrl` | string | Yes | HTTPS URL, max 512 characters |
|
||||
|
||||
**Response 200**
|
||||
|
||||
```json
|
||||
{ "ok": true }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/setup/validate/db`
|
||||
|
||||
Proves DB connectivity via `SELECT 1`. Returns `423` if setup is locked.
|
||||
|
||||
**Response 200** — `{ "ok": true }`
|
||||
|
||||
**Response 503** — `{ "ok": false, "error": "DB unavailable" }`
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/setup/validate/oidc`
|
||||
|
||||
Fetches `{oidcIssuer}/.well-known/openid-configuration` (5-second timeout) to validate the issuer stored by `POST /api/setup/config`. Returns `423` if setup is locked.
|
||||
|
||||
**Response 200** — `{ "ok": true }`
|
||||
|
||||
**Response 400** — `{ "ok": false, "error": "OIDC discovery failed. Check the issuer URL." }`
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/setup/validate/vapid`
|
||||
|
||||
Validates the VAPID public key stored in `app_config` against the `VAPID_PUBLIC_KEY` and `VAPID_PRIVATE_KEY` environment variables. The submitted public key must exactly equal `process.env.VAPID_PUBLIC_KEY`. Returns `423` if setup is locked.
|
||||
|
||||
**Response 200** — `{ "ok": true }`
|
||||
|
||||
**Response 400** — `{ "ok": false, "error": "VAPID public key does not match..." }`
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/setup/credential`
|
||||
|
||||
Creates the first admin user (no OIDC identity yet, `claimed: false`) and validates/encrypts/stores their Fastmail CalDAV app password. Returns `423` if setup is locked; `409` if an unclaimed admin row already exists (concurrent wizard request).
|
||||
|
||||
**Request body**
|
||||
|
||||
```json
|
||||
{
|
||||
"fastmailEmail": "broker@fastmail.com",
|
||||
"appPassword": "xxxx-xxxx-xxxx-xxxx"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Constraints |
|
||||
| --------------- | ------ | -------- | ---------------- |
|
||||
| `fastmailEmail` | string | Yes | Valid email, max 256 characters |
|
||||
| `appPassword` | string | Yes | 1–500 characters |
|
||||
|
||||
Zod validation errors for this route never echo received values (the app password is never included in error responses).
|
||||
|
||||
**Response 200** — `{ "ok": true }`
|
||||
|
||||
**Error responses:** `400` invalid request; `409` setup already in progress; `423` setup locked; `503` service unavailable.
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/setup/complete`
|
||||
|
||||
Locks the setup wizard by writing `setup_complete=true` to `app_config`. Requires an unclaimed admin user with a Fastmail credential to exist (guards against skipping the credential step). Returns `423` if already locked.
|
||||
|
||||
**Response 200** — `{ "ok": true }`
|
||||
|
||||
**Response 422** — `{ "error": "Cannot lock setup: no credential configured" }`
|
||||
|
||||
---
|
||||
|
||||
## Auth Mode
|
||||
|
||||
### `GET /api/auth/mode`
|
||||
|
||||
Pre-auth endpoint. Returns which authentication methods are currently enabled. Used by the PWA on app load to decide which login flow to present.
|
||||
|
||||
`oidcEnabled` is true when `OIDC_ISSUER` is set in the environment **or** when `app_config` has an `oidc_issuer` row (wizard-configured OIDC before container restart). `localEnabled` is always `true`.
|
||||
|
||||
**Response 200**
|
||||
|
||||
```json
|
||||
{ "localEnabled": true, "oidcEnabled": false }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Local Auth
|
||||
|
||||
### `POST /api/auth/local/login`
|
||||
|
||||
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)
|
||||
|
||||
Timing-oracle defense: `verifyPassword` (scrypt) is always called, even for unknown usernames.
|
||||
|
||||
**Request body**
|
||||
|
||||
```json
|
||||
{ "username": "alice", "password": "hunter2" }
|
||||
```
|
||||
|
||||
| Field | Type | Required | Constraints |
|
||||
| ---------- | ------ | -------- | ---------------- |
|
||||
| `username` | string | Yes | 1–128 characters (trimmed) |
|
||||
| `password` | string | Yes | 1–1000 characters |
|
||||
|
||||
Zod validation errors never echo received values.
|
||||
|
||||
**Response 200**
|
||||
|
||||
```json
|
||||
{ "ok": true }
|
||||
```
|
||||
|
||||
Sets a `local-session` cookie (`httpOnly; Secure; SameSite`).
|
||||
|
||||
**Error responses:** `400` invalid request; `401` invalid credentials (same body for wrong password and unknown username — no field discrimination); `423` account locked; `429` too many attempts; `503` service unavailable.
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/auth/local/logout`
|
||||
|
||||
Clears the `local-session` cookie. Also available as `GET /api/auth/local/logout` for browser-redirect compatibility.
|
||||
|
||||
**Response 200** — `{ "ok": true }`
|
||||
|
||||
---
|
||||
|
||||
## Identity
|
||||
|
||||
### `GET /api/me`
|
||||
|
||||
Returns the authenticated member's identity and their assigned color.
|
||||
Returns the authenticated member's identity, admin role, and credential setup status.
|
||||
|
||||
Display name is derived from OIDC claims in priority order: `name` → `preferred_username` → `email` → `sub`. The user row is upserted on first visit (keyed on `oidc_iss` + `oidc_sub`).
|
||||
Display name is derived from OIDC claims in priority order: `name` → `preferred_username` → `email` → `sub`. The user row is upserted on first OIDC visit (keyed on `oidc_iss` + `oidc_sub`).
|
||||
|
||||
`isAdmin` is exposed for PWA navigation gating only — it is not the security boundary. The server enforces admin role via `requireAdmin` middleware on every `/api/admin/*` request.
|
||||
|
||||
**Response 200**
|
||||
|
||||
@@ -82,15 +288,96 @@ Display name is derived from OIDC claims in priority order: `name` → `preferre
|
||||
"user": {
|
||||
"id": 1,
|
||||
"displayName": "Lucas",
|
||||
"color": "#4A90D9"
|
||||
"color": "#4A90D9",
|
||||
"isAdmin": true,
|
||||
"needsProviderSetup": false,
|
||||
"hasLocalCredential": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 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.
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/me/credential`
|
||||
|
||||
Member self-service endpoint to set or rotate their own Fastmail CalDAV app password. Validates against CalDAV (PROPFIND) before storing. Always writes to the authenticated member's record — the request body cannot specify a different user ID.
|
||||
|
||||
**Request body**
|
||||
|
||||
```json
|
||||
{
|
||||
"providerType": "caldav",
|
||||
"fastmailEmail": "member@fastmail.com",
|
||||
"appPassword": "xxxx-xxxx-xxxx-xxxx"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Constraints |
|
||||
| --------------- | ------ | -------- | ---------------- |
|
||||
| `providerType` | string | Yes | Must be `"caldav"` |
|
||||
| `fastmailEmail` | string | Yes | Valid email, max 256 characters |
|
||||
| `appPassword` | string | Yes | 1–500 characters |
|
||||
|
||||
**Response 200** — `{ "ok": true }`
|
||||
|
||||
**Error responses:** `400` invalid request or CalDAV validation failed; `401` unauthorized; `503` service unavailable.
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/me/password`
|
||||
|
||||
Self-service password change for local-auth members. Requires the current password to be supplied. Returns `403` (not `401`) for a wrong current password to avoid triggering the PWA's global session-expiry handler.
|
||||
|
||||
**Request body**
|
||||
|
||||
```json
|
||||
{
|
||||
"currentPassword": "old-password",
|
||||
"newPassword": "new-password-min8"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Constraints |
|
||||
| ----------------- | ------ | -------- | ---------------- |
|
||||
| `currentPassword` | string | Yes | 1+ characters |
|
||||
| `newPassword` | string | Yes | Minimum 8 characters |
|
||||
|
||||
**Response 200** — `{ "ok": true }`
|
||||
|
||||
**Error responses:** `400` invalid request; `401` unauthorized; `403` current password incorrect; `404` no local credential found; `503` service unavailable.
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/me/link-oidc`
|
||||
|
||||
Initiates the OIDC authorization-code flow for a locally-authenticated member. Returns a signed `state` JWT and the OIDC authorization URL. The PWA redirects the user to the authorization URL; on successful OIDC login, the `/callback` handler binds the OIDC identity to the local user and removes the local credential.
|
||||
|
||||
Returns `authorizationUrl: null` when OIDC is not configured.
|
||||
|
||||
**Response 200**
|
||||
|
||||
```json
|
||||
{
|
||||
"signedState": "eyJ...",
|
||||
"authorizationUrl": "https://auth.example.com/api/oidc/authorization?..."
|
||||
}
|
||||
```
|
||||
|
||||
**Error responses:** `401` unauthorized; `503` service unavailable (missing `LOCAL_SESSION_SECRET`).
|
||||
|
||||
---
|
||||
|
||||
## Calendar Events
|
||||
|
||||
Calendar data is read from a MariaDB cache populated by the CalDAV broker poller. Write operations enqueue outbox rows; the outbox worker dispatches them to Fastmail CalDAV asynchronously. Clients receive `202 Accepted` immediately and poll `GET /api/events/sync-status` to confirm settlement.
|
||||
@@ -615,6 +902,201 @@ Removes all push subscription rows for the authenticated member. Cannot affect a
|
||||
|
||||
---
|
||||
|
||||
## Admin
|
||||
|
||||
All `/api/admin/*` routes require the authenticated member to have the admin role (`users.isAdmin = true`). The `requireAdmin` middleware is the first statement on the admin router — no sub-route is reachable without passing this guard. Non-admins receive `403`.
|
||||
|
||||
### `GET /api/admin/members`
|
||||
|
||||
Returns all household members with their credential and local-auth status.
|
||||
|
||||
**Response 200**
|
||||
|
||||
```json
|
||||
{
|
||||
"members": [
|
||||
{
|
||||
"id": 1,
|
||||
"displayName": "Lucas",
|
||||
"color": "#4A90D9",
|
||||
"hasCredential": true,
|
||||
"hasLocalCredential": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/admin/members`
|
||||
|
||||
Creates a new local-auth member: inserts a `users` row and a `local_credentials` row with a hashed initial password in a single transaction. Returns `409` if the username is already in use.
|
||||
|
||||
**Request body**
|
||||
|
||||
```json
|
||||
{
|
||||
"displayName": "Alice",
|
||||
"username": "alice",
|
||||
"initialPassword": "minimum8chars"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Constraints |
|
||||
| ----------------- | ------ | -------- | ---------------- |
|
||||
| `displayName` | string | Yes | 1–256 characters |
|
||||
| `username` | string | Yes | 1–128 characters |
|
||||
| `initialPassword` | string | Yes | Minimum 8 characters |
|
||||
|
||||
Zod validation errors never echo received values (the initial password is never included in error responses).
|
||||
|
||||
**Response 201**
|
||||
|
||||
```json
|
||||
{ "id": 2 }
|
||||
```
|
||||
|
||||
**Error responses:** `400` invalid request; `403` not admin; `409` username already in use; `503` service unavailable.
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/admin/members/:id/password`
|
||||
|
||||
Admin resets a local member's password without requiring the current password. Also clears any active rate-limit or lockout state for the member's username.
|
||||
|
||||
**Path parameter:** `id` — integer member ID.
|
||||
|
||||
**Request body**
|
||||
|
||||
```json
|
||||
{ "newPassword": "minimum8chars" }
|
||||
```
|
||||
|
||||
| Field | Type | Required | Constraints |
|
||||
| ------------- | ------ | -------- | -------------------- |
|
||||
| `newPassword` | string | Yes | Minimum 8 characters |
|
||||
|
||||
**Response 200** — `{ "ok": true }`
|
||||
|
||||
**Error responses:** `400` invalid request; `403` not admin; `404` member not found or has no local credential; `503` service unavailable.
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/admin/credentials`
|
||||
|
||||
Validates and stores a Fastmail CalDAV app password for any household member. Performs a PROPFIND against Fastmail CalDAV to verify the credential before encrypting and persisting it.
|
||||
|
||||
**Request body**
|
||||
|
||||
```json
|
||||
{
|
||||
"userId": 2,
|
||||
"providerType": "caldav",
|
||||
"fastmailEmail": "member@fastmail.com",
|
||||
"appPassword": "xxxx-xxxx-xxxx-xxxx"
|
||||
}
|
||||
```
|
||||
|
||||
| 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 | 1–500 characters |
|
||||
|
||||
Zod validation errors and CalDAV validation failures return `400` with `{ "error": "Invalid request" }` — the app password is never echoed.
|
||||
|
||||
**Response 200** — `{ "ok": true }`
|
||||
|
||||
**Error responses:** `400` invalid request or CalDAV validation failed; `403` not admin; `503` service unavailable.
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/admin/calendars`
|
||||
|
||||
Lists all synced calendars with their shared-calendar designation.
|
||||
|
||||
**Response 200**
|
||||
|
||||
```json
|
||||
{
|
||||
"calendars": [
|
||||
{ "id": 1, "displayName": "Personal", "isShared": false },
|
||||
{ "id": 2, "displayName": "Family", "isShared": true }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `PUT /api/admin/calendars/:id/shared`
|
||||
|
||||
Exclusively designates one calendar as the household shared calendar. Clears `isShared` on any previously-shared calendar in the same transaction. Returns `404` if the target calendar does not exist.
|
||||
|
||||
**Path parameter:** `id` — integer calendar ID.
|
||||
|
||||
**Response 200** — `{ "ok": true }`
|
||||
|
||||
**Error responses:** `400` invalid calendar ID; `403` not admin; `404` calendar not found.
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/admin/config/timezone`
|
||||
|
||||
Returns the household IANA timezone and whether it has been explicitly configured (vs. using the system default fallback).
|
||||
|
||||
**Response 200**
|
||||
|
||||
```json
|
||||
{ "timezone": "America/Toronto", "isExplicitlySet": true }
|
||||
```
|
||||
|
||||
`isExplicitlySet: false` when no `household_timezone` row exists in `app_config`; `timezone` still contains the resolved fallback value.
|
||||
|
||||
---
|
||||
|
||||
### `PUT /api/admin/config/timezone`
|
||||
|
||||
Validates and upserts the household IANA timezone into `app_config`.
|
||||
|
||||
**Request body**
|
||||
|
||||
```json
|
||||
{ "timezone": "America/Toronto" }
|
||||
```
|
||||
|
||||
| Field | Type | Required | Constraints |
|
||||
| ---------- | ------ | -------- | -------------------------- |
|
||||
| `timezone` | string | Yes | Valid IANA timezone, 1–64 characters |
|
||||
|
||||
**Response 200** — `{ "ok": true }`
|
||||
|
||||
**Error responses:** `400` invalid timezone; `403` not admin.
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/admin/config/timezone/seed`
|
||||
|
||||
Seeds the `household_timezone` key in `app_config` **only when it is not already set** (no-overwrite). Used by the setup wizard and browser timezone auto-detect to store the detected zone without clobbering an admin's explicit choice. Uses `INSERT IGNORE` so the operation is safe under concurrent requests.
|
||||
|
||||
**Request body**
|
||||
|
||||
```json
|
||||
{ "timezone": "America/Toronto" }
|
||||
```
|
||||
|
||||
**Response 200**
|
||||
|
||||
```json
|
||||
{ "ok": true, "seeded": true }
|
||||
```
|
||||
|
||||
`seeded: true` when the row was inserted; `seeded: false` when it already existed (no change made).
|
||||
|
||||
**Error responses:** `400` invalid timezone; `403` not admin.
|
||||
|
||||
---
|
||||
|
||||
## Error Codes
|
||||
|
||||
All error responses use a consistent JSON envelope.
|
||||
@@ -627,15 +1109,18 @@ All error responses use a consistent JSON envelope.
|
||||
| ----------- | ------------------------------------------------------------------------------ |
|
||||
| `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) |
|
||||
| `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 |
|
||||
|
||||
Zod validation failures return `400` with a structured body from `@hono/zod-validator` rather than the `{ "error": "..." }` envelope.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Rate Limits
|
||||
|
||||
No rate limiting is configured in the application layer. <!-- VERIFY: confirm whether Pangolin/Newt or Authelia enforce rate limits at the network edge -->
|
||||
No rate limiting is configured in the application layer for general API routes. Local auth login is rate-limited per-username: 5 failures within 60 seconds returns `429`; 10 cumulative failures locks the account with `423` for 15 minutes. See `POST /api/auth/local/login` for details. <!-- VERIFY: confirm whether Pangolin/Newt or Authelia enforce rate limits at the network edge -->
|
||||
|
||||
Reference in New Issue
Block a user