Commit graph

18 commits

Author SHA1 Message Date
Jens Reinemann
7c17f8ea2f feat(server): Rate-Limiting auf alle API-Endpoints
Ktor RateLimit-Plugin mit abgestuften Limits pro Endpoint-Gruppe:
- Auth (/api/auth/*): 10 req/min per IP (Brute-Force-Schutz)
- Messages (/api/messages/*): 30 req/min per IP (Spam-Schutz)
- Inventory (/api/inventory/*): 60 req/min per IP (DoS-Schutz)
- Admin (/api/admin/*): 20 req/min per IP

Neue Dateien:
- RateLimiting.kt: Plugin-Konfiguration mit 4 benannten Limitern
- RateLimitingTest.kt: 5 Tests (Limit-Ueberschreitung, Within-Limit,
  Health-Endpoint ohne Limit, Retry-After-Header)

Geaenderte Dateien:
- Routing.kt: rateLimit()-Wrapper um Route-Gruppen
- Application.kt: configureRateLimiting() in Plugin-Pipeline
- libs.versions.toml + build.gradle.kts: ktor-server-rate-limit Dep

Closes #75
2026-05-17 03:31:57 +02:00
Jens Reinemann
0f25c180ed feat(sync): Full-Inventory-Sync durch Delta-Sync ersetzen
Server + Client: Timestamp-basierter Delta-Sync als Alternative zum
Full-Sync. GET /api/inventory akzeptiert jetzt optionalen ?since=<ts>
Query-Parameter und liefert nur Items mit lastUpdated > since.

Shared: InventoryDto um deletedItemIds-Feld erweitert (Default: leer,
backward-compatible mit bestehenden Clients).

Server:
- DeletedItems-Tabelle trackt geloeschte Item-IDs pro Inventory
- InventoryRepository.loadInventorySince(): Delta-Query mit Items +
  deletedItemIds seit Timestamp
- saveInventory()/deleteItem(): Loeschungen werden in DeletedItems
  protokolliert
- DatabaseFactory: DeletedItems-Tabelle registriert

Client (App):
- SyncService.downloadInventory(since: Long?): optionaler since-Param
- SyncServiceImpl: haengt ?since= an GET-Request
- ItemDao.deleteByIds(): Batch-Loeschung fuer Delta-Sync
- ImportExportRepositoryImpl: verarbeitet deletedItemIds aus DTO
- SettingsViewModel.pullSync(fullSync): Delta-Sync mit letztem
  Sync-Timestamp; fullSyncRequired-Event loest weiterhin Full-Sync aus

Entscheidungen:
- Timestamp-basiert (nutzt bestehendes lastUpdated-Feld)
- Full-Sync bleibt Fallback (fullSyncRequired, erster Sync)
- Categories/Locations/Settings immer vollstaendig (klein)

6 neue DeltaSyncTests + 3 Repository-Tests + 2 SyncService-Tests

Closes #74
2026-05-17 03:15:49 +02:00
Jens Reinemann
f792213b1e refactor(server): H2 durch PostgreSQL ersetzen
- DatabaseFactory: HikariCP Connection-Pool fuer PostgreSQL (10 Connections,
  REPEATABLE_READ), H2 weiterhin ohne Pool (fuer Tests)
- Dependencies: postgresql-Treiber + HikariCP hinzugefuegt, H2 nur noch
  testImplementation
- Migration-SQL: uppercase Tabellennamen auf lowercase normalisiert
  (dialect-agnostisch fuer H2 und PostgreSQL)
- docker-compose.yml: PostgreSQL 17 + Krisenvorrat-Server mit DB-Env-Vars
- Env-Var-Konfiguration: KRISENVORRAT_DB_URL, _DB_USER, _DB_PASSWORD,
  _DB_DRIVER (Defaults auf PostgreSQL localhost)
- Alle 554 Tests gruen (H2 in-memory fuer Tests beibehalten)

Closes #70
2026-05-17 02:35:08 +02:00
Jens Reinemann
033b0fae61 feat(server): Statistik-Kacheln auf Admin-Inventarübersicht
- InventoryStatsDto: neues DTO mit totalItems, totalLocations,
  totalCategories, lastUpdated, recentTransactions
- InventoryRepository.getAggregatedStats(): Aggregierte Statistiken
  über alle Inventare (COUNT, MAX, 30-Tage-Filter)
- AdminRoutes: GET /api/admin/stats Endpoint (admin-only)
- Admin-UI: Stats-Grid mit 5 Kacheln (Artikel, Orte, Kategorien,
  Änderungen 30 Tage, letzte Änderung) oben auf der Übersichtsseite
- 7 neue Tests: 4 Endpoint-Tests (InventoryStatsTest),
  3 Repository-Tests (getAggregatedStats)
- Alle 87 Tests grün

Closes #68
2026-05-17 02:19:18 +02:00
Jens Reinemann
549e4c916e feat(server): PATCH /api/inventory/items/{id} auf partielles Update umstellen
- Route empfängt JsonObject statt ItemDto, nur übergebene Felder werden aktualisiert
- validatePartialItem() validiert nur vorhandene Felder (name, unit, expiryDate)
- patchItemPartial() im Repository: Guard für leere Felder, kein SQL-Update wenn nichts zu ändern
- Response liefert das aktualisierte Item aus der DB (loadItem) statt des Inputs
- Bestehende Tests in InputValidationTest angepasst (senden nun partielle JSON-Bodys)
- Neue PatchItemTest-Klasse: 10 Tests (Happy Path, 404, Auth, Validierung, Persistenz)
- Alle 554 Tests grün

Closes #56
2026-05-17 02:13:24 +02:00
Jens Reinemann
4b1a5818f2 feat(chat): UTF-8-Unterstützung für Umlaute und Emoji-Eingabe
Closes #66

Server (Serialization.kt):
- ContentNegotiation explizit mit charset=UTF-8 konfiguriert, damit
  Response-Header immer 'application/json; charset=utf-8' enthält

App (ChatScreen.kt):
- Emoji-Button (Face-Icon) zur MessageInputBar hinzugefügt, der bei Klick
  den Fokus auf das Eingabefeld setzt und die Soft-Keyboard öffnet (System-
  IME mit Emoji-Panel-Zugang)
- FocusRequester + LocalSoftwareKeyboardController integriert

Tests:
- Utf8MessagingTest (Server): 6 Tests für Umlaute, Emojis, Multi-Codepoint-
  Emojis, gemischte Nachrichten, Konversationsabruf, charset-Header
- ChatViewModelTest (App): 4 neue Tests für Umlaut-, Emoji- und gemischte
  Nachrichten
- run-integration-tests.ps1: Szenario 6a mit 5 Testfällen (Umlaute, Emojis,
  gemischt, Konversation, WebSocket-Delivery mit UTF-8)
2026-05-17 01:58:27 +02:00
Jens Reinemann
d354e3b37c security: Server-seitige Input-Validierung & Body-Size-Limit (#67)
- Routing.kt: Content-Length-Intercept (max 1 MB, HTTP 413)
- InventoryRoutes.kt: String-Längen (name ≤255, unit ≤50, id ≤36,
  category/location ≤255, setting key ≤255), expiryDate-Regex
  YYYY-MM-DD, Array-Limits (items ≤10000, categories/locations ≤500)
- AdminRoutes.kt: username-Länge ≤255
- InputValidationTest.kt: 16 neue negative Tests, alle 532 Tests grün
2026-05-17 01:20:49 +02:00
Jens Reinemann
2555a942ec fix: Server-App WebSocket-Kompatibilität und DELETE-Route
- WebSocketManager: inventory_updated -> inventoryUpdated,
  full_sync_required -> fullSyncRequired (camelCase wie App erwartet)
- InventoryRoutes: DELETE /api/inventory/items/{id} hinzugefügt
- InventoryRepository: deleteItem(inventoryId, itemId) implementiert
2026-05-17 00:42:03 +02:00
Jens Reinemann
2d4ebd63b0 fix(server): Code-Review-Korrekturen Inventory Sharing
- UserRow: inventoryId-Feld ergaenzt
- AdminRoutes: Delete nutzt findById() statt listAll().find{}
- AdminRoutes: PUT/POST inventory/new geben 404 wenn User nicht existiert
- index.html: Doppelten alten HTML-Content entfernt (277 Zeilen)
2026-05-17 00:35:15 +02:00
Jens Reinemann
c03475e7e5 feat(server): Inventory Sharing – User:Inventory N:1
- Inventories-Tabelle neu: id, created_at
- Users.inventory_id FK → Inventories
- Items/Categories/Locations/Settings: user_id → inventory_id
- DatabaseFactory: Schema-Migration + Data-Migration (user_id→inventory_id)
- InventoryRepository: getEffectiveInventoryId(), createInventory(),
  assignUserToInventory(), cleanupOrphanedInventory(),
  listInventoriesWithUsers(); alle Ops auf inventoryId umgestellt
- UserRepository.create(): legt automatisch neues Inventory an
- InventoryRoutes: löst inventoryId via getEffectiveInventoryId()
- AdminRoutes: PUT /users/{id}/inventory (zuweisen),
  POST /users/{id}/inventory/new (trennen), GET /admin/inventories
- Admin-UI: Inventar-Spalte, 'Inventar wechseln'-Modal, 'Neues
  Inventar'-Button, Inventar-Übersicht (gruppiert)
- InventorySharingTest: 8 neue Integrationstests (Sharing, Isolation,
  Cleanup, Berechtigungen)
- Alle 48 Server-Tests gruen (inkl. bestehende Tests unveraendert)
2026-05-17 00:30:06 +02:00
Jens Reinemann
56ac9b1425 feat: Messaging-System mit Offline-First und WebSocket-Push (#58)
## Server
- Messages-Tabelle (id, sender_id, receiver_id, body, sent_at, delivered_at)
- MessageRepository: save/getUndelivered/getConversation/markDelivered (JOIN statt N+1)
- POST /api/messages, GET /api/messages/{userId}: Nachrichten senden/abrufen
- GET /api/users: User-Liste fuer authentifizierte User (ohne eigenen Account)
- WebSocketManager: notifyNewMessage() + isOnline()
- WebSocketRoutes: unzugestellte Nachrichten bei Reconnect pushen
- LoginResponse: userId + username ergaenzt
- Server-Dependency: kotlinx.serialization fuer shared

## App
- MessageEntity + MessageDao (Room, Migration 3->4)
- KrisenvorratDatabase v4, Migrations.MIGRATION_3_4
- MessageRepositoryImpl: Offline-First (isPending), drain bei WebSocket-Connect
- WebSocketEvent.NewMessage -> MessageDto aus shared
- WebSocketClientImpl: new_message-Event parsen
- AUTH_USER_ID in SettingsKeys, SyncServiceImpl speichert userId bei Login
- UserListScreen + UserListViewModel: User-Liste anzeigen
- ChatScreen + ChatViewModel: WhatsApp-Style Chat (links/rechts, Zeitstempel)
- Navigation: Screen.UserList, Screen.Chat, MESSAGES in Bottom-Nav
- RepositoryModule: MessageRepository gebunden

## Tests
- 234 Tests, 0 Fehler
2026-05-16 23:35:25 +02:00
Jens Reinemann
14631c7327 feat(server): User-Konzept Auth, JWT, Admin-CRUD, WebSocket-Push, Admin-UI (#57)
- Users-Tabelle mit bcrypt-Passwort-Hash
- JWT-Auth (Access + Refresh Token) ersetzt API-Key
- POST /api/auth/login + /api/auth/refresh
- Admin-Endpoints: GET/POST/DELETE/PUT /api/admin/users
- Seed-Admin beim Start (KRISENVORRAT_ADMIN_PASSWORD)
- Inventar-Endpoints user-spezifisch (userId aus JWT)
- PATCH /api/inventory/items/{id} fuer Einzel-Updates
- WebSocket /ws/sync mit Push-Events (inventoryUpdated, fullSyncRequired)
- Minimale Admin-Web-UI unter /admin/ (XSS-sicher)
- Categories/Locations: Surrogate-PK fuer User-Isolation
- Alle Server-Tests gruen (43 Tests)
2026-05-16 19:28:03 +02:00
Jens Reinemann
8280a9daf9 refactor: kcal/100g -> kcal/kg umbenennen und Mindestbestand entfernen
- ItemEntity, ItemDto: kcalPer100g -> kcalPerKg (kcal_per_kg),
  minStock-Spalte komplett entfernt
- CalculateSupplyRangeUseCase: Formel angepasst (/ 1000.0 * kcalPerKg)
- GetMinStockWarningsUseCase + MinStockWarning: gelöscht
- UI (ItemFormScreen, WarningsScreen, DashboardScreen): Mindestbestand-
  Felder und Warnungsabschnitte entfernt
- ViewModels, UiState, Repository: alle Referenzen bereinigt
- Server (Tables, InventoryRepository): Schema angepasst
- Room: fallbackToDestructiveMigration() hinzugefügt (keine Produktivdaten)
- Alle 434 Tests gruen
2026-05-16 14:19:10 +02:00
Jens Reinemann
cb576349e0 feat(server): add LAN dev-server integration & end-to-end sync tests
Start scripts (PS1 + Shell), Dockerfile, E2E sync tests, and README
documentation for Phase 2 LAN server deployment.

New files:
- start-server.ps1 / start-server.sh: one-command server startup with
  auto-build, LAN-IP detection, and configurable API key
- Dockerfile: multi-stage build (Gradle → JRE Alpine) for container
  deployment with volume mount for persistent data
- .dockerignore: excludes app/, .git, build artifacts from Docker context
- EndToEndSyncTest.kt: 7 E2E tests covering full push/pull sync cycle,
  multi-client overwrite, empty DB pull, multiple round-trips, and auth
  rejection for unauthenticated requests
- README.md: project overview, build instructions, and complete Phase 2
  server setup docs (4 start options, LAN setup, API reference, security)

Changed files:
- AndroidManifest.xml: added usesCleartextTraffic=true for HTTP in LAN

Closes #46
2026-05-14 21:45:33 +02:00
Jens Reinemann
cb9bd2bdf4 feat(server): add API-Key authentication for REST endpoints
server/plugins/Authentication.kt:
- Custom Ktor AuthenticationProvider supporting both X-API-Key header
  and Authorization: Bearer <key> for API-Key validation
- ApiKeyPrincipal data class implementing Principal interface
- 401 Unauthorized with ErrorResponse body for missing/invalid keys

server/plugins/Routing.kt:
- Inventory routes wrapped in authenticate(api-key) block
- Health endpoint remains public (no auth required)

server/src/main/resources/application.conf:
- API key configurable via krisenvorrat.apiKey property
- Environment variable override via KRISENVORRAT_API_KEY

server/tests:
- 7 new AuthenticationTest cases (valid bearer, valid X-API-Key,
  missing key, invalid bearer, invalid X-API-Key, PUT without key,
  health without key)
- All existing ApplicationTest cases updated with bearer auth header

Closes #43
2026-05-14 20:50:16 +02:00
Jens Reinemann
5974144621 feat(server): add REST-API endpoints for inventory sync & CRUD
server/src/main/kotlin/.../routes/InventoryRoutes.kt:
- GET /api/inventory: returns full inventory as JSON
- PUT /api/inventory: full-sync replaces entire server inventory

server/src/main/kotlin/.../plugins/StatusPages.kt:
- Structured error handling via Ktor StatusPages plugin
- BadRequestException, SerializationException, IllegalArgumentException → 400
- Unhandled exceptions → 500 with logging

server/src/main/kotlin/.../plugins/CallLogging.kt:
- Request logging via Ktor CallLogging plugin (INFO level)

server/src/main/kotlin/.../model/ErrorResponse.kt:
- Serializable error response DTO (status + message)

server/src/main/kotlin/.../plugins/Routing.kt:
- Health endpoint moved from /health to /api/health
- Inventory routes mounted under /api/inventory

server/src/main/kotlin/.../Application.kt:
- Added configurePlugins() for testability (DB init separate)
- StatusPages and CallLogging plugins configured

server/src/test/.../ApplicationTest.kt:
- 8 endpoint tests using Ktor TestApplication with in-memory H2
- Tests: health, 404, empty GET, PUT valid, PUT+GET roundtrip,
  invalid JSON → 400, data replacement, JSON content type

Closes #42
2026-05-14 20:30:34 +02:00
Jens Reinemann
2387c6ee5a feat(server): add Exposed ORM database layer with H2
server/db/Tables.kt:
- Exposed table definitions for Categories, Locations, Items, Settings
- Schema mirrors Room entities from app module
- Foreign keys on Items referencing Categories and Locations

server/db/DatabaseFactory.kt:
- H2 file-based DB initialization (jdbc:h2:file:./data/krisenvorrat)
- Parameterized for testability (in-memory DB for tests)
- Schema auto-creation via SchemaUtils.create()

server/repository/InventoryRepository.kt:
- Full CRUD: saveInventory() and loadInventory()
- Atomic replace via transaction (deleteAll + insert)
- Direct mapping between Exposed rows and shared DTOs

4 repository tests with H2 in-memory covering:
- Empty DB, full round-trip, overwrite, nullable fields

Closes #41
2026-05-14 20:15:07 +02:00
Jens Reinemann
cb190e61e9 feat(server): add Ktor server module with health endpoint
New Gradle module :server (Kotlin/JVM) with Ktor 3.1.2 framework,
configured as an embedded Netty HTTP server.

server/src/main/kotlin/de/krisenvorrat/server/:
- Application.kt: entry point using EngineMain for HOCON config
- plugins/Routing.kt: GET /health endpoint returning 200 OK
- plugins/Serialization.kt: ContentNegotiation with kotlinx.json

Configuration:
- application.conf (HOCON): host 0.0.0.0, port 8080, module reference
- logback.xml: SLF4J/Logback console logging

Build config:
- server/build.gradle.kts: Ktor plugin with Fat JAR (server.jar)
- libs.versions.toml: Ktor 3.1.2, Logback 1.5.18 dependencies
- settings.gradle.kts: include(:server)
- :server depends on :shared for common DTO models

Tests: 2 tests (health endpoint, 404 on unknown route) via
Ktor testApplication.

Closes #40
2026-05-14 20:06:40 +02:00