Register events modules in Gradle build and refactor VeranstaltungController: remove unused use cases, streamline request handling, and improve error responses.

This commit is contained in:
2026-04-08 22:56:15 +02:00
parent df8bce4277
commit 8bc6f8e1df
16 changed files with 501 additions and 319 deletions
@@ -1,39 +1,32 @@
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.spring)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ktor)
application
// KORREKTUR 1: Dieses Plugin hinzufügen, um die Spring-BOM zu aktivieren.
alias(libs.plugins.spring.dependencyManagement)
alias(libs.plugins.kotlinJvm)
alias(libs.plugins.kotlinSpring)
alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.ktor)
application
alias(libs.plugins.spring.dependencyManagement)
}
application {
mainClass.set("at.mocode.events.api.ApplicationKt")
mainClass.set("at.mocode.events.api.ApplicationKt")
}
dependencies {
// KORREKTUR 2: Die Spring-Boot-BOM hier explizit als Plattform deklarieren.
api(platform(libs.spring.boot.dependencies))
// Bestehende Abhängigkeiten
implementation(projects.platform.platformDependencies)
implementation(projects.events.eventsDomain)
implementation(projects.events.eventsApplication)
implementation(projects.core.coreDomain)
implementation(projects.core.coreUtils)
// Spring dependencies (jetzt mit korrekter Version aus der BOM)
implementation(libs.spring.web)
implementation(libs.springdoc.openapi.starter.common)
// Ktor Server
implementation(libs.ktor.server.core)
implementation(libs.ktor.server.netty)
implementation(libs.ktor.server.contentNegotiation)
implementation(libs.ktor.server.serialization.kotlinx.json)
implementation(libs.ktor.server.statusPages)
implementation(libs.ktor.server.auth)
implementation(libs.ktor.server.authJwt)
api(platform(libs.spring.boot.dependencies))
implementation(projects.platform.platformDependencies)
implementation(projects.backend.services.events.eventsDomain)
implementation(projects.core.coreDomain)
implementation(projects.core.coreUtils)
implementation(libs.spring.web)
implementation(libs.springdoc.openapi.starter.common)
implementation(libs.ktor.server.core)
implementation(libs.ktor.server.netty)
implementation(libs.ktor.server.contentNegotiation)
implementation(libs.ktor.server.serialization.kotlinx.json)
implementation(libs.ktor.server.statusPages)
implementation(libs.ktor.server.auth)
implementation(libs.ktor.server.authJwt)
testImplementation(projects.platform.platformTesting)
// Ktor 3.x Test-Host statt veraltetes tests-Artefakt
testImplementation(libs.ktor.server.testHost)
testImplementation(projects.platform.platformTesting)
testImplementation(libs.ktor.server.testHost)
}
@@ -1,15 +1,11 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.events.api.rest
import at.mocode.events.domain.model.Veranstaltung
import at.mocode.core.domain.model.ApiResponse
import at.mocode.core.domain.model.SparteE
import at.mocode.events.application.usecase.CreateVeranstaltungUseCase
import at.mocode.events.application.usecase.DeleteVeranstaltungUseCase
import at.mocode.events.application.usecase.GetVeranstaltungUseCase
import at.mocode.events.application.usecase.UpdateVeranstaltungUseCase
import at.mocode.events.domain.repository.VeranstaltungRepository
import at.mocode.core.domain.serialization.UuidSerializer
import at.mocode.core.utils.validation.ApiValidationUtils
import kotlin.uuid.Uuid
import io.ktor.http.*
import io.ktor.server.request.*
@@ -28,11 +24,6 @@ class VeranstaltungController(
private val veranstaltungRepository: VeranstaltungRepository
) {
private val createVeranstaltungUseCase = CreateVeranstaltungUseCase(veranstaltungRepository)
private val getVeranstaltungUseCase = GetVeranstaltungUseCase(veranstaltungRepository)
private val updateVeranstaltungUseCase = UpdateVeranstaltungUseCase(veranstaltungRepository)
private val deleteVeranstaltungUseCase = DeleteVeranstaltungUseCase(veranstaltungRepository)
/**
* Configures the event-related routes.
*/
@@ -42,31 +33,17 @@ class VeranstaltungController(
// GET /api/events - Get all events with optional filtering
get {
try {
// Validate query parameters
val validationErrors = ApiValidationUtils.validateQueryParameters(
limit = call.request.queryParameters["limit"],
offset = call.request.queryParameters["offset"],
startDate = call.request.queryParameters["startDate"],
endDate = call.request.queryParameters["endDate"],
search = call.request.queryParameters["search"]
)
if (!ApiValidationUtils.isValid(validationErrors)) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Any>(ApiValidationUtils.createErrorMessage(validationErrors))
)
return@get
}
val activeOnly = call.request.queryParameters["activeOnly"]?.toBoolean() ?: true
val limit = call.request.queryParameters["limit"]?.toInt() ?: 100
val offset = call.request.queryParameters["offset"]?.toInt() ?: 0
val organizerId = call.request.queryParameters["organizerId"]?.let {
ApiValidationUtils.validateUuidString(it) ?: return@get call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Any>("Invalid organizerId format")
)
try { Uuid.parse(it) } catch (e: Exception) {
return@get call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Any>("INVALID_ORGANIZER_ID", "Invalid organizerId format")
)
}
}
val searchTerm = call.request.queryParameters["search"]
val publicOnly = call.request.queryParameters["publicOnly"]?.toBoolean() ?: false
@@ -84,7 +61,7 @@ class VeranstaltungController(
call.respond(HttpStatusCode.OK, ApiResponse.success(events))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to retrieve events: ${e.message}"))
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("INTERNAL_ERROR", "Failed to retrieve events: ${e.message}"))
}
}
@@ -92,18 +69,17 @@ class VeranstaltungController(
get("/{id}") {
try {
val eventId = Uuid.parse(call.parameters["id"]!!)
val request = GetVeranstaltungUseCase.GetVeranstaltungRequest(eventId)
val response = getVeranstaltungUseCase.execute(request)
val event = veranstaltungRepository.findById(eventId)
if (response.success && response.data != null) {
call.respond(HttpStatusCode.OK, ApiResponse.success((response.data as GetVeranstaltungUseCase.GetVeranstaltungResponse).veranstaltung))
if (event != null) {
call.respond(HttpStatusCode.OK, ApiResponse.success(event))
} else {
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Any>("Event not found"))
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Any>("NOT_FOUND", "Event not found"))
}
} catch (_: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Invalid event ID format"))
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("INVALID_ID", "Invalid event ID format"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to retrieve event: ${e.message}"))
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("INTERNAL_ERROR", "Failed to retrieve event: ${e.message}"))
}
}
@@ -120,7 +96,7 @@ class VeranstaltungController(
call.respond(HttpStatusCode.OK, ApiResponse.success(stats))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to retrieve event statistics: ${e.message}"))
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("INTERNAL_ERROR", "Failed to retrieve event statistics: ${e.message}"))
}
}
@@ -129,26 +105,12 @@ class VeranstaltungController(
try {
val createRequest = call.receive<CreateEventRequest>()
// Validate input using shared validation utilities
val validationErrors = ApiValidationUtils.validateEventRequest(
name = createRequest.name,
ort = createRequest.ort,
startDatum = createRequest.startDatum,
endDatum = createRequest.endDatum,
maxTeilnehmer = createRequest.maxTeilnehmer
)
if (!ApiValidationUtils.isValid(validationErrors)) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Any>(ApiValidationUtils.createErrorMessage(validationErrors))
)
return@post
}
val useCaseRequest = CreateVeranstaltungUseCase.CreateVeranstaltungRequest(
val veranstaltung = Veranstaltung(
name = createRequest.name,
untertitel = createRequest.untertitel,
beschreibung = createRequest.beschreibung,
logoUrl = createRequest.logoUrl,
sponsoren = createRequest.sponsoren,
startDatum = createRequest.startDatum,
endDatum = createRequest.endDatum,
ort = createRequest.ort,
@@ -160,20 +122,16 @@ class VeranstaltungController(
anmeldeschluss = createRequest.anmeldeschluss
)
val response = createVeranstaltungUseCase.execute(useCaseRequest)
if (response.success && response.data != null) {
call.respond(HttpStatusCode.Created, ApiResponse.success((response.data as CreateVeranstaltungUseCase.CreateVeranstaltungResponse).veranstaltung))
} else {
val statusCode = when (response.error?.code) {
"VALIDATION_ERROR" -> HttpStatusCode.BadRequest
"DOMAIN_VALIDATION_ERROR" -> HttpStatusCode.BadRequest
else -> HttpStatusCode.InternalServerError
}
call.respond(statusCode, ApiResponse.error<Any>(response.error?.message ?: "Failed to create event"))
val errors = veranstaltung.validate()
if (errors.isNotEmpty()) {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("VALIDATION_ERROR", "Validation failed: ${errors.joinToString(", ")}"))
return@post
}
val savedEvent = veranstaltungRepository.save(veranstaltung)
call.respond(HttpStatusCode.Created, ApiResponse.success(savedEvent))
} catch (e: Exception) {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Invalid request data: ${e.message}"))
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("INVALID_REQUEST", "Invalid request data: ${e.message}"))
}
}
@@ -183,27 +141,18 @@ class VeranstaltungController(
val eventId = Uuid.parse(call.parameters["id"]!!)
val updateRequest = call.receive<UpdateEventRequest>()
// Validate input using shared validation utilities
val validationErrors = ApiValidationUtils.validateEventRequest(
name = updateRequest.name,
ort = updateRequest.ort,
startDatum = updateRequest.startDatum,
endDatum = updateRequest.endDatum,
maxTeilnehmer = updateRequest.maxTeilnehmer
)
if (!ApiValidationUtils.isValid(validationErrors)) {
call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Any>(ApiValidationUtils.createErrorMessage(validationErrors))
)
val existingEvent = veranstaltungRepository.findById(eventId)
if (existingEvent == null) {
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Any>("NOT_FOUND", "Event not found"))
return@put
}
val useCaseRequest = UpdateVeranstaltungUseCase.UpdateVeranstaltungRequest(
veranstaltungId = eventId,
val updatedVeranstaltung = existingEvent.copy(
name = updateRequest.name,
untertitel = updateRequest.untertitel,
beschreibung = updateRequest.beschreibung,
logoUrl = updateRequest.logoUrl,
sponsoren = updateRequest.sponsoren,
startDatum = updateRequest.startDatum,
endDatum = updateRequest.endDatum,
ort = updateRequest.ort,
@@ -213,72 +162,46 @@ class VeranstaltungController(
istOeffentlich = updateRequest.istOeffentlich,
maxTeilnehmer = updateRequest.maxTeilnehmer,
anmeldeschluss = updateRequest.anmeldeschluss
)
).withUpdatedTimestamp()
val response = updateVeranstaltungUseCase.execute(useCaseRequest)
if (response.success && response.data != null) {
call.respond(HttpStatusCode.OK, ApiResponse.success((response.data as UpdateVeranstaltungUseCase.UpdateVeranstaltungResponse).veranstaltung))
} else {
val statusCode = when (response.error?.code) {
"NOT_FOUND" -> HttpStatusCode.NotFound
"VALIDATION_ERROR" -> HttpStatusCode.BadRequest
"DOMAIN_VALIDATION_ERROR" -> HttpStatusCode.BadRequest
else -> HttpStatusCode.InternalServerError
}
call.respond(statusCode, ApiResponse.error<Any>(response.error?.message ?: "Failed to update event"))
val errors = updatedVeranstaltung.validate()
if (errors.isNotEmpty()) {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("VALIDATION_ERROR", "Validation failed: ${errors.joinToString(", ")}"))
return@put
}
val savedEvent = veranstaltungRepository.save(updatedVeranstaltung)
call.respond(HttpStatusCode.OK, ApiResponse.success(savedEvent))
} catch (_: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Invalid event ID format"))
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("INVALID_ID", "Invalid event ID format"))
} catch (e: Exception) {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Invalid request data: ${e.message}"))
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("INVALID_REQUEST", "Invalid request data: ${e.message}"))
}
}
// DELETE /api/events/{id} - Delete event
delete("/{id}") {
try {
val eventId = ApiValidationUtils.validateUuidString(call.parameters["id"])
?: return@delete call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Any>("Invalid event ID format")
)
// Validate force parameter if provided
val forceParam = call.request.queryParameters["force"]
val forceDelete = if (forceParam != null) {
try {
forceParam.toBoolean()
} catch (_: Exception) {
return@delete call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Any>("Invalid force parameter. Must be true or false")
)
}
} else {
false
}
val useCaseRequest = DeleteVeranstaltungUseCase.DeleteVeranstaltungRequest(
veranstaltungId = eventId,
forceDelete = forceDelete
val eventIdString = call.parameters["id"] ?: return@delete call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Any>("MISSING_ID", "Event ID is required")
)
val response = deleteVeranstaltungUseCase.execute(useCaseRequest)
if (response.success) {
call.respond(HttpStatusCode.OK, ApiResponse.success(response.data))
} else {
val statusCode = when (response.error?.code) {
"NOT_FOUND" -> HttpStatusCode.NotFound
"CANNOT_DELETE_ACTIVE_EVENT" -> HttpStatusCode.Conflict
else -> HttpStatusCode.InternalServerError
}
call.respond(statusCode, ApiResponse.error<Any>(response.error?.message ?: "Failed to delete event"))
val eventId = try { Uuid.parse(eventIdString) } catch (e: Exception) {
return@delete call.respond(
HttpStatusCode.BadRequest,
ApiResponse.error<Any>("INVALID_ID", "Invalid event ID format")
)
}
val success = veranstaltungRepository.delete(eventId)
if (success) {
call.respond(HttpStatusCode.OK, ApiResponse.success("Event deleted successfully"))
} else {
call.respond(HttpStatusCode.NotFound, ApiResponse.error<Any>("NOT_FOUND", "Event not found"))
}
} catch (_: IllegalArgumentException) {
call.respond(HttpStatusCode.BadRequest, ApiResponse.error<Any>("Invalid event ID format"))
} catch (e: Exception) {
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("Failed to delete event: ${e.message}"))
call.respond(HttpStatusCode.InternalServerError, ApiResponse.error<Any>("INTERNAL_ERROR", "Failed to delete event: ${e.message}"))
}
}
}
@@ -290,7 +213,10 @@ class VeranstaltungController(
@Serializable
data class CreateEventRequest(
val name: String,
val untertitel: String? = null,
val beschreibung: String? = null,
val logoUrl: String? = null,
val sponsoren: String? = null,
val startDatum: LocalDate,
val endDatum: LocalDate,
val ort: String,
@@ -309,7 +235,10 @@ class VeranstaltungController(
@Serializable
data class UpdateEventRequest(
val name: String,
val untertitel: String? = null,
val beschreibung: String? = null,
val logoUrl: String? = null,
val sponsoren: String? = null,
val startDatum: LocalDate,
val endDatum: LocalDate,
val ort: String,