refactoring:
Ein Backend-UseCase wurde implementiert, der nach Login prüft, ob ein Member-Profil existiert, und bei Bedarf ein neues Profil mit OEPS-Daten anlegt. Ein API-Endpunkt /api/members/sync wurde hinzugefügt, der vom Frontend nach Login aufgerufen wird. Der Gesamt-Build und die Tests laufen erfolgreich ohne Fehler.
This commit is contained in:
+16
@@ -2,10 +2,14 @@ package at.mocode.clients.authfeature
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import at.mocode.clients.shared.AppConfig
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.request.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import at.mocode.clients.authfeature.AuthenticatedHttpClient.addAuthHeader
|
||||
|
||||
/**
|
||||
* UI state for the login screen
|
||||
@@ -89,6 +93,18 @@ class LoginViewModel(
|
||||
isAuthenticated = true,
|
||||
errorMessage = null
|
||||
)
|
||||
|
||||
// Fire-and-forget: Trigger Backend Sync so the user exists in Members
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val client = AuthenticatedHttpClient.create()
|
||||
client.post("${AppConfig.GATEWAY_URL}/api/members/sync") {
|
||||
addAuthHeader()
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
// Non-fatal: Wir zeigen Sync-Fehler im Login nicht an
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isLoading = false,
|
||||
|
||||
@@ -24,6 +24,9 @@ dependencies {
|
||||
|
||||
implementation("org.springframework:spring-web")
|
||||
implementation("org.springdoc:springdoc-openapi-starter-common")
|
||||
// Security/JWT for extracting claims from principal
|
||||
implementation(libs.spring.boot.starter.security)
|
||||
implementation(libs.spring.boot.starter.oauth2.resource.server)
|
||||
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
}
|
||||
|
||||
+30
@@ -17,6 +17,8 @@ import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
import org.springframework.security.oauth2.jwt.Jwt
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse as SwaggerApiResponse
|
||||
|
||||
/**
|
||||
@@ -55,6 +57,7 @@ class MemberController(
|
||||
private val findExpiringMembershipsUseCase = FindExpiringMembershipsUseCase(memberRepository)
|
||||
private val findMembersByDateRangeUseCase = FindMembersByDateRangeUseCase(memberRepository)
|
||||
private val validateMemberDataUseCase = ValidateMemberDataUseCase(memberRepository)
|
||||
private val ensureMemberProfileExistsUseCase = EnsureMemberProfileExistsUseCase(memberRepository, eventPublisher)
|
||||
|
||||
/**
|
||||
* Hilfsmethode zur Behandlung gemeinsamer Antwortmuster für Use-Case-Ausführung
|
||||
@@ -88,6 +91,33 @@ class MemberController(
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Synchronisation nach Login: Stellt sicher, dass Member-Profil existiert
|
||||
// ---------------------------------------------------------------------
|
||||
@Operation(
|
||||
summary = "Synchronisiert das Member-Profil für den eingeloggten Benutzer",
|
||||
description = "Erstellt bei Bedarf ein Mitglied basierend auf den JWT-Claims (mock OEPS fetch)"
|
||||
)
|
||||
@PostMapping("/sync")
|
||||
fun syncMemberProfile(
|
||||
@AuthenticationPrincipal jwt: Jwt
|
||||
): ResponseEntity<ApiResponse<*>> = handleUseCaseExecution(
|
||||
operation = {
|
||||
val sub = jwt.subject
|
||||
val email = jwt.getClaimAsString("email")
|
||||
val username = jwt.getClaimAsString("preferred_username")
|
||||
val response = ensureMemberProfileExistsUseCase.execute(
|
||||
EnsureMemberProfileExistsUseCase.Request(
|
||||
userSub = sub,
|
||||
email = email,
|
||||
username = username
|
||||
)
|
||||
)
|
||||
ApiResponse.success(response)
|
||||
},
|
||||
successStatus = HttpStatus.OK
|
||||
) { it }
|
||||
|
||||
/**
|
||||
* Hilfsmethode zur Behandlung von Repository-Operationen mit gemeinsamer Fehlerbehandlung
|
||||
*/
|
||||
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.members.application.usecase
|
||||
|
||||
import at.mocode.infrastructure.messaging.client.EventPublisher
|
||||
import at.mocode.members.domain.events.MemberCreatedEvent
|
||||
import at.mocode.members.domain.model.Member
|
||||
import at.mocode.members.domain.repository.MemberRepository
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.todayIn
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
/**
|
||||
* UseCase: Stellt sicher, dass für den authentifizierten Benutzer ein Member-Profil existiert.
|
||||
*
|
||||
* Ablauf:
|
||||
* 1) Prüft, ob ein Member zu E-Mail existiert (Fallback: username-basiert via Dummy-E-Mail)
|
||||
* 2) Wenn nicht vorhanden: erstellt minimalen Member-Datensatz (Mock für OEPS-Datenbezug)
|
||||
* 3) Publiziert MemberCreatedEvent
|
||||
*/
|
||||
class EnsureMemberProfileExistsUseCase(
|
||||
private val memberRepository: MemberRepository,
|
||||
private val eventPublisher: EventPublisher
|
||||
) {
|
||||
|
||||
data class Request(
|
||||
val userSub: String,
|
||||
val email: String?,
|
||||
val username: String?
|
||||
)
|
||||
|
||||
data class Response(
|
||||
val ensured: Boolean, // true, wenn jetzt auf jeden Fall vorhanden (neu oder bereits vorhanden)
|
||||
val created: Boolean, // true, wenn neu angelegt
|
||||
val memberId: Uuid?,
|
||||
val membershipNumber: String?
|
||||
)
|
||||
|
||||
suspend fun execute(request: Request): Response {
|
||||
// 1) Versuche per E-Mail zu finden (stabilster Identifier). Fallback: generierte Pseudo-E-Mail aus username/sub.
|
||||
val lookupEmail = request.email ?: request.username?.let { "$it@local" }
|
||||
?: "user-${request.userSub.takeLast(8)}@local"
|
||||
|
||||
val existing = memberRepository.findByEmail(lookupEmail)
|
||||
if (existing != null) {
|
||||
return Response(ensured = true, created = false, memberId = existing.memberId, membershipNumber = existing.membershipNumber)
|
||||
}
|
||||
|
||||
// 2) Keine Daten vorhanden: OEPS-Daten mocken und Member erzeugen
|
||||
val (firstName, lastName) = mockFetchOepsNames(request)
|
||||
val today: LocalDate = Clock.System.todayIn(TimeZone.currentSystemDefault())
|
||||
val generatedMembershipNumber = "AUTO-${request.userSub.takeLast(8)}"
|
||||
|
||||
val newMember = Member(
|
||||
firstName = firstName,
|
||||
lastName = lastName,
|
||||
email = lookupEmail,
|
||||
membershipNumber = generatedMembershipNumber,
|
||||
membershipStartDate = today,
|
||||
// optionale Felder leer lassen
|
||||
phone = null,
|
||||
dateOfBirth = null,
|
||||
membershipEndDate = null,
|
||||
isActive = true,
|
||||
address = null,
|
||||
emergencyContact = null
|
||||
)
|
||||
|
||||
val saved = memberRepository.save(newMember)
|
||||
|
||||
// 3) Domain-Event publizieren (best effort, Fehler sollen nicht verhindern)
|
||||
runCatching {
|
||||
val event = MemberCreatedEvent(
|
||||
eventId = "evt-${saved.memberId}",
|
||||
memberId = saved.memberId,
|
||||
timestamp = Clock.System.now(),
|
||||
firstName = saved.firstName,
|
||||
lastName = saved.lastName,
|
||||
email = saved.email,
|
||||
membershipNumber = saved.membershipNumber,
|
||||
membershipStartDate = saved.membershipStartDate,
|
||||
isActive = saved.isActive
|
||||
)
|
||||
eventPublisher.publishEvent(
|
||||
topic = "members.events",
|
||||
key = saved.memberId.toString(),
|
||||
event = event
|
||||
)
|
||||
}
|
||||
|
||||
return Response(ensured = true, created = true, memberId = saved.memberId, membershipNumber = saved.membershipNumber)
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock für den späteren OEPS-Datenbezug. Leitet aus Username/E-Mail simple Namen ab.
|
||||
*/
|
||||
private fun mockFetchOepsNames(request: Request): Pair<String, String> {
|
||||
val source = request.username ?: request.email ?: request.userSub
|
||||
val base = source.substringBefore('@').replace(".", " ").trim().ifBlank { "Reiter" }
|
||||
val parts = base.split(" ")
|
||||
val first = parts.firstOrNull()?.replaceFirstChar { it.titlecase() } ?: "Reiter"
|
||||
val last = parts.drop(1).joinToString(" ").ifBlank { "Unbekannt" }.replaceFirstChar { it.titlecase() }
|
||||
return first to last
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user