(vision) SCS/DDD

This commit is contained in:
2025-07-18 23:07:05 +02:00
parent 029b0c86bc
commit 611e31e196
68 changed files with 6949 additions and 137 deletions
+4
View File
@@ -18,6 +18,10 @@ kotlin {
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.datetime)
implementation(libs.uuid)
implementation(libs.exposed.core)
implementation(libs.exposed.dao)
implementation(libs.exposed.jdbc)
implementation(libs.exposed.kotlinDatetime)
}
commonTest.dependencies {
@@ -0,0 +1,117 @@
package at.mocode.members.application.usecase
import at.mocode.members.domain.model.DomPersonRolle
import at.mocode.members.domain.repository.PersonRepository
import at.mocode.members.domain.repository.PersonRolleRepository
import at.mocode.members.domain.repository.RolleRepository
import at.mocode.members.domain.repository.VereinRepository
import com.benasher44.uuid.Uuid
import kotlinx.datetime.Clock
import kotlinx.datetime.LocalDate
/**
* Use Case für das Zuweisen einer Rolle zu einer Person.
*
* Dieser Use Case validiert die Eingabedaten und erstellt eine neue Person-Rolle-Zuordnung,
* falls diese noch nicht existiert.
*/
class AssignRoleToPersonUseCase(
private val personRepository: PersonRepository,
private val rolleRepository: RolleRepository,
private val personRolleRepository: PersonRolleRepository,
private val vereinRepository: VereinRepository
) {
/**
* Weist einer Person eine Rolle zu.
*
* @param request Die Anfrage mit den Zuordnungsdaten.
* @return Die erstellte Person-Rolle-Zuordnung.
* @throws IllegalArgumentException wenn ungültige Daten übergeben wurden oder die Zuordnung bereits existiert.
*/
suspend fun execute(request: AssignRoleToPersonRequest): DomPersonRolle {
// Validierung der Eingabedaten
validateRequest(request)
// Prüfen, ob Person existiert
val person = personRepository.findById(request.personId)
?: throw IllegalArgumentException("Person mit ID '${request.personId}' wurde nicht gefunden.")
// Prüfen, ob Rolle existiert
val rolle = rolleRepository.findById(request.rolleId)
?: throw IllegalArgumentException("Rolle mit ID '${request.rolleId}' wurde nicht gefunden.")
// Prüfen, ob Rolle aktiv ist
if (!rolle.istAktiv) {
throw IllegalArgumentException("Die Rolle '${rolle.name}' ist nicht aktiv und kann nicht zugewiesen werden.")
}
// Prüfen, ob Verein existiert (falls angegeben)
request.vereinId?.let { vereinId ->
val verein = vereinRepository.findById(vereinId)
?: throw IllegalArgumentException("Verein mit ID '$vereinId' wurde nicht gefunden.")
if (!verein.istAktiv) {
throw IllegalArgumentException("Der Verein '${verein.name}' ist nicht aktiv.")
}
}
// Prüfen, ob die Zuordnung bereits existiert
val existierendeZuordnung = personRolleRepository.findByPersonAndRolle(
request.personId,
request.rolleId,
request.vereinId
)
if (existierendeZuordnung != null && existierendeZuordnung.istAktiv) {
throw IllegalArgumentException("Die Person '${person.nachname}, ${person.vorname}' hat bereits die Rolle '${rolle.name}'.")
}
// Neue Person-Rolle-Zuordnung erstellen
val personRolle = DomPersonRolle(
personId = request.personId,
rolleId = request.rolleId,
vereinId = request.vereinId,
gueltigVon = request.gueltigVon,
gueltigBis = request.gueltigBis,
istAktiv = true,
zugewiesenVon = request.zugewiesenVon,
notizen = request.notizen,
updatedAt = Clock.System.now()
)
// Person-Rolle-Zuordnung speichern
return personRolleRepository.save(personRolle)
}
private fun validateRequest(request: AssignRoleToPersonRequest) {
// Prüfen, ob gueltigBis nach gueltigVon liegt
request.gueltigBis?.let { gueltigBis ->
if (gueltigBis <= request.gueltigVon) {
throw IllegalArgumentException("Das Enddatum muss nach dem Startdatum liegen.")
}
}
// Prüfen, ob gueltigVon nicht in der Vergangenheit liegt (optional, je nach Geschäftslogik)
// Hier könnte man auch erlauben, dass Rollen rückwirkend zugewiesen werden
request.notizen?.let { notizen ->
if (notizen.length > 1000) {
throw IllegalArgumentException("Die Notizen dürfen maximal 1000 Zeichen lang sein.")
}
}
}
}
/**
* Request-Datenklasse für das Zuweisen einer Rolle zu einer Person.
*/
data class AssignRoleToPersonRequest(
val personId: Uuid,
val rolleId: Uuid,
val vereinId: Uuid? = null,
val gueltigVon: LocalDate,
val gueltigBis: LocalDate? = null,
val zugewiesenVon: Uuid? = null,
val notizen: String? = null
)
@@ -0,0 +1,128 @@
package at.mocode.members.application.usecase
import at.mocode.dto.base.ApiResponse
import at.mocode.dto.base.ErrorDto
import at.mocode.enums.BerechtigungE
import at.mocode.members.domain.model.DomBerechtigung
import at.mocode.members.domain.repository.BerechtigungRepository
import at.mocode.validation.ValidationUtils
import at.mocode.validation.ValidationResult
import at.mocode.validation.ValidationError
import kotlinx.datetime.Clock
/**
* Use case for creating new permissions (Berechtigungen) in the system.
*/
class CreateBerechtigungUseCase(
private val berechtigungRepository: BerechtigungRepository
) {
data class CreateBerechtigungRequest(
val berechtigungTyp: BerechtigungE,
val name: String,
val beschreibung: String? = null,
val ressource: String,
val aktion: String,
val istSystemBerechtigung: Boolean = false
)
data class CreateBerechtigungResponse(
val berechtigung: DomBerechtigung
)
suspend fun execute(request: CreateBerechtigungRequest): ApiResponse<CreateBerechtigungResponse> {
try {
// Validate request
val validationResult = validateRequest(request)
if (!validationResult.isValid()) {
val errors = (validationResult as ValidationResult.Invalid).errors
return ApiResponse(
success = false,
error = ErrorDto(
code = "VALIDATION_ERROR",
message = "Validation failed",
details = errors.associate { it.field to it.message }
)
)
}
// Check if permission with this type already exists
val existingBerechtigung = berechtigungRepository.findByTyp(request.berechtigungTyp)
if (existingBerechtigung != null) {
return ApiResponse(
success = false,
error = ErrorDto(
code = "BERECHTIGUNG_ALREADY_EXISTS",
message = "A permission with this type already exists",
details = mapOf("berechtigungTyp" to request.berechtigungTyp.toString())
)
)
}
// Create new permission
val berechtigung = DomBerechtigung(
berechtigungTyp = request.berechtigungTyp,
name = request.name,
beschreibung = request.beschreibung,
ressource = request.ressource,
aktion = request.aktion,
istSystemBerechtigung = request.istSystemBerechtigung,
createdAt = Clock.System.now(),
updatedAt = Clock.System.now()
)
// Save to repository
val savedBerechtigung = berechtigungRepository.save(berechtigung)
return ApiResponse(
success = true,
data = CreateBerechtigungResponse(savedBerechtigung)
)
} catch (e: Exception) {
return ApiResponse(
success = false,
error = ErrorDto(
code = "INTERNAL_ERROR",
message = "An error occurred while creating the permission",
details = mapOf("error" to e.message.orEmpty())
)
)
}
}
private fun validateRequest(request: CreateBerechtigungRequest): ValidationResult {
val errors = mutableListOf<ValidationError>()
// Validate name
ValidationUtils.validateNotBlank(request.name, "name")?.let { error ->
errors.add(error)
}
// Validate ressource
ValidationUtils.validateNotBlank(request.ressource, "ressource")?.let { error ->
errors.add(error)
}
// Validate aktion
ValidationUtils.validateNotBlank(request.aktion, "aktion")?.let { error ->
errors.add(error)
}
// Validate name length
if (request.name.length > 100) {
errors.add(ValidationError("name", "Name must not exceed 100 characters"))
}
// Validate ressource length
if (request.ressource.length > 50) {
errors.add(ValidationError("ressource", "Ressource must not exceed 50 characters"))
}
// Validate aktion length
if (request.aktion.length > 50) {
errors.add(ValidationError("aktion", "Aktion must not exceed 50 characters"))
}
return ValidationResult(errors)
}
}
@@ -6,6 +6,9 @@ import at.mocode.members.domain.model.DomPerson
import at.mocode.members.domain.repository.PersonRepository
import at.mocode.members.domain.repository.VereinRepository
import at.mocode.members.domain.service.MasterDataService
import at.mocode.validation.ValidationUtils
import at.mocode.validation.ValidationResult
import at.mocode.validation.ValidationError
import kotlinx.datetime.Clock
/**
@@ -68,14 +71,15 @@ class CreatePersonUseCase(
suspend fun execute(request: CreatePersonRequest): ApiResponse<CreatePersonResponse> {
try {
// Validate required fields
val validationErrors = validateRequest(request)
if (validationErrors.isNotEmpty()) {
val validationResult = validateRequest(request)
if (!validationResult.isValid()) {
val errors = (validationResult as ValidationResult.Invalid).errors
return ApiResponse(
success = false,
error = ErrorDto(
code = "VALIDATION_ERROR",
message = "Invalid input data",
details = validationErrors
details = errors.associate { it.field to it.message }
)
)
}
@@ -94,14 +98,15 @@ class CreatePersonUseCase(
}
// Validate referenced entities
val entityValidationErrors = validateReferencedEntities(request)
if (entityValidationErrors.isNotEmpty()) {
val entityValidationResult = validateReferencedEntities(request)
if (!entityValidationResult.isValid()) {
val errors = (entityValidationResult as ValidationResult.Invalid).errors
return ApiResponse(
success = false,
error = ErrorDto(
code = "INVALID_REFERENCES",
message = "Referenced entities not found",
details = entityValidationErrors
details = errors.associate { it.field to it.message }
)
)
}
@@ -154,50 +159,73 @@ class CreatePersonUseCase(
}
}
private fun validateRequest(request: CreatePersonRequest): Map<String, String> {
val errors = mutableMapOf<String, String>()
private fun validateRequest(request: CreatePersonRequest): ValidationResult {
val errors = mutableListOf<ValidationError>()
if (request.nachname.isBlank()) {
errors["nachname"] = "Last name is required"
// Validate required fields using ValidationUtils
ValidationUtils.validateNotBlank(request.nachname, "nachname")?.let { error ->
errors.add(error)
}
if (request.vorname.isBlank()) {
errors["vorname"] = "First name is required"
ValidationUtils.validateNotBlank(request.vorname, "vorname")?.let { error ->
errors.add(error)
}
if (request.oepsSatzNr != null && request.oepsSatzNr.length != 6) {
errors["oepsSatzNr"] = "OEPS Satznummer must be exactly 6 digits"
// Validate OEPS Satz number using ValidationUtils
ValidationUtils.validateOepsSatzNr(request.oepsSatzNr, "oepsSatzNr")?.let { error ->
errors.add(error)
}
if (request.email != null && !isValidEmail(request.email)) {
errors["email"] = "Invalid email format"
// Validate email using ValidationUtils
ValidationUtils.validateEmail(request.email, "email")?.let { error ->
errors.add(error)
}
return errors
// Validate phone number using ValidationUtils
ValidationUtils.validatePhoneNumber(request.telefon, "telefon")?.let { error ->
errors.add(error)
}
// Validate postal code using ValidationUtils
ValidationUtils.validatePostalCode(request.plz, "plz")?.let { error ->
errors.add(error)
}
// Validate birth date using ValidationUtils
ValidationUtils.validateBirthDate(request.geburtsdatum, "geburtsdatum")?.let { error ->
errors.add(error)
}
return if (errors.isEmpty()) {
ValidationResult.Valid
} else {
ValidationResult.Invalid(errors)
}
}
private suspend fun validateReferencedEntities(request: CreatePersonRequest): Map<String, String> {
val errors = mutableMapOf<String, String>()
private suspend fun validateReferencedEntities(request: CreatePersonRequest): ValidationResult {
val errors = mutableListOf<ValidationError>()
// Validate club reference
if (request.stammVereinId != null) {
val verein = vereinRepository.findById(request.stammVereinId)
if (verein == null) {
errors["stammVereinId"] = "Referenced club not found"
errors.add(ValidationError("stammVereinId", "Referenced club not found", "NOT_FOUND"))
}
}
// Validate country reference
if (request.nationalitaetLandId != null) {
if (!masterDataService.countryExists(request.nationalitaetLandId)) {
errors["nationalitaetLandId"] = "Referenced country not found"
errors.add(ValidationError("nationalitaetLandId", "Referenced country not found", "NOT_FOUND"))
}
}
return errors
return if (errors.isEmpty()) {
ValidationResult.Valid
} else {
ValidationResult.Invalid(errors)
}
}
private fun isValidEmail(email: String): Boolean {
return email.contains("@") && email.contains(".")
}
}
@@ -0,0 +1,74 @@
package at.mocode.members.application.usecase
import at.mocode.enums.RolleE
import at.mocode.members.domain.model.DomRolle
import at.mocode.members.domain.repository.RolleRepository
import kotlinx.datetime.Clock
/**
* Use Case für das Erstellen einer neuen Rolle im System.
*
* Dieser Use Case validiert die Eingabedaten und erstellt eine neue Rolle,
* falls diese noch nicht existiert.
*/
class CreateRolleUseCase(
private val rolleRepository: RolleRepository
) {
/**
* Erstellt eine neue Rolle im System.
*
* @param request Die Anfrage mit den Rollendaten.
* @return Die erstellte Rolle.
* @throws IllegalArgumentException wenn die Rolle bereits existiert oder ungültige Daten übergeben wurden.
*/
suspend fun execute(request: CreateRolleRequest): DomRolle {
// Validierung der Eingabedaten
validateRequest(request)
// Prüfen, ob eine Rolle mit diesem Typ bereits existiert
if (rolleRepository.existsByTyp(request.rolleTyp)) {
throw IllegalArgumentException("Eine Rolle mit dem Typ '${request.rolleTyp}' existiert bereits.")
}
// Neue Rolle erstellen
val neueRolle = DomRolle(
rolleTyp = request.rolleTyp,
name = request.name,
beschreibung = request.beschreibung,
istAktiv = request.istAktiv ?: true,
istSystemRolle = request.istSystemRolle ?: false,
updatedAt = Clock.System.now()
)
// Rolle speichern
return rolleRepository.save(neueRolle)
}
private fun validateRequest(request: CreateRolleRequest) {
if (request.name.isBlank()) {
throw IllegalArgumentException("Der Name der Rolle darf nicht leer sein.")
}
if (request.name.length > 100) {
throw IllegalArgumentException("Der Name der Rolle darf maximal 100 Zeichen lang sein.")
}
request.beschreibung?.let { beschreibung ->
if (beschreibung.length > 500) {
throw IllegalArgumentException("Die Beschreibung der Rolle darf maximal 500 Zeichen lang sein.")
}
}
}
}
/**
* Request-Datenklasse für das Erstellen einer Rolle.
*/
data class CreateRolleRequest(
val rolleTyp: RolleE,
val name: String,
val beschreibung: String? = null,
val istAktiv: Boolean? = null,
val istSystemRolle: Boolean? = null
)
@@ -0,0 +1,48 @@
package at.mocode.members.domain.model
import at.mocode.enums.BerechtigungE
import at.mocode.serializers.KotlinInstantSerializer
import at.mocode.serializers.UuidSerializer
import com.benasher44.uuid.Uuid
import com.benasher44.uuid.uuid4
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
/**
* Repräsentiert eine Berechtigung im System für die Zugriffskontrolle.
*
* Berechtigungen definieren spezifische Aktionen, die im System ausgeführt werden können
* (z.B. Personen lesen, Vereine erstellen, Veranstaltungen bearbeiten).
* Berechtigungen werden Rollen zugeordnet, die wiederum Personen zugewiesen werden.
*
* @property berechtigungId Eindeutiger interner Identifikator für diese Berechtigung (UUID).
* @property berechtigungTyp Der Typ der Berechtigung aus der BerechtigungE Enumeration.
* @property name Anzeigename der Berechtigung (z.B. "Personen lesen", "Vereine erstellen").
* @property beschreibung Detaillierte Beschreibung der Berechtigung und ihres Zwecks.
* @property ressource Die Ressource, auf die sich diese Berechtigung bezieht (z.B. "Person", "Verein").
* @property aktion Die Aktion, die mit dieser Berechtigung ausgeführt werden kann (z.B. "lesen", "erstellen").
* @property istAktiv Gibt an, ob diese Berechtigung aktuell aktiv ist.
* @property istSystemBerechtigung Gibt an, ob es sich um eine Systemberechtigung handelt, die nicht gelöscht werden kann.
* @property createdAt Zeitstempel der Erstellung dieser Berechtigung.
* @property updatedAt Zeitstempel der letzten Aktualisierung dieser Berechtigung.
*/
@Serializable
data class DomBerechtigung(
@Serializable(with = UuidSerializer::class)
val berechtigungId: Uuid = uuid4(),
val berechtigungTyp: BerechtigungE,
var name: String,
var beschreibung: String? = null,
var ressource: String,
var aktion: String,
var istAktiv: Boolean = true,
var istSystemBerechtigung: Boolean = false,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant = Clock.System.now(),
@Serializable(with = KotlinInstantSerializer::class)
var updatedAt: Instant = Clock.System.now()
)
@@ -0,0 +1,63 @@
package at.mocode.members.domain.model
import at.mocode.serializers.KotlinInstantSerializer
import at.mocode.serializers.KotlinLocalDateSerializer
import at.mocode.serializers.UuidSerializer
import com.benasher44.uuid.Uuid
import com.benasher44.uuid.uuid4
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import kotlinx.serialization.Serializable
/**
* Repräsentiert die Zuordnung einer Rolle zu einer Person.
*
* Diese Entität verwaltet die Many-to-Many-Beziehung zwischen Personen und Rollen.
* Eine Person kann mehrere Rollen haben (z.B. gleichzeitig Reiter und Trainer),
* und eine Rolle kann mehreren Personen zugeordnet werden.
*
* @property personRolleId Eindeutiger interner Identifikator für diese Rollenzuordnung (UUID).
* @property personId Fremdschlüssel zur Person (DomPerson.personId).
* @property rolleId Fremdschlüssel zur Rolle (DomRolle.rolleId).
* @property vereinId Optionale Verknüpfung zu einem Verein, falls die Rolle vereinsspezifisch ist.
* @property gueltigVon Datum, ab dem diese Rollenzuordnung gültig ist.
* @property gueltigBis Optionales Datum, bis zu dem diese Rollenzuordnung gültig ist.
* @property istAktiv Gibt an, ob diese Rollenzuordnung aktuell aktiv ist.
* @property zugewiesenVon Optionale Referenz auf die Person, die diese Rolle zugewiesen hat.
* @property notizen Optionale Notizen zur Rollenzuordnung.
* @property createdAt Zeitstempel der Erstellung dieser Rollenzuordnung.
* @property updatedAt Zeitstempel der letzten Aktualisierung dieser Rollenzuordnung.
*/
@Serializable
data class DomPersonRolle(
@Serializable(with = UuidSerializer::class)
val personRolleId: Uuid = uuid4(),
@Serializable(with = UuidSerializer::class)
val personId: Uuid,
@Serializable(with = UuidSerializer::class)
val rolleId: Uuid,
@Serializable(with = UuidSerializer::class)
var vereinId: Uuid? = null, // Für vereinsspezifische Rollen
@Serializable(with = KotlinLocalDateSerializer::class)
var gueltigVon: LocalDate,
@Serializable(with = KotlinLocalDateSerializer::class)
var gueltigBis: LocalDate? = null,
var istAktiv: Boolean = true,
@Serializable(with = UuidSerializer::class)
var zugewiesenVon: Uuid? = null, // PersonId des Zuweisers
var notizen: String? = null,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant = Clock.System.now(),
@Serializable(with = KotlinInstantSerializer::class)
var updatedAt: Instant = Clock.System.now()
)
@@ -0,0 +1,44 @@
package at.mocode.members.domain.model
import at.mocode.enums.RolleE
import at.mocode.serializers.KotlinInstantSerializer
import at.mocode.serializers.UuidSerializer
import com.benasher44.uuid.Uuid
import com.benasher44.uuid.uuid4
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
/**
* Repräsentiert eine Rolle im System für die Mitgliederverwaltung.
*
* 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.
*
* @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 istSystemRolle Gibt an, ob es sich um eine Systemrolle handelt, die nicht gelöscht werden kann.
* @property createdAt Zeitstempel der Erstellung dieser Rolle.
* @property updatedAt Zeitstempel der letzten Aktualisierung dieser Rolle.
*/
@Serializable
data class DomRolle(
@Serializable(with = UuidSerializer::class)
val rolleId: Uuid = uuid4(),
val rolleTyp: RolleE,
var name: String,
var beschreibung: String? = null,
var istAktiv: Boolean = true,
var istSystemRolle: Boolean = false,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant = Clock.System.now(),
@Serializable(with = KotlinInstantSerializer::class)
var updatedAt: Instant = Clock.System.now()
)
@@ -0,0 +1,49 @@
package at.mocode.members.domain.model
import at.mocode.serializers.KotlinInstantSerializer
import at.mocode.serializers.UuidSerializer
import com.benasher44.uuid.Uuid
import com.benasher44.uuid.uuid4
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
/**
* Repräsentiert die Zuordnung einer Berechtigung zu einer Rolle.
*
* Diese Entität verwaltet die Many-to-Many-Beziehung zwischen Rollen und Berechtigungen.
* Eine Rolle kann mehrere Berechtigungen haben (z.B. Trainer kann Personen lesen und Pferde bearbeiten),
* und eine Berechtigung kann mehreren Rollen zugeordnet werden.
*
* @property rolleBerechtigungId Eindeutiger interner Identifikator für diese Berechtigungszuordnung (UUID).
* @property rolleId Fremdschlüssel zur Rolle (DomRolle.rolleId).
* @property berechtigungId Fremdschlüssel zur Berechtigung (DomBerechtigung.berechtigungId).
* @property istAktiv Gibt an, ob diese Berechtigungszuordnung aktuell aktiv ist.
* @property zugewiesenVon Optionale Referenz auf die Person, die diese Berechtigung zugewiesen hat.
* @property notizen Optionale Notizen zur Berechtigungszuordnung.
* @property createdAt Zeitstempel der Erstellung dieser Berechtigungszuordnung.
* @property updatedAt Zeitstempel der letzten Aktualisierung dieser Berechtigungszuordnung.
*/
@Serializable
data class DomRolleBerechtigung(
@Serializable(with = UuidSerializer::class)
val rolleBerechtigungId: Uuid = uuid4(),
@Serializable(with = UuidSerializer::class)
val rolleId: Uuid,
@Serializable(with = UuidSerializer::class)
val berechtigungId: Uuid,
var istAktiv: Boolean = true,
@Serializable(with = UuidSerializer::class)
var zugewiesenVon: Uuid? = null, // PersonId des Zuweisers
var notizen: String? = null,
@Serializable(with = KotlinInstantSerializer::class)
val createdAt: Instant = Clock.System.now(),
@Serializable(with = KotlinInstantSerializer::class)
var updatedAt: Instant = Clock.System.now()
)
@@ -0,0 +1,63 @@
package at.mocode.members.domain.model
import at.mocode.serializers.KotlinInstantSerializer
import at.mocode.serializers.UuidSerializer
import com.benasher44.uuid.Uuid
import com.benasher44.uuid.uuid4
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
/**
* Repräsentiert einen Benutzer für die Authentifizierung 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.
*
* @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 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 createdAt Zeitstempel der Erstellung dieses Benutzers.
* @property updatedAt Zeitstempel der letzten Aktualisierung dieses Benutzers.
*/
@Serializable
data class DomUser(
@Serializable(with = UuidSerializer::class)
val userId: Uuid = uuid4(),
@Serializable(with = UuidSerializer::class)
val personId: Uuid,
var username: String,
var email: String,
var passwordHash: String,
var salt: String,
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)
val createdAt: Instant = Clock.System.now(),
@Serializable(with = KotlinInstantSerializer::class)
var updatedAt: Instant = Clock.System.now()
)
@@ -0,0 +1,100 @@
package at.mocode.members.domain.repository
import at.mocode.enums.BerechtigungE
import at.mocode.members.domain.model.DomBerechtigung
import com.benasher44.uuid.Uuid
/**
* Repository-Interface für die Verwaltung von Berechtigungen.
*
* Definiert die Operationen für das Erstellen, Lesen, Aktualisieren und Löschen
* von Berechtigungen im System.
*/
interface BerechtigungRepository {
/**
* Speichert eine Berechtigung (erstellen oder aktualisieren).
*
* @param berechtigung Die zu speichernde Berechtigung.
* @return Die gespeicherte Berechtigung mit aktualisierten Zeitstempeln.
*/
suspend fun save(berechtigung: DomBerechtigung): DomBerechtigung
/**
* Sucht eine Berechtigung anhand ihrer ID.
*
* @param berechtigungId Die eindeutige ID der Berechtigung.
* @return Die gefundene Berechtigung oder null, falls nicht vorhanden.
*/
suspend fun findById(berechtigungId: Uuid): DomBerechtigung?
/**
* Sucht eine Berechtigung anhand ihres Typs.
*
* @param berechtigungTyp Der Typ der Berechtigung.
* @return Die gefundene Berechtigung oder null, falls nicht vorhanden.
*/
suspend fun findByTyp(berechtigungTyp: BerechtigungE): DomBerechtigung?
/**
* Sucht Berechtigungen anhand ihres Namens (Teilstring-Suche).
*
* @param name Der Name oder Teilname der Berechtigung.
* @return Liste der gefundenen Berechtigungen.
*/
suspend fun findByName(name: String): List<DomBerechtigung>
/**
* Sucht Berechtigungen anhand der Ressource.
*
* @param ressource Die Ressource (z.B. "Person", "Verein").
* @return Liste der gefundenen Berechtigungen.
*/
suspend fun findByRessource(ressource: String): List<DomBerechtigung>
/**
* Sucht Berechtigungen anhand der Aktion.
*
* @param aktion Die Aktion (z.B. "lesen", "erstellen").
* @return Liste der gefundenen Berechtigungen.
*/
suspend fun findByAktion(aktion: String): List<DomBerechtigung>
/**
* Gibt alle aktiven Berechtigungen zurück.
*
* @return Liste aller aktiven Berechtigungen.
*/
suspend fun findAllActive(): List<DomBerechtigung>
/**
* Gibt alle Berechtigungen zurück (aktive und inaktive).
*
* @return Liste aller Berechtigungen.
*/
suspend fun findAll(): List<DomBerechtigung>
/**
* Deaktiviert eine Berechtigung (soft delete).
*
* @param berechtigungId Die ID der zu deaktivierenden Berechtigung.
* @return true, wenn die Deaktivierung erfolgreich war, false sonst.
*/
suspend fun deactivateBerechtigung(berechtigungId: Uuid): Boolean
/**
* Löscht eine Berechtigung permanent (nur für nicht-System-Berechtigungen).
*
* @param berechtigungId Die ID der zu löschenden Berechtigung.
* @return true, wenn das Löschen erfolgreich war, false sonst.
*/
suspend fun deleteBerechtigung(berechtigungId: Uuid): Boolean
/**
* Prüft, ob eine Berechtigung mit dem gegebenen Typ bereits existiert.
*
* @param berechtigungTyp Der zu prüfende Berechtigungstyp.
* @return true, wenn eine Berechtigung mit diesem Typ existiert, false sonst.
*/
suspend fun existsByTyp(berechtigungTyp: BerechtigungE): Boolean
}
@@ -0,0 +1,113 @@
package at.mocode.members.domain.repository
import at.mocode.members.domain.model.DomPersonRolle
import com.benasher44.uuid.Uuid
import kotlinx.datetime.LocalDate
/**
* Repository-Interface für die Verwaltung von Person-Rolle-Zuordnungen.
*
* Definiert die Operationen für das Erstellen, Lesen, Aktualisieren und Löschen
* von Person-Rolle-Beziehungen im System.
*/
interface PersonRolleRepository {
/**
* Speichert eine Person-Rolle-Zuordnung (erstellen oder aktualisieren).
*
* @param personRolle Die zu speichernde Person-Rolle-Zuordnung.
* @return Die gespeicherte Person-Rolle-Zuordnung mit aktualisierten Zeitstempeln.
*/
suspend fun save(personRolle: DomPersonRolle): DomPersonRolle
/**
* Sucht eine Person-Rolle-Zuordnung anhand ihrer ID.
*
* @param personRolleId Die eindeutige ID der Person-Rolle-Zuordnung.
* @return Die gefundene Person-Rolle-Zuordnung oder null, falls nicht vorhanden.
*/
suspend fun findById(personRolleId: Uuid): DomPersonRolle?
/**
* Sucht alle Rollen einer bestimmten Person.
*
* @param personId Die eindeutige ID der Person.
* @param nurAktive Wenn true, werden nur aktive Zuordnungen zurückgegeben.
* @return Liste der Person-Rolle-Zuordnungen.
*/
suspend fun findByPersonId(personId: Uuid, nurAktive: Boolean = true): List<DomPersonRolle>
/**
* Sucht alle Personen mit einer bestimmten Rolle.
*
* @param rolleId Die eindeutige ID der Rolle.
* @param nurAktive Wenn true, werden nur aktive Zuordnungen zurückgegeben.
* @return Liste der Person-Rolle-Zuordnungen.
*/
suspend fun findByRolleId(rolleId: Uuid, nurAktive: Boolean = true): List<DomPersonRolle>
/**
* Sucht alle Person-Rolle-Zuordnungen für einen bestimmten Verein.
*
* @param vereinId Die eindeutige ID des Vereins.
* @param nurAktive Wenn true, werden nur aktive Zuordnungen zurückgegeben.
* @return Liste der Person-Rolle-Zuordnungen.
*/
suspend fun findByVereinId(vereinId: Uuid, nurAktive: Boolean = true): List<DomPersonRolle>
/**
* Sucht eine spezifische Person-Rolle-Zuordnung.
*
* @param personId Die eindeutige ID der Person.
* @param rolleId Die eindeutige ID der Rolle.
* @param vereinId Die eindeutige ID des Vereins (optional).
* @return Die gefundene Person-Rolle-Zuordnung oder null, falls nicht vorhanden.
*/
suspend fun findByPersonAndRolle(personId: Uuid, rolleId: Uuid, vereinId: Uuid? = null): DomPersonRolle?
/**
* Sucht alle Person-Rolle-Zuordnungen, die zu einem bestimmten Datum gültig sind.
*
* @param stichtag Das Datum, für das die Gültigkeit geprüft werden soll.
* @param nurAktive Wenn true, werden nur aktive Zuordnungen zurückgegeben.
* @return Liste der gültigen Person-Rolle-Zuordnungen.
*/
suspend fun findValidAt(stichtag: LocalDate, nurAktive: Boolean = true): List<DomPersonRolle>
/**
* Sucht alle Person-Rolle-Zuordnungen einer Person, die zu einem bestimmten Datum gültig sind.
*
* @param personId Die eindeutige ID der Person.
* @param stichtag Das Datum, für das die Gültigkeit geprüft werden soll.
* @param nurAktive Wenn true, werden nur aktive Zuordnungen zurückgegeben.
* @return Liste der gültigen Person-Rolle-Zuordnungen.
*/
suspend fun findByPersonValidAt(personId: Uuid, stichtag: LocalDate, nurAktive: Boolean = true): List<DomPersonRolle>
/**
* Deaktiviert eine Person-Rolle-Zuordnung.
*
* @param personRolleId Die ID der zu deaktivierenden Person-Rolle-Zuordnung.
* @return true, wenn die Deaktivierung erfolgreich war, false sonst.
*/
suspend fun deactivatePersonRolle(personRolleId: Uuid): Boolean
/**
* Löscht eine Person-Rolle-Zuordnung permanent.
*
* @param personRolleId Die ID der zu löschenden Person-Rolle-Zuordnung.
* @return true, wenn das Löschen erfolgreich war, false sonst.
*/
suspend fun deletePersonRolle(personRolleId: Uuid): Boolean
/**
* Prüft, ob eine Person eine bestimmte Rolle hat.
*
* @param personId Die eindeutige ID der Person.
* @param rolleId Die eindeutige ID der Rolle.
* @param vereinId Die eindeutige ID des Vereins (optional).
* @param stichtag Das Datum, für das die Gültigkeit geprüft werden soll (optional, default: heute).
* @return true, wenn die Person die Rolle hat, false sonst.
*/
suspend fun hasPersonRolle(personId: Uuid, rolleId: Uuid, vereinId: Uuid? = null, stichtag: LocalDate? = null): Boolean
}
@@ -0,0 +1,114 @@
package at.mocode.members.domain.repository
import at.mocode.members.domain.model.DomRolleBerechtigung
import com.benasher44.uuid.Uuid
/**
* Repository-Interface für die Verwaltung von Rolle-Berechtigung-Zuordnungen.
*
* Definiert die Operationen für das Erstellen, Lesen, Aktualisieren und Löschen
* von Rolle-Berechtigung-Beziehungen im System.
*/
interface RolleBerechtigungRepository {
/**
* Speichert eine Rolle-Berechtigung-Zuordnung (erstellen oder aktualisieren).
*
* @param rolleBerechtigung Die zu speichernde Rolle-Berechtigung-Zuordnung.
* @return Die gespeicherte Rolle-Berechtigung-Zuordnung mit aktualisierten Zeitstempeln.
*/
suspend fun save(rolleBerechtigung: DomRolleBerechtigung): DomRolleBerechtigung
/**
* Sucht eine Rolle-Berechtigung-Zuordnung anhand ihrer ID.
*
* @param rolleBerechtigungId Die eindeutige ID der Rolle-Berechtigung-Zuordnung.
* @return Die gefundene Rolle-Berechtigung-Zuordnung oder null, falls nicht vorhanden.
*/
suspend fun findById(rolleBerechtigungId: Uuid): DomRolleBerechtigung?
/**
* Sucht alle Berechtigungen einer bestimmten Rolle.
*
* @param rolleId Die eindeutige ID der Rolle.
* @param nurAktive Wenn true, werden nur aktive Zuordnungen zurückgegeben.
* @return Liste der Rolle-Berechtigung-Zuordnungen.
*/
suspend fun findByRolleId(rolleId: Uuid, nurAktive: Boolean = true): List<DomRolleBerechtigung>
/**
* Sucht alle Rollen mit einer bestimmten Berechtigung.
*
* @param berechtigungId Die eindeutige ID der Berechtigung.
* @param nurAktive Wenn true, werden nur aktive Zuordnungen zurückgegeben.
* @return Liste der Rolle-Berechtigung-Zuordnungen.
*/
suspend fun findByBerechtigungId(berechtigungId: Uuid, nurAktive: Boolean = true): List<DomRolleBerechtigung>
/**
* Sucht eine spezifische Rolle-Berechtigung-Zuordnung.
*
* @param rolleId Die eindeutige ID der Rolle.
* @param berechtigungId Die eindeutige ID der Berechtigung.
* @return Die gefundene Rolle-Berechtigung-Zuordnung oder null, falls nicht vorhanden.
*/
suspend fun findByRolleAndBerechtigung(rolleId: Uuid, berechtigungId: Uuid): DomRolleBerechtigung?
/**
* Gibt alle aktiven Rolle-Berechtigung-Zuordnungen zurück.
*
* @return Liste aller aktiven Rolle-Berechtigung-Zuordnungen.
*/
suspend fun findAllActive(): List<DomRolleBerechtigung>
/**
* Gibt alle Rolle-Berechtigung-Zuordnungen zurück (aktive und inaktive).
*
* @return Liste aller Rolle-Berechtigung-Zuordnungen.
*/
suspend fun findAll(): List<DomRolleBerechtigung>
/**
* Deaktiviert eine Rolle-Berechtigung-Zuordnung.
*
* @param rolleBerechtigungId Die ID der zu deaktivierenden Rolle-Berechtigung-Zuordnung.
* @return true, wenn die Deaktivierung erfolgreich war, false sonst.
*/
suspend fun deactivateRolleBerechtigung(rolleBerechtigungId: Uuid): Boolean
/**
* Löscht eine Rolle-Berechtigung-Zuordnung permanent.
*
* @param rolleBerechtigungId Die ID der zu löschenden Rolle-Berechtigung-Zuordnung.
* @return true, wenn das Löschen erfolgreich war, false sonst.
*/
suspend fun deleteRolleBerechtigung(rolleBerechtigungId: Uuid): Boolean
/**
* Prüft, ob eine Rolle eine bestimmte Berechtigung hat.
*
* @param rolleId Die eindeutige ID der Rolle.
* @param berechtigungId Die eindeutige ID der Berechtigung.
* @return true, wenn die Rolle die Berechtigung hat, false sonst.
*/
suspend fun hasRolleBerechtigung(rolleId: Uuid, berechtigungId: Uuid): Boolean
/**
* Weist einer Rolle eine Berechtigung zu.
*
* @param rolleId Die eindeutige ID der Rolle.
* @param berechtigungId Die eindeutige ID der Berechtigung.
* @param zugewiesenVon Die ID der Person, die die Zuweisung vornimmt (optional).
* @return Die erstellte Rolle-Berechtigung-Zuordnung.
*/
suspend fun assignBerechtigungToRolle(rolleId: Uuid, berechtigungId: Uuid, zugewiesenVon: Uuid? = null): DomRolleBerechtigung
/**
* Entzieht einer Rolle eine Berechtigung.
*
* @param rolleId Die eindeutige ID der Rolle.
* @param berechtigungId Die eindeutige ID der Berechtigung.
* @return true, wenn die Berechtigung erfolgreich entzogen wurde, false sonst.
*/
suspend fun revokeBerechtigungFromRolle(rolleId: Uuid, berechtigungId: Uuid): Boolean
}
@@ -0,0 +1,93 @@
package at.mocode.members.domain.repository
import at.mocode.enums.RolleE
import at.mocode.members.domain.model.DomRolle
import com.benasher44.uuid.Uuid
/**
* Repository-Interface für die Verwaltung von Rollen.
*
* Definiert die Operationen für das Erstellen, Lesen, Aktualisieren und Löschen
* von Rollen im System.
*/
interface RolleRepository {
/**
* Erstellt eine neue Rolle im System.
*
* @param rolle Die zu erstellende Rolle.
* @return Die erstellte Rolle mit aktualisierten Zeitstempeln.
*/
suspend fun save(rolle: DomRolle): DomRolle
/**
* Sucht eine Rolle anhand ihrer ID.
*
* @param rolleId Die eindeutige ID der Rolle.
* @return Die gefundene Rolle oder null, falls nicht vorhanden.
*/
suspend fun findById(rolleId: Uuid): DomRolle?
/**
* Sucht eine Rolle anhand ihres Typs.
*
* @param rolleTyp Der Typ der Rolle.
* @return Die gefundene Rolle oder null, falls nicht vorhanden.
*/
suspend fun findByTyp(rolleTyp: RolleE): DomRolle?
/**
* Sucht Rollen anhand ihres Namens (Teilstring-Suche).
*
* @param name Der Name oder Teilname der Rolle.
* @return Liste der gefundenen Rollen.
*/
suspend fun findByName(name: String): List<DomRolle>
/**
* Gibt alle aktiven Rollen zurück.
*
* @return Liste aller aktiven Rollen.
*/
suspend fun findAllActive(): List<DomRolle>
/**
* Gibt alle Rollen zurück (aktive und inaktive).
*
* @return Liste aller Rollen.
*/
suspend fun findAll(): List<DomRolle>
/**
* Aktualisiert eine bestehende Rolle.
* Note: This is handled by the save method which works for both create and update.
*
* @param rolle Die zu aktualisierende Rolle.
* @return Die aktualisierte Rolle mit aktualisierten Zeitstempeln.
*/
// suspend fun updateRolle(rolle: DomRolle): DomRolle // Handled by save method
/**
* Deaktiviert eine Rolle (soft delete).
*
* @param rolleId Die ID der zu deaktivierenden Rolle.
* @return true, wenn die Deaktivierung erfolgreich war, false sonst.
*/
suspend fun deactivateRolle(rolleId: Uuid): Boolean
/**
* Löscht eine Rolle permanent (nur für nicht-System-Rollen).
*
* @param rolleId Die ID der zu löschenden Rolle.
* @return true, wenn das Löschen erfolgreich war, false sonst.
*/
suspend fun deleteRolle(rolleId: Uuid): Boolean
/**
* Prüft, ob eine Rolle mit dem gegebenen Typ bereits existiert.
*
* @param rolleTyp Der zu prüfende Rollentyp.
* @return true, wenn eine Rolle mit diesem Typ existiert, false sonst.
*/
suspend fun existsByTyp(rolleTyp: RolleE): Boolean
}
@@ -0,0 +1,143 @@
package at.mocode.members.domain.repository
import at.mocode.members.domain.model.DomUser
import com.benasher44.uuid.Uuid
/**
* Repository interface for user management operations.
*
* Provides methods for user authentication, user management,
* and user-related database operations.
*/
interface UserRepository {
/**
* Creates a new user in the system.
*
* @param user The user to create
* @return The created user with generated ID
*/
suspend fun createUser(user: DomUser): DomUser
/**
* Finds a user by their unique user ID.
*
* @param userId The unique user ID
* @return The user if found, null otherwise
*/
suspend fun findById(userId: Uuid): DomUser?
/**
* Finds a user by their username.
*
* @param username The username to search for
* @return The user if found, null otherwise
*/
suspend fun findByUsername(username: String): DomUser?
/**
* Finds a user by their email address.
*
* @param email The email address to search for
* @return The user if found, null otherwise
*/
suspend fun findByEmail(email: String): DomUser?
/**
* Finds a user by their associated person ID.
*
* @param personId The person ID to search for
* @return The user if found, null otherwise
*/
suspend fun findByPersonId(personId: Uuid): DomUser?
/**
* Updates an existing user.
*
* @param user The user to update
* @return The updated user
*/
suspend fun updateUser(user: DomUser): DomUser
/**
* Updates the last login timestamp for a user.
*
* @param userId The user ID
*/
suspend fun updateLastLogin(userId: Uuid)
/**
* Increments the failed login attempts counter for a user.
*
* @param userId The user ID
*/
suspend fun incrementFailedLoginAttempts(userId: Uuid)
/**
* Resets the failed login attempts counter for a user.
*
* @param userId The user ID
*/
suspend fun resetFailedLoginAttempts(userId: Uuid)
/**
* Locks a user account until the specified timestamp.
*
* @param userId The user ID
* @param lockedUntil The timestamp until when the user is locked
*/
suspend fun lockUser(userId: Uuid, lockedUntil: kotlinx.datetime.Instant)
/**
* Unlocks a user account.
*
* @param userId The user ID
*/
suspend fun unlockUser(userId: Uuid)
/**
* Activates or deactivates a user account.
*
* @param userId The user ID
* @param isActive Whether the user should be active
*/
suspend fun setUserActive(userId: Uuid, isActive: Boolean)
/**
* Marks a user's email as verified.
*
* @param userId The user ID
*/
suspend fun markEmailAsVerified(userId: Uuid)
/**
* Updates a user's password hash and salt.
*
* @param userId The user ID
* @param passwordHash The new password hash
* @param salt The new salt
*/
suspend fun updatePassword(userId: Uuid, passwordHash: String, salt: String)
/**
* Deletes a user from the system.
*
* @param userId The user ID to delete
* @return True if the user was deleted, false if not found
*/
suspend fun deleteUser(userId: Uuid): Boolean
/**
* Gets all users in the system.
*
* @return List of all users
*/
suspend fun getAllUsers(): List<DomUser>
/**
* Gets all active users in the system.
*
* @return List of all active users
*/
suspend fun getActiveUsers(): List<DomUser>
}
@@ -0,0 +1,326 @@
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)
}
}
@@ -0,0 +1,213 @@
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.
*/
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 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")
}
}
@@ -0,0 +1,96 @@
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.
*/
class PasswordService {
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
}
}
@@ -0,0 +1,173 @@
package at.mocode.members.domain.service
import at.mocode.members.domain.model.DomUser
import at.mocode.members.domain.repository.UserRepository
import at.mocode.members.domain.repository.PersonRolleRepository
import at.mocode.members.domain.repository.RolleRepository
import at.mocode.members.domain.repository.RolleBerechtigungRepository
import at.mocode.members.domain.repository.BerechtigungRepository
import at.mocode.enums.RolleE
import at.mocode.enums.BerechtigungE
import com.benasher44.uuid.Uuid
import kotlinx.datetime.LocalDate
import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone
import kotlinx.datetime.todayIn
/**
* Service for managing user authorization data.
*
* This service provides methods to fetch user roles and permissions from the database
* and convert them to the format expected by the authorization system.
*/
class UserAuthorizationService(
private val userRepository: UserRepository,
private val personRolleRepository: PersonRolleRepository,
private val rolleRepository: RolleRepository,
private val rolleBerechtigungRepository: RolleBerechtigungRepository,
private val berechtigungRepository: BerechtigungRepository
) {
/**
* Data class representing user authorization information.
*/
data class UserAuthInfo(
val userId: Uuid,
val personId: Uuid,
val username: String,
val email: String,
val roles: List<RolleE>,
val permissions: List<BerechtigungE>
)
/**
* Fetches complete authorization information for a user.
*
* @param userId The user ID
* @return UserAuthInfo if user exists and is active, null otherwise
*/
suspend fun getUserAuthInfo(userId: Uuid): UserAuthInfo? {
// Get user
val user = userRepository.findById(userId) ?: return null
// Check if user is active
if (!user.istAktiv) return null
// Check if user is locked
val now = Clock.System.now()
if (user.gesperrtBis != null && user.gesperrtBis!! > now) return null
// Get user's roles
val roles = getUserRoles(user.personId)
// Get permissions for those roles
val permissions = getPermissionsForRoles(roles)
return UserAuthInfo(
userId = user.userId,
personId = user.personId,
username = user.username,
email = user.email,
roles = roles,
permissions = permissions
)
}
/**
* Fetches authorization information for a user by username or email.
*
* @param usernameOrEmail The username or email
* @return UserAuthInfo if user exists and is active, null otherwise
*/
suspend fun getUserAuthInfoByUsernameOrEmail(usernameOrEmail: String): UserAuthInfo? {
// Try to find user by username first
val user = userRepository.findByUsername(usernameOrEmail)
?: userRepository.findByEmail(usernameOrEmail)
?: return null
return getUserAuthInfo(user.userId)
}
/**
* Gets all active roles for a person.
*
* @param personId The person ID
* @return List of active role types
*/
suspend fun getUserRoles(personId: Uuid): List<RolleE> {
val today = Clock.System.todayIn(TimeZone.currentSystemDefault())
// Get active person roles
val personRoles = personRolleRepository.findByPersonId(personId)
.filter { personRolle ->
personRolle.istAktiv &&
personRolle.gueltigVon <= today &&
(personRolle.gueltigBis == null || personRolle.gueltigBis!! >= today)
}
// Get the actual roles
val roles = mutableListOf<RolleE>()
for (personRolle in personRoles) {
val rolle = rolleRepository.findById(personRolle.rolleId)
if (rolle != null && rolle.istAktiv) {
roles.add(rolle.rolleTyp)
}
}
return roles.distinct()
}
/**
* Gets all permissions for the given roles.
*
* @param roles List of role types
* @return List of permission types
*/
suspend fun getPermissionsForRoles(roles: List<RolleE>): List<BerechtigungE> {
val permissions = mutableSetOf<BerechtigungE>()
for (roleType in roles) {
// Find the role by type
val rolle = rolleRepository.findByTyp(roleType)
if (rolle != null && rolle.rolleId != null) {
// Get role permissions
val rolleBerechtigungen = rolleBerechtigungRepository.findByRolleId(rolle.rolleId)
.filter { it.istAktiv }
// Get the actual permissions
for (rolleBerechtigung in rolleBerechtigungen) {
val berechtigung = berechtigungRepository.findById(rolleBerechtigung.berechtigungId)
if (berechtigung != null && berechtigung.istAktiv) {
permissions.add(berechtigung.berechtigungTyp)
}
}
}
}
return permissions.toList()
}
/**
* Checks if a user has a specific role.
*
* @param userId The user ID
* @param role The role to check
* @return true if user has the role, false otherwise
*/
suspend fun hasRole(userId: Uuid, role: RolleE): Boolean {
val authInfo = getUserAuthInfo(userId) ?: return false
return authInfo.roles.contains(role)
}
/**
* Checks if a user has a specific permission.
*
* @param userId The user ID
* @param permission The permission to check
* @return true if user has the permission, false otherwise
*/
suspend fun hasPermission(userId: Uuid, permission: BerechtigungE): Boolean {
val authInfo = getUserAuthInfo(userId) ?: return false
return authInfo.permissions.contains(permission)
}
}
@@ -0,0 +1,121 @@
package at.mocode.members.infrastructure.repository
import at.mocode.enums.BerechtigungE
import at.mocode.members.domain.model.DomBerechtigung
import at.mocode.members.domain.repository.BerechtigungRepository
import com.benasher44.uuid.Uuid
import kotlinx.datetime.Clock
import kotlinx.datetime.toKotlinInstant
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.like
/**
* Exposed-based implementation of BerechtigungRepository.
*
* This implementation provides data persistence for Berechtigung entities
* using the Exposed SQL framework and PostgreSQL database.
*/
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.toJavaInstant()
it[updatedAt] = updatedBerechtigung.updatedAt.toJavaInstant()
}
return updatedBerechtigung
}
override suspend fun findById(berechtigungId: Uuid): DomBerechtigung? {
return BerechtigungTable.select { BerechtigungTable.id eq berechtigungId }
.map { rowToDomBerechtigung(it) }
.singleOrNull()
}
override suspend fun findByTyp(berechtigungTyp: BerechtigungE): DomBerechtigung? {
return BerechtigungTable.select { BerechtigungTable.berechtigungTyp eq berechtigungTyp }
.map { rowToDomBerechtigung(it) }
.singleOrNull()
}
override suspend fun findByName(name: String): List<DomBerechtigung> {
val searchPattern = "%$name%"
return BerechtigungTable.select { BerechtigungTable.name like searchPattern }
.map { rowToDomBerechtigung(it) }
}
override suspend fun findByRessource(ressource: String): List<DomBerechtigung> {
return BerechtigungTable.select { BerechtigungTable.ressource eq ressource }
.map { rowToDomBerechtigung(it) }
}
override suspend fun findByAktion(aktion: String): List<DomBerechtigung> {
return BerechtigungTable.select { BerechtigungTable.aktion eq aktion }
.map { rowToDomBerechtigung(it) }
}
override suspend fun findAllActive(): List<DomBerechtigung> {
return BerechtigungTable.select { 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.toJavaInstant()
}
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.select { BerechtigungTable.berechtigungTyp eq berechtigungTyp }
.count() > 0
}
/**
* Converts a database row to a DomBerechtigung domain object.
*/
private fun rowToDomBerechtigung(row: ResultRow): DomBerechtigung {
return DomBerechtigung(
berechtigungId = row[BerechtigungTable.id].value,
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].toKotlinInstant(),
updatedAt = row[BerechtigungTable.updatedAt].toKotlinInstant()
)
}
}
@@ -0,0 +1,20 @@
package at.mocode.members.infrastructure.repository
import at.mocode.enums.BerechtigungE
import org.jetbrains.exposed.dao.id.UUIDTable
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
/**
* Database table definition for permissions (Berechtigungen).
*/
object BerechtigungTable : UUIDTable("berechtigung") {
val berechtigungTyp = enumerationByName("berechtigung_typ", 50, BerechtigungE::class)
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")
val updatedAt = datetime("updated_at")
}
@@ -0,0 +1,97 @@
package at.mocode.members.infrastructure.repository
import at.mocode.members.domain.model.DomPersonRolle
import at.mocode.members.domain.repository.PersonRolleRepository
import com.benasher44.uuid.Uuid
import kotlinx.datetime.Clock
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.todayIn
/**
* 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.
*/
class PersonRolleRepositoryImpl : PersonRolleRepository {
private val personRoles = mutableMapOf<Uuid, DomPersonRolle>()
override suspend fun save(personRolle: DomPersonRolle): DomPersonRolle {
val now = Clock.System.now()
val updatedPersonRolle = personRolle.copy(updatedAt = now)
personRoles[updatedPersonRolle.personRolleId] = updatedPersonRolle
return updatedPersonRolle
}
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)
}
}
override suspend fun findByRolleId(rolleId: Uuid, nurAktive: Boolean): List<DomPersonRolle> {
return personRoles.values.filter { personRolle ->
personRolle.rolleId == rolleId && (!nurAktive || personRolle.istAktiv)
}
}
override suspend fun findByVereinId(vereinId: Uuid, nurAktive: Boolean): List<DomPersonRolle> {
return personRoles.values.filter { personRolle ->
personRolle.vereinId == vereinId && (!nurAktive || personRolle.istAktiv)
}
}
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 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 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 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 deletePersonRolle(personRolleId: Uuid): Boolean {
return personRoles.remove(personRolleId) != null
}
override suspend fun hasPersonRolle(personId: Uuid, rolleId: Uuid, vereinId: Uuid?, stichtag: LocalDate?): Boolean {
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)
}
}
}
@@ -0,0 +1,99 @@
package at.mocode.members.infrastructure.repository
import at.mocode.members.domain.model.DomRolleBerechtigung
import at.mocode.members.domain.repository.RolleBerechtigungRepository
import com.benasher44.uuid.Uuid
import com.benasher44.uuid.uuid4
import kotlinx.datetime.Clock
/**
* 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.
*/
class RolleBerechtigungRepositoryImpl : RolleBerechtigungRepository {
private val rolePermissions = mutableMapOf<Uuid, DomRolleBerechtigung>()
override suspend fun save(rolleBerechtigung: DomRolleBerechtigung): DomRolleBerechtigung {
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]
}
override suspend fun findByRolleId(rolleId: Uuid, nurAktive: Boolean): List<DomRolleBerechtigung> {
return rolePermissions.values.filter { rolleBerechtigung ->
rolleBerechtigung.rolleId == rolleId && (!nurAktive || rolleBerechtigung.istAktiv)
}
}
override suspend fun findByBerechtigungId(berechtigungId: Uuid, nurAktive: Boolean): List<DomRolleBerechtigung> {
return rolePermissions.values.filter { rolleBerechtigung ->
rolleBerechtigung.berechtigungId == berechtigungId && (!nurAktive || rolleBerechtigung.istAktiv)
}
}
override suspend fun findByRolleAndBerechtigung(rolleId: Uuid, berechtigungId: Uuid): DomRolleBerechtigung? {
return rolePermissions.values.find { rolleBerechtigung ->
rolleBerechtigung.rolleId == rolleId && rolleBerechtigung.berechtigungId == berechtigungId
}
}
override suspend fun findAllActive(): List<DomRolleBerechtigung> {
return rolePermissions.values.filter { it.istAktiv }
}
override suspend fun findAll(): List<DomRolleBerechtigung> {
return rolePermissions.values.toList()
}
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 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 assignBerechtigungToRolle(rolleId: Uuid, berechtigungId: Uuid, zugewiesenVon: Uuid?): DomRolleBerechtigung {
// 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
}
// Create new assignment
val newAssignment = DomRolleBerechtigung(
rolleId = rolleId,
berechtigungId = berechtigungId,
zugewiesenVon = zugewiesenVon
)
return save(newAssignment)
}
override suspend fun revokeBerechtigungFromRolle(rolleId: Uuid, berechtigungId: Uuid): Boolean {
val rolleBerechtigung = findByRolleAndBerechtigung(rolleId, berechtigungId) ?: return false
return deactivateRolleBerechtigung(rolleBerechtigung.rolleBerechtigungId)
}
}
@@ -0,0 +1,99 @@
package at.mocode.members.infrastructure.repository
import at.mocode.members.domain.model.DomRolle
import at.mocode.members.domain.repository.RolleRepository
import at.mocode.enums.RolleE
import com.benasher44.uuid.Uuid
import com.benasher44.uuid.uuid4
import kotlinx.datetime.Clock
/**
* 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.
*/
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
)
)
defaultRoles.forEach { role ->
roles[role.rolleId!!] = role
}
}
override suspend fun save(rolle: DomRolle): DomRolle {
val now = Clock.System.now()
val updatedRolle = rolle.copy(updatedAt = now)
roles[updatedRolle.rolleId!!] = updatedRolle
return updatedRolle
}
override suspend fun findById(rolleId: Uuid): DomRolle? {
return roles[rolleId]
}
override suspend fun findByTyp(rolleTyp: RolleE): DomRolle? {
return roles.values.find { it.rolleTyp == rolleTyp }
}
override suspend fun findByName(name: String): List<DomRolle> {
return roles.values.filter { it.name.contains(name, ignoreCase = true) }
}
override suspend fun findAllActive(): List<DomRolle> {
return roles.values.filter { it.istAktiv }
}
override suspend fun findAll(): List<DomRolle> {
return roles.values.toList()
}
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 {
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 existsByTyp(rolleTyp: RolleE): Boolean {
return roles.values.any { it.rolleTyp == rolleTyp }
}
}
@@ -0,0 +1,32 @@
package at.mocode.members.infrastructure.repository
import at.mocode.enums.RolleE
import org.jetbrains.exposed.dao.id.UUIDTable
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
/**
* Exposed table definition for Rolle entities.
*
* This table represents the database schema for storing role data
* in the member management bounded context.
*/
object RolleTable : UUIDTable("rollen") {
// Role identification
val rolleTyp = enumerationByName("rolle_typ", 20, RolleE::class)
val name = varchar("name", 100)
val beschreibung = text("beschreibung").nullable()
// Status flags
val istAktiv = bool("ist_aktiv").default(true)
val istSystemRolle = bool("ist_system_rolle").default(false)
// Audit fields
val createdAt = datetime("created_at")
val updatedAt = datetime("updated_at")
// Unique constraint on rolle_typ to ensure each role type exists only once
init {
uniqueIndex(rolleTyp)
}
}
@@ -0,0 +1,130 @@
package at.mocode.members.infrastructure.repository
import at.mocode.members.domain.model.DomUser
import at.mocode.members.domain.repository.UserRepository
import com.benasher44.uuid.Uuid
import com.benasher44.uuid.uuid4
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
/**
* 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.
*/
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
)
}
override suspend fun resetFailedLoginAttempts(userId: Uuid) {
val user = users[userId] ?: return
val now = Clock.System.now()
users[userId] = user.copy(fehlgeschlageneAnmeldungen = 0, updatedAt = now)
}
override suspend fun lockUser(userId: Uuid, lockedUntil: Instant) {
val user = users[userId] ?: return
val now = Clock.System.now()
users[userId] = user.copy(gesperrtBis = lockedUntil, updatedAt = now)
}
override suspend fun unlockUser(userId: Uuid) {
val user = users[userId] ?: return
val now = Clock.System.now()
users[userId] = user.copy(gesperrtBis = null, updatedAt = now)
}
override suspend fun setUserActive(userId: Uuid, isActive: Boolean) {
val user = users[userId] ?: return
val now = Clock.System.now()
users[userId] = user.copy(istAktiv = isActive, updatedAt = now)
}
override suspend fun markEmailAsVerified(userId: Uuid) {
val user = users[userId] ?: return
val now = Clock.System.now()
users[userId] = user.copy(istEmailVerifiziert = true, updatedAt = now)
}
override suspend fun updatePassword(userId: Uuid, passwordHash: String, salt: String) {
val user = users[userId] ?: return
val now = Clock.System.now()
users[userId] = user.copy(passwordHash = passwordHash, salt = salt, updatedAt = now)
}
override suspend fun deleteUser(userId: Uuid): Boolean {
return users.remove(userId) != null
}
override suspend fun getAllUsers(): List<DomUser> {
return users.values.toList()
}
override suspend fun getActiveUsers(): List<DomUser> {
return users.values.filter { it.istAktiv }
}
}
@@ -0,0 +1,20 @@
package at.mocode.members.infrastructure.repository
import at.mocode.enums.BerechtigungE
import org.jetbrains.exposed.dao.id.UUIDTable
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
/**
* Database table definition for permissions (Berechtigungen).
*/
object BerechtigungTable : UUIDTable("berechtigung") {
val berechtigungTyp = enumerationByName("berechtigung_typ", 50, BerechtigungE::class)
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")
val updatedAt = datetime("updated_at")
}
@@ -0,0 +1,25 @@
package at.mocode.members.infrastructure.repository
import org.jetbrains.exposed.dao.id.UUIDTable
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
/**
* Database table definition for person-role assignments (PersonRolle).
* This is a many-to-many relationship table between persons and roles.
*/
object PersonRolleTable : UUIDTable("person_rolle") {
val personId = uuid("person_id").references(PersonTable.id)
val rolleId = uuid("rolle_id").references(RolleTable.id)
val istAktiv = bool("ist_aktiv").default(true)
val gueltigVon = datetime("gueltig_von").nullable()
val gueltigBis = datetime("gueltig_bis").nullable()
val zugewiesenVon = uuid("zugewiesen_von").nullable() // Person who assigned this role
val notizen = text("notizen").nullable()
val createdAt = datetime("created_at")
val updatedAt = datetime("updated_at")
// Unique constraint to prevent duplicate assignments
init {
uniqueIndex(personId, rolleId)
}
}
@@ -0,0 +1,60 @@
package at.mocode.members.infrastructure.repository
import at.mocode.enums.DatenQuelleE
import at.mocode.enums.GeschlechtE
import org.jetbrains.exposed.dao.id.UUIDTable
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
import org.jetbrains.exposed.sql.kotlin.datetime.date
/**
* Exposed table definition for Person entities.
*
* This table represents the database schema for storing person data
* in the member management bounded context.
*/
object PersonTable : UUIDTable("persons") {
// Basic person information
val oepsSatzNr = varchar("oeps_satz_nr", 6).nullable().uniqueIndex()
val nachname = varchar("nachname", 100)
val vorname = varchar("vorname", 100)
val titel = varchar("titel", 50).nullable()
// Personal details
val geburtsdatum = date("geburtsdatum").nullable()
val geschlecht = enumerationByName("geschlecht", 10, GeschlechtE::class).nullable()
val nationalitaetLandId = uuid("nationalitaet_land_id").nullable()
val feiId = varchar("fei_id", 20).nullable()
// Contact information
val telefon = varchar("telefon", 50).nullable()
val email = varchar("email", 100).nullable()
// Address information
val strasse = varchar("strasse", 200).nullable()
val plz = varchar("plz", 10).nullable()
val ort = varchar("ort", 100).nullable()
val adresszusatzZusatzinfo = varchar("adresszusatz_zusatzinfo", 200).nullable()
// Club membership
val stammVereinId = uuid("stamm_verein_id").nullable()
val mitgliedsNummerBeiStammVerein = varchar("mitglieds_nummer_bei_stamm_verein", 50).nullable()
// Status and restrictions
val istGesperrt = bool("ist_gesperrt").default(false)
val sperrGrund = varchar("sperr_grund", 500).nullable()
// OEPS specific data
val altersklasseOepsCodeRaw = varchar("altersklasse_oeps_code_raw", 10).nullable()
val istJungerReiterOepsFlag = bool("ist_junger_reiter_oeps_flag").default(false)
val kaderStatusOepsRaw = varchar("kader_status_oeps_raw", 10).nullable()
// Metadata
val datenQuelle = enumerationByName("daten_quelle", 20, DatenQuelleE::class).default(DatenQuelleE.MANUELL)
val istAktiv = bool("ist_aktiv").default(true)
val notizenIntern = text("notizen_intern").nullable()
// Audit fields
val createdAt = datetime("created_at")
val updatedAt = datetime("updated_at")
}
@@ -0,0 +1,25 @@
package at.mocode.members.infrastructure.repository
import org.jetbrains.exposed.dao.id.UUIDTable
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
/**
* Database table definition for role-permission assignments (RolleBerechtigung).
* This is a many-to-many relationship table between roles and permissions.
*/
object RolleBerechtigungTable : UUIDTable("rolle_berechtigung") {
val rolleId = uuid("rolle_id").references(RolleTable.id)
val berechtigungId = uuid("berechtigung_id").references(BerechtigungTable.id)
val istAktiv = bool("ist_aktiv").default(true)
val gueltigVon = datetime("gueltig_von").nullable()
val gueltigBis = datetime("gueltig_bis").nullable()
val zugewiesenVon = uuid("zugewiesen_von").nullable() // Person who assigned this permission
val notizen = text("notizen").nullable()
val createdAt = datetime("created_at")
val updatedAt = datetime("updated_at")
// Unique constraint to prevent duplicate assignments
init {
uniqueIndex(rolleId, berechtigungId)
}
}
@@ -0,0 +1,32 @@
package at.mocode.members.infrastructure.repository
import at.mocode.enums.RolleE
import org.jetbrains.exposed.dao.id.UUIDTable
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
/**
* Exposed table definition for Rolle entities.
*
* This table represents the database schema for storing role data
* in the member management bounded context.
*/
object RolleTable : UUIDTable("rollen") {
// Role identification
val rolleTyp = enumerationByName("rolle_typ", 20, RolleE::class)
val name = varchar("name", 100)
val beschreibung = text("beschreibung").nullable()
// Status flags
val istAktiv = bool("ist_aktiv").default(true)
val istSystemRolle = bool("ist_system_rolle").default(false)
// Audit fields
val createdAt = datetime("created_at")
val updatedAt = datetime("updated_at")
// Unique constraint on rolle_typ to ensure each role type exists only once
init {
uniqueIndex(rolleTyp)
}
}
@@ -0,0 +1,36 @@
package at.mocode.members.infrastructure.repository
import org.jetbrains.exposed.dao.id.UUIDTable
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
/**
* Database table definition for users (authentication).
*
* This table stores user authentication data and is linked to the Person table.
* It follows the Exposed framework conventions for UUID-based tables.
*/
object UserTable : UUIDTable("users") {
// Foreign key to the Person table
val personId = uuid("person_id").references(PersonTable.id)
// Authentication fields
val username = varchar("username", 100).uniqueIndex()
val email = varchar("email", 255).uniqueIndex()
val passwordHash = varchar("password_hash", 255)
val salt = varchar("salt", 255)
// Status flags
val istAktiv = bool("ist_aktiv").default(true)
val istEmailVerifiziert = bool("ist_email_verifiziert").default(false)
// Login tracking
val letzteAnmeldung = datetime("letzte_anmeldung").nullable()
val fehlgeschlageneAnmeldungen = integer("fehlgeschlagene_anmeldungen").default(0)
val gesperrtBis = datetime("gesperrt_bis").nullable()
val passwortAendernErforderlich = bool("passwort_aendern_erforderlich").default(false)
// Audit fields
val createdAt = datetime("created_at")
val updatedAt = datetime("updated_at")
}