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.
21 KiB
21 KiB
AGENTS.md – Kontext & Fortschritt für KI-Agenten
Diese Datei hält den Projekt-Stand und die Architekturentscheidungen fest, damit ein Agent (z.B. zuhause) sofort weiterarbeiten kann. Sie ist Teil des Repos.
Projekt: Mitbringsl
Local-First Einkaufslisten-App (Bring-Alternative, werbefrei). Backend: Go (net/http, pgx, golang-migrate, go-oidc) als Docker-Container. App: Kotlin + Jetpack Compose, Room, Hilt, WorkManager, offline-first. Deployment: docker-compose (Caddy + backend + migrate + postgres:16), auto-HTTPS.
Vollständiger Plan liegt als genehmigtem Plan zugrunde (siehe Abschnitt "Roadmap").
Getroffene Architekturentscheidungen (verbindlich)
- Backend-Sprache: Go (nicht Rust).
- DB: PostgreSQL (
postgres:16-alpine), Extensionspgcrypto+pg_trgm. - Sync: Local-First, keine Datenverluste – Append-only
op_log(SOURCE OF TRUTH),items/listssind Projektionen; Konfliktlösung per LWW-Register(hlc_ts, client_id)- Tombstones (
deleted_at). HLC = Hybrid Logical Clock.
- Push:
POST /api/lists/{id}/ops(idempotent viaUNIQUE(client_id, client_seq)). - Pull:
GET /api/lists/{id}/ops?since={seq}.
- Tombstones (
- Auth:
- Eigene User: E-Mail/Passwort, Argon2id (PHC-Format).
- OIDC: Google + Generic OIDC. Die App macht den Code+PKCE-Flow selbst
und schickt nur das
id_tokenans Backend (POST /auth/oidc). Backend verifiziert viagithub.com/coreos/go-oidc/v3(Signatur gegen JWKS, iss/aud/exp) und stellt eigene opaque Session-Tokens aus (sessions-Tabelle, SHA-256-Hash gespeichert). - AppAuth-Android bewusst NICHT verwendet (seit 2021 verwaist). Stattdessen: Google via Credential Manager, Generic OIDC via Custom Tabs + eigenes PKCE.
- Vorschläge: aggregiert aus Item-Namen aller User (
item_names-Tabelle, pg_trgm fuzzy search). EndpointGET /api/suggestions?q=. - Post-MVP (umgesetzt):
- Geteilte Listen per Invite-Code:
lists.invite_code(8 Zeichen, UNIQUE, Migration 000002).POST /api/lists/{id}/invite(Owner/Member lesen Code, wird lazy generiert),POST /api/lists/join(beitreten alsmember).GET /api/lists+GET /api/lists/{id}+ Push/Pull-Ops prüfen Membership (owner ODERlist_members-Eintrag). Kein Leave/Revoke-Endpoint, kein Rate-Limiting auf join (bekannt, bewusst offengehalten). - Account optional / Local-Only-Mode: App startet ohne Login direkt in die
Listen (
local_user-Fallback imSessionManager); Sync/Account ist optionaler Einstieg. Teilen/Beitreten erfordert aktive Server-Verbindung (App-gated). - Self-Hosted Server-URL: konfigurierbar in der App (Auth-Screen), umgesetzt
via
DynamicBaseUrlInterceptor(schreibt scheme/host/port pro Request um). - Server-getriebene Auth-Discovery: Öffentlicher Endpoint
GET /api/configverrät der App vor dem Login die verfügbaren Methoden (password_enabled, OIDC-Provider mitdisplay_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(aktuelldisplay_name),SettingsScreenmit 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_createinapplyServerOpProjectionergänzt; Projektions-Write + op_log-Insert in einer Room-Transaktion;clientId/clientSeqpersistent in SessionManager; keinfallbackToDestructiveMigrationmehr (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
(
provider: "generic"); die App erlaubt manuelle id_token-Eingabe.
- Geteilte Listen per Invite-Code:
- MVP-Scope: keine Echtzeit-Push (nur Periodic-Pull 15 min + Pull-on-Online); keine Web-UI.
Technologie-Stacks (final)
Backend
| Bereich | Wahl |
|---|---|
| Go-Version | 1.26 (go.mod hat go 1.26; Docker-Image golang:1.26-alpine) |
| HTTP | stdlib net/http (Go 1.22+ Routing mit Methoden + Path-Vars) |
| DB-Driver | github.com/jackc/pgx/v5 (pgxpool) |
| Migrationen | github.com/golang-migrate/migrate/v4 + source/iofs (eingebettet via embed.FS) |
| OIDC | github.com/coreos/go-oidc/v3 + golang.org/x/oauth2 |
| Passwörter | Argon2id (golang.org/x/crypto/argon2) |
| Sessions | opaque Tokens (32 B base64url), sessions-Tabelle, SHA-256-Hash |
| Config | github.com/caarlos0/env/v11 (struct-tag env) |
| Logging | log/slog mit NewJSONHandler → stdout |
| Docker | Multi-Stage, CGO_ENABLED=0, gcr.io/distroless/static-debian12:nonroot |
| Queries | direkt mit pgx (sqlc wurde aus Skalierbarkeit bewusst auf später verschoben) |
Android (noch nicht begonnen)
| Bereich | Wahl |
|---|---|
| Build | Kotlin DSL + libs.versions.toml, Single-Module :app |
| Kotlin/AGP/Gradle | Kotlin 2.x (K2), AGP 8.9+, Gradle 8.11+, JDK 17 |
| UI | Compose BOM (2025.x) + Material 3 |
| Arch | MVVM + ViewModel + StateFlow, UDF |
| DB | Room (KSP), Source of Truth via Flow |
| Netzwerk | Retrofit + OkHttp + kotlinx.serialization |
| Sync | WorkManager CoroutineWorker (Outbox-Drain + Cursor-Pull) |
| DI | Hilt (KSP) + hilt-navigation-compose + hilt-work |
| IDs | Client-seitige UUIDs |
| Auth | Google: Credential Manager; Generic OIDC: Custom Tabs + PKCE selbst |
| Suche | OutlinedTextField + DropdownMenu, Room-FTS, Server-Fallback |
| SDK | minSdk 26, compile/target 36 |
Repository-Struktur
mitbringsl/
├── AGENTS.md ← DIES DATEI
├── README.md
├── .gitignore
├── backend/
│ ├── cmd/
│ │ ├── server/main.go # HTTP-Server-Einstieg (verdrahtet cfg/logger/pool/api)
│ │ └── migrate/main.go # Migrations-Runner (iofs-embedded)
│ ├── internal/
│ │ ├── config/config.go # Config (caarlos0/env)
│ │ ├── logging/logging.go # slog JSON-Setup
│ │ ├── store/db.go # pgxpool-Setup
│ │ ├── auth/ # PHASE B – Password + Session + OIDC
│ │ │ ├── password.go # Argon2id im PHC-Format (HashPassword/VerifyPassword)
│ │ │ ├── password_test.go # PHC-Roundtrip-Tests
│ │ │ ├── user.go # UserStore: CreateUser/GetByEmail/GetByID/GetByOIDCSubject/CreateOIDCUser/UpdateDisplayName
│ │ │ ├── session.go # SessionStore: opaque Tokens, SHA-256-Hash, Create/Lookup/Revoke
│ │ │ ├── oidc.go # OIDCService: id_token-Verifikation (JWKS/iss/aud/exp), Provider-Caching
│ │ │ └── pgcode.go # isUniqueViolation (SQLSTATE 23505)
│ │ ├── sync/ # PHASE C – Hybrid Logical Clock
│ │ │ ├── hlc.go # HLC: Tick/Now/After, wall_ms<<16|counter, global mutex
│ │ │ └── hlc_test.go # HLC Unit-Tests (monoton, kausal, keine Duplikate)
│ │ ├── store/ # PHASE C – Store-Schicht
│ │ │ ├── db.go # pgxpool-Setup
│ │ │ ├── opstore.go # AppendOps (idempotent, LWW-Projektion), PullOps (cursor)
│ │ │ ├── liststore.go # CreateList/GetLists/GetList/GetInviteCode/JoinByInviteCode
│ │ │ ├── liststore_test.go # Invite/Join/Membership-Integrationstests (brauchen TEST_DATABASE_URL)
│ │ │ ├── itemstore.go # GetItems
│ │ │ └── suggeststore.go # Search (pg_trgm fuzzy)
│ │ └── httpapi/
│ │ ├── api.go # API-Objekt + Router (alle Routen aktiv)
│ │ ├── api_test.go # E2E-HTTP-Tests: Share/Join/Sync + 401-Gating (TEST_DATABASE_URL)
│ │ ├── render.go # JSON-Render + Problem + Fehler-Sentinale + decodeJSON
│ │ ├── middleware.go # requestID/logging/recover/cors + Chain
│ │ ├── health.go # /healthz + /readyz
│ │ ├── 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
│ │ ├── ops.go # POST/GET /api/lists/{id}/ops (Push/Pull, Membership-Check)
│ │ └── suggest.go # GET /api/suggestions?q=
│ ├── migrations/
│ │ ├── embed.go # //go:embed *.sql
│ │ ├── 000001_init_schema.up.sql # users/sessions/lists/list_members/items/op_log/item_names
│ │ ├── 000001_init_schema.down.sql
│ │ ├── 000002_add_invite_code.up.sql # lists.invite_code UNIQUE + Backfill owner→list_members
│ │ └── 000002_add_invite_code.down.sql
│ ├── Dockerfile # Multi-Stage, baut server + migrate
│ ├── .dockerignore
│ ├── go.mod / go.sum
├── deploy/
│ ├── docker-compose.yml # caddy + backend + migrate + db (CADDY_HTTP/HTTPS_PORT)
│ ├── Caddyfile # standalone: auto-HTTPS (Let's Encrypt)
│ ├── Caddyfile.behind-proxy # hinter externem Reverse Proxy: auto_https off, trusted_proxies
│ ├── .env.example # alle env-Vars dokumentiert inkl. Caddy-Ports
│ └── db/init/001_extensions.sql # CREATE EXTENSION pgcrypto, pg_trgm
├── docs/
│ ├── ARCHITECTURE.md
│ ├── SYNC.md # HLC/LWW/op_log
│ └── API.md # REST Specs (inkl. join/invite)
└── android/ # Single-Module :app (Compose M3, Room, Hilt)
└── app/src/main/java/com/example/mitbringsl/
├── data/auth/ # SessionManager (Local-User-Fallback, Server-URL, clientId/clientSeq, Theme-Pref)
├── data/local/ # Room DB (lists, items, op_log), DAOs (LWW upsert)
├── data/remote/ # Retrofit-API, DTOs, DynamicBaseUrlInterceptor, AuthInterceptor
├── data/repository/ # ShoppingRepository, AuthRepository
├── data/sync/ # HybridLogicalClock, SyncManager, SyncWorker (pullt ALLE Listen, HLC-Tick pro Server-Op)
├── di/ # Hilt Modules
├── ui/auth/ # AuthScreen (ConnectView: Server-URL + /api/config-Discovery, dann LoginView)
├── ui/lists/ # ListsScreen/ViewModel (Join-Dialog, Offline-Banner, Settings-Zahnrad)
├── ui/detail/ # ListDetailScreen/ViewModel (Share-Button, Autocomplete)
├── ui/settings/ # SettingsScreen/ViewModel (Account, Profil, Theme, Reset, About)
└── util/ # NetworkMonitor
Roadmap / Fortschritt
Legende: ✅ erledigt · 🚧 in Arbeit · ⬜ offen
- ✅ Repo-Struktur +
.gitignore+README.md+git init(Branchmain) - ✅ Phase A – Backend-Fundament: Config, slog, pgxpool,
/healthz+/readyz, Server-Main. - ✅ Phase A – Migrationen: vollständiges Init-Schema (up+down) +
migrate-Binary (iofs). - ✅ Phase A – Docker: Dockerfile (Go 1.26 → distroless nonroot), docker-compose
(caddy/backend/migrate/db), Caddyfile,
.env.example. Verifiziert: Image baut, beide Binaries laufen im Container (Smoke-Test OK). - ✅ Phase B – Auth (Password): Argon2id im PHC-Format +
UserStore(Create/GetByEmail/GetByID) +SessionStore(opaque Tokens, SHA-256-Hash, Create/Lookup/Revoke) + HandlerRegister/Login/Logout+RequireAuth-Middleware. Verifiziert:go test ./internal/auth/...grün, E2E-Smoke-Test gegen echtes PostgreSQL via Docker (Register/Login/Logout/Duplicate/Short-PW/Wrong-PW alle korrekt). - ✅ Phase B – OIDC:
OIDCService(go-oidc v3, JWKS-Signatur, iss/aud/exp, Provider-Caching) +POST /auth/oidc(find-or-create User via(oidc_issuer, oidc_subject), reusesissueSession) + Config-Validierung (enabled → client_id/issuer Pflicht). Verifiziert: E2E-Flow gegen lokalen Mock-IdP (Discovery → JWKS → signiertes id_token → User angelegt → 2. Login findet gleichen User → tampered Token → 401). Alle Fehlerpfade geprüft (disabled→400, unknown provider→400, missing fields→400, invalid→401). - ✅ Phase C – Sync-Kern: HLC (
internal/sync/hlc.go),op_log-Append idempotent mit LWW-Projektion (items/lists) + Tombstones.internal/store/opstore.go. Verifiziert:go build/vet/test ./...grün. HLC-Tests (monoton, kausal, keine Duplikate). - ✅ Phase C – Endpoints:
GET/POST /api/lists,GET /api/lists/{id},POST/GET /api/lists/{id}/ops(Push idempotent + Pull mit Cursor?since=). - ✅ Phase C – Suggestions:
suggeststore.go(pg_trgm fuzzy),GET /api/suggestions?q=,upsertItemNamein Projektion (innerhalb Push-Transaktion). - ✅ Phase C – Caddy:
Caddyfile.behind-proxy(auto_https off, trusted_proxies),CADDY_HTTP_PORT/CADDY_HTTPS_PORTin docker-compose. - ✅ Phase D – Android-Fundament: Gradle (Kotlin DSL, Version Catalog, Hilt/KSP, Compose BOM, Room, Retrofit, WorkManager), Theme, Nav.
Verifiziert:
./gradlew assembleDebugund./gradlew testerfolgreich. - ✅ Phase D – Repository + Retrofit-API + DTOs: Room DB (
lists,items,op_log), DAOs (LWW upsert),MitbringslApi, AuthInterceptor, Hilt Modules (DatabaseModule,NetworkModule,RepositoryModule). - ✅ Phase D – Login-Screen:
AuthScreen(E-Mail/Passwort Login + Registrierung),AuthViewModel,AuthRepository,SessionManager. - ✅ Phase E – SyncEngine:
HybridLogicalClock(client-seitig),SyncWorker(CoroutineWorkerOutbox Drain + Server Cursor Pull),SyncManager(15 min periodisch + Sofort-Sync),ShoppingRepository(local-first mutations via Room +op_log). - ✅ Phase E – Listen-Übersicht + Detail + AddItemBar:
ListsScreen&ListsViewModel,ListDetailScreen&ListDetailViewModel(Sectioning erledigt/offen, Autocomplete Suggestions dropdown), Compose Navigation 3. - ✅ Phase F – Polish:
NetworkMonitor(ConnectivityManager StateFlow), Offline-Banner inListsScreen, Material 3 Empty States. - ✅ Phase F – README + docs:
README.mdQuickstart,docs/ARCHITECTURE.md,docs/SYNC.md(HLC/LWW/op_log),docs/API.md(REST Specs). - ✅ Post-MVP – Account optional: App startet ohne Login direkt in die Listen (Local-Only-Default), Sync/Account optional.
- ✅ Post-MVP – Self-Hosted Server-URL + Authentik: Server-URL in der App
konfigurierbar (
DynamicBaseUrlInterceptor), Authentik über Generic-OIDC. - ✅ Post-MVP – Geteilte Listen: Invite-Codes (Migration 000002), Join/Invite-
Endpoints, Membership-Checks in
GetLists/GetList/Push/Pull. - ✅ Post-MVP – Tests für Share/Join:
liststore_test.go(Store-Level) +api_test.go(E2E über HTTP): Invite/Join/Idempotenz/Rollen/Access-Control (Fremde → 404 auf allen Listen-Endpoints), Cross-Member-Sync via op_log, 401-Gating aller geschützten Routen. Beide Dateien skippen ohneTEST_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?
MVP + Post-MVP-Features sind abgeschlossen. Das Backend ist feature-complete für den aktuellen Scope. Offene Punkte, geordnet nach Nutzen:
- Join-Rate-Limiting:
POST /api/lists/joinhat kein Rate-Limit; 8-Zeichen- Code ist brute-force-bar (62⁸ ≈ 2×10¹⁴, aber trotzdem). Z.B. pro Session/IP drosseln oder fehlerhafte Joins verzögern. - Leave-List / Member-Removal / Code-Revocation: Es gibt keinen Endpoint,
eine Liste zu verlassen, Mitglieder zu entfernen oder einen Invite-Code zu
rotieren.
list_members-Rollen (owner/member) werden bisher kaum genutzt. - Invite-Code-Optimierung: Codes sind UUID-Präfixe (
uuid[:8]), nicht kollisionsresistent geprüft (UNIQUE-Constraint fängt es, aber CreateList kann dann fehlschlagen). Besser: kryptografisches Alphabet ohne Verwechslungsbuchstaben. - Tests für neuere Endpoints:
GET /api/config,GET/PUT /api/meund dasAUTH_PASSWORD_ENABLED=false-Verhalten (403) haben noch keine Testabdeckung – das Test-Setup dafür steht inapi_test.gobereit. - Echtzeit-Push (langfristig): Periodic-Pull 15 min ist MVP; SSE/WebSocket für sofortige Updates wäre der nächste Schritt.
- Android-Tests: UI/ViewModel-Tests fehlen fast komplett (nur 2 Stock-Tests).
Wichtige technische Notizen / Fallstricke (bereits gelöst)
- golang-migrate
iofs-Pfad: Der Import istgithub.com/golang-migrate/migrate/v4/source/iofs(MIT/source/). Die Pfade ohne/source/gibt es nicht mehr → Build-Fehler. - Go-Version:
pgx/v5 v5.10.0braucht Go ≥ 1.25, deshalb Go 1.26 (lokal + Docker-Imagegolang:1.26-alpine).go.modhatgo 1.26. - Docker-Build auf Windows/Docker Desktop:
--network hostim Build-Container funktioniert NICHT (WSL2-NAT). Für Tests: echtesdocker network create+ Container-Namen nutzen. - Compose braucht zwingend
POSTGRES_PASSWORD(.envoder Env), dadb.environmentmit${POSTGRES_PASSWORD:?...}-Assertion vor dem Build validiert wird. - Healthcheck im
backend-Service wurde entfernt, weil distroless/static keinwget/curlenthält. Später: eigenes Binary, das/healthzper Go-HTTP prüft, oderhealthchecküber Caddy/extern. - SQL-Queries werden direkt mit pgx geschrieben (kein sqlc im MVP), da sqlc lokal nicht installiert ist und ein extra Code-Gen-Schritt nötig wäre. Bei Bedarf später problemlos nachrüstbar.
Build- & Test-Befehle
# Backend lokal bauen
cd backend && go build ./... && go vet ./...
# Unit-Tests (ohne DB)
cd backend && go test ./...
# Integrationstests (Store + E2E-HTTP) gegen Docker-PostgreSQL:
docker run -d --name mitbringsl-test-pg -e POSTGRES_USER=app \
-e POSTGRES_PASSWORD=testpw -e POSTGRES_DB=appdb -p 55432:5432 postgres:16-alpine
cd backend && TEST_DATABASE_URL="postgres://app:testpw@localhost:55432/appdb?sslmode=disable" \
go test ./...
docker rm -f mitbringsl-test-pg # danach aufräumen
# Backend-Image bauen
cd backend && docker build -t mitbringsl-backend:test .
# Komplettes Stack starten (braucht deploy/.env)
cd deploy && cp .env.example .env && docker compose up -d --build
# Android bauen
cd android && ./gradlew assembleDebug && ./gradlew test
Git-Status
- Branch
main, Remoteoriginkonfiguriert (git.dietzlabs.net). - MVP (Phase A–F) + Post-MVP-Features (optionale Accounts, Server-URL-Config, geteilte Listen) committed und gepusht.