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

5.7 KiB
Raw Permalink Blame History

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); use POST /auth/oidc instead. Check GET /api/config first.

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.
  • 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/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:
    {
      "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"] }