mitbringsl/docs/API.md
Tronax cdc0c785b9
Docs: catch up to auth discovery, settings screen and sync fixes
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.
2026-08-22 09:45:45 +02:00

215 lines
5.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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**:
```json
{ "email": "user@example.com", "password": "secretpassword", "display_name": "Max" }
```
- **Response (201 Created)**:
```json
{
"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); 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": "<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)**:
```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": "<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.
- **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": "<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)**:
```json
{
"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/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": "<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)**:
```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": "<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)**:
```json
{ "suggestions": ["milch", "mineralwasser", "müsli"] }
```