chore(ping-service, security): integrate centralized security module and enhance Ping-Service
- Replaced local `SecurityConfig` in `ping-service` with the shared `infrastructure:security` module. - Added `GlobalSecurityConfig` to standardize OAuth2, JWT validation, and CORS for all services. - Introduced new endpoints (`/ping/public`, `/ping/secure`) with role-based access control. - Updated database schema with Flyway migration (`V1__init_ping.sql`) and refactored persistence layer to align with the standardized approach (`createdAt` field). - Enhanced application configuration (`application.yaml`) to use shared security and Flyway settings.
This commit is contained in:
parent
9456f28562
commit
05962487e7
|
|
@ -25,7 +25,12 @@ spring:
|
|||
response-timeout: 5s
|
||||
routes:
|
||||
- id: ping-service
|
||||
uri: http://ping-service:8080
|
||||
# Nutze lb:// wenn Service Discovery aktiv ist, sonst http://hostname:port
|
||||
# Da wir Consul nutzen, ist lb://ping-service besser, aber für Tracer Bullet
|
||||
# und direkte Docker-Kommunikation ist http://ping-service:8082 sicherer,
|
||||
# falls Consul noch nicht 100% stabil ist.
|
||||
# Wir nutzen hier den Docker Alias und den konfigurierten Port.
|
||||
uri: http://ping-service:8082
|
||||
predicates:
|
||||
- Path=/api/ping/**
|
||||
filters:
|
||||
|
|
@ -42,13 +47,12 @@ management:
|
|||
include: health,info,prometheus
|
||||
tracing:
|
||||
sampling:
|
||||
probability: 1.0 # 100% der Requests tracen (für Dev/Test sinnvoll, in Prod reduzieren)
|
||||
probability: 1.0
|
||||
propagation:
|
||||
type: w3c # Standard W3C Trace Context (kompatibel mit OpenTelemetry)
|
||||
type: w3c
|
||||
|
||||
# Gateway-spezifische Einstellungen
|
||||
gateway:
|
||||
ratelimit:
|
||||
enabled: false # Start: ausgeschaltet; zum Aktivieren default-filters plus RequestRateLimiter in YAML hinzufügen
|
||||
enabled: false
|
||||
replenish-rate: 10
|
||||
burst-capacity: 20
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
package at.mocode.infrastructure.security
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||
import org.springframework.security.config.http.SessionCreationPolicy
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
import org.springframework.web.cors.CorsConfiguration
|
||||
import org.springframework.web.cors.CorsConfigurationSource
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableMethodSecurity(prePostEnabled = true) // Erlaubt @PreAuthorize in Services/Controllern
|
||||
class GlobalSecurityConfig {
|
||||
|
||||
@Bean
|
||||
fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http
|
||||
.csrf { it.disable() } // CSRF nicht nötig für Stateless REST APIs
|
||||
.cors { it.configurationSource(corsConfigurationSource()) }
|
||||
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
|
||||
.authorizeHttpRequests { auth ->
|
||||
// Explizite Freigaben (Health, Info, Public Endpoints)
|
||||
auth.requestMatchers("/actuator/**").permitAll()
|
||||
auth.requestMatchers("/ping/public").permitAll()
|
||||
auth.requestMatchers("/error").permitAll()
|
||||
|
||||
// Alles andere muss authentifiziert sein
|
||||
auth.anyRequest().authenticated()
|
||||
}
|
||||
.oauth2ResourceServer { oauth2 ->
|
||||
oauth2.jwt { jwt ->
|
||||
jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())
|
||||
}
|
||||
}
|
||||
|
||||
return http.build()
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
|
||||
val converter = JwtAuthenticationConverter()
|
||||
converter.setJwtGrantedAuthoritiesConverter(KeycloakRoleConverter())
|
||||
return converter
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun corsConfigurationSource(): CorsConfigurationSource {
|
||||
val configuration = CorsConfiguration()
|
||||
// Erlaube Frontend (localhost, docker host)
|
||||
configuration.allowedOriginPatterns = listOf("*") // Für Dev; in Prod einschränken!
|
||||
configuration.allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||
configuration.allowedHeaders = listOf("*")
|
||||
configuration.allowCredentials = true
|
||||
|
||||
val source = UrlBasedCorsConfigurationSource()
|
||||
source.registerCorsConfiguration("/**", configuration)
|
||||
return source
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package at.mocode.infrastructure.security
|
||||
|
||||
import org.springframework.core.convert.converter.Converter
|
||||
import org.springframework.security.core.GrantedAuthority
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||
import org.springframework.security.oauth2.jwt.Jwt
|
||||
|
||||
/**
|
||||
* Konvertiert Keycloak-Rollen aus dem JWT (Realm Access & Resource Access)
|
||||
* in Spring Security GrantedAuthorities.
|
||||
*
|
||||
* Erwartetes Format im Token:
|
||||
* "realm_access": { "roles": ["admin", "user"] }
|
||||
* "resource_access": { "my-client": { "roles": ["client-role"] } }
|
||||
*/
|
||||
class KeycloakRoleConverter : Converter<Jwt, Collection<GrantedAuthority>> {
|
||||
|
||||
override fun convert(jwt: Jwt): Collection<GrantedAuthority> {
|
||||
val roles = mutableSetOf<String>()
|
||||
|
||||
// 1. Realm Roles extrahieren
|
||||
val realmAccess = jwt.claims["realm_access"] as? Map<*, *>
|
||||
if (realmAccess != null) {
|
||||
(realmAccess["roles"] as? List<*>)?.forEach { role ->
|
||||
if (role is String) {
|
||||
roles.add(role)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Resource (Client) Roles extrahieren
|
||||
// Optional: Falls wir Client-spezifische Rollen brauchen.
|
||||
// Hier mappen wir vorerst nur Realm-Rollen global.
|
||||
|
||||
// 3. Mapping zu GrantedAuthority (Prefix "ROLE_" ist Standard in Spring Security)
|
||||
return roles.map { role ->
|
||||
SimpleGrantedAuthority("ROLE_${role.uppercase()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,11 +2,13 @@ package at.mocode.ping
|
|||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.context.annotation.ComponentScan
|
||||
import org.springframework.context.annotation.EnableAspectJAutoProxy
|
||||
|
||||
@SpringBootApplication
|
||||
// Scannt explizit alle Sub-Packages (infrastructure, application, domain)
|
||||
@EnableAspectJAutoProxy
|
||||
// Scannt das eigene Service-Package UND das Security-Infrastruktur-Package
|
||||
@ComponentScan(basePackages = ["at.mocode.ping", "at.mocode.infrastructure.security"])
|
||||
class PingServiceApplication
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
package at.mocode.ping.infrastructure
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http
|
||||
.authorizeHttpRequests { auth ->
|
||||
auth
|
||||
// Public endpoints
|
||||
.requestMatchers("/actuator/**").permitAll()
|
||||
.requestMatchers("/ping/simple", "/ping/enhanced", "/ping/health", "/ping/history").permitAll()
|
||||
// Secure endpoints
|
||||
.requestMatchers("/ping/secure").authenticated()
|
||||
// Default deny
|
||||
.anyRequest().authenticated()
|
||||
}
|
||||
.oauth2ResourceServer { oauth2 ->
|
||||
oauth2.jwt { }
|
||||
}
|
||||
|
||||
return http.build()
|
||||
}
|
||||
}
|
||||
|
|
@ -6,18 +6,13 @@ import jakarta.persistence.Table
|
|||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* JPA Entity (Infrastructure Detail).
|
||||
* Spiegelt die Datenbank-Tabelle wider.
|
||||
* Nutzt java.util.UUID für JPA-Kompatibilität (bis Hibernate kotlin.uuid nativ unterstützt).
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "pings")
|
||||
@Table(name = "ping")
|
||||
class PingJpaEntity(
|
||||
@Id
|
||||
val id: UUID,
|
||||
val message: String,
|
||||
val timestamp: Instant
|
||||
val createdAt: Instant
|
||||
) {
|
||||
// Default constructor for JPA
|
||||
protected constructor() : this(UUID.randomUUID(), "", Instant.now())
|
||||
|
|
|
|||
|
|
@ -2,48 +2,41 @@ package at.mocode.ping.infrastructure.persistence
|
|||
|
||||
import at.mocode.ping.domain.Ping
|
||||
import at.mocode.ping.domain.PingRepository
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.stereotype.Repository
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
import kotlin.uuid.toJavaUuid
|
||||
import kotlin.uuid.toKotlinUuid
|
||||
|
||||
/**
|
||||
* Driven Adapter.
|
||||
* Implementiert den Domain-Port `PingRepository` mithilfe von Spring Data JPA.
|
||||
* Mappt zwischen Domain-Entity und JPA-Entity.
|
||||
*/
|
||||
@Component
|
||||
@OptIn(ExperimentalUuidApi::class)
|
||||
@Repository
|
||||
class PingRepositoryAdapter(
|
||||
private val jpaRepository: SpringDataPingRepository
|
||||
) : PingRepository {
|
||||
|
||||
override fun save(ping: Ping): Ping {
|
||||
val jpaEntity = PingJpaEntity(
|
||||
id = ping.id.toJavaUuid(),
|
||||
message = ping.message,
|
||||
timestamp = ping.timestamp
|
||||
)
|
||||
val saved = jpaRepository.save(jpaEntity)
|
||||
return mapToDomain(saved)
|
||||
val entity = ping.toEntity()
|
||||
val savedEntity = jpaRepository.save(entity)
|
||||
return savedEntity.toDomain()
|
||||
}
|
||||
|
||||
override fun findAll(): List<Ping> {
|
||||
return jpaRepository.findAll().map { mapToDomain(it) }
|
||||
return jpaRepository.findAll().map { it.toDomain() }
|
||||
}
|
||||
|
||||
override fun findById(id: Uuid): Ping? {
|
||||
return jpaRepository.findById(id.toJavaUuid())
|
||||
.map { mapToDomain(it) }
|
||||
.orElse(null)
|
||||
return jpaRepository.findById(id.toJavaUuid()).map { it.toDomain() }.orElse(null)
|
||||
}
|
||||
|
||||
private fun mapToDomain(entity: PingJpaEntity): Ping {
|
||||
return Ping(
|
||||
id = entity.id.toKotlinUuid(),
|
||||
message = entity.message,
|
||||
timestamp = entity.timestamp
|
||||
)
|
||||
}
|
||||
private fun Ping.toEntity() = PingJpaEntity(
|
||||
id = this.id.toJavaUuid(),
|
||||
message = this.message,
|
||||
createdAt = this.timestamp
|
||||
)
|
||||
|
||||
private fun PingJpaEntity.toDomain() = Ping(
|
||||
id = this.id.toKotlinUuid(),
|
||||
message = this.message,
|
||||
timestamp = this.createdAt
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
package at.mocode.ping.infrastructure.persistence
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.util.UUID
|
||||
|
||||
@Repository
|
||||
interface SpringDataPingRepository : JpaRepository<PingJpaEntity, UUID>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import at.mocode.ping.api.PingResponse
|
|||
import at.mocode.ping.application.PingUseCase
|
||||
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
|
@ -32,14 +33,8 @@ class PingController(
|
|||
|
||||
@GetMapping("/ping/simple")
|
||||
override suspend fun simplePing(): PingResponse {
|
||||
// Ruft Use Case auf -> Speichert in DB
|
||||
val domainPing = pingUseCase.executePing("Simple Ping")
|
||||
|
||||
return PingResponse(
|
||||
status = "pong",
|
||||
timestamp = domainPing.timestamp.atOffset(ZoneOffset.UTC).format(formatter),
|
||||
service = "ping-service"
|
||||
)
|
||||
return createResponse(domainPing, "pong")
|
||||
}
|
||||
|
||||
@GetMapping("/ping/enhanced")
|
||||
|
|
@ -53,9 +48,7 @@ class PingController(
|
|||
throw RuntimeException("Simulated service failure")
|
||||
}
|
||||
|
||||
// Use Case Aufruf
|
||||
val domainPing = pingUseCase.executePing("Enhanced Ping")
|
||||
|
||||
val elapsedMs = (System.nanoTime() - start) / 1_000_000
|
||||
|
||||
return EnhancedPingResponse(
|
||||
|
|
@ -67,7 +60,29 @@ class PingController(
|
|||
)
|
||||
}
|
||||
|
||||
// Fallback muss public sein für Resilience4j Proxy
|
||||
// Neue Endpunkte
|
||||
|
||||
@GetMapping("/ping/public")
|
||||
override suspend fun publicPing(): PingResponse {
|
||||
val domainPing = pingUseCase.executePing("Public Ping")
|
||||
return createResponse(domainPing, "public-pong")
|
||||
}
|
||||
|
||||
@GetMapping("/ping/secure")
|
||||
@PreAuthorize("hasRole('MELD_USER') or hasRole('MELD_ADMIN')") // Beispiel-Rollen
|
||||
override suspend fun securePing(): PingResponse {
|
||||
val domainPing = pingUseCase.executePing("Secure Ping")
|
||||
return createResponse(domainPing, "secure-pong")
|
||||
}
|
||||
|
||||
// Helper
|
||||
private fun createResponse(domainPing: at.mocode.ping.domain.Ping, status: String) = PingResponse(
|
||||
status = status,
|
||||
timestamp = domainPing.timestamp.atOffset(ZoneOffset.UTC).format(formatter),
|
||||
service = "ping-service"
|
||||
)
|
||||
|
||||
// Fallback
|
||||
fun fallbackPing(simulate: Boolean, ex: Exception): EnhancedPingResponse {
|
||||
logger.warn("Circuit breaker fallback triggered: {}", ex.message)
|
||||
return EnhancedPingResponse(
|
||||
|
|
@ -89,7 +104,6 @@ class PingController(
|
|||
)
|
||||
}
|
||||
|
||||
// Zusätzlicher Endpunkt um die DB zu prüfen (History)
|
||||
@GetMapping("/ping/history")
|
||||
fun getHistory() = pingUseCase.getPingHistory().map {
|
||||
mapOf("id" to it.id.toString(), "message" to it.message, "time" to it.timestamp.toString())
|
||||
|
|
|
|||
|
|
@ -8,27 +8,29 @@ spring:
|
|||
profiles:
|
||||
active: ${SPRING_PROFILES_ACTIVE:dev}
|
||||
|
||||
# datasource:
|
||||
# url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/pg-meldestelle-db}
|
||||
# username: ${SPRING_DATASOURCE_USERNAME:pg-user}
|
||||
# password: ${SPRING_DATASOURCE_PASSWORD:pg-password}
|
||||
# driver-class-name: org.postgresql.Driver
|
||||
datasource:
|
||||
url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/meldestelle}
|
||||
username: ${SPRING_DATASOURCE_USERNAME:postgres}
|
||||
password: ${SPRING_DATASOURCE_PASSWORD:postgres}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
|
||||
# # --- REDIS KONFIGURATION (NEU) ---
|
||||
# data:
|
||||
# redis:
|
||||
# host: ${SPRING_DATA_REDIS_HOST:localhost}
|
||||
# port: ${SPRING_DATA_REDIS_PORT:6379}
|
||||
# password: ${SPRING_DATA_REDIS_PASSWORD:redis-password} # Leer lassen als Default
|
||||
# # Optional: Timeouts für Stabilität
|
||||
# connect-timeout: 5s
|
||||
# timeout: 2s
|
||||
jpa:
|
||||
database-platform: org.hibernate.dialect.PostgreSQLDialect
|
||||
hibernate:
|
||||
ddl-auto: validate # Flyway kümmert sich um Schema!
|
||||
open-in-view: false
|
||||
|
||||
# jpa:
|
||||
# database-platform: org.hibernate.dialect.PostgreSQLDialect
|
||||
# hibernate:
|
||||
# ddl-auto: update
|
||||
# open-in-view: false
|
||||
flyway:
|
||||
enabled: true
|
||||
baseline-on-migrate: true
|
||||
|
||||
security:
|
||||
oauth2:
|
||||
resourceserver:
|
||||
jwt:
|
||||
# Keycloak URL (innerhalb Docker Netzwerk oder Localhost)
|
||||
issuer-uri: ${KEYCLOAK_ISSUER_URI:http://localhost:9090/realms/meldestelle}
|
||||
jwk-set-uri: ${KEYCLOAK_JWK_SET_URI:http://localhost:9090/realms/meldestelle/protocol/openid-connect/certs}
|
||||
|
||||
cloud:
|
||||
consul:
|
||||
|
|
@ -54,49 +56,29 @@ management:
|
|||
probes:
|
||||
enabled: true
|
||||
tracing:
|
||||
# Disable tracing by default to avoid Zipkin connection errors
|
||||
enabled: ${TRACING_ENABLED:false}
|
||||
sampling:
|
||||
probability: ${TRACING_SAMPLING_PROBABILITY:0.1}
|
||||
zipkin:
|
||||
tracing:
|
||||
# Only configure endpoint if tracing is explicitly enabled
|
||||
endpoint: ${ZIPKIN_TRACING_ENDPOINT:http://localhost:9411/api/v2/spans}
|
||||
# Configure timeout and connection settings to handle missing Zipkin gracefully
|
||||
connect-timeout: 1s
|
||||
read-timeout: 5s
|
||||
|
||||
# Resilience4j Circuit Breaker Configuration
|
||||
resilience4j:
|
||||
circuitbreaker:
|
||||
configs:
|
||||
default:
|
||||
# Circuit breaker opens when the failure rate exceeds 50%
|
||||
failure-rate-threshold: 50
|
||||
# Minimum number of calls to calculate the failure rate
|
||||
minimum-number-of-calls: 5
|
||||
# Time to wait before transitioning from OPEN to HALF_OPEN
|
||||
wait-duration-in-open-state: 10s
|
||||
# Number of calls in HALF_OPEN state before deciding to close/open
|
||||
permitted-number-of-calls-in-half-open-state: 3
|
||||
# Sliding window size for calculating failure rate
|
||||
sliding-window-size: 10
|
||||
# Type of sliding window (COUNT_BASED or TIME_BASED)
|
||||
sliding-window-type: COUNT_BASED
|
||||
# Record exceptions that should be considered as failures
|
||||
record-exceptions:
|
||||
- java.lang.Exception
|
||||
# Ignore certain exceptions (don't count as failures)
|
||||
ignore-exceptions:
|
||||
- java.lang.IllegalArgumentException
|
||||
instances:
|
||||
pingCircuitBreaker:
|
||||
# Use default configuration
|
||||
base-config: default
|
||||
# Override specific settings for this instance if needed
|
||||
failure-rate-threshold: 60
|
||||
minimum-number-of-calls: 4
|
||||
wait-duration-in-open-state: 5s
|
||||
|
||||
# Metrics configuration removed to avoid property resolution warnings
|
||||
# Use micrometer and actuator endpoints for metrics instead
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
CREATE TABLE ping (
|
||||
id UUID PRIMARY KEY,
|
||||
message VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL
|
||||
);
|
||||
|
||||
-- Index für schnelle Sortierung nach Zeit (wichtig für Sync später)
|
||||
CREATE INDEX idx_ping_created_at ON ping(created_at);
|
||||
|
|
@ -4,4 +4,8 @@ interface PingApi {
|
|||
suspend fun simplePing(): PingResponse
|
||||
suspend fun enhancedPing(simulate: Boolean = false): EnhancedPingResponse
|
||||
suspend fun healthCheck(): HealthResponse
|
||||
|
||||
// Neue Endpunkte für Security Hardening
|
||||
suspend fun publicPing(): PingResponse
|
||||
suspend fun securePing(): PingResponse
|
||||
}
|
||||
|
|
|
|||
38
docs/05_Backend/Services/PingService.md
Normal file
38
docs/05_Backend/Services/PingService.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# Ping Service
|
||||
|
||||
Der `ping-service` ist der "Tracer Bullet" Service für die Meldestelle-Architektur. Er dient als Blueprint für alle weiteren Microservices.
|
||||
|
||||
## Verantwortlichkeit
|
||||
* Technischer Durchstich (Frontend -> Gateway -> Service -> DB).
|
||||
* Validierung der Infrastruktur (Security, Resilience, Observability).
|
||||
* Referenzimplementierung für DDD, Hexagonal Architecture und KMP-Integration.
|
||||
|
||||
## API Endpunkte
|
||||
|
||||
| Methode | Pfad | Beschreibung | Auth |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| GET | `/ping/simple` | Einfacher Ping, speichert in DB | Public |
|
||||
| GET | `/ping/enhanced` | Ping mit Circuit Breaker Simulation | Public |
|
||||
| GET | `/ping/public` | Expliziter Public Endpoint | Public |
|
||||
| GET | `/ping/secure` | Geschützter Endpoint (benötigt Rolle) | **Secure** (MELD_USER) |
|
||||
| GET | `/ping/health` | Health Check | Public |
|
||||
| GET | `/ping/history` | Historie aller Pings | Public (Debug) |
|
||||
|
||||
## Architektur
|
||||
Der Service folgt der Hexagonalen Architektur (Ports & Adapters):
|
||||
* **Domain:** `at.mocode.ping.domain` (Pure Kotlin, keine Frameworks).
|
||||
* **Application:** `at.mocode.ping.application` (Use Cases, Spring Service).
|
||||
* **Infrastructure:** `at.mocode.ping.infrastructure` (Web, Persistence, Security).
|
||||
|
||||
## Security
|
||||
* Nutzt das zentrale Modul `backend:infrastructure:security`.
|
||||
* OAuth2 Resource Server (JWT Validation via Keycloak).
|
||||
* Rollen-Mapping: Keycloak Realm Roles -> Spring Security Authorities (`ROLE_...`).
|
||||
|
||||
## Persistence
|
||||
* Datenbank: PostgreSQL.
|
||||
* Migration: Flyway (`V1__init_ping.sql`).
|
||||
* ORM: Spring Data JPA (für Write Model).
|
||||
|
||||
## Resilience
|
||||
* Circuit Breaker: Resilience4j (für DB-Zugriffe und simulierte Fehler).
|
||||
|
|
@ -34,7 +34,7 @@ Dieses Modul wurde neu angelegt. Fülle es mit Leben.
|
|||
* Implementiere OAuth2 Resource Server Support (JWT Validierung).
|
||||
* Definiere globale CORS-Regeln (Frontend darf zugreifen).
|
||||
* [ ] **Role Converter:**
|
||||
* Implementiere einen `KeycloakRoleConverter`, der die Rollen aus dem JWT (Realm/Resource Access) in Spring Security `GrantedAuthority` mappt.
|
||||
* Implementiere einen `KeycloakRoleConverter`, der die Rollen aus dem JWT (Realm/Resource Access) in Spring Security `GrantedAuthority` mapping.
|
||||
* **Wichtig:** Achte auf Kompatibilität. Das Gateway nutzt WebFlux (Reactive), die Services nutzen WebMVC (Servlet). Falls nötig, trenne die Konfigurationen oder nutze `ConditionalOnWebApplication`.
|
||||
|
||||
### B. Persistence Layer (`backend/infrastructure/persistence`)
|
||||
|
|
@ -46,7 +46,7 @@ Das Modul ist bereits konfiguriert.
|
|||
* Nutze `JpaRepository` für Standard-CRUD-Operationen.
|
||||
|
||||
### C. Ping Service Hardening (`backend/services/ping/ping-service`)
|
||||
Mache den Service "Production Ready".
|
||||
Mache den Service "Production Ready."
|
||||
|
||||
* [ ] **Flyway:**
|
||||
* Erstelle `src/main/resources/db/migration/V1__init_ping.sql`.
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user