(fix) Umbau zu SCS
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
package at.mocode.gateway.config
|
||||
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.auth.jwt.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.routing.*
|
||||
import io.ktor.util.pipeline.*
|
||||
import at.mocode.enums.RolleE
|
||||
import at.mocode.enums.BerechtigungE
|
||||
|
||||
/**
|
||||
* Authorization configuration and middleware for role-based access control.
|
||||
*
|
||||
* Provides utilities for checking user roles and permissions on protected endpoints.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Enum representing user roles in the system.
|
||||
*/
|
||||
enum class UserRole {
|
||||
ADMIN,
|
||||
VEREINS_ADMIN,
|
||||
FUNKTIONAER,
|
||||
REITER,
|
||||
TRAINER,
|
||||
RICHTER,
|
||||
TIERARZT,
|
||||
ZUSCHAUER,
|
||||
GAST
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum representing permissions in the system.
|
||||
*/
|
||||
enum class Permission {
|
||||
// Person management
|
||||
PERSON_READ,
|
||||
PERSON_CREATE,
|
||||
PERSON_UPDATE,
|
||||
PERSON_DELETE,
|
||||
|
||||
// Club management
|
||||
VEREIN_READ,
|
||||
VEREIN_CREATE,
|
||||
VEREIN_UPDATE,
|
||||
VEREIN_DELETE,
|
||||
|
||||
// Event management
|
||||
VERANSTALTUNG_READ,
|
||||
VERANSTALTUNG_CREATE,
|
||||
VERANSTALTUNG_UPDATE,
|
||||
VERANSTALTUNG_DELETE,
|
||||
|
||||
// Horse management
|
||||
PFERD_READ,
|
||||
PFERD_CREATE,
|
||||
PFERD_UPDATE,
|
||||
PFERD_DELETE,
|
||||
|
||||
// Master data management
|
||||
STAMMDATEN_READ,
|
||||
STAMMDATEN_UPDATE,
|
||||
|
||||
// System administration
|
||||
SYSTEM_ADMIN,
|
||||
BENUTZER_VERWALTEN,
|
||||
ROLLEN_VERWALTEN
|
||||
}
|
||||
|
||||
/**
|
||||
* Data class representing user authorization context.
|
||||
*/
|
||||
data class UserAuthContext(
|
||||
val userId: String,
|
||||
val username: String,
|
||||
val roles: List<UserRole>,
|
||||
val permissions: List<Permission>
|
||||
)
|
||||
|
||||
/**
|
||||
* Maps domain role enum to authorization role enum.
|
||||
*/
|
||||
private fun mapDomainRoleToUserRole(domainRole: RolleE): UserRole {
|
||||
return when (domainRole) {
|
||||
RolleE.ADMIN -> UserRole.ADMIN
|
||||
RolleE.VEREINS_ADMIN -> UserRole.VEREINS_ADMIN
|
||||
RolleE.FUNKTIONAER -> UserRole.FUNKTIONAER
|
||||
RolleE.REITER -> UserRole.REITER
|
||||
RolleE.TRAINER -> UserRole.TRAINER
|
||||
RolleE.RICHTER -> UserRole.RICHTER
|
||||
RolleE.TIERARZT -> UserRole.TIERARZT
|
||||
RolleE.ZUSCHAUER -> UserRole.ZUSCHAUER
|
||||
RolleE.GAST -> UserRole.GAST
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps domain permission enum to authorization permission enum.
|
||||
*/
|
||||
private fun mapDomainPermissionToPermission(domainPermission: BerechtigungE): Permission {
|
||||
return when (domainPermission) {
|
||||
BerechtigungE.PERSON_READ -> Permission.PERSON_READ
|
||||
BerechtigungE.PERSON_CREATE -> Permission.PERSON_CREATE
|
||||
BerechtigungE.PERSON_UPDATE -> Permission.PERSON_UPDATE
|
||||
BerechtigungE.PERSON_DELETE -> Permission.PERSON_DELETE
|
||||
BerechtigungE.VEREIN_READ -> Permission.VEREIN_READ
|
||||
BerechtigungE.VEREIN_CREATE -> Permission.VEREIN_CREATE
|
||||
BerechtigungE.VEREIN_UPDATE -> Permission.VEREIN_UPDATE
|
||||
BerechtigungE.VEREIN_DELETE -> Permission.VEREIN_DELETE
|
||||
BerechtigungE.VERANSTALTUNG_READ -> Permission.VERANSTALTUNG_READ
|
||||
BerechtigungE.VERANSTALTUNG_CREATE -> Permission.VERANSTALTUNG_CREATE
|
||||
BerechtigungE.VERANSTALTUNG_UPDATE -> Permission.VERANSTALTUNG_UPDATE
|
||||
BerechtigungE.VERANSTALTUNG_DELETE -> Permission.VERANSTALTUNG_DELETE
|
||||
BerechtigungE.PFERD_READ -> Permission.PFERD_READ
|
||||
BerechtigungE.PFERD_CREATE -> Permission.PFERD_CREATE
|
||||
BerechtigungE.PFERD_UPDATE -> Permission.PFERD_UPDATE
|
||||
BerechtigungE.PFERD_DELETE -> Permission.PFERD_DELETE
|
||||
BerechtigungE.STAMMDATEN_READ -> Permission.STAMMDATEN_READ
|
||||
BerechtigungE.STAMMDATEN_UPDATE -> Permission.STAMMDATEN_UPDATE
|
||||
BerechtigungE.SYSTEM_ADMIN -> Permission.SYSTEM_ADMIN
|
||||
BerechtigungE.BENUTZER_VERWALTEN -> Permission.BENUTZER_VERWALTEN
|
||||
BerechtigungE.ROLLEN_VERWALTEN -> Permission.ROLLEN_VERWALTEN
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension function to get user authorization context from JWT principal.
|
||||
*/
|
||||
fun JWTPrincipal.getUserAuthContext(): UserAuthContext? {
|
||||
val userId = getClaim("userId", String::class) ?: return null
|
||||
val username = getClaim("username", String::class) ?: return null
|
||||
|
||||
// Get roles and permissions from JWT token
|
||||
val domainRoles = getClaim("roles", Array<RolleE>::class)?.toList() ?: emptyList()
|
||||
val domainPermissions = getClaim("permissions", Array<BerechtigungE>::class)?.toList() ?: emptyList()
|
||||
|
||||
// Map domain enums to authorization enums
|
||||
val roles = domainRoles.map { mapDomainRoleToUserRole(it) }
|
||||
val permissions = domainPermissions.map { mapDomainPermissionToPermission(it) }
|
||||
|
||||
return UserAuthContext(
|
||||
userId = userId,
|
||||
username = username,
|
||||
roles = roles,
|
||||
permissions = permissions
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps roles to their corresponding permissions.
|
||||
*/
|
||||
private fun getRolePermissions(roles: List<UserRole>): List<Permission> {
|
||||
val permissions = mutableSetOf<Permission>()
|
||||
|
||||
roles.forEach { role ->
|
||||
when (role) {
|
||||
UserRole.ADMIN -> {
|
||||
permissions.addAll(Permission.values())
|
||||
}
|
||||
UserRole.VEREINS_ADMIN -> {
|
||||
permissions.addAll(listOf(
|
||||
Permission.PERSON_READ, Permission.PERSON_CREATE, Permission.PERSON_UPDATE,
|
||||
Permission.VEREIN_READ, Permission.VEREIN_UPDATE,
|
||||
Permission.PFERD_READ, Permission.PFERD_CREATE, Permission.PFERD_UPDATE,
|
||||
Permission.STAMMDATEN_READ
|
||||
))
|
||||
}
|
||||
UserRole.FUNKTIONAER -> {
|
||||
permissions.addAll(listOf(
|
||||
Permission.PERSON_READ,
|
||||
Permission.VEREIN_READ,
|
||||
Permission.VERANSTALTUNG_READ, Permission.VERANSTALTUNG_CREATE, Permission.VERANSTALTUNG_UPDATE,
|
||||
Permission.PFERD_READ,
|
||||
Permission.STAMMDATEN_READ
|
||||
))
|
||||
}
|
||||
UserRole.TRAINER -> {
|
||||
permissions.addAll(listOf(
|
||||
Permission.PERSON_READ,
|
||||
Permission.VEREIN_READ,
|
||||
Permission.VERANSTALTUNG_READ,
|
||||
Permission.PFERD_READ,
|
||||
Permission.STAMMDATEN_READ
|
||||
))
|
||||
}
|
||||
UserRole.REITER -> {
|
||||
permissions.addAll(listOf(
|
||||
Permission.PERSON_READ,
|
||||
Permission.VEREIN_READ,
|
||||
Permission.VERANSTALTUNG_READ,
|
||||
Permission.PFERD_READ,
|
||||
Permission.STAMMDATEN_READ
|
||||
))
|
||||
}
|
||||
UserRole.RICHTER -> {
|
||||
permissions.addAll(listOf(
|
||||
Permission.PERSON_READ,
|
||||
Permission.VEREIN_READ,
|
||||
Permission.VERANSTALTUNG_READ,
|
||||
Permission.PFERD_READ,
|
||||
Permission.STAMMDATEN_READ
|
||||
))
|
||||
}
|
||||
UserRole.TIERARZT -> {
|
||||
permissions.addAll(listOf(
|
||||
Permission.PERSON_READ,
|
||||
Permission.PFERD_READ,
|
||||
Permission.STAMMDATEN_READ
|
||||
))
|
||||
}
|
||||
UserRole.ZUSCHAUER -> {
|
||||
permissions.addAll(listOf(
|
||||
Permission.VERANSTALTUNG_READ,
|
||||
Permission.STAMMDATEN_READ
|
||||
))
|
||||
}
|
||||
UserRole.GAST -> {
|
||||
permissions.addAll(listOf(
|
||||
Permission.STAMMDATEN_READ
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return permissions.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Route extension function to require specific roles.
|
||||
*/
|
||||
fun Route.requireRoles(vararg roles: UserRole, build: Route.() -> Unit): Route {
|
||||
val route = createChild(object : RouteSelector() {
|
||||
override suspend fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation {
|
||||
return RouteSelectorEvaluation.Constant
|
||||
}
|
||||
|
||||
override fun toString(): String = "requireRoles(${roles.joinToString()})"
|
||||
})
|
||||
|
||||
route.intercept(ApplicationCallPipeline.Call) {
|
||||
val principal = call.principal<JWTPrincipal>()
|
||||
val authContext = principal?.getUserAuthContext()
|
||||
|
||||
if (authContext == null) {
|
||||
call.respond(HttpStatusCode.Unauthorized, "Authentication required")
|
||||
finish()
|
||||
return@intercept
|
||||
}
|
||||
|
||||
val hasRequiredRole = roles.any { requiredRole ->
|
||||
authContext.roles.contains(requiredRole)
|
||||
}
|
||||
|
||||
if (!hasRequiredRole) {
|
||||
call.respond(
|
||||
HttpStatusCode.Forbidden,
|
||||
"Access denied. Required roles: ${roles.joinToString()}"
|
||||
)
|
||||
finish()
|
||||
return@intercept
|
||||
}
|
||||
}
|
||||
|
||||
route.build()
|
||||
return route
|
||||
}
|
||||
|
||||
/**
|
||||
* Route extension function to require specific permissions.
|
||||
*/
|
||||
fun Route.requirePermissions(vararg permissions: Permission, build: Route.() -> Unit): Route {
|
||||
val route = createChild(object : RouteSelector() {
|
||||
override suspend fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation {
|
||||
return RouteSelectorEvaluation.Constant
|
||||
}
|
||||
|
||||
override fun toString(): String = "requirePermissions(${permissions.joinToString()})"
|
||||
})
|
||||
|
||||
route.intercept(ApplicationCallPipeline.Call) {
|
||||
val principal = call.principal<JWTPrincipal>()
|
||||
val authContext = principal?.getUserAuthContext()
|
||||
|
||||
if (authContext == null) {
|
||||
call.respond(HttpStatusCode.Unauthorized, "Authentication required")
|
||||
finish()
|
||||
return@intercept
|
||||
}
|
||||
|
||||
val hasAllPermissions = permissions.all { requiredPermission ->
|
||||
authContext.permissions.contains(requiredPermission)
|
||||
}
|
||||
|
||||
if (!hasAllPermissions) {
|
||||
call.respond(
|
||||
HttpStatusCode.Forbidden,
|
||||
"Access denied. Required permissions: ${permissions.joinToString()}"
|
||||
)
|
||||
finish()
|
||||
return@intercept
|
||||
}
|
||||
}
|
||||
|
||||
route.build()
|
||||
return route
|
||||
}
|
||||
|
||||
/**
|
||||
* Pipeline context extension to get current user authorization context.
|
||||
*/
|
||||
val PipelineContext<Unit, ApplicationCall>.userAuthContext: UserAuthContext?
|
||||
get() = call.principal<JWTPrincipal>()?.getUserAuthContext()
|
||||
|
||||
/**
|
||||
* Application call extension to check if user has specific role.
|
||||
*/
|
||||
fun ApplicationCall.hasRole(role: UserRole): Boolean {
|
||||
val authContext = principal<JWTPrincipal>()?.getUserAuthContext()
|
||||
return authContext?.roles?.contains(role) == true
|
||||
}
|
||||
|
||||
/**
|
||||
* Application call extension to check if user has specific permission.
|
||||
*/
|
||||
fun ApplicationCall.hasPermission(permission: Permission): Boolean {
|
||||
val authContext = principal<JWTPrincipal>()?.getUserAuthContext()
|
||||
return authContext?.permissions?.contains(permission) == true
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
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
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
/**
|
||||
* Database configuration for the API Gateway.
|
||||
*
|
||||
* Sets up database connections and schema initialization for all bounded contexts.
|
||||
*/
|
||||
fun Application.configureDatabase() {
|
||||
val log = LoggerFactory.getLogger("DatabaseConfig")
|
||||
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
|
||||
)
|
||||
|
||||
// Event Management Context tables
|
||||
SchemaUtils.createMissingTablesAndColumns(
|
||||
at.mocode.events.infrastructure.repository.VeranstaltungTable
|
||||
)
|
||||
|
||||
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.ApiResponse
|
||||
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,
|
||||
ApiResponse.error<Any>("Internal server error: ${cause.message}")
|
||||
)
|
||||
}
|
||||
|
||||
status(HttpStatusCode.NotFound) { call, status ->
|
||||
call.respond(
|
||||
status,
|
||||
ApiResponse.error<Any>("Endpoint not found: ${call.request.path()}")
|
||||
)
|
||||
}
|
||||
|
||||
status(HttpStatusCode.Unauthorized) { call, status ->
|
||||
call.respond(
|
||||
status,
|
||||
ApiResponse.error<Any>("Authentication required")
|
||||
)
|
||||
}
|
||||
|
||||
status(HttpStatusCode.Forbidden) { call, status ->
|
||||
call.respond(
|
||||
status,
|
||||
ApiResponse.error<Any>("Access forbidden")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package at.mocode.gateway.config
|
||||
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.plugins.openapi.*
|
||||
import io.ktor.server.plugins.swagger.*
|
||||
import io.ktor.server.routing.*
|
||||
|
||||
/**
|
||||
* Configuration for OpenAPI/Swagger documentation.
|
||||
*
|
||||
* This module configures the OpenAPI specification generation and Swagger UI
|
||||
* for the API Gateway, providing comprehensive API documentation.
|
||||
*/
|
||||
fun Application.configureOpenApi() {
|
||||
// Configure OpenAPI using a static file
|
||||
routing {
|
||||
// Serve the OpenAPI specification from a file
|
||||
openAPI(path = "openapi", swaggerFile = "openapi/documentation.yaml") {
|
||||
// Additional configuration can be added here if needed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for Swagger UI.
|
||||
*
|
||||
* Provides an interactive web interface for exploring and testing the API.
|
||||
*/
|
||||
fun Application.configureSwagger() {
|
||||
routing {
|
||||
swaggerUI(path = "swagger", swaggerFile = "openapi/documentation.yaml") {
|
||||
version = "4.15.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package at.mocode.gateway.config
|
||||
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.plugins.cors.routing.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.auth.jwt.*
|
||||
import io.ktor.http.*
|
||||
import com.auth0.jwt.JWT
|
||||
import com.auth0.jwt.algorithms.Algorithm
|
||||
import io.ktor.server.response.respond
|
||||
|
||||
/**
|
||||
* Security configuration for the API Gateway.
|
||||
*
|
||||
* Configures CORS, JWT 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
|
||||
}
|
||||
|
||||
// JWT Configuration
|
||||
val jwtConfig = JwtConfig.fromEnvironment()
|
||||
|
||||
install(Authentication) {
|
||||
jwt("auth-jwt") {
|
||||
realm = jwtConfig.realm
|
||||
verifier(
|
||||
JWT
|
||||
.require(Algorithm.HMAC512(jwtConfig.secret))
|
||||
.withAudience(jwtConfig.audience)
|
||||
.withIssuer(jwtConfig.issuer)
|
||||
.build()
|
||||
)
|
||||
validate { credential ->
|
||||
if (credential.payload.getClaim("userId").asString() != null) {
|
||||
JWTPrincipal(credential.payload)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
challenge { defaultScheme, realm ->
|
||||
call.respond(HttpStatusCode.Unauthorized, "Token is not valid or has expired")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT Configuration data class.
|
||||
*/
|
||||
data class JwtConfig(
|
||||
val secret: String,
|
||||
val issuer: String,
|
||||
val audience: String,
|
||||
val realm: String,
|
||||
val expirationTime: Long = 3600000L // 1 hour in milliseconds
|
||||
) {
|
||||
companion object {
|
||||
fun fromEnvironment(): JwtConfig {
|
||||
return JwtConfig(
|
||||
secret = System.getenv("JWT_SECRET") ?: "default-secret-key-change-in-production",
|
||||
issuer = System.getenv("JWT_ISSUER") ?: "meldestelle-api",
|
||||
audience = System.getenv("JWT_AUDIENCE") ?: "meldestelle-users",
|
||||
realm = System.getenv("JWT_REALM") ?: "Meldestelle API",
|
||||
expirationTime = System.getenv("JWT_EXPIRATION")?.toLongOrNull() ?: 3600000L
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
package at.mocode.gateway.test
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.net.http.HttpClient
|
||||
import java.net.http.HttpRequest
|
||||
import java.net.http.HttpResponse
|
||||
import java.net.URI
|
||||
import java.time.Duration
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* Test class for authentication and authorization functionality.
|
||||
*
|
||||
* This test verifies the complete authentication and authorization flow:
|
||||
* 1. User registration
|
||||
* 2. User login
|
||||
* 3. Access to protected endpoints
|
||||
* 4. Token refresh
|
||||
* 5. Password change
|
||||
* 6. Logout
|
||||
*/
|
||||
class AuthenticationAuthorizationTest {
|
||||
|
||||
private val baseUrl = "http://localhost:8080"
|
||||
private val client = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(10))
|
||||
.build()
|
||||
|
||||
@Test
|
||||
fun testAuthenticationFlow() = runBlocking {
|
||||
println("🚀 Starting Authentication and Authorization Tests")
|
||||
println("=" * 60)
|
||||
|
||||
try {
|
||||
// Test 1: Health Check
|
||||
println("\n📋 Test 1: API Health Check")
|
||||
testHealthCheck()
|
||||
|
||||
// Test 2: User Registration
|
||||
println("\n📝 Test 2: User Registration")
|
||||
testUserRegistration()
|
||||
|
||||
// Test 3: User Login
|
||||
println("\n🔐 Test 3: User Login")
|
||||
val token = testUserLogin()
|
||||
|
||||
if (token != null) {
|
||||
// Test 4: Access Protected Profile Endpoint
|
||||
println("\n👤 Test 4: Access Protected Profile")
|
||||
testProtectedProfile(token)
|
||||
|
||||
// Test 5: Token Refresh
|
||||
println("\n🔄 Test 5: Token Refresh")
|
||||
val newToken = testTokenRefresh(token)
|
||||
|
||||
// Test 6: Change Password
|
||||
println("\n🔑 Test 6: Change Password")
|
||||
testChangePassword(newToken ?: token)
|
||||
|
||||
// Test 7: Logout
|
||||
println("\n👋 Test 7: Logout")
|
||||
testLogout(newToken ?: token)
|
||||
}
|
||||
|
||||
println("\n✅ All tests completed!")
|
||||
|
||||
} catch (e: Exception) {
|
||||
println("\n❌ Test failed with error: ${e.message}")
|
||||
e.printStackTrace()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun testHealthCheck() {
|
||||
val request = HttpRequest.newBuilder()
|
||||
.uri(URI.create("$baseUrl/health"))
|
||||
.GET()
|
||||
.build()
|
||||
|
||||
val response = client.send(request, HttpResponse.BodyHandlers.ofString())
|
||||
|
||||
if (response.statusCode() == 200) {
|
||||
println("✅ Health check passed")
|
||||
println(" Response: ${response.body()}")
|
||||
assertEquals(200, response.statusCode(), "Health check should return 200 OK")
|
||||
} else {
|
||||
println("❌ Health check failed: ${response.statusCode()}")
|
||||
println(" Response: ${response.body()}")
|
||||
assertEquals(200, response.statusCode(), "Health check should return 200 OK")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun testUserRegistration() {
|
||||
val registrationData = """
|
||||
{
|
||||
"personId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"username": "testuser_${System.currentTimeMillis()}",
|
||||
"email": "test_${System.currentTimeMillis()}@example.com",
|
||||
"password": "SecurePassword123!"
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val request = HttpRequest.newBuilder()
|
||||
.uri(URI.create("$baseUrl/auth/register"))
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(registrationData))
|
||||
.build()
|
||||
|
||||
val response = client.send(request, HttpResponse.BodyHandlers.ofString())
|
||||
|
||||
if (response.statusCode() == 201) {
|
||||
println("✅ User registration successful")
|
||||
println(" Response: ${response.body()}")
|
||||
assertEquals(201, response.statusCode(), "User registration should return 201 Created")
|
||||
} else {
|
||||
println("⚠️ User registration response: ${response.statusCode()}")
|
||||
println(" Response: ${response.body()}")
|
||||
println(" Note: This might be expected if registration requires existing person ID")
|
||||
// Don't assert here as registration might fail for valid reasons in test environment
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun testUserLogin(): String? {
|
||||
// Try to login with a test user (this assumes there's already a user in the system)
|
||||
val loginData = """
|
||||
{
|
||||
"usernameOrEmail": "admin",
|
||||
"password": "admin123"
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val request = HttpRequest.newBuilder()
|
||||
.uri(URI.create("$baseUrl/auth/login"))
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(loginData))
|
||||
.build()
|
||||
|
||||
val response = client.send(request, HttpResponse.BodyHandlers.ofString())
|
||||
|
||||
if (response.statusCode() == 200) {
|
||||
println("✅ User login successful")
|
||||
println(" Response: ${response.body()}")
|
||||
|
||||
// Extract token from response (simplified - in real scenario, parse JSON)
|
||||
val responseBody = response.body()
|
||||
val tokenStart = responseBody.indexOf("\"token\":\"") + 9
|
||||
val tokenEnd = responseBody.indexOf("\"", tokenStart)
|
||||
|
||||
return if (tokenStart > 8 && tokenEnd > tokenStart) {
|
||||
val token = responseBody.substring(tokenStart, tokenEnd)
|
||||
println(" Token extracted: ${token.take(20)}...")
|
||||
assertNotNull(token, "Token should not be null")
|
||||
assertTrue(token.isNotEmpty(), "Token should not be empty")
|
||||
token
|
||||
} else {
|
||||
println(" Could not extract token from response")
|
||||
null
|
||||
}
|
||||
} else {
|
||||
println("⚠️ User login failed: ${response.statusCode()}")
|
||||
println(" Response: ${response.body()}")
|
||||
println(" Note: This is expected if no test user exists in the database")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun testProtectedProfile(token: String) {
|
||||
val request = HttpRequest.newBuilder()
|
||||
.uri(URI.create("$baseUrl/auth/profile"))
|
||||
.header("Authorization", "Bearer $token")
|
||||
.GET()
|
||||
.build()
|
||||
|
||||
val response = client.send(request, HttpResponse.BodyHandlers.ofString())
|
||||
|
||||
if (response.statusCode() == 200) {
|
||||
println("✅ Protected profile access successful")
|
||||
println(" Response: ${response.body()}")
|
||||
assertEquals(200, response.statusCode(), "Protected profile access should return 200 OK")
|
||||
} else {
|
||||
println("❌ Protected profile access failed: ${response.statusCode()}")
|
||||
println(" Response: ${response.body()}")
|
||||
assertEquals(200, response.statusCode(), "Protected profile access should return 200 OK")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun testTokenRefresh(token: String): String? {
|
||||
val request = HttpRequest.newBuilder()
|
||||
.uri(URI.create("$baseUrl/auth/refresh"))
|
||||
.header("Authorization", "Bearer $token")
|
||||
.POST(HttpRequest.BodyPublishers.noBody())
|
||||
.build()
|
||||
|
||||
val response = client.send(request, HttpResponse.BodyHandlers.ofString())
|
||||
|
||||
if (response.statusCode() == 200) {
|
||||
println("✅ Token refresh successful")
|
||||
println(" Response: ${response.body()}")
|
||||
|
||||
// Extract new token from response (simplified)
|
||||
val responseBody = response.body()
|
||||
val tokenStart = responseBody.indexOf("\"token\":\"") + 9
|
||||
val tokenEnd = responseBody.indexOf("\"", tokenStart)
|
||||
|
||||
return if (tokenStart > 8 && tokenEnd > tokenStart) {
|
||||
val newToken = responseBody.substring(tokenStart, tokenEnd)
|
||||
println(" New token extracted: ${newToken.take(20)}...")
|
||||
assertNotNull(newToken, "New token should not be null")
|
||||
assertTrue(newToken.isNotEmpty(), "New token should not be empty")
|
||||
newToken
|
||||
} else {
|
||||
println(" Could not extract new token from response")
|
||||
null
|
||||
}
|
||||
} else {
|
||||
println("❌ Token refresh failed: ${response.statusCode()}")
|
||||
println(" Response: ${response.body()}")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun testChangePassword(token: String) {
|
||||
val changePasswordData = """
|
||||
{
|
||||
"currentPassword": "admin123",
|
||||
"newPassword": "NewSecurePassword123!"
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val request = HttpRequest.newBuilder()
|
||||
.uri(URI.create("$baseUrl/auth/change-password"))
|
||||
.header("Authorization", "Bearer $token")
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(changePasswordData))
|
||||
.build()
|
||||
|
||||
val response = client.send(request, HttpResponse.BodyHandlers.ofString())
|
||||
|
||||
if (response.statusCode() == 200) {
|
||||
println("✅ Password change successful")
|
||||
println(" Response: ${response.body()}")
|
||||
assertEquals(200, response.statusCode(), "Password change should return 200 OK")
|
||||
} else {
|
||||
println("⚠️ Password change response: ${response.statusCode()}")
|
||||
println(" Response: ${response.body()}")
|
||||
println(" Note: This might fail if current password is incorrect")
|
||||
// Don't assert here as password change might fail for valid reasons in test environment
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun testLogout(token: String) {
|
||||
val request = HttpRequest.newBuilder()
|
||||
.uri(URI.create("$baseUrl/auth/logout"))
|
||||
.header("Authorization", "Bearer $token")
|
||||
.POST(HttpRequest.BodyPublishers.noBody())
|
||||
.build()
|
||||
|
||||
val response = client.send(request, HttpResponse.BodyHandlers.ofString())
|
||||
|
||||
if (response.statusCode() == 200) {
|
||||
println("✅ Logout successful")
|
||||
println(" Response: ${response.body()}")
|
||||
assertEquals(200, response.statusCode(), "Logout should return 200 OK")
|
||||
} else {
|
||||
println("❌ Logout failed: ${response.statusCode()}")
|
||||
println(" Response: ${response.body()}")
|
||||
assertEquals(200, response.statusCode(), "Logout should return 200 OK")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extension function for string repetition
|
||||
operator fun String.times(n: Int): String = this.repeat(n)
|
||||
Reference in New Issue
Block a user