(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:
@@ -0,0 +1,42 @@
|
||||
package at.mocode.gateway.auth
|
||||
|
||||
import at.mocode.shared.config.AppConfig
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
|
||||
/**
|
||||
* Konfiguriert die API-Key-Authentifizierung für die Anwendung.
|
||||
* Diese einfache Authentifizierung kann für externe Systeme verwendet werden,
|
||||
* die keinen JWT-basierten Zugriff benötigen.
|
||||
*/
|
||||
fun Application.configureApiKeyAuth() {
|
||||
val apiKey = AppConfig.security.apiKey ?: "api-key-not-configured"
|
||||
|
||||
install(Authentication) {
|
||||
register(object : AuthenticationProvider(object : AuthenticationProvider.Config("api-key") {}) {
|
||||
override suspend fun onAuthenticate(context: AuthenticationContext) {
|
||||
val call = context.call
|
||||
|
||||
val requestApiKey = call.request.header("X-API-Key")
|
||||
?: call.request.queryParameters["api_key"]
|
||||
|
||||
if (requestApiKey == apiKey) {
|
||||
context.principal(ApiKeyPrincipal(apiKey))
|
||||
} else {
|
||||
context.challenge("ApiKeyAuth", AuthenticationFailedCause.InvalidCredentials) { challenge, call ->
|
||||
call.respond(HttpStatusCode.Unauthorized, "Ungültiger API-Key")
|
||||
challenge.complete()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Principal für die API-Key-Authentifizierung.
|
||||
*/
|
||||
class ApiKeyPrincipal(val apiKey: String) : Principal
|
||||
@@ -0,0 +1,107 @@
|
||||
package at.mocode.gateway.auth
|
||||
|
||||
import at.mocode.enums.BerechtigungE
|
||||
import at.mocode.members.domain.service.JwtService
|
||||
import at.mocode.shared.config.AppConfig
|
||||
import com.auth0.jwt.JWT
|
||||
import com.auth0.jwt.algorithms.Algorithm
|
||||
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.*
|
||||
|
||||
/**
|
||||
* Konfiguriert die JWT-Authentifizierung für die Anwendung.
|
||||
*/
|
||||
fun Application.configureJwtAuth(jwtService: JwtService) {
|
||||
val jwtConfig = AppConfig.security.jwt
|
||||
|
||||
install(Authentication) {
|
||||
jwt("jwt") {
|
||||
realm = jwtConfig.realm
|
||||
verifier {
|
||||
com.auth0.jwt.JWT.require(com.auth0.jwt.algorithms.Algorithm.HMAC512(jwtConfig.secret))
|
||||
.withIssuer(jwtConfig.issuer)
|
||||
.withAudience(jwtConfig.audience)
|
||||
.build()
|
||||
}
|
||||
validate { credential ->
|
||||
// Token is already validated by the verifier above
|
||||
// Just check if required claims are present
|
||||
val subject = credential.payload.subject
|
||||
val permissions = credential.payload.getClaim("permissions")
|
||||
|
||||
if (subject != null && permissions != null) {
|
||||
JWTPrincipal(credential.payload)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
challenge { _, _ ->
|
||||
call.respond(HttpStatusCode.Unauthorized, "Token ungültig oder abgelaufen")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob der aktuelle Benutzer die angegebene Berechtigung hat.
|
||||
* Muss innerhalb einer authenticate("jwt")-Block verwendet werden.
|
||||
*
|
||||
* @param permission Die erforderliche Berechtigung
|
||||
* @param onFailure Funktion, die bei fehlender Berechtigung aufgerufen wird
|
||||
* @param onSuccess Funktion, die bei vorhandener Berechtigung aufgerufen wird
|
||||
*/
|
||||
suspend fun ApplicationCall.requirePermission(
|
||||
permission: BerechtigungE,
|
||||
onFailure: suspend () -> Unit = { respond(HttpStatusCode.Forbidden, "Keine Berechtigung") },
|
||||
onSuccess: suspend () -> Unit
|
||||
) {
|
||||
val principal = principal<JWTPrincipal>()
|
||||
if (principal == null) {
|
||||
respond(HttpStatusCode.Unauthorized, "Nicht authentifiziert")
|
||||
return
|
||||
}
|
||||
|
||||
val permissions = principal.getClaim("permissions", Array<String>::class)
|
||||
?.map { try { BerechtigungE.valueOf(it) } catch (e: Exception) { null } }
|
||||
?.filterNotNull() ?: emptyList()
|
||||
|
||||
if (permissions.contains(permission) || permissions.contains(BerechtigungE.SYSTEM_ADMIN)) {
|
||||
onSuccess()
|
||||
} else {
|
||||
onFailure()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft, ob der aktuelle Benutzer eine der angegebenen Berechtigungen hat.
|
||||
* Muss innerhalb einer authenticate("jwt")-Block verwendet werden.
|
||||
*
|
||||
* @param permissions Die erforderlichen Berechtigungen (eine davon ist ausreichend)
|
||||
* @param onFailure Funktion, die bei fehlender Berechtigung aufgerufen wird
|
||||
* @param onSuccess Funktion, die bei vorhandener Berechtigung aufgerufen wird
|
||||
*/
|
||||
suspend fun ApplicationCall.requireAnyPermission(
|
||||
vararg permissions: BerechtigungE,
|
||||
onFailure: suspend () -> Unit = { respond(HttpStatusCode.Forbidden, "Keine Berechtigung") },
|
||||
onSuccess: suspend () -> Unit
|
||||
) {
|
||||
val principal = principal<JWTPrincipal>()
|
||||
if (principal == null) {
|
||||
respond(HttpStatusCode.Unauthorized, "Nicht authentifiziert")
|
||||
return
|
||||
}
|
||||
|
||||
val userPermissions = principal.getClaim("permissions", Array<String>::class)
|
||||
?.map { try { BerechtigungE.valueOf(it) } catch (e: Exception) { null } }
|
||||
?.filterNotNull() ?: emptyList()
|
||||
|
||||
if (userPermissions.contains(BerechtigungE.SYSTEM_ADMIN) ||
|
||||
permissions.any { userPermissions.contains(it) }) {
|
||||
onSuccess()
|
||||
} else {
|
||||
onFailure()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
package at.mocode.gateway.routing
|
||||
|
||||
import at.mocode.dto.base.ApiResponse
|
||||
import at.mocode.members.domain.service.AuthenticationService
|
||||
import at.mocode.members.domain.service.JwtService
|
||||
import at.mocode.validation.ApiValidationUtils
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.auth.jwt.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Konfiguriert die Authentifizierungs-Routen.
|
||||
*/
|
||||
fun Routing.authRoutes(
|
||||
authenticationService: AuthenticationService,
|
||||
jwtService: JwtService
|
||||
) {
|
||||
route("/auth") {
|
||||
// Login-Route
|
||||
post("/login") {
|
||||
try {
|
||||
// Request-Daten lesen
|
||||
val request = call.receive<LoginRequest>()
|
||||
|
||||
// Validierung
|
||||
val validationErrors = ApiValidationUtils.validateLoginRequest(request.username, request.password)
|
||||
if (!ApiValidationUtils.isValid(validationErrors)) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<LoginResponse>(ApiValidationUtils.createErrorMessage(validationErrors))
|
||||
)
|
||||
return@post
|
||||
}
|
||||
|
||||
// Authentifizierung durchführen
|
||||
val authResult = authenticationService.authenticate(request.username, request.password)
|
||||
|
||||
// Antwort basierend auf dem Ergebnis senden
|
||||
when (authResult) {
|
||||
is AuthenticationService.AuthResult.Success -> {
|
||||
call.respond(
|
||||
HttpStatusCode.OK,
|
||||
ApiResponse.success(
|
||||
LoginResponse(
|
||||
token = authResult.token,
|
||||
userId = authResult.user.userId.toString(),
|
||||
personId = authResult.user.personId.toString(),
|
||||
username = authResult.user.username,
|
||||
email = authResult.user.email
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
is AuthenticationService.AuthResult.Failure -> {
|
||||
call.respond(
|
||||
HttpStatusCode.Unauthorized,
|
||||
ApiResponse.error<LoginResponse>(authResult.reason)
|
||||
)
|
||||
}
|
||||
|
||||
is AuthenticationService.AuthResult.Locked -> {
|
||||
call.respond(
|
||||
HttpStatusCode.Locked,
|
||||
ApiResponse.error<LoginResponse>(
|
||||
"Account gesperrt bis ${authResult.lockedUntil}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(
|
||||
HttpStatusCode.InternalServerError,
|
||||
ApiResponse.error<LoginResponse>("Fehler bei der Anmeldung: ${e.message}")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Registrierung (Beispiel, sollte an die Anforderungen angepasst werden)
|
||||
post("/register") {
|
||||
// Würde hier Registrierung implementieren
|
||||
call.respond(
|
||||
HttpStatusCode.NotImplemented,
|
||||
ApiResponse.error<Any>("Registrierung noch nicht implementiert")
|
||||
)
|
||||
}
|
||||
|
||||
// Passwort ändern (geschützte Route)
|
||||
authenticate("jwt") {
|
||||
post("/change-password") {
|
||||
try {
|
||||
// Request-Daten lesen
|
||||
val request = call.receive<ChangePasswordRequest>()
|
||||
|
||||
// Validierung
|
||||
val validationErrors = ApiValidationUtils.validateChangePasswordRequest(
|
||||
request.currentPassword,
|
||||
request.newPassword,
|
||||
request.confirmPassword
|
||||
)
|
||||
if (!ApiValidationUtils.isValid(validationErrors)) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<Any>(ApiValidationUtils.createErrorMessage(validationErrors))
|
||||
)
|
||||
return@post
|
||||
}
|
||||
|
||||
// Benutzer-ID aus dem Token extrahieren
|
||||
val principal = call.principal<JWTPrincipal>()
|
||||
val userId = principal?.getClaim("sub", String::class) ?: run {
|
||||
call.respond(
|
||||
HttpStatusCode.Unauthorized,
|
||||
ApiResponse.error<Any>("Ungültiges Token")
|
||||
)
|
||||
return@post
|
||||
}
|
||||
|
||||
// Passwort ändern
|
||||
val result = authenticationService.changePassword(
|
||||
com.benasher44.uuid.Uuid.fromString(userId),
|
||||
request.currentPassword,
|
||||
request.newPassword
|
||||
)
|
||||
|
||||
// Antwort basierend auf dem Ergebnis senden
|
||||
when (result) {
|
||||
is AuthenticationService.PasswordChangeResult.Success -> {
|
||||
call.respond(
|
||||
HttpStatusCode.OK,
|
||||
ApiResponse.success("Passwort erfolgreich geändert")
|
||||
)
|
||||
}
|
||||
|
||||
is AuthenticationService.PasswordChangeResult.Failure -> {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<Any>(result.reason)
|
||||
)
|
||||
}
|
||||
|
||||
is AuthenticationService.PasswordChangeResult.WeakPassword -> {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<Any>("Das neue Passwort ist zu schwach")
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
call.respond(
|
||||
HttpStatusCode.InternalServerError,
|
||||
ApiResponse.error<Any>("Fehler bei der Passwortänderung: ${e.message}")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Benutzerinformationen abrufen
|
||||
get("/me") {
|
||||
try {
|
||||
// Token validieren und Benutzerinformationen abrufen
|
||||
val principal = call.principal<JWTPrincipal>()
|
||||
val userId = principal?.getClaim("sub", String::class) ?: run {
|
||||
call.respond(
|
||||
HttpStatusCode.Unauthorized,
|
||||
ApiResponse.error<Any>("Ungültiges Token")
|
||||
)
|
||||
return@get
|
||||
}
|
||||
|
||||
// Hier können zusätzliche Informationen aus dem Token oder der Datenbank abgerufen werden
|
||||
val username = principal.getClaim("username", String::class) ?: ""
|
||||
val personId = principal.getClaim("personId", String::class) ?: ""
|
||||
val permissions = principal.getClaim("permissions", String::class)?.split(",") ?: listOf()
|
||||
|
||||
call.respond(
|
||||
HttpStatusCode.OK,
|
||||
ApiResponse.success(
|
||||
UserInfoResponse(
|
||||
userId = userId,
|
||||
personId = personId,
|
||||
username = username,
|
||||
permissions = permissions
|
||||
)
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
call.respond(
|
||||
HttpStatusCode.InternalServerError,
|
||||
ApiResponse.error<Any>("Fehler beim Abrufen der Benutzerinformationen: ${e.message}")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Request-Modell für die Anmeldung.
|
||||
*/
|
||||
@Serializable
|
||||
data class LoginRequest(
|
||||
val username: String,
|
||||
val password: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Response-Modell für eine erfolgreiche Anmeldung.
|
||||
*/
|
||||
@Serializable
|
||||
data class LoginResponse(
|
||||
val token: String,
|
||||
val userId: String,
|
||||
val personId: String,
|
||||
val username: String,
|
||||
val email: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Request-Modell für die Passwortänderung.
|
||||
*/
|
||||
@Serializable
|
||||
data class ChangePasswordRequest(
|
||||
val currentPassword: String,
|
||||
val newPassword: String,
|
||||
val confirmPassword: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Response-Modell für Benutzerinformationen.
|
||||
*/
|
||||
@Serializable
|
||||
data class UserInfoResponse(
|
||||
val userId: String,
|
||||
val personId: String,
|
||||
val username: String,
|
||||
val permissions: List<String>
|
||||
)
|
||||
@@ -0,0 +1,104 @@
|
||||
package at.mocode.gateway.validation
|
||||
|
||||
import at.mocode.dto.base.ApiResponse
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
|
||||
/**
|
||||
* Klasse für die Validierung von API-Anfragen.
|
||||
* Bietet Methoden zum Validieren und Verarbeiten von Request-Daten.
|
||||
*/
|
||||
class RequestValidator {
|
||||
companion object {
|
||||
/**
|
||||
* Validiert und verarbeitet eine Anfrage.
|
||||
*
|
||||
* @param call Der ApplicationCall
|
||||
* @param validator Eine Funktion, die den Request validiert und eine Liste von Fehlern zurückgibt
|
||||
* @param processor Eine Funktion, die den validierten Request verarbeitet
|
||||
* @return true, wenn die Validierung erfolgreich war, false sonst
|
||||
*/
|
||||
suspend inline fun <reified T : Any> validateAndProcess(
|
||||
call: ApplicationCall,
|
||||
crossinline validator: (T) -> List<String>,
|
||||
crossinline processor: suspend (T) -> Unit
|
||||
): Boolean {
|
||||
try {
|
||||
// Request-Daten lesen
|
||||
val request = call.receive<T>()
|
||||
|
||||
// Validierung durchführen
|
||||
val errors = validator(request)
|
||||
if (errors.isNotEmpty()) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<T>("Validierungsfehler")
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
// Request verarbeiten
|
||||
processor(request)
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
call.respond(
|
||||
HttpStatusCode.BadRequest,
|
||||
ApiResponse.error<T>("Fehler bei der Anfrageverarbeitung: ${e.message}")
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert Pflichtfelder in einem Request.
|
||||
*
|
||||
* @param fields Map von Feldnamen zu Feldwerten
|
||||
* @return Liste von Fehlermeldungen für fehlende Pflichtfelder
|
||||
*/
|
||||
fun validateRequiredFields(vararg fields: Pair<String, Any?>): List<String> {
|
||||
return fields
|
||||
.filter { (_, value) ->
|
||||
when (value) {
|
||||
null -> true
|
||||
is String -> value.isBlank()
|
||||
is Collection<*> -> value.isEmpty()
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
.map { (name, _) -> "Das Feld '$name' ist erforderlich" }
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert die Länge eines Textfeldes.
|
||||
*
|
||||
* @param name Name des Feldes
|
||||
* @param value Wert des Feldes
|
||||
* @param minLength Minimale Länge
|
||||
* @param maxLength Maximale Länge
|
||||
* @return Fehlermeldung, wenn die Länge ungültig ist, sonst null
|
||||
*/
|
||||
fun validateStringLength(name: String, value: String?, minLength: Int, maxLength: Int): String? {
|
||||
if (value == null) return null
|
||||
|
||||
return when {
|
||||
value.length < minLength -> "Das Feld '$name' muss mindestens $minLength Zeichen enthalten"
|
||||
value.length > maxLength -> "Das Feld '$name' darf höchstens $maxLength Zeichen enthalten"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert eine E-Mail-Adresse.
|
||||
*
|
||||
* @param email Die zu validierende E-Mail-Adresse
|
||||
* @return true, wenn die E-Mail-Adresse gültig ist, false sonst
|
||||
*/
|
||||
fun isValidEmail(email: String?): Boolean {
|
||||
if (email == null) return false
|
||||
val emailRegex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$"
|
||||
return email.matches(emailRegex.toRegex())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user