(fix) Umbau zu SCS
This commit is contained in:
@@ -38,11 +38,22 @@ kotlin {
|
||||
jsMain.dependencies {
|
||||
// Kotlin React dependencies with explicit stable versions
|
||||
implementation("org.jetbrains.kotlin-wrappers:kotlin-react:18.2.0-pre.467")
|
||||
implementation("org.jetbrains.kotlin-wrappers:kotlin-react-dom:18.2.0-pre.467")
|
||||
implementation("org.jetbrains.kotlin-wrappers:kotlin-emotion:11.10.5-pre.467")
|
||||
|
||||
// Ktor client dependencies for API calls
|
||||
implementation(libs.ktor.client.core)
|
||||
implementation(libs.ktor.client.js)
|
||||
implementation(libs.ktor.client.contentNegotiation)
|
||||
implementation(libs.ktor.client.serializationKotlinxJson)
|
||||
|
||||
// Coroutines for async operations
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
|
||||
// NPM dependencies
|
||||
implementation(npm("react", "18.2.0"))
|
||||
implementation(npm("react-dom", "18.2.0"))
|
||||
implementation(npm("@r2wc/react-to-web-component", "2.0.4"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+227
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user