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:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:theme="@style/Theme.Krisenvorrat">
|
||||
<activity
|
||||
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