Umbau zu SCS
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.multiplatform)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvm {
|
||||
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)
|
||||
mainRun {
|
||||
mainClass.set("at.mocode.gateway.ApplicationKt")
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(project(":shared-kernel"))
|
||||
implementation(project(":master-data"))
|
||||
implementation(project(":member-management"))
|
||||
implementation(project(":horse-registry"))
|
||||
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
implementation(libs.uuid)
|
||||
}
|
||||
|
||||
commonTest.dependencies {
|
||||
implementation(libs.kotlin.test)
|
||||
implementation(libs.kotlinx.coroutines.test)
|
||||
}
|
||||
|
||||
jvmMain.dependencies {
|
||||
implementation(libs.ktor.server.core)
|
||||
implementation(libs.ktor.server.netty)
|
||||
implementation(libs.ktor.server.contentNegotiation)
|
||||
implementation(libs.ktor.server.cors)
|
||||
implementation(libs.ktor.server.auth)
|
||||
implementation(libs.ktor.server.authJwt)
|
||||
implementation(libs.ktor.server.callLogging)
|
||||
implementation(libs.ktor.server.statusPages)
|
||||
implementation(libs.ktor.server.serializationKotlinxJson)
|
||||
implementation(libs.logback)
|
||||
}
|
||||
|
||||
jvmTest.dependencies {
|
||||
implementation(libs.ktor.server.tests)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package at.mocode.gateway
|
||||
|
||||
import at.mocode.gateway.config.configureDatabase
|
||||
import at.mocode.gateway.config.configureSerialization
|
||||
import at.mocode.gateway.config.configureMonitoring
|
||||
import at.mocode.gateway.config.configureSecurity
|
||||
import at.mocode.gateway.routing.configureRouting
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.engine.*
|
||||
import io.ktor.server.netty.*
|
||||
|
||||
/**
|
||||
* Main application entry point for the Self-Contained Systems API Gateway.
|
||||
*
|
||||
* This gateway aggregates all bounded context APIs into a unified interface
|
||||
* while maintaining the independence of each context.
|
||||
*/
|
||||
fun main() {
|
||||
embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module)
|
||||
.start(wait = true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Main application module configuration.
|
||||
*
|
||||
* Configures all necessary components for the API Gateway including:
|
||||
* - Database connections for all contexts
|
||||
* - Serialization and content negotiation
|
||||
* - Security and authentication
|
||||
* - Monitoring and logging
|
||||
* - Route aggregation from all bounded contexts
|
||||
*/
|
||||
fun Application.module() {
|
||||
// Configure core components
|
||||
configureDatabase()
|
||||
configureSerialization()
|
||||
configureMonitoring()
|
||||
configureSecurity()
|
||||
|
||||
// Configure routing - aggregates all bounded context routes
|
||||
configureRouting()
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package at.mocode.gateway.config
|
||||
|
||||
import io.ktor.server.application.*
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.jetbrains.exposed.sql.SchemaUtils
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
|
||||
/**
|
||||
* Database configuration for the API Gateway.
|
||||
*
|
||||
* Sets up database connections and schema initialization for all bounded contexts.
|
||||
*/
|
||||
fun Application.configureDatabase() {
|
||||
val databaseUrl = environment.config.propertyOrNull("database.url")?.getString()
|
||||
?: "jdbc:postgresql://localhost:5432/meldestelle"
|
||||
val databaseUser = environment.config.propertyOrNull("database.user")?.getString()
|
||||
?: "meldestelle_user"
|
||||
val databasePassword = environment.config.propertyOrNull("database.password")?.getString()
|
||||
?: "meldestelle_password"
|
||||
|
||||
// Initialize database connection
|
||||
Database.connect(
|
||||
url = databaseUrl,
|
||||
driver = "org.postgresql.Driver",
|
||||
user = databaseUser,
|
||||
password = databasePassword
|
||||
)
|
||||
|
||||
// Initialize database schemas for all contexts
|
||||
transaction {
|
||||
// Import table definitions from all contexts
|
||||
try {
|
||||
// Master Data Context tables
|
||||
SchemaUtils.createMissingTablesAndColumns(
|
||||
at.mocode.masterdata.infrastructure.repository.LandTable
|
||||
)
|
||||
|
||||
// Member Management Context tables
|
||||
SchemaUtils.createMissingTablesAndColumns(
|
||||
at.mocode.members.infrastructure.repository.PersonTable,
|
||||
at.mocode.members.infrastructure.repository.VereinTable
|
||||
)
|
||||
|
||||
// Horse Registry Context tables
|
||||
SchemaUtils.createMissingTablesAndColumns(
|
||||
at.mocode.horses.infrastructure.repository.HorseTable
|
||||
)
|
||||
|
||||
log.info("Database schemas initialized successfully")
|
||||
} catch (e: Exception) {
|
||||
log.error("Failed to initialize database schemas: ${e.message}")
|
||||
// In production, you might want to fail fast here
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package at.mocode.gateway.config
|
||||
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.plugins.calllogging.*
|
||||
import io.ktor.server.plugins.statuspages.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.response.*
|
||||
import at.mocode.dto.base.BaseDto
|
||||
import org.slf4j.event.Level
|
||||
|
||||
/**
|
||||
* Monitoring and logging configuration for the API Gateway.
|
||||
*
|
||||
* Configures request logging, error handling, and status pages.
|
||||
*/
|
||||
fun Application.configureMonitoring() {
|
||||
install(CallLogging) {
|
||||
level = Level.INFO
|
||||
filter { call -> call.request.path().startsWith("/api") }
|
||||
format { call ->
|
||||
val status = call.response.status()
|
||||
val httpMethod = call.request.httpMethod.value
|
||||
val userAgent = call.request.headers["User-Agent"]
|
||||
"$status: $httpMethod ${call.request.path()} - $userAgent"
|
||||
}
|
||||
}
|
||||
|
||||
install(StatusPages) {
|
||||
exception<Throwable> { call, cause ->
|
||||
call.application.log.error("Unhandled exception", cause)
|
||||
call.respond(
|
||||
HttpStatusCode.InternalServerError,
|
||||
BaseDto.error<Any>("Internal server error: ${cause.message}")
|
||||
)
|
||||
}
|
||||
|
||||
status(HttpStatusCode.NotFound) { call, status ->
|
||||
call.respond(
|
||||
status,
|
||||
BaseDto.error<Any>("Endpoint not found: ${call.request.path()}")
|
||||
)
|
||||
}
|
||||
|
||||
status(HttpStatusCode.Unauthorized) { call, status ->
|
||||
call.respond(
|
||||
status,
|
||||
BaseDto.error<Any>("Authentication required")
|
||||
)
|
||||
}
|
||||
|
||||
status(HttpStatusCode.Forbidden) { call, status ->
|
||||
call.respond(
|
||||
status,
|
||||
BaseDto.error<Any>("Access forbidden")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package at.mocode.gateway.config
|
||||
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.plugins.cors.routing.*
|
||||
import io.ktor.http.*
|
||||
|
||||
/**
|
||||
* Security configuration for the API Gateway.
|
||||
*
|
||||
* Configures CORS, authentication, and other security-related settings.
|
||||
*/
|
||||
fun Application.configureSecurity() {
|
||||
install(CORS) {
|
||||
allowMethod(HttpMethod.Options)
|
||||
allowMethod(HttpMethod.Put)
|
||||
allowMethod(HttpMethod.Delete)
|
||||
allowMethod(HttpMethod.Patch)
|
||||
allowHeader(HttpHeaders.Authorization)
|
||||
allowHeader(HttpHeaders.ContentType)
|
||||
allowHeader("X-Requested-With")
|
||||
|
||||
// Allow requests from common development origins
|
||||
allowHost("localhost:3000")
|
||||
allowHost("localhost:8080")
|
||||
allowHost("127.0.0.1:3000")
|
||||
allowHost("127.0.0.1:8080")
|
||||
|
||||
// In production, configure specific allowed origins
|
||||
anyHost() // This should be restricted in production
|
||||
}
|
||||
|
||||
// TODO: Add JWT authentication configuration
|
||||
// install(Authentication) {
|
||||
// jwt("auth-jwt") {
|
||||
// // JWT configuration
|
||||
// }
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package at.mocode.gateway.config
|
||||
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.plugins.contentnegotiation.*
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
* Serialization configuration for the API Gateway.
|
||||
*
|
||||
* Configures JSON serialization settings that are consistent across all bounded contexts.
|
||||
*/
|
||||
fun Application.configureSerialization() {
|
||||
install(ContentNegotiation) {
|
||||
json(Json {
|
||||
prettyPrint = true
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
encodeDefaults = true
|
||||
explicitNulls = false
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package at.mocode.gateway.routing
|
||||
|
||||
import at.mocode.dto.base.BaseDto
|
||||
import at.mocode.horses.infrastructure.api.HorseController
|
||||
import at.mocode.horses.infrastructure.repository.HorseRepositoryImpl
|
||||
import at.mocode.masterdata.application.usecase.CreateCountryUseCase
|
||||
import at.mocode.masterdata.application.usecase.GetCountryUseCase
|
||||
import at.mocode.masterdata.infrastructure.api.CountryController
|
||||
import at.mocode.masterdata.infrastructure.repository.LandRepositoryImpl
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Main routing configuration for the API Gateway.
|
||||
*
|
||||
* This aggregates routes from all bounded contexts into a unified API
|
||||
* while maintaining the independence and self-contained nature of each context.
|
||||
*/
|
||||
fun Application.configureRouting() {
|
||||
|
||||
// Initialize repository implementations for each context
|
||||
val landRepository = LandRepositoryImpl()
|
||||
val horseRepository = HorseRepositoryImpl()
|
||||
|
||||
// Initialize use cases
|
||||
val getCountryUseCase = GetCountryUseCase(landRepository)
|
||||
val createCountryUseCase = CreateCountryUseCase(landRepository)
|
||||
|
||||
// Initialize controllers for each bounded context
|
||||
val countryController = CountryController(getCountryUseCase, createCountryUseCase)
|
||||
val horseController = HorseController(horseRepository)
|
||||
|
||||
routing {
|
||||
|
||||
// Root endpoint - API Gateway health check and info
|
||||
get("/") {
|
||||
call.respond(HttpStatusCode.OK, BaseDto.success(
|
||||
ApiGatewayInfo(
|
||||
name = "Meldestelle API Gateway",
|
||||
version = "1.0.0",
|
||||
description = "Self-Contained Systems API Gateway for Austrian Equestrian Federation",
|
||||
availableContexts = listOf(
|
||||
"master-data",
|
||||
"horse-registry"
|
||||
),
|
||||
endpoints = mapOf(
|
||||
"master-data" to "/api/masterdata/*",
|
||||
"horse-registry" to "/api/horses/*"
|
||||
)
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
// Health check endpoint
|
||||
get("/health") {
|
||||
call.respond(HttpStatusCode.OK, BaseDto.success(
|
||||
HealthStatus(
|
||||
status = "UP",
|
||||
contexts = mapOf(
|
||||
"master-data" to "UP",
|
||||
"horse-registry" to "UP"
|
||||
)
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
// API documentation endpoint
|
||||
get("/api") {
|
||||
call.respond(HttpStatusCode.OK, BaseDto.success(
|
||||
ApiDocumentation(
|
||||
title = "Meldestelle Self-Contained Systems API",
|
||||
description = "Unified API Gateway for all bounded contexts",
|
||||
contexts = listOf(
|
||||
ContextInfo(
|
||||
name = "Master Data Context",
|
||||
path = "/api/masterdata",
|
||||
description = "Reference data management (countries, states, age classes, venues)"
|
||||
),
|
||||
ContextInfo(
|
||||
name = "Horse Registry Context",
|
||||
path = "/api/horses",
|
||||
description = "Horse registration, ownership, and pedigree management"
|
||||
)
|
||||
)
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
// Configure routes for each bounded context
|
||||
|
||||
// Master Data Context Routes
|
||||
countryController.configureRoutes(this)
|
||||
|
||||
// Horse Registry Context Routes
|
||||
horseController.configureRoutes(this)
|
||||
|
||||
// Catch-all for undefined routes
|
||||
route("{...}") {
|
||||
handle {
|
||||
call.respond(
|
||||
HttpStatusCode.NotFound,
|
||||
BaseDto.error<Any>("Endpoint not found. Check /api for available endpoints.")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API Gateway information DTO.
|
||||
*/
|
||||
@Serializable
|
||||
data class ApiGatewayInfo(
|
||||
val name: String,
|
||||
val version: String,
|
||||
val description: String,
|
||||
val availableContexts: List<String>,
|
||||
val endpoints: Map<String, String>
|
||||
)
|
||||
|
||||
/**
|
||||
* Health status DTO.
|
||||
*/
|
||||
@Serializable
|
||||
data class HealthStatus(
|
||||
val status: String,
|
||||
val contexts: Map<String, String>
|
||||
)
|
||||
|
||||
/**
|
||||
* API documentation DTO.
|
||||
*/
|
||||
@Serializable
|
||||
data class ApiDocumentation(
|
||||
val title: String,
|
||||
val description: String,
|
||||
val contexts: List<ContextInfo>
|
||||
)
|
||||
|
||||
/**
|
||||
* Context information DTO.
|
||||
*/
|
||||
@Serializable
|
||||
data class ContextInfo(
|
||||
val name: String,
|
||||
val path: String,
|
||||
val description: String
|
||||
)
|
||||
Reference in New Issue
Block a user