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.
215 lines
5.7 KiB
Markdown
215 lines
5.7 KiB
Markdown
# 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"] }
|
||
```
|