Commit graph

20 commits

Author SHA1 Message Date
15b3f1071f
Fix: Android compile errors in AuthScreen and MainScreen preview
- AuthScreen.kt: FocusManager is imported from androidx.compose.ui.focus,
  not androidx.compose.ui.platform (only LocalFocusManager lives there).
  The fully-qualified platform.FocusManager parameter types therefore
  failed to resolve, cascading into the clearFocus() errors.
- MainScreen.kt: MitbringslTheme gained a required sessionManager
  parameter in 729865b; the leftover template preview now passes
  SessionManager(LocalContext.current).

Verified: ./gradlew compileDebugKotlin, assembleDebug and test all green.
Note: local builds must use JDK 21 (JAVA_HOME=/usr/lib/jvm/java-21-openjdk);
the system default JDK 26 breaks AGP's JdkImageTransform (jlink) on
android-36's core-for-system-modules.jar.
2026-08-22 10:00:42 +02:00
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
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
85c790ed23
Build: make Android build work behind restrictive proxies
- settings.gradle.kts: comment out the foojay-resolver-convention plugin.
  It tries to auto-download JDK toolchains from api.foojay.io, which fails
  with 503 behind restrictive proxies. Re-enable at home if you want
  auto-provisioning.
- app/build.gradle.kts: comment out jvmToolchain(17) for the same reason
  (triggers toolchain resolution -> foojay download). The build JDK 21
  plus compileOptions(source/target 17) achieves the same result locally.
- gradle-wrapper.properties: bump to gradle-9.6.1-all (matches the
  manually provided distribution).
- .gitignore: ignore .ssl-work/ (local corporate-CA truststore).
2026-08-06 11:43:03 +02:00
729865be78
Feature: Settings screen with account, theme, profile, reset + credits
Backend:
- New GET/PUT /api/me endpoint to read and update the authenticated
  user's profile (currently display_name). UserStore.UpdateDisplayName.
- Wired into the authed router.

App:
- New SettingsScreen + SettingsViewModel with five sections:
  * Account & Sync: login status, email, server URL, logout, connect.
  * Profile: edit display name (pushed to PUT /api/me when logged in).
  * Appearance: System / Light / Dark theme switch, persisted in
    SessionManager and applied via MitbringslTheme(sessionManager).
  * Data: 'Reset local data' wipes Room tables (server data kept).
  * About: version, 'Developed by Janik Dietz', and credits to
    GLM-5.2 + Gemini 3.6 Flash.
- Theme.kt now reads the user's theme preference (StateFlow) instead
  of only the system default; MainActivity passes SessionManager in.
- Navigation: new SettingsNavKey; settings gear icon in ListsScreen
  top bar; Settings links back to the Sync/Account screen.
- DTOs/API: UpdateMeRequestDto + getMe()/updateMe() for /api/me.
2026-08-06 10:39:12 +02:00
3f187f1ede
Feature: Server-driven auth method discovery + OIDC-only enforcement
Backend:
- New AUTH_PASSWORD_ENABLED flag (default true). When false, email/password
  registration and login return 403; the server enforces OIDC-only login.
- New OIDC_GENERIC_DISPLAY_NAME so the app can show 'Authentik'/'Keycloak'
  instead of a generic 'OIDC' label.
- New public endpoint GET /api/config returns which auth methods the
  server offers (password_enabled + per-provider OIDC capabilities).
  No auth required, so the login screen can query it before logging in.
- .env.example and docker-compose.yml expose the new env vars.

App:
- DTOs + MitbringslApi.getServerConfig() for /api/config.
- AuthViewModel: new 'connect' flow. The user enters the server URL,
  taps 'Verbinden', and the app fetches /api/config. The returned
  ServerAuthConfig drives which login options are shown:
    * password-only -> email/password form
    * OIDC-only     -> OIDC token form
    * both          -> toggle between the two
  If the server offers no method, a clear error is shown.
- AuthScreen: split into ConnectView (server URL) and LoginView (the
  login form matching the server's capabilities). The mode toggle only
  appears when the server offers more than one method.
2026-08-06 10:28:12 +02:00
b44bc8c3af
Fix: Critical sync, build and data-safety bugs in Android app
Sync engine (critical):
- SyncWorker now pulls server ops for EVERY tracked list, not just
  lists with pending local outbox ops. Previously remote edits on
  'quiet' lists (incl. shared/joined lists) never arrived.
- SyncWorker advances the local HLC with each incoming server op
  (tick(op.hlcTs)) so LWW is correct across devices with skewed
  clocks; previously a fast-clock device permanently won conflicts
  and a slow-clock device's own edits were silently rejected.
- Added missing 'list_create' branch in applyServerOpProjection.
- Use server hlc_ts for createdAt/updatedAt in projections so lists
  and items keep a stable order instead of jumping by sync time.
- Prune old synced op_log rows (deleteOldSynced) to bound growth.
- Added ListDao.getAllListIds() one-shot query for the pull loop.

Build / runtime (critical):
- Added proguard-rules.pro with keep rules for kotlinx.serialization
  serializers and Retrofit interfaces; release builds with R8 would
  otherwise crash with SerializationException on the first API call.
- Added network_security_config.xml allowing cleartext only to
  10.0.2.2/localhost so the debug build can reach the local backend
  (blocked by default on Android 9+).
- Manifest: disable default WorkManager initializer so Hilt's
  HiltWorkerFactory is used (otherwise SyncWorker can fail to
  instantiate); added ACCESS_NETWORK_STATE permission.

Data safety (critical/major):
- Removed fallbackToDestructiveMigration from DatabaseModule: in a
  local-first app a destructive migration on schema bump would wipe
  the source of truth. Provide explicit Migrations instead.
- ShoppingRepository: wrap every local projection write + op_log
  insert in db.withTransaction{} so a crash between them can no
  longer silently lose a pending sync op.
- ShoppingRepository: replace manual JSON string concatenation with
  kotlinx.serialization buildJsonObject; the old escapeJson did not
  handle tab/CR/control chars, producing malformed op payloads.
- Persist device clientId and clientSeq counter in SessionManager so
  they survive process restarts (idempotency stays stable per device).
- Trigger immediate + periodic sync after login/register/OIDC so
  users see their lists without waiting up to 15 minutes.

.gitignore: ignore desktop.ini and backend/.testbin.
2026-08-06 10:01:57 +02:00
97583340a4
Refactor: Require active server connection for sharing and joining lists
- ListDetailScreen: hide Share action icon when not connected to a server (isLoggedIn == false)
- ListsScreen: show informative prompt dialog if user attempts to join a list without active server connection
- ListDetailViewModel: expose sessionManager to check auth state cleanly
- ADB reinstall & launch verified 
2026-08-05 20:40:08 +02:00
69591df12a
Feature: Shared lists & Invite Code invitation mechanism
- Backend:
  - Migration 000002: add invite_code column to lists & list_members auto-population
  - ListStore: GetLists/GetList check owner_id & list_members; add GetInviteCode & JoinByInviteCode
  - HTTP API: add POST /api/lists/{id}/invite & POST /api/lists/join
- Android App:
  - DTOs & MitbringslApi: add JoinListRequestDto, InviteCodeResponseDto & endpoints
  - ListsScreen & ViewModel: add 'Liste beitreten' action button & dialog for entering invite code
  - ListDetailScreen & ViewModel: add 'Liste teilen' action icon in top bar with System Share Sheet
- ADB reinstall & launch verified 
2026-08-05 20:36:43 +02:00
47b24dfcdf
Feature: Self-Hosted Server URL configuration & Authentik OIDC support
- DynamicBaseUrlInterceptor: dynamically rewrites Retrofit HTTP requests to custom server URL
- SessionManager: persist custom server_url setting (e.g. https://mitbringsl.mydomain.com)
- AuthScreen & ViewModel: add Server URL chip & edit dialog + Authentik / OIDC login mode
- NetworkModule: inject SessionManager into DynamicBaseUrlInterceptor & AuthInterceptor
- ADB reinstall & launch verified 
2026-08-05 20:32:08 +02:00
04e5c55c2a
Fix: Resolve list selection crash & update to minimal flat app icon
- ListDetailViewModel & Screen: set listId safely via setListId() avoiding SavedStateHandle null crashes
- Navigation: pass listId = key.listId explicitly into ListDetailScreen
- App Icon: replace 3D icon with a clean, minimal flat line-art shopping bag & checkmark icon suited for Samsung One UI & stock Android adaptive icon masks
- ADB reinstall & launch verified 
2026-08-05 20:27:38 +02:00
b9f10b1a2e
Design: Add modern 3D vector shopping bag launcher icon resources
- Generate high-res 3D shopping bag icon with checkmark badge
- Populate mipmap densities (mdpi, hdpi, xhdpi, xxhdpi, xxxhdpi)
- Configure adaptive icon XML & dark blue background color
- Re-install via ADB verified 
2026-08-05 20:22:25 +02:00
a5ad5cd766
Refactor: Make account 100% optional, default to local-only mode
- SessionManager: return stable local_user ID when not logged in
- Navigation: start directly at ListsNavKey without requiring login
- ListsScreen: add 'Sync / Account' chip in top bar; lists work 100% locally by default
- AuthScreen: update text & add 'Zurück zu meinen Listen' button for returning to local mode
- ADB reinstall & launch verified 
2026-08-05 20:19:29 +02:00
a00db14cba
Phase F: NetworkMonitor, offline banner, README & architecture docs
- NetworkMonitor: ConnectivityState Flow using ConnectivityManager.NetworkCallback
- ListsScreen & ViewModel: live offline indicator banner when disconnected
- Documentation:
  - docs/ARCHITECTURE.md: system design & tech stack overview
  - docs/SYNC.md: HLC timestamping, op_log outbox & LWW projection specification
  - docs/API.md: REST API endpoint specification
  - README.md: quickstart guide for backend, docker compose & Android app
- Verification: backend & android test suites 100% green 
2026-08-05 20:14:04 +02:00
174aad535a
Android Phase D & E: Auth Screen, SyncEngine, Lists & Detail Screens
- Auth UI & Logic: AuthScreen (Login/Register), AuthViewModel, AuthRepository, SessionManager
- SyncEngine: HybridLogicalClock (client-side), SyncWorker (outbox drain + server cursor pull), SyncManager
- Repository Layer: ShoppingRepository for local-first Room mutations + op_log outbox queue
- Screens & UI: ListsScreen (list overview & creation dialog), ListDetailScreen (checked/open sectioning, autocomplete AddItemBar)
- Navigation: MainNavigation (AuthNavKey -> ListsNavKey -> ListDetailNavKey)
- Verification: ./gradlew assembleDebug & ./gradlew test green 
2026-08-05 20:12:28 +02:00
e44d645112
Android Phase D: Project foundation, Room DB, Retrofit API, Hilt
- Android CLI Setup: initialized empty-activity app (AGP 9.0, Kotlin 2.3.20)
- Version catalog: Compose BOM 2026.03.01, Material 3, Hilt 2.60.1, Room 2.7.2,
  Retrofit 2.11.0, OkHttp 4.12.0, WorkManager 2.10.2, Kotlinx Serialization
- Room Database (v1): ListEntity, ItemEntity, OpLogEntity
- Room DAOs: ListDao, ItemDao, OpLogDao with LWW upsert queries
- Network layer: MitbringslApi Retrofit interface + DTOs + AuthInterceptor
- Dependency Injection: DatabaseModule, NetworkModule, RepositoryModule, HiltAndroidApp
- Build verification: assembleDebug & test green 
2026-08-05 20:09:34 +02:00
895725b5e5
Backend Phase C: Sync-Kern + Caddy behind-proxy
Sync-Kern:
- internal/sync/hlc.go: Hybrid Logical Clock (wall_ms<<16|counter)
  Tick/Now/After, global mutex, strikt monoton + kausal korrekt
- internal/sync/hlc_test.go: Unit-Tests (monoton, kausal, keine Duplikate)

Store-Schicht:
- internal/store/opstore.go: AppendOps idempotent via UNIQUE(client_id,
  client_seq) ON CONFLICT DO NOTHING; LWW-Projektion (list_create/
  rename/delete, item_add/update/remove) in derselben Transaktion;
  PullOps mit Cursor (seq > since, 500er Pages)
- internal/store/liststore.go: CreateList / GetLists / GetList
- internal/store/itemstore.go: GetItems (nicht-gelöschte Items)
- internal/store/suggeststore.go: Search (pg_trgm + LIKE-fallback, 10)

HTTP-Handler:
- internal/httpapi/lists.go: GET/POST /api/lists, GET /api/lists/{id}
- internal/httpapi/ops.go: POST /api/lists/{id}/ops (Push),
  GET /api/lists/{id}/ops (Pull ?since=)
- internal/httpapi/suggest.go: GET /api/suggestions?q=
- internal/httpapi/api.go: alle Routen verdrahtet (RequireAuth)

Deployment:
- deploy/Caddyfile.behind-proxy: auto_https off, trusted_proxies
- deploy/Caddyfile: X-Forwarded-Proto hinzugefügt, Kommentar aktualisiert
- deploy/docker-compose.yml: CADDY_HTTP_PORT + CADDY_HTTPS_PORT
- deploy/.env.example: Caddy-Port-Variablen dokumentiert

go build ./... && go vet ./... && go test ./... 
HLC-Tests: monoton, kausal, keine Duplikate 
AGENTS.md: Phase C vollständig 
2026-08-05 19:56:05 +02:00
a5ef8cf3ba
Backend Phase B (2/2): OIDC auth + config validation
- internal/auth/oidc.go: OIDCService mit go-oidc v3
  - id_token-Verifikation via JWKS (Signatur, iss, aud, exp)
  - Provider-Caching (sync.Map, lazy init per Issuer-URL)
  - Unterstützt Google + Generic OIDC
- internal/auth/user.go: GetByOIDCSubject + CreateOIDCUser
  (find-or-create via (oidc_issuer, oidc_subject))
- internal/httpapi/auth.go: POST /auth/oidc Handler
  (id_token verifiziern → find-or-create User → issueSession)
- internal/httpapi/api.go: /auth/oidc Route verdrahtet
- internal/config/config.go: OIDC-Validierung
  (enabled → client_id + issuer Pflicht)
- go.mod/go.sum: go-oidc/v3 + oauth2 Abhängigkeiten
- AGENTS.md: Phase B vollständig als erledigt markiert

Verifiziert: E2E gegen lokalen Mock-IdP (Discovery → JWKS →
signiertes id_token → User angelegt → 2. Login gleicher User →
tampered Token → 401). Alle Fehlerpfade geprüft.

go build ./... && go vet ./... && go test ./internal/auth/... 
2026-08-05 19:45:00 +02:00
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
2899eb205b
Backend Phase A: foundation, migrations, Docker setup
- Go backend skeleton: config (caarlos0/env), slog JSON logging,
  pgxpool store, HTTP server with graceful shutdown.
- httpapi: render helpers, Problem errors, middleware chain
  (requestID / logging / recover / CORS), /healthz and /readyz.
- Migrations: full initial schema (users, sessions, lists,
  list_members, items, op_log SOURCE OF TRUTH, item_names) +
  golang-migrate runner binary using source/iofs (embedded).
- Docker: multi-stage Dockerfile (Go 1.26 -> distroless nonroot),
  builds both server and migrate binaries.
- deploy: docker-compose (caddy + backend + migrate + postgres:16),
  Caddyfile (auto-HTTPS), .env.example, pg extensions init script.
- AGENTS.md: project context + roadmap for AI agents.

Verified: image builds, both binaries run in container (smoke test).
2026-08-05 15:14:37 +02:00