(fix) Umbau zu SCS

This commit is contained in:
stefan
2025-07-21 12:08:20 +02:00
parent 83d0d81193
commit 62b5e71427
34 changed files with 3403 additions and 20 deletions
@@ -0,0 +1,37 @@
import at.mocode.members.ui.components.MitgliederListe
import at.mocode.members.ui.components.LoginForm
import react.create
/**
* Main entry point for the Member Management JavaScript build.
*
* This function serves as the entry point for the Kotlin/JS application.
* It registers the React components as web components using r2wc.
*/
fun main() {
console.log("Member Management JS module loaded successfully!")
// Import r2wc function from @r2wc/react-to-web-component npm package
val r2wc = js("require('@r2wc/react-to-web-component')")
// Convert MitgliederListe React component to Web Component using r2wc
val MitgliederListeWebComponent = r2wc(MitgliederListe, js("{}"))
// Register the MitgliederListe component with a custom HTML tag
js("customElements.define('mitglieder-liste', arguments[0])")(MitgliederListeWebComponent)
console.log("Web component 'mitglieder-liste' registered successfully!")
// Convert LoginForm React component to Web Component using r2wc
// Define props configuration for the LoginForm component
val loginFormProps = js("{}")
js("loginFormProps.onLoginSuccess = { type: Function }")
val LoginFormWebComponent = r2wc(LoginForm, loginFormProps)
// Register the LoginForm component with a custom HTML tag
js("customElements.define('login-form', arguments[0])")(LoginFormWebComponent)
console.log("Web component 'login-form' registered successfully!")
console.log("You can now use <mitglieder-liste></mitglieder-liste> and <login-form></login-form> in your HTML")
}
@@ -0,0 +1,284 @@
package at.mocode.members.ui.components
import at.mocode.validation.ApiValidationUtils
import at.mocode.validation.ValidationError
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import react.*
import react.dom.html.InputType
import react.dom.html.ReactHTML.button
import react.dom.html.ReactHTML.div
import react.dom.html.ReactHTML.form
import react.dom.html.ReactHTML.h2
import react.dom.html.ReactHTML.input
import react.dom.html.ReactHTML.label
import react.dom.html.ReactHTML.p
import react.dom.html.ReactHTML.span
import emotion.react.css
/**
* Props for the LoginForm component
*/
external interface LoginFormProps : Props {
var onLoginSuccess: (String) -> Unit
}
/**
* Request body for login API
*/
@Serializable
private data class LoginRequest(
val username: String,
val password: String
)
/**
* Response from login API
*/
@Serializable
private data class LoginResponse(
val token: String,
val username: String
)
/**
* Error response from API
*/
@Serializable
private data class ErrorResponse(
val message: String,
val status: String
)
// Create Ktor client for API calls
private val apiClient = HttpClient {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
})
}
}
/**
* React component that displays a login form with client-side validation.
*
* This component demonstrates how to use the existing validation utilities
* for client-side validation before submitting the form to the server.
*/
val LoginForm = FC<LoginFormProps> { props ->
// State management with useState
var username by useState("")
var password by useState("")
var validationErrors by useState<List<ValidationError>>(emptyList())
var serverError by useState<String?>(null)
var isLoading by useState(false)
// Function to handle login
val handleLogin = {
// Clear previous errors
validationErrors = emptyList()
serverError = null
// Perform client-side validation
val errors = ApiValidationUtils.validateLoginRequest(username, password)
if (errors.isNotEmpty()) {
// If validation fails, update the validationErrors state
validationErrors = errors
} else {
// If validation passes, submit the form
isLoading = true
val scope = MainScope()
scope.launch {
try {
val response = apiClient.post("http://localhost:8080/auth/login") {
contentType(ContentType.Application.Json)
setBody(LoginRequest(username, password))
}
if (response.status.isSuccess()) {
val loginResponse: LoginResponse = response.body()
props.onLoginSuccess(loginResponse.token)
} else {
val errorResponse: ErrorResponse = response.body()
serverError = errorResponse.message
}
} catch (e: Exception) {
serverError = "Login failed: ${e.message}"
console.error("Login error:", e)
} finally {
isLoading = false
}
}
}
}
// Helper function to get validation error for a field
val getFieldError = { fieldName: String ->
validationErrors.find { it.field == fieldName }?.message
}
// Render the form
div {
css {
"maxWidth" to "400px"
"margin" to "0 auto"
"padding" to "20px"
"backgroundColor" to "#f9f9f9"
"borderRadius" to "8px"
"boxShadow" to "0 2px 4px rgba(0,0,0,0.1)"
}
h2 {
css {
"textAlign" to "center"
"color" to "#2c3e50"
"marginBottom" to "20px"
}
+"Login"
}
// Display server error if any
serverError?.let {
div {
css {
"backgroundColor" to "#fdeaea"
"color" to "#e74c3c"
"padding" to "10px"
"borderRadius" to "4px"
"marginBottom" to "15px"
"textAlign" to "center"
}
+it
}
}
form {
// No onSubmit handler, using button click instead
// Username field
div {
css {
"marginBottom" to "15px"
}
label {
css {
"display" to "block"
"marginBottom" to "5px"
"fontWeight" to "bold"
}
htmlFor = "username"
+"Username or Email"
}
input {
css {
"width" to "100%"
"padding" to "8px"
"borderRadius" to "4px"
"border" to if (getFieldError("username") != null) "1px solid #e74c3c" else "1px solid #ddd"
}
type = InputType.text
id = "username"
value = username
onChange = { event -> username = event.target.value }
disabled = isLoading
required = true
}
// Display validation error for username if any
getFieldError("username")?.let {
p {
css {
"color" to "#e74c3c"
"fontSize" to "12px"
"margin" to "5px 0 0 0"
}
+it
}
}
}
// Password field
div {
css {
"marginBottom" to "20px"
}
label {
css {
"display" to "block"
"marginBottom" to "5px"
"fontWeight" to "bold"
}
htmlFor = "password"
+"Password"
}
input {
css {
"width" to "100%"
"padding" to "8px"
"borderRadius" to "4px"
"border" to if (getFieldError("password") != null) "1px solid #e74c3c" else "1px solid #ddd"
}
type = InputType.password
id = "password"
value = password
onChange = { event -> password = event.target.value }
disabled = isLoading
required = true
}
// Display validation error for password if any
getFieldError("password")?.let {
p {
css {
"color" to "#e74c3c"
"fontSize" to "12px"
"margin" to "5px 0 0 0"
}
+it
}
}
}
// Submit button
button {
css {
"width" to "100%"
"padding" to "10px"
"backgroundColor" to "#3498db"
"color" to "white"
"border" to "none"
"borderRadius" to "4px"
"cursor" to if (isLoading) "not-allowed" else "pointer"
"opacity" to if (isLoading) "0.7" else "1"
"transition" to "background-color 0.3s"
"hover" to {
"backgroundColor" to if (!isLoading) "#2980b9" else "#3498db"
}
}
type = react.dom.html.ButtonType.button
disabled = isLoading
onClick = { handleLogin() }
if (isLoading) {
+"Logging in..."
} else {
+"Login"
}
}
}
}
}
@@ -0,0 +1,227 @@
package at.mocode.members.ui.components
import at.mocode.members.domain.model.DomUser
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import react.*
import react.dom.html.ReactHTML.div
import react.dom.html.ReactHTML.h1
import react.dom.html.ReactHTML.h3
import react.dom.html.ReactHTML.p
import react.dom.html.ReactHTML.span
import emotion.react.css
/**
* Props for the MitgliederListe component
*/
external interface MitgliederListeProps : Props
// Create Ktor client for API calls
private val apiClient = HttpClient {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
})
}
}
/**
* React component that displays a list of members (Mitglieder).
*
* This component loads member data from the API and renders it as HTML.
* Uses useState for state management and useEffectOnce for data loading.
*/
val MitgliederListe = FC<MitgliederListeProps> { _ ->
// State management with useState
var members by useState<List<DomUser>>(emptyList())
var loading by useState(true)
var error by useState<String?>(null)
// Data loading with useEffectOnce hook
useEffectOnce {
val scope = MainScope()
scope.launch {
try {
loading = true
error = null
// Load data with Ktor client
val response = apiClient.get("http://localhost:8080/api/members")
val loadedMembers: List<DomUser> = response.body()
members = loadedMembers
} catch (e: Exception) {
error = "Fehler beim Laden der Mitglieder: ${e.message}"
console.error("Error loading members:", e)
} finally {
loading = false
}
}
}
// Render HTML with React DOM elements
div {
css {
// Basic styling for the main container
"padding" to "20px"
"fontFamily" to "Arial, sans-serif"
"maxWidth" to "1200px"
"margin" to "0 auto"
}
h1 {
css {
"color" to "#2c3e50"
"borderBottom" to "2px solid #3498db"
"paddingBottom" to "10px"
"marginBottom" to "20px"
}
+"Mitglieder"
}
when {
loading -> {
div {
css {
"padding" to "20px"
"textAlign" to "center"
"color" to "#666"
"fontSize" to "18px"
}
+"Lade Mitglieder..."
}
}
error != null -> {
div {
css {
"padding" to "20px"
"textAlign" to "center"
"color" to "#e74c3c"
"backgroundColor" to "#fdeaea"
"border" to "1px solid #e74c3c"
"borderRadius" to "8px"
"margin" to "20px 0"
}
+error!!
}
}
members.isEmpty() -> {
div {
css {
"padding" to "20px"
"textAlign" to "center"
"color" to "#666"
"backgroundColor" to "#f8f9fa"
"border" to "1px solid #e0e0e0"
"borderRadius" to "8px"
"margin" to "20px 0"
}
+"Keine Mitglieder verfügbar"
}
}
else -> {
div {
css {
"display" to "grid"
"gridTemplateColumns" to "repeat(auto-fill, minmax(300px, 1fr))"
"gap" to "20px"
}
members.forEach { member ->
div {
css {
"border" to "1px solid #e0e0e0"
"borderRadius" to "8px"
"padding" to "15px"
"backgroundColor" to "#f9f9f9"
"boxShadow" to "0 2px 4px rgba(0,0,0,0.1)"
"transition" to "transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out"
"hover" to {
"transform" to "translateY(-5px)"
"boxShadow" to "0 5px 15px rgba(0,0,0,0.1)"
}
}
h3 {
css {
"color" to "#3498db"
"marginTop" to "0"
"marginBottom" to "10px"
"borderBottom" to "1px solid #e0e0e0"
"paddingBottom" to "5px"
}
+member.username
}
p {
span {
+"📧"
}
+" E-Mail: ${member.email}"
}
p {
span {
+"🆔"
}
+" Person-ID: ${member.personId}"
}
// Status indicators
val statusList = mutableListOf<String>()
if (member.istAktiv) statusList.add("Aktiv") else statusList.add("Inaktiv")
if (member.istEmailVerifiziert) statusList.add("E-Mail verifiziert")
if (member.isLocked()) statusList.add("Gesperrt")
if (member.canLogin()) statusList.add("Kann sich anmelden")
p {
span {
+""
}
+" Status: ${statusList.joinToString(", ")}"
}
// Failed login attempts
if (member.fehlgeschlageneAnmeldungen > 0) {
p {
span {
+"⚠️"
}
+" Fehlgeschlagene Anmeldungen: ${member.fehlgeschlageneAnmeldungen}"
}
}
// Last login
member.letzteAnmeldung?.let { lastLogin ->
p {
span {
+"🔐"
}
+" Letzte Anmeldung: $lastLogin"
}
}
// Creation date
p {
span {
+"📅"
}
+" Erstellt am: ${member.createdAt}"
}
// Last update
p {
span {
+"🔄"
}
+" Zuletzt geändert: ${member.updatedAt}"
}
}
}
}
}
}
}
}
@@ -0,0 +1,75 @@
package at.mocode.members.test
import at.mocode.members.domain.service.UserAuthorizationService
import at.mocode.members.domain.service.JwtService
import at.mocode.members.domain.service.AuthenticationService
import at.mocode.members.infrastructure.repository.*
import com.benasher44.uuid.uuid4
import kotlin.test.Test
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import kotlinx.coroutines.runBlocking
/**
* Test class for the authentication system.
*
* This test verifies that the authentication services can be created
* and basic authentication operations work correctly.
*/
class AuthenticationTest {
@Test
fun testAuthenticationSystem() = runBlocking {
println("[DEBUG_LOG] Testing Authentication System")
try {
// Try to create the services
val userRepository = UserRepositoryImpl()
val personRolleRepository = PersonRolleRepositoryImpl()
val rolleRepository = RolleRepositoryImpl()
val rolleBerechtigungRepository = RolleBerechtigungRepositoryImpl()
val berechtigungRepository = BerechtigungRepositoryImpl()
val userAuthorizationService = UserAuthorizationService(
userRepository,
personRolleRepository,
rolleRepository,
rolleBerechtigungRepository,
berechtigungRepository
)
val jwtService = JwtService(userAuthorizationService)
println("[DEBUG_LOG] Services created successfully")
// Try to get user auth info for a test user
val testUsers = userRepository.getAllUsers()
println("[DEBUG_LOG] Found ${testUsers.size} test users")
if (testUsers.isNotEmpty()) {
val testUser = testUsers.first()
println("[DEBUG_LOG] Testing with user: ${testUser.username}")
val authInfo = userAuthorizationService.getUserAuthInfo(testUser.userId)
println("[DEBUG_LOG] Auth info for test user: $authInfo")
assertNotNull(authInfo, "Auth info should not be null")
// Test JWT token generation
val token = jwtService.createToken(testUser)
println("[DEBUG_LOG] Generated JWT token: ${token}")
assertNotNull(token, "JWT token should not be null")
assertTrue(token.isNotEmpty(), "JWT token should not be empty")
// Test token validation
val payload = jwtService.validateToken(token)
println("[DEBUG_LOG] Token validation result: $payload")
assertNotNull(payload, "Token validation payload should not be null")
}
} catch (e: Exception) {
println("[DEBUG_LOG] Error testing authentication system: ${e.message}")
e.printStackTrace()
throw e
}
}
}