(fix) Umbau zu SCS

**Backend:**
- Vervollständigen Sie alle Repository-Implementierungen
- Implementieren Sie die Authentifizierung und Autorisierung
- Fügen Sie Validierung für alle API-Endpunkte hinzu
This commit is contained in:
stefan
2025-07-19 17:54:25 +02:00
parent db465e461e
commit 8c1ddb6cb2
47 changed files with 4278 additions and 1422 deletions
@@ -0,0 +1,188 @@
package at.mocode.gateway.auth
import at.mocode.enums.BerechtigungE
import at.mocode.enums.RolleE
import at.mocode.members.domain.service.JwtService
import at.mocode.members.domain.service.UserAuthorizationService
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.server.response.*
import com.benasher44.uuid.Uuid
/**
* Helper class for authorization checks in API endpoints.
*/
class AuthorizationHelper(
private val jwtService: JwtService,
private val userAuthorizationService: UserAuthorizationService
) {
/**
* Checks if the current user has the required permission.
*
* @param call The application call
* @param requiredPermission The permission required to access the resource
* @return true if the user has the permission, false otherwise
*/
suspend fun hasPermission(call: ApplicationCall, requiredPermission: BerechtigungE): Boolean {
val principal = call.principal<JWTPrincipal>()
val userIdString = principal?.subject ?: return false
val userId = try {
Uuid.fromString(userIdString)
} catch (e: Exception) {
return false
}
return userAuthorizationService.hasPermission(userId, requiredPermission)
}
/**
* Checks if the current user has the required role.
*
* @param call The application call
* @param requiredRole The role required to access the resource
* @return true if the user has the role, false otherwise
*/
suspend fun hasRole(call: ApplicationCall, requiredRole: RolleE): Boolean {
val principal = call.principal<JWTPrincipal>()
val userIdString = principal?.subject ?: return false
val userId = try {
Uuid.fromString(userIdString)
} catch (e: Exception) {
return false
}
return userAuthorizationService.hasRole(userId, requiredRole)
}
/**
* Checks if the current user has any of the required permissions.
*
* @param call The application call
* @param requiredPermissions List of permissions, user needs at least one
* @return true if the user has at least one of the permissions, false otherwise
*/
suspend fun hasAnyPermission(call: ApplicationCall, requiredPermissions: List<BerechtigungE>): Boolean {
val principal = call.principal<JWTPrincipal>()
val userIdString = principal?.subject ?: return false
val userId = try {
Uuid.fromString(userIdString)
} catch (e: Exception) {
return false
}
val authInfo = userAuthorizationService.getUserAuthInfo(userId) ?: return false
return authInfo.permissions.any { it in requiredPermissions }
}
/**
* Checks if the current user has any of the required roles.
*
* @param call The application call
* @param requiredRoles List of roles, user needs at least one
* @return true if the user has at least one of the roles, false otherwise
*/
suspend fun hasAnyRole(call: ApplicationCall, requiredRoles: List<RolleE>): Boolean {
val principal = call.principal<JWTPrincipal>()
val userIdString = principal?.subject ?: return false
val userId = try {
Uuid.fromString(userIdString)
} catch (e: Exception) {
return false
}
val authInfo = userAuthorizationService.getUserAuthInfo(userId) ?: return false
return authInfo.roles.any { it in requiredRoles }
}
/**
* Gets the current user's ID from the JWT token.
*
* @param call The application call
* @return The user ID if valid, null otherwise
*/
fun getCurrentUserId(call: ApplicationCall): Uuid? {
val principal = call.principal<JWTPrincipal>()
val userIdString = principal?.subject ?: return null
return try {
Uuid.fromString(userIdString)
} catch (e: Exception) {
null
}
}
/**
* Responds with a 403 Forbidden status when authorization fails.
*
* @param call The application call
* @param message Optional custom message
*/
suspend fun respondForbidden(call: ApplicationCall, message: String = "Insufficient permissions") {
call.respond(
HttpStatusCode.Forbidden,
mapOf("error" to message)
)
}
/**
* Responds with a 401 Unauthorized status when authentication fails.
*
* @param call The application call
* @param message Optional custom message
*/
suspend fun respondUnauthorized(call: ApplicationCall, message: String = "Authentication required") {
call.respond(
HttpStatusCode.Unauthorized,
mapOf("error" to message)
)
}
}
/**
* Extension function to check permission and respond with 403 if not authorized.
*/
suspend fun ApplicationCall.requirePermission(
authHelper: AuthorizationHelper,
permission: BerechtigungE
): Boolean {
if (!authHelper.hasPermission(this, permission)) {
authHelper.respondForbidden(this, "Required permission: ${permission.name}")
return false
}
return true
}
/**
* Extension function to check role and respond with 403 if not authorized.
*/
suspend fun ApplicationCall.requireRole(
authHelper: AuthorizationHelper,
role: RolleE
): Boolean {
if (!authHelper.hasRole(this, role)) {
authHelper.respondForbidden(this, "Required role: ${role.name}")
return false
}
return true
}
/**
* Extension function to check any of the permissions and respond with 403 if not authorized.
*/
suspend fun ApplicationCall.requireAnyPermission(
authHelper: AuthorizationHelper,
permissions: List<BerechtigungE>
): Boolean {
if (!authHelper.hasAnyPermission(this, permissions)) {
authHelper.respondForbidden(this, "Required permissions: ${permissions.joinToString { it.name }}")
return false
}
return true
}
@@ -41,7 +41,7 @@ fun Application.configureSecurity() {
realm = jwtConfig.realm
verifier(
JWT
.require(Algorithm.HMAC256(jwtConfig.secret))
.require(Algorithm.HMAC512(jwtConfig.secret))
.withAudience(jwtConfig.audience)
.withIssuer(jwtConfig.issuer)
.build()
@@ -111,34 +111,43 @@ fun Route.authRoutes(
loginRequest.password
)
if (authResult.isSuccess) {
val user = authResult.user!!
val tokenInfo = authResult.tokenInfo!!
call.respond(
HttpStatusCode.OK,
LoginResponse(
success = true,
token = tokenInfo.token,
message = "Login successful",
user = UserProfileResponse(
userId = user.userId.toString(),
username = user.username,
email = user.email,
isActive = user.istAktiv,
isEmailVerified = user.istEmailVerifiziert,
lastLogin = user.letzteAnmeldung?.toString()
when (authResult) {
is at.mocode.members.domain.service.AuthenticationService.AuthResult.Success -> {
call.respond(
HttpStatusCode.OK,
LoginResponse(
success = true,
token = authResult.token,
message = "Login successful",
user = UserProfileResponse(
userId = authResult.user.userId.toString(),
username = authResult.user.username,
email = authResult.user.email,
isActive = authResult.user.istAktiv,
isEmailVerified = authResult.user.istEmailVerifiziert,
lastLogin = authResult.user.letzteAnmeldung?.toString()
)
)
)
)
} else {
call.respond(
HttpStatusCode.Unauthorized,
LoginResponse(
success = false,
message = authResult.errorMessage ?: "Invalid credentials"
}
is at.mocode.members.domain.service.AuthenticationService.AuthResult.Failure -> {
call.respond(
HttpStatusCode.Unauthorized,
LoginResponse(
success = false,
message = authResult.reason
)
)
)
}
is at.mocode.members.domain.service.AuthenticationService.AuthResult.Locked -> {
call.respond(
HttpStatusCode.Unauthorized,
LoginResponse(
success = false,
message = "Account ist gesperrt bis ${authResult.lockedUntil}"
)
)
}
}
} catch (e: Exception) {
call.respond(
@@ -156,39 +165,22 @@ fun Route.authRoutes(
try {
val registerRequest = call.receive<RegisterRequest>()
// TODO: Implement actual registration logic
// For now, return a mock response
if (registerRequest.username.isNotEmpty() &&
registerRequest.email.isNotEmpty() &&
registerRequest.password.length >= 8) {
call.respond(
HttpStatusCode.Created,
RegisterResponse(
success = true,
message = "User registered successfully",
user = UserProfileResponse(
userId = "mock-user-id-${System.currentTimeMillis()}",
username = registerRequest.username,
email = registerRequest.email,
isActive = true,
isEmailVerified = false,
lastLogin = null
)
)
)
} else {
val errors = mutableListOf<ValidationErrorResponse>()
if (registerRequest.username.isEmpty()) {
errors.add(ValidationErrorResponse("username", "Username is required"))
}
if (registerRequest.email.isEmpty()) {
errors.add(ValidationErrorResponse("email", "Email is required"))
}
if (registerRequest.password.length < 8) {
errors.add(ValidationErrorResponse("password", "Password must be at least 8 characters"))
}
// Validate input
val errors = mutableListOf<ValidationErrorResponse>()
if (registerRequest.username.isEmpty()) {
errors.add(ValidationErrorResponse("username", "Username is required"))
}
if (registerRequest.email.isEmpty()) {
errors.add(ValidationErrorResponse("email", "Email is required"))
}
if (registerRequest.password.length < 8) {
errors.add(ValidationErrorResponse("password", "Password must be at least 8 characters"))
}
if (registerRequest.personId.isEmpty()) {
errors.add(ValidationErrorResponse("personId", "Person ID is required"))
}
if (errors.isNotEmpty()) {
call.respond(
HttpStatusCode.BadRequest,
RegisterResponse(
@@ -197,6 +189,71 @@ fun Route.authRoutes(
errors = errors
)
)
return@post
}
// Parse personId
val personId = try {
com.benasher44.uuid.Uuid.fromString(registerRequest.personId)
} catch (e: Exception) {
call.respond(
HttpStatusCode.BadRequest,
RegisterResponse(
success = false,
message = "Invalid person ID format",
errors = listOf(ValidationErrorResponse("personId", "Invalid UUID format"))
)
)
return@post
}
// Register user
val registerResult = authenticationService.registerUser(
registerRequest.username,
registerRequest.email,
registerRequest.password,
personId
)
when (registerResult) {
is at.mocode.members.domain.service.AuthenticationService.RegisterResult.Success -> {
call.respond(
HttpStatusCode.Created,
RegisterResponse(
success = true,
message = "User registered successfully",
user = UserProfileResponse(
userId = registerResult.user.userId.toString(),
username = registerResult.user.username,
email = registerResult.user.email,
isActive = registerResult.user.istAktiv,
isEmailVerified = registerResult.user.istEmailVerifiziert,
lastLogin = registerResult.user.letzteAnmeldung?.toString()
)
)
)
}
is at.mocode.members.domain.service.AuthenticationService.RegisterResult.Failure -> {
call.respond(
HttpStatusCode.BadRequest,
RegisterResponse(
success = false,
message = registerResult.reason
)
)
}
is at.mocode.members.domain.service.AuthenticationService.RegisterResult.WeakPassword -> {
call.respond(
HttpStatusCode.BadRequest,
RegisterResponse(
success = false,
message = "Password is too weak",
errors = registerResult.issues.map {
ValidationErrorResponse("password", it)
}
)
)
}
}
} catch (e: Exception) {
call.respond(
@@ -216,21 +273,35 @@ fun Route.authRoutes(
get("/profile") {
try {
val principal = call.principal<JWTPrincipal>()
val userId = principal?.getClaim("userId", String::class)
val userIdString = principal?.subject
if (userId != null) {
// TODO: Fetch actual user data from database
call.respond(
HttpStatusCode.OK,
UserProfileResponse(
userId = userId,
username = "mock_user",
email = "mock@example.com",
isActive = true,
isEmailVerified = true,
lastLogin = null
if (userIdString != null) {
val userId = try {
com.benasher44.uuid.Uuid.fromString(userIdString)
} catch (e: Exception) {
call.respond(HttpStatusCode.Unauthorized, "Invalid token format")
return@get
}
// Fetch actual user data from database
val userRepository = at.mocode.members.infrastructure.repository.UserRepositoryImpl()
val user = userRepository.findById(userId)
if (user != null) {
call.respond(
HttpStatusCode.OK,
UserProfileResponse(
userId = user.userId.toString(),
username = user.username,
email = user.email,
isActive = user.istAktiv,
isEmailVerified = user.istEmailVerifiziert,
lastLogin = user.letzteAnmeldung?.toString()
)
)
)
} else {
call.respond(HttpStatusCode.NotFound, "User not found")
}
} else {
call.respond(HttpStatusCode.Unauthorized, "Invalid token")
}
@@ -243,31 +314,81 @@ fun Route.authRoutes(
post("/change-password") {
try {
val principal = call.principal<JWTPrincipal>()
val userId = principal?.getClaim("userId", String::class)
val userIdString = principal?.subject
if (userIdString != null) {
val userId = try {
com.benasher44.uuid.Uuid.fromString(userIdString)
} catch (e: Exception) {
call.respond(HttpStatusCode.Unauthorized, "Invalid token format")
return@post
}
if (userId != null) {
val changePasswordRequest = call.receive<ChangePasswordRequest>()
// TODO: Implement actual password change logic
if (changePasswordRequest.newPassword.length >= 8) {
call.respond(
HttpStatusCode.OK,
ChangePasswordResponse(
success = true,
message = "Password changed successfully"
)
)
} else {
// Validate input
if (changePasswordRequest.currentPassword.isEmpty()) {
call.respond(
HttpStatusCode.BadRequest,
ChangePasswordResponse(
success = false,
message = "Password change failed",
errors = listOf(
ValidationErrorResponse("newPassword", "Password must be at least 8 characters")
)
message = "Current password is required",
errors = listOf(ValidationErrorResponse("currentPassword", "Current password is required"))
)
)
return@post
}
if (changePasswordRequest.newPassword.length < 8) {
call.respond(
HttpStatusCode.BadRequest,
ChangePasswordResponse(
success = false,
message = "New password must be at least 8 characters",
errors = listOf(ValidationErrorResponse("newPassword", "Password must be at least 8 characters"))
)
)
return@post
}
// Change password using AuthenticationService
val changeResult = authenticationService.changePassword(
userId,
changePasswordRequest.currentPassword,
changePasswordRequest.newPassword
)
when (changeResult) {
is at.mocode.members.domain.service.AuthenticationService.PasswordChangeResult.Success -> {
call.respond(
HttpStatusCode.OK,
ChangePasswordResponse(
success = true,
message = "Password changed successfully"
)
)
}
is at.mocode.members.domain.service.AuthenticationService.PasswordChangeResult.Failure -> {
call.respond(
HttpStatusCode.BadRequest,
ChangePasswordResponse(
success = false,
message = changeResult.reason
)
)
}
is at.mocode.members.domain.service.AuthenticationService.PasswordChangeResult.WeakPassword -> {
call.respond(
HttpStatusCode.BadRequest,
ChangePasswordResponse(
success = false,
message = "Password is too weak",
errors = changeResult.issues.map {
ValidationErrorResponse("newPassword", it)
}
)
)
}
}
} else {
call.respond(HttpStatusCode.Unauthorized, "Invalid token")
@@ -288,19 +409,41 @@ fun Route.authRoutes(
try {
val token = call.request.header("Authorization")?.removePrefix("Bearer ")
if (token != null) {
// TODO: Implement actual token refresh logic
call.respond(
HttpStatusCode.OK,
mapOf(
"token" to "refreshed_mock_jwt_token_${System.currentTimeMillis()}",
"message" to "Token refreshed successfully"
// Validate the current token
val tokenInfo = jwtService.validateToken(token)
if (tokenInfo != null) {
// Get user from database to ensure they're still active
val userRepository = at.mocode.members.infrastructure.repository.UserRepositoryImpl()
val user = userRepository.findById(tokenInfo.userId)
if (user != null && user.canLogin()) {
// Create a new token
val newToken = jwtService.createToken(user)
call.respond(
HttpStatusCode.OK,
mapOf(
"token" to newToken,
"message" to "Token refreshed successfully"
)
)
} else {
call.respond(
HttpStatusCode.Unauthorized,
mapOf("message" to "User is no longer active or account is locked")
)
}
} else {
call.respond(
HttpStatusCode.Unauthorized,
mapOf("message" to "Invalid or expired token")
)
)
}
} else {
call.respond(HttpStatusCode.BadRequest, "No token provided")
call.respond(HttpStatusCode.BadRequest, mapOf("message" to "No token provided"))
}
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, "Error refreshing token: ${e.message}")
call.respond(HttpStatusCode.InternalServerError, mapOf("message" to "Error refreshing token: ${e.message}"))
}
}
@@ -7,11 +7,14 @@ 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 at.mocode.events.infrastructure.api.VeranstaltungController
import at.mocode.events.infrastructure.repository.VeranstaltungRepositoryImpl
import at.mocode.members.domain.service.AuthenticationService
import at.mocode.members.domain.service.JwtService
import at.mocode.members.domain.service.UserAuthorizationService
import at.mocode.members.domain.service.PasswordService
import at.mocode.members.infrastructure.repository.*
import at.mocode.gateway.auth.AuthorizationHelper
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.response.*
@@ -29,6 +32,7 @@ fun Application.configureRouting() {
// Initialize repository implementations for each context
val landRepository = LandRepositoryImpl()
val horseRepository = HorseRepositoryImpl()
val veranstaltungRepository = VeranstaltungRepositoryImpl()
// Initialize authentication repositories
val userRepository = UserRepositoryImpl()
@@ -53,6 +57,9 @@ fun Application.configureRouting() {
jwtService
)
// Initialize authorization helper
val authorizationHelper = AuthorizationHelper(jwtService, userAuthorizationService)
// Initialize use cases
val getCountryUseCase = GetCountryUseCase(landRepository)
val createCountryUseCase = CreateCountryUseCase(landRepository)
@@ -60,6 +67,7 @@ fun Application.configureRouting() {
// Initialize controllers for each bounded context
val countryController = CountryController(getCountryUseCase, createCountryUseCase)
val horseController = HorseController(horseRepository)
val veranstaltungController = VeranstaltungController(veranstaltungRepository)
routing {
@@ -73,12 +81,14 @@ fun Application.configureRouting() {
availableContexts = listOf(
"authentication",
"master-data",
"horse-registry"
"horse-registry",
"event-management"
),
endpoints = mapOf(
"authentication" to "/auth/*",
"master-data" to "/api/masterdata/*",
"horse-registry" to "/api/horses/*"
"horse-registry" to "/api/horses/*",
"event-management" to "/api/events/*"
)
)
))
@@ -92,7 +102,8 @@ fun Application.configureRouting() {
contexts = mapOf(
"authentication" to "UP",
"master-data" to "UP",
"horse-registry" to "UP"
"horse-registry" to "UP",
"event-management" to "UP"
)
)
))
@@ -119,6 +130,11 @@ fun Application.configureRouting() {
name = "Horse Registry Context",
path = "/api/horses",
description = "Horse registration, ownership, and pedigree management"
),
ContextInfo(
name = "Event Management Context",
path = "/api/events",
description = "Event creation, management, and participant registration"
)
)
)
@@ -136,6 +152,9 @@ fun Application.configureRouting() {
// Horse Registry Context Routes
horseController.configureRoutes(this)
// Event Management Context Routes
veranstaltungController.configureRoutes(this)
// Catch-all for undefined routes
route("{...}") {
handle {