mitbringsl/AGENTS.md
Tronax 7b1c18590e
Backend Phase B (1/2): password auth + sessions
Argon2id password hashing (PHC format, self-encoded/decoded without an
external lib) with constant-time verification, UserStore (create/get by
email and id) and SessionStore (opaque crypto/rand tokens, SHA-256 hashed
in DB, create/lookup/revoke, last_seen_at bump on lookup).

HTTP layer: Register/Login/Logout handlers + RequireAuth middleware.
Login uses a dummy-hash path so unknown-email and wrong-password yield the
same timing/shape, narrowing user enumeration. Tokens accepted via Bearer
header (native clients) or session cookie (HttpOnly, SameSite=Lax).

Routes wired in api.go: POST /auth/register, /auth/login, /auth/logout.
Verified with go test, go vet and an end-to-end smoke test against a real
PostgreSQL container (register/login/logout/duplicate/short-pw/wrong-pw).

OIDC (Phase B part 2) follows next; the issueSession helper is reused.
2026-08-05 19:05:07 +02:00

212 lines
11 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.

# 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`), Extensions `pgcrypto` + `pg_trgm`.
- **Sync:** Local-First, **keine Datenverluste** Append-only `op_log` (SOURCE OF TRUTH),
`items`/`lists` sind Projektionen; Konfliktlösung per LWW-Register `(hlc_ts, client_id)`
+ Tombstones (`deleted_at`). HLC = Hybrid Logical Clock.
- Push: `POST /api/lists/{id}/ops` (idempotent via `UNIQUE(client_id, client_seq)`).
- Pull: `GET /api/lists/{id}/ops?since={seq}`.
- **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_token` ans Backend (`POST /auth/oidc`).
Backend verifiziert via `github.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). Endpoint `GET /api/suggestions?q=`.
- **MVP-Scope:** Single-Owner-Listen (`list_members` existiert, wird in Phase 2 für
geteilte Listen genutzt); 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
│ │ │ ├── password.go # Argon2id im PHC-Format (HashPassword/VerifyPassword)
│ │ │ ├── password_test.go # PHC-Roundtrip-Tests
│ │ │ ├── user.go # UserStore: CreateUser/GetByEmail/GetByID
│ │ │ ├── session.go # SessionStore: opaque Tokens, SHA-256-Hash, Create/Lookup/Revoke
│ │ │ └── pgcode.go # isUniqueViolation (SQLSTATE 23505)
│ │ └── httpapi/
│ │ ├── api.go # API-Objekt + Router (Health + /auth/* aktiv, Rest auskommentiert)
│ │ ├── render.go # JSON-Render + Problem + Fehler-Sentinale + decodeJSON
│ │ ├── middleware.go # requestID/logging/recover/cors + Chain
│ │ ├── health.go # /healthz + /readyz
│ │ └── auth.go # Register/Login/Logout-Handler + RequireAuth-Middleware
│ ├── 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
│ ├── Dockerfile # Multi-Stage, baut server + migrate
│ ├── .dockerignore
│ ├── go.mod / go.sum
├── deploy/
│ ├── docker-compose.yml # caddy + backend + migrate + db (mit YAML-anchors)
│ ├── Caddyfile # auto-HTTPS
│ ├── .env.example # alle env-Vars dokumentiert
│ └── db/init/001_extensions.sql # CREATE EXTENSION pgcrypto, pg_trgm
├── docs/ # NOCH LEER (folgt Phase F)
└── android/ # NOCH LEER (folgt Phase D)
```
---
## Roadmap / Fortschritt
Legende: ✅ erledigt · 🚧 in Arbeit · ⬜ offen
- ✅ Repo-Struktur + `.gitignore` + `README.md` + `git init` (Branch `main`)
-**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) + Handler
`Register`/`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:** go-oidc-Verifikation (Google + Generic) + `POST /auth/oidc`.
-**Phase C Sync-Kern:** `op_log`-Append (idempotent), HLC, Projektion op→items/lists (LWW+Tombstones).
-**Phase C Endpoints:** `/api/lists`, `/api/lists/{id}/ops` (push+pull).
-**Phase C Suggestions:** `item_names`-Trigger + `/api/suggestions`.
-**Phase D Android-Fundament:** Gradle (Kotlin DSL, Version Catalog, Hilt/KSP, Compose BOM),
Theme, Nav, Room.
-**Phase D Repository + Retrofit-API + DTOs.**
-**Phase D Login-Screen** (eigene User + Google Credential Manager + Generic OIDC PKCE).
-**Phase E SyncEngine** (OutboxDrain + CursorPull via WorkManager), HLC client-side.
-**Phase E Listen-Übersicht + Detail + AddItemBar (Autocomplete) + Settings.**
-**Phase F Polish** (Fehlerbehandlung, Offline-Indikator, Empty States, Tests).
-**Phase F README + docs** (ARCHITECTURE/SYNC/API).
### Wo genau weitermachen?
**Nächster Schritt = Phase B Teil 2 (OIDC)** Teil 1 (Password-Auth) ist fertig ✅:
Teil 1 (erledigt):
-`internal/auth/password.go` Argon2id im PHC-Format (HashPassword/VerifyPassword).
-`internal/auth/user.go` UserStore (CreateUser/GetByEmail/GetByID).
-`internal/auth/session.go` SessionStore (crypto/rand + base64url + SHA-256, Create/Lookup/Revoke).
-`internal/httpapi/auth.go` Register/Login/Logout + RequireAuth-Middleware.
- ✅ In `api.go` sind `/auth/register`, `/auth/login`, `/auth/logout` aktiv verdrahtet.
Teil 2 (offen OIDC):
1. `internal/auth/oidc.go` Verifikation eines `id_token` via `github.com/coreos/go-oidc/v3`
(JWKS-Signatur, iss/aud/exp prüfen). Provider-Auswahl anhand `issuer` aus Config.
2. `POST /auth/oidc`-Handler in `internal/httpapi/auth.go`: empfängt `{provider, id_token}`,
verifiziert, findet/legt User an (oidc_issuer+oidc_subject UNIQUE), stellt Session aus
(gleicher `issueSession`-Pfad wie Login).
3. In `api.go` `mux.HandleFunc("POST /auth/oidc", a.auth.OIDC)` einkommentieren.
4. Config-Validierung: wenn `OIDC_GOOGLE_ENABLED=true`, muss `OIDC_GOOGLE_CLIENT_ID` gesetzt sein.
---
## Wichtige technische Notizen / Fallstricke (bereits gelöst)
- **golang-migrate `iofs`-Pfad:** Der Import ist
`github.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.0` braucht Go ≥ 1.25, deshalb Go 1.26
(lokal + Docker-Image `golang:1.26-alpine`). `go.mod` hat `go 1.26`.
- **Docker-Build auf Windows/Docker Desktop:** `--network host` im Build-Container
funktioniert NICHT (WSL2-NAT). Für Tests: echtes `docker network create` + Container-Namen nutzen.
- **Compose braucht zwingend `POSTGRES_PASSWORD`** (`.env` oder Env), da
`db.environment` mit `${POSTGRES_PASSWORD:?...}`-Assertion vor dem Build validiert wird.
- **Healthcheck im `backend`-Service wurde entfernt**, weil distroless/static kein
`wget`/`curl` enthält. Später: eigenes Binary, das `/healthz` per Go-HTTP prüft,
oder `healthcheck` ü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
```bash
# Backend lokal bauen
cd backend && go build ./... && go vet ./...
# 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
# Migrationen manuell gegen bestehende DB anwenden
docker run --rm --network <net> \
-e DATABASE_URL="postgres://app:PW@<db-host>:5432/appdb?sslmode=disable" \
mitbringsl-backend:test /app/migrate up
```
## Git-Status
- Repo initialisiert, Branch `main`. **Remote ist noch NICHT konfiguriert.**
- Es wurde **noch nicht committet** (Stand beim Schreiben dieser Datei).