refactor(ping-service): remove CORS configuration from code, restructure modules, and update Gradle dependencies

Migrated CORS settings from code to `application.yaml` for better separation of concerns. Integrated `ping-api` into the new `contracts` module for improved modularity. Updated Gradle scripts and dependencies accordingly to reflect the new project structure.
This commit is contained in:
2026-01-12 13:04:00 +01:00
parent 02317e6c00
commit 1f9697d504
14 changed files with 301 additions and 106 deletions
@@ -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)
}
}
}
}
@@ -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
}
@@ -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
)
@@ -14,7 +14,7 @@ kotlin {
dependencies { dependencies {
// === Project Dependencies === // === Project Dependencies ===
implementation(projects.backend.services.ping.pingApi) implementation(projects.contracts.pingApi)
implementation(projects.platform.platformDependencies) implementation(projects.platform.platformDependencies)
// NEU: Zugriff auf die verschobenen DatabaseUtils // NEU: Zugriff auf die verschobenen DatabaseUtils
implementation(projects.backend.infrastructure.persistence) implementation(projects.backend.infrastructure.persistence)
@@ -2,33 +2,12 @@ package at.mocode.ping
import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.EnableAspectJAutoProxy import org.springframework.context.annotation.EnableAspectJAutoProxy
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
@SpringBootApplication @SpringBootApplication
// Scannt explizit alle Sub-Packages (infrastructure, application, domain) // Scannt explizit alle Sub-Packages (infrastructure, application, domain)
@EnableAspectJAutoProxy @EnableAspectJAutoProxy
class PingServiceApplication { 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)
}
}
}
}
fun main(args: Array<String>) { fun main(args: Array<String>) {
runApplication<PingServiceApplication>(*args) runApplication<PingServiceApplication>(*args)
@@ -42,6 +42,17 @@ spring:
health-check-interval: 10s health-check-interval: 10s
instance-id: ${spring.application.name}-${server.port}-${random.uuid} 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: management:
endpoints: endpoints:
web: web:
+3 -11
View File
@@ -10,7 +10,7 @@ import java.util.zip.GZIPOutputStream
plugins { plugins {
// Version management plugin for dependency updates // 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 // Kotlin plugins declared here with 'apply false' to centralize version management
// This prevents "plugin loaded multiple times" errors in Gradle 9.2.1+ // This prevents "plugin loaded multiple times" errors in Gradle 9.2.1+
@@ -29,8 +29,8 @@ plugins {
alias(libs.plugins.dokka) alias(libs.plugins.dokka)
// Static analysis (enabled at root and inherited by subprojects) // Static analysis (enabled at root and inherited by subprojects)
id("io.gitlab.arturbosch.detekt") version "1.23.6" alias(libs.plugins.detekt)
id("org.jlleitschuh.gradle.ktlint") version "12.1.1" alias(libs.plugins.ktlint)
} }
// ################################################################## // ##################################################################
@@ -80,14 +80,6 @@ subprojects {
// The agent configuration was causing Task.project access at execution time // 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 // Dedicated performance test task per JVM subproject
plugins.withId("java") { plugins.withId("java") {
val javaExt = extensions.getByType<JavaPluginExtension>() val javaExt = extensions.getByType<JavaPluginExtension>()
+39
View File
@@ -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)
}
}
}
}
@@ -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
}
@@ -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
)
+202
View File
@@ -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.
@@ -39,7 +39,7 @@ kotlin {
sourceSets { sourceSets {
commonMain.dependencies { commonMain.dependencies {
// Contract from backend // Contract from backend
implementation(projects.backend.services.ping.pingApi) implementation(projects.contracts.pingApi)
// UI Kit (Design System) // UI Kit (Design System)
implementation(projects.frontend.core.designSystem) implementation(projects.frontend.core.designSystem)
+8 -1
View File
@@ -88,6 +88,10 @@ testcontainersKafka = "1.21.4"
# Gradle Plugins # Gradle Plugins
foojayResolver = "1.0.0" foojayResolver = "1.0.0"
benManesVersions = "0.51.0"
detekt = "1.23.6"
ktlint = "12.1.1"
dokka = "2.1.0"
[libraries] [libraries]
# ============================================================================== # ==============================================================================
@@ -370,4 +374,7 @@ sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
# --- Tools --- # --- Tools ---
foojayResolver = { id = "org.gradle.toolchains.foojay-resolver-convention", version.ref = "foojayResolver" } 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" }
+5 -1
View File
@@ -28,6 +28,11 @@ dependencyResolutionManagement {
} }
} }
// ==========================================================================
// CONTRACTS
// ==========================================================================
include(":contracts:ping-api")
// ========================================================================== // ==========================================================================
// Backend // Backend
// ========================================================================== // ==========================================================================
@@ -89,7 +94,6 @@ include(":backend:services:entries:entries-service")
// include(":backend:services:members:members-service") // include(":backend:services:members:members-service")
// --- PING (Ping Service) --- // --- PING (Ping Service) ---
include(":backend:services:ping:ping-api")
include(":backend:services:ping:ping-service") include(":backend:services:ping:ping-service")
// --- REGISTRY (Single Source of Truth) --- // --- REGISTRY (Single Source of Truth) ---