chore(ci): Align GH Workflows with Docker SSoT, new paths; minimal SSoT guard; staticAnalysis (#23)
* chore(MP-21): snapshot pre-refactor state (Epic 1)
* chore(MP-22): scaffold new repo structure, relocate Docker Compose, move frontend/backend modules, update Makefile; add docs mapping and env template
* MP-22 Epic 2: Erfolgreich umgesetzt und verifiziert
* MP-23 Epic 3: Gradle/Build Governance zentralisieren
* MP-23 Epic 3: Gradle/Build Governance zentralisieren
* chore(devops)!: Docker-SSoT (.env) konsolidiert, Compose-Mounts ergänzt, Makefile entfernt
- ENV Single Source of Truth
- docker/.env.example neu (inkl. REDIS_PASSWORD, Ports, Build-Overrides)
- config/.env(.example) als DEPRECATED markiert (Verweis auf docker/.env[.example])
- Docker Compose vereinheitlicht (docker/docker-compose.yaml)
- Postgres: zentralen postgresql.conf mounten (../config/postgres/postgresql.conf)
und Start mit -c config_file=/etc/postgresql/postgresql.conf
- Redis: zentralen redis.conf mounten (../config/redis/redis.conf)
und Start via "redis-server … ${REDIS_PASSWORD:+--requirepass $REDIS_PASSWORD}"
- Web-Nginx: ../config/nginx/nginx.prod.conf → /etc/nginx/nginx.conf (ro)
- Monitoring: Prometheus/Grafana nutzen ../config/monitoring/* als SSoT
- Frontend/DI/Network (MP-23 Grundlage)
- :frontend:core:network Modul mit Koin `apiClient` (Ktor + JSON/Retry/Timeout/Logging)
- Plattform-Basis-URL-Auflösung (JVM: ENV API_BASE_URL; JS: globalThis.API_BASE_URL / Same-Origin)
- Web index.html setzt API_BASE_URL (Query `?apiBaseUrl=…` > Same-Origin > Fallback)
- Build/Gradle & Module-Refs
- settings.gradle.kts: neue Frontend-/Backend-Pfade bereits inkludiert
- Features/Shell: Abhängigkeiten auf :frontend:shared / :frontend:core:* angepasst
- Ping-API-Refs auf :backend:services:ping:ping-api vereinheitlicht
- Dockerfiles angepasst
- backend/infrastructure/gateway/Dockerfile → Tasks/Pfade auf :backend:gateway
- backend/services/ping/Dockerfile → Tasks/Pfade auf :backend:services:ping:ping-service
- Static Analysis / Guards
- config/detekt/detekt.yml hinzugefügt
- Leichter Arch-Guard (Frontend) gegen manuelle Authorization-Header vorbereitet
- Doku
- docs/ARCHITECTURE.md (Struktur, Mapping, Next Steps) ergänzt
- docs/adr/README.md angelegt
BREAKING CHANGES:
- Makefile komplett entfernt (bitte direkt `docker compose` verwenden)
- ENV-Quelle ist jetzt docker/.env (statt config/.env oder Root)
- Compose-Datei unter docker/docker-compose.yaml (nicht mehr compose.yaml im Repo-Root)
Verifikation (lokal):
- ENV anlegen: `cp docker/.env.example docker/.env` (Werte anpassen)
- Compose prüfen: `docker compose --env-file docker/.env -f docker/docker-compose.yaml config`
- Infrastruktur: `docker compose --env-file docker/.env -f docker/docker-compose.yaml -p meldestelle up -d postgres redis keycloak web-app`
- Services bauen: `docker compose --env-file docker/.env -f docker/docker-compose.yaml -p meldestelle build api-gateway ping-service --no-cache --progress=plain`
Refs: MP-22 (Epic 2), MP-23 (Epic 3)
* chore(devops)!: Docker-SSoT (.env) konsolidiert, Compose-Mounts ergänzt, Makefile entfernt
- ENV Single Source of Truth
- docker/.env.example neu (inkl. REDIS_PASSWORD, Ports, Build-Overrides)
- config/.env(.example) als DEPRECATED markiert (Verweis auf docker/.env[.example])
- Docker Compose vereinheitlicht (docker/docker-compose.yaml)
- Postgres: zentralen postgresql.conf mounten (../config/postgres/postgresql.conf)
und Start mit -c config_file=/etc/postgresql/postgresql.conf
- Redis: zentralen redis.conf mounten (../config/redis/redis.conf)
und Start via "redis-server … ${REDIS_PASSWORD:+--requirepass $REDIS_PASSWORD}"
- Web-Nginx: ../config/nginx/nginx.prod.conf → /etc/nginx/nginx.conf (ro)
- Monitoring: Prometheus/Grafana nutzen ../config/monitoring/* als SSoT
- Frontend/DI/Network (MP-23 Grundlage)
- :frontend:core:network Modul mit Koin `apiClient` (Ktor + JSON/Retry/Timeout/Logging)
- Plattform-Basis-URL-Auflösung (JVM: ENV API_BASE_URL; JS: globalThis.API_BASE_URL / Same-Origin)
- Web index.html setzt API_BASE_URL (Query `?apiBaseUrl=…` > Same-Origin > Fallback)
- Build/Gradle & Module-Refs
- settings.gradle.kts: neue Frontend-/Backend-Pfade bereits inkludiert
- Features/Shell: Abhängigkeiten auf :frontend:shared / :frontend:core:* angepasst
- Ping-API-Refs auf :backend:services:ping:ping-api vereinheitlicht
- Dockerfiles angepasst
- backend/infrastructure/gateway/Dockerfile → Tasks/Pfade auf :backend:gateway
- backend/services/ping/Dockerfile → Tasks/Pfade auf :backend:services:ping:ping-service
- Static Analysis / Guards
- config/detekt/detekt.yml hinzugefügt
- Leichter Arch-Guard (Frontend) gegen manuelle Authorization-Header vorbereitet
- Doku
- docs/ARCHITECTURE.md (Struktur, Mapping, Next Steps) ergänzt
- docs/adr/README.md angelegt
BREAKING CHANGES:
- Makefile komplett entfernt (bitte direkt `docker compose` verwenden)
- ENV-Quelle ist jetzt docker/.env (statt config/.env oder Root)
- Compose-Datei unter docker/docker-compose.yaml (nicht mehr compose.yaml im Repo-Root)
Verifikation (lokal):
- ENV anlegen: `cp docker/.env.example docker/.env` (Werte anpassen)
- Compose prüfen: `docker compose --env-file docker/.env -f docker/docker-compose.yaml config`
- Infrastruktur: `docker compose --env-file docker/.env -f docker/docker-compose.yaml -p meldestelle up -d postgres redis keycloak web-app`
- Services bauen: `docker compose --env-file docker/.env -f docker/docker-compose.yaml -p meldestelle build api-gateway ping-service --no-cache --progress=plain`
Refs: MP-22 (Epic 2), MP-23 (Epic 3)
* chore(devops)!: Docker-SSoT (.env) konsolidiert, Compose-Mounts ergänzt, Makefile entfernt
- ENV Single Source of Truth
- docker/.env.example neu (inkl. REDIS_PASSWORD, Ports, Build-Overrides)
- config/.env(.example) als DEPRECATED markiert (Verweis auf docker/.env[.example])
- Docker Compose vereinheitlicht (docker/docker-compose.yaml)
- Postgres: zentralen postgresql.conf mounten (../config/postgres/postgresql.conf)
und Start mit -c config_file=/etc/postgresql/postgresql.conf
- Redis: zentralen redis.conf mounten (../config/redis/redis.conf)
und Start via "redis-server … ${REDIS_PASSWORD:+--requirepass $REDIS_PASSWORD}"
- Web-Nginx: ../config/nginx/nginx.prod.conf → /etc/nginx/nginx.conf (ro)
- Monitoring: Prometheus/Grafana nutzen ../config/monitoring/* als SSoT
- Frontend/DI/Network (MP-23 Grundlage)
- :frontend:core:network Modul mit Koin `apiClient` (Ktor + JSON/Retry/Timeout/Logging)
- Plattform-Basis-URL-Auflösung (JVM: ENV API_BASE_URL; JS: globalThis.API_BASE_URL / Same-Origin)
- Web index.html setzt API_BASE_URL (Query `?apiBaseUrl=…` > Same-Origin > Fallback)
- Build/Gradle & Module-Refs
- settings.gradle.kts: neue Frontend-/Backend-Pfade bereits inkludiert
- Features/Shell: Abhängigkeiten auf :frontend:shared / :frontend:core:* angepasst
- Ping-API-Refs auf :backend:services:ping:ping-api vereinheitlicht
- Dockerfiles angepasst
- backend/infrastructure/gateway/Dockerfile → Tasks/Pfade auf :backend:gateway
- backend/services/ping/Dockerfile → Tasks/Pfade auf :backend:services:ping:ping-service
- Static Analysis / Guards
- config/detekt/detekt.yml hinzugefügt
- Leichter Arch-Guard (Frontend) gegen manuelle Authorization-Header vorbereitet
- Doku
- docs/ARCHITECTURE.md (Struktur, Mapping, Next Steps) ergänzt
- docs/adr/README.md angelegt
BREAKING CHANGES:
- Makefile komplett entfernt (bitte direkt `docker compose` verwenden)
- ENV-Quelle ist jetzt docker/.env (statt config/.env oder Root)
- Compose-Datei unter docker/docker-compose.yaml (nicht mehr compose.yaml im Repo-Root)
Verifikation (lokal):
- ENV anlegen: `cp docker/.env.example docker/.env` (Werte anpassen)
- Compose prüfen: `docker compose --env-file docker/.env -f docker/docker-compose.yaml config`
- Infrastruktur: `docker compose --env-file docker/.env -f docker/docker-compose.yaml -p meldestelle up -d postgres redis keycloak web-app`
- Services bauen: `docker compose --env-file docker/.env -f docker/docker-compose.yaml -p meldestelle build api-gateway ping-service --no-cache --progress=plain`
Refs: MP-22 (Epic 2), MP-23 (Epic 3)
* chore(ci): Workflows an Docker-SSoT & neue Struktur angepasst, minimaler SSoT-Guard
- ssot-guard.yml: Option B (minimal) → `docker compose -f docker/docker-compose.yaml config` als Lint
- integration-tests.yml: `./gradlew staticAnalysis` vor Integrationstests
- docs-kdoc-sync.yml: Dokka-Task Fallback (dokkaGfmAll || dokkaGfm), YouTrack-Sync nur wenn Script vorhanden
- deploy-proxmox.yml: Compose-Pfade auf docker/docker-compose.yaml + `--env-file docker/.env`; Build/Test Schritte vereinheitlicht
- ci-main.yml: SSoT-Skripte per `if: hashFiles(...)` guarded, Compose-Lint Fallback; OpenAPI‑Pfad → backend/gateway; ADR‑Pfade → docs/adr/**; `staticAnalysis` in Build integriert
- youtrack-sync.yml: unverändert (funktional)
Refs: MP-22, MP-23
* chore(ci): Workflows an Docker-SSoT & neue Struktur angepasst, minimaler SSoT-Guard
- ssot-guard.yml: Option B (minimal) → `docker compose -f docker/docker-compose.yaml config` als Lint
- integration-tests.yml: `./gradlew staticAnalysis` vor Integrationstests
- docs-kdoc-sync.yml: Dokka-Task Fallback (dokkaGfmAll || dokkaGfm), YouTrack-Sync nur wenn Script vorhanden
- deploy-proxmox.yml: Compose-Pfade auf docker/docker-compose.yaml + `--env-file docker/.env`; Build/Test Schritte vereinheitlicht
- ci-main.yml: SSoT-Skripte per `if: hashFiles(...)` guarded, Compose-Lint Fallback; OpenAPI‑Pfad → backend/gateway; ADR‑Pfade → docs/adr/**; `staticAnalysis` in Build integriert
- youtrack-sync.yml: unverändert (funktional)
Refs: MP-22, MP-23
* fix(ci): create .env from example before validating compose config
* fix(ci): update ssot-guard filename (.yaml) and sync workflow state
* fixing
* fix(webpack): correct sql.js fallback configuration for webpack 5
This commit is contained in:
+11
@@ -0,0 +1,11 @@
|
||||
package at.mocode.infrastructure.gateway
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
|
||||
@SpringBootApplication
|
||||
class GatewayApplication
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<GatewayApplication>(*args)
|
||||
}
|
||||
+256
@@ -0,0 +1,256 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.infrastructure.gateway.config
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain
|
||||
import org.springframework.cloud.gateway.filter.GlobalFilter
|
||||
import org.springframework.core.Ordered
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.server.ServerWebExchange
|
||||
import reactor.core.publisher.Mono
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
/**
|
||||
* Gateway-Konfiguration für erweiterte Funktionalitäten wie Logging, Rate Limiting und Security.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Global Filter für Correlations-IDs zur Request-Verfolgung.
|
||||
*/
|
||||
@Component
|
||||
class CorrelationIdFilter : GlobalFilter, Ordered {
|
||||
|
||||
companion object {
|
||||
const val CORRELATION_ID_HEADER = "X-Correlation-ID"
|
||||
}
|
||||
|
||||
override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain): Mono<Void> {
|
||||
val request = exchange.request
|
||||
val correlationId = request.headers.getFirst(CORRELATION_ID_HEADER)
|
||||
?: Uuid.random().toString()
|
||||
|
||||
val mutatedRequest = request.mutate()
|
||||
.header(CORRELATION_ID_HEADER, correlationId)
|
||||
.build()
|
||||
|
||||
val mutatedExchange = exchange.mutate()
|
||||
.request(mutatedRequest)
|
||||
.build()
|
||||
|
||||
// Response-Header nach der Verarbeitung hinzufügen
|
||||
mutatedExchange.response.headers.add(CORRELATION_ID_HEADER, correlationId)
|
||||
|
||||
return chain.filter(mutatedExchange)
|
||||
}
|
||||
|
||||
override fun getOrder(): Int = Ordered.HIGHEST_PRECEDENCE
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced Logging Filter für strukturiertes Logging mit Request/Response Details.
|
||||
*/
|
||||
@Component
|
||||
class EnhancedLoggingFilter : GlobalFilter, Ordered {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(EnhancedLoggingFilter::class.java)
|
||||
|
||||
override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain): Mono<Void> {
|
||||
val startTime = System.currentTimeMillis()
|
||||
val request = exchange.request
|
||||
val correlationId = request.headers.getFirst(CorrelationIdFilter.CORRELATION_ID_HEADER)
|
||||
|
||||
logRequest(request, correlationId)
|
||||
|
||||
return chain.filter(exchange)
|
||||
.doOnSuccess {
|
||||
val responseTime = System.currentTimeMillis() - startTime
|
||||
logResponse(exchange.response, correlationId, responseTime)
|
||||
}
|
||||
.doOnError { error ->
|
||||
val responseTime = System.currentTimeMillis() - startTime
|
||||
logError(error, correlationId, responseTime)
|
||||
}
|
||||
}
|
||||
|
||||
private fun logRequest(request: ServerHttpRequest, correlationId: String?) {
|
||||
logger.info("""
|
||||
[REQUEST] [{}]
|
||||
Method: {}
|
||||
URI: {}
|
||||
RemoteAddress: {}
|
||||
UserAgent: {}
|
||||
""".trimIndent(),
|
||||
correlationId,
|
||||
request.method,
|
||||
request.uri,
|
||||
request.remoteAddress,
|
||||
request.headers.getFirst("User-Agent")
|
||||
)
|
||||
}
|
||||
|
||||
private fun logResponse(response: ServerHttpResponse, correlationId: String?, responseTime: Long) {
|
||||
logger.info("""
|
||||
[RESPONSE] [{}]
|
||||
Status: {}
|
||||
ResponseTime: {}ms
|
||||
""".trimIndent(),
|
||||
correlationId,
|
||||
response.statusCode,
|
||||
responseTime
|
||||
)
|
||||
}
|
||||
|
||||
private fun logError(error: Throwable, correlationId: String?, responseTime: Long) {
|
||||
logger.error("""
|
||||
[ERROR] [{}]
|
||||
Error: {}
|
||||
ResponseTime: {}ms
|
||||
""".trimIndent(),
|
||||
correlationId,
|
||||
error.message,
|
||||
responseTime,
|
||||
error
|
||||
)
|
||||
}
|
||||
|
||||
override fun getOrder(): Int = Ordered.HIGHEST_PRECEDENCE + 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate Limiting Filter basierend auf IP-Adresse und User-Typ.
|
||||
*
|
||||
* Optimierungen:
|
||||
* - Memory-Leak-Schutz durch regelmäßige Bereinigung alter Einträge
|
||||
* - Sichere Rollenvalidierung basierend auf JWT-Authentifizierung
|
||||
* - Bessere Verteilung der Rate-Limits basierend auf Benutzerrollen
|
||||
*/
|
||||
@Component
|
||||
class RateLimitingFilter : GlobalFilter, Ordered {
|
||||
|
||||
private val requestCounts = ConcurrentHashMap<String, RequestCounter>()
|
||||
private val logger = LoggerFactory.getLogger(RateLimitingFilter::class.java)
|
||||
|
||||
// Timestamp der letzten Bereinigung
|
||||
@Volatile
|
||||
private var lastCleanup = System.currentTimeMillis()
|
||||
|
||||
companion object {
|
||||
const val RATE_LIMIT_ENABLED_HEADER = "X-RateLimit-Enabled"
|
||||
const val RATE_LIMIT_LIMIT_HEADER = "X-RateLimit-Limit"
|
||||
const val RATE_LIMIT_REMAINING_HEADER = "X-RateLimit-Remaining"
|
||||
|
||||
// Rate Limits pro Minute
|
||||
const val ANONYMOUS_LIMIT = 50
|
||||
const val AUTHENTICATED_LIMIT = 200
|
||||
const val ADMIN_LIMIT = 500
|
||||
const val AUTH_ENDPOINT_LIMIT = 20
|
||||
const val DEFAULT_LIMIT = 100
|
||||
|
||||
// Bereinigungsintervall: alle 5 Minuten
|
||||
const val CLEANUP_INTERVAL_MS = 5 * 60 * 1000L
|
||||
// Einträge, die älter als 10 Minuten sind, werden entfernt
|
||||
const val ENTRY_MAX_AGE_MS = 10 * 60 * 1000L
|
||||
}
|
||||
|
||||
data class RequestCounter(
|
||||
var count: Int = 0,
|
||||
var lastReset: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain): Mono<Void> {
|
||||
val request = exchange.request
|
||||
val response = exchange.response
|
||||
val clientIp = getClientIp(request)
|
||||
val path = request.path.value()
|
||||
|
||||
// Periodische Bereinigung des Caches zur Vermeidung von memory Leaks
|
||||
performPeriodicCleanup()
|
||||
|
||||
val limit = determineRateLimit(request, path)
|
||||
val counter = requestCounts.computeIfAbsent(clientIp) { RequestCounter() }
|
||||
|
||||
// Zähler zurücksetzen, wenn mehr als eine Minute vergangen ist
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - counter.lastReset > 60_000) {
|
||||
counter.count = 0
|
||||
counter.lastReset = now
|
||||
}
|
||||
|
||||
counter.count++
|
||||
|
||||
// Rate-Limit-Header hinzufügen
|
||||
response.headers.add(RATE_LIMIT_ENABLED_HEADER, "true")
|
||||
response.headers.add(RATE_LIMIT_LIMIT_HEADER, limit.toString())
|
||||
response.headers.add(RATE_LIMIT_REMAINING_HEADER, maxOf(0, limit - counter.count).toString())
|
||||
|
||||
return if (counter.count > limit) {
|
||||
response.statusCode = HttpStatus.TOO_MANY_REQUESTS
|
||||
response.setComplete()
|
||||
} else {
|
||||
chain.filter(exchange)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getClientIp(request: ServerHttpRequest): String {
|
||||
return request.headers.getFirst("X-Forwarded-For")?.split(",")?.first()?.trim()
|
||||
?: request.headers.getFirst("X-Real-IP")
|
||||
?: request.remoteAddress?.address?.hostAddress
|
||||
?: "unknown"
|
||||
}
|
||||
|
||||
private fun determineRateLimit(request: ServerHttpRequest, path: String): Int {
|
||||
return when {
|
||||
path.startsWith("/api/auth") -> AUTH_ENDPOINT_LIMIT
|
||||
isAdminUser(request) -> ADMIN_LIMIT
|
||||
isAuthenticatedUser(request) -> AUTHENTICATED_LIMIT
|
||||
else -> ANONYMOUS_LIMIT
|
||||
}
|
||||
}
|
||||
|
||||
private fun isAuthenticatedUser(request: ServerHttpRequest): Boolean {
|
||||
return request.headers.getFirst("Authorization") != null
|
||||
}
|
||||
|
||||
private fun isAdminUser(request: ServerHttpRequest): Boolean {
|
||||
// Sichere Rollenvalidierung basierend auf JWT-Authentifizierung
|
||||
// die X-User-Role wird vom JwtAuthenticationFilter nach erfolgreicher JWT-Validierung gesetzt
|
||||
val userRole = request.headers.getFirst("X-User-Role")
|
||||
val userId = request.headers.getFirst("X-User-ID")
|
||||
|
||||
// Zusätzliche Sicherheitsprüfung: Beide Header müssen vorhanden sein.
|
||||
// Dies reduziert die Wahrscheinlichkeit von Header-Spoofing
|
||||
return userRole == "ADMIN" && userId != null
|
||||
}
|
||||
|
||||
/**
|
||||
* Bereinigt alte Einträge aus dem requestCounts Cache zur Vermeidung von memory Leaks.
|
||||
* Wird nur alle CLEANUP_INTERVAL_MS ausgeführt für bessere Performance.
|
||||
*/
|
||||
private fun performPeriodicCleanup() {
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastCleanup > CLEANUP_INTERVAL_MS) {
|
||||
val sizeBefore = requestCounts.size
|
||||
val cutoffTime = now - ENTRY_MAX_AGE_MS
|
||||
|
||||
// Entferne alle Einträge, die älter als ENTRY_MAX_AGE_MS sind
|
||||
requestCounts.entries.removeIf { (_, counter) ->
|
||||
counter.lastReset < cutoffTime
|
||||
}
|
||||
|
||||
lastCleanup = now
|
||||
val sizeAfter = requestCounts.size
|
||||
|
||||
if (sizeBefore > sizeAfter) {
|
||||
logger.debug("Rate limit cache cleanup: removed {} old entries, {} entries remaining",
|
||||
sizeBefore - sizeAfter, sizeAfter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getOrder(): Int = Ordered.HIGHEST_PRECEDENCE + 2
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package at.mocode.infrastructure.gateway.controller
|
||||
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RequestMethod
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import java.time.LocalDateTime
|
||||
|
||||
/**
|
||||
* Fallback Controller für Circuit Breaker Szenarien.
|
||||
* Bietet standardisierte Fehlermeldungen, wenn Backend-Services nicht verfügbar sind.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/fallback")
|
||||
class FallbackController {
|
||||
|
||||
@RequestMapping(value = ["/members"], method = [RequestMethod.GET, RequestMethod.POST])
|
||||
fun membersFallback(): ResponseEntity<ErrorResponse> {
|
||||
return createFallbackResponse("members-service", "Member operations are temporarily unavailable")
|
||||
}
|
||||
|
||||
@RequestMapping(value = ["/horses"], method = [RequestMethod.GET, RequestMethod.POST])
|
||||
fun horsesFallback(): ResponseEntity<ErrorResponse> {
|
||||
return createFallbackResponse("horses-service", "Horse registry operations are temporarily unavailable")
|
||||
}
|
||||
|
||||
@RequestMapping(value = ["/events"], method = [RequestMethod.GET, RequestMethod.POST])
|
||||
fun eventsFallback(): ResponseEntity<ErrorResponse> {
|
||||
return createFallbackResponse("events-service", "Event management operations are temporarily unavailable")
|
||||
}
|
||||
|
||||
@RequestMapping(value = ["/masterdata"], method = [RequestMethod.GET, RequestMethod.POST])
|
||||
fun masterdataFallback(): ResponseEntity<ErrorResponse> {
|
||||
return createFallbackResponse("masterdata-service", "Master data operations are temporarily unavailable")
|
||||
}
|
||||
|
||||
@RequestMapping(value = ["/auth"], method = [RequestMethod.GET, RequestMethod.POST])
|
||||
fun authFallback(): ResponseEntity<ErrorResponse> {
|
||||
return createFallbackResponse("auth-service", "Authentication operations are temporarily unavailable")
|
||||
}
|
||||
|
||||
@RequestMapping(value = [""], method = [RequestMethod.GET, RequestMethod.POST])
|
||||
fun defaultFallback(): ResponseEntity<ErrorResponse> {
|
||||
return createFallbackResponse("unknown-service", "Service is temporarily unavailable")
|
||||
}
|
||||
|
||||
private fun createFallbackResponse(service: String, message: String): ResponseEntity<ErrorResponse> {
|
||||
val errorResponse = ErrorResponse(
|
||||
error = "SERVICE_UNAVAILABLE",
|
||||
message = message,
|
||||
service = service,
|
||||
timestamp = LocalDateTime.now(),
|
||||
status = HttpStatus.SERVICE_UNAVAILABLE.value(),
|
||||
suggestion = "Please try again in a few moments. If the problem persists, contact support."
|
||||
)
|
||||
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(errorResponse)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Standardisierte Fehlerantwort für Circuit Breaker Fallbacks.
|
||||
*/
|
||||
data class ErrorResponse(
|
||||
val error: String,
|
||||
val message: String,
|
||||
val service: String,
|
||||
val timestamp: LocalDateTime,
|
||||
val status: Int,
|
||||
val suggestion: String
|
||||
)
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
package at.mocode.infrastructure.gateway.health
|
||||
|
||||
import org.springframework.boot.actuate.health.Health
|
||||
import org.springframework.boot.actuate.health.HealthIndicator
|
||||
import org.springframework.cloud.client.discovery.DiscoveryClient
|
||||
import org.springframework.core.env.Environment
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.reactive.function.client.WebClient
|
||||
import org.springframework.web.reactive.function.client.WebClientResponseException
|
||||
import java.time.Duration
|
||||
|
||||
/**
|
||||
* Gateway Health Indicator zur Überwachung der Downstream Services.
|
||||
*
|
||||
* Prüft die Verfügbarkeit aller registrierten Services über Consul Discovery
|
||||
* und führt Health-Checks für kritische Services durch.
|
||||
*/
|
||||
@Component
|
||||
class GatewayHealthIndicator(
|
||||
private val discoveryClient: DiscoveryClient,
|
||||
private val webClient: WebClient.Builder,
|
||||
private val environment: Environment
|
||||
) : HealthIndicator {
|
||||
|
||||
companion object {
|
||||
private val CRITICAL_SERVICES = setOf(
|
||||
"ping-service"
|
||||
)
|
||||
|
||||
private val OPTIONAL_SERVICES = setOf(
|
||||
"members-service",
|
||||
"horses-service",
|
||||
"events-service",
|
||||
"masterdata-service",
|
||||
"auth-service"
|
||||
)
|
||||
|
||||
private val HEALTH_CHECK_TIMEOUT = Duration.ofSeconds(5)
|
||||
}
|
||||
|
||||
override fun health(): Health {
|
||||
val builder = Health.up()
|
||||
val details = mutableMapOf<String, Any>()
|
||||
|
||||
try {
|
||||
// Prüfe alle registrierten Services in Consul
|
||||
val allServices = discoveryClient.services
|
||||
val discoveredServices = mutableMapOf<String, Any>()
|
||||
|
||||
allServices.forEach { serviceName ->
|
||||
val instances = discoveryClient.getInstances(serviceName)
|
||||
discoveredServices[serviceName] = mapOf(
|
||||
"instanceCount" to instances.size,
|
||||
"instances" to instances.map { "${it.host}:${it.port}" }
|
||||
)
|
||||
}
|
||||
|
||||
details["discoveredServices"] = discoveredServices
|
||||
details["totalServices"] = allServices.size
|
||||
|
||||
// Prüfe kritische Services
|
||||
val criticalServiceStatus = mutableMapOf<String, String>()
|
||||
var hasCriticalFailure = false
|
||||
|
||||
CRITICAL_SERVICES.forEach { serviceName ->
|
||||
val status = checkServiceHealth(serviceName)
|
||||
criticalServiceStatus[serviceName] = status
|
||||
if (status != "UP") {
|
||||
hasCriticalFailure = true
|
||||
}
|
||||
}
|
||||
|
||||
// Prüfe optionale Services
|
||||
val optionalServiceStatus = mutableMapOf<String, String>()
|
||||
OPTIONAL_SERVICES.forEach { serviceName ->
|
||||
optionalServiceStatus[serviceName] = checkServiceHealth(serviceName)
|
||||
}
|
||||
|
||||
details["criticalServices"] = criticalServiceStatus
|
||||
details["optionalServices"] = optionalServiceStatus
|
||||
|
||||
// Gateway Status basierend auf kritischen Services
|
||||
val isTestEnvironment = environment.activeProfiles.contains("test")
|
||||
val isDevEnvironment = environment.activeProfiles.contains("dev")
|
||||
|
||||
if (hasCriticalFailure && !isTestEnvironment && !isDevEnvironment) {
|
||||
builder.down()
|
||||
details["status"] = "DOWN"
|
||||
details["reason"] = "Ein oder mehrere kritische Services sind nicht verfügbar"
|
||||
} else {
|
||||
details["status"] = "UP"
|
||||
details["reason"] = when {
|
||||
isTestEnvironment -> "Gesundheitsprüfung erfolgreich (Testumgebung)"
|
||||
isDevEnvironment -> "Gesundheitsprüfung erfolgreich (Entwicklungsumgebung - nicht alle Services erforderlich)"
|
||||
else -> "Alle kritischen Services sind verfügbar"
|
||||
}
|
||||
}
|
||||
|
||||
} catch (exception: Exception) {
|
||||
builder.down()
|
||||
.withException(exception)
|
||||
details["status"] = "DOWN"
|
||||
details["reason"] = "Fehler beim Prüfen der nachgelagerten Services: ${exception.message}"
|
||||
}
|
||||
|
||||
return builder.withDetails(details).build()
|
||||
}
|
||||
|
||||
private fun checkServiceHealth(serviceName: String): String {
|
||||
return try {
|
||||
val instances = discoveryClient.getInstances(serviceName)
|
||||
|
||||
if (instances.isEmpty()) {
|
||||
"NO_INSTANCES"
|
||||
} else {
|
||||
// Versuche Health-Check für die erste verfügbare Instanz
|
||||
val instance = instances.first()
|
||||
val healthUrl = "http://${instance.host}:${instance.port}/actuator/health"
|
||||
|
||||
val client = webClient.build()
|
||||
val response = client.get()
|
||||
.uri(healthUrl)
|
||||
.retrieve()
|
||||
.bodyToMono(Map::class.java)
|
||||
.timeout(HEALTH_CHECK_TIMEOUT)
|
||||
.onErrorReturn(mapOf("status" to "DOWN"))
|
||||
.block()
|
||||
|
||||
val status = response?.get("status")?.toString() ?: "UNKNOWN"
|
||||
if (status == "UP") "UP" else "DOWN"
|
||||
}
|
||||
} catch (exception: WebClientResponseException) {
|
||||
when (exception.statusCode.value()) {
|
||||
404 -> "NO_HEALTH_ENDPOINT"
|
||||
503 -> "DOWN"
|
||||
else -> "ERROR"
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
"ERROR"
|
||||
}
|
||||
}
|
||||
}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
package at.mocode.infrastructure.gateway.metrics
|
||||
|
||||
import io.micrometer.core.instrument.Counter
|
||||
import io.micrometer.core.instrument.MeterRegistry
|
||||
import io.micrometer.core.instrument.Timer
|
||||
import io.micrometer.core.instrument.config.MeterFilter
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.web.server.ServerWebExchange
|
||||
import org.springframework.web.server.WebFilter
|
||||
import org.springframework.web.server.WebFilterChain
|
||||
import reactor.core.publisher.Mono
|
||||
import java.time.Duration
|
||||
|
||||
/**
|
||||
* Konfiguration für Gateway-spezifische Metriken mit Micrometer.
|
||||
*
|
||||
* Diese Konfiguration stellt folgende Metriken bereit:
|
||||
* - Request/Response Zeit Metriken (Timer)
|
||||
* - Fehlerrate Tracking (Counter)
|
||||
* - Custom Business Metrics
|
||||
*
|
||||
* Alle Metriken werden automatisch an Prometheus exportiert durch die
|
||||
* bestehende monitoring-client Integration.
|
||||
*/
|
||||
@Configuration
|
||||
class GatewayMetricsConfig {
|
||||
|
||||
companion object {
|
||||
// Metric Namen als Konstanten für bessere Wartbarkeit
|
||||
const val GATEWAY_REQUEST_TIMER = "gateway_custom_request_duration"
|
||||
const val GATEWAY_ERROR_COUNTER = "gateway_errors_total"
|
||||
const val GATEWAY_REQUESTS_COUNTER = "gateway_requests_total"
|
||||
const val GATEWAY_CIRCUIT_BREAKER_COUNTER = "gateway_circuit_breaker_events_total"
|
||||
const val GATEWAY_DOWNSTREAM_HEALTH_GAUGE = "gateway_downstream_health_status"
|
||||
}
|
||||
|
||||
/**
|
||||
* Konfiguriert globale Meter-Registry Einstellungen für das Gateway.
|
||||
*/
|
||||
@Bean
|
||||
fun gatewayMeterRegistryCustomizer(): MeterRegistryCustomizer<MeterRegistry> {
|
||||
return MeterRegistryCustomizer { registry ->
|
||||
// Gemeinsame Tags für alle Gateway-Metriken
|
||||
registry.config()
|
||||
.commonTags("service", "gateway", "component", "infrastructure")
|
||||
// Filterung von zu detaillierten Metriken
|
||||
.meterFilter(MeterFilter.deny { id ->
|
||||
val name = id.name
|
||||
// Ausschluss von internen Spring/Netty Metriken, die zu viel Rauschen erzeugen
|
||||
name.startsWith("reactor.netty.connection.provider") ||
|
||||
name.startsWith("reactor.netty.bytebuf.allocator") ||
|
||||
name.startsWith("jvm.gc.overhead")
|
||||
})
|
||||
// Histogram-Buckets für Request Duration optimieren
|
||||
.meterFilter(MeterFilter.accept())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WebFilter für automatische Request/Response Zeit und Error Rate Tracking.
|
||||
*
|
||||
* Dieser Filter misst:
|
||||
* - Gesamte Request-Verarbeitungszeit
|
||||
* - Anzahl der Requests nach Status-Code kategorisiert
|
||||
* - Error-Rate basierend auf HTTP Status Codes
|
||||
*/
|
||||
@Bean
|
||||
fun gatewayMetricsWebFilter(meterRegistry: MeterRegistry): WebFilter {
|
||||
return GatewayMetricsWebFilter(meterRegistry)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bean für Request Duration Timer - entfernt um Konflikte mit dem WebFilter zu vermeiden.
|
||||
* Die Request-Zeiten werden automatisch im GatewayMetricsWebFilter erfasst.
|
||||
*/
|
||||
// @Bean - entfernt, um Prometheus Meter-Konflikte zu vermeiden,
|
||||
// fun requestTimer(meterRegistry: MeterRegistry): Timer { ... }
|
||||
|
||||
/**
|
||||
* Bean für Error Counter - ermöglicht manuelles Error Tracking.
|
||||
*/
|
||||
@Bean
|
||||
fun errorCounter(meterRegistry: MeterRegistry): Counter {
|
||||
return Counter.builder(GATEWAY_ERROR_COUNTER)
|
||||
.description("Gesamtanzahl der Gateway-Fehler")
|
||||
.register(meterRegistry)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bean für Request Counter - ermöglicht Request-Volumen Tracking.
|
||||
* Hinweis: Dieser Counter wird nur als Fallback registriert.
|
||||
* Die tatsächlichen Requests werden mit dynamischen Tags im WebFilter erfasst.
|
||||
*/
|
||||
@Bean
|
||||
fun requestCounter(meterRegistry: MeterRegistry): Counter {
|
||||
return Counter.builder("${GATEWAY_REQUESTS_COUNTER}_fallback")
|
||||
.description("Gateway-Requests Fallback Counter")
|
||||
.register(meterRegistry)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bean für Circuit Breaker Events Counter.
|
||||
*/
|
||||
@Bean
|
||||
fun circuitBreakerCounter(meterRegistry: MeterRegistry): Counter {
|
||||
return Counter.builder(GATEWAY_CIRCUIT_BREAKER_COUNTER)
|
||||
.description("Circuit Breaker Events im Gateway")
|
||||
.register(meterRegistry)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WebFilter Implementation für automatische Metrics-Erfassung.
|
||||
*/
|
||||
class GatewayMetricsWebFilter(private val meterRegistry: MeterRegistry) : WebFilter {
|
||||
|
||||
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
|
||||
val startTime = System.nanoTime()
|
||||
val request = exchange.request
|
||||
val path = request.path.value()
|
||||
val method = request.method.toString()
|
||||
|
||||
// Request Counter incrementer
|
||||
Counter.builder(GatewayMetricsConfig.GATEWAY_REQUESTS_COUNTER)
|
||||
.tag("method", method)
|
||||
.tag("path", normalizePath(path))
|
||||
.description("Gateway-Requests gesamt")
|
||||
.register(meterRegistry)
|
||||
.increment()
|
||||
|
||||
return chain.filter(exchange)
|
||||
.doFinally { _ ->
|
||||
val duration = Duration.ofNanos(System.nanoTime() - startTime)
|
||||
val response = exchange.response
|
||||
val statusCode = response.statusCode?.value()?.toString() ?: "unknown"
|
||||
val statusSeries = when {
|
||||
statusCode.startsWith("2") -> "2xx"
|
||||
statusCode.startsWith("3") -> "3xx"
|
||||
statusCode.startsWith("4") -> "4xx"
|
||||
statusCode.startsWith("5") -> "5xx"
|
||||
else -> "unknown"
|
||||
}
|
||||
|
||||
// Request Duration Timer
|
||||
Timer.builder(GatewayMetricsConfig.GATEWAY_REQUEST_TIMER)
|
||||
.tag("method", method)
|
||||
.tag("path", normalizePath(path))
|
||||
.tag("status", statusCode)
|
||||
.tag("status_series", statusSeries)
|
||||
.description("Gateway Request-Verarbeitungszeit")
|
||||
.register(meterRegistry)
|
||||
.record(duration)
|
||||
|
||||
// Error Counter für 4xx und 5xx Responses
|
||||
if (statusCode.startsWith("4") || statusCode.startsWith("5")) {
|
||||
Counter.builder(GatewayMetricsConfig.GATEWAY_ERROR_COUNTER)
|
||||
.tag("method", method)
|
||||
.tag("path", normalizePath(path))
|
||||
.tag("status", statusCode)
|
||||
.tag("status_series", statusSeries)
|
||||
.tag("error_type", if (statusCode.startsWith("4")) "client_error" else "server_error")
|
||||
.description("Gateway-Fehleranzahl")
|
||||
.register(meterRegistry)
|
||||
.increment()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalisiert Pfade für Metriken, um Kardinalität-Explosion zu vermeiden.
|
||||
* Beispiel: /api/horses/123 → /api/horses/{id}
|
||||
*/
|
||||
private fun normalizePath(path: String): String {
|
||||
return path
|
||||
// UUID pattern ersetzen
|
||||
.replace(Regex("/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"), "/{uuid}")
|
||||
// Numerische IDs ersetzen
|
||||
.replace(Regex("/\\d+"), "/{id}")
|
||||
// Sehr lange Pfade kürzen
|
||||
.let { if (it.length > 100) "${it.substring(0, 97)}..." else it }
|
||||
}
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
package at.mocode.infrastructure.gateway.security
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
|
||||
import org.springframework.security.config.web.server.ServerHttpSecurity
|
||||
import org.springframework.security.config.web.server.invoke
|
||||
import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder
|
||||
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder
|
||||
import org.springframework.security.web.server.SecurityWebFilterChain
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers
|
||||
import org.springframework.web.cors.CorsConfiguration
|
||||
import org.springframework.web.cors.reactive.CorsConfigurationSource
|
||||
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource
|
||||
import java.time.Duration
|
||||
|
||||
@Configuration
|
||||
@EnableWebFluxSecurity
|
||||
@EnableConfigurationProperties(GatewaySecurityProperties::class)
|
||||
class SecurityConfig(
|
||||
private val securityProperties: GatewaySecurityProperties
|
||||
) {
|
||||
|
||||
/**
|
||||
* Konfiguriert die zentrale Security-Filter-Kette für das Gateway.
|
||||
*
|
||||
* Diese Konfiguration nutzt den Standard-OAuth2-Resource-Server von Spring Security,
|
||||
* um JWTs (z.B. von Keycloak) automatisch zu validieren.
|
||||
*/
|
||||
@Bean
|
||||
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
|
||||
return http { // Start der modernen Kotlin-DSL
|
||||
// 1. CORS-Konfiguration anwenden
|
||||
cors { }
|
||||
|
||||
// 2. CSRF deaktivieren (für zustandslose APIs)
|
||||
csrf { disable() }
|
||||
|
||||
// 3. Routen-Berechtigungen definieren
|
||||
authorizeExchange {
|
||||
// Öffentlich zugängliche Pfade aus der .yml-Datei laden
|
||||
authorize(
|
||||
pathMatchers(*securityProperties.publicPaths.toTypedArray()),
|
||||
permitAll
|
||||
)
|
||||
// Alle anderen Pfade erfordern eine Authentifizierung
|
||||
authorize(anyExchange, authenticated)
|
||||
}
|
||||
|
||||
// 4. JWT-Validierung via Keycloak aktivieren
|
||||
oauth2ResourceServer {
|
||||
jwt { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen ReactiveJwtDecoder für die JWT-Validierung.
|
||||
*
|
||||
* Verwendet die JWK Set URI aus der Konfiguration, um die öffentlichen Schlüssel
|
||||
* von Keycloak zu laden. Falls die URI nicht konfiguriert ist oder Keycloak
|
||||
* nicht erreichbar ist, wird trotzdem ein Bean erstellt, um Startfehler zu vermeiden.
|
||||
*/
|
||||
@Bean
|
||||
fun reactiveJwtDecoder(
|
||||
@Value($$"${spring.security.oauth2.resourceserver.jwt.jwk-set-uri:}") jwkSetUri: String
|
||||
): ReactiveJwtDecoder {
|
||||
return if (jwkSetUri.isNotBlank()) {
|
||||
try {
|
||||
NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build()
|
||||
} catch (e: Exception) {
|
||||
// Log warning and return a no-op decoder to allow startup
|
||||
println("WARN: Failed to configure JWT decoder with JWK Set URI: $jwkSetUri - ${e.message}")
|
||||
println("WARN: JWT authentication will not work until Keycloak is available")
|
||||
createNoOpJwtDecoder()
|
||||
}
|
||||
} else {
|
||||
println("INFO: No JWK Set URI configured, using no-op JWT decoder")
|
||||
createNoOpJwtDecoder()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen No-Op JWT Decoder für Fälle, in denen Keycloak nicht verfügbar ist.
|
||||
* Dieser Decoder lehnt alle Token ab, erlaubt aber den Anwendungsstart.
|
||||
*/
|
||||
private fun createNoOpJwtDecoder(): ReactiveJwtDecoder {
|
||||
return ReactiveJwtDecoder { token ->
|
||||
throw IllegalStateException("JWT validation is not available - Keycloak may not be running")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Definiert die zentrale und einzige CORS-Konfiguration für das Gateway.
|
||||
*/
|
||||
@Bean
|
||||
fun corsConfigurationSource(): CorsConfigurationSource {
|
||||
val configuration = CorsConfiguration().apply {
|
||||
allowedOriginPatterns = securityProperties.cors.allowedOriginPatterns.toList()
|
||||
allowedMethods = securityProperties.cors.allowedMethods.toList()
|
||||
allowedHeaders = securityProperties.cors.allowedHeaders.toList()
|
||||
exposedHeaders = securityProperties.cors.exposedHeaders.toList()
|
||||
allowCredentials = securityProperties.cors.allowCredentials
|
||||
maxAge = securityProperties.cors.maxAge.seconds
|
||||
}
|
||||
|
||||
return UrlBasedCorsConfigurationSource().apply {
|
||||
registerCorsConfiguration("/**", configuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configurations-Properties für alle sicherheitsrelevanten Einstellungen des Gateways.
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "gateway.security")
|
||||
data class GatewaySecurityProperties(
|
||||
val cors: CorsProperties = CorsProperties(),
|
||||
val publicPaths: List<String> = listOf(
|
||||
"/",
|
||||
"/fallback/**",
|
||||
"/actuator/**",
|
||||
"/webjars/**",
|
||||
"/v3/api-docs/**",
|
||||
"/api/auth/**", // Alle Auth-Endpunkte
|
||||
"/api/ping/**"
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* DTO für CORS-Properties mit sinnvollen Standardwerten.
|
||||
*/
|
||||
data class CorsProperties(
|
||||
val allowedOriginPatterns: Set<String> = setOf("http://localhost:[*]", "https://*.meldestelle.at"),
|
||||
val allowedMethods: Set<String> = setOf("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"),
|
||||
val allowedHeaders: Set<String> = setOf("*"),
|
||||
val exposedHeaders: Set<String> = setOf("X-Correlation-ID", "X-RateLimit-Limit", "X-RateLimit-Remaining"),
|
||||
val allowCredentials: Boolean = true,
|
||||
val maxAge: Duration = Duration.ofHours(1)
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
# ===================================================================
|
||||
# Keycloak Profile Configuration
|
||||
# ===================================================================
|
||||
# This profile configures OAuth2/JWT authentication with Keycloak.
|
||||
# Uses Spring Security's oauth2ResourceServer for secure JWT validation.
|
||||
# ===================================================================
|
||||
|
||||
spring:
|
||||
security:
|
||||
oauth2:
|
||||
resourceserver:
|
||||
jwt:
|
||||
# Issuer URI for JWT validation - Docker internal: keycloak:8080, External: localhost:8180
|
||||
issuer-uri: ${KEYCLOAK_ISSUER_URI:http://keycloak:8180/realms/meldestelle}
|
||||
# JWK Set URI for fetching public keys to validate JWT signatures
|
||||
jwk-set-uri: ${KEYCLOAK_JWK_SET_URI:http://keycloak:8180/realms/meldestelle/protocol/openid-connect/certs}
|
||||
|
||||
# Keycloak-spezifische Konfiguration
|
||||
keycloak:
|
||||
# Internal Docker service name, external via port 8180
|
||||
server-url: ${KEYCLOAK_SERVER_URL:http://keycloak:8180}
|
||||
issuer-uri: ${KEYCLOAK_ISSUER_URI:http://keycloak:8180/realms/meldestelle}
|
||||
jwk-set-uri: ${KEYCLOAK_JWK_SET_URI:http://keycloak:8180/realms/meldestelle/protocol/openid-connect/certs}
|
||||
realm: ${KEYCLOAK_REALM:meldestelle}
|
||||
resource: ${KEYCLOAK_CLIENT_ID:api-gateway}
|
||||
client-id: ${KEYCLOAK_CLIENT_ID:api-gateway}
|
||||
public-client: false
|
||||
bearer-only: true
|
||||
@@ -0,0 +1,3 @@
|
||||
# Placeholder HOCON configuration for compatibility with legacy test scripts
|
||||
# The actual configuration is provided in application.yml.
|
||||
# This file ensures scripts that check for application.conf do not fail.
|
||||
@@ -0,0 +1,299 @@
|
||||
# Port, auf dem das Gateway läuft
|
||||
server:
|
||||
port: ${SERVER_PORT:8081}
|
||||
# Optimierte Netty-Konfiguration für reaktive Anwendungen
|
||||
netty:
|
||||
connection-timeout: 5s
|
||||
idle-timeout: 15s
|
||||
|
||||
# Der Name, unter dem sich das Gateway in Consul registriert
|
||||
spring:
|
||||
application:
|
||||
name: api-gateway
|
||||
profiles:
|
||||
active: ${SPRING_PROFILES_ACTIVE:dev}
|
||||
security:
|
||||
user:
|
||||
name: ${GATEWAY_ADMIN_USER:admin}
|
||||
password: ${GATEWAY_ADMIN_PASSWORD:admin}
|
||||
cloud:
|
||||
consul:
|
||||
host: ${CONSUL_HOST:localhost}
|
||||
port: ${CONSUL_PORT:8500}
|
||||
enabled: ${CONSUL_ENABLED:true}
|
||||
discovery:
|
||||
enabled: ${CONSUL_ENABLED:true}
|
||||
register: ${CONSUL_ENABLED:true}
|
||||
health-check-path: /actuator/health
|
||||
health-check-interval: 10s
|
||||
instance-id: ${spring.application.name}-${server.port}-${random.uuid}
|
||||
gateway:
|
||||
server:
|
||||
webflux:
|
||||
httpclient:
|
||||
connect-timeout: 5000
|
||||
response-timeout: 30s
|
||||
pool:
|
||||
max-idle-time: 15s
|
||||
max-life-time: 60s
|
||||
default-filters:
|
||||
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
|
||||
- name: CircuitBreaker
|
||||
args:
|
||||
name: defaultCircuitBreaker
|
||||
fallbackUri: forward:/fallback
|
||||
- name: Retry
|
||||
args:
|
||||
retries: 3
|
||||
statuses: BAD_GATEWAY,GATEWAY_TIMEOUT
|
||||
methods: GET,POST,PUT,DELETE
|
||||
backoff:
|
||||
firstBackoff: 50ms
|
||||
maxBackoff: 500ms
|
||||
factor: 2
|
||||
basedOnPreviousValue: false
|
||||
- name: AddResponseHeader
|
||||
args:
|
||||
name: X-Content-Type-Options
|
||||
value: nosniff
|
||||
- name: AddResponseHeader
|
||||
args:
|
||||
name: X-Frame-Options
|
||||
value: DENY
|
||||
- name: AddResponseHeader
|
||||
args:
|
||||
name: X-XSS-Protection
|
||||
value: 1; mode=block
|
||||
- name: AddResponseHeader
|
||||
args:
|
||||
name: Referrer-Policy
|
||||
value: strict-origin-when-cross-origin
|
||||
- name: AddResponseHeader
|
||||
args:
|
||||
name: Cache-Control
|
||||
value: no-cache, no-store, must-revalidate
|
||||
routes:
|
||||
|
||||
# ==============================================================
|
||||
# --- Gateway-Info-Route (optional) ---
|
||||
# ==============================================================
|
||||
- id: gateway-info-route
|
||||
uri: http://localhost:${server.port}
|
||||
predicates:
|
||||
- Method=GET
|
||||
- Path=/gateway-info
|
||||
filters:
|
||||
- name: SetStatus
|
||||
args:
|
||||
status: 200
|
||||
- name: SetResponseHeader
|
||||
args:
|
||||
name: Content-Type
|
||||
value: application/json
|
||||
|
||||
# ==============================================================
|
||||
# --- Ping-Service-Integration (optional) ---
|
||||
# ==============================================================
|
||||
- id: ping-service-route
|
||||
uri: lb://ping-service
|
||||
predicates:
|
||||
- Path=/api/ping/**
|
||||
filters:
|
||||
- StripPrefix=1
|
||||
|
||||
# ==============================================================
|
||||
# --- Members-Service-Integration (optional) ---
|
||||
# ==============================================================
|
||||
# - id: members-service-route
|
||||
# uri: lb://members-service
|
||||
# predicates:
|
||||
# - Path=/api/members/**
|
||||
# filters:
|
||||
# - StripPrefix=1
|
||||
# - name: CircuitBreaker
|
||||
# args:
|
||||
# name: membersCircuitBreaker
|
||||
# fallbackUri: forward:/fallback/members
|
||||
|
||||
# ==============================================================
|
||||
# --- Horses-Service-Integration (optional) ---
|
||||
# ==============================================================
|
||||
# - id: horses-service-route
|
||||
# uri: lb://horses-service
|
||||
# predicates:
|
||||
# - Path=/api/horses/**
|
||||
# filters:
|
||||
# - StripPrefix=1
|
||||
# - name: CircuitBreaker
|
||||
# args:
|
||||
# name: horsesCircuitBreaker
|
||||
# fallbackUri: forward:/fallback/horses
|
||||
|
||||
# ==============================================================
|
||||
# --- Events-Service-Integration (optional) ---
|
||||
# ==============================================================
|
||||
# - id: events-service-route
|
||||
# uri: lb://events-service
|
||||
# predicates:
|
||||
# - Path=/api/events/**
|
||||
# filters:
|
||||
# - StripPrefix=1
|
||||
# - name: CircuitBreaker
|
||||
# args:
|
||||
# name: eventsCircuitBreaker
|
||||
# fallbackUri: forward:/fallback/events
|
||||
|
||||
# ==============================================================
|
||||
# --- Masterdata-Service-Integration (optional) ---
|
||||
# ==============================================================
|
||||
# - id: masterdata-service-route
|
||||
# uri: lb://masterdata-service
|
||||
# predicates:
|
||||
# - Path=/api/masterdata/**
|
||||
# filters:
|
||||
# - StripPrefix=1
|
||||
# - name: CircuitBreaker
|
||||
# args:
|
||||
# name: masterdataCircuitBreaker
|
||||
# fallbackUri: forward:/fallback/masterdata
|
||||
|
||||
# ==============================================================
|
||||
# --- Auth-Service-Integration (optional) ---
|
||||
# ==============================================================
|
||||
# - id: auth-service-route
|
||||
# uri: lb://auth-service
|
||||
# predicates:
|
||||
# - Path=/api/auth/**
|
||||
# filters:
|
||||
# - StripPrefix=1
|
||||
# - name: CircuitBreaker
|
||||
# args:
|
||||
# name: authCircuitBreaker
|
||||
# fallbackUri: forward:/fallback/auth
|
||||
# Circuit Breaker Konfiguration
|
||||
resilience4j:
|
||||
circuitbreaker:
|
||||
configs:
|
||||
default:
|
||||
registerHealthIndicator: true
|
||||
slidingWindowSize: 100
|
||||
minimumNumberOfCalls: 20
|
||||
permittedNumberOfCallsInHalfOpenState: 3
|
||||
automaticTransitionFromOpenToHalfOpenEnabled: true
|
||||
waitDurationInOpenState: 5s
|
||||
failureRateThreshold: 50
|
||||
eventConsumerBufferSize: 10
|
||||
recordExceptions:
|
||||
- org.springframework.web.client.HttpServerErrorException
|
||||
- java.util.concurrent.TimeoutException
|
||||
- java.io.IOException
|
||||
instances:
|
||||
defaultCircuitBreaker:
|
||||
baseConfig: default
|
||||
membersCircuitBreaker:
|
||||
baseConfig: default
|
||||
slidingWindowSize: 50
|
||||
horsesCircuitBreaker:
|
||||
baseConfig: default
|
||||
slidingWindowSize: 50
|
||||
eventsCircuitBreaker:
|
||||
baseConfig: default
|
||||
slidingWindowSize: 75
|
||||
masterdataCircuitBreaker:
|
||||
baseConfig: default
|
||||
slidingWindowSize: 30
|
||||
authCircuitBreaker:
|
||||
baseConfig: default
|
||||
slidingWindowSize: 20
|
||||
failureRateThreshold: 30
|
||||
|
||||
# Management und Monitoring
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus,gateway,circuitbreakers
|
||||
base-path: /actuator
|
||||
cors:
|
||||
allowed-origins:
|
||||
- "https://*.meldestelle.at"
|
||||
- "http://localhost:*"
|
||||
allowed-methods: GET,POST
|
||||
allowed-headers: "*"
|
||||
allow-credentials: true
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
show-components: always
|
||||
probes:
|
||||
enabled: true
|
||||
metrics:
|
||||
access: unrestricted
|
||||
info:
|
||||
access: unrestricted
|
||||
prometheus:
|
||||
access: unrestricted
|
||||
gateway:
|
||||
access: unrestricted
|
||||
circuitbreakers:
|
||||
enabled: true
|
||||
metrics:
|
||||
distribution:
|
||||
percentiles-histogram:
|
||||
http.server.requests: true
|
||||
percentiles:
|
||||
http.server.requests: 0.5,0.90,0.95,0.99
|
||||
minimum-expected-value:
|
||||
http.server.requests: 1ms
|
||||
maximum-expected-value:
|
||||
http.server.requests: 30s
|
||||
tags:
|
||||
application: ${spring.application.name}
|
||||
environment: ${spring.profiles.active}
|
||||
instance: ${spring.cloud.consul.discovery.instance-id}
|
||||
service: gateway
|
||||
component: infrastructure
|
||||
gateway: api-gateway
|
||||
info:
|
||||
env:
|
||||
enabled: true
|
||||
git:
|
||||
mode: full
|
||||
build:
|
||||
enabled: true
|
||||
java:
|
||||
enabled: true
|
||||
# Tracing-Konfiguration - Aktiviert (Micrometer Tracing + Zipkin)
|
||||
tracing:
|
||||
enabled: ${TRACING_ENABLED:false}
|
||||
sampling:
|
||||
probability: ${TRACING_SAMPLING_PROBABILITY:1.0}
|
||||
zipkin:
|
||||
tracing:
|
||||
endpoint: ${ZIPKIN_TRACING_ENDPOINT:http://localhost:9411/api/v2/spans}
|
||||
# Reduziert Verbindungsfehler, wenn Zipkin nicht verfügbar ist
|
||||
connect-timeout: 1s
|
||||
read-timeout: 10s
|
||||
|
||||
# Erweiterte Logging-Konfiguration
|
||||
logging:
|
||||
level:
|
||||
org.springframework.cloud.gateway: INFO
|
||||
org.springframework.cloud.loadbalancer: DEBUG
|
||||
org.springframework.cloud.consul: INFO
|
||||
at.mocode.infrastructure.gateway: DEBUG
|
||||
io.github.resilience4j: INFO
|
||||
reactor.netty.http.client: INFO
|
||||
org.springframework.security: WARN
|
||||
org.springframework.web: INFO
|
||||
pattern:
|
||||
console: "%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr([%X{correlationId:-}]){yellow} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"
|
||||
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{correlationId:-}] %logger{36} - %msg%n"
|
||||
file:
|
||||
name: infrastructure/gateway/logs/gateway.log
|
||||
logback:
|
||||
rolling policy:
|
||||
clean-history-on-start: true
|
||||
max-file-size: 100MB
|
||||
total-size-cap: 1GB
|
||||
max-history: 30
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<property name="LOG_PATTERN" value="%d{ISO8601} %-5level [%X{traceId:-}:%X{spanId:-}] %logger{36} - %msg%n"/>
|
||||
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>${LOG_PATTERN}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="org.springframework" level="INFO"/>
|
||||
<logger name="org.springframework.web" level="INFO"/>
|
||||
<logger name="org.springframework.boot.actuate" level="INFO"/>
|
||||
<logger name="reactor.netty" level="WARN"/>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</root>
|
||||
</configuration>
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration scan="true">
|
||||
<property name="LOG_FILE" value="logs/gateway.log"/>
|
||||
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{correlationId}] %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_FILE}</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>logs/gateway.log.%d{yyyy-MM-dd}.%i.gz</fileNamePattern>
|
||||
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||
<maxFileSize>100MB</maxFileSize>
|
||||
</timeBasedFileNamingAndTriggeringPolicy>
|
||||
<maxHistory>30</maxHistory>
|
||||
<totalSizeCap>1GB</totalSizeCap>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{correlationId}] %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="org.springframework.cloud.gateway" level="INFO"/>
|
||||
<logger name="org.springframework.cloud.loadbalancer" level="DEBUG"/>
|
||||
<logger name="org.springframework.cloud.consul" level="INFO"/>
|
||||
<logger name="io.github.resilience4j" level="INFO"/>
|
||||
<logger name="reactor.netty.http.client" level="INFO"/>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
<appender-ref ref="FILE"/>
|
||||
</root>
|
||||
</configuration>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,432 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Meldestelle API Documentation</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #3498db;
|
||||
--secondary-color: #2c3e50;
|
||||
--accent-color: #e74c3c;
|
||||
--light-bg: #f5f5f5;
|
||||
--dark-bg: #2c3e50;
|
||||
--text-color: #333;
|
||||
--light-text: #f5f5f5;
|
||||
--border-color: #ddd;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--text-color);
|
||||
background-color: var(--light-bg);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
header {
|
||||
background-color: var(--dark-bg);
|
||||
color: var(--light-text);
|
||||
padding: 20px 0;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
header .container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
nav ul {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
nav ul li {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
nav ul li a {
|
||||
color: var(--light-text);
|
||||
text-decoration: none;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
nav ul li a:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.hero {
|
||||
background-color: var(--primary-color);
|
||||
color: var(--light-text);
|
||||
padding: 50px 0;
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 36px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
font-size: 18px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto 30px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
background-color: var(--secondary-color);
|
||||
color: var(--light-text);
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
text-decoration: none;
|
||||
transition: background-color 0.3s;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
font-size: 20px;
|
||||
margin-bottom: 15px;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.card p {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.card .endpoints {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.card .endpoints h4 {
|
||||
font-size: 16px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card .endpoints ul {
|
||||
list-style: none;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.card .endpoints ul li {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.card .endpoints .method {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.card .endpoints .get {
|
||||
background-color: #61affe;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card .endpoints .post {
|
||||
background-color: #49cc90;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card .endpoints .put {
|
||||
background-color: #fca130;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card .endpoints .delete {
|
||||
background-color: #f93e3e;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.resources {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.resource-card {
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.resource-card h3 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 10px;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.resource-card p {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
footer {
|
||||
background-color: var(--dark-bg);
|
||||
color: var(--light-text);
|
||||
padding: 20px 0;
|
||||
text-align: center;
|
||||
margin-top: 50px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="container">
|
||||
<div class="logo">Meldestelle API</div>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="#overview">Overview</a></li>
|
||||
<li><a href="#contexts">API Contexts</a></li>
|
||||
<li><a href="#resources">Resources</a></li>
|
||||
<li><a href="/swagger" target="_blank">Swagger UI</a></li>
|
||||
<li><a href="/openapi" target="_blank">OpenAPI Spec</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
<h1>Meldestelle Self-Contained Systems API</h1>
|
||||
<p>Unified API Gateway for all bounded contexts of the Austrian Equestrian Federation's Meldestelle system.</p>
|
||||
<div>
|
||||
<a href="/swagger" class="btn" target="_blank">Interactive API Documentation</a>
|
||||
<a href="/openapi" class="btn" target="_blank">OpenAPI Specification</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="container">
|
||||
<section id="overview" class="section">
|
||||
<h2>Overview</h2>
|
||||
<div class="card">
|
||||
<p>The Meldestelle API provides a unified interface to various bounded contexts while maintaining the independence of each context. This API Gateway aggregates all bounded context APIs and provides a single entry point for clients.</p>
|
||||
<p>The API follows REST principles and uses JSON for data exchange. All responses are wrapped in a consistent format using the <code>BaseDto</code> wrapper.</p>
|
||||
<p>Authentication is handled using JWT (JSON Web Token) based authentication. Most endpoints require authentication, which can be obtained by registering and logging in through the Authentication Context.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="contexts" class="section">
|
||||
<h2>API Contexts</h2>
|
||||
|
||||
<div class="card">
|
||||
<h3>Authentication Context</h3>
|
||||
<p>User authentication, registration, and profile management</p>
|
||||
<p><strong>Base Path:</strong> /auth</p>
|
||||
<div class="endpoints">
|
||||
<h4>Key Endpoints:</h4>
|
||||
<ul>
|
||||
<li><span class="method post">POST</span> /auth/register - User registration</li>
|
||||
<li><span class="method post">POST</span> /auth/login - User authentication</li>
|
||||
<li><span class="method get">GET</span> /auth/profile - Get user profile</li>
|
||||
<li><span class="method put">PUT</span> /auth/profile - Update user profile</li>
|
||||
<li><span class="method post">POST</span> /auth/change-password - Change password</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Members Context</h3>
|
||||
<p>Member registration, profile management, and membership administration</p>
|
||||
<p><strong>Base Path:</strong> /api/members</p>
|
||||
<div class="endpoints">
|
||||
<h4>Key Endpoints:</h4>
|
||||
<ul>
|
||||
<li><span class="method get">GET</span> /api/members - Get all members with pagination</li>
|
||||
<li><span class="method get">GET</span> /api/members/search - Search members by criteria</li>
|
||||
<li><span class="method get">GET</span> /api/members/{id} - Get member by ID</li>
|
||||
<li><span class="method post">POST</span> /api/members - Create new member</li>
|
||||
<li><span class="method put">PUT</span> /api/members/{id} - Update member information</li>
|
||||
<li><span class="method delete">DELETE</span> /api/members/{id} - Delete member (soft delete)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Master Data Context</h3>
|
||||
<p>Reference data management (countries, states, age classes, venues)</p>
|
||||
<p><strong>Base Path:</strong> /api/masterdata</p>
|
||||
<div class="endpoints">
|
||||
<h4>Key Endpoints:</h4>
|
||||
<ul>
|
||||
<li><span class="method get">GET</span> /api/masterdata/countries - Get all countries</li>
|
||||
<li><span class="method get">GET</span> /api/masterdata/countries/active - Get active countries</li>
|
||||
<li><span class="method get">GET</span> /api/masterdata/countries/{id} - Get country by ID</li>
|
||||
<li><span class="method post">POST</span> /api/masterdata/countries - Create country</li>
|
||||
<li><span class="method put">PUT</span> /api/masterdata/countries/{id} - Update country</li>
|
||||
<li><span class="method delete">DELETE</span> /api/masterdata/countries/{id} - Delete country</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Horse Registry Context</h3>
|
||||
<p>Horse registration, ownership, and pedigree management</p>
|
||||
<p><strong>Base Path:</strong> /api/horses</p>
|
||||
<div class="endpoints">
|
||||
<h4>Key Endpoints:</h4>
|
||||
<ul>
|
||||
<li><span class="method get">GET</span> /api/horses - Get all horses</li>
|
||||
<li><span class="method get">GET</span> /api/horses/active - Get active horses</li>
|
||||
<li><span class="method get">GET</span> /api/horses/{id} - Get horse by ID</li>
|
||||
<li><span class="method get">GET</span> /api/horses/search - Search horses by name</li>
|
||||
<li><span class="method post">POST</span> /api/horses - Create horse</li>
|
||||
<li><span class="method put">PUT</span> /api/horses/{id} - Update horse</li>
|
||||
<li><span class="method delete">DELETE</span> /api/horses/{id} - Delete horse</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Event Management Context</h3>
|
||||
<p>Event creation, management, and participant registration</p>
|
||||
<p><strong>Base Path:</strong> /api/events</p>
|
||||
<div class="endpoints">
|
||||
<h4>Key Endpoints:</h4>
|
||||
<ul>
|
||||
<li><span class="method get">GET</span> /api/events - Get all events</li>
|
||||
<li><span class="method get">GET</span> /api/events/stats - Get event statistics</li>
|
||||
<li><span class="method post">POST</span> /api/events - Create event</li>
|
||||
<li><span class="method get">GET</span> /api/events/{id} - Get event by ID</li>
|
||||
<li><span class="method put">PUT</span> /api/events/{id} - Update event</li>
|
||||
<li><span class="method delete">DELETE</span> /api/events/{id} - Delete event</li>
|
||||
<li><span class="method get">GET</span> /api/events/search - Search events</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="resources" class="section">
|
||||
<h2>Documentation Resources</h2>
|
||||
<div class="resources">
|
||||
<div class="resource-card">
|
||||
<h3>Swagger UI</h3>
|
||||
<p>Interactive documentation for exploring and testing the API endpoints.</p>
|
||||
<a href="/swagger" class="btn" target="_blank" aria-label="Open Swagger UI in new tab">Open Swagger UI</a>
|
||||
</div>
|
||||
<div class="resource-card">
|
||||
<h3>OpenAPI Specification</h3>
|
||||
<p>Raw OpenAPI 3.0.3 specification in YAML format for code generation or import into other tools.</p>
|
||||
<a href="/openapi" class="btn" target="_blank" aria-label="View OpenAPI specification in new tab">View OpenAPI Spec</a>
|
||||
</div>
|
||||
<div class="resource-card">
|
||||
<h3>Postman Collection</h3>
|
||||
<p>Comprehensive API collection covering all endpoints with pre-configured request examples.</p>
|
||||
<a href="/docs/postman/Meldestelle_API_Collection.json" class="btn" target="_blank" aria-label="Download Postman collection">Download Collection</a>
|
||||
</div>
|
||||
<div class="resource-card">
|
||||
<h3>Health Monitoring</h3>
|
||||
<p>Real-time health status and monitoring information for all downstream services.</p>
|
||||
<a href="/actuator/health" class="btn" target="_blank" aria-label="View health monitoring in new tab">View Health Status</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="monitoring" class="section">
|
||||
<h2>System Monitoring & Health</h2>
|
||||
<div class="card">
|
||||
<h3>Health Check Endpoints</h3>
|
||||
<p>The API Gateway provides comprehensive health monitoring for all downstream services:</p>
|
||||
<div class="endpoints">
|
||||
<h4>Monitoring Endpoints:</h4>
|
||||
<ul>
|
||||
<li><span class="method get">GET</span> /actuator/health - Comprehensive health status of all services</li>
|
||||
<li><span class="method get">GET</span> /actuator/metrics - System metrics and performance data</li>
|
||||
<li><span class="method get">GET</span> /actuator/info - Application information and build details</li>
|
||||
<li><span class="method get">GET</span> /actuator/prometheus - Prometheus-compatible metrics export</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p><strong>Health Indicator Features:</strong></p>
|
||||
<ul>
|
||||
<li>Monitors critical services: Members, Horses, Events, Masterdata, Auth</li>
|
||||
<li>Optional service monitoring: Ping service</li>
|
||||
<li>Circuit breaker status integration</li>
|
||||
<li>Service discovery status from Consul</li>
|
||||
<li>Detailed error reporting and status codes</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Getting Started</h2>
|
||||
<div class="card">
|
||||
<h3>Authentication</h3>
|
||||
<p>The API uses JWT (JSON Web Token) based authentication:</p>
|
||||
<ol>
|
||||
<li>Register a new user via <code>POST /auth/register</code></li>
|
||||
<li>Login with credentials via <code>POST /auth/login</code></li>
|
||||
<li>Extract the JWT token from the login response</li>
|
||||
<li>Include the token in the <code>Authorization</code> header: <code>Bearer <token></code></li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Response Format</h3>
|
||||
<p>All API responses follow a consistent format using the <code>BaseDto</code> wrapper:</p>
|
||||
<pre><code>{
|
||||
"success": true,
|
||||
"data": {
|
||||
"example": "Actual response data goes here"
|
||||
},
|
||||
"message": "Operation completed successfully",
|
||||
"timestamp": "2024-01-15T10:30:00Z"
|
||||
}</code></pre>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<p>© 2024 Meldestelle API. All rights reserved.</p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
+576
@@ -0,0 +1,576 @@
|
||||
{
|
||||
"info": {
|
||||
"name": "Meldestelle Self-Contained Systems API",
|
||||
"description": "Comprehensive API collection for the Austrian Equestrian Federation Meldestelle system. This collection covers all bounded contexts including Authentication, Master Data, and Horse Registry.",
|
||||
"version": "1.0.0",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"variable": [
|
||||
{
|
||||
"key": "baseUrl",
|
||||
"value": "http://localhost:8080",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "authToken",
|
||||
"value": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
"type": "bearer",
|
||||
"bearer": [
|
||||
{
|
||||
"key": "token",
|
||||
"value": "{{authToken}}",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "System Information",
|
||||
"item": [
|
||||
{
|
||||
"name": "API Gateway Info",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": [""]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Health Check",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/health",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["health"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "API Documentation",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["api"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Swagger UI",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/swagger",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["swagger"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Authentication Context",
|
||||
"item": [
|
||||
{
|
||||
"name": "User Registration",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"email\": \"test@example.com\",\n \"password\": \"SecurePassword123!\",\n \"firstName\": \"Test\",\n \"lastName\": \"User\",\n \"phoneNumber\": \"+43123456789\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/auth/register",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["auth", "register"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "User Login",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"email\": \"test@example.com\",\n \"password\": \"SecurePassword123!\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/auth/login",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["auth", "login"]
|
||||
}
|
||||
},
|
||||
"response": [],
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"exec": [
|
||||
"if (pm.response.code === 200) {",
|
||||
" const response = pm.response.json();",
|
||||
" if (response.success && response.data && response.data.token) {",
|
||||
" pm.collectionVariables.set('authToken', response.data.token);",
|
||||
" console.log('Auth token saved:', response.data.token);",
|
||||
" }",
|
||||
"}"
|
||||
],
|
||||
"type": "text/javascript"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Get User Profile",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{authToken}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/auth/profile",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["auth", "profile"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Update User Profile",
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{authToken}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"firstName\": \"Updated\",\n \"lastName\": \"User\",\n \"phoneNumber\": \"+43987654321\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/auth/profile",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["auth", "profile"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Change Password",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{authToken}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"currentPassword\": \"SecurePassword123!\",\n \"newPassword\": \"NewSecurePassword456!\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/auth/change-password",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["auth", "change-password"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Master Data Context",
|
||||
"item": [
|
||||
{
|
||||
"name": "Countries",
|
||||
"item": [
|
||||
{
|
||||
"name": "Get All Countries",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/masterdata/countries",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["api", "masterdata", "countries"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Get Active Countries",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/masterdata/countries/active",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["api", "masterdata", "countries", "active"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Get Country by ID",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/masterdata/countries/{{countryId}}",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["api", "masterdata", "countries", "{{countryId}}"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Get Country by ISO Code",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/masterdata/countries/iso/AT",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["api", "masterdata", "countries", "iso", "AT"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Create Country",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{authToken}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"isoAlpha2Code\": \"TS\",\n \"isoAlpha3Code\": \"TST\",\n \"isoNumerischerCode\": \"999\",\n \"nameDeutsch\": \"Testland\",\n \"nameEnglisch\": \"Testland\",\n \"istEuMitglied\": false,\n \"istEwrMitglied\": false,\n \"istAktiv\": true,\n \"sortierReihenfolge\": 999\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/masterdata/countries",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["api", "masterdata", "countries"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Update Country",
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{authToken}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"isoAlpha2Code\": \"TS\",\n \"isoAlpha3Code\": \"TST\",\n \"isoNumerischerCode\": \"999\",\n \"nameDeutsch\": \"Updated Testland\",\n \"nameEnglisch\": \"Updated Testland\",\n \"istEuMitglied\": false,\n \"istEwrMitglied\": false,\n \"istAktiv\": true,\n \"sortierReihenfolge\": 999\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/masterdata/countries/{{countryId}}",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["api", "masterdata", "countries", "{{countryId}}"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Delete Country",
|
||||
"request": {
|
||||
"method": "DELETE",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{authToken}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/masterdata/countries/{{countryId}}",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["api", "masterdata", "countries", "{{countryId}}"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Horse Registry Context",
|
||||
"item": [
|
||||
{
|
||||
"name": "Get All Horses",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{authToken}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/horses",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["api", "horses"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Get Active Horses",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{authToken}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/horses/active",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["api", "horses", "active"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Get Horse by ID",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{authToken}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/horses/{{horseId}}",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["api", "horses", "{{horseId}}"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Search Horses by Name",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{authToken}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/horses/search?name=Test&limit=10",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["api", "horses", "search"],
|
||||
"query": [
|
||||
{
|
||||
"key": "name",
|
||||
"value": "Test"
|
||||
},
|
||||
{
|
||||
"key": "limit",
|
||||
"value": "10"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Get Horses by Owner",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{authToken}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/horses/owner/{{ownerId}}",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["api", "horses", "owner", "{{ownerId}}"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Create Horse",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{authToken}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"pferdeName\": \"Test Horse\",\n \"geschlecht\": \"WALLACH\",\n \"geburtsdatum\": \"2020-05-15\",\n \"rasse\": \"Warmblut\",\n \"farbe\": \"Braun\",\n \"zuechterName\": \"Test Breeder\",\n \"stockmass\": 165,\n \"istAktiv\": true,\n \"bemerkungen\": \"Test horse for API demonstration\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/horses",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["api", "horses"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Update Horse",
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{authToken}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"pferdeName\": \"Updated Test Horse\",\n \"geschlecht\": \"WALLACH\",\n \"geburtsdatum\": \"2020-05-15\",\n \"rasse\": \"Warmblut\",\n \"farbe\": \"Dunkelbraun\",\n \"zuechterName\": \"Updated Test Breeder\",\n \"stockmass\": 167,\n \"istAktiv\": true,\n \"bemerkungen\": \"Updated test horse for API demonstration\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/horses/{{horseId}}",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["api", "horses", "{{horseId}}"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Delete Horse",
|
||||
"request": {
|
||||
"method": "DELETE",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{authToken}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/horses/{{horseId}}",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["api", "horses", "{{horseId}}"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Batch Delete Horses",
|
||||
"request": {
|
||||
"method": "DELETE",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{authToken}}"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"horseIds\": [\"{{horseId1}}\", \"{{horseId2}}\"],\n \"forceDelete\": false\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/horses/batch",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["api", "horses", "batch"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"name": "Get Horse Statistics",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [
|
||||
{
|
||||
"key": "Authorization",
|
||||
"value": "Bearer {{authToken}}"
|
||||
}
|
||||
],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/horses/stats",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["api", "horses", "stats"]
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user