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
This commit is contained in:
parent
a972ce34ca
commit
cb576349e0
7 changed files with 570 additions and 0 deletions
12
.dockerignore
Normal file
12
.dockerignore
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
.git
|
||||||
|
.github
|
||||||
|
.gradle
|
||||||
|
.idea
|
||||||
|
app/
|
||||||
|
build/
|
||||||
|
tmp/
|
||||||
|
memories/
|
||||||
|
Anforderungen/
|
||||||
|
local.properties
|
||||||
|
*.apk
|
||||||
|
*.aab
|
||||||
19
Dockerfile
Normal file
19
Dockerfile
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Stage 1: Build the fat JAR
|
||||||
|
FROM gradle:8.11.1-jdk21 AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY gradle/ gradle/
|
||||||
|
COPY gradlew gradlew.bat build.gradle.kts settings.gradle.kts gradle.properties ./
|
||||||
|
COPY shared/ shared/
|
||||||
|
COPY server/ server/
|
||||||
|
RUN gradle :server:buildFatJar --no-daemon
|
||||||
|
|
||||||
|
# Stage 2: Run
|
||||||
|
FROM eclipse-temurin:21-jre-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /app/server/build/libs/server.jar server.jar
|
||||||
|
|
||||||
|
ENV KRISENVORRAT_API_KEY="change-me-to-a-secure-key-at-least-32-chars"
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
ENTRYPOINT ["java", "-jar", "server.jar"]
|
||||||
143
README.md
Normal file
143
README.md
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
# Krisenvorrat
|
||||||
|
|
||||||
|
Android-App zur Verwaltung eines Krisenvorrats-Inventars mit lokaler Datenhaltung und Sync-Möglichkeit über einen LAN-Server.
|
||||||
|
|
||||||
|
## Projektstruktur
|
||||||
|
|
||||||
|
| Modul | Beschreibung |
|
||||||
|
|----------|-------------------------------------------------|
|
||||||
|
| `app` | Android-App (Kotlin, Jetpack Compose, Room) |
|
||||||
|
| `server` | Ktor REST-Server (H2-Datenbank, Fat-JAR) |
|
||||||
|
| `shared` | Gemeinsame DTOs (kotlinx.serialization) |
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
|
||||||
|
- **Android Studio** oder IntelliJ IDEA
|
||||||
|
- **JDK 21** (für Server) / JDK 11+ (für App)
|
||||||
|
- **Android SDK** (API Level 35)
|
||||||
|
|
||||||
|
## App bauen
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Windows
|
||||||
|
.\gradlew assembleDebug
|
||||||
|
|
||||||
|
# Linux/Mac
|
||||||
|
./gradlew assembleDebug
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tests ausführen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Alle Tests (App + Server)
|
||||||
|
./gradlew test
|
||||||
|
|
||||||
|
# Nur Server-Tests
|
||||||
|
./gradlew :server:test
|
||||||
|
|
||||||
|
# Nur App-Tests
|
||||||
|
./gradlew :app:test
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Server-Setup (LAN-Sync)
|
||||||
|
|
||||||
|
### Übersicht
|
||||||
|
|
||||||
|
Der Sync-Server empfängt das Inventar der App als JSON und gibt es an andere App-Instanzen im selben LAN weiter. Die Kommunikation läuft über einfaches HTTP mit API-Key-Authentifizierung.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────┐ PUT /api/inventory ┌──────────┐
|
||||||
|
│ App A │ ───────────────────────► │ Server │
|
||||||
|
│ (Upload) │ │ :8080 │
|
||||||
|
└──────────┘ └────┬─────┘
|
||||||
|
│
|
||||||
|
┌──────────┐ GET /api/inventory ┌────┴─────┐
|
||||||
|
│ App B │ ◄─────────────────────── │ Server │
|
||||||
|
│(Download)│ │ :8080 │
|
||||||
|
└──────────┘ └──────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schnellstart
|
||||||
|
|
||||||
|
#### Option A: Direkt starten (empfohlen für Entwicklung)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Windows
|
||||||
|
.\start-server.ps1
|
||||||
|
|
||||||
|
# Linux/Mac
|
||||||
|
chmod +x start-server.sh
|
||||||
|
./start-server.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Das Skript baut automatisch das Fat-JAR und zeigt die LAN-IP an.
|
||||||
|
|
||||||
|
#### Option B: Manuell mit Gradle
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :server:run
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option C: Fat-JAR
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# JAR bauen
|
||||||
|
./gradlew :server:buildFatJar
|
||||||
|
|
||||||
|
# Starten
|
||||||
|
java -jar server/build/libs/server.jar
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option D: Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Image bauen
|
||||||
|
docker build -t krisenvorrat-server .
|
||||||
|
|
||||||
|
# Container starten
|
||||||
|
docker run -d \
|
||||||
|
--name krisenvorrat \
|
||||||
|
-p 8080:8080 \
|
||||||
|
-e KRISENVORRAT_API_KEY="mein-sicherer-api-key" \
|
||||||
|
-v krisenvorrat-data:/app/data \
|
||||||
|
krisenvorrat-server
|
||||||
|
```
|
||||||
|
|
||||||
|
### LAN-Setup
|
||||||
|
|
||||||
|
1. **Server-IP ermitteln:**
|
||||||
|
- Windows: `ipconfig` → IPv4-Adresse des WLAN/LAN-Adapters
|
||||||
|
- Linux/Mac: `ip addr` oder `ifconfig`
|
||||||
|
- Die Start-Skripte zeigen die IP automatisch an
|
||||||
|
|
||||||
|
2. **Firewall-Regel:** Port **8080** (TCP eingehend) muss freigeschaltet sein:
|
||||||
|
- Windows: `netsh advfirewall firewall add rule name="Krisenvorrat" dir=in action=allow protocol=TCP localport=8080`
|
||||||
|
- Linux: `sudo ufw allow 8080/tcp`
|
||||||
|
|
||||||
|
3. **App konfigurieren:**
|
||||||
|
- Einstellungen → Server-URL: `http://<SERVER-IP>:8080`
|
||||||
|
- Einstellungen → API-Key: den gleichen Key wie beim Server-Start
|
||||||
|
|
||||||
|
### API-Endpunkte
|
||||||
|
|
||||||
|
| Methode | Pfad | Auth | Beschreibung |
|
||||||
|
|---------|-------------------|-----------|------------------------------------|
|
||||||
|
| GET | `/api/health` | – | Health-Check, gibt "OK" zurück |
|
||||||
|
| GET | `/api/inventory` | API-Key | Inventar vom Server laden (Pull) |
|
||||||
|
| PUT | `/api/inventory` | API-Key | Inventar auf Server speichern (Push)|
|
||||||
|
|
||||||
|
**Authentifizierung:** API-Key als `X-API-Key`-Header oder `Bearer`-Token.
|
||||||
|
|
||||||
|
### Umgebungsvariablen
|
||||||
|
|
||||||
|
| Variable | Standard | Beschreibung |
|
||||||
|
|-----------------------|-------------------------------------------------|-------------------|
|
||||||
|
| `KRISENVORRAT_API_KEY` | `change-me-to-a-secure-key-at-least-32-chars` | API-Key für Auth |
|
||||||
|
|
||||||
|
### Sicherheitshinweise
|
||||||
|
|
||||||
|
- Der Dev-Server nutzt **HTTP** (kein HTTPS) – nur im vertrauenswürdigen LAN verwenden.
|
||||||
|
- Für Produktionseinsatz: HTTPS mit Reverse-Proxy (z.B. nginx/Caddy) konfigurieren.
|
||||||
|
- API-Key sollte mindestens 32 Zeichen lang sein.
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
android:theme="@style/Theme.Krisenvorrat">
|
android:theme="@style/Theme.Krisenvorrat">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,279 @@
|
||||||
|
package de.krisenvorrat.server
|
||||||
|
|
||||||
|
import de.krisenvorrat.server.db.DatabaseFactory
|
||||||
|
import de.krisenvorrat.shared.model.CategoryDto
|
||||||
|
import de.krisenvorrat.shared.model.InventoryDto
|
||||||
|
import de.krisenvorrat.shared.model.ItemDto
|
||||||
|
import de.krisenvorrat.shared.model.LocationDto
|
||||||
|
import de.krisenvorrat.shared.model.SettingDto
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.client.statement.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.server.config.*
|
||||||
|
import io.ktor.server.testing.*
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* End-to-End Sync-Tests: Simuliert den vollständigen Sync-Workflow
|
||||||
|
* (App exportiert Inventar → Server speichert → andere App importiert).
|
||||||
|
*/
|
||||||
|
class EndToEndSyncTest {
|
||||||
|
|
||||||
|
private val json = Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
encodeDefaults = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testApp(block: suspend ApplicationTestBuilder.() -> Unit) = testApplication {
|
||||||
|
environment {
|
||||||
|
config = MapApplicationConfig("krisenvorrat.apiKey" to API_KEY)
|
||||||
|
}
|
||||||
|
application {
|
||||||
|
DatabaseFactory.init(
|
||||||
|
jdbcUrl = "jdbc:h2:mem:e2e_test_${System.nanoTime()};DB_CLOSE_DELAY=-1",
|
||||||
|
driver = "org.h2.Driver"
|
||||||
|
)
|
||||||
|
configurePlugins()
|
||||||
|
}
|
||||||
|
block()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_fullSyncCycle_pushThenPull_roundTripsCorrectly() = testApp {
|
||||||
|
// Given – "Client A" hat ein vollständiges Inventar
|
||||||
|
val clientAInventory = createFullInventory()
|
||||||
|
val body = json.encodeToString(InventoryDto.serializer(), clientAInventory)
|
||||||
|
|
||||||
|
// When – Client A pusht (Upload)
|
||||||
|
val pushResponse = client.put("/api/inventory") {
|
||||||
|
header("X-API-Key", API_KEY)
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then – Push erfolgreich
|
||||||
|
assertEquals(HttpStatusCode.OK, pushResponse.status)
|
||||||
|
|
||||||
|
// When – "Client B" pullt (Download)
|
||||||
|
val pullResponse = client.get("/api/inventory") {
|
||||||
|
header("X-API-Key", API_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then – Client B erhält exakt die Daten von Client A
|
||||||
|
assertEquals(HttpStatusCode.OK, pullResponse.status)
|
||||||
|
val pulledInventory = json.decodeFromString<InventoryDto>(pullResponse.bodyAsText())
|
||||||
|
|
||||||
|
assertEquals(clientAInventory.categories.size, pulledInventory.categories.size)
|
||||||
|
assertEquals(clientAInventory.locations.size, pulledInventory.locations.size)
|
||||||
|
assertEquals(clientAInventory.items.size, pulledInventory.items.size)
|
||||||
|
assertEquals(clientAInventory.settings.size, pulledInventory.settings.size)
|
||||||
|
|
||||||
|
// Inhalte prüfen
|
||||||
|
assertEquals("Konserven", pulledInventory.categories[0].name)
|
||||||
|
assertEquals("Getränke", pulledInventory.categories[1].name)
|
||||||
|
assertEquals("Keller", pulledInventory.locations[0].name)
|
||||||
|
assertEquals("Dosenbrot", pulledInventory.items[0].name)
|
||||||
|
assertEquals(5.0, pulledInventory.items[0].quantity, 0.01)
|
||||||
|
assertEquals("dark", pulledInventory.settings[0].value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_multiClientSync_secondClientOverwritesFirst() = testApp {
|
||||||
|
// Given – Client A pusht
|
||||||
|
val clientAInventory = createFullInventory()
|
||||||
|
client.put("/api/inventory") {
|
||||||
|
header("X-API-Key", API_KEY)
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(json.encodeToString(InventoryDto.serializer(), clientAInventory))
|
||||||
|
}
|
||||||
|
|
||||||
|
// When – Client B pusht ein anderes Inventar (überschreibt)
|
||||||
|
val clientBInventory = InventoryDto(
|
||||||
|
categories = listOf(CategoryDto(id = 10, name = "Hygiene")),
|
||||||
|
locations = listOf(LocationDto(id = 10, name = "Badezimmer")),
|
||||||
|
items = listOf(
|
||||||
|
ItemDto(
|
||||||
|
id = "item-b1",
|
||||||
|
name = "Seife",
|
||||||
|
categoryId = 10,
|
||||||
|
quantity = 3.0,
|
||||||
|
unit = "Stück",
|
||||||
|
unitPrice = 1.50,
|
||||||
|
kcalPer100g = null,
|
||||||
|
expiryDate = null,
|
||||||
|
locationId = 10,
|
||||||
|
minStock = 1.0,
|
||||||
|
notes = "",
|
||||||
|
lastUpdated = 1715100000L
|
||||||
|
)
|
||||||
|
),
|
||||||
|
settings = listOf(SettingDto(key = "lang", value = "de"))
|
||||||
|
)
|
||||||
|
val pushResponse = client.put("/api/inventory") {
|
||||||
|
header("X-API-Key", API_KEY)
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(json.encodeToString(InventoryDto.serializer(), clientBInventory))
|
||||||
|
}
|
||||||
|
assertEquals(HttpStatusCode.OK, pushResponse.status)
|
||||||
|
|
||||||
|
// Then – Client C pullt und sieht nur Client B's Daten
|
||||||
|
val pullResponse = client.get("/api/inventory") {
|
||||||
|
header("X-API-Key", API_KEY)
|
||||||
|
}
|
||||||
|
val pulledInventory = json.decodeFromString<InventoryDto>(pullResponse.bodyAsText())
|
||||||
|
|
||||||
|
assertEquals(1, pulledInventory.categories.size)
|
||||||
|
assertEquals("Hygiene", pulledInventory.categories[0].name)
|
||||||
|
assertEquals(1, pulledInventory.locations.size)
|
||||||
|
assertEquals("Badezimmer", pulledInventory.locations[0].name)
|
||||||
|
assertEquals(1, pulledInventory.items.size)
|
||||||
|
assertEquals("Seife", pulledInventory.items[0].name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_pullFromEmptyServer_returnsEmptyInventory() = testApp {
|
||||||
|
// When – Pull ohne vorherigen Push
|
||||||
|
val response = client.get("/api/inventory") {
|
||||||
|
header("X-API-Key", API_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertEquals(HttpStatusCode.OK, response.status)
|
||||||
|
val inventory = json.decodeFromString<InventoryDto>(response.bodyAsText())
|
||||||
|
assertTrue(inventory.categories.isEmpty())
|
||||||
|
assertTrue(inventory.locations.isEmpty())
|
||||||
|
assertTrue(inventory.items.isEmpty())
|
||||||
|
assertTrue(inventory.settings.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_pushPullPushPull_multipleRoundTrips() = testApp {
|
||||||
|
// Round-Trip 1: Push + Pull
|
||||||
|
val inventory1 = createFullInventory()
|
||||||
|
client.put("/api/inventory") {
|
||||||
|
header("X-API-Key", API_KEY)
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(json.encodeToString(InventoryDto.serializer(), inventory1))
|
||||||
|
}
|
||||||
|
val pull1 = client.get("/api/inventory") { header("X-API-Key", API_KEY) }
|
||||||
|
val result1 = json.decodeFromString<InventoryDto>(pull1.bodyAsText())
|
||||||
|
assertEquals(2, result1.items.size)
|
||||||
|
|
||||||
|
// Round-Trip 2: Aktualisiertes Inventar pushen
|
||||||
|
val inventory2 = InventoryDto(
|
||||||
|
categories = listOf(CategoryDto(id = 1, name = "Konserven")),
|
||||||
|
locations = listOf(LocationDto(id = 1, name = "Keller")),
|
||||||
|
items = listOf(
|
||||||
|
ItemDto(
|
||||||
|
id = "item-1",
|
||||||
|
name = "Dosenbrot",
|
||||||
|
categoryId = 1,
|
||||||
|
quantity = 10.0,
|
||||||
|
unit = "Stück",
|
||||||
|
unitPrice = 3.99,
|
||||||
|
kcalPer100g = 250,
|
||||||
|
expiryDate = "2027-06-15",
|
||||||
|
locationId = 1,
|
||||||
|
minStock = 2.0,
|
||||||
|
notes = "Menge erhöht",
|
||||||
|
lastUpdated = 1715200000L
|
||||||
|
)
|
||||||
|
),
|
||||||
|
settings = emptyList()
|
||||||
|
)
|
||||||
|
client.put("/api/inventory") {
|
||||||
|
header("X-API-Key", API_KEY)
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(json.encodeToString(InventoryDto.serializer(), inventory2))
|
||||||
|
}
|
||||||
|
val pull2 = client.get("/api/inventory") { header("X-API-Key", API_KEY) }
|
||||||
|
val result2 = json.decodeFromString<InventoryDto>(pull2.bodyAsText())
|
||||||
|
|
||||||
|
assertEquals(1, result2.items.size)
|
||||||
|
assertEquals(10.0, result2.items[0].quantity, 0.01)
|
||||||
|
assertEquals("Menge erhöht", result2.items[0].notes)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_healthEndpoint_alwaysAvailableWithoutAuth() = testApp {
|
||||||
|
// When
|
||||||
|
val response = client.get("/api/health")
|
||||||
|
|
||||||
|
// Then – Health-Endpoint braucht keinen API-Key
|
||||||
|
assertEquals(HttpStatusCode.OK, response.status)
|
||||||
|
assertEquals("OK", response.bodyAsText())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_pushWithoutAuth_returns401() = testApp {
|
||||||
|
// When – Push ohne API-Key
|
||||||
|
val response = client.put("/api/inventory") {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(json.encodeToString(InventoryDto.serializer(), createFullInventory()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertEquals(HttpStatusCode.Unauthorized, response.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun test_pullWithoutAuth_returns401() = testApp {
|
||||||
|
// When – Pull ohne API-Key
|
||||||
|
val response = client.get("/api/inventory")
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertEquals(HttpStatusCode.Unauthorized, response.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createFullInventory(): InventoryDto = InventoryDto(
|
||||||
|
categories = listOf(
|
||||||
|
CategoryDto(id = 1, name = "Konserven"),
|
||||||
|
CategoryDto(id = 2, name = "Getränke")
|
||||||
|
),
|
||||||
|
locations = listOf(
|
||||||
|
LocationDto(id = 1, name = "Keller"),
|
||||||
|
LocationDto(id = 2, name = "Speisekammer")
|
||||||
|
),
|
||||||
|
items = listOf(
|
||||||
|
ItemDto(
|
||||||
|
id = "item-1",
|
||||||
|
name = "Dosenbrot",
|
||||||
|
categoryId = 1,
|
||||||
|
quantity = 5.0,
|
||||||
|
unit = "Stück",
|
||||||
|
unitPrice = 3.99,
|
||||||
|
kcalPer100g = 250,
|
||||||
|
expiryDate = "2027-06-15",
|
||||||
|
locationId = 1,
|
||||||
|
minStock = 2.0,
|
||||||
|
notes = "Vollkornbrot in der Dose",
|
||||||
|
lastUpdated = 1715000000L
|
||||||
|
),
|
||||||
|
ItemDto(
|
||||||
|
id = "item-2",
|
||||||
|
name = "Mineralwasser",
|
||||||
|
categoryId = 2,
|
||||||
|
quantity = 24.0,
|
||||||
|
unit = "Liter",
|
||||||
|
unitPrice = 0.49,
|
||||||
|
kcalPer100g = 0,
|
||||||
|
expiryDate = "2028-01-01",
|
||||||
|
locationId = 2,
|
||||||
|
minStock = 12.0,
|
||||||
|
notes = "Stilles Wasser",
|
||||||
|
lastUpdated = 1715000000L
|
||||||
|
)
|
||||||
|
),
|
||||||
|
settings = listOf(
|
||||||
|
SettingDto(key = "theme", value = "dark"),
|
||||||
|
SettingDto(key = "household_size", value = "4")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val API_KEY = "e2e-test-api-key-for-sync"
|
||||||
|
}
|
||||||
|
}
|
||||||
54
start-server.ps1
Normal file
54
start-server.ps1
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Startet den Krisenvorrat Dev-Server im LAN.
|
||||||
|
.DESCRIPTION
|
||||||
|
Baut das Fat-JAR (falls nötig) und startet den Server auf Port 8080.
|
||||||
|
Der Server ist unter der LAN-IP des Rechners erreichbar.
|
||||||
|
.PARAMETER Build
|
||||||
|
Erzwingt einen Neubau des Fat-JARs, auch wenn es bereits existiert.
|
||||||
|
.PARAMETER ApiKey
|
||||||
|
Setzt einen benutzerdefinierten API-Key. Standard: "dev-api-key-change-in-production"
|
||||||
|
.EXAMPLE
|
||||||
|
.\start-server.ps1
|
||||||
|
.\start-server.ps1 -Build
|
||||||
|
.\start-server.ps1 -ApiKey "mein-geheimer-key"
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[switch]$Build,
|
||||||
|
[string]$ApiKey = "dev-api-key-change-in-production"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
Set-Location $PSScriptRoot
|
||||||
|
|
||||||
|
$jarPath = "server/build/libs/server.jar"
|
||||||
|
|
||||||
|
# Fat-JAR bauen falls nötig
|
||||||
|
if ($Build -or -not (Test-Path $jarPath)) {
|
||||||
|
Write-Host "Building server fat JAR..." -ForegroundColor Cyan
|
||||||
|
& .\gradlew.bat :server:buildFatJar 2>&1 | Out-String | Write-Host
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Error "Build failed with exit code $LASTEXITCODE"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
Write-Host "Build successful." -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
# LAN-IP ermitteln
|
||||||
|
$lanIp = (Get-NetIPAddress -AddressFamily IPv4 |
|
||||||
|
Where-Object { $_.InterfaceAlias -notmatch "Loopback" -and $_.IPAddress -notmatch "^169\." } |
|
||||||
|
Select-Object -First 1).IPAddress
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "=== Krisenvorrat Dev-Server ===" -ForegroundColor Green
|
||||||
|
Write-Host "Local: http://localhost:8080" -ForegroundColor Yellow
|
||||||
|
if ($lanIp) {
|
||||||
|
Write-Host "LAN: http://${lanIp}:8080" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
Write-Host "API-Key: $ApiKey" -ForegroundColor Yellow
|
||||||
|
Write-Host "Press Ctrl+C to stop" -ForegroundColor Gray
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Server starten
|
||||||
|
$env:KRISENVORRAT_API_KEY = $ApiKey
|
||||||
|
java -jar $jarPath
|
||||||
62
start-server.sh
Normal file
62
start-server.sh
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Startet den Krisenvorrat Dev-Server im LAN.
|
||||||
|
#
|
||||||
|
# Verwendung:
|
||||||
|
# ./start-server.sh # Standard API-Key
|
||||||
|
# ./start-server.sh --build # Fat-JAR neu bauen
|
||||||
|
# ./start-server.sh --api-key "mein-key" # Eigener API-Key
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
API_KEY="dev-api-key-change-in-production"
|
||||||
|
FORCE_BUILD=false
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--build)
|
||||||
|
FORCE_BUILD=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--api-key)
|
||||||
|
API_KEY="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option: $1" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
JAR_PATH="server/build/libs/server.jar"
|
||||||
|
|
||||||
|
# Fat-JAR bauen falls nötig
|
||||||
|
if [ "$FORCE_BUILD" = true ] || [ ! -f "$JAR_PATH" ]; then
|
||||||
|
echo "Building server fat JAR..."
|
||||||
|
./gradlew :server:buildFatJar
|
||||||
|
echo "Build successful."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# LAN-IP ermitteln
|
||||||
|
LAN_IP=""
|
||||||
|
if command -v ip &>/dev/null; then
|
||||||
|
LAN_IP=$(ip -4 addr show scope global | grep -oP 'inet \K[\d.]+' | head -1)
|
||||||
|
elif command -v ifconfig &>/dev/null; then
|
||||||
|
LAN_IP=$(ifconfig | grep 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | head -1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Krisenvorrat Dev-Server ==="
|
||||||
|
echo "Local: http://localhost:8080"
|
||||||
|
if [ -n "$LAN_IP" ]; then
|
||||||
|
echo "LAN: http://${LAN_IP}:8080"
|
||||||
|
fi
|
||||||
|
echo "API-Key: $API_KEY"
|
||||||
|
echo "Press Ctrl+C to stop"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Server starten
|
||||||
|
export KRISENVORRAT_API_KEY="$API_KEY"
|
||||||
|
java -jar "$JAR_PATH"
|
||||||
Loading…
Reference in a new issue