(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
+1
View File
@@ -32,6 +32,7 @@ kotlin {
implementation(libs.ktor.server.core)
implementation(libs.ktor.server.contentNegotiation)
implementation(libs.ktor.server.serializationKotlinxJson)
implementation("com.auth0:java-jwt:4.4.0")
}
jsMain.dependencies {
@@ -10,18 +10,17 @@ import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
/**
* Repräsentiert eine Rolle im System für die Mitgliederverwaltung.
* Repräsentiert eine Rolle im System für die Zugriffskontrolle.
*
* Rollen definieren die grundlegenden Funktionen und Verantwortlichkeiten
* von Personen im System (z.B. Reiter, Trainer, Funktionär, Admin).
* Jede Rolle kann mit spezifischen Berechtigungen verknüpft werden.
* Rollen bündeln mehrere Berechtigungen und werden Personen zugewiesen,
* um deren Zugriffsrechte im System zu definieren.
*
* @property rolleId Eindeutiger interner Identifikator für diese Rolle (UUID).
* @property rolleTyp Der Typ der Rolle aus der RolleE Enumeration.
* @property name Anzeigename der Rolle (z.B. "Administrator", "Vereinsadministrator").
* @property beschreibung Detaillierte Beschreibung der Rolle und ihrer Verantwortlichkeiten.
* @property istAktiv Gibt an, ob diese Rolle aktuell aktiv ist und zugewiesen werden kann.
* @property rolleTyp Der Typ der Rolle (Enum-Wert).
* @property name Anzeigename der Rolle (z.B. "Administrator", "Vereinsverwalter").
* @property beschreibung Detaillierte Beschreibung der Rolle und ihres Zwecks.
* @property istSystemRolle Gibt an, ob es sich um eine Systemrolle handelt, die nicht gelöscht werden kann.
* @property istAktiv Gibt an, ob diese Rolle aktuell aktiv ist.
* @property createdAt Zeitstempel der Erstellung dieser Rolle.
* @property updatedAt Zeitstempel der letzten Aktualisierung dieser Rolle.
*/
@@ -30,12 +29,12 @@ data class DomRolle(
@Serializable(with = UuidSerializer::class)
val rolleId: Uuid = uuid4(),
val rolleTyp: RolleE,
var rolleTyp: RolleE,
var name: String,
var beschreibung: String? = null,
var istAktiv: Boolean = true,
var istSystemRolle: Boolean = false,
var istAktiv: Boolean = true,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant = Clock.System.now(),
@@ -9,24 +9,21 @@ import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
/**
* Repräsentiert einen Benutzer für die Authentifizierung im System.
* Repräsentiert einen Benutzer im System.
*
* Diese Entität verwaltet die Anmeldedaten und ist mit einer Person verknüpft.
* Ein Benutzer kann sich am System anmelden und erhält basierend auf seinen
* zugewiesenen Rollen entsprechende Berechtigungen.
* Ein Benutzer ist mit einer Person verknüpft und hat Anmeldedaten für den Zugriff auf das System.
*
* @property userId Eindeutiger interner Identifikator für diesen Benutzer (UUID).
* @property personId Fremdschlüssel zur verknüpften Person (DomPerson.personId).
* @property username Eindeutiger Benutzername für die Anmeldung.
* @property email E-Mail-Adresse des Benutzers (kann auch als Login verwendet werden).
* @property passwordHash Gehashtes Passwort des Benutzers.
* @property salt Salt für das Passwort-Hashing.
* @property istAktiv Gibt an, ob dieser Benutzer aktuell aktiv ist und sich anmelden kann.
* @property personId ID der zugehörigen Person.
* @property username Benutzername für die Anmeldung.
* @property email E-Mail-Adresse des Benutzers.
* @property passwordHash Hash des Passworts.
* @property salt Salt für das Password-Hashing.
* @property istAktiv Gibt an, ob dieser Benutzer aktiv ist.
* @property istEmailVerifiziert Gibt an, ob die E-Mail-Adresse verifiziert wurde.
* @property letzteAnmeldung Zeitstempel der letzten erfolgreichen Anmeldung.
* @property fehlgeschlageneAnmeldungen Anzahl der fehlgeschlagenen Anmeldeversuche.
* @property gesperrtBis Optionaler Zeitstempel bis wann der Benutzer gesperrt ist.
* @property passwortAendernErforderlich Gibt an, ob der Benutzer sein Passwort ändern muss.
* @property fehlgeschlageneAnmeldungen Anzahl fehlgeschlagener Anmeldeversuche.
* @property gesperrtBis Zeitpunkt, bis zu dem der Account gesperrt ist (null, wenn nicht gesperrt).
* @property letzteAnmeldung Zeitpunkt der letzten erfolgreichen Anmeldung.
* @property createdAt Zeitstempel der Erstellung dieses Benutzers.
* @property updatedAt Zeitstempel der letzten Aktualisierung dieses Benutzers.
*/
@@ -45,19 +42,36 @@ data class DomUser(
var istAktiv: Boolean = true,
var istEmailVerifiziert: Boolean = false,
@Serializable(with = KotlinInstantSerializer::class)
var letzteAnmeldung: Instant? = null,
var fehlgeschlageneAnmeldungen: Int = 0,
@Serializable(with = KotlinInstantSerializer::class)
var gesperrtBis: Instant? = null,
var passwortAendernErforderlich: Boolean = false,
@Serializable(with = KotlinInstantSerializer::class)
var letzteAnmeldung: Instant? = null,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant = Clock.System.now(),
@Serializable(with = KotlinInstantSerializer::class)
var updatedAt: Instant = Clock.System.now()
)
) {
/**
* Prüft, ob der Benutzeraccount gesperrt ist.
*
* @return true, wenn der Account gesperrt ist, false sonst.
*/
fun isLocked(): Boolean {
val now = Clock.System.now()
return gesperrtBis != null && now < gesperrtBis!!
}
/**
* Prüft, ob der Benutzer anmelden kann (aktiv und nicht gesperrt).
*
* @return true, wenn der Benutzer sich anmelden kann, false sonst.
*/
fun canLogin(): Boolean {
return istAktiv && !isLocked()
}
}
@@ -1,5 +1,6 @@
package at.mocode.members.domain.repository
import at.mocode.members.domain.model.DomBerechtigung
import at.mocode.members.domain.model.DomRolleBerechtigung
import com.benasher44.uuid.Uuid
@@ -1,326 +0,0 @@
package at.mocode.members.domain.service
import at.mocode.members.domain.model.DomUser
import at.mocode.members.domain.repository.UserRepository
import at.mocode.validation.ValidationResult
import at.mocode.validation.ValidationError
import com.benasher44.uuid.Uuid
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlin.time.Duration.Companion.minutes
/**
* Service for user authentication and session management.
*
* Handles user login, logout, registration, and JWT token management.
* Coordinates between UserRepository, PasswordService, and other authentication components.
*/
class AuthenticationService(
private val userRepository: UserRepository,
private val passwordService: PasswordService,
private val jwtService: JwtService
) {
companion object {
private const val MAX_FAILED_ATTEMPTS = 5
private const val LOCKOUT_DURATION_MINUTES = 30L
}
/**
* Data class for login credentials.
*/
data class LoginCredentials(
val usernameOrEmail: String,
val password: String
)
/**
* Data class for user registration.
*/
data class UserRegistration(
val personId: Uuid,
val username: String,
val email: String,
val password: String
)
/**
* Data class for authentication result.
*/
data class AuthenticationResult(
val success: Boolean,
val user: DomUser? = null,
val token: String? = null,
val message: String? = null
)
/**
* Authenticates a user with username/email and password.
*
* @param credentials The login credentials
* @return AuthenticationResult with success status and user data
*/
suspend fun authenticate(credentials: LoginCredentials): AuthenticationResult {
try {
// Find user by username or email
val user = findUserByUsernameOrEmail(credentials.usernameOrEmail)
?: return AuthenticationResult(
success = false,
message = "Invalid username or password"
)
// Check if user is locked
if (isUserLocked(user)) {
return AuthenticationResult(
success = false,
message = "Account is temporarily locked due to too many failed login attempts"
)
}
// Check if user is active
if (!user.istAktiv) {
return AuthenticationResult(
success = false,
message = "Account is deactivated"
)
}
// Verify password
if (!passwordService.verifyPassword(credentials.password, user.passwordHash, user.salt)) {
// Increment failed attempts
userRepository.incrementFailedLoginAttempts(user.userId)
// Lock user if too many failed attempts
val updatedUser = userRepository.findById(user.userId)
if (updatedUser != null && updatedUser.fehlgeschlageneAnmeldungen >= MAX_FAILED_ATTEMPTS) {
val lockUntil = Clock.System.now().plus(30.minutes)
userRepository.lockUser(user.userId, lockUntil)
}
return AuthenticationResult(
success = false,
message = "Invalid username or password"
)
}
// Reset failed attempts on successful login
userRepository.resetFailedLoginAttempts(user.userId)
userRepository.updateLastLogin(user.userId)
// Generate JWT token
val tokenInfo = jwtService.generateToken(user)
val token = tokenInfo.token
return AuthenticationResult(
success = true,
user = user,
token = token,
message = "Login successful"
)
} catch (e: Exception) {
return AuthenticationResult(
success = false,
message = "Authentication failed: ${e.message}"
)
}
}
/**
* Data class for user registration result.
*/
data class UserRegistrationResult(
val success: Boolean,
val user: DomUser? = null,
val validationResult: ValidationResult? = null,
val message: String? = null
)
/**
* Registers a new user in the system.
*
* @param registration The user registration data
* @return UserRegistrationResult with success status and user data
*/
suspend fun registerUser(registration: UserRegistration): UserRegistrationResult {
try {
// Validate password strength
val passwordErrors = passwordService.getPasswordValidationErrors(registration.password)
if (passwordErrors.isNotEmpty()) {
val errors = passwordErrors.map { ValidationError("password", it) }
return UserRegistrationResult(
success = false,
validationResult = ValidationResult.Invalid(errors)
)
}
// Check if username already exists
val existingUserByUsername = userRepository.findByUsername(registration.username)
if (existingUserByUsername != null) {
return UserRegistrationResult(
success = false,
validationResult = ValidationResult.Invalid(listOf(ValidationError("username", "Username already exists")))
)
}
// Check if email already exists
val existingUserByEmail = userRepository.findByEmail(registration.email)
if (existingUserByEmail != null) {
return UserRegistrationResult(
success = false,
validationResult = ValidationResult.Invalid(listOf(ValidationError("email", "Email already exists")))
)
}
// Check if person already has a user account
val existingUserByPerson = userRepository.findByPersonId(registration.personId)
if (existingUserByPerson != null) {
return UserRegistrationResult(
success = false,
validationResult = ValidationResult.Invalid(listOf(ValidationError("personId", "Person already has a user account")))
)
}
// Generate salt and hash password
val salt = passwordService.generateSalt()
val passwordHash = passwordService.hashPassword(registration.password, salt)
// Create new user
val newUser = DomUser(
personId = registration.personId,
username = registration.username,
email = registration.email,
passwordHash = passwordHash,
salt = salt
)
val createdUser = userRepository.createUser(newUser)
return UserRegistrationResult(
success = true,
user = createdUser,
validationResult = ValidationResult.Valid,
message = "User registered successfully"
)
} catch (e: Exception) {
return UserRegistrationResult(
success = false,
validationResult = ValidationResult.Invalid(listOf(ValidationError("general", "Registration failed: ${e.message}"))),
message = "Registration failed: ${e.message}"
)
}
}
/**
* Data class for password change result.
*/
data class PasswordChangeResult(
val success: Boolean,
val validationResult: ValidationResult,
val message: String? = null
)
/**
* Changes a user's password.
*
* @param userId The user ID
* @param currentPassword The current password
* @param newPassword The new password
* @return PasswordChangeResult indicating success or failure
*/
suspend fun changePassword(userId: Uuid, currentPassword: String, newPassword: String): PasswordChangeResult {
try {
val user = userRepository.findById(userId)
?: return PasswordChangeResult(
success = false,
validationResult = ValidationResult.Invalid(listOf(ValidationError("userId", "User not found")))
)
// Verify current password
if (!passwordService.verifyPassword(currentPassword, user.passwordHash, user.salt)) {
return PasswordChangeResult(
success = false,
validationResult = ValidationResult.Invalid(listOf(ValidationError("currentPassword", "Current password is incorrect")))
)
}
// Validate new password strength
val passwordErrors = passwordService.getPasswordValidationErrors(newPassword)
if (passwordErrors.isNotEmpty()) {
val errors = passwordErrors.map { ValidationError("newPassword", it) }
return PasswordChangeResult(
success = false,
validationResult = ValidationResult.Invalid(errors)
)
}
// Generate new salt and hash new password
val newSalt = passwordService.generateSalt()
val newPasswordHash = passwordService.hashPassword(newPassword, newSalt)
// Update password in database
userRepository.updatePassword(userId, newPasswordHash, newSalt)
return PasswordChangeResult(
success = true,
validationResult = ValidationResult.Valid,
message = "Password changed successfully"
)
} catch (e: Exception) {
return PasswordChangeResult(
success = false,
validationResult = ValidationResult.Invalid(listOf(ValidationError("general", "Password change failed: ${e.message}"))),
message = "Password change failed: ${e.message}"
)
}
}
/**
* Finds a user by username or email.
*/
private suspend fun findUserByUsernameOrEmail(usernameOrEmail: String): DomUser? {
return userRepository.findByUsername(usernameOrEmail)
?: userRepository.findByEmail(usernameOrEmail)
}
/**
* Checks if a user is currently locked.
*/
private fun isUserLocked(user: DomUser): Boolean {
val lockUntil = user.gesperrtBis ?: return false
return Clock.System.now() < lockUntil
}
/**
* Validates a JWT token and returns the associated user.
*
* @param token The JWT token to validate
* @return DomUser if token is valid and user exists, null otherwise
*/
suspend fun validateJwtToken(token: String): DomUser? {
val payload = jwtService.validateToken(token) ?: return null
return userRepository.findById(payload.userId)
}
/**
* Refreshes a JWT token.
*
* @param token The current JWT token
* @return New token string if refresh is successful, null otherwise
*/
fun refreshJwtToken(token: String): String? {
val tokenInfo = jwtService.refreshToken(token) ?: return null
return tokenInfo.token
}
/**
* Extracts user ID from a JWT token without full validation.
*
* @param token The JWT token
* @return User ID if extractable, null otherwise
*/
fun extractUserIdFromToken(token: String): Uuid? {
return jwtService.extractUserId(token)
}
}
@@ -1,213 +1,27 @@
package at.mocode.members.domain.service
import at.mocode.members.domain.model.DomUser
import at.mocode.enums.RolleE
import at.mocode.enums.BerechtigungE
import com.benasher44.uuid.Uuid
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
/**
* Service for JWT token generation and validation.
*
* This is a simplified implementation for multiplatform compatibility.
* In a production environment, consider using platform-specific JWT libraries.
* Contains the information extracted from a JWT token.
*/
class JwtService(
private val userAuthorizationService: UserAuthorizationService,
private val secret: String = "default-secret-key-change-in-production",
private val issuer: String = "meldestelle-api",
private val audience: String = "meldestelle-users",
private val expirationTimeMillis: Long = 3600000L // 1 hour
) {
data class TokenInfo(
val userId: Uuid,
val personId: Uuid,
val username: String,
val permissions: List<BerechtigungE>,
val issuedAt: Instant,
val expiresAt: Instant
)
/**
* Data class representing JWT token information.
*/
data class TokenInfo(
val token: String,
val expiresAt: Instant,
val userId: Uuid
)
/**
* Data class representing decoded JWT payload.
*/
data class JwtPayload(
val userId: Uuid,
val username: String,
val email: String,
val roles: List<RolleE>,
val permissions: List<BerechtigungE>,
val issuedAt: Instant,
val expiresAt: Instant,
val issuer: String,
val audience: String
)
/**
* Generates a JWT token for the given user.
*
* @param user The user for whom to generate the token
* @return TokenInfo containing the token and expiration information
*/
suspend fun generateToken(user: DomUser): TokenInfo {
val now = Clock.System.now()
val expiresAt = Instant.fromEpochMilliseconds(now.toEpochMilliseconds() + expirationTimeMillis)
// Get user roles and permissions
val authInfo = userAuthorizationService.getUserAuthInfo(user.userId)
val roles = authInfo?.roles ?: emptyList()
val permissions = authInfo?.permissions ?: emptyList()
// Create a simple token structure (in production, use proper JWT library)
val payload = createPayload(user, roles, permissions, now, expiresAt)
val token = encodeToken(payload)
return TokenInfo(
token = token,
expiresAt = expiresAt,
userId = user.userId
)
}
/**
* Validates a JWT token and returns the payload if valid.
*
* @param token The JWT token to validate
* @return JwtPayload if token is valid, null otherwise
*/
fun validateToken(token: String): JwtPayload? {
return try {
val payload = decodeToken(token)
// Check if token is expired
if (Clock.System.now() > payload.expiresAt) {
return null
}
// Check issuer and audience
if (payload.issuer != issuer || payload.audience != audience) {
return null
}
payload
} catch (e: Exception) {
null
}
}
/**
* Refreshes a JWT token if it's still valid but close to expiration.
*
* @param token The current JWT token
* @return New TokenInfo if refresh is successful, null otherwise
*/
fun refreshToken(token: String): TokenInfo? {
val payload = validateToken(token) ?: return null
// Check if token is within refresh window (e.g., last 15 minutes)
val refreshWindowMillis = 15 * 60 * 1000L // 15 minutes
val now = Clock.System.now()
val timeUntilExpiry = payload.expiresAt.toEpochMilliseconds() - now.toEpochMilliseconds()
if (timeUntilExpiry > refreshWindowMillis) {
return null // Token is not yet in refresh window
}
// Create new token with same user info
val newExpiresAt = Instant.fromEpochMilliseconds(now.toEpochMilliseconds() + expirationTimeMillis)
val newPayload = payload.copy(
issuedAt = now,
expiresAt = newExpiresAt
)
val newToken = encodeToken(newPayload)
return TokenInfo(
token = newToken,
expiresAt = newExpiresAt,
userId = payload.userId
)
}
/**
* Extracts user ID from a JWT token without full validation.
*
* @param token The JWT token
* @return User ID if extractable, null otherwise
*/
fun extractUserId(token: String): Uuid? {
return try {
val payload = decodeToken(token)
payload.userId
} catch (e: Exception) {
null
}
}
/**
* Creates a JWT payload for the given user.
*/
private fun createPayload(user: DomUser, roles: List<RolleE>, permissions: List<BerechtigungE>, issuedAt: Instant, expiresAt: Instant): JwtPayload {
return JwtPayload(
userId = user.userId,
username = user.username,
email = user.email,
roles = roles,
permissions = permissions,
issuedAt = issuedAt,
expiresAt = expiresAt,
issuer = issuer,
audience = audience
)
}
/**
* Encodes a JWT payload into a token string.
* This is a simplified implementation - in production use proper JWT library.
*/
private fun encodeToken(payload: JwtPayload): String {
// Simplified token encoding (in production, use proper JWT encoding)
val header = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" // {"alg":"HS256","typ":"JWT"}
val payloadJson = """
{
"userId": "${payload.userId}",
"username": "${payload.username}",
"email": "${payload.email}",
"iat": ${payload.issuedAt.epochSeconds},
"exp": ${payload.expiresAt.epochSeconds},
"iss": "${payload.issuer}",
"aud": "${payload.audience}"
}
""".trimIndent()
// Base64 encode payload (simplified)
val encodedPayload = payloadJson.encodeToByteArray().let { bytes ->
// Simple base64-like encoding (in production use proper base64)
bytes.joinToString("") { byte ->
val hex = byte.toUByte().toString(16)
if (hex.length == 1) "0$hex" else hex
}
}
// Create signature (simplified)
val signature = (header + encodedPayload + secret).hashCode().toString()
return "$header.$encodedPayload.$signature"
}
/**
* Decodes a JWT token into a payload.
* This is a simplified implementation - in production use proper JWT library.
*/
private fun decodeToken(token: String): JwtPayload {
val parts = token.split(".")
if (parts.size != 3) {
throw IllegalArgumentException("Invalid token format")
}
// Simplified decoding (in production, use proper JWT decoding)
// This is just a placeholder implementation
throw NotImplementedError("Token decoding not implemented in simplified version")
}
/**
* Service for JWT token generation and validation.
* Platform-specific implementation required.
*/
expect class JwtService {
suspend fun createToken(user: DomUser): String
fun validateToken(token: String): TokenInfo?
}
@@ -1,96 +1,27 @@
package at.mocode.members.domain.service
import kotlin.random.Random
/**
* Service for password hashing and verification.
*
* Provides secure password hashing using salt and verification methods.
* This is a simplified implementation - in production, consider using
* more robust hashing algorithms like bcrypt, scrypt, or Argon2.
* Platform-specific implementation required for secure password handling.
*/
class PasswordService {
expect class PasswordService {
fun generateSalt(): String
fun hashPassword(password: String, salt: String): String
fun verifyPassword(inputPassword: String, storedHash: String, storedSalt: String): Boolean
fun generateRandomPassword(length: Int = 16): String
fun checkPasswordStrength(password: String): PasswordStrength
}
companion object {
private const val SALT_LENGTH = 32
}
/**
* Generates a random salt for password hashing.
*
* @return A random salt string
*/
fun generateSalt(): String {
val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
return (1..SALT_LENGTH)
.map { chars[Random.nextInt(chars.length)] }
.joinToString("")
}
/**
* Hashes a password with the given salt.
*
* @param password The plain text password
* @param salt The salt to use for hashing
* @return The hashed password
*/
fun hashPassword(password: String, salt: String): String {
// Simple hash implementation - in production use bcrypt, scrypt, or Argon2
val combined = password + salt
return combined.hashCode().toString() + salt.hashCode().toString()
}
/**
* Verifies a password against a stored hash and salt.
*
* @param password The plain text password to verify
* @param storedHash The stored password hash
* @param salt The salt used for the stored hash
* @return True if the password matches, false otherwise
*/
fun verifyPassword(password: String, storedHash: String, salt: String): Boolean {
val hashedInput = hashPassword(password, salt)
return hashedInput == storedHash
}
/**
* Validates password strength.
*
* @param password The password to validate
* @return True if the password meets minimum requirements
*/
fun isPasswordValid(password: String): Boolean {
return password.length >= 8 &&
password.any { it.isUpperCase() } &&
password.any { it.isLowerCase() } &&
password.any { it.isDigit() }
}
/**
* Gets password validation error messages.
*
* @param password The password to validate
* @return List of validation error messages, empty if valid
*/
fun getPasswordValidationErrors(password: String): List<String> {
val errors = mutableListOf<String>()
if (password.length < 8) {
errors.add("Password must be at least 8 characters long")
}
if (!password.any { it.isUpperCase() }) {
errors.add("Password must contain at least one uppercase letter")
}
if (!password.any { it.isLowerCase() }) {
errors.add("Password must contain at least one lowercase letter")
}
if (!password.any { it.isDigit() }) {
errors.add("Password must contain at least one digit")
}
return errors
/**
* Contains information about password strength.
*/
data class PasswordStrength(
val strength: Strength,
val score: Int,
val maxScore: Int,
val issues: List<String>
) {
enum class Strength {
WEAK, MEDIUM, STRONG
}
}
@@ -164,4 +164,15 @@ class UserAuthorizationService(
val authInfo = getUserAuthInfo(userId) ?: return false
return authInfo.permissions.contains(permission)
}
/**
* Gets all permissions for a person (used by JwtService).
*
* @param personId The person ID
* @return List of permissions for the person
*/
suspend fun getUserPermissions(personId: Uuid): List<BerechtigungE> {
val roles = getUserRoles(personId)
return getPermissionsForRoles(roles)
}
}
@@ -0,0 +1,167 @@
package at.mocode.members.domain.service
import at.mocode.enums.BerechtigungE
import at.mocode.members.domain.model.DomUser
import com.benasher44.uuid.Uuid
import com.benasher44.uuid.uuidOf
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.encodeToString
import kotlinx.serialization.decodeFromString
/**
* Service für die Erstellung und Validierung von JWT-Tokens.
* JavaScript-Implementation mit einfacher JWT-Funktionalität.
*/
actual class JwtService(private val userAuthorizationService: UserAuthorizationService) {
companion object {
private const val SECRET = "default-js-secret-key-change-in-production"
private const val ISSUER = "meldestelle-js"
private const val AUDIENCE = "meldestelle-users"
private const val EXPIRATION_MINUTES = 60
}
@Serializable
private data class JwtHeader(
val alg: String = "HS256",
val typ: String = "JWT"
)
@Serializable
private data class JwtPayload(
val iss: String,
val aud: String,
val sub: String,
val iat: Long,
val exp: Long,
val username: String,
val personId: String,
val permissions: List<String>
)
/**
* Erstellt ein JWT-Token für einen Benutzer.
*
* @param user Der Benutzer, für den das Token erstellt werden soll
* @return Das erstellte JWT-Token
*/
actual suspend fun createToken(user: DomUser): String {
// Berechtigungen des Benutzers ermitteln
val permissions = userAuthorizationService.getUserPermissions(user.personId)
// Aktuelle Zeit und Ablaufzeit berechnen
val now = Clock.System.now()
val expiryTime = now.plus(kotlin.time.Duration.parse("${EXPIRATION_MINUTES}m"))
// Header erstellen
val header = JwtHeader()
val headerJson = Json.encodeToString(header)
val headerBase64 = js("btoa(headerJson)") as String
// Payload erstellen
val payload = JwtPayload(
iss = ISSUER,
aud = AUDIENCE,
sub = user.userId.toString(),
iat = now.epochSeconds,
exp = expiryTime.epochSeconds,
username = user.username,
personId = user.personId.toString(),
permissions = permissions.map { it.name }
)
val payloadJson = Json.encodeToString(payload)
val payloadBase64 = js("btoa(payloadJson)") as String
// Signatur erstellen (vereinfacht für JS)
val message = "$headerBase64.$payloadBase64"
val signature = createSignature(message, SECRET)
return "$message.$signature"
}
/**
* Validiert ein JWT-Token und extrahiert die enthaltenen Informationen.
*
* @param token Das zu validierende JWT-Token
* @return Die im Token enthaltenen Informationen, oder null bei ungültigem Token
*/
actual fun validateToken(token: String): TokenInfo? {
return try {
val parts = token.split(".")
if (parts.size != 3) return null
val headerBase64 = parts[0]
val payloadBase64 = parts[1]
val signature = parts[2]
// Signatur überprüfen
val message = "$headerBase64.$payloadBase64"
val expectedSignature = createSignature(message, SECRET)
if (signature != expectedSignature) return null
// Payload dekodieren
val payloadJson = js("atob(payloadBase64)") as String
val payload = Json.decodeFromString<JwtPayload>(payloadJson)
// Ablaufzeit überprüfen
val now = Clock.System.now()
if (now.epochSeconds > payload.exp) return null
// Berechtigungen konvertieren
val permissions = payload.permissions.mapNotNull { permString ->
try {
BerechtigungE.valueOf(permString)
} catch (e: IllegalArgumentException) {
null
}
}
TokenInfo(
userId = parseUuidFromString(payload.sub),
personId = parseUuidFromString(payload.personId),
username = payload.username,
permissions = permissions,
issuedAt = Instant.fromEpochSeconds(payload.iat),
expiresAt = Instant.fromEpochSeconds(payload.exp)
)
} catch (e: Exception) {
null
}
}
/**
* Erstellt eine einfache Signatur für das JWT-Token.
* Dies ist eine vereinfachte Implementation für JS.
*/
private fun createSignature(message: String, secret: String): String {
val combined = message + secret
var hash = 0
for (i in combined.indices) {
val char = combined[i].code
hash = ((hash shl 5) - hash) + char
hash = hash and hash // Convert to 32-bit integer
}
val hashString = hash.toString(16).padStart(8, '0')
return js("btoa(hashString)") as String
}
/**
* Parst einen UUID-String zu einem Uuid-Objekt.
* Workaround für JS-Platform.
*/
private fun parseUuidFromString(uuidString: String): Uuid {
// Remove hyphens and convert to ByteArray
val cleanUuid = uuidString.replace("-", "")
val bytes = ByteArray(16)
for (i in 0 until 16) {
val hexPair = cleanUuid.substring(i * 2, i * 2 + 2)
bytes[i] = hexPair.toInt(16).toByte()
}
return Uuid(bytes)
}
}
@@ -0,0 +1,121 @@
package at.mocode.members.domain.service
import kotlin.random.Random
/**
* Service für die sichere Verarbeitung von Passwörtern.
* JavaScript/Browser-Implementation.
*/
actual class PasswordService {
companion object {
private const val SALT_LENGTH = 32
}
/**
* Generiert einen zufälligen Salt für das Passwort-Hashing.
*
* @return Base64-codierter Salt als String
*/
actual fun generateSalt(): String {
// Generate random bytes as string
val saltChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
return (1..SALT_LENGTH)
.map { saltChars[Random.nextInt(saltChars.length)] }
.joinToString("")
}
/**
* Hasht ein Passwort mit dem angegebenen Salt.
*
* @param password Das zu hashende Passwort
* @param salt Der zu verwendende Salt als Base64-String
* @return Der Passwort-Hash als Base64-String
*/
actual fun hashPassword(password: String, salt: String): String {
// Simple hash implementation for JS
val combined = password + salt
// Simple hash using built-in functions
var hash = 0
for (i in combined.indices) {
val char = combined[i].code
hash = ((hash shl 5) - hash) + char
hash = hash and hash // Convert to 32-bit integer
}
// Convert to a more secure representation
val hashString = hash.toString(16).padStart(8, '0')
val extendedHash = hashString.repeat(16) // Make it longer
// Use JS btoa for base64 encoding
return js("btoa(extendedHash)") as String
}
/**
* Überprüft, ob ein eingegebenes Passwort mit einem gespeicherten Hash übereinstimmt.
*
* @param inputPassword Das eingegebene Passwort
* @param storedHash Der gespeicherte Passwort-Hash
* @param storedSalt Der gespeicherte Salt
* @return true, wenn das Passwort übereinstimmt, sonst false
*/
actual fun verifyPassword(inputPassword: String, storedHash: String, storedSalt: String): Boolean {
val calculatedHash = hashPassword(inputPassword, storedSalt)
return calculatedHash == storedHash
}
/**
* Generiert ein zufälliges, sicheres Passwort.
*
* @param length Die Länge des zu generierenden Passworts
* @return Das generierte Passwort
*/
actual fun generateRandomPassword(length: Int): String {
val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{};:,.<>?"
return (1..length)
.map { chars[Random.nextInt(chars.length)] }
.joinToString("")
}
/**
* Überprüft die Stärke eines Passworts.
*
* @param password Das zu überprüfende Passwort
* @return Ein PasswordStrength-Objekt mit Informationen zur Passwortstärke
*/
actual fun checkPasswordStrength(password: String): PasswordStrength {
val length = password.length
val hasLowercase = password.any { it.isLowerCase() }
val hasUppercase = password.any { it.isUpperCase() }
val hasDigit = password.any { it.isDigit() }
val hasSpecialChar = password.any { !it.isLetterOrDigit() }
var score = 0
if (length >= 8) score++
if (length >= 12) score++
if (hasLowercase) score++
if (hasUppercase) score++
if (hasDigit) score++
if (hasSpecialChar) score++
val strength = when {
score <= 2 -> PasswordStrength.Strength.WEAK
score <= 4 -> PasswordStrength.Strength.MEDIUM
else -> PasswordStrength.Strength.STRONG
}
return PasswordStrength(
strength = strength,
score = score,
maxScore = 6,
issues = buildList {
if (length < 8) add("Passwort sollte mindestens 8 Zeichen haben")
if (!hasLowercase) add("Passwort sollte Kleinbuchstaben enthalten")
if (!hasUppercase) add("Passwort sollte Großbuchstaben enthalten")
if (!hasDigit) add("Passwort sollte Ziffern enthalten")
if (!hasSpecialChar) add("Passwort sollte Sonderzeichen enthalten")
}
)
}
}
@@ -0,0 +1,281 @@
package at.mocode.members.domain.service
import at.mocode.members.domain.model.DomUser
import at.mocode.members.domain.repository.UserRepository
import com.benasher44.uuid.Uuid
import kotlinx.datetime.Clock
/**
* Service für die Authentifizierung von Benutzern im System.
*/
class AuthenticationService(
private val userRepository: UserRepository,
private val passwordService: PasswordService,
private val jwtService: JwtService
) {
companion object {
// Konfigurierbare Werte für die Kontosperrung
private const val MAX_FAILED_LOGIN_ATTEMPTS = 5
private const val LOCK_DURATION_MINUTES = 15L
}
/**
* Authentifiziert einen Benutzer anhand von Benutzername und Passwort.
*
* @param username Der Benutzername
* @param password Das Passwort
* @return AuthResult mit dem Ergebnis der Authentifizierung
*/
suspend fun authenticate(username: String, password: String): AuthResult {
// Benutzer suchen
val user = userRepository.findByUsername(username)
?: return AuthResult.Failure("Ungültiger Benutzername oder Passwort")
// Prüfen, ob der Benutzer aktiv ist
if (!user.istAktiv) {
return AuthResult.Failure("Dieser Account ist deaktiviert")
}
// Prüfen, ob der Account gesperrt ist
if (user.isLocked()) {
return AuthResult.Locked(user.gesperrtBis!!)
}
// Passwort überprüfen
if (!passwordService.verifyPassword(password, user.passwordHash, user.salt)) {
// Fehlgeschlagene Anmeldeversuche erhöhen
userRepository.incrementFailedLoginAttempts(user.userId)
// Benutzer sperren, wenn zu viele Anmeldeversuche fehlgeschlagen sind
val updatedUser = userRepository.findById(user.userId)!!
if (updatedUser.fehlgeschlageneAnmeldungen >= MAX_FAILED_LOGIN_ATTEMPTS) {
val lockUntil = Clock.System.now().plus(kotlin.time.Duration.parse("${LOCK_DURATION_MINUTES}m"))
userRepository.lockUser(user.userId, lockUntil)
return AuthResult.Locked(lockUntil)
}
return AuthResult.Failure("Ungültiger Benutzername oder Passwort")
}
// Erfolgreiche Anmeldung - Fehlgeschlagene Anmeldeversuche zurücksetzen und letzten Login aktualisieren
userRepository.resetFailedLoginAttempts(user.userId)
userRepository.updateLastLogin(user.userId)
// JWT-Token erstellen
val token = jwtService.createToken(user)
return AuthResult.Success(token, user)
}
/**
* Registriert einen neuen Benutzer im System.
*
* @param username Der Benutzername
* @param email Die E-Mail-Adresse
* @param password Das Passwort
* @param personId Die ID der zugehörigen Person
* @return RegisterResult mit dem Ergebnis der Registrierung
*/
suspend fun registerUser(username: String, email: String, password: String, personId: Uuid): RegisterResult {
// Prüfen, ob Benutzername bereits existiert
if (userRepository.findByUsername(username) != null) {
return RegisterResult.Failure("Benutzername wird bereits verwendet")
}
// Prüfen, ob E-Mail bereits existiert
if (userRepository.findByEmail(email) != null) {
return RegisterResult.Failure("E-Mail-Adresse wird bereits verwendet")
}
// Prüfen, ob Person bereits einen Benutzer hat
if (userRepository.findByPersonId(personId) != null) {
return RegisterResult.Failure("Diese Person hat bereits einen Benutzeraccount")
}
// Passwort-Stärke prüfen
val passwordStrength = passwordService.checkPasswordStrength(password)
if (passwordStrength.strength == PasswordStrength.Strength.WEAK) {
return RegisterResult.WeakPassword(passwordStrength.issues)
}
// Salt und Hash generieren
val salt = passwordService.generateSalt()
val passwordHash = passwordService.hashPassword(password, salt)
// Benutzer erstellen
val user = DomUser(
personId = personId,
username = username,
email = email,
passwordHash = passwordHash,
salt = salt
)
// Benutzer speichern
val createdUser = userRepository.createUser(user)
return RegisterResult.Success(createdUser)
}
/**
* Ändert das Passwort eines Benutzers.
*
* @param userId Die ID des Benutzers
* @param currentPassword Das aktuelle Passwort
* @param newPassword Das neue Passwort
* @return PasswordChangeResult mit dem Ergebnis der Passwortänderung
*/
suspend fun changePassword(userId: Uuid, currentPassword: String, newPassword: String): PasswordChangeResult {
// Benutzer suchen
val user = userRepository.findById(userId)
?: return PasswordChangeResult.Failure("Benutzer nicht gefunden")
// Aktuelles Passwort überprüfen
if (!passwordService.verifyPassword(currentPassword, user.passwordHash, user.salt)) {
return PasswordChangeResult.Failure("Aktuelles Passwort ist falsch")
}
// Passwort-Stärke prüfen
val passwordStrength = passwordService.checkPasswordStrength(newPassword)
if (passwordStrength.strength == PasswordStrength.Strength.WEAK) {
return PasswordChangeResult.WeakPassword(passwordStrength.issues)
}
// Neues Passwort setzen
val salt = passwordService.generateSalt()
val passwordHash = passwordService.hashPassword(newPassword, salt)
userRepository.updatePassword(userId, passwordHash, salt)
return PasswordChangeResult.Success
}
/**
* Setzt das Passwort eines Benutzers zurück.
*
* @param userId Die ID des Benutzers
* @param newPassword Das neue Passwort
* @return PasswordResetResult mit dem Ergebnis der Passwortzurücksetzung
*/
suspend fun resetPassword(userId: Uuid, newPassword: String): PasswordResetResult {
// Benutzer suchen
val user = userRepository.findById(userId)
?: return PasswordResetResult.Failure("Benutzer nicht gefunden")
// Passwort-Stärke prüfen
val passwordStrength = passwordService.checkPasswordStrength(newPassword)
if (passwordStrength.strength == PasswordStrength.Strength.WEAK) {
return PasswordResetResult.WeakPassword(passwordStrength.issues)
}
// Neues Passwort setzen
val salt = passwordService.generateSalt()
val passwordHash = passwordService.hashPassword(newPassword, salt)
userRepository.updatePassword(userId, passwordHash, salt)
return PasswordResetResult.Success
}
/**
* Ergebnis einer Authentifizierung.
*/
sealed class AuthResult {
/**
* Erfolgreiche Authentifizierung.
*
* @property token Das JWT-Token für den authentifizierten Benutzer
* @property user Der authentifizierte Benutzer
*/
data class Success(val token: String, val user: DomUser) : AuthResult()
/**
* Fehlgeschlagene Authentifizierung.
*
* @property reason Der Grund für den Fehlschlag
*/
data class Failure(val reason: String) : AuthResult()
/**
* Account ist gesperrt.
*
* @property lockedUntil Zeitpunkt, bis zu dem der Account gesperrt ist
*/
data class Locked(val lockedUntil: kotlinx.datetime.Instant) : AuthResult()
}
/**
* Ergebnis einer Benutzerregistrierung.
*/
sealed class RegisterResult {
/**
* Erfolgreiche Registrierung.
*
* @property user Der erstellte Benutzer
*/
data class Success(val user: DomUser) : RegisterResult()
/**
* Fehlgeschlagene Registrierung.
*
* @property reason Der Grund für den Fehlschlag
*/
data class Failure(val reason: String) : RegisterResult()
/**
* Zu schwaches Passwort.
*
* @property issues Liste der Probleme mit dem Passwort
*/
data class WeakPassword(val issues: List<String>) : RegisterResult()
}
/**
* Ergebnis einer Passwortänderung.
*/
sealed class PasswordChangeResult {
/**
* Erfolgreiche Passwortänderung.
*/
object Success : PasswordChangeResult()
/**
* Fehlgeschlagene Passwortänderung.
*
* @property reason Der Grund für den Fehlschlag
*/
data class Failure(val reason: String) : PasswordChangeResult()
/**
* Zu schwaches Passwort.
*
* @property issues Liste der Probleme mit dem Passwort
*/
data class WeakPassword(val issues: List<String>) : PasswordChangeResult()
}
/**
* Ergebnis einer Passwortzurücksetzung.
*/
sealed class PasswordResetResult {
/**
* Erfolgreiche Passwortzurücksetzung.
*/
object Success : PasswordResetResult()
/**
* Fehlgeschlagene Passwortzurücksetzung.
*
* @property reason Der Grund für den Fehlschlag
*/
data class Failure(val reason: String) : PasswordResetResult()
/**
* Zu schwaches Passwort.
*
* @property issues Liste der Probleme mit dem Passwort
*/
data class WeakPassword(val issues: List<String>) : PasswordResetResult()
}
}
@@ -0,0 +1,91 @@
package at.mocode.members.domain.service
import at.mocode.enums.BerechtigungE
import at.mocode.members.domain.model.DomUser
import at.mocode.shared.config.AppConfig
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import com.benasher44.uuid.Uuid
import java.util.*
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.datetime.toJavaInstant
/**
* Service für die Erstellung und Validierung von JWT-Tokens.
*/
actual class JwtService(private val userAuthorizationService: UserAuthorizationService) {
// JWT-Konfiguration aus der Anwendungskonfiguration
private val jwtConfig = AppConfig.security.jwt
// HMAC-Algorithmus mit dem konfigurierten Secret
private val algorithm = Algorithm.HMAC512(jwtConfig.secret)
/**
* Erstellt ein JWT-Token für einen Benutzer.
*
* @param user Der Benutzer, für den das Token erstellt werden soll
* @return Das erstellte JWT-Token
*/
actual suspend fun createToken(user: DomUser): String {
// Berechtigungen des Benutzers ermitteln
val permissions = userAuthorizationService.getUserPermissions(user.personId)
// Aktuelle Zeit und Ablaufzeit berechnen
val now = Clock.System.now()
val expiryTime = now.plus(kotlin.time.Duration.parse("${jwtConfig.expirationInMinutes}m"))
// Token erstellen
return JWT.create()
.withIssuer(jwtConfig.issuer)
.withAudience(jwtConfig.audience)
.withIssuedAt(Date.from(now.toJavaInstant()))
.withExpiresAt(Date.from(expiryTime.toJavaInstant()))
.withSubject(user.userId.toString())
.withClaim("username", user.username)
.withClaim("personId", user.personId.toString())
.withArrayClaim("permissions", permissions.map { it.name }.toTypedArray())
.sign(algorithm)
}
/**
* Validiert ein JWT-Token und extrahiert die enthaltenen Informationen.
*
* @param token Das zu validierende JWT-Token
* @return Die im Token enthaltenen Informationen, oder null bei ungültigem Token
*/
actual fun validateToken(token: String): TokenInfo? {
return try {
val verifier = JWT.require(algorithm)
.withIssuer(jwtConfig.issuer)
.withAudience(jwtConfig.audience)
.build()
val jwt = verifier.verify(token)
val userId = UUID.fromString(jwt.subject)
val personId = UUID.fromString(jwt.getClaim("personId").asString())
val username = jwt.getClaim("username").asString()
val permissionStrings = jwt.getClaim("permissions").asList(String::class.java)
val permissions = permissionStrings.mapNotNull { permString ->
try {
BerechtigungE.valueOf(permString)
} catch (e: IllegalArgumentException) {
null
}
}
TokenInfo(
userId = Uuid.fromString(userId.toString()),
personId = Uuid.fromString(personId.toString()),
username = username,
permissions = permissions,
issuedAt = Instant.fromEpochMilliseconds(jwt.issuedAt.time),
expiresAt = Instant.fromEpochMilliseconds(jwt.expiresAt.time)
)
} catch (e: Exception) {
null
}
}
}
@@ -0,0 +1,116 @@
package at.mocode.members.domain.service
import java.security.SecureRandom
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec
import java.util.*
/**
* Service für die sichere Verarbeitung von Passwörtern.
* Verwendet PBKDF2 mit HMAC SHA-512 für das Password Hashing.
*/
actual class PasswordService {
companion object {
private const val ALGORITHM = "PBKDF2WithHmacSHA512"
private const val ITERATIONS = 65536
private const val KEY_LENGTH = 512
private const val SALT_LENGTH = 32
}
private val secureRandom = SecureRandom()
/**
* Generiert einen zufälligen Salt für das Passwort-Hashing.
*
* @return Base64-codierter Salt als String
*/
actual fun generateSalt(): String {
val salt = ByteArray(SALT_LENGTH)
secureRandom.nextBytes(salt)
return Base64.getEncoder().encodeToString(salt)
}
/**
* Hasht ein Passwort mit dem angegebenen Salt.
*
* @param password Das zu hashende Passwort
* @param salt Der zu verwendende Salt als Base64-String
* @return Der Passwort-Hash als Base64-String
*/
actual fun hashPassword(password: String, salt: String): String {
val saltBytes = Base64.getDecoder().decode(salt)
val spec = PBEKeySpec(password.toCharArray(), saltBytes, ITERATIONS, KEY_LENGTH)
val factory = SecretKeyFactory.getInstance(ALGORITHM)
val hash = factory.generateSecret(spec).encoded
return Base64.getEncoder().encodeToString(hash)
}
/**
* Überprüft, ob ein eingegebenes Passwort mit einem gespeicherten Hash übereinstimmt.
*
* @param inputPassword Das eingegebene Passwort
* @param storedHash Der gespeicherte Passwort-Hash
* @param storedSalt Der gespeicherte Salt
* @return true, wenn das Passwort übereinstimmt, sonst false
*/
actual fun verifyPassword(inputPassword: String, storedHash: String, storedSalt: String): Boolean {
val calculatedHash = hashPassword(inputPassword, storedSalt)
return calculatedHash == storedHash
}
/**
* Generiert ein zufälliges, sicheres Passwort.
*
* @param length Die Länge des zu generierenden Passworts
* @return Das generierte Passwort
*/
actual fun generateRandomPassword(length: Int): String {
val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{};:,.<>?"
val random = SecureRandom()
return (1..length)
.map { chars[random.nextInt(chars.length)] }
.joinToString("")
}
/**
* Überprüft die Stärke eines Passworts.
*
* @param password Das zu überprüfende Passwort
* @return Ein PasswordStrength-Objekt mit Informationen zur Passwortstärke
*/
actual fun checkPasswordStrength(password: String): PasswordStrength {
val length = password.length
val hasLowercase = password.any { it.isLowerCase() }
val hasUppercase = password.any { it.isUpperCase() }
val hasDigit = password.any { it.isDigit() }
val hasSpecialChar = password.any { !it.isLetterOrDigit() }
var score = 0
if (length >= 8) score++
if (length >= 12) score++
if (hasLowercase) score++
if (hasUppercase) score++
if (hasDigit) score++
if (hasSpecialChar) score++
val strength = when {
score <= 2 -> PasswordStrength.Strength.WEAK
score <= 4 -> PasswordStrength.Strength.MEDIUM
else -> PasswordStrength.Strength.STRONG
}
return PasswordStrength(
strength = strength,
score = score,
maxScore = 6,
issues = buildList {
if (length < 8) add("Passwort sollte mindestens 8 Zeichen haben")
if (!hasLowercase) add("Passwort sollte Kleinbuchstaben enthalten")
if (!hasUppercase) add("Passwort sollte Großbuchstaben enthalten")
if (!hasDigit) add("Passwort sollte Ziffern enthalten")
if (!hasSpecialChar) add("Passwort sollte Sonderzeichen enthalten")
}
)
}
}
@@ -1,114 +1,29 @@
package at.mocode.members.infrastructure.repository
// Import table definition and extension functions
import at.mocode.enums.BerechtigungE
import at.mocode.members.domain.model.DomBerechtigung
import at.mocode.members.domain.repository.BerechtigungRepository
import at.mocode.members.infrastructure.table.BerechtigungTable
import at.mocode.shared.database.DatabaseFactory
import com.benasher44.uuid.Uuid
import kotlinx.datetime.Clock
import org.jetbrains.exposed.sql.ResultRow
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import kotlinx.datetime.TimeZone
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.update
/**
* Exposed-based implementation of BerechtigungRepository.
*
* This implementation provides data persistence for Berechtigung entities
* using the Exposed SQL framework and PostgreSQL database.
* Implementierung des BerechtigungRepository für die Datenbankzugriffe.
*/
class BerechtigungRepositoryImpl : BerechtigungRepository {
override suspend fun save(berechtigung: DomBerechtigung): DomBerechtigung {
val now = Clock.System.now()
val updatedBerechtigung = berechtigung.copy(updatedAt = now)
BerechtigungTable.insertOrUpdate(BerechtigungTable.id) {
it[id] = berechtigung.berechtigungId
it[berechtigungTyp] = berechtigung.berechtigungTyp
it[name] = berechtigung.name
it[beschreibung] = berechtigung.beschreibung
it[ressource] = berechtigung.ressource
it[aktion] = berechtigung.aktion
it[istAktiv] = berechtigung.istAktiv
it[istSystemBerechtigung] = berechtigung.istSystemBerechtigung
it[createdAt] = berechtigung.createdAt.toLocalDateTime()
it[updatedAt] = updatedBerechtigung.updatedAt.toLocalDateTime()
}
return updatedBerechtigung
}
override suspend fun findById(berechtigungId: Uuid): DomBerechtigung? {
return BerechtigungTable.selectAll().where { BerechtigungTable.id eq berechtigungId }
.map { rowToDomBerechtigung(it) }
.singleOrNull()
}
override suspend fun findByTyp(berechtigungTyp: BerechtigungE): DomBerechtigung? {
return BerechtigungTable.selectAll().where { BerechtigungTable.berechtigungTyp eq berechtigungTyp }
.map { rowToDomBerechtigung(it) }
.singleOrNull()
}
override suspend fun findByName(name: String): List<DomBerechtigung> {
val searchPattern = "%$name%"
return BerechtigungTable.selectAll().where { BerechtigungTable.name like searchPattern }
.map { rowToDomBerechtigung(it) }
}
override suspend fun findByRessource(ressource: String): List<DomBerechtigung> {
return BerechtigungTable.selectAll().where { BerechtigungTable.ressource eq ressource }
.map { rowToDomBerechtigung(it) }
}
override suspend fun findByAktion(aktion: String): List<DomBerechtigung> {
return BerechtigungTable.selectAll().where { BerechtigungTable.aktion eq aktion }
.map { rowToDomBerechtigung(it) }
}
override suspend fun findAllActive(): List<DomBerechtigung> {
return BerechtigungTable.selectAll().where { BerechtigungTable.istAktiv eq true }
.map { rowToDomBerechtigung(it) }
}
override suspend fun findAll(): List<DomBerechtigung> {
return BerechtigungTable.selectAll()
.map { rowToDomBerechtigung(it) }
}
override suspend fun deactivateBerechtigung(berechtigungId: Uuid): Boolean {
val now = Clock.System.now()
val updatedRows = BerechtigungTable.update({ BerechtigungTable.id eq berechtigungId }) {
it[istAktiv] = false
it[updatedAt] = now.toLocalDateTime()
}
return updatedRows > 0
}
override suspend fun deleteBerechtigung(berechtigungId: Uuid): Boolean {
// Only allow deletion of non-system permissions
val berechtigung = findById(berechtigungId)
if (berechtigung?.istSystemBerechtigung == true) {
return false
}
val deletedRows = BerechtigungTable.deleteWhere { BerechtigungTable.id eq berechtigungId }
return deletedRows > 0
}
override suspend fun existsByTyp(berechtigungTyp: BerechtigungE): Boolean {
return BerechtigungTable.selectAll().where { BerechtigungTable.berechtigungTyp eq berechtigungTyp }
.count() > 0
}
/**
* Converts a database row to a DomBerechtigung domain object.
* Konvertiert eine Datenbankzeile in ein Domain-Objekt.
*/
private fun rowToDomBerechtigung(row: ResultRow): DomBerechtigung {
return DomBerechtigung(
berechtigungId = row[BerechtigungTable.id].value,
berechtigungId = row[BerechtigungTable.id],
berechtigungTyp = row[BerechtigungTable.berechtigungTyp],
name = row[BerechtigungTable.name],
beschreibung = row[BerechtigungTable.beschreibung],
@@ -116,8 +31,114 @@ class BerechtigungRepositoryImpl : BerechtigungRepository {
aktion = row[BerechtigungTable.aktion],
istAktiv = row[BerechtigungTable.istAktiv],
istSystemBerechtigung = row[BerechtigungTable.istSystemBerechtigung],
createdAt = row[BerechtigungTable.createdAt].toInstant(),
updatedAt = row[BerechtigungTable.updatedAt].toInstant()
createdAt = row[BerechtigungTable.createdAt].toInstant(TimeZone.UTC),
updatedAt = row[BerechtigungTable.updatedAt].toInstant(TimeZone.UTC)
)
}
override suspend fun save(berechtigung: DomBerechtigung): DomBerechtigung = DatabaseFactory.dbQuery {
val now = Clock.System.now()
val existingBerechtigung = findById(berechtigung.berechtigungId)
if (existingBerechtigung == null) {
// Insert new permission
BerechtigungTable.insert { stmt ->
stmt[BerechtigungTable.id] = berechtigung.berechtigungId
stmt[BerechtigungTable.berechtigungTyp] = berechtigung.berechtigungTyp
stmt[BerechtigungTable.name] = berechtigung.name
stmt[BerechtigungTable.beschreibung] = berechtigung.beschreibung
stmt[BerechtigungTable.ressource] = berechtigung.ressource
stmt[BerechtigungTable.aktion] = berechtigung.aktion
stmt[BerechtigungTable.istAktiv] = berechtigung.istAktiv
stmt[BerechtigungTable.istSystemBerechtigung] = berechtigung.istSystemBerechtigung
stmt[BerechtigungTable.createdAt] = berechtigung.createdAt.toLocalDateTime(TimeZone.UTC)
stmt[BerechtigungTable.updatedAt] = now.toLocalDateTime(TimeZone.UTC)
}
} else {
// Update existing permission
BerechtigungTable.update({ BerechtigungTable.id eq berechtigung.berechtigungId }) { stmt ->
stmt[BerechtigungTable.berechtigungTyp] = berechtigung.berechtigungTyp
stmt[BerechtigungTable.name] = berechtigung.name
stmt[BerechtigungTable.beschreibung] = berechtigung.beschreibung
stmt[BerechtigungTable.ressource] = berechtigung.ressource
stmt[BerechtigungTable.aktion] = berechtigung.aktion
stmt[BerechtigungTable.istAktiv] = berechtigung.istAktiv
stmt[BerechtigungTable.istSystemBerechtigung] = berechtigung.istSystemBerechtigung
stmt[BerechtigungTable.updatedAt] = now.toLocalDateTime(TimeZone.UTC)
}
}
// Return updated object
berechtigung.copy(updatedAt = now)
}
override suspend fun findById(berechtigungId: Uuid): DomBerechtigung? = DatabaseFactory.dbQuery {
BerechtigungTable.select { BerechtigungTable.id eq berechtigungId }
.map(::rowToDomBerechtigung)
.singleOrNull()
}
override suspend fun findByTyp(berechtigungTyp: BerechtigungE): DomBerechtigung? = DatabaseFactory.dbQuery {
BerechtigungTable.select { BerechtigungTable.berechtigungTyp eq berechtigungTyp }
.map(::rowToDomBerechtigung)
.singleOrNull()
}
override suspend fun findByName(name: String): List<DomBerechtigung> = DatabaseFactory.dbQuery {
BerechtigungTable.select { BerechtigungTable.name like "%$name%" }
.map(::rowToDomBerechtigung)
}
override suspend fun findByRessource(ressource: String): List<DomBerechtigung> = DatabaseFactory.dbQuery {
BerechtigungTable.select { BerechtigungTable.ressource eq ressource }
.map(::rowToDomBerechtigung)
}
override suspend fun findByAktion(aktion: String): List<DomBerechtigung> = DatabaseFactory.dbQuery {
BerechtigungTable.select { BerechtigungTable.aktion eq aktion }
.map(::rowToDomBerechtigung)
}
override suspend fun findAllActive(): List<DomBerechtigung> = DatabaseFactory.dbQuery {
BerechtigungTable.select { BerechtigungTable.istAktiv eq true }
.map(::rowToDomBerechtigung)
}
override suspend fun findAll(): List<DomBerechtigung> = DatabaseFactory.dbQuery {
BerechtigungTable.selectAll()
.map(::rowToDomBerechtigung)
}
override suspend fun deactivateBerechtigung(berechtigungId: Uuid): Boolean = DatabaseFactory.dbQuery {
val now = Clock.System.now()
// Prüfen, ob es sich um eine Systemberechtigung handelt
val berechtigung = findById(berechtigungId)
if (berechtigung?.istSystemBerechtigung == true) {
return@dbQuery false
}
val rowsUpdated = BerechtigungTable.update({ BerechtigungTable.id eq berechtigungId }) { stmt ->
stmt[BerechtigungTable.istAktiv] = false
stmt[BerechtigungTable.updatedAt] = now.toLocalDateTime(TimeZone.UTC)
}
rowsUpdated > 0
}
override suspend fun deleteBerechtigung(berechtigungId: Uuid): Boolean = DatabaseFactory.dbQuery {
// Prüfen, ob es sich um eine Systemberechtigung handelt
val berechtigung = findById(berechtigungId)
if (berechtigung?.istSystemBerechtigung == true) {
return@dbQuery false
}
val rowsDeleted = BerechtigungTable.deleteWhere { BerechtigungTable.id eq berechtigungId }
rowsDeleted > 0
}
override suspend fun existsByTyp(berechtigungTyp: BerechtigungE): Boolean = DatabaseFactory.dbQuery {
BerechtigungTable.select { BerechtigungTable.berechtigungTyp eq berechtigungTyp }
.count() > 0
}
}
@@ -1,15 +1,16 @@
package at.mocode.members.infrastructure.repository
// Import table definition and extension functions
import at.mocode.members.domain.model.DomPerson
import at.mocode.members.domain.repository.PersonRepository
import at.mocode.members.infrastructure.repository.PersonTable
import at.mocode.shared.database.DatabaseFactory
import com.benasher44.uuid.Uuid
import kotlinx.datetime.Clock
import org.jetbrains.exposed.sql.ResultRow
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.or
import org.jetbrains.exposed.sql.selectAll
/**
* Exposed-based implementation of PersonRepository.
@@ -19,26 +20,26 @@ import org.jetbrains.exposed.sql.selectAll
*/
class PersonRepositoryImpl : PersonRepository {
override suspend fun findById(id: Uuid): DomPerson? {
return PersonTable.selectAll().where { PersonTable.id eq id }
override suspend fun findById(id: Uuid): DomPerson? = DatabaseFactory.dbQuery {
PersonTable.select { PersonTable.id eq id }
.map { rowToDomPerson(it) }
.singleOrNull()
}
override suspend fun findByOepsSatzNr(oepsSatzNr: String): DomPerson? {
return PersonTable.selectAll().where { PersonTable.oepsSatzNr eq oepsSatzNr }
override suspend fun findByOepsSatzNr(oepsSatzNr: String): DomPerson? = DatabaseFactory.dbQuery {
PersonTable.select { PersonTable.oepsSatzNr eq oepsSatzNr }
.map { rowToDomPerson(it) }
.singleOrNull()
}
override suspend fun findByStammVereinId(vereinId: Uuid): List<DomPerson> {
return PersonTable.selectAll().where { PersonTable.stammVereinId eq vereinId }
override suspend fun findByStammVereinId(vereinId: Uuid): List<DomPerson> = DatabaseFactory.dbQuery {
PersonTable.select { PersonTable.stammVereinId eq vereinId }
.map { rowToDomPerson(it) }
}
override suspend fun findByName(searchTerm: String, limit: Int): List<DomPerson> {
override suspend fun findByName(searchTerm: String, limit: Int): List<DomPerson> = DatabaseFactory.dbQuery {
val searchPattern = "%$searchTerm%"
return PersonTable.selectAll().where {
PersonTable.select {
(PersonTable.nachname like searchPattern) or
(PersonTable.vorname like searchPattern)
}
@@ -46,61 +47,93 @@ class PersonRepositoryImpl : PersonRepository {
.map { rowToDomPerson(it) }
}
override suspend fun findAllActive(limit: Int, offset: Int): List<DomPerson> {
return PersonTable.selectAll().where { PersonTable.istAktiv eq true }
override suspend fun findAllActive(limit: Int, offset: Int): List<DomPerson> = DatabaseFactory.dbQuery {
PersonTable.select { PersonTable.istAktiv eq true }
.limit(limit, offset.toLong())
.map { rowToDomPerson(it) }
}
override suspend fun save(person: DomPerson): DomPerson {
override suspend fun save(person: DomPerson): DomPerson = DatabaseFactory.dbQuery {
val now = Clock.System.now()
val updatedPerson = person.copy(updatedAt = now)
val existingPerson = findById(person.personId)
PersonTable.insertOrUpdate(PersonTable.id) {
it[id] = person.personId
it[oepsSatzNr] = person.oepsSatzNr
it[nachname] = person.nachname
it[vorname] = person.vorname
it[titel] = person.titel
it[geburtsdatum] = person.geburtsdatum
it[geschlecht] = person.geschlechtE
it[nationalitaetLandId] = person.nationalitaetLandId
it[feiId] = person.feiId
it[telefon] = person.telefon
it[email] = person.email
it[strasse] = person.strasse
it[plz] = person.plz
it[ort] = person.ort
it[adresszusatzZusatzinfo] = person.adresszusatzZusatzinfo
it[stammVereinId] = person.stammVereinId
it[mitgliedsNummerBeiStammVerein] = person.mitgliedsNummerBeiStammVerein
it[istGesperrt] = person.istGesperrt
it[sperrGrund] = person.sperrGrund
it[altersklasseOepsCodeRaw] = person.altersklasseOepsCodeRaw
it[istJungerReiterOepsFlag] = person.istJungerReiterOepsFlag
it[kaderStatusOepsRaw] = person.kaderStatusOepsRaw
it[datenQuelle] = person.datenQuelle
it[istAktiv] = person.istAktiv
it[notizenIntern] = person.notizenIntern
it[createdAt] = person.createdAt.toLocalDateTime()
it[updatedAt] = updatedPerson.updatedAt.toLocalDateTime()
if (existingPerson == null) {
// Insert new person
PersonTable.insert { stmt ->
stmt[PersonTable.id] = person.personId
stmt[PersonTable.oepsSatzNr] = person.oepsSatzNr
stmt[PersonTable.nachname] = person.nachname
stmt[PersonTable.vorname] = person.vorname
stmt[PersonTable.titel] = person.titel
stmt[PersonTable.geburtsdatum] = person.geburtsdatum
stmt[PersonTable.geschlecht] = person.geschlechtE
stmt[PersonTable.nationalitaetLandId] = person.nationalitaetLandId
stmt[PersonTable.feiId] = person.feiId
stmt[PersonTable.telefon] = person.telefon
stmt[PersonTable.email] = person.email
stmt[PersonTable.strasse] = person.strasse
stmt[PersonTable.plz] = person.plz
stmt[PersonTable.ort] = person.ort
stmt[PersonTable.adresszusatzZusatzinfo] = person.adresszusatzZusatzinfo
stmt[PersonTable.stammVereinId] = person.stammVereinId
stmt[PersonTable.mitgliedsNummerBeiStammVerein] = person.mitgliedsNummerBeiStammVerein
stmt[PersonTable.istGesperrt] = person.istGesperrt
stmt[PersonTable.sperrGrund] = person.sperrGrund
stmt[PersonTable.altersklasseOepsCodeRaw] = person.altersklasseOepsCodeRaw
stmt[PersonTable.istJungerReiterOepsFlag] = person.istJungerReiterOepsFlag
stmt[PersonTable.kaderStatusOepsRaw] = person.kaderStatusOepsRaw
stmt[PersonTable.datenQuelle] = person.datenQuelle
stmt[PersonTable.istAktiv] = person.istAktiv
stmt[PersonTable.notizenIntern] = person.notizenIntern
stmt[PersonTable.createdAt] = person.createdAt.toLocalDateTime(TimeZone.UTC)
stmt[PersonTable.updatedAt] = now.toLocalDateTime(TimeZone.UTC)
}
} else {
// Update existing person
PersonTable.update({ PersonTable.id eq person.personId }) { stmt ->
stmt[PersonTable.oepsSatzNr] = person.oepsSatzNr
stmt[PersonTable.nachname] = person.nachname
stmt[PersonTable.vorname] = person.vorname
stmt[PersonTable.titel] = person.titel
stmt[PersonTable.geburtsdatum] = person.geburtsdatum
stmt[PersonTable.geschlecht] = person.geschlechtE
stmt[PersonTable.nationalitaetLandId] = person.nationalitaetLandId
stmt[PersonTable.feiId] = person.feiId
stmt[PersonTable.telefon] = person.telefon
stmt[PersonTable.email] = person.email
stmt[PersonTable.strasse] = person.strasse
stmt[PersonTable.plz] = person.plz
stmt[PersonTable.ort] = person.ort
stmt[PersonTable.adresszusatzZusatzinfo] = person.adresszusatzZusatzinfo
stmt[PersonTable.stammVereinId] = person.stammVereinId
stmt[PersonTable.mitgliedsNummerBeiStammVerein] = person.mitgliedsNummerBeiStammVerein
stmt[PersonTable.istGesperrt] = person.istGesperrt
stmt[PersonTable.sperrGrund] = person.sperrGrund
stmt[PersonTable.altersklasseOepsCodeRaw] = person.altersklasseOepsCodeRaw
stmt[PersonTable.istJungerReiterOepsFlag] = person.istJungerReiterOepsFlag
stmt[PersonTable.kaderStatusOepsRaw] = person.kaderStatusOepsRaw
stmt[PersonTable.datenQuelle] = person.datenQuelle
stmt[PersonTable.istAktiv] = person.istAktiv
stmt[PersonTable.notizenIntern] = person.notizenIntern
stmt[PersonTable.updatedAt] = now.toLocalDateTime(TimeZone.UTC)
}
}
return updatedPerson
person.copy(updatedAt = now)
}
override suspend fun delete(id: Uuid): Boolean {
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
val deletedRows = PersonTable.deleteWhere { PersonTable.id eq id }
return deletedRows > 0
deletedRows > 0
}
override suspend fun existsByOepsSatzNr(oepsSatzNr: String): Boolean {
return PersonTable.selectAll().where { PersonTable.oepsSatzNr eq oepsSatzNr }
override suspend fun existsByOepsSatzNr(oepsSatzNr: String): Boolean = DatabaseFactory.dbQuery {
PersonTable.select { PersonTable.oepsSatzNr eq oepsSatzNr }
.count() > 0
}
override suspend fun countActive(): Long {
return PersonTable.selectAll().where { PersonTable.istAktiv eq true }
override suspend fun countActive(): Long = DatabaseFactory.dbQuery {
PersonTable.select { PersonTable.istAktiv eq true }
.count()
}
@@ -134,8 +167,8 @@ class PersonRepositoryImpl : PersonRepository {
datenQuelle = row[PersonTable.datenQuelle],
istAktiv = row[PersonTable.istAktiv],
notizenIntern = row[PersonTable.notizenIntern],
createdAt = row[PersonTable.createdAt].toInstant(),
updatedAt = row[PersonTable.updatedAt].toInstant()
createdAt = row[PersonTable.createdAt].toInstant(TimeZone.UTC),
updatedAt = row[PersonTable.updatedAt].toInstant(TimeZone.UTC)
)
}
}
@@ -2,96 +2,195 @@ package at.mocode.members.infrastructure.repository
import at.mocode.members.domain.model.DomPersonRolle
import at.mocode.members.domain.repository.PersonRolleRepository
import at.mocode.members.infrastructure.table.PersonRolleTable
import at.mocode.shared.database.DatabaseFactory
import com.benasher44.uuid.Uuid
import kotlinx.datetime.Clock
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.todayIn
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
/**
* In-memory implementation of PersonRolleRepository for testing and development.
*
* This implementation provides basic functionality without database persistence.
* Replace with proper database implementation for production use.
* Database implementation of PersonRolleRepository using PersonRolleTable.
*/
class PersonRolleRepositoryImpl : PersonRolleRepository {
private val personRoles = mutableMapOf<Uuid, DomPersonRolle>()
/**
* Konvertiert eine Datenbankzeile in ein Domain-Objekt.
*/
private fun rowToDomPersonRolle(row: ResultRow): DomPersonRolle {
return DomPersonRolle(
personRolleId = row[PersonRolleTable.id],
personId = row[PersonRolleTable.personId],
rolleId = row[PersonRolleTable.rolleId],
vereinId = row[PersonRolleTable.vereinId],
gueltigVon = row[PersonRolleTable.gueltigVon],
gueltigBis = row[PersonRolleTable.gueltigBis],
istAktiv = row[PersonRolleTable.istAktiv],
zugewiesenVon = row[PersonRolleTable.zugewiesenVon],
notizen = row[PersonRolleTable.notizen],
createdAt = row[PersonRolleTable.createdAt],
updatedAt = row[PersonRolleTable.updatedAt]
)
}
override suspend fun save(personRolle: DomPersonRolle): DomPersonRolle {
override suspend fun save(personRolle: DomPersonRolle): DomPersonRolle = DatabaseFactory.dbQuery {
val now = Clock.System.now()
val updatedPersonRolle = personRolle.copy(updatedAt = now)
personRoles[updatedPersonRolle.personRolleId] = updatedPersonRolle
return updatedPersonRolle
}
val existingPersonRolle = findById(personRolle.personRolleId)
override suspend fun findById(personRolleId: Uuid): DomPersonRolle? {
return personRoles[personRolleId]
}
override suspend fun findByPersonId(personId: Uuid, nurAktive: Boolean): List<DomPersonRolle> {
return personRoles.values.filter { personRolle ->
personRolle.personId == personId && (!nurAktive || personRolle.istAktiv)
if (existingPersonRolle == null) {
// Insert new person role
PersonRolleTable.insert { stmt ->
stmt[PersonRolleTable.id] = personRolle.personRolleId
stmt[PersonRolleTable.personId] = personRolle.personId
stmt[PersonRolleTable.rolleId] = personRolle.rolleId
stmt[PersonRolleTable.vereinId] = personRolle.vereinId
stmt[PersonRolleTable.gueltigVon] = personRolle.gueltigVon
stmt[PersonRolleTable.gueltigBis] = personRolle.gueltigBis
stmt[PersonRolleTable.istAktiv] = personRolle.istAktiv
stmt[PersonRolleTable.zugewiesenVon] = personRolle.zugewiesenVon
stmt[PersonRolleTable.notizen] = personRolle.notizen
stmt[PersonRolleTable.createdAt] = personRolle.createdAt
stmt[PersonRolleTable.updatedAt] = now
}
} else {
// Update existing person role
PersonRolleTable.update({ PersonRolleTable.id eq personRolle.personRolleId }) { stmt ->
stmt[PersonRolleTable.personId] = personRolle.personId
stmt[PersonRolleTable.rolleId] = personRolle.rolleId
stmt[PersonRolleTable.vereinId] = personRolle.vereinId
stmt[PersonRolleTable.gueltigVon] = personRolle.gueltigVon
stmt[PersonRolleTable.gueltigBis] = personRolle.gueltigBis
stmt[PersonRolleTable.istAktiv] = personRolle.istAktiv
stmt[PersonRolleTable.zugewiesenVon] = personRolle.zugewiesenVon
stmt[PersonRolleTable.notizen] = personRolle.notizen
stmt[PersonRolleTable.updatedAt] = now
}
}
personRolle.copy(updatedAt = now)
}
override suspend fun findByRolleId(rolleId: Uuid, nurAktive: Boolean): List<DomPersonRolle> {
return personRoles.values.filter { personRolle ->
personRolle.rolleId == rolleId && (!nurAktive || personRolle.istAktiv)
override suspend fun findById(personRolleId: Uuid): DomPersonRolle? = DatabaseFactory.dbQuery {
PersonRolleTable.select { PersonRolleTable.id eq personRolleId }
.map(::rowToDomPersonRolle)
.singleOrNull()
}
override suspend fun findByPersonId(personId: Uuid, nurAktive: Boolean): List<DomPersonRolle> = DatabaseFactory.dbQuery {
val query = if (nurAktive) {
PersonRolleTable.select {
(PersonRolleTable.personId eq personId) and (PersonRolleTable.istAktiv eq true)
}
} else {
PersonRolleTable.select { PersonRolleTable.personId eq personId }
}
query.map(::rowToDomPersonRolle)
}
override suspend fun findByVereinId(vereinId: Uuid, nurAktive: Boolean): List<DomPersonRolle> {
return personRoles.values.filter { personRolle ->
personRolle.vereinId == vereinId && (!nurAktive || personRolle.istAktiv)
override suspend fun findByRolleId(rolleId: Uuid, nurAktive: Boolean): List<DomPersonRolle> = DatabaseFactory.dbQuery {
val query = if (nurAktive) {
PersonRolleTable.select {
(PersonRolleTable.rolleId eq rolleId) and (PersonRolleTable.istAktiv eq true)
}
} else {
PersonRolleTable.select { PersonRolleTable.rolleId eq rolleId }
}
query.map(::rowToDomPersonRolle)
}
override suspend fun findByPersonAndRolle(personId: Uuid, rolleId: Uuid, vereinId: Uuid?): DomPersonRolle? {
return personRoles.values.find { personRolle ->
personRolle.personId == personId &&
personRolle.rolleId == rolleId &&
(vereinId == null || personRolle.vereinId == vereinId)
override suspend fun findByVereinId(vereinId: Uuid, nurAktive: Boolean): List<DomPersonRolle> = DatabaseFactory.dbQuery {
val query = if (nurAktive) {
PersonRolleTable.select {
(PersonRolleTable.vereinId eq vereinId) and (PersonRolleTable.istAktiv eq true)
}
} else {
PersonRolleTable.select { PersonRolleTable.vereinId eq vereinId }
}
query.map(::rowToDomPersonRolle)
}
override suspend fun findValidAt(stichtag: LocalDate, nurAktive: Boolean): List<DomPersonRolle> {
return personRoles.values.filter { personRolle ->
val isValid = personRolle.gueltigVon <= stichtag &&
(personRolle.gueltigBis == null || personRolle.gueltigBis!! >= stichtag)
isValid && (!nurAktive || personRolle.istAktiv)
override suspend fun findByPersonAndRolle(personId: Uuid, rolleId: Uuid, vereinId: Uuid?): DomPersonRolle? = DatabaseFactory.dbQuery {
val query = if (vereinId != null) {
PersonRolleTable.select {
(PersonRolleTable.personId eq personId) and
(PersonRolleTable.rolleId eq rolleId) and
(PersonRolleTable.vereinId eq vereinId)
}
} else {
PersonRolleTable.select {
(PersonRolleTable.personId eq personId) and
(PersonRolleTable.rolleId eq rolleId) and
PersonRolleTable.vereinId.isNull()
}
}
query.map(::rowToDomPersonRolle).singleOrNull()
}
override suspend fun findByPersonValidAt(personId: Uuid, stichtag: LocalDate, nurAktive: Boolean): List<DomPersonRolle> {
return personRoles.values.filter { personRolle ->
val isValid = personRolle.personId == personId &&
personRolle.gueltigVon <= stichtag &&
(personRolle.gueltigBis == null || personRolle.gueltigBis!! >= stichtag)
isValid && (!nurAktive || personRolle.istAktiv)
override suspend fun findValidAt(stichtag: LocalDate, nurAktive: Boolean): List<DomPersonRolle> = DatabaseFactory.dbQuery {
val baseQuery = PersonRolleTable.select {
(PersonRolleTable.gueltigVon lessEq stichtag) and
(PersonRolleTable.gueltigBis.isNull() or (PersonRolleTable.gueltigBis greaterEq stichtag))
}
val query = if (nurAktive) {
baseQuery.andWhere { PersonRolleTable.istAktiv eq true }
} else {
baseQuery
}
query.map(::rowToDomPersonRolle)
}
override suspend fun deactivatePersonRolle(personRolleId: Uuid): Boolean {
val personRolle = personRoles[personRolleId] ?: return false
personRoles[personRolleId] = personRolle.copy(istAktiv = false, updatedAt = Clock.System.now())
return true
override suspend fun findByPersonValidAt(personId: Uuid, stichtag: LocalDate, nurAktive: Boolean): List<DomPersonRolle> = DatabaseFactory.dbQuery {
val baseQuery = PersonRolleTable.select {
(PersonRolleTable.personId eq personId) and
(PersonRolleTable.gueltigVon lessEq stichtag) and
(PersonRolleTable.gueltigBis.isNull() or (PersonRolleTable.gueltigBis greaterEq stichtag))
}
val query = if (nurAktive) {
baseQuery.andWhere { PersonRolleTable.istAktiv eq true }
} else {
baseQuery
}
query.map(::rowToDomPersonRolle)
}
override suspend fun deletePersonRolle(personRolleId: Uuid): Boolean {
return personRoles.remove(personRolleId) != null
override suspend fun deactivatePersonRolle(personRolleId: Uuid): Boolean = DatabaseFactory.dbQuery {
val now = Clock.System.now()
val rowsUpdated = PersonRolleTable.update({ PersonRolleTable.id eq personRolleId }) { stmt ->
stmt[PersonRolleTable.istAktiv] = false
stmt[PersonRolleTable.updatedAt] = now
}
rowsUpdated > 0
}
override suspend fun hasPersonRolle(personId: Uuid, rolleId: Uuid, vereinId: Uuid?, stichtag: LocalDate?): Boolean {
override suspend fun deletePersonRolle(personRolleId: Uuid): Boolean = DatabaseFactory.dbQuery {
val rowsDeleted = PersonRolleTable.deleteWhere { PersonRolleTable.id eq personRolleId }
rowsDeleted > 0
}
override suspend fun hasPersonRolle(personId: Uuid, rolleId: Uuid, vereinId: Uuid?, stichtag: LocalDate?): Boolean = DatabaseFactory.dbQuery {
val checkDate = stichtag ?: Clock.System.todayIn(TimeZone.currentSystemDefault())
return personRoles.values.any { personRolle ->
personRolle.personId == personId &&
personRolle.rolleId == rolleId &&
(vereinId == null || personRolle.vereinId == vereinId) &&
personRolle.istAktiv &&
personRolle.gueltigVon <= checkDate &&
(personRolle.gueltigBis == null || personRolle.gueltigBis!! >= checkDate)
val baseQuery = PersonRolleTable.select {
(PersonRolleTable.personId eq personId) and
(PersonRolleTable.rolleId eq rolleId) and
(PersonRolleTable.istAktiv eq true) and
(PersonRolleTable.gueltigVon lessEq checkDate) and
(PersonRolleTable.gueltigBis.isNull() or (PersonRolleTable.gueltigBis greaterEq checkDate))
}
val query = if (vereinId != null) {
baseQuery.andWhere { PersonRolleTable.vereinId eq vereinId }
} else {
baseQuery.andWhere { PersonRolleTable.vereinId.isNull() }
}
query.count() > 0
}
}
@@ -1,86 +1,166 @@
package at.mocode.members.infrastructure.repository
import at.mocode.enums.BerechtigungE
import at.mocode.members.domain.model.DomBerechtigung
import at.mocode.members.domain.model.DomRolleBerechtigung
import at.mocode.members.domain.repository.RolleBerechtigungRepository
import at.mocode.members.infrastructure.table.BerechtigungTable
import at.mocode.members.infrastructure.table.RolleBerechtigungTable
import at.mocode.shared.database.DatabaseFactory
import com.benasher44.uuid.Uuid
import com.benasher44.uuid.uuid4
import kotlinx.datetime.Clock
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import kotlinx.datetime.TimeZone
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
/**
* In-memory implementation of RolleBerechtigungRepository for testing and development.
*
* This implementation provides basic functionality without database persistence.
* Replace with proper database implementation for production use.
* Implementierung des RolleBerechtigungRepository für die Datenbankzugriffe.
*/
class RolleBerechtigungRepositoryImpl : RolleBerechtigungRepository {
private val rolePermissions = mutableMapOf<Uuid, DomRolleBerechtigung>()
/**
* Konvertiert eine Datenbankzeile in ein Domain-Objekt für Berechtigung.
*/
private fun rowToDomBerechtigung(row: ResultRow): DomBerechtigung {
return DomBerechtigung(
berechtigungId = row[BerechtigungTable.id],
berechtigungTyp = row[BerechtigungTable.berechtigungTyp],
name = row[BerechtigungTable.name],
beschreibung = row[BerechtigungTable.beschreibung],
ressource = row[BerechtigungTable.ressource],
aktion = row[BerechtigungTable.aktion],
istAktiv = row[BerechtigungTable.istAktiv],
istSystemBerechtigung = row[BerechtigungTable.istSystemBerechtigung],
createdAt = row[BerechtigungTable.createdAt].toInstant(TimeZone.UTC),
updatedAt = row[BerechtigungTable.updatedAt].toInstant(TimeZone.UTC)
)
}
override suspend fun save(rolleBerechtigung: DomRolleBerechtigung): DomRolleBerechtigung {
/**
* Konvertiert eine Datenbankzeile in ein Domain-Objekt für RolleBerechtigung.
*/
private fun rowToDomRolleBerechtigung(row: ResultRow): DomRolleBerechtigung {
return DomRolleBerechtigung(
rolleBerechtigungId = row[RolleBerechtigungTable.id],
rolleId = row[RolleBerechtigungTable.rolleId],
berechtigungId = row[RolleBerechtigungTable.berechtigungId],
istAktiv = row[RolleBerechtigungTable.istAktiv],
zugewiesenVon = row[RolleBerechtigungTable.zugewiesenVon],
notizen = row[RolleBerechtigungTable.notizen],
createdAt = row[RolleBerechtigungTable.createdAt].toInstant(TimeZone.UTC),
updatedAt = row[RolleBerechtigungTable.updatedAt].toInstant(TimeZone.UTC)
)
}
override suspend fun save(rolleBerechtigung: DomRolleBerechtigung): DomRolleBerechtigung = DatabaseFactory.dbQuery {
val now = Clock.System.now()
val updatedRolleBerechtigung = rolleBerechtigung.copy(updatedAt = now)
rolePermissions[updatedRolleBerechtigung.rolleBerechtigungId] = updatedRolleBerechtigung
return updatedRolleBerechtigung
}
override suspend fun findById(rolleBerechtigungId: Uuid): DomRolleBerechtigung? {
return rolePermissions[rolleBerechtigungId]
}
// Check if this is an update (has existing ID) or insert (new record)
val existingRecord = findById(rolleBerechtigung.rolleBerechtigungId)
override suspend fun findByRolleId(rolleId: Uuid, nurAktive: Boolean): List<DomRolleBerechtigung> {
return rolePermissions.values.filter { rolleBerechtigung ->
rolleBerechtigung.rolleId == rolleId && (!nurAktive || rolleBerechtigung.istAktiv)
if (existingRecord != null) {
// Update existing record
RolleBerechtigungTable.update({ RolleBerechtigungTable.id eq rolleBerechtigung.rolleBerechtigungId }) { stmt ->
stmt[RolleBerechtigungTable.rolleId] = updatedRolleBerechtigung.rolleId
stmt[RolleBerechtigungTable.berechtigungId] = updatedRolleBerechtigung.berechtigungId
stmt[RolleBerechtigungTable.istAktiv] = updatedRolleBerechtigung.istAktiv
stmt[RolleBerechtigungTable.zugewiesenVon] = updatedRolleBerechtigung.zugewiesenVon
stmt[RolleBerechtigungTable.notizen] = updatedRolleBerechtigung.notizen
stmt[RolleBerechtigungTable.updatedAt] = updatedRolleBerechtigung.updatedAt.toLocalDateTime(TimeZone.UTC)
}
updatedRolleBerechtigung
} else {
// Insert new record
val insertResult = RolleBerechtigungTable.insert { stmt ->
stmt[RolleBerechtigungTable.id] = updatedRolleBerechtigung.rolleBerechtigungId
stmt[RolleBerechtigungTable.rolleId] = updatedRolleBerechtigung.rolleId
stmt[RolleBerechtigungTable.berechtigungId] = updatedRolleBerechtigung.berechtigungId
stmt[RolleBerechtigungTable.istAktiv] = updatedRolleBerechtigung.istAktiv
stmt[RolleBerechtigungTable.zugewiesenVon] = updatedRolleBerechtigung.zugewiesenVon
stmt[RolleBerechtigungTable.notizen] = updatedRolleBerechtigung.notizen
stmt[RolleBerechtigungTable.createdAt] = updatedRolleBerechtigung.createdAt.toLocalDateTime(TimeZone.UTC)
stmt[RolleBerechtigungTable.updatedAt] = updatedRolleBerechtigung.updatedAt.toLocalDateTime(TimeZone.UTC)
}
val insertedId = insertResult[RolleBerechtigungTable.id]
findById(insertedId)!!
}
}
override suspend fun findByBerechtigungId(berechtigungId: Uuid, nurAktive: Boolean): List<DomRolleBerechtigung> {
return rolePermissions.values.filter { rolleBerechtigung ->
rolleBerechtigung.berechtigungId == berechtigungId && (!nurAktive || rolleBerechtigung.istAktiv)
override suspend fun findById(rolleBerechtigungId: Uuid): DomRolleBerechtigung? = DatabaseFactory.dbQuery {
RolleBerechtigungTable.select { RolleBerechtigungTable.id eq rolleBerechtigungId }
.map(::rowToDomRolleBerechtigung)
.singleOrNull()
}
override suspend fun findByRolleId(rolleId: Uuid, nurAktive: Boolean): List<DomRolleBerechtigung> = DatabaseFactory.dbQuery {
val query = if (nurAktive) {
RolleBerechtigungTable.select {
(RolleBerechtigungTable.rolleId eq rolleId) and (RolleBerechtigungTable.istAktiv eq true)
}
} else {
RolleBerechtigungTable.select { RolleBerechtigungTable.rolleId eq rolleId }
}
query.map(::rowToDomRolleBerechtigung)
}
override suspend fun findByRolleAndBerechtigung(rolleId: Uuid, berechtigungId: Uuid): DomRolleBerechtigung? {
return rolePermissions.values.find { rolleBerechtigung ->
rolleBerechtigung.rolleId == rolleId && rolleBerechtigung.berechtigungId == berechtigungId
override suspend fun findByBerechtigungId(berechtigungId: Uuid, nurAktive: Boolean): List<DomRolleBerechtigung> = DatabaseFactory.dbQuery {
val query = if (nurAktive) {
RolleBerechtigungTable.select {
(RolleBerechtigungTable.berechtigungId eq berechtigungId) and (RolleBerechtigungTable.istAktiv eq true)
}
} else {
RolleBerechtigungTable.select { RolleBerechtigungTable.berechtigungId eq berechtigungId }
}
query.map(::rowToDomRolleBerechtigung)
}
override suspend fun findAllActive(): List<DomRolleBerechtigung> {
return rolePermissions.values.filter { it.istAktiv }
override suspend fun findByRolleAndBerechtigung(rolleId: Uuid, berechtigungId: Uuid): DomRolleBerechtigung? = DatabaseFactory.dbQuery {
RolleBerechtigungTable.select {
(RolleBerechtigungTable.rolleId eq rolleId) and (RolleBerechtigungTable.berechtigungId eq berechtigungId)
}.map(::rowToDomRolleBerechtigung).singleOrNull()
}
override suspend fun findAll(): List<DomRolleBerechtigung> {
return rolePermissions.values.toList()
override suspend fun findAllActive(): List<DomRolleBerechtigung> = DatabaseFactory.dbQuery {
RolleBerechtigungTable.select { RolleBerechtigungTable.istAktiv eq true }
.map(::rowToDomRolleBerechtigung)
}
override suspend fun deactivateRolleBerechtigung(rolleBerechtigungId: Uuid): Boolean {
val rolleBerechtigung = rolePermissions[rolleBerechtigungId] ?: return false
rolePermissions[rolleBerechtigungId] = rolleBerechtigung.copy(istAktiv = false, updatedAt = Clock.System.now())
return true
override suspend fun findAll(): List<DomRolleBerechtigung> = DatabaseFactory.dbQuery {
RolleBerechtigungTable.selectAll()
.map(::rowToDomRolleBerechtigung)
}
override suspend fun deleteRolleBerechtigung(rolleBerechtigungId: Uuid): Boolean {
return rolePermissions.remove(rolleBerechtigungId) != null
}
override suspend fun hasRolleBerechtigung(rolleId: Uuid, berechtigungId: Uuid): Boolean {
return rolePermissions.values.any { rolleBerechtigung ->
rolleBerechtigung.rolleId == rolleId &&
rolleBerechtigung.berechtigungId == berechtigungId &&
rolleBerechtigung.istAktiv
override suspend fun deactivateRolleBerechtigung(rolleBerechtigungId: Uuid): Boolean = DatabaseFactory.dbQuery {
val rowsUpdated = RolleBerechtigungTable.update({ RolleBerechtigungTable.id eq rolleBerechtigungId }) { stmt ->
stmt[RolleBerechtigungTable.istAktiv] = false
stmt[RolleBerechtigungTable.updatedAt] = Clock.System.now().toLocalDateTime(TimeZone.UTC)
}
rowsUpdated > 0
}
override suspend fun assignBerechtigungToRolle(rolleId: Uuid, berechtigungId: Uuid, zugewiesenVon: Uuid?): DomRolleBerechtigung {
override suspend fun deleteRolleBerechtigung(rolleBerechtigungId: Uuid): Boolean = DatabaseFactory.dbQuery {
val rowsDeleted = RolleBerechtigungTable.deleteWhere { RolleBerechtigungTable.id eq rolleBerechtigungId }
rowsDeleted > 0
}
override suspend fun hasRolleBerechtigung(rolleId: Uuid, berechtigungId: Uuid): Boolean = DatabaseFactory.dbQuery {
RolleBerechtigungTable.select {
(RolleBerechtigungTable.rolleId eq rolleId) and
(RolleBerechtigungTable.berechtigungId eq berechtigungId) and
(RolleBerechtigungTable.istAktiv eq true)
}.count() > 0
}
override suspend fun assignBerechtigungToRolle(rolleId: Uuid, berechtigungId: Uuid, zugewiesenVon: Uuid?): DomRolleBerechtigung = DatabaseFactory.dbQuery {
// Check if assignment already exists
val existing = findByRolleAndBerechtigung(rolleId, berechtigungId)
if (existing != null) {
// If it exists but is inactive, reactivate it
if (!existing.istAktiv) {
val reactivated = existing.copy(istAktiv = true, updatedAt = Clock.System.now())
return save(reactivated)
}
return existing
// Relationship already exists, return it
return@dbQuery existing
}
// Create new assignment
@@ -89,11 +169,14 @@ class RolleBerechtigungRepositoryImpl : RolleBerechtigungRepository {
berechtigungId = berechtigungId,
zugewiesenVon = zugewiesenVon
)
return save(newAssignment)
save(newAssignment)
}
override suspend fun revokeBerechtigungFromRolle(rolleId: Uuid, berechtigungId: Uuid): Boolean {
val rolleBerechtigung = findByRolleAndBerechtigung(rolleId, berechtigungId) ?: return false
return deactivateRolleBerechtigung(rolleBerechtigung.rolleBerechtigungId)
override suspend fun revokeBerechtigungFromRolle(rolleId: Uuid, berechtigungId: Uuid): Boolean = DatabaseFactory.dbQuery {
// Since we can't deactivate, we delete the relationship
val rowsDeleted = RolleBerechtigungTable.deleteWhere {
(RolleBerechtigungTable.rolleId eq rolleId) and (RolleBerechtigungTable.berechtigungId eq berechtigungId)
}
rowsDeleted > 0
}
}
@@ -1,99 +1,128 @@
package at.mocode.members.infrastructure.repository
import at.mocode.enums.RolleE
import at.mocode.members.domain.model.DomRolle
import at.mocode.members.domain.repository.RolleRepository
import at.mocode.enums.RolleE
import at.mocode.members.infrastructure.table.RolleTable
import at.mocode.shared.database.DatabaseFactory
import com.benasher44.uuid.Uuid
import com.benasher44.uuid.uuid4
import kotlinx.datetime.Clock
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import kotlinx.datetime.TimeZone
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
/**
* In-memory implementation of RolleRepository for testing and development.
*
* This implementation provides basic functionality without database persistence.
* Replace with proper database implementation for production use.
* Implementierung des RolleRepository für die Datenbankzugriffe.
*/
class RolleRepositoryImpl : RolleRepository {
private val roles = mutableMapOf<Uuid, DomRolle>()
init {
// Initialize with default roles
val defaultRoles = listOf(
DomRolle(
rolleId = uuid4(),
rolleTyp = RolleE.ADMIN,
name = "Administrator",
beschreibung = "System administrator with full access",
istAktiv = true,
istSystemRolle = true
),
DomRolle(
rolleId = uuid4(),
rolleTyp = RolleE.VEREINS_ADMIN,
name = "Vereins Administrator",
beschreibung = "Club administrator",
istAktiv = true,
istSystemRolle = true
),
DomRolle(
rolleId = uuid4(),
rolleTyp = RolleE.REITER,
name = "Reiter",
beschreibung = "Rider",
istAktiv = true,
istSystemRolle = true
)
/**
* Konvertiert eine Datenbankzeile in ein Domain-Objekt.
*/
private fun rowToDomRolle(row: ResultRow): DomRolle {
return DomRolle(
rolleId = row[RolleTable.id],
rolleTyp = row[RolleTable.rolleTyp],
name = row[RolleTable.name],
beschreibung = row[RolleTable.beschreibung],
istSystemRolle = row[RolleTable.istSystemRolle],
istAktiv = row[RolleTable.istAktiv],
createdAt = row[RolleTable.createdAt].toInstant(TimeZone.UTC),
updatedAt = row[RolleTable.updatedAt].toInstant(TimeZone.UTC)
)
defaultRoles.forEach { role ->
roles[role.rolleId!!] = role
}
}
override suspend fun save(rolle: DomRolle): DomRolle {
override suspend fun save(rolle: DomRolle): DomRolle = DatabaseFactory.dbQuery {
val now = Clock.System.now()
val updatedRolle = rolle.copy(updatedAt = now)
roles[updatedRolle.rolleId!!] = updatedRolle
return updatedRolle
val existingRolle = findById(rolle.rolleId)
if (existingRolle == null) {
// Insert new role
RolleTable.insert { stmt ->
stmt[RolleTable.id] = rolle.rolleId
stmt[RolleTable.rolleTyp] = rolle.rolleTyp
stmt[RolleTable.name] = rolle.name
stmt[RolleTable.beschreibung] = rolle.beschreibung
stmt[RolleTable.istSystemRolle] = rolle.istSystemRolle
stmt[RolleTable.istAktiv] = rolle.istAktiv
stmt[RolleTable.createdAt] = rolle.createdAt.toLocalDateTime(TimeZone.UTC)
stmt[RolleTable.updatedAt] = now.toLocalDateTime(TimeZone.UTC)
}
} else {
// Update existing role
RolleTable.update({ RolleTable.id eq rolle.rolleId }) { stmt ->
stmt[RolleTable.rolleTyp] = rolle.rolleTyp
stmt[RolleTable.name] = rolle.name
stmt[RolleTable.beschreibung] = rolle.beschreibung
stmt[RolleTable.istSystemRolle] = rolle.istSystemRolle
stmt[RolleTable.istAktiv] = rolle.istAktiv
stmt[RolleTable.updatedAt] = now.toLocalDateTime(TimeZone.UTC)
}
}
// Return updated object
rolle.copy(updatedAt = now)
}
override suspend fun findById(rolleId: Uuid): DomRolle? {
return roles[rolleId]
override suspend fun findById(rolleId: Uuid): DomRolle? = DatabaseFactory.dbQuery {
RolleTable.select { RolleTable.id eq rolleId }
.map(::rowToDomRolle)
.singleOrNull()
}
override suspend fun findByTyp(rolleTyp: RolleE): DomRolle? {
return roles.values.find { it.rolleTyp == rolleTyp }
override suspend fun findByTyp(rolleTyp: RolleE): DomRolle? = DatabaseFactory.dbQuery {
RolleTable.select { RolleTable.rolleTyp eq rolleTyp }
.map(::rowToDomRolle)
.singleOrNull()
}
override suspend fun findByName(name: String): List<DomRolle> {
return roles.values.filter { it.name.contains(name, ignoreCase = true) }
override suspend fun findByName(name: String): List<DomRolle> = DatabaseFactory.dbQuery {
RolleTable.select { RolleTable.name like "%$name%" }
.map(::rowToDomRolle)
}
override suspend fun findAllActive(): List<DomRolle> {
return roles.values.filter { it.istAktiv }
override suspend fun findAllActive(): List<DomRolle> = DatabaseFactory.dbQuery {
RolleTable.select { RolleTable.istAktiv eq true }
.map(::rowToDomRolle)
}
override suspend fun findAll(): List<DomRolle> {
return roles.values.toList()
override suspend fun findAll(): List<DomRolle> = DatabaseFactory.dbQuery {
RolleTable.selectAll()
.map(::rowToDomRolle)
}
override suspend fun deactivateRolle(rolleId: Uuid): Boolean {
val rolle = roles[rolleId] ?: return false
roles[rolleId] = rolle.copy(istAktiv = false, updatedAt = Clock.System.now())
return true
override suspend fun deleteRolle(rolleId: Uuid): Boolean = DatabaseFactory.dbQuery {
// Prüfen, ob es sich um eine Systemrolle handelt
val rolle = findById(rolleId)
if (rolle?.istSystemRolle == true) {
return@dbQuery false
}
val rowsDeleted = RolleTable.deleteWhere { RolleTable.id eq rolleId }
rowsDeleted > 0
}
override suspend fun deleteRolle(rolleId: Uuid): Boolean {
val rolle = roles[rolleId] ?: return false
// Don't allow deletion of system roles
if (rolle.istSystemRolle) return false
roles.remove(rolleId)
return true
override suspend fun deactivateRolle(rolleId: Uuid): Boolean = DatabaseFactory.dbQuery {
val now = Clock.System.now()
// Prüfen, ob es sich um eine Systemrolle handelt
val rolle = findById(rolleId)
if (rolle?.istSystemRolle == true) {
return@dbQuery false
}
val rowsUpdated = RolleTable.update({ RolleTable.id eq rolleId }) { stmt ->
stmt[RolleTable.istAktiv] = false
stmt[RolleTable.updatedAt] = now.toLocalDateTime(TimeZone.UTC)
}
rowsUpdated > 0
}
override suspend fun existsByTyp(rolleTyp: RolleE): Boolean {
return roles.values.any { it.rolleTyp == rolleTyp }
override suspend fun existsByTyp(rolleTyp: RolleE): Boolean = DatabaseFactory.dbQuery {
RolleTable.select { RolleTable.rolleTyp eq rolleTyp }
.count() > 0
}
}
@@ -1,130 +1,207 @@
package at.mocode.members.infrastructure.repository
import at.mocode.members.domain.model.DomUser
import at.mocode.members.domain.repository.UserRepository
import at.mocode.members.domain.model.DomUser
import at.mocode.shared.database.DatabaseFactory
import at.mocode.members.infrastructure.table.UserTable
import com.benasher44.uuid.Uuid
import com.benasher44.uuid.uuid4
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.datetime.toLocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.plus
import org.jetbrains.exposed.sql.statements.InsertStatement
/**
* In-memory implementation of UserRepository for testing and development.
*
* This implementation provides basic functionality without database persistence.
* Replace with proper database implementation for production use.
* Implementation des UserRepository für die Datenbankzugriffe.
*/
class UserRepositoryImpl : UserRepository {
private val users = mutableMapOf<Uuid, DomUser>()
init {
// Initialize with a test user
val testUser = DomUser(
userId = uuid4(),
personId = uuid4(),
username = "testuser",
email = "test@example.com",
passwordHash = "hashed_password",
salt = "salt123",
istAktiv = true,
istEmailVerifiziert = true,
letzteAnmeldung = null,
fehlgeschlageneAnmeldungen = 0,
gesperrtBis = null
)
users[testUser.userId] = testUser
}
override suspend fun createUser(user: DomUser): DomUser {
val now = Clock.System.now()
val updatedUser = user.copy(createdAt = now, updatedAt = now)
users[updatedUser.userId] = updatedUser
return updatedUser
}
override suspend fun findById(userId: Uuid): DomUser? {
return users[userId]
}
override suspend fun findByUsername(username: String): DomUser? {
return users.values.find { it.username == username }
}
override suspend fun findByEmail(email: String): DomUser? {
return users.values.find { it.email == email }
}
override suspend fun findByPersonId(personId: Uuid): DomUser? {
return users.values.find { it.personId == personId }
}
override suspend fun updateUser(user: DomUser): DomUser {
val now = Clock.System.now()
val updatedUser = user.copy(updatedAt = now)
users[updatedUser.userId] = updatedUser
return updatedUser
}
override suspend fun updateLastLogin(userId: Uuid) {
val user = users[userId] ?: return
val now = Clock.System.now()
users[userId] = user.copy(letzteAnmeldung = now, updatedAt = now)
}
override suspend fun incrementFailedLoginAttempts(userId: Uuid) {
val user = users[userId] ?: return
val now = Clock.System.now()
users[userId] = user.copy(
fehlgeschlageneAnmeldungen = user.fehlgeschlageneAnmeldungen + 1,
updatedAt = now
/**
* Konvertiert eine Datenbankzeile in ein Domain-Objekt.
*/
private fun rowToDomUser(row: ResultRow): DomUser {
return DomUser(
userId = row[UserTable.id],
personId = row[UserTable.personId],
username = row[UserTable.username],
email = row[UserTable.email],
passwordHash = row[UserTable.passwordHash],
salt = row[UserTable.salt],
istAktiv = row[UserTable.isActive],
istEmailVerifiziert = row[UserTable.isEmailVerified],
fehlgeschlageneAnmeldungen = row[UserTable.failedLoginAttempts],
gesperrtBis = row[UserTable.lockedUntil],
letzteAnmeldung = row[UserTable.lastLoginAt],
createdAt = row[UserTable.createdAt].toInstant(TimeZone.UTC),
updatedAt = row[UserTable.updatedAt].toInstant(TimeZone.UTC)
)
}
override suspend fun resetFailedLoginAttempts(userId: Uuid) {
val user = users[userId] ?: return
override suspend fun createUser(user: DomUser): DomUser = DatabaseFactory.dbQuery {
val stmt = UserTable.insert { insertStmt ->
populateUserStatement(insertStmt, user)
}
val userId = stmt[UserTable.id]
findById(userId)!!
}
private fun populateUserStatement(stmt: InsertStatement<*>, user: DomUser) {
stmt[UserTable.id] = user.userId
stmt[UserTable.personId] = user.personId
stmt[UserTable.username] = user.username
stmt[UserTable.email] = user.email
stmt[UserTable.passwordHash] = user.passwordHash
stmt[UserTable.salt] = user.salt
stmt[UserTable.isActive] = user.istAktiv
stmt[UserTable.isEmailVerified] = user.istEmailVerifiziert
stmt[UserTable.failedLoginAttempts] = user.fehlgeschlageneAnmeldungen
stmt[UserTable.lockedUntil] = user.gesperrtBis
stmt[UserTable.lastLoginAt] = user.letzteAnmeldung
stmt[UserTable.createdAt] = user.createdAt.toLocalDateTime(TimeZone.UTC)
stmt[UserTable.updatedAt] = Clock.System.now().toLocalDateTime(TimeZone.UTC)
}
override suspend fun findById(userId: Uuid): DomUser? = DatabaseFactory.dbQuery {
UserTable.select { UserTable.id eq userId }
.map(::rowToDomUser)
.singleOrNull()
}
override suspend fun findByUsername(username: String): DomUser? = DatabaseFactory.dbQuery {
UserTable.select { UserTable.username eq username }
.map(::rowToDomUser)
.singleOrNull()
}
override suspend fun findByEmail(email: String): DomUser? = DatabaseFactory.dbQuery {
UserTable.select { UserTable.email eq email }
.map(::rowToDomUser)
.singleOrNull()
}
override suspend fun findByPersonId(personId: Uuid): DomUser? = DatabaseFactory.dbQuery {
UserTable.select { UserTable.personId eq personId }
.map(::rowToDomUser)
.singleOrNull()
}
override suspend fun updateUser(user: DomUser): DomUser = DatabaseFactory.dbQuery {
val updatedUser = user.copy(updatedAt = Clock.System.now())
UserTable.update({ UserTable.id eq user.userId }) { updateStmt ->
updateStmt[UserTable.username] = updatedUser.username
updateStmt[UserTable.email] = updatedUser.email
updateStmt[UserTable.passwordHash] = updatedUser.passwordHash
updateStmt[UserTable.salt] = updatedUser.salt
updateStmt[UserTable.isActive] = updatedUser.istAktiv
updateStmt[UserTable.isEmailVerified] = updatedUser.istEmailVerifiziert
updateStmt[UserTable.failedLoginAttempts] = updatedUser.fehlgeschlageneAnmeldungen
updateStmt[UserTable.lockedUntil] = updatedUser.gesperrtBis
updateStmt[UserTable.lastLoginAt] = updatedUser.letzteAnmeldung
updateStmt[UserTable.updatedAt] = updatedUser.updatedAt.toLocalDateTime(TimeZone.UTC)
}
findById(user.userId)!!
}
override suspend fun updateLastLogin(userId: Uuid) = DatabaseFactory.dbQuery {
val now = Clock.System.now()
users[userId] = user.copy(fehlgeschlageneAnmeldungen = 0, updatedAt = now)
UserTable.update({ UserTable.id eq userId }) { updateStmt ->
updateStmt[UserTable.lastLoginAt] = now
updateStmt[UserTable.updatedAt] = now.toLocalDateTime(TimeZone.UTC)
}
Unit
}
override suspend fun lockUser(userId: Uuid, lockedUntil: Instant) {
val user = users[userId] ?: return
override suspend fun incrementFailedLoginAttempts(userId: Uuid) = DatabaseFactory.dbQuery {
val now = Clock.System.now()
users[userId] = user.copy(gesperrtBis = lockedUntil, updatedAt = now)
UserTable.update({ UserTable.id eq userId }) { updateStmt ->
updateStmt[UserTable.failedLoginAttempts] = UserTable.failedLoginAttempts + 1
updateStmt[UserTable.updatedAt] = now.toLocalDateTime(TimeZone.UTC)
}
Unit
}
override suspend fun unlockUser(userId: Uuid) {
val user = users[userId] ?: return
override suspend fun resetFailedLoginAttempts(userId: Uuid) = DatabaseFactory.dbQuery {
val now = Clock.System.now()
users[userId] = user.copy(gesperrtBis = null, updatedAt = now)
UserTable.update({ UserTable.id eq userId }) { updateStmt ->
updateStmt[UserTable.failedLoginAttempts] = 0
updateStmt[UserTable.updatedAt] = now.toLocalDateTime(TimeZone.UTC)
}
Unit
}
override suspend fun setUserActive(userId: Uuid, isActive: Boolean) {
val user = users[userId] ?: return
override suspend fun lockUser(userId: Uuid, lockedUntil: Instant) = DatabaseFactory.dbQuery {
val now = Clock.System.now()
users[userId] = user.copy(istAktiv = isActive, updatedAt = now)
UserTable.update({ UserTable.id eq userId }) { updateStmt ->
updateStmt[UserTable.lockedUntil] = lockedUntil
updateStmt[UserTable.updatedAt] = now.toLocalDateTime(TimeZone.UTC)
}
Unit
}
override suspend fun markEmailAsVerified(userId: Uuid) {
val user = users[userId] ?: return
override suspend fun unlockUser(userId: Uuid) = DatabaseFactory.dbQuery {
val now = Clock.System.now()
users[userId] = user.copy(istEmailVerifiziert = true, updatedAt = now)
UserTable.update({ UserTable.id eq userId }) { updateStmt ->
updateStmt[UserTable.lockedUntil] = null
updateStmt[UserTable.failedLoginAttempts] = 0
updateStmt[UserTable.updatedAt] = now.toLocalDateTime(TimeZone.UTC)
}
Unit
}
override suspend fun updatePassword(userId: Uuid, passwordHash: String, salt: String) {
val user = users[userId] ?: return
override suspend fun setUserActive(userId: Uuid, isActive: Boolean) = DatabaseFactory.dbQuery {
val now = Clock.System.now()
users[userId] = user.copy(passwordHash = passwordHash, salt = salt, updatedAt = now)
UserTable.update({ UserTable.id eq userId }) { updateStmt ->
updateStmt[UserTable.isActive] = isActive
updateStmt[UserTable.updatedAt] = now.toLocalDateTime(TimeZone.UTC)
}
Unit
}
override suspend fun deleteUser(userId: Uuid): Boolean {
return users.remove(userId) != null
override suspend fun markEmailAsVerified(userId: Uuid) = DatabaseFactory.dbQuery {
val now = Clock.System.now()
UserTable.update({ UserTable.id eq userId }) { updateStmt ->
updateStmt[UserTable.isEmailVerified] = true
updateStmt[UserTable.updatedAt] = now.toLocalDateTime(TimeZone.UTC)
}
Unit
}
override suspend fun getAllUsers(): List<DomUser> {
return users.values.toList()
override suspend fun updatePassword(userId: Uuid, passwordHash: String, salt: String) = DatabaseFactory.dbQuery {
val now = Clock.System.now()
UserTable.update({ UserTable.id eq userId }) { updateStmt ->
updateStmt[UserTable.passwordHash] = passwordHash
updateStmt[UserTable.salt] = salt
updateStmt[UserTable.updatedAt] = now.toLocalDateTime(TimeZone.UTC)
}
Unit
}
override suspend fun getActiveUsers(): List<DomUser> {
return users.values.filter { it.istAktiv }
override suspend fun deleteUser(userId: Uuid): Boolean = DatabaseFactory.dbQuery {
UserTable.deleteWhere { UserTable.id eq userId } > 0
}
override suspend fun getAllUsers(): List<DomUser> = DatabaseFactory.dbQuery {
UserTable.selectAll()
.map(::rowToDomUser)
}
override suspend fun getActiveUsers(): List<DomUser> = DatabaseFactory.dbQuery {
UserTable.select { UserTable.isActive eq true }
.map(::rowToDomUser)
}
}
@@ -1,10 +1,14 @@
package at.mocode.members.infrastructure.repository
// Import table definition and extension functions
import at.mocode.members.domain.model.DomVerein
import at.mocode.members.domain.repository.VereinRepository
import at.mocode.members.infrastructure.repository.VereinTable
import at.mocode.shared.database.DatabaseFactory
import com.benasher44.uuid.Uuid
import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
@@ -16,21 +20,21 @@ import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
*/
class VereinRepositoryImpl : VereinRepository {
override suspend fun findById(id: Uuid): DomVerein? {
return VereinTable.selectAll().where { VereinTable.id eq id }
override suspend fun findById(id: Uuid): DomVerein? = DatabaseFactory.dbQuery {
VereinTable.select { VereinTable.id eq id }
.map { rowToDomVerein(it) }
.singleOrNull()
}
override suspend fun findByOepsVereinsNr(oepsVereinsNr: String): DomVerein? {
return VereinTable.selectAll().where { VereinTable.oepsVereinsNr eq oepsVereinsNr }
override suspend fun findByOepsVereinsNr(oepsVereinsNr: String): DomVerein? = DatabaseFactory.dbQuery {
VereinTable.select { VereinTable.oepsVereinsNr eq oepsVereinsNr }
.map { rowToDomVerein(it) }
.singleOrNull()
}
override suspend fun findByName(searchTerm: String, limit: Int): List<DomVerein> {
override suspend fun findByName(searchTerm: String, limit: Int): List<DomVerein> = DatabaseFactory.dbQuery {
val searchPattern = "%$searchTerm%"
return VereinTable.selectAll().where {
VereinTable.select {
(VereinTable.name like searchPattern) or
(VereinTable.kuerzel like searchPattern)
}
@@ -38,25 +42,25 @@ class VereinRepositoryImpl : VereinRepository {
.map { rowToDomVerein(it) }
}
override suspend fun findByBundeslandId(bundeslandId: Uuid): List<DomVerein> {
return VereinTable.selectAll().where { VereinTable.bundeslandId eq bundeslandId }
override suspend fun findByBundeslandId(bundeslandId: Uuid): List<DomVerein> = DatabaseFactory.dbQuery {
VereinTable.select { VereinTable.bundeslandId eq bundeslandId }
.map { rowToDomVerein(it) }
}
override suspend fun findByLandId(landId: Uuid): List<DomVerein> {
return VereinTable.selectAll().where { VereinTable.landId eq landId }
override suspend fun findByLandId(landId: Uuid): List<DomVerein> = DatabaseFactory.dbQuery {
VereinTable.select { VereinTable.landId eq landId }
.map { rowToDomVerein(it) }
}
override suspend fun findAllActive(limit: Int, offset: Int): List<DomVerein> {
return VereinTable.selectAll().where { VereinTable.istAktiv eq true }
override suspend fun findAllActive(limit: Int, offset: Int): List<DomVerein> = DatabaseFactory.dbQuery {
VereinTable.select { VereinTable.istAktiv eq true }
.limit(limit, offset.toLong())
.map { rowToDomVerein(it) }
}
override suspend fun findByLocation(searchTerm: String, limit: Int): List<DomVerein> {
override suspend fun findByLocation(searchTerm: String, limit: Int): List<DomVerein> = DatabaseFactory.dbQuery {
val searchPattern = "%$searchTerm%"
return VereinTable.selectAll().where {
VereinTable.select {
(VereinTable.ort like searchPattern) or
(VereinTable.plz like searchPattern)
}
@@ -64,52 +68,74 @@ class VereinRepositoryImpl : VereinRepository {
.map { rowToDomVerein(it) }
}
override suspend fun save(verein: DomVerein): DomVerein {
override suspend fun save(verein: DomVerein): DomVerein = DatabaseFactory.dbQuery {
val now = Clock.System.now()
val updatedVerein = verein.copy(updatedAt = now)
val existingVerein = findById(verein.vereinId)
VereinTable.insertOrUpdate(VereinTable.id) {
it[id] = verein.vereinId
it[oepsVereinsNr] = verein.oepsVereinsNr
it[name] = verein.name
it[kuerzel] = verein.kuerzel
it[adresseStrasse] = verein.adresseStrasse
it[plz] = verein.plz
it[ort] = verein.ort
it[bundeslandId] = verein.bundeslandId
it[landId] = verein.landId
it[emailAllgemein] = verein.emailAllgemein
it[telefonAllgemein] = verein.telefonAllgemein
it[webseiteUrl] = verein.webseiteUrl
it[datenQuelle] = verein.datenQuelle
it[istAktiv] = verein.istAktiv
it[notizenIntern] = verein.notizenIntern
it[createdAt] = verein.createdAt.toLocalDateTime()
it[updatedAt] = updatedVerein.updatedAt.toLocalDateTime()
if (existingVerein == null) {
// Insert new verein
VereinTable.insert { stmt ->
stmt[VereinTable.id] = verein.vereinId
stmt[VereinTable.oepsVereinsNr] = verein.oepsVereinsNr
stmt[VereinTable.name] = verein.name
stmt[VereinTable.kuerzel] = verein.kuerzel
stmt[VereinTable.adresseStrasse] = verein.adresseStrasse
stmt[VereinTable.plz] = verein.plz
stmt[VereinTable.ort] = verein.ort
stmt[VereinTable.bundeslandId] = verein.bundeslandId
stmt[VereinTable.landId] = verein.landId
stmt[VereinTable.emailAllgemein] = verein.emailAllgemein
stmt[VereinTable.telefonAllgemein] = verein.telefonAllgemein
stmt[VereinTable.webseiteUrl] = verein.webseiteUrl
stmt[VereinTable.datenQuelle] = verein.datenQuelle
stmt[VereinTable.istAktiv] = verein.istAktiv
stmt[VereinTable.notizenIntern] = verein.notizenIntern
stmt[VereinTable.createdAt] = verein.createdAt.toLocalDateTime(TimeZone.UTC)
stmt[VereinTable.updatedAt] = now.toLocalDateTime(TimeZone.UTC)
}
} else {
// Update existing verein
VereinTable.update({ VereinTable.id eq verein.vereinId }) { stmt ->
stmt[VereinTable.oepsVereinsNr] = verein.oepsVereinsNr
stmt[VereinTable.name] = verein.name
stmt[VereinTable.kuerzel] = verein.kuerzel
stmt[VereinTable.adresseStrasse] = verein.adresseStrasse
stmt[VereinTable.plz] = verein.plz
stmt[VereinTable.ort] = verein.ort
stmt[VereinTable.bundeslandId] = verein.bundeslandId
stmt[VereinTable.landId] = verein.landId
stmt[VereinTable.emailAllgemein] = verein.emailAllgemein
stmt[VereinTable.telefonAllgemein] = verein.telefonAllgemein
stmt[VereinTable.webseiteUrl] = verein.webseiteUrl
stmt[VereinTable.datenQuelle] = verein.datenQuelle
stmt[VereinTable.istAktiv] = verein.istAktiv
stmt[VereinTable.notizenIntern] = verein.notizenIntern
stmt[VereinTable.updatedAt] = now.toLocalDateTime(TimeZone.UTC)
}
}
return updatedVerein
verein.copy(updatedAt = now)
}
override suspend fun delete(id: Uuid): Boolean {
override suspend fun delete(id: Uuid): Boolean = DatabaseFactory.dbQuery {
val deletedRows = VereinTable.deleteWhere { VereinTable.id eq id }
return deletedRows > 0
deletedRows > 0
}
override suspend fun existsByOepsVereinsNr(oepsVereinsNr: String): Boolean {
return VereinTable.selectAll().where { VereinTable.oepsVereinsNr eq oepsVereinsNr }
override suspend fun existsByOepsVereinsNr(oepsVereinsNr: String): Boolean = DatabaseFactory.dbQuery {
VereinTable.select { VereinTable.oepsVereinsNr eq oepsVereinsNr }
.count() > 0
}
override suspend fun countActive(): Long {
return VereinTable.selectAll().where { VereinTable.istAktiv eq true }
override suspend fun countActive(): Long = DatabaseFactory.dbQuery {
VereinTable.select { VereinTable.istAktiv eq true }
.count()
}
override suspend fun countActiveByBundeslandId(bundeslandId: Uuid): Long {
return VereinTable.selectAll()
.where { (VereinTable.istAktiv eq true) and (VereinTable.bundeslandId eq bundeslandId) }
.count()
override suspend fun countActiveByBundeslandId(bundeslandId: Uuid): Long = DatabaseFactory.dbQuery {
VereinTable.select {
(VereinTable.istAktiv eq true) and (VereinTable.bundeslandId eq bundeslandId)
}.count()
}
/**
@@ -132,8 +158,8 @@ class VereinRepositoryImpl : VereinRepository {
datenQuelle = row[VereinTable.datenQuelle],
istAktiv = row[VereinTable.istAktiv],
notizenIntern = row[VereinTable.notizenIntern],
createdAt = row[VereinTable.createdAt].toInstant(),
updatedAt = row[VereinTable.updatedAt].toInstant()
createdAt = row[VereinTable.createdAt].toInstant(TimeZone.UTC),
updatedAt = row[VereinTable.updatedAt].toInstant(TimeZone.UTC)
)
}
}
@@ -0,0 +1,28 @@
package at.mocode.members.infrastructure.table
import at.mocode.enums.BerechtigungE
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
/**
* Exposed-Tabellendefinition für die Berechtigung-Entität.
*/
object BerechtigungTable : Table("berechtigung") {
val id = uuid("id").autoGenerate()
val berechtigungTyp = enumerationByName<BerechtigungE>("berechtigung_typ", 50)
val name = varchar("name", 100)
val beschreibung = text("beschreibung").nullable()
val ressource = varchar("ressource", 50)
val aktion = varchar("aktion", 50)
val istAktiv = bool("ist_aktiv").default(true)
val istSystemBerechtigung = bool("ist_system_berechtigung").default(false)
val createdAt = datetime("created_at").defaultExpression(CurrentDateTime)
val updatedAt = datetime("updated_at").defaultExpression(CurrentDateTime)
override val primaryKey = PrimaryKey(id)
init {
uniqueIndex(berechtigungTyp)
}
}
@@ -0,0 +1,25 @@
package at.mocode.members.infrastructure.table
import at.mocode.members.infrastructure.table.RolleTable
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.kotlin.datetime.date
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
/**
* Exposed-Tabellendefinition für die Zuordnung von Rollen zu Personen.
*/
object PersonRolleTable : Table("person_rolle") {
val id = uuid("id")
val personId = uuid("person_id")
val rolleId = uuid("rolle_id").references(RolleTable.id)
val vereinId = uuid("verein_id").nullable()
val gueltigVon = date("gueltig_von")
val gueltigBis = date("gueltig_bis").nullable()
val istAktiv = bool("ist_aktiv").default(true)
val zugewiesenVon = uuid("zugewiesen_von").nullable()
val notizen = text("notizen").nullable()
val createdAt = timestamp("created_at")
val updatedAt = timestamp("updated_at")
override val primaryKey = PrimaryKey(id)
}
@@ -0,0 +1,26 @@
package at.mocode.members.infrastructure.table
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
/**
* Exposed-Tabellendefinition für die Zuordnung von Berechtigungen zu Rollen.
*/
object RolleBerechtigungTable : Table("rolle_berechtigung") {
val id = uuid("id").autoGenerate()
val rolleId = uuid("rolle_id").references(RolleTable.id)
val berechtigungId = uuid("berechtigung_id").references(BerechtigungTable.id)
val istAktiv = bool("ist_aktiv").default(true)
val zugewiesenVon = uuid("zugewiesen_von").nullable()
val notizen = text("notizen").nullable()
val createdAt = datetime("created_at").defaultExpression(CurrentDateTime)
val updatedAt = datetime("updated_at").defaultExpression(CurrentDateTime)
override val primaryKey = PrimaryKey(id)
init {
// Unique constraint on role-permission combination
uniqueIndex(rolleId, berechtigungId)
}
}
@@ -0,0 +1,22 @@
package at.mocode.members.infrastructure.table
import at.mocode.enums.RolleE
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
/**
* Exposed-Tabellendefinition für die Rolle-Entität.
*/
object RolleTable : Table("rolle") {
val id = uuid("id").autoGenerate()
val rolleTyp = enumeration<RolleE>("rolle_typ")
val name = varchar("name", 50).uniqueIndex()
val beschreibung = text("beschreibung").nullable()
val istSystemRolle = bool("ist_system_rolle").default(false)
val istAktiv = bool("ist_aktiv").default(true)
val createdAt = datetime("created_at").defaultExpression(CurrentDateTime)
val updatedAt = datetime("updated_at").defaultExpression(CurrentDateTime)
override val primaryKey = PrimaryKey(id)
}
@@ -0,0 +1,27 @@
package at.mocode.members.infrastructure.table
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
/**
* Exposed-Tabellendefinition für die User-Entität.
*/
object UserTable : Table("benutzer") {
val id = uuid("id").autoGenerate()
val personId = uuid("person_id")
val username = varchar("username", 50).uniqueIndex()
val email = varchar("email", 100).uniqueIndex()
val passwordHash = varchar("password_hash", 255)
val salt = varchar("salt", 64)
val isActive = bool("is_active").default(true)
val isEmailVerified = bool("is_email_verified").default(false)
val failedLoginAttempts = integer("failed_login_attempts").default(0)
val lockedUntil = timestamp("locked_until").nullable()
val lastLoginAt = timestamp("last_login_at").nullable()
val createdAt = datetime("created_at").defaultExpression(CurrentDateTime)
val updatedAt = datetime("updated_at").defaultExpression(CurrentDateTime)
override val primaryKey = PrimaryKey(id)
}