mitbringsl/docs/API.md
Tronax 67033e561c
Tests for shared lists & docs catch-up to post-MVP state
Integration tests for the invite/join/membership feature that shipped
without any coverage:

- internal/store/liststore_test.go: CreateList adds owner as member with
  invite code, GetLists returns owned+joined but not foreign lists,
  GetList access control (owner/member yes, stranger and soft-deleted no),
  JoinByInviteCode normalization/idempotency/role-keeping, lazy invite
  code generation. Runs against TEST_DATABASE_URL, skips otherwise.
- internal/httpapi/api_test.go: full E2E over the real router — register,
  create list (code in response), invite endpoint, join (lowercase),
  cross-member op push/pull sync, stranger gets 404 on every list
  endpoint, invalid code 400, idempotent re-join, and 401 gating of all
  protected routes.
- lists.go Invite handler: store errors now map through apiError, so
  non-members get 404 instead of 400 (consistent with Get/Push/Pull).

Docs updated to the actual post-MVP state: AGENTS.md (post-MVP features,
repo structure, roadmap with open points like join rate limiting),
API.md (join/invite endpoints, invite_code fields, membership rules),
SYNC.md (shared lists section), README (local-only default, sharing,
integration test recipe).
2026-08-22 09:40:57 +02:00

173 lines
4.4 KiB
Markdown
Raw 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" }
}
```
### `POST /auth/login`
Authenticate with email/password.
- **Request**:
```json
{ "email": "user@example.com", "password": "secretpassword" }
```
- **Response (200 OK)**: Same shape as 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`.
---
## 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"] }
```