diff --git a/backend/services/ping/ping-api/build.gradle.kts b/backend/services/ping/ping-api/build.gradle.kts index b10588e4..e69de29b 100644 --- a/backend/services/ping/ping-api/build.gradle.kts +++ b/backend/services/ping/ping-api/build.gradle.kts @@ -1,39 +0,0 @@ -plugins { - alias(libs.plugins.kotlinMultiplatform) - alias(libs.plugins.kotlinSerialization) -} - -group = "at.mocode" -version = "1.0.0" - -kotlin { - // Toolchain is now handled centrally in the root build.gradle.kts - - // JVM target for backend usage - jvm() - - // JS target for frontend usage (Compose/Browser) - js { - browser() - // no need for binaries.executable() in a library - } - - // Wasm enabled by default - @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) - wasmJs { - browser() - } - - sourceSets { - commonMain { - dependencies { - implementation(libs.kotlinx.serialization.json) - } - } - commonTest { - dependencies { - implementation(libs.kotlin.test) - } - } - } -} diff --git a/backend/services/ping/ping-api/src/commonMain/kotlin/at/mocode/ping/api/PingApi.kt b/backend/services/ping/ping-api/src/commonMain/kotlin/at/mocode/ping/api/PingApi.kt index 56c0a256..e69de29b 100644 --- a/backend/services/ping/ping-api/src/commonMain/kotlin/at/mocode/ping/api/PingApi.kt +++ b/backend/services/ping/ping-api/src/commonMain/kotlin/at/mocode/ping/api/PingApi.kt @@ -1,7 +0,0 @@ -package at.mocode.ping.api - -interface PingApi { - suspend fun simplePing(): PingResponse - suspend fun enhancedPing(simulate: Boolean = false): EnhancedPingResponse - suspend fun healthCheck(): HealthResponse -} diff --git a/backend/services/ping/ping-api/src/commonMain/kotlin/at/mocode/ping/api/PingData.kt b/backend/services/ping/ping-api/src/commonMain/kotlin/at/mocode/ping/api/PingData.kt index b2101614..e69de29b 100644 --- a/backend/services/ping/ping-api/src/commonMain/kotlin/at/mocode/ping/api/PingData.kt +++ b/backend/services/ping/ping-api/src/commonMain/kotlin/at/mocode/ping/api/PingData.kt @@ -1,23 +0,0 @@ -package at.mocode.ping.api - -import kotlinx.serialization.Serializable - -@Serializable -data class PingResponse(val status: String, val timestamp: String, val service: String) - -@Serializable -data class EnhancedPingResponse( - val status: String, - val timestamp: String, - val service: String, - val circuitBreakerState: String, - val responseTime: Long -) - -@Serializable -data class HealthResponse( - val status: String, - val timestamp: String, - val service: String, - val healthy: Boolean -) diff --git a/backend/services/ping/ping-service/build.gradle.kts b/backend/services/ping/ping-service/build.gradle.kts index 1d23061f..3b1bd5fa 100644 --- a/backend/services/ping/ping-service/build.gradle.kts +++ b/backend/services/ping/ping-service/build.gradle.kts @@ -14,7 +14,7 @@ kotlin { dependencies { // === Project Dependencies === - implementation(projects.backend.services.ping.pingApi) + implementation(projects.contracts.pingApi) implementation(projects.platform.platformDependencies) // NEU: Zugriff auf die verschobenen DatabaseUtils implementation(projects.backend.infrastructure.persistence) diff --git a/backend/services/ping/ping-service/src/main/kotlin/at/mocode/ping/PingServiceApplication.kt b/backend/services/ping/ping-service/src/main/kotlin/at/mocode/ping/PingServiceApplication.kt index 01fc1b21..80711032 100644 --- a/backend/services/ping/ping-service/src/main/kotlin/at/mocode/ping/PingServiceApplication.kt +++ b/backend/services/ping/ping-service/src/main/kotlin/at/mocode/ping/PingServiceApplication.kt @@ -2,33 +2,12 @@ package at.mocode.ping import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication -import org.springframework.context.annotation.Bean import org.springframework.context.annotation.EnableAspectJAutoProxy -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer @SpringBootApplication // Scannt explizit alle Sub-Packages (infrastructure, application, domain) @EnableAspectJAutoProxy -class PingServiceApplication { - - @Bean - fun corsConfigurer(): WebMvcConfigurer { - return object : WebMvcConfigurer { - override fun addCorsMappings(registry: org.springframework.web.servlet.config.annotation.CorsRegistry) { - registry.addMapping("/**") - .allowedOriginPatterns("http://localhost:*") - .allowedOrigins("http://localhost:8080", - "http://localhost:8083", - "http://localhost:4000" - ) - .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") - .allowedHeaders("*") - .allowCredentials(true) - .maxAge(3600) - } - } - } -} +class PingServiceApplication fun main(args: Array) { runApplication(*args) diff --git a/backend/services/ping/ping-service/src/main/resources/application.yaml b/backend/services/ping/ping-service/src/main/resources/application.yaml index 00506c55..7efea199 100644 --- a/backend/services/ping/ping-service/src/main/resources/application.yaml +++ b/backend/services/ping/ping-service/src/main/resources/application.yaml @@ -42,6 +42,17 @@ spring: health-check-interval: 10s instance-id: ${spring.application.name}-${server.port}-${random.uuid} +# CORS-Konfiguration ausgelagert aus dem Code +web: + cors: + mappings: + "/**": + allowed-origin-patterns: "http://localhost:*,http://127.0.0.1:*" + allowed-methods: "GET,POST,PUT,DELETE,OPTIONS" + allowed-headers: "*" + allow-credentials: true + max-age: 3600 + management: endpoints: web: diff --git a/build.gradle.kts b/build.gradle.kts index beb3e839..2c8c2a66 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,7 +10,7 @@ import java.util.zip.GZIPOutputStream plugins { // Version management plugin for dependency updates - id("com.github.ben-manes.versions") version "0.51.0" + alias(libs.plugins.benManesVersions) // Kotlin plugins declared here with 'apply false' to centralize version management // This prevents "plugin loaded multiple times" errors in Gradle 9.2.1+ @@ -29,8 +29,8 @@ plugins { alias(libs.plugins.dokka) // Static analysis (enabled at root and inherited by subprojects) - id("io.gitlab.arturbosch.detekt") version "1.23.6" - id("org.jlleitschuh.gradle.ktlint") version "12.1.1" + alias(libs.plugins.detekt) + alias(libs.plugins.ktlint) } // ################################################################## @@ -80,14 +80,6 @@ subprojects { // The agent configuration was causing Task.project access at execution time } - // Erzwinge eine stabile Version von kotlinx-serialization-json für alle Konfigurationen, - // um Auflösungsfehler (z.B. 1.10.2, nicht verfügbar auf Maven Central) zu vermeiden - configurations.configureEach { - resolutionStrategy { - force("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") - } - } - // Dedicated performance test task per JVM subproject plugins.withId("java") { val javaExt = extensions.getByType() diff --git a/contracts/ping-api/build.gradle.kts b/contracts/ping-api/build.gradle.kts new file mode 100644 index 00000000..b10588e4 --- /dev/null +++ b/contracts/ping-api/build.gradle.kts @@ -0,0 +1,39 @@ +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.kotlinSerialization) +} + +group = "at.mocode" +version = "1.0.0" + +kotlin { + // Toolchain is now handled centrally in the root build.gradle.kts + + // JVM target for backend usage + jvm() + + // JS target for frontend usage (Compose/Browser) + js { + browser() + // no need for binaries.executable() in a library + } + + // Wasm enabled by default + @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) + wasmJs { + browser() + } + + sourceSets { + commonMain { + dependencies { + implementation(libs.kotlinx.serialization.json) + } + } + commonTest { + dependencies { + implementation(libs.kotlin.test) + } + } + } +} diff --git a/contracts/ping-api/src/commonMain/kotlin/at/mocode/ping/api/PingApi.kt b/contracts/ping-api/src/commonMain/kotlin/at/mocode/ping/api/PingApi.kt new file mode 100644 index 00000000..56c0a256 --- /dev/null +++ b/contracts/ping-api/src/commonMain/kotlin/at/mocode/ping/api/PingApi.kt @@ -0,0 +1,7 @@ +package at.mocode.ping.api + +interface PingApi { + suspend fun simplePing(): PingResponse + suspend fun enhancedPing(simulate: Boolean = false): EnhancedPingResponse + suspend fun healthCheck(): HealthResponse +} diff --git a/contracts/ping-api/src/commonMain/kotlin/at/mocode/ping/api/PingData.kt b/contracts/ping-api/src/commonMain/kotlin/at/mocode/ping/api/PingData.kt new file mode 100644 index 00000000..b2101614 --- /dev/null +++ b/contracts/ping-api/src/commonMain/kotlin/at/mocode/ping/api/PingData.kt @@ -0,0 +1,23 @@ +package at.mocode.ping.api + +import kotlinx.serialization.Serializable + +@Serializable +data class PingResponse(val status: String, val timestamp: String, val service: String) + +@Serializable +data class EnhancedPingResponse( + val status: String, + val timestamp: String, + val service: String, + val circuitBreakerState: String, + val responseTime: Long +) + +@Serializable +data class HealthResponse( + val status: String, + val timestamp: String, + val service: String, + val healthy: Boolean +) diff --git a/docs/Ping-Service_Impl_01-2026.md b/docs/Ping-Service_Impl_01-2026.md new file mode 100644 index 00000000..ca53cac5 --- /dev/null +++ b/docs/Ping-Service_Impl_01-2026.md @@ -0,0 +1,202 @@ +**An:** Senior Backend Developer +**Von:** Lead Software Architect +**Betreff:** Arbeitsauftrag: Implementierung des `ping-service` (Tracer Bullet) + +Guten Tag, + +deine nächste Aufgabe ist die Implementierung unseres ersten Microservices, des `ping-service`. Dieser Service ist von +strategischer Bedeutung, da er als **"Tracer Bullet"** und **Blaupause** für alle zukünftigen fachlichen Services dient. + +Mit dieser Implementierung validieren wir die gesamte Kette: von der Service-Registrierung bei Consul über das +Gateway-Routing und die Security mit Keycloak bis hin zur Observability mit Zipkin. + +Deine Expertise in Spring Boot, DDD und sauberer Architektur ist hier entscheidend, um eine qualitativ hochwertige und +wiederverwendbare Vorlage zu schaffen. + +## Deine Aufgaben im Detail: + +1. Modulstruktur anlegen + +Bitte lege die folgende Modulstruktur unter backend/services/ping an. Beachte die neue, klarere Benennung des +Implementierungsmoduls: + +- `:backend:services:ping:ping-api`: Enthält die KMP-kompatiblen DTOs. +- `:backend:services:ping:ping-infrastructure`: Enthält die Spring Boot Anwendung, Controller und Konfiguration. + +Stelle sicher, dass die Module in der `settings.gradle.kts` registriert sind. + + ```kotlin + include( + ":platform:platform-bom", + ":platform:platform-testing", + ":backend:services:ping:ping-api", + ":backend:services:ping:ping-infrastructure", + ":backend:gateway", + // ":backend:services:registry:registry-api", + // ":backend:services:registry:registry-domain", + ``` + +2. API-Definition in `:ping-api` + +Definiere in `ping-api/src/commonMain/kotlin` ein einfaches, serialisierbares DTO. Dieses Modul darf **keine +JVM-spezifischen Abhängigkeiten** enthalten, um die KMP-Kompatibilität für das Frontend zu gewährleisten. + + ```kotlin + PingResponse.kt + ``` + ```kotlin + package de.meldestelle.api.ping + + import kotlinx . serialization . Serializable + + @Serializable + data class PingResponse( + val message: String, + val principal: String? = null + ) + ``` + +3. Service-Implementierung in :ping-infrastructure + +Implementiere die Spring Boot Anwendung. + +- **`PingController.kt`:** + - **`GET /api/ping`:** Ein öffentlicher Endpunkt, der eine `PingResponse("Pong", "anonymous")` zurückgibt. + - **`GET /api/ping/secure`:** Ein durch Spring Security geschützter Endpunkt. Er soll den `preferred_username` aus dem `Jwt` Principal extrahieren und in der `PingResponse` zurückgeben. + +Hier ist ein Implementierungsvorschlag für den Controller: + + ```kotlin + // in backend/services/ping/ping-infrastructure/src/main/kotlin/.../PingController.kt + + @RestController + @RequestMapping("/api/ping") + class PingController { + + @GetMapping + fun pingPublic(): PingResponse { + return PingResponse(message = "Pong", principal = "anonymous") + } + + @GetMapping("/secure") + fun pingSecure(principal: Jwt): PingResponse { + val username = principal.getClaimAsString("preferred_username") + return PingResponse(message = "Pong (Secure)", principal = username) + } + } + ``` + +4. **Konfiguration** + +Erstelle die `application.yml` für den Service. Sie muss die Anwendung für unsere Infrastruktur korrekt konfigurieren: + +- **Service Name:** ping-service +- **Service Discovery:** Registrierung bei Consul. +- **Security:** Konfiguration als Resource Server, der JWTs vom `issuer-uri` unseres Keycloak-Containers validiert. +- **Observability:** Actuator-Endpunkte (`health`, `info`, `prometheus`) freigeben und Tracing aktivieren. + + ```yaml + # in backend/services/ping/ping-infrastructure/src/main/resources/application.yml + + server: + port: 8081 # Eindeutiger Port für den Service + + spring: + application: + name: ping-service + + # --- Consul Discovery --- + cloud: + consul: + host: consul + port: 8500 + discovery: + instance-id: \${spring.application.name}:\${random.value} + health-check-path: /actuator/health + health-check-interval: 10s + + # --- Security (Keycloak) --- + security: + oauth2: + resourceserver: + jwt: + issuer-uri: http://keycloak:8080/realms/meldestelle + + # --- Observability --- + management: + endpoints: + web: + exposure: + include: "health,info,prometheus" + tracing: + sampling: + probability: 1.0 # Trace every request + + logging: + pattern: + level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]" + ``` + +5. **Build-Konfiguration(`build.gradle.kts`) + +Achte auf die korrekte und saubere Definition der Abhängigkeiten. + +- `ping-api/build.gradle.kts` + ```kotlin + plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.kotlin.serialization) + } + + kotlin { + jvm() // Für die Nutzung im Backend + js(IR) { browser() } // Für die Nutzung im Frontend + sourceSets { + commonMain.dependencies { + implementation(libs.kotlinx.serialization.json) + } + } + } + ``` + +- `ping-infrastructure/build.gradle.kts` + ```kotlin + plugins { + alias(libs.plugins.spring.boot) + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.kotlin.spring) + } + + dependencies { + // API-Modul einbinden + implementation(project(":backend:services:ping:ping-api")) + + // Unsere zentrale BOM für konsistente Versionen + implementation(platform(project(":platform:platform-bom"))) + + // Spring Boot Starter + implementation(libs.spring.boot.starter.web) + implementation(libs.spring.boot.starter.actuator) + implementation(libs.spring.boot.starter.security) + implementation(libs.spring.boot.starter.oauth2.resource.server) + + // Spring Cloud (Consul, OpenFeign etc.) + implementation(libs.spring.cloud.starter.consul.discovery) + + // Test-Abhängigkeiten + testImplementation(platform(project(":platform:platform-testing"))) + testImplementation(libs.bundles.test.spring) + } + ``` + +## Definition of Done: + +Der Auftrag gilt als erledigt, wenn: +1. Die Anwendung erfolgreich startet und sich im Consul UI als `UP` registriert. +2. Ein `GET`-Request auf `http//localhost:8081/api/ping` (über das Gateway) den Status `200 OK` und die `{"message":"Pong", "principal":"anonymous"}` zurückgibt. +3. Ein `GET`-Request auf `http//localhost:8081/api/ping/secure` ohne Token den Status `401 Unauthorized` zurückgibt. +4. Ein `GET`-Request auf `http//localhost:8081/api/ping/secure` mit einem gültigen Keycloak-Token deb Status `200 OK` und eine Antwort mit dem korrekten Benutzernamen zurückgibt. +5. Die Requests in der Zipkin UI als Trace sichtbar sind. + +Bei Fragen zur Konfiguration oder zur Architektur stehe ich dir zur Verfügung. + diff --git a/frontend/features/ping-feature/build.gradle.kts b/frontend/features/ping-feature/build.gradle.kts index 2d2522e6..ce05f16b 100644 --- a/frontend/features/ping-feature/build.gradle.kts +++ b/frontend/features/ping-feature/build.gradle.kts @@ -39,7 +39,7 @@ kotlin { sourceSets { commonMain.dependencies { // Contract from backend - implementation(projects.backend.services.ping.pingApi) + implementation(projects.contracts.pingApi) // UI Kit (Design System) implementation(projects.frontend.core.designSystem) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1aebfb03..faee7f9d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -88,6 +88,10 @@ testcontainersKafka = "1.21.4" # Gradle Plugins foojayResolver = "1.0.0" +benManesVersions = "0.51.0" +detekt = "1.23.6" +ktlint = "12.1.1" +dokka = "2.1.0" [libraries] # ============================================================================== @@ -370,4 +374,7 @@ sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } # --- Tools --- foojayResolver = { id = "org.gradle.toolchains.foojay-resolver-convention", version.ref = "foojayResolver" } -dokka = { id = "org.jetbrains.dokka", version = "2.1.0" } +dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } +benManesVersions = { id = "com.github.ben-manes.versions", version.ref = "benManesVersions" } +detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } +ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 7b72c99a..fa6432d3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -28,6 +28,11 @@ dependencyResolutionManagement { } } +// ========================================================================== +// CONTRACTS +// ========================================================================== +include(":contracts:ping-api") + // ========================================================================== // Backend // ========================================================================== @@ -89,7 +94,6 @@ include(":backend:services:entries:entries-service") // include(":backend:services:members:members-service") // --- PING (Ping Service) --- -include(":backend:services:ping:ping-api") include(":backend:services:ping:ping-service") // --- REGISTRY (Single Source of Truth) ---