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.
This commit is contained in:
Tronax 2026-08-22 09:45:45 +02:00
parent 67033e561c
commit cdc0c785b9
Signed by: Tronax
SSH key fingerprint: SHA256:2pKKXDZucWvaF/GzXNz0FY53EAO1YDLN80bqS+TTz/o
4 changed files with 100 additions and 9 deletions

View file

@ -46,6 +46,25 @@ Vollständiger Plan liegt als genehmigtem Plan zugrunde (siehe Abschnitt "Roadma
Einstieg. Teilen/Beitreten erfordert aktive Server-Verbindung (App-gated). Einstieg. Teilen/Beitreten erfordert aktive Server-Verbindung (App-gated).
- **Self-Hosted Server-URL:** konfigurierbar in der App (Auth-Screen), umgesetzt - **Self-Hosted Server-URL:** konfigurierbar in der App (Auth-Screen), umgesetzt
via `DynamicBaseUrlInterceptor` (schreibt scheme/host/port pro Request um). via `DynamicBaseUrlInterceptor` (schreibt scheme/host/port pro Request um).
- **Server-getriebene Auth-Discovery:** Öffentlicher Endpoint `GET /api/config`
verrät der App vor dem Login die verfügbaren Methoden (`password_enabled`,
OIDC-Provider mit `display_name`, z.B. „Authentik"). `AUTH_PASSWORD_ENABLED=false`
→ Register/Login per Passwort → 403 (OIDC-only-Enforcement). App-Login zeigt
nur noch die angebotenen Methoden (ConnectView → LoginView).
- **Profil + Settings-Screen:** `GET/PUT /api/me` (aktuell `display_name`),
`SettingsScreen` mit Account & Sync, Profil, Theme (System/Hell/Dunkel,
persistiert in SessionManager), „Lokale Daten zurücksetzen" (nur Room-Tabellen,
Serverdaten bleiben) und About.
- **Kritische Sync-/Datensicherheits-Fixes (Android):** SyncWorker pullt jetzt
**alle** getrackten Listen (nicht nur welche mit pending Outbox vorher kamen
Remote-Änderungen stiller Listen nie an); lokale HLC tickt mit jedem eingehenden
Server-Op (`tick(op.hlcTs)` → LWW korrekt bei Uhr-Drift); `list_create` in
`applyServerOpProjection` ergänzt; Projektions-Write + op_log-Insert in einer
Room-Transaktion; `clientId`/`clientSeq` persistent in SessionManager;
**kein** `fallbackToDestructiveMigration` mehr (Source of Truth darf nie
gewispt werden); ProGuard-Keep-Rules für kotlinx.serialization/Retrofit
(R8-Releases crashten sonst); `network_security_config` (Cleartext nur für
localhost/10.0.2.2); WorkManager-Default-Initializer deaktiviert (Hilt).
- **Authentik-OIDC:** läuft über den bestehenden Generic-OIDC-Provider - **Authentik-OIDC:** läuft über den bestehenden Generic-OIDC-Provider
(`provider: "generic"`); die App erlaubt manuelle id_token-Eingabe. (`provider: "generic"`); die App erlaubt manuelle id_token-Eingabe.
- **MVP-Scope:** keine Echtzeit-Push (nur Periodic-Pull 15 min + Pull-on-Online); - **MVP-Scope:** keine Echtzeit-Push (nur Periodic-Pull 15 min + Pull-on-Online);
@ -106,7 +125,7 @@ mitbringsl/
│ │ ├── auth/ # PHASE B Password + Session + OIDC │ │ ├── auth/ # PHASE B Password + Session + OIDC
│ │ │ ├── password.go # Argon2id im PHC-Format (HashPassword/VerifyPassword) │ │ │ ├── password.go # Argon2id im PHC-Format (HashPassword/VerifyPassword)
│ │ │ ├── password_test.go # PHC-Roundtrip-Tests │ │ │ ├── password_test.go # PHC-Roundtrip-Tests
│ │ │ ├── user.go # UserStore: CreateUser/GetByEmail/GetByID/GetByOIDCSubject/CreateOIDCUser │ │ │ ├── user.go # UserStore: CreateUser/GetByEmail/GetByID/GetByOIDCSubject/CreateOIDCUser/UpdateDisplayName
│ │ │ ├── session.go # SessionStore: opaque Tokens, SHA-256-Hash, Create/Lookup/Revoke │ │ │ ├── session.go # SessionStore: opaque Tokens, SHA-256-Hash, Create/Lookup/Revoke
│ │ │ ├── oidc.go # OIDCService: id_token-Verifikation (JWKS/iss/aud/exp), Provider-Caching │ │ │ ├── oidc.go # OIDCService: id_token-Verifikation (JWKS/iss/aud/exp), Provider-Caching
│ │ │ └── pgcode.go # isUniqueViolation (SQLSTATE 23505) │ │ │ └── pgcode.go # isUniqueViolation (SQLSTATE 23505)
@ -126,7 +145,9 @@ mitbringsl/
│ │ ├── render.go # JSON-Render + Problem + Fehler-Sentinale + decodeJSON │ │ ├── render.go # JSON-Render + Problem + Fehler-Sentinale + decodeJSON
│ │ ├── middleware.go # requestID/logging/recover/cors + Chain │ │ ├── middleware.go # requestID/logging/recover/cors + Chain
│ │ ├── health.go # /healthz + /readyz │ │ ├── health.go # /healthz + /readyz
│ │ ├── auth.go # Register/Login/Logout/OIDC-Handler + RequireAuth-Middleware │ │ ├── auth.go # Register/Login/Logout/OIDC-Handler + RequireAuth-Middleware (403 wenn Passwort-Login deaktiviert)
│ │ ├── config.go # GET /api/config (öffentlich): verfügbare Auth-Methoden für die App
│ │ ├── me.go # GET/PUT /api/me (eigenes Profil, display_name)
│ │ ├── lists.go # GET/POST /api/lists, GET /api/lists/{id}, POST /invite, POST /join │ │ ├── lists.go # GET/POST /api/lists, GET /api/lists/{id}, POST /invite, POST /join
│ │ ├── ops.go # POST/GET /api/lists/{id}/ops (Push/Pull, Membership-Check) │ │ ├── ops.go # POST/GET /api/lists/{id}/ops (Push/Pull, Membership-Check)
│ │ └── suggest.go # GET /api/suggestions?q= │ │ └── suggest.go # GET /api/suggestions?q=
@ -151,15 +172,16 @@ mitbringsl/
│ └── API.md # REST Specs (inkl. join/invite) │ └── API.md # REST Specs (inkl. join/invite)
└── android/ # Single-Module :app (Compose M3, Room, Hilt) └── android/ # Single-Module :app (Compose M3, Room, Hilt)
└── app/src/main/java/com/example/mitbringsl/ └── app/src/main/java/com/example/mitbringsl/
├── data/auth/ # SessionManager (Local-User-Fallback, Server-URL) ├── data/auth/ # SessionManager (Local-User-Fallback, Server-URL, clientId/clientSeq, Theme-Pref)
├── data/local/ # Room DB (lists, items, op_log), DAOs (LWW upsert) ├── data/local/ # Room DB (lists, items, op_log), DAOs (LWW upsert)
├── data/remote/ # Retrofit-API, DTOs, DynamicBaseUrlInterceptor, AuthInterceptor ├── data/remote/ # Retrofit-API, DTOs, DynamicBaseUrlInterceptor, AuthInterceptor
├── data/repository/ # ShoppingRepository, AuthRepository ├── data/repository/ # ShoppingRepository, AuthRepository
├── data/sync/ # HybridLogicalClock, SyncManager, SyncWorker ├── data/sync/ # HybridLogicalClock, SyncManager, SyncWorker (pullt ALLE Listen, HLC-Tick pro Server-Op)
├── di/ # Hilt Modules ├── di/ # Hilt Modules
├── ui/auth/ # AuthScreen (Login/Register/OIDC/Server-URL) ├── ui/auth/ # AuthScreen (ConnectView: Server-URL + /api/config-Discovery, dann LoginView)
├── ui/lists/ # ListsScreen/ViewModel (Join-Dialog, Offline-Banner) ├── ui/lists/ # ListsScreen/ViewModel (Join-Dialog, Offline-Banner, Settings-Zahnrad)
├── ui/detail/ # ListDetailScreen/ViewModel (Share-Button, Autocomplete) ├── ui/detail/ # ListDetailScreen/ViewModel (Share-Button, Autocomplete)
├── ui/settings/ # SettingsScreen/ViewModel (Account, Profil, Theme, Reset, About)
└── util/ # NetworkMonitor └── util/ # NetworkMonitor
``` ```
@ -215,6 +237,18 @@ Legende: ✅ erledigt · 🚧 in Arbeit · ⬜ offen
(Fremde → 404 auf allen Listen-Endpoints), Cross-Member-Sync via op_log, (Fremde → 404 auf allen Listen-Endpoints), Cross-Member-Sync via op_log,
401-Gating aller geschützten Routen. Beide Dateien skippen ohne 401-Gating aller geschützten Routen. Beide Dateien skippen ohne
`TEST_DATABASE_URL` (Docker-Rezept im Dateikopf). `TEST_DATABASE_URL` (Docker-Rezept im Dateikopf).
- ✅ **Post-MVP Kritische Android-Fixes (Sync/Build/Data-Safety):** SyncWorker
pullt alle getrackten Listen, HLC-Tick pro Server-Op, Room-Transaktionen um
Projektion+Outbox, persistente clientId/clientSeq, kein destructive Migration,
ProGuard-Rules, Network-Security-Config, Hilt-WorkManager-Init.
- ✅ **Post-MVP Auth-Discovery + OIDC-only:** `GET /api/config` (öffentlich),
`AUTH_PASSWORD_ENABLED` (403-Enforcement), `OIDC_GENERIC_DISPLAY_NAME`,
App-Connect-Flow (Server-URL → Methoden-Discovery → LoginView).
- ✅ **Post-MVP Settings-Screen + Profil:** `GET/PUT /api/me`
(`UserStore.UpdateDisplayName`), `SettingsScreen`/`SettingsViewModel`
(Account&Sync, Profil, Theme, Reset, About), Theme-Pref in SessionManager.
- ✅ **Post-MVP Build hinter restriktiven Proxies:** foojay-Resolver &
jvmToolchain auskommentiert, Gradle-Wrapper 9.6.1, `.ssl-work/` ignoriert.
### Wo genau weitermachen? ### Wo genau weitermachen?
**MVP + Post-MVP-Features sind abgeschlossen.** Das Backend ist feature-complete **MVP + Post-MVP-Features sind abgeschlossen.** Das Backend ist feature-complete
@ -229,9 +263,12 @@ für den aktuellen Scope. Offene Punkte, geordnet nach Nutzen:
3. **Invite-Code-Optimierung:** Codes sind UUID-Präfixe (`uuid[:8]`), nicht 3. **Invite-Code-Optimierung:** Codes sind UUID-Präfixe (`uuid[:8]`), nicht
kollisionsresistent geprüft (UNIQUE-Constraint fängt es, aber CreateList kann kollisionsresistent geprüft (UNIQUE-Constraint fängt es, aber CreateList kann
dann fehlschlagen). Besser: kryptografisches Alphabet ohne Verwechslungsbuchstaben. dann fehlschlagen). Besser: kryptografisches Alphabet ohne Verwechslungsbuchstaben.
4. **Echtzeit-Push (langfristig):** Periodic-Pull 15 min ist MVP; SSE/WebSocket 4. **Tests für neuere Endpoints:** `GET /api/config`, `GET/PUT /api/me` und das
`AUTH_PASSWORD_ENABLED=false`-Verhalten (403) haben noch keine Testabdeckung
das Test-Setup dafür steht in `api_test.go` bereit.
5. **Echtzeit-Push (langfristig):** Periodic-Pull 15 min ist MVP; SSE/WebSocket
für sofortige Updates wäre der nächste Schritt. für sofortige Updates wäre der nächste Schritt.
5. **Android-Tests:** UI/ViewModel-Tests fehlen fast komplett (nur 2 Stock-Tests). 6. **Android-Tests:** UI/ViewModel-Tests fehlen fast komplett (nur 2 Stock-Tests).
--- ---

View file

@ -12,7 +12,8 @@ Mitbringsl ist eine moderne, schnelle und werbefreie Einkaufslisten-App mit **Lo
- 🔌 **Local-Only by Default**: Kein Account nötig — die App startet direkt ohne Login; Sync & Account sind optional (auch Self-Hosted, Server-URL in der App konfigurierbar). - 🔌 **Local-Only by Default**: Kein Account nötig — die App startet direkt ohne Login; Sync & Account sind optional (auch Self-Hosted, Server-URL in der App konfigurierbar).
- 👥 **Geteilte Listen**: Listen per 8-Zeichen-Invite-Code teilen und gemeinsam bearbeiten; Membership wird serverseitig bei jedem Sync geprüft. - 👥 **Geteilte Listen**: Listen per 8-Zeichen-Invite-Code teilen und gemeinsam bearbeiten; Membership wird serverseitig bei jedem Sync geprüft.
- ⚡ **Local-First Sync Engine**: Hybrid Logical Clock (HLC), append-only `op_log`, Last-Write-Wins (LWW) Projektionen, Idempotente Push/Pull-Algorithmen. - ⚡ **Local-First Sync Engine**: Hybrid Logical Clock (HLC), append-only `op_log`, Last-Write-Wins (LWW) Projektionen, Idempotente Push/Pull-Algorithmen.
- 🔐 **Datenschutz & Auth**: Argon2id Passwort-Hashing, opaque Session-Tokens, OIDC-Unterstützung (Google & eigene IdPs wie Authentik/Keycloak). - 🔐 **Datenschutz & Auth**: Argon2id Passwort-Hashing, opaque Session-Tokens, OIDC-Unterstützung (Google & eigene IdPs wie Authentik/Keycloak) — Login-Methoden werden serverseitig per `/api/config` entdeckt, Passwort-Login lässt sich per `AUTH_PASSWORD_ENABLED=false` komplett abschalten.
- ⚙️ **Settings & Themes**: Account-/Profil-Verwaltung (`/api/me`), Theme-Umschalter (System/Hell/Dunkel), lokaler Daten-Reset — alles direkt in der App.
- 🚀 **Go Backend**: Stdlib `net/http` Routing (Go 1.26), PostgreSQL 16 (`pgxpool`), `pg_trgm` Fuzzy Autocomplete. - 🚀 **Go Backend**: Stdlib `net/http` Routing (Go 1.26), PostgreSQL 16 (`pgxpool`), `pg_trgm` Fuzzy Autocomplete.
- 🛡️ **Deployment**: Multi-Stage Docker Container, Caddy Reverse Proxy mit automatischem HTTPS / Standalone & Behind-Proxy Modi. - 🛡️ **Deployment**: Multi-Stage Docker Container, Caddy Reverse Proxy mit automatischem HTTPS / Standalone & Behind-Proxy Modi.

View file

@ -33,6 +33,8 @@ Create a new email/password account.
"user": { "id": "<uuid>", "email": "user@example.com", "display_name": "Max" } "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` ### `POST /auth/login`
Authenticate with email/password. Authenticate with email/password.
@ -41,6 +43,7 @@ Authenticate with email/password.
{ "email": "user@example.com", "password": "secretpassword" } { "email": "user@example.com", "password": "secretpassword" }
``` ```
- **Response (200 OK)**: Same shape as register. - **Response (200 OK)**: Same shape as register.
- **403 Forbidden**: Password login disabled (see register).
### `POST /auth/oidc` ### `POST /auth/oidc`
Authenticate via Google or custom OpenID Connect provider. Authenticate via Google or custom OpenID Connect provider.
@ -55,6 +58,45 @@ 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`) ## Lists (`/api/lists`)
All list endpoints return `404 Not Found` when the authenticated user is neither All list endpoints return `404 Not Found` when the authenticated user is neither

View file

@ -56,6 +56,17 @@ All state modifications are represented as structured operations:
Each operation carries a `(client_id, client_seq)` tuple enforced by a `UNIQUE` constraint in Postgres (`op_log`). Re-sent requests return previous `(seq, hlc_ts)` assignments without duplicating side effects. Each operation carries a `(client_id, client_seq)` tuple enforced by a `UNIQUE` constraint in Postgres (`op_log`). Re-sent requests return previous `(seq, hlc_ts)` assignments without duplicating side effects.
- **Cursor Pull (`GET /api/lists/{id}/ops?since={seq}`)**: - **Cursor Pull (`GET /api/lists/{id}/ops?since={seq}`)**:
Clients track `max(server_seq)` locally. Incremental sync fetches ops where `seq > cursor`, sorted by monotonic server sequence `seq ASC`. Clients track `max(server_seq)` locally. Incremental sync fetches ops where `seq > cursor`, sorted by monotonic server sequence `seq ASC`.
- **Client pull loop**:
The `SyncWorker` pulls for **every** tracked list on each run (not only lists with pending outbox ops), so remote edits on "quiet" (e.g. shared/joined) lists arrive reliably. Old synced `op_log` rows are pruned locally to bound growth.
### Client HLC discipline
On every incoming server op the client advances its local HLC with
`tick(op.hlc_ts)`. This is essential for LWW correctness across devices with
skewed clocks: without it, a fast-clock device would permanently win conflicts
while a slow-clock device's own edits would be silently rejected by the
`hlc_ts <` projection guard. The same rule applies on the server (`opstore`
ticks the server HLC per incoming op).
--- ---