Document the four features that landed on main in b44bc8c..85c790e: - AGENTS.md: post-MVP section now covers server-driven auth discovery (GET /api/config, AUTH_PASSWORD_ENABLED 403 enforcement, OIDC_GENERIC_DISPLAY_NAME), the settings screen with GET/PUT /api/me, and the critical Android sync/data-safety fixes (pull-all-lists, client HLC tick per server op, no destructive migration, ProGuard rules). Repo structure updated (httpapi config.go/me.go, ui/settings), roadmap entries added, open-points list extended with test backlog for the new endpoints. - API.md: new sections for GET /api/config (public) and GET/PUT /api/me; 403 responses documented for register/login when password auth is off. - SYNC.md: client pull loop (every tracked list, op_log pruning) and the client HLC discipline (tick per incoming server op) that keeps LWW correct across devices with skewed clocks. - README: highlights for auth discovery/OIDC-only mode and the settings screen.
5.7 KiB
Mitbringsl – REST API Specification
Base URL: /
Authentication: Bearer Token via Authorization: Bearer <session_token> 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:
{ "email": "user@example.com", "password": "secretpassword", "display_name": "Max" } - Response (201 Created):
{ "token": "<opaque_session_token>", "expires_at": "2026-09-04T20:00:00Z", "user": { "id": "<uuid>", "email": "user@example.com", "display_name": "Max" } } - 403 Forbidden: The server runs with
AUTH_PASSWORD_ENABLED=false(OIDC-only); usePOST /auth/oidcinstead. CheckGET /api/configfirst.
POST /auth/login
Authenticate with email/password.
- Request:
{ "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:
{ "provider": "google", "id_token": "<jwt>" } - 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):
{ "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):
{ "id": "<uuid>", "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.
- Omitted field → unchanged; empty string
- 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):
{ "lists": [ { "id": "<uuid>", "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": "<uuid>", "name": "Supermarkt", "invite_code": "A1B2C3D4", ... }
GET /api/lists/{id}
Fetch list detail including items (owner or member only).
- Response (200 OK):
{ "id": "<uuid>", "name": "Wocheneinkauf", "invite_code": "A1B2C3D4", "updated_at": "...", "hlc_ts": 177000000000000, "items": [ { "id": "<uuid>", "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/listsitems). - 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:
{ "client_id": "<client_uuid>", "ops": [ { "client_seq": 1, "op_type": "item_add", "target_id": "<item_uuid>", "hlc_ts": 177000000000000, "payload": { "name": "Brot", "quantity": "1 Stück" } } ] } - Response (200 OK):
{ "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):
{ "ops": [ { "seq": 42, "client_id": "<client_uuid>", "op_type": "item_add", "target_id": "<item_uuid>", "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):
{ "suggestions": ["milch", "mineralwasser", "müsli"] }