(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,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())
}
}
}