# Mitbringsl – REST API Specification Base URL: `/` Authentication: Bearer Token via `Authorization: Bearer ` header or session cookie. --- ## Health & Diagnostics ### `GET /healthz` Liveness probe. Returns HTTP 200 `OK`. ### `GET /readyz` Readiness probe. Checks PostgreSQL connection pool health. - **200 OK**: Database reachable. - **503 Service Unavailable**: Database error. --- ## Authentication (`/auth`) ### `POST /auth/register` Create a new email/password account. - **Request**: ```json { "email": "user@example.com", "password": "secretpassword", "display_name": "Max" } ``` - **Response (201 Created)**: ```json { "token": "", "expires_at": "2026-09-04T20:00:00Z", "user": { "id": "", "email": "user@example.com", "display_name": "Max" } } ``` - **403 Forbidden**: The server runs with `AUTH_PASSWORD_ENABLED=false` (OIDC-only); use `POST /auth/oidc` instead. Check `GET /api/config` first. ### `POST /auth/login` Authenticate with email/password. - **Request**: ```json { "email": "user@example.com", "password": "secretpassword" } ``` - **Response (200 OK)**: Same shape as register. - **403 Forbidden**: Password login disabled (see register). ### `POST /auth/oidc` Authenticate via Google or custom OpenID Connect provider. - **Request**: ```json { "provider": "google", "id_token": "" } ``` - **Response (200 OK)**: Same shape as login. ### `POST /auth/logout` Revoke active session token. Returns `204 No Content`. --- ## Server Config (`/api/config`) ### `GET /api/config` Public (no auth). Returns the auth methods this server offers so the app can render the right login form before connecting. Driven by `AUTH_PASSWORD_ENABLED` and the `OIDC_*` env vars; `display_name` for the generic provider comes from `OIDC_GENERIC_DISPLAY_NAME` (e.g. "Authentik", "Keycloak"). - **Response (200 OK)**: ```json { "auth": { "password_enabled": true, "oidc": { "google": { "enabled": false, "display_name": "Google" }, "generic": { "enabled": true, "issuer": "https://idp.example.com", "display_name": "Authentik" } } } } ``` --- ## Profile (`/api/me`) ### `GET /api/me` Returns the authenticated user's own profile. - **Response (200 OK)**: ```json { "id": "", "email": "user@example.com", "display_name": "Max" } ``` ### `PUT /api/me` Updates editable profile fields (currently `display_name`). - **Request**: `{ "display_name": "Max Mustermann" }` - Omitted field → unchanged; empty string `""` → clears the field. - **Response (200 OK)**: the updated profile (same shape as GET). --- ## Lists (`/api/lists`) All list endpoints return `404 Not Found` when the authenticated user is neither owner nor member of the list (no membership leaks), and `401` without a valid session. List responses carry an `invite_code` (8 characters) for sharing. ### `GET /api/lists` Fetch all active (non-deleted) lists the authenticated user owns **or** joined as a member. - **Response (200 OK)**: ```json { "lists": [ { "id": "", "name": "Wocheneinkauf", "invite_code": "A1B2C3D4", "updated_at": "2026-08-05T19:00:00Z", "hlc_ts": 177000000000000 } ] } ``` ### `POST /api/lists` Create a new list. The owner is automatically added to `list_members` (role `owner`) and an invite code is generated. - **Request**: `{ "name": "Supermarkt" }` - **Response (201 Created)**: `{ "id": "", "name": "Supermarkt", "invite_code": "A1B2C3D4", ... }` ### `GET /api/lists/{id}` Fetch list detail including items (owner or member only). - **Response (200 OK)**: ```json { "id": "", "name": "Wocheneinkauf", "invite_code": "A1B2C3D4", "updated_at": "...", "hlc_ts": 177000000000000, "items": [ { "id": "", "name": "Milch", "quantity": "1L", "checked": false, "hlc_ts": 177000000000001 } ] } ``` ### `POST /api/lists/{id}/invite` Return the invite code of a list (owner or member only). Generates a code on the fly for legacy lists without one. - **Request**: empty JSON object `{}` - **Response (200 OK)**: `{ "invite_code": "A1B2C3D4" }` ### `POST /api/lists/join` Join a shared list using its invite code (case-insensitive). Joining again is idempotent; the existing membership role is kept (an owner cannot be demoted). - **Request**: `{ "invite_code": "a1b2c3d4" }` - **Response (200 OK)**: the joined list (same shape as `GET /api/lists` items). - **400 Bad Request**: unknown or empty invite code. --- ## Sync & Ops (`/api/lists/{id}/ops`) Both endpoints require membership (owner or member); otherwise `404 Not Found`. ### `POST /api/lists/{id}/ops` Push a batch of client operations (max 100). - **Request**: ```json { "client_id": "", "ops": [ { "client_seq": 1, "op_type": "item_add", "target_id": "", "hlc_ts": 177000000000000, "payload": { "name": "Brot", "quantity": "1 Stück" } } ] } ``` - **Response (200 OK)**: ```json { "results": [ { "client_seq": 1, "seq": 42, "hlc_ts": 177000000000001 } ] } ``` ### `GET /api/lists/{id}/ops?since={seq}` Pull operations since server sequence `since`. - **Response (200 OK)**: ```json { "ops": [ { "seq": 42, "client_id": "", "op_type": "item_add", "target_id": "", "payload": { "name": "Brot" }, "client_seq": 1, "hlc_ts": 177000000000001, "created_at": "2026-08-05T20:00:00Z" } ], "has_more": false } ``` --- ## Suggestions (`/api/suggestions`) ### `GET /api/suggestions?q={query}` Fuzzy autocomplete search for item names (pg_trgm). - **Response (200 OK)**: ```json { "suggestions": ["milch", "mineralwasser", "müsli"] } ```