From e38ab27fbea3d8c3260bf9998ec80fa5b06682ec Mon Sep 17 00:00:00 2001 From: stefan Date: Sat, 19 Jul 2025 14:13:51 +0200 Subject: [PATCH] (fix) Konfiguration-Setup Umbau zu SCS --- README_CONFIG.md | 173 ++++++++++++++++ .../kotlin/at/mocode/gateway/Application.kt | 26 +-- .../kotlin/at/mocode/gateway/module.kt | 53 +++++ config/application-dev.properties | 13 ++ config/application-prod.properties | 16 ++ config/application-staging.properties | 16 ++ config/application-test.properties | 14 ++ config/application.properties | 32 +++ .../repository/VeranstaltungRepositoryImpl.kt | 28 +-- .../repository/HorseRepositoryImpl.kt | 8 +- .../repository/LandRepositoryImpl.kt | 32 ++- .../service/UserAuthorizationService.kt | 2 +- .../repository/BerechtigungRepositoryImpl.kt | 28 ++- .../repository/PersonRepositoryImpl.kt | 31 ++- .../repository/VereinRepositoryImpl.kt | 36 ++-- .../at/mocode/shared/config/AppConfig.kt | 187 ++++++++++++++++++ .../at/mocode/shared/config/AppEnvironment.kt | 48 +++++ .../mocode/shared/database/DatabaseConfig.kt | 33 +++- 18 files changed, 655 insertions(+), 121 deletions(-) create mode 100644 README_CONFIG.md create mode 100644 api-gateway/src/jvmMain/kotlin/at/mocode/gateway/module.kt create mode 100644 config/application-dev.properties create mode 100644 config/application-prod.properties create mode 100644 config/application-staging.properties create mode 100644 config/application-test.properties create mode 100644 config/application.properties create mode 100644 shared-kernel/src/jvmMain/kotlin/at/mocode/shared/config/AppConfig.kt create mode 100644 shared-kernel/src/jvmMain/kotlin/at/mocode/shared/config/AppEnvironment.kt diff --git a/README_CONFIG.md b/README_CONFIG.md new file mode 100644 index 00000000..3183b930 --- /dev/null +++ b/README_CONFIG.md @@ -0,0 +1,173 @@ +# Konfigurationsmanagement + +Dieses Dokument beschreibt, wie die Konfiguration des Meldestelle-Projekts verwaltet wird. + +## Übersicht + +Das Projekt verwendet einen mehrschichtigen Konfigurationsansatz, um verschiedene Umgebungen (Entwicklung, Test, Staging, Produktion) zu unterstützen. Die Konfiguration kann über folgende Quellen bereitgestellt werden: + +1. Umgebungsvariablen (höchste Priorität) +2. Umgebungsspezifische Konfigurationsdateien (.properties) +3. Basis-Konfigurationsdatei (application.properties) +4. Standardwerte im Code (niedrigste Priorität) + +## Konfigurationsquellen + +### Umgebungsvariablen + +Umgebungsvariablen haben die höchste Priorität und überschreiben alle anderen Konfigurationen. Sie werden typischerweise verwendet, um sensible Informationen wie Passwörter oder umgebungsspezifische Werte zu setzen. + +Beispiel: +```bash +# Umgebung festlegen +export APP_ENV=PRODUCTION + +# Datenbank-Konfiguration +export DB_HOST=db.example.com +export DB_PORT=5432 +export DB_NAME=meldestelle_db +export DB_USER=db_user +export DB_PASSWORD=secret_password + +# Server-Konfiguration +export API_PORT=8081 +``` + +### Konfigurationsdateien + +Das Projekt verwendet .properties-Dateien im `/config`-Verzeichnis. Die folgenden Dateien werden geladen (in dieser Reihenfolge): + +1. `application.properties` - Basiseinstellungen für alle Umgebungen +2. Umgebungsspezifische Datei - abhängig von `APP_ENV`: + - `application-dev.properties` - Entwicklungsumgebung (Standard) + - `application-test.properties` - Testumgebung + - `application-staging.properties` - Staging-Umgebung + - `application-prod.properties` - Produktionsumgebung + +## Umgebungen + +Das Projekt unterstützt folgende Umgebungen: + +| Umgebung | Beschreibung | Typische Verwendung | +|----------|-------------|--------------------| +| DEVELOPMENT | Lokale Entwicklungsumgebung | Lokale Entwicklung, Debug-Modus aktiv | +| TEST | Testumgebung | Automatisierte Tests, Integrationstests | +| STAGING | Vorabproduktionsumgebung | Manuelle Tests, UAT, Demos | +| PRODUCTION | Produktionsumgebung | Live-System | + +Die aktuelle Umgebung wird über die Umgebungsvariable `APP_ENV` festgelegt. Wenn diese Variable nicht gesetzt ist, wird standardmäßig `DEVELOPMENT` verwendet. + +## Konfigurationsstruktur + +Die Konfiguration ist in mehrere Kategorien unterteilt: + +### AppInfo + +Allgemeine Anwendungsinformationen: + +```properties +app.name=Meldestelle +app.version=1.0.0 +app.description=Pferdesport Meldestelle System +``` + +### Server + +Server-Konfiguration: + +```properties +server.port=8081 +server.host=0.0.0.0 +server.workers=4 +server.cors.enabled=true +server.cors.allowedOrigins=* +``` + +### Datenbank + +Datenbank-Konfiguration: + +```properties +database.host=localhost +database.port=5432 +database.name=meldestelle_db +database.username=meldestelle_user +database.password=secure_password_change_me +database.maxPoolSize=10 +database.autoMigrate=true +``` + +### Sicherheit + +Sicherheitseinstellungen (JWT, etc.): + +```properties +security.jwt.secret=your-secret-key +security.jwt.issuer=meldestelle-api +security.jwt.audience=meldestelle-clients +security.jwt.realm=meldestelle +security.jwt.expirationInMinutes=1440 +``` + +### Logging + +Logging-Konfiguration: + +```properties +logging.level=INFO +logging.requests=true +logging.responses=false +``` + +## Verwendung im Code + +Die Konfiguration wird über die zentrale `AppConfig`-Klasse bereitgestellt: + +```kotlin +import at.mocode.shared.config.AppConfig + +// Verwendung der Konfiguration +fun example() { + // Umgebung prüfen + if (AppConfig.environment.isDevelopment()) { + println("Debug-Modus aktiv") + } + + // Server-Port abrufen + val port = AppConfig.server.port + + // Datenbank-Konfiguration + val dbConfig = AppConfig.database + + // JWT-Secret + val jwtSecret = AppConfig.security.jwt.secret +} +``` + +## Konfiguration für Docker + +Bei Verwendung von Docker werden Umgebungsvariablen in der `.env`-Datei und im `docker-compose.yml` definiert: + +```yaml +services: + server: + environment: + - APP_ENV=PRODUCTION + - DB_HOST=db + - DB_PORT=5432 + - DB_NAME=${POSTGRES_DB} + - DB_USER=${POSTGRES_USER} + - DB_PASSWORD=${POSTGRES_PASSWORD} +``` + +## Beste Praktiken + +1. **Sensible Daten**: Speichern Sie niemals sensible Daten wie Passwörter oder API-Schlüssel direkt in Konfigurationsdateien, die in die Versionskontrolle eingecheckt werden. Verwenden Sie stattdessen Umgebungsvariablen. + +2. **Umgebungsspezifische Konfiguration**: Verwenden Sie umgebungsspezifische Konfigurationsdateien nur für Werte, die sich zwischen den Umgebungen unterscheiden. + +3. **Standardwerte**: Geben Sie für alle Konfigurationsparameter sinnvolle Standardwerte an, damit die Anwendung auch funktioniert, wenn nicht alle Konfigurationen explizit gesetzt sind. + +4. **Validierung**: Validieren Sie kritische Konfigurationen beim Anwendungsstart, um Fehler frühzeitig zu erkennen. + +5. **Dokumentation**: Halten Sie die Dokumentation der Konfigurationsparameter aktuell, damit neue Teammitglieder die Anwendung leicht konfigurieren können. diff --git a/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/Application.kt b/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/Application.kt index 3440c731..40c89ee2 100644 --- a/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/Application.kt +++ b/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/Application.kt @@ -1,38 +1,30 @@ package at.mocode.gateway import at.mocode.gateway.config.MigrationSetup +import at.mocode.shared.config.AppConfig import at.mocode.shared.database.DatabaseConfig import at.mocode.shared.database.DatabaseFactory +import io.ktor.serialization.kotlinx.json.* import io.ktor.server.application.* import io.ktor.server.engine.* import io.ktor.server.netty.* import io.ktor.server.plugins.contentnegotiation.* -import io.ktor.serialization.kotlinx.json.* -import io.ktor.server.routing.* import io.ktor.server.response.* +import io.ktor.server.routing.* fun main() { + // Konfiguration laden (wird automatisch beim ersten Zugriff auf AppConfig initialisiert) + val config = AppConfig + // Datenbank initialisieren - val databaseConfig = DatabaseConfig.fromEnv() - DatabaseFactory.init(databaseConfig) + DatabaseFactory.init(config.database) // Migrationen ausführen MigrationSetup.runMigrations() // Server starten - embeddedServer(Netty, port = System.getenv("API_PORT")?.toIntOrNull() ?: 8081) { - configureApplication() + embeddedServer(Netty, port = config.server.port, host = config.server.host) { + module() }.start(wait = true) } -fun Application.configureApplication() { - install(ContentNegotiation) { - json() - } - - routing { - get("/health") { - call.respond(mapOf("status" to "OK")) - } - } -} diff --git a/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/module.kt b/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/module.kt new file mode 100644 index 00000000..b81df9f0 --- /dev/null +++ b/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/module.kt @@ -0,0 +1,53 @@ +package at.mocode.gateway + +import at.mocode.shared.config.AppConfig +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import io.ktor.server.application.* +import io.ktor.server.plugins.calllogging.* +import io.ktor.server.plugins.contentnegotiation.* +import io.ktor.server.plugins.cors.routing.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +fun Application.module() { + val config = AppConfig + + // ContentNegotiation installieren + install(ContentNegotiation) { + json() + } + + // CORS installieren, wenn aktiviert + if (config.server.cors.enabled) { + install(CORS) { + if (config.server.cors.allowedOrigins.contains("*")) { + anyHost() + } else { + config.server.cors.allowedOrigins.forEach { allowHost(it, schemes = listOf("http", "https")) } + } + allowHeader(HttpHeaders.ContentType) + allowHeader(HttpHeaders.Authorization) + allowMethod(HttpMethod.Options) + allowMethod(HttpMethod.Get) + allowMethod(HttpMethod.Post) + allowMethod(HttpMethod.Put) + allowMethod(HttpMethod.Delete) + } + } + + // Call-Logging installieren + if (config.logging.logRequests) { + install(CallLogging) + } + + routing { + // Hauptrouten + get("/") { + call.respondText( + "${config.appInfo.name} API v${config.appInfo.version} (${config.environment})", + ContentType.Text.Plain + ) + } + } +} diff --git a/config/application-dev.properties b/config/application-dev.properties new file mode 100644 index 00000000..a9fef008 --- /dev/null +++ b/config/application-dev.properties @@ -0,0 +1,13 @@ +# Entwicklungsumgebung spezifische Konfiguration + +# Server-Einstellungen +server.port=8081 + +# Datenbank-Einstellungen +database.host=localhost +database.port=5432 + +# Logging-Einstellungen +logging.level=DEBUG +logging.requests=true +logging.responses=true diff --git a/config/application-prod.properties b/config/application-prod.properties new file mode 100644 index 00000000..ea60cbdf --- /dev/null +++ b/config/application-prod.properties @@ -0,0 +1,16 @@ +# Produktionsumgebung spezifische Konfiguration + +# Server-Einstellungen +server.port=8081 +server.workers=4 +server.cors.allowedOrigins=https://meldestelle.at,https://app.meldestelle.at + +# Datenbank-Einstellungen +database.host=db +database.port=5432 +database.maxPoolSize=20 + +# Logging-Einstellungen +logging.level=INFO +logging.requests=true +logging.responses=false diff --git a/config/application-staging.properties b/config/application-staging.properties new file mode 100644 index 00000000..9b4be4e5 --- /dev/null +++ b/config/application-staging.properties @@ -0,0 +1,16 @@ +# Staging-Umgebung spezifische Konfiguration + +# Server-Einstellungen +server.port=8081 +server.workers=2 +server.cors.allowedOrigins=https://staging.meldestelle.at + +# Datenbank-Einstellungen +database.host=db +database.port=5432 +database.name=meldestelle_staging_db + +# Logging-Einstellungen +logging.level=INFO +logging.requests=true +logging.responses=false diff --git a/config/application-test.properties b/config/application-test.properties new file mode 100644 index 00000000..de92125d --- /dev/null +++ b/config/application-test.properties @@ -0,0 +1,14 @@ +# Testumgebung spezifische Konfiguration + +# Server-Einstellungen +server.port=8082 + +# Datenbank-Einstellungen +database.host=localhost +database.port=5432 +database.name=meldestelle_test_db + +# Logging-Einstellungen +logging.level=DEBUG +logging.requests=true +logging.responses=true diff --git a/config/application.properties b/config/application.properties new file mode 100644 index 00000000..f05c4e0e --- /dev/null +++ b/config/application.properties @@ -0,0 +1,32 @@ +# Allgemeine Anwendungseinstellungen +app.name=Meldestelle +app.version=1.0.0 +app.description=Pferdesport Meldestelle System + +# Server-Einstellungen +server.port=8081 +server.host=0.0.0.0 +server.workers=4 +server.cors.enabled=true +server.cors.allowedOrigins=* + +# Datenbank-Einstellungen +database.host=localhost +database.port=5432 +database.name=meldestelle_db +database.username=meldestelle_user +database.password=secure_password_change_me +database.maxPoolSize=10 +database.autoMigrate=true + +# Sicherheits-Einstellungen +security.jwt.secret=default-jwt-secret-key-please-change-in-production +security.jwt.issuer=meldestelle-api +security.jwt.audience=meldestelle-clients +security.jwt.realm=meldestelle +security.jwt.expirationInMinutes=1440 + +# Logging-Einstellungen +logging.level=INFO +logging.requests=true +logging.responses=false diff --git a/event-management/src/jvmMain/kotlin/at/mocode/events/infrastructure/repository/VeranstaltungRepositoryImpl.kt b/event-management/src/jvmMain/kotlin/at/mocode/events/infrastructure/repository/VeranstaltungRepositoryImpl.kt index 7f7780d2..c909764d 100644 --- a/event-management/src/jvmMain/kotlin/at/mocode/events/infrastructure/repository/VeranstaltungRepositoryImpl.kt +++ b/event-management/src/jvmMain/kotlin/at/mocode/events/infrastructure/repository/VeranstaltungRepositoryImpl.kt @@ -1,16 +1,14 @@ package at.mocode.events.infrastructure.repository +import at.mocode.enums.SparteE import at.mocode.events.domain.model.Veranstaltung import at.mocode.events.domain.repository.VeranstaltungRepository -import at.mocode.enums.SparteE import com.benasher44.uuid.Uuid import kotlinx.datetime.Clock import kotlinx.datetime.LocalDate -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq -import org.jetbrains.exposed.sql.SqlExpressionBuilder.like import org.jetbrains.exposed.sql.statements.UpdateBuilder /** @@ -22,21 +20,21 @@ import org.jetbrains.exposed.sql.statements.UpdateBuilder class VeranstaltungRepositoryImpl : VeranstaltungRepository { override suspend fun findById(id: Uuid): Veranstaltung? { - return VeranstaltungTable.select { VeranstaltungTable.id eq id } + return VeranstaltungTable.selectAll().where { VeranstaltungTable.id eq id } .map { rowToVeranstaltung(it) } .singleOrNull() } override suspend fun findByName(searchTerm: String, limit: Int): List { val searchPattern = "%$searchTerm%" - return VeranstaltungTable.select { VeranstaltungTable.name like searchPattern } + return VeranstaltungTable.selectAll().where { VeranstaltungTable.name like searchPattern } .orderBy(VeranstaltungTable.startDatum, SortOrder.DESC) .limit(limit) .map { rowToVeranstaltung(it) } } override suspend fun findByVeranstalterVereinId(vereinId: Uuid, activeOnly: Boolean): List { - val query = VeranstaltungTable.select { VeranstaltungTable.veranstalterVereinId eq vereinId } + val query = VeranstaltungTable.selectAll().where { VeranstaltungTable.veranstalterVereinId eq vereinId } return if (activeOnly) { query.andWhere { VeranstaltungTable.istAktiv eq true } @@ -47,9 +45,9 @@ class VeranstaltungRepositoryImpl : VeranstaltungRepository { } override suspend fun findByDateRange(startDate: LocalDate, endDate: LocalDate, activeOnly: Boolean): List { - val query = VeranstaltungTable.select { + val query = VeranstaltungTable.selectAll().where { (VeranstaltungTable.startDatum greaterEq startDate) and - (VeranstaltungTable.endDatum lessEq endDate) + (VeranstaltungTable.endDatum lessEq endDate) } return if (activeOnly) { @@ -61,7 +59,7 @@ class VeranstaltungRepositoryImpl : VeranstaltungRepository { } override suspend fun findByStartDate(date: LocalDate, activeOnly: Boolean): List { - val query = VeranstaltungTable.select { VeranstaltungTable.startDatum eq date } + val query = VeranstaltungTable.selectAll().where { VeranstaltungTable.startDatum eq date } return if (activeOnly) { query.andWhere { VeranstaltungTable.istAktiv eq true } @@ -72,14 +70,14 @@ class VeranstaltungRepositoryImpl : VeranstaltungRepository { } override suspend fun findAllActive(limit: Int, offset: Int): List { - return VeranstaltungTable.select { VeranstaltungTable.istAktiv eq true } + return VeranstaltungTable.selectAll().where { VeranstaltungTable.istAktiv eq true } .orderBy(VeranstaltungTable.startDatum, SortOrder.DESC) .limit(limit, offset.toLong()) .map { rowToVeranstaltung(it) } } override suspend fun findPublicEvents(activeOnly: Boolean): List { - val query = VeranstaltungTable.select { VeranstaltungTable.istOeffentlich eq true } + val query = VeranstaltungTable.selectAll().where { VeranstaltungTable.istOeffentlich eq true } return if (activeOnly) { query.andWhere { VeranstaltungTable.istAktiv eq true } @@ -94,7 +92,9 @@ class VeranstaltungRepositoryImpl : VeranstaltungRepository { val updatedVeranstaltung = veranstaltung.copy(updatedAt = now) // Check if record exists - val existingRecord = VeranstaltungTable.select { VeranstaltungTable.id eq veranstaltung.veranstaltungId }.singleOrNull() + val existingRecord = VeranstaltungTable.selectAll() + .where { VeranstaltungTable.id eq veranstaltung.veranstaltungId } + .singleOrNull() return if (existingRecord != null) { // Update existing record @@ -118,12 +118,12 @@ class VeranstaltungRepositoryImpl : VeranstaltungRepository { } override suspend fun countActive(): Long { - return VeranstaltungTable.select { VeranstaltungTable.istAktiv eq true } + return VeranstaltungTable.selectAll().where { VeranstaltungTable.istAktiv eq true } .count() } override suspend fun countByVeranstalterVereinId(vereinId: Uuid, activeOnly: Boolean): Long { - val query = VeranstaltungTable.select { VeranstaltungTable.veranstalterVereinId eq vereinId } + val query = VeranstaltungTable.selectAll().where { VeranstaltungTable.veranstalterVereinId eq vereinId } return if (activeOnly) { query.andWhere { VeranstaltungTable.istAktiv eq true } diff --git a/horse-registry/src/jvmMain/kotlin/at/mocode/horses/infrastructure/repository/HorseRepositoryImpl.kt b/horse-registry/src/jvmMain/kotlin/at/mocode/horses/infrastructure/repository/HorseRepositoryImpl.kt index 1282f8e2..0f89cdd4 100644 --- a/horse-registry/src/jvmMain/kotlin/at/mocode/horses/infrastructure/repository/HorseRepositoryImpl.kt +++ b/horse-registry/src/jvmMain/kotlin/at/mocode/horses/infrastructure/repository/HorseRepositoryImpl.kt @@ -41,7 +41,7 @@ class HorseRepositoryImpl : HorseRepository { } override suspend fun findByOepsNummer(oepsNummer: String): DomPferd? { - return HorseTable.select { HorseTable.oepsNummer eq oepsNummer } + return HorseTable.selectAll().where { HorseTable.oepsNummer eq oepsNummer } .map { rowToDomPferd(it) } .singleOrNull() } @@ -224,17 +224,17 @@ class HorseRepositoryImpl : HorseRepository { } override suspend fun existsByFeiNummer(feiNummer: String): Boolean { - return HorseTable.select { HorseTable.feiNummer eq feiNummer } + return HorseTable.selectAll().where { HorseTable.feiNummer eq feiNummer } .count() > 0 } override suspend fun countActive(): Long { - return HorseTable.select { HorseTable.istAktiv eq true } + return HorseTable.selectAll().where { HorseTable.istAktiv eq true } .count() } override suspend fun countByOwnerId(ownerId: Uuid, activeOnly: Boolean): Long { - val query = HorseTable.select { HorseTable.besitzerId eq ownerId } + val query = HorseTable.selectAll().where { HorseTable.besitzerId eq ownerId } return if (activeOnly) { query.andWhere { HorseTable.istAktiv eq true } diff --git a/master-data/src/jvmMain/kotlin/at/mocode/masterdata/infrastructure/repository/LandRepositoryImpl.kt b/master-data/src/jvmMain/kotlin/at/mocode/masterdata/infrastructure/repository/LandRepositoryImpl.kt index 6c294f48..020b8b27 100644 --- a/master-data/src/jvmMain/kotlin/at/mocode/masterdata/infrastructure/repository/LandRepositoryImpl.kt +++ b/master-data/src/jvmMain/kotlin/at/mocode/masterdata/infrastructure/repository/LandRepositoryImpl.kt @@ -6,8 +6,6 @@ import com.benasher44.uuid.Uuid import kotlinx.datetime.Clock import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq -import org.jetbrains.exposed.sql.SqlExpressionBuilder.like -import org.jetbrains.exposed.sql.SortOrder /** * PostgreSQL implementation of LandRepository using Exposed ORM. @@ -18,29 +16,29 @@ import org.jetbrains.exposed.sql.SortOrder class LandRepositoryImpl : LandRepository { override suspend fun findById(id: Uuid): LandDefinition? { - return LandTable.select { LandTable.id eq id } + return LandTable.selectAll().where { LandTable.id eq id } .singleOrNull() ?.toLandDefinition() } override suspend fun findByIsoAlpha2Code(isoAlpha2Code: String): LandDefinition? { - return LandTable.select { LandTable.isoAlpha2Code eq isoAlpha2Code } + return LandTable.selectAll().where { LandTable.isoAlpha2Code eq isoAlpha2Code } .singleOrNull() ?.toLandDefinition() } override suspend fun findByIsoAlpha3Code(isoAlpha3Code: String): LandDefinition? { - return LandTable.select { LandTable.isoAlpha3Code eq isoAlpha3Code } + return LandTable.selectAll().where { LandTable.isoAlpha3Code eq isoAlpha3Code } .singleOrNull() ?.toLandDefinition() } override suspend fun findByName(searchTerm: String, limit: Int): List { val searchPattern = "%$searchTerm%" - return LandTable.select { + return LandTable.selectAll().where { (LandTable.nameGerman like searchPattern) or - (LandTable.nameEnglish like searchPattern) or - (LandTable.nameLocal like searchPattern) + (LandTable.nameEnglish like searchPattern) or + (LandTable.nameLocal like searchPattern) } .orderBy(LandTable.sortierReihenfolge) .limit(limit) @@ -48,7 +46,7 @@ class LandRepositoryImpl : LandRepository { } override suspend fun findAllActive(orderBySortierung: Boolean): List { - val query = LandTable.select { LandTable.isActive eq true } + val query = LandTable.selectAll().where { LandTable.isActive eq true } return if (orderBySortierung) { query.orderBy(LandTable.sortierReihenfolge to SortOrder.ASC, LandTable.nameGerman to SortOrder.ASC) @@ -58,17 +56,13 @@ class LandRepositoryImpl : LandRepository { } override suspend fun findEuMembers(): List { - return LandTable.select { - (LandTable.isActive eq true) and (LandTable.isEuMember eq true) - } + return LandTable.selectAll().where { (LandTable.isActive eq true) and (LandTable.isEuMember eq true) } .orderBy(LandTable.sortierReihenfolge to SortOrder.ASC, LandTable.nameGerman to SortOrder.ASC) .map { it.toLandDefinition() } } override suspend fun findEwrMembers(): List { - return LandTable.select { - (LandTable.isActive eq true) and (LandTable.isEwrMember eq true) - } + return LandTable.selectAll().where { (LandTable.isActive eq true) and (LandTable.isEwrMember eq true) } .orderBy(LandTable.sortierReihenfolge to SortOrder.ASC, LandTable.nameGerman to SortOrder.ASC) .map { it.toLandDefinition() } } @@ -77,7 +71,7 @@ class LandRepositoryImpl : LandRepository { val now = Clock.System.now() // Check if record exists - val existingRecord = LandTable.select { LandTable.id eq land.landId }.singleOrNull() + val existingRecord = LandTable.selectAll().where { LandTable.id eq land.landId }.singleOrNull() return if (existingRecord != null) { // Update existing record @@ -126,17 +120,17 @@ class LandRepositoryImpl : LandRepository { } override suspend fun existsByIsoAlpha2Code(isoAlpha2Code: String): Boolean { - return LandTable.select { LandTable.isoAlpha2Code eq isoAlpha2Code } + return LandTable.selectAll().where { LandTable.isoAlpha2Code eq isoAlpha2Code } .count() > 0 } override suspend fun existsByIsoAlpha3Code(isoAlpha3Code: String): Boolean { - return LandTable.select { LandTable.isoAlpha3Code eq isoAlpha3Code } + return LandTable.selectAll().where { LandTable.isoAlpha3Code eq isoAlpha3Code } .count() > 0 } override suspend fun countActive(): Long { - return LandTable.select { LandTable.isActive eq true }.count() + return LandTable.selectAll().where { LandTable.isActive eq true }.count() } /** diff --git a/member-management/src/commonMain/kotlin/at/mocode/members/domain/service/UserAuthorizationService.kt b/member-management/src/commonMain/kotlin/at/mocode/members/domain/service/UserAuthorizationService.kt index a351d608..965c9b7c 100644 --- a/member-management/src/commonMain/kotlin/at/mocode/members/domain/service/UserAuthorizationService.kt +++ b/member-management/src/commonMain/kotlin/at/mocode/members/domain/service/UserAuthorizationService.kt @@ -129,7 +129,7 @@ class UserAuthorizationService( for (roleType in roles) { // Find the role by type val rolle = rolleRepository.findByTyp(roleType) - if (rolle != null && rolle.rolleId != null) { + if (rolle != null) { // Get role permissions val rolleBerechtigungen = rolleBerechtigungRepository.findByRolleId(rolle.rolleId) .filter { it.istAktiv } diff --git a/member-management/src/jvmMain/kotlin/at/mocode/members/infrastructure/repository/BerechtigungRepositoryImpl.kt b/member-management/src/jvmMain/kotlin/at/mocode/members/infrastructure/repository/BerechtigungRepositoryImpl.kt index b2ea6179..42ed2966 100644 --- a/member-management/src/jvmMain/kotlin/at/mocode/members/infrastructure/repository/BerechtigungRepositoryImpl.kt +++ b/member-management/src/jvmMain/kotlin/at/mocode/members/infrastructure/repository/BerechtigungRepositoryImpl.kt @@ -1,20 +1,16 @@ package at.mocode.members.infrastructure.repository +// Import table definition and extension functions import at.mocode.enums.BerechtigungE import at.mocode.members.domain.model.DomBerechtigung import at.mocode.members.domain.repository.BerechtigungRepository import com.benasher44.uuid.Uuid import kotlinx.datetime.Clock -import kotlinx.datetime.toKotlinInstant -import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq -import org.jetbrains.exposed.sql.SqlExpressionBuilder.like - -// Import table definition and extension functions -import at.mocode.members.infrastructure.repository.BerechtigungTable -import at.mocode.members.infrastructure.repository.insertOrUpdate -import at.mocode.members.infrastructure.repository.toLocalDateTime -import at.mocode.members.infrastructure.repository.toInstant +import org.jetbrains.exposed.sql.deleteWhere +import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.update /** * Exposed-based implementation of BerechtigungRepository. @@ -45,35 +41,35 @@ class BerechtigungRepositoryImpl : BerechtigungRepository { } override suspend fun findById(berechtigungId: Uuid): DomBerechtigung? { - return BerechtigungTable.select { BerechtigungTable.id eq berechtigungId } + return BerechtigungTable.selectAll().where { BerechtigungTable.id eq berechtigungId } .map { rowToDomBerechtigung(it) } .singleOrNull() } override suspend fun findByTyp(berechtigungTyp: BerechtigungE): DomBerechtigung? { - return BerechtigungTable.select { BerechtigungTable.berechtigungTyp eq berechtigungTyp } + return BerechtigungTable.selectAll().where { BerechtigungTable.berechtigungTyp eq berechtigungTyp } .map { rowToDomBerechtigung(it) } .singleOrNull() } override suspend fun findByName(name: String): List { val searchPattern = "%$name%" - return BerechtigungTable.select { BerechtigungTable.name like searchPattern } + return BerechtigungTable.selectAll().where { BerechtigungTable.name like searchPattern } .map { rowToDomBerechtigung(it) } } override suspend fun findByRessource(ressource: String): List { - return BerechtigungTable.select { BerechtigungTable.ressource eq ressource } + return BerechtigungTable.selectAll().where { BerechtigungTable.ressource eq ressource } .map { rowToDomBerechtigung(it) } } override suspend fun findByAktion(aktion: String): List { - return BerechtigungTable.select { BerechtigungTable.aktion eq aktion } + return BerechtigungTable.selectAll().where { BerechtigungTable.aktion eq aktion } .map { rowToDomBerechtigung(it) } } override suspend fun findAllActive(): List { - return BerechtigungTable.select { BerechtigungTable.istAktiv eq true } + return BerechtigungTable.selectAll().where { BerechtigungTable.istAktiv eq true } .map { rowToDomBerechtigung(it) } } @@ -103,7 +99,7 @@ class BerechtigungRepositoryImpl : BerechtigungRepository { } override suspend fun existsByTyp(berechtigungTyp: BerechtigungE): Boolean { - return BerechtigungTable.select { BerechtigungTable.berechtigungTyp eq berechtigungTyp } + return BerechtigungTable.selectAll().where { BerechtigungTable.berechtigungTyp eq berechtigungTyp } .count() > 0 } diff --git a/member-management/src/jvmMain/kotlin/at/mocode/members/infrastructure/repository/PersonRepositoryImpl.kt b/member-management/src/jvmMain/kotlin/at/mocode/members/infrastructure/repository/PersonRepositoryImpl.kt index a9d80a8b..b107ec83 100644 --- a/member-management/src/jvmMain/kotlin/at/mocode/members/infrastructure/repository/PersonRepositoryImpl.kt +++ b/member-management/src/jvmMain/kotlin/at/mocode/members/infrastructure/repository/PersonRepositoryImpl.kt @@ -1,20 +1,15 @@ package at.mocode.members.infrastructure.repository +// Import table definition and extension functions import at.mocode.members.domain.model.DomPerson import at.mocode.members.domain.repository.PersonRepository import com.benasher44.uuid.Uuid import kotlinx.datetime.Clock -import kotlinx.datetime.toKotlinInstant -import kotlinx.datetime.toKotlinLocalDate -import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq -import org.jetbrains.exposed.sql.SqlExpressionBuilder.like - -// Import table definition and extension functions -import at.mocode.members.infrastructure.repository.PersonTable -import at.mocode.members.infrastructure.repository.insertOrUpdate -import at.mocode.members.infrastructure.repository.toLocalDateTime -import at.mocode.members.infrastructure.repository.toInstant +import org.jetbrains.exposed.sql.deleteWhere +import org.jetbrains.exposed.sql.or +import org.jetbrains.exposed.sql.selectAll /** * Exposed-based implementation of PersonRepository. @@ -25,34 +20,34 @@ import at.mocode.members.infrastructure.repository.toInstant class PersonRepositoryImpl : PersonRepository { override suspend fun findById(id: Uuid): DomPerson? { - return PersonTable.select { PersonTable.id eq id } + return PersonTable.selectAll().where { PersonTable.id eq id } .map { rowToDomPerson(it) } .singleOrNull() } override suspend fun findByOepsSatzNr(oepsSatzNr: String): DomPerson? { - return PersonTable.select { PersonTable.oepsSatzNr eq oepsSatzNr } + return PersonTable.selectAll().where { PersonTable.oepsSatzNr eq oepsSatzNr } .map { rowToDomPerson(it) } .singleOrNull() } override suspend fun findByStammVereinId(vereinId: Uuid): List { - return PersonTable.select { PersonTable.stammVereinId eq vereinId } + return PersonTable.selectAll().where { PersonTable.stammVereinId eq vereinId } .map { rowToDomPerson(it) } } override suspend fun findByName(searchTerm: String, limit: Int): List { val searchPattern = "%$searchTerm%" - return PersonTable.select { + return PersonTable.selectAll().where { (PersonTable.nachname like searchPattern) or - (PersonTable.vorname like searchPattern) + (PersonTable.vorname like searchPattern) } .limit(limit) .map { rowToDomPerson(it) } } override suspend fun findAllActive(limit: Int, offset: Int): List { - return PersonTable.select { PersonTable.istAktiv eq true } + return PersonTable.selectAll().where { PersonTable.istAktiv eq true } .limit(limit, offset.toLong()) .map { rowToDomPerson(it) } } @@ -100,12 +95,12 @@ class PersonRepositoryImpl : PersonRepository { } override suspend fun existsByOepsSatzNr(oepsSatzNr: String): Boolean { - return PersonTable.select { PersonTable.oepsSatzNr eq oepsSatzNr } + return PersonTable.selectAll().where { PersonTable.oepsSatzNr eq oepsSatzNr } .count() > 0 } override suspend fun countActive(): Long { - return PersonTable.select { PersonTable.istAktiv eq true } + return PersonTable.selectAll().where { PersonTable.istAktiv eq true } .count() } diff --git a/member-management/src/jvmMain/kotlin/at/mocode/members/infrastructure/repository/VereinRepositoryImpl.kt b/member-management/src/jvmMain/kotlin/at/mocode/members/infrastructure/repository/VereinRepositoryImpl.kt index 7b3a6532..fdb6d9b2 100644 --- a/member-management/src/jvmMain/kotlin/at/mocode/members/infrastructure/repository/VereinRepositoryImpl.kt +++ b/member-management/src/jvmMain/kotlin/at/mocode/members/infrastructure/repository/VereinRepositoryImpl.kt @@ -1,19 +1,12 @@ package at.mocode.members.infrastructure.repository +// Import table definition and extension functions import at.mocode.members.domain.model.DomVerein import at.mocode.members.domain.repository.VereinRepository import com.benasher44.uuid.Uuid import kotlinx.datetime.Clock -import kotlinx.datetime.toKotlinInstant import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq -import org.jetbrains.exposed.sql.SqlExpressionBuilder.like - -// Import table definition and extension functions -import at.mocode.members.infrastructure.repository.VereinTable -import at.mocode.members.infrastructure.repository.insertOrUpdate -import at.mocode.members.infrastructure.repository.toLocalDateTime -import at.mocode.members.infrastructure.repository.toInstant /** * Exposed-based implementation of VereinRepository. @@ -24,48 +17,48 @@ import at.mocode.members.infrastructure.repository.toInstant class VereinRepositoryImpl : VereinRepository { override suspend fun findById(id: Uuid): DomVerein? { - return VereinTable.select { VereinTable.id eq id } + return VereinTable.selectAll().where { VereinTable.id eq id } .map { rowToDomVerein(it) } .singleOrNull() } override suspend fun findByOepsVereinsNr(oepsVereinsNr: String): DomVerein? { - return VereinTable.select { VereinTable.oepsVereinsNr eq oepsVereinsNr } + return VereinTable.selectAll().where { VereinTable.oepsVereinsNr eq oepsVereinsNr } .map { rowToDomVerein(it) } .singleOrNull() } override suspend fun findByName(searchTerm: String, limit: Int): List { val searchPattern = "%$searchTerm%" - return VereinTable.select { + return VereinTable.selectAll().where { (VereinTable.name like searchPattern) or - (VereinTable.kuerzel like searchPattern) + (VereinTable.kuerzel like searchPattern) } .limit(limit) .map { rowToDomVerein(it) } } override suspend fun findByBundeslandId(bundeslandId: Uuid): List { - return VereinTable.select { VereinTable.bundeslandId eq bundeslandId } + return VereinTable.selectAll().where { VereinTable.bundeslandId eq bundeslandId } .map { rowToDomVerein(it) } } override suspend fun findByLandId(landId: Uuid): List { - return VereinTable.select { VereinTable.landId eq landId } + return VereinTable.selectAll().where { VereinTable.landId eq landId } .map { rowToDomVerein(it) } } override suspend fun findAllActive(limit: Int, offset: Int): List { - return VereinTable.select { VereinTable.istAktiv eq true } + return VereinTable.selectAll().where { VereinTable.istAktiv eq true } .limit(limit, offset.toLong()) .map { rowToDomVerein(it) } } override suspend fun findByLocation(searchTerm: String, limit: Int): List { val searchPattern = "%$searchTerm%" - return VereinTable.select { + return VereinTable.selectAll().where { (VereinTable.ort like searchPattern) or - (VereinTable.plz like searchPattern) + (VereinTable.plz like searchPattern) } .limit(limit) .map { rowToDomVerein(it) } @@ -104,19 +97,18 @@ class VereinRepositoryImpl : VereinRepository { } override suspend fun existsByOepsVereinsNr(oepsVereinsNr: String): Boolean { - return VereinTable.select { VereinTable.oepsVereinsNr eq oepsVereinsNr } + return VereinTable.selectAll().where { VereinTable.oepsVereinsNr eq oepsVereinsNr } .count() > 0 } override suspend fun countActive(): Long { - return VereinTable.select { VereinTable.istAktiv eq true } + return VereinTable.selectAll().where { VereinTable.istAktiv eq true } .count() } override suspend fun countActiveByBundeslandId(bundeslandId: Uuid): Long { - return VereinTable.select { - (VereinTable.istAktiv eq true) and (VereinTable.bundeslandId eq bundeslandId) - } + return VereinTable.selectAll() + .where { (VereinTable.istAktiv eq true) and (VereinTable.bundeslandId eq bundeslandId) } .count() } diff --git a/shared-kernel/src/jvmMain/kotlin/at/mocode/shared/config/AppConfig.kt b/shared-kernel/src/jvmMain/kotlin/at/mocode/shared/config/AppConfig.kt new file mode 100644 index 00000000..7dec9613 --- /dev/null +++ b/shared-kernel/src/jvmMain/kotlin/at/mocode/shared/config/AppConfig.kt @@ -0,0 +1,187 @@ +package at.mocode.shared.config + +import at.mocode.shared.database.DatabaseConfig +import java.io.File +import java.util.Properties + +/** + * Zentrale Konfigurationsverwaltung für die Anwendung. + * Lädt Konfigurationen aus verschiedenen Quellen (Umgebungsvariablen, Property-Dateien). + */ +object AppConfig { + // Aktuelle Umgebung + val environment: AppEnvironment = AppEnvironment.current() + + // Anwendungs-Informationen + val appInfo = AppInfoConfig() + + // Server-Konfiguration + val server = ServerConfig() + + // Sicherheits-Konfiguration + val security = SecurityConfig() + + // Logging-Konfiguration + val logging = LoggingConfig() + + // Datenbank-Konfiguration (wird nach dem Laden der Properties initialisiert) + val database: DatabaseConfig + + init { + // Lade Umgebungsspezifische Properties + val props = loadProperties() + + // Konfiguriere Komponenten mit Properties + appInfo.configure(props) + server.configure(props) + security.configure(props) + logging.configure(props) + + // Datenbank-Konfiguration mit Properties initialisieren + database = DatabaseConfig.fromEnv(props) + + // Log Konfigurationsinformationen + if (!AppEnvironment.isProduction()) { + println("=== Anwendungskonfiguration ===") + println("Umgebung: $environment") + println("App: ${appInfo.name} v${appInfo.version}") + println("Server: Port ${server.port}, ${server.workers} Worker") + println("Datenbank: ${database.jdbcUrl}") + println("===============================\n") + } + } + + /** + * Lädt die Properties für die aktuelle Umgebung. + */ + private fun loadProperties(): Properties { + val props = Properties() + + // Lade Basis-Properties + loadPropertiesFile("application.properties", props) + + // Lade umgebungsspezifische Properties + val envFile = when (environment) { + AppEnvironment.DEVELOPMENT -> "application-dev.properties" + AppEnvironment.TEST -> "application-test.properties" + AppEnvironment.STAGING -> "application-staging.properties" + AppEnvironment.PRODUCTION -> "application-prod.properties" + } + + loadPropertiesFile(envFile, props) + + return props + } + + /** + * Lädt eine Property-Datei, wenn sie existiert. + */ + private fun loadPropertiesFile(filename: String, props: Properties) { + val resourceStream = javaClass.classLoader.getResourceAsStream(filename) + if (resourceStream != null) { + props.load(resourceStream) + resourceStream.close() + } else { + // Versuche aus dem Dateisystem zu laden + val file = File("config/$filename") + if (file.exists()) { + file.inputStream().use { props.load(it) } + } + } + } + + /** + * Gibt den Wert einer Property zurück, wobei die Priorität wie folgt ist: + * 1. Umgebungsvariable + * 2. Property aus Datei + * 3. Standardwert + */ + fun getProperty(key: String, defaultValue: String? = null): String? { + val envKey = key.replace('.', '_').uppercase() + return System.getenv(envKey) ?: defaultValue + } +} + +/** + * Konfiguration für Anwendungsinformationen. + */ +class AppInfoConfig { + var name: String = "Meldestelle" + var version: String = "1.0.0" + var description: String = "Pferdesport Meldestelle System" + + fun configure(props: Properties) { + name = props.getProperty("app.name", name) + version = props.getProperty("app.version", version) + description = props.getProperty("app.description", description) + } +} + +/** + * Konfiguration für den Server. + */ +class ServerConfig { + var port: Int = System.getenv("API_PORT")?.toIntOrNull() ?: 8081 + var host: String = System.getenv("API_HOST") ?: "0.0.0.0" + var workers: Int = Runtime.getRuntime().availableProcessors() + var cors: CorsConfig = CorsConfig() + + fun configure(props: Properties) { + port = props.getProperty("server.port")?.toIntOrNull() ?: port + host = props.getProperty("server.host") ?: host + workers = props.getProperty("server.workers")?.toIntOrNull() ?: workers + + // CORS Konfiguration + cors.enabled = props.getProperty("server.cors.enabled")?.toBoolean() ?: cors.enabled + props.getProperty("server.cors.allowedOrigins")?.split(",")?.map { it.trim() }?.let { + cors.allowedOrigins = it + } + } + + class CorsConfig { + var enabled: Boolean = true + var allowedOrigins: List = listOf("*") + } +} + +/** + * Konfiguration für die Sicherheit. + */ +class SecurityConfig { + var jwt = JwtConfig() + + fun configure(props: Properties) { + // JWT Konfiguration + jwt.secret = System.getenv("JWT_SECRET") ?: props.getProperty("security.jwt.secret") ?: jwt.secret + jwt.issuer = System.getenv("JWT_ISSUER") ?: props.getProperty("security.jwt.issuer") ?: jwt.issuer + jwt.audience = System.getenv("JWT_AUDIENCE") ?: props.getProperty("security.jwt.audience") ?: jwt.audience + jwt.realm = System.getenv("JWT_REALM") ?: props.getProperty("security.jwt.realm") ?: jwt.realm + + props.getProperty("security.jwt.expirationInMinutes")?.toLongOrNull()?.let { + jwt.expirationInMinutes = it + } + } + + class JwtConfig { + var secret: String = "default-jwt-secret-key-please-change-in-production" + var issuer: String = "meldestelle-api" + var audience: String = "meldestelle-clients" + var realm: String = "meldestelle" + var expirationInMinutes: Long = 60 * 24 // 24 Stunden + } +} + +/** + * Konfiguration für das Logging. + */ +class LoggingConfig { + var level: String = if (AppEnvironment.isProduction()) "INFO" else "DEBUG" + var logRequests: Boolean = true + var logResponses: Boolean = !AppEnvironment.isProduction() + + fun configure(props: Properties) { + level = props.getProperty("logging.level") ?: level + logRequests = props.getProperty("logging.requests")?.toBoolean() ?: logRequests + logResponses = props.getProperty("logging.responses")?.toBoolean() ?: logResponses + } +} diff --git a/shared-kernel/src/jvmMain/kotlin/at/mocode/shared/config/AppEnvironment.kt b/shared-kernel/src/jvmMain/kotlin/at/mocode/shared/config/AppEnvironment.kt new file mode 100644 index 00000000..6c225ccb --- /dev/null +++ b/shared-kernel/src/jvmMain/kotlin/at/mocode/shared/config/AppEnvironment.kt @@ -0,0 +1,48 @@ +package at.mocode.shared.config + +/** + * Aufzählung der verschiedenen Anwendungsumgebungen. + */ +enum class AppEnvironment { + DEVELOPMENT, // Lokale Entwicklungsumgebung + TEST, // Testumgebung (CI/CD, Integrationstests) + STAGING, // Vorabproduktionsumgebung + PRODUCTION; // Produktionsumgebung + + companion object { + /** + * Ermittelt die aktuelle Umgebung basierend auf der APP_ENV Umgebungsvariable. + * + * @return Die aktuelle Umgebung (Standardmäßig DEVELOPMENT wenn nicht definiert) + */ + fun current(): AppEnvironment { + val envName = System.getenv("APP_ENV")?.uppercase() ?: "DEVELOPMENT" + return try { + valueOf(envName) + } catch (e: IllegalArgumentException) { + println("Warnung: Unbekannte Umgebung '$envName', verwende DEVELOPMENT") + DEVELOPMENT + } + } + + /** + * Prüft, ob die aktuelle Umgebung die Entwicklungsumgebung ist. + */ + fun isDevelopment() = current() == DEVELOPMENT + + /** + * Prüft, ob die aktuelle Umgebung die Testumgebung ist. + */ + fun isTest() = current() == TEST + + /** + * Prüft, ob die aktuelle Umgebung die Staging-Umgebung ist. + */ + fun isStaging() = current() == STAGING + + /** + * Prüft, ob die aktuelle Umgebung die Produktionsumgebung ist. + */ + fun isProduction() = current() == PRODUCTION + } +} diff --git a/shared-kernel/src/jvmMain/kotlin/at/mocode/shared/database/DatabaseConfig.kt b/shared-kernel/src/jvmMain/kotlin/at/mocode/shared/database/DatabaseConfig.kt index 6efc3b92..29f9e926 100644 --- a/shared-kernel/src/jvmMain/kotlin/at/mocode/shared/database/DatabaseConfig.kt +++ b/shared-kernel/src/jvmMain/kotlin/at/mocode/shared/database/DatabaseConfig.kt @@ -1,32 +1,45 @@ package at.mocode.shared.database +import java.util.Properties + /** * Konfiguration für die Datenbankverbindung. - * Parameter werden aus Umgebungsvariablen gelesen oder Standardwerte verwendet. + * Parameter werden aus Umgebungsvariablen oder Property-Dateien gelesen. */ data class DatabaseConfig( val jdbcUrl: String, val username: String, val password: String, val driverClassName: String = "org.postgresql.Driver", - val maxPoolSize: Int = 10 + val maxPoolSize: Int = 10, + val autoMigrate: Boolean = true ) { companion object { /** - * Erstellt eine Datenbank-Konfiguration aus Umgebungsvariablen. + * Erstellt eine Datenbank-Konfiguration aus Umgebungsvariablen und Properties. * Wenn keine Umgebungsvariablen gefunden werden, werden Standardwerte für die Entwicklung verwendet. */ - fun fromEnv(): DatabaseConfig { - val host = System.getenv("DB_HOST") ?: "localhost" - val port = System.getenv("DB_PORT") ?: "5432" - val database = System.getenv("DB_NAME") ?: "meldestelle_db" - val username = System.getenv("DB_USER") ?: "meldestelle_user" - val password = System.getenv("DB_PASSWORD") ?: "secure_password_change_me" + fun fromEnv(props: Properties = Properties()): DatabaseConfig { + // Priorität: Umgebungsvariablen > Properties > Standardwerte + val host = System.getenv("DB_HOST") ?: props.getProperty("database.host") ?: "localhost" + val port = System.getenv("DB_PORT") ?: props.getProperty("database.port") ?: "5432" + val database = System.getenv("DB_NAME") ?: props.getProperty("database.name") ?: "meldestelle_db" + val username = System.getenv("DB_USER") ?: props.getProperty("database.username") ?: "meldestelle_user" + val password = System.getenv("DB_PASSWORD") ?: props.getProperty("database.password") ?: "secure_password_change_me" + val maxPoolSize = System.getenv("DB_MAX_POOL_SIZE")?.toIntOrNull() + ?: props.getProperty("database.maxPoolSize")?.toIntOrNull() + ?: 10 + val autoMigrate = System.getenv("DB_AUTO_MIGRATE")?.toBoolean() + ?: props.getProperty("database.autoMigrate")?.toBoolean() + ?: true return DatabaseConfig( jdbcUrl = "jdbc:postgresql://$host:$port/$database", username = username, - password = password + password = password, + driverClassName = "org.postgresql.Driver", + maxPoolSize = maxPoolSize, + autoMigrate = autoMigrate ) } }