(fix) Umbau zu SCS

### 1.1 OpenAPI-Dokumentation implementieren
- Swagger/OpenAPI in bestehenden Ktor-Diensten integrieren
- Zentrale API-Dokumentationsseite im API-Gateway erstellen
- CI/CD-Pipeline um automatische API-Dokumentationsgenerierung erweitern
- Entwickler-Guidelines für API-Dokumentation erstellen
This commit is contained in:
stefan
2025-07-21 13:45:58 +02:00
parent 77953c18f6
commit 81e40e70fc
13 changed files with 3192 additions and 72 deletions
+95
View File
@@ -1,6 +1,101 @@
plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.kotlin.serialization)
id("org.openapi.generator") version "7.3.0" // Updated to latest version
}
// Get project version for documentation versioning
val projectVersion = project.version.toString()
// Configure OpenAPI Generator
openApiGenerate {
generatorName.set("html2")
inputSpec.set("$projectDir/src/jvmMain/resources/openapi/documentation.yaml")
outputDir.set("$projectDir/build/generated-docs")
// Configure HTML2 generator options
configOptions.set(mapOf(
"infoUrl" to "https://meldestelle.at",
"infoEmail" to "support@meldestelle.at",
"title" to "Meldestelle API Documentation v$projectVersion"
))
// Validate OpenAPI specification before generation
validateSpec.set(true)
}
// Task to validate OpenAPI specification
tasks.register("validateOpenApi") {
group = "documentation"
description = "Validates the OpenAPI specification"
doLast {
// Use the OpenAPI Generator's validate task
tasks.named("openApiValidate").get().actions.forEach { action ->
action.execute(tasks.named("openApiValidate").get())
}
println("OpenAPI specification validated successfully")
}
}
// Task to generate API documentation
tasks.register("generateApiDocs") {
group = "documentation"
description = "Generates API documentation from OpenAPI specification"
doFirst {
// Validate the OpenAPI specification before generating documentation
println("Validating OpenAPI specification...")
tasks.named("validateOpenApi").get().actions.forEach { action ->
action.execute(tasks.named("validateOpenApi").get())
}
}
doLast {
try {
// Ensure the output directory exists
mkdir("$projectDir/build/docs")
// Create version directory for documentation versioning
val docsVersionDir = file("$projectDir/src/jvmMain/resources/static/docs/v$projectVersion")
mkdir(docsVersionDir)
// Copy all generated documentation files to the static docs directory
copy {
from("$projectDir/build/generated-docs")
into("$projectDir/src/jvmMain/resources/static/docs")
include("**/*")
}
// Also copy to the versioned directory
copy {
from("$projectDir/build/generated-docs")
into(docsVersionDir)
include("**/*")
}
// Create a version.json file with version information
val timestamp = System.currentTimeMillis()
file("$projectDir/src/jvmMain/resources/static/docs/version.json").writeText("""
{
"version": "$projectVersion",
"generatedAt": "$timestamp",
"latestVersionUrl": "/docs/v$projectVersion"
}
""".trimIndent())
println("API documentation generated successfully at:")
println("- Latest: $projectDir/src/jvmMain/resources/static/docs/")
println("- Versioned: $projectDir/src/jvmMain/resources/static/docs/v$projectVersion/")
} catch (e: Exception) {
println("Error generating API documentation: ${e.message}")
e.printStackTrace()
throw e
}
}
// This task depends on the openApiGenerate task
dependsOn("openApiGenerate")
}
kotlin {
@@ -10,9 +10,12 @@ import io.ktor.server.routing.*
*
* This module configures the OpenAPI specification generation and Swagger UI
* for the API Gateway, providing comprehensive API documentation.
*
* The OpenAPI specification is loaded from a static YAML file located at:
* resources/openapi/documentation.yaml
*/
fun Application.configureOpenApi() {
// Configure OpenAPI using a static file
// Configure OpenAPI endpoint using the static YAML file
routing {
// Serve the OpenAPI specification from a file
openAPI(path = "openapi", swaggerFile = "openapi/documentation.yaml") {
@@ -1,9 +1,13 @@
package at.mocode.gateway
import at.mocode.gateway.config.configureOpenApi
import at.mocode.gateway.config.configureSwagger
import at.mocode.gateway.routing.docRoutes
import at.mocode.shared.config.AppConfig
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.http.content.*
import io.ktor.server.plugins.calllogging.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.cors.routing.*
@@ -41,6 +45,10 @@ fun Application.module() {
install(CallLogging)
}
// OpenAPI und Swagger UI konfigurieren
configureOpenApi()
configureSwagger()
routing {
// Hauptrouten
get("/") {
@@ -49,5 +57,14 @@ fun Application.module() {
ContentType.Text.Plain
)
}
// Static resources for documentation
static("/docs") {
resources("static/docs")
defaultResource("static/docs/index.html")
}
// API Documentation routes
docRoutes()
}
}
@@ -0,0 +1,76 @@
package at.mocode.gateway.routing
import at.mocode.dto.base.ApiResponse
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
import java.io.File
/**
* Sets up routes for API documentation
*/
fun Routing.docRoutes() {
// Central API documentation endpoint - HTML version
get("/api") {
call.respondRedirect("/docs", permanent = false)
}
// JSON API documentation endpoint for backward compatibility
get("/api/json") {
val apiDocumentation = ApiDocumentationData(
title = "Meldestelle Self-Contained Systems API",
description = "Unified API Gateway for all bounded contexts",
contexts = listOf(
ApiContext(
name = "Authentication Context",
path = "/auth",
description = "User authentication, registration, and profile management"
),
ApiContext(
name = "Master Data Context",
path = "/api/masterdata",
description = "Reference data management (countries, states, age classes, venues)"
),
ApiContext(
name = "Horse Registry Context",
path = "/api/horses",
description = "Horse registration, ownership, and pedigree management"
),
ApiContext(
name = "Event Management Context",
path = "/api/events",
description = "Event creation, management, and participant registration"
)
)
)
call.respond(
ApiResponse.success(
data = apiDocumentation,
message = "API documentation retrieved successfully"
)
)
}
}
/**
* Data class for API documentation response
*/
@Serializable
data class ApiDocumentationData(
val title: String,
val description: String,
val contexts: List<ApiContext>
)
/**
* Data class for API context information
*/
@Serializable
data class ApiContext(
val name: String,
val path: String,
val description: String
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,385 @@
<!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>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">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">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">Download Collection</a>
</div>
</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 &lt;token&gt;</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>&copy; 2024 Meldestelle API. All rights reserved.</p>
</div>
</footer>
</body>
</html>