diff --git a/CLEANUP_IMPLEMENTATION_PLAN.md b/CLEANUP_IMPLEMENTATION_PLAN.md new file mode 100644 index 00000000..f60752bc --- /dev/null +++ b/CLEANUP_IMPLEMENTATION_PLAN.md @@ -0,0 +1,142 @@ +# Cleanup Implementation Plan + +This document outlines the plan for cleaning up the codebase, improving its structure, and enhancing maintainability as requested. + +## 1. Directory Structure Standardization + +### 1.1 Fix api-gateway Module Inconsistency + +The api-gateway module currently has an inconsistent directory structure with both `src/main` and `src/jvmMain` directories, which causes confusion and potential maintenance issues. + +**Actions:** +1. Compare files in both directories to identify duplicates and differences +2. Consolidate all code into the `src/jvmMain` directory to be consistent with other modules +3. Update any references to the old directory structure +4. Remove the `src/main` directory after ensuring all functionality is preserved + +### 1.2 Standardize Module Structure + +Ensure all modules follow the same directory structure pattern: +- `src/jvmMain/kotlin` for backend code +- `src/jsMain/kotlin` for frontend code +- `src/commonMain/kotlin` for shared code +- `src/jvmTest/kotlin` for backend tests +- `src/jsTest/kotlin` for frontend tests +- `src/commonTest/kotlin` for shared tests + +## 2. Test Files Organization + +### 2.1 Move Standalone Test Scripts + +Move the following standalone test scripts from the root directory to appropriate test directories: +- `test_authentication.kt` → `member-management/src/jvmTest/kotlin/at/mocode/members/test/` +- `test_authentication_authorization.kt` → `api-gateway/src/jvmTest/kotlin/at/mocode/gateway/test/` +- `test_validation.kt` → `shared-kernel/src/jvmTest/kotlin/at/mocode/validation/test/` +- `database-integration-test.kt` → `shared-kernel/src/jvmTest/kotlin/at/mocode/shared/database/test/` + +### 2.2 Convert to Proper Unit Tests + +Convert the standalone test scripts to proper unit tests using the Kotlin test framework: +1. Add appropriate test annotations +2. Organize tests into test classes +3. Use proper assertions +4. Set up test dependencies + +## 3. Documentation Cleanup + +### 3.1 Update Outdated Documentation + +Update the following documentation files to reflect the current project structure: +- `README_CODE_ORGANIZATION.md` - Update to reflect actual code organization +- `README_API_Implementation.md` - Verify and update if needed +- Other README files - Review and update as needed + +### 3.2 Consolidate Documentation + +Consolidate fragmented documentation into a more organized structure: +1. Create a `docs` directory with subdirectories for different topics +2. Move documentation files from the root directory to appropriate subdirectories +3. Create an index document that links to all documentation +4. Update the main README.md to point to the new documentation structure + +Suggested structure: +``` +docs/ +├── architecture/ +│ ├── code-organization.md +│ └── system-overview.md +├── api/ +│ ├── api-implementation.md +│ └── validation.md +├── database/ +│ ├── setup.md +│ └── migrations.md +├── security/ +│ ├── authentication.md +│ └── authorization.md +└── development/ + ├── getting-started.md + └── testing.md +``` + +### 3.3 Remove Redundant Documentation + +Identify and remove any redundant or obsolete documentation files after consolidation. + +## 4. Code Cleanup + +### 4.1 Remove Duplicate Code + +Identify and remove duplicate code, particularly in the api-gateway module where files exist in both src/main and src/jvmMain. + +### 4.2 Standardize Naming Conventions + +Ensure consistent naming conventions across the codebase: +- Repository interfaces: `EntityRepository` +- Repository implementations: `EntityRepositoryImpl` +- Service interfaces: `EntityService` +- Service implementations: `EntityServiceImpl` +- Controllers/Routes: `EntityRoutes` +- Data classes: `EntityDto` for DTOs, `Entity` for domain models + +### 4.3 Improve Separation of Concerns + +Ensure proper separation of concerns: +- Domain logic in domain layer +- Infrastructure concerns in infrastructure layer +- API endpoints in presentation layer +- Shared utilities in appropriate utility classes + +## 5. Implementation Approach + +### 5.1 Phase 1: Directory Structure and Documentation + +1. Fix api-gateway module directory structure +2. Move standalone test scripts +3. Update and consolidate documentation + +### 5.2 Phase 2: Code Cleanup + +1. Remove duplicate code +2. Standardize naming conventions +3. Improve separation of concerns + +### 5.3 Phase 3: Verification + +1. Ensure the project builds correctly +2. Run tests to verify functionality +3. Manual testing of key features + +## 6. Success Criteria + +The cleanup will be considered successful when: +1. All modules follow a consistent directory structure +2. All test files are properly organized in test directories +3. Documentation is accurate, up-to-date, and well-organized +4. No duplicate code or files exist +5. Naming conventions are consistent +6. The project builds and all tests pass + +## Last Updated + +2025-07-21 diff --git a/CLIENT_VALIDATION_IMPLEMENTATION.md b/CLIENT_VALIDATION_IMPLEMENTATION.md new file mode 100644 index 00000000..37224c1f --- /dev/null +++ b/CLIENT_VALIDATION_IMPLEMENTATION.md @@ -0,0 +1,164 @@ +# Client-Side Validation Implementation + +## Übersicht + +Dieses Dokument beschreibt die Implementierung der Client-seitigen Validierung für die Meldestelle-Anwendung. Die Validierung wurde gemäß der Anforderung "Implementiere Client-seitige Validierung" umgesetzt. + +## Implementierungsansatz + +Die Client-seitige Validierung nutzt die bereits vorhandenen Validierungsklassen aus dem `shared-kernel`-Modul, die in `commonMain` definiert sind und somit sowohl auf dem Server (JVM) als auch im Client (JS) verfügbar sind: + +1. **ApiValidationUtils**: Enthält spezifische Validierungsmethoden für API-Anfragen +2. **ValidationUtils**: Enthält allgemeine Validierungsmethoden +3. **ValidationError**: Repräsentiert einen einzelnen Validierungsfehler +4. **ValidationResult**: Repräsentiert das Ergebnis einer Validierung + +Durch die Nutzung dieser gemeinsamen Klassen wird sichergestellt, dass die Validierungslogik auf Client- und Serverseite konsistent ist. + +## Beispielimplementierung + +Als Beispiel wurde eine `LoginForm`-Komponente implementiert, die Client-seitige Validierung für Benutzername und Passwort durchführt, bevor die Daten an den Server gesendet werden. + +### Validierungsablauf + +1. Benutzer gibt Daten in das Formular ein +2. Bei Klick auf den Login-Button werden die Eingaben mit `ApiValidationUtils.validateLoginRequest()` validiert +3. Bei Validierungsfehlern werden diese angezeigt, ohne dass eine Serveranfrage gesendet wird +4. Nur bei erfolgreicher Validierung wird die Anfrage an den Server gesendet + +### Code-Beispiel + +```kotlin +// 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 + // ... API call code ... +} +``` + +### Fehleranzeige + +Validierungsfehler werden benutzerfreundlich angezeigt: + +```kotlin +// 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 + } +} +``` + +## Vorteile der Client-seitigen Validierung + +1. **Bessere Benutzererfahrung**: Sofortiges Feedback ohne Wartezeit auf Serverantwort +2. **Reduzierte Serverlast**: Ungültige Anfragen werden bereits im Client abgefangen +3. **Konsistente Validierung**: Gleiche Validierungslogik auf Client und Server +4. **Offline-Fähigkeit**: Grundlegende Validierung funktioniert auch ohne Serververbindung +5. **Erhöhte Sicherheit**: Doppelte Validierungsschicht (Client und Server) + +## Implementierte Komponenten + +### LoginForm + +Eine React-Komponente, die als Web-Komponente registriert ist und in HTML verwendet werden kann: + +```html + +``` + +Die Komponente validiert: +- Benutzername/E-Mail (Pflichtfeld, Länge, E-Mail-Format wenn @ enthalten) +- Passwort (Pflichtfeld, Mindestlänge) + +## Anleitung zur Implementierung weiterer Validierungen + +### 1. Nutzung vorhandener Validierungsmethoden + +Für die meisten Anwendungsfälle können die vorhandenen Methoden in `ApiValidationUtils` verwendet werden: + +```kotlin +// Validierung von Login-Daten +ApiValidationUtils.validateLoginRequest(username, password) + +// Validierung von Länder-Daten +ApiValidationUtils.validateCountryRequest(isoAlpha2Code, isoAlpha3Code, nameDeutsch, nameEnglisch) + +// Validierung von Pferde-Daten +ApiValidationUtils.validateHorseRequest(pferdeName, lebensnummer, chipNummer, oepsNummer, feiNummer) + +// Validierung von Veranstaltungs-Daten +ApiValidationUtils.validateEventRequest(name, ort, startDatum, endDatum, maxTeilnehmer) + +// Validierung von Query-Parametern +ApiValidationUtils.validateQueryParameters(limit, offset, startDate, endDate, search, q) + +// Validierung von UUID-Strings +ApiValidationUtils.validateUuidString(uuidString) +``` + +### 2. Implementierung eigener Validierungen + +Für spezifische Validierungen können die Basismethoden in `ValidationUtils` verwendet werden: + +```kotlin +// Prüfung auf nicht-leere Eingabe +ValidationUtils.validateNotBlank(value, fieldName) + +// Längenvalidierung +ValidationUtils.validateLength(value, fieldName, maxLength, minLength) + +// E-Mail-Validierung +ValidationUtils.validateEmail(email, fieldName) + +// Telefonnummer-Validierung +ValidationUtils.validatePhoneNumber(phone, fieldName) + +// Postleitzahl-Validierung +ValidationUtils.validatePostalCode(postalCode, fieldName) + +// Ländercode-Validierung +ValidationUtils.validateCountryCode(countryCode, fieldName) + +// Geburtsdatum-Validierung +ValidationUtils.validateBirthDate(birthDate, fieldName) + +// Jahres-Validierung +ValidationUtils.validateYear(year, fieldName, minYear) +``` + +### 3. Anzeige von Validierungsfehlern + +Validierungsfehler sollten benutzerfreundlich angezeigt werden: + +```kotlin +// Hilfsfunktion zum Abrufen eines Fehlers für ein bestimmtes Feld +val getFieldError = { fieldName: String -> + validationErrors.find { it.field == fieldName }?.message +} + +// Anzeige des Fehlers +getFieldError("fieldName")?.let { + // Fehleranzeige-Code +} +``` + +## Fazit + +Die Client-seitige Validierung wurde erfolgreich implementiert und kann als Grundlage für weitere Formularvalidierungen dienen. Durch die Nutzung der gemeinsamen Validierungsklassen wird eine konsistente Benutzererfahrung und erhöhte Datensicherheit gewährleistet. + +--- + +**Implementiert am**: 2025-07-21 +**Status**: ✅ Vollständig implementiert +**Dokumentation**: ✅ Vollständig diff --git a/DATABASE_INSTALLATION_COMPLETED.md b/DATABASE_INSTALLATION_COMPLETED.md new file mode 100644 index 00000000..e0f28341 --- /dev/null +++ b/DATABASE_INSTALLATION_COMPLETED.md @@ -0,0 +1,50 @@ +# Datenbank-Installation Vervollständigt + +## Überblick +Dieses Dokument beschreibt die Änderungen, die vorgenommen wurden, um die Datenbank-Installation zu vervollständigen. + +## Vorgenommene Änderungen + +### 1. PgAdmin-Service aktiviert +**Problem:** Der PgAdmin-Service war in der docker-compose.yml-Datei auskommentiert, was die Verwaltung der Datenbank erschwerte. + +**Lösung:** +- Auskommentierung des PgAdmin-Service in docker-compose.yml entfernt +- Standard-Passwort auf den Wert aus der .env-Datei angepasst (`admin_password_change_me`) +- pgadmin_data-Volume aktiviert, um PgAdmin-Daten zu persistieren + +### 2. Überprüfung der Konfiguration +Die folgenden Konfigurationen wurden überprüft und sind korrekt eingerichtet: + +- **Umgebungsvariablen in .env-Datei:** Alle erforderlichen Variablen (DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD) sind vorhanden und korrekt konfiguriert. +- **Datenbank-Konfiguration:** DatabaseConfig.kt liest die Umgebungsvariablen korrekt ein und erstellt die JDBC-URL entsprechend. +- **Datenbank-Initialisierung:** DatabaseFactory.kt initialisiert die Datenbankverbindung korrekt mit HikariCP. +- **Migrations-System:** MigrationSetup.kt registriert und führt alle Migrationen aus, die in den entsprechenden Modulen definiert sind. +- **Anwendungsstart:** Application.kt initialisiert die Datenbank und führt Migrationen beim Start aus. + +## Nächste Schritte +1. Starten Sie die Anwendung mit Docker Compose: + ``` + docker-compose up -d + ``` + +2. Zugriff auf PgAdmin: + - Öffnen Sie http://localhost:5050 im Browser + - Melden Sie sich mit den Zugangsdaten aus der .env-Datei an: + - E-Mail: admin@example.com (oder Wert von PGADMIN_DEFAULT_EMAIL) + - Passwort: admin_password_change_me (oder Wert von PGADMIN_DEFAULT_PASSWORD) + +3. Verbindung zur Datenbank in PgAdmin einrichten: + - Rechtsklick auf "Servers" > "Create" > "Server..." + - Name: Meldestelle + - Connection-Tab: + - Host: db + - Port: 5432 + - Maintenance database: meldestelle_db + - Username: meldestelle_user + - Password: secure_password_change_me (oder Wert von DB_PASSWORD) + +4. Überprüfen Sie, ob die Tabellen korrekt erstellt wurden, einschließlich der _migrations-Tabelle. + +## Datum +2025-07-21 10:14 diff --git a/README.md b/README.md index fdb1a06f..132281af 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ The project follows Domain-Driven Design (DDD) principles with clearly separated * **`master-data`** - Master data management (countries, regions, age classes, venues) * **`member-management`** - Person and club/association management * **`horse-registry`** - Horse registration and management +* **`event-management`** - Event and tournament management * **`api-gateway`** - Central API gateway aggregating all services ### Module Dependencies @@ -21,6 +22,11 @@ api-gateway ├── shared-kernel ├── master-data ├── member-management +├── horse-registry +└── event-management + +event-management +├── shared-kernel └── horse-registry horse-registry @@ -63,3 +69,7 @@ master-data ## Documentation See the `docs/` directory for detailed architecture documentation and diagrams. + +## Last Updated + +2025-07-21 diff --git a/README_API_Implementation.md b/README_API_Implementation.md index f378e32b..d61525ea 100644 --- a/README_API_Implementation.md +++ b/README_API_Implementation.md @@ -244,3 +244,7 @@ The server module now provides a **production-ready RESTful API** that: - Can be easily extended with additional features The API serves as a solid foundation for the Meldestelle system and can support web applications, mobile apps, and third-party integrations. + +## Last Updated + +2025-07-21 diff --git a/README_CODE_ORGANIZATION.md b/README_CODE_ORGANIZATION.md index 9c30a645..948b80f9 100644 --- a/README_CODE_ORGANIZATION.md +++ b/README_CODE_ORGANIZATION.md @@ -222,3 +222,7 @@ val data = call.safeReceive() ?: return 6. **Performance**: Lazy loading and efficient resource management This reorganization provides a solid foundation for future development while maintaining backward compatibility and improving code quality. + +## Last Updated + +2025-07-21 diff --git a/README_CONFIG.md b/README_CONFIG.md index 054e7fe5..3ea9aaca 100644 --- a/README_CONFIG.md +++ b/README_CONFIG.md @@ -158,6 +158,11 @@ services: - DB_NAME=${POSTGRES_DB} - DB_USER=${POSTGRES_USER} - DB_PASSWORD=${POSTGRES_PASSWORD} + + pgadmin: + environment: + - PGADMIN_DEFAULT_EMAIL=${PGADMIN_DEFAULT_EMAIL:-admin@example.com} + - PGADMIN_DEFAULT_PASSWORD=${PGADMIN_DEFAULT_PASSWORD:-admin_password_change_me} ``` ## Beste Praktiken @@ -171,3 +176,7 @@ services: 4. **Validierung**: Validieren Sie kritische Konfigurationen beim Anwendungsstart, um Fehler frühzeitig zu erkennen. 5. **Dokumentation**: Halten Sie die Dokumentation der Konfigurationsparameter aktuell, damit neue Teammitglieder die Anwendung leicht konfigurieren können. + +## Letztes Update + +2025-07-21 diff --git a/README_DATABASE_SETUP.md b/README_DATABASE_SETUP.md index 701db1d3..9cf0db07 100644 --- a/README_DATABASE_SETUP.md +++ b/README_DATABASE_SETUP.md @@ -28,6 +28,11 @@ POSTGRES_USER=meldestelle_user POSTGRES_PASSWORD=secure_password_change_me POSTGRES_DB=meldestelle_db +# PgAdmin Konfiguration +PGADMIN_DEFAULT_EMAIL=admin@example.com +PGADMIN_DEFAULT_PASSWORD=admin_password_change_me +PGADMIN_PORT=5050 + # API Gateway Konfiguration API_PORT=8081 ``` @@ -77,6 +82,33 @@ Die Datenbankstruktur ist in verschiedene Bereiche unterteilt, die den Modulen d 2. Führen Sie `docker-compose up -d db` aus, um nur die Datenbank zu starten 3. Alternativ `docker-compose up -d` für das gesamte System +### PgAdmin verwenden + +Das Projekt enthält einen PgAdmin-Service für die einfache Verwaltung der Datenbank über eine Web-Oberfläche. + +1. Starten Sie die Anwendung mit Docker Compose: + ``` + docker-compose up -d + ``` + +2. Zugriff auf PgAdmin: + - Öffnen Sie http://localhost:5050 im Browser + - Melden Sie sich mit den Zugangsdaten aus der .env-Datei an: + - E-Mail: admin@example.com (oder Wert von PGADMIN_DEFAULT_EMAIL) + - Passwort: admin_password_change_me (oder Wert von PGADMIN_DEFAULT_PASSWORD) + +3. Verbindung zur Datenbank in PgAdmin einrichten: + - Rechtsklick auf "Servers" > "Create" > "Server..." + - Name: Meldestelle + - Connection-Tab: + - Host: db + - Port: 5432 + - Maintenance database: meldestelle_db + - Username: meldestelle_user + - Password: secure_password_change_me (oder Wert von DB_PASSWORD) + +4. Überprüfen Sie, ob die Tabellen korrekt erstellt wurden, einschließlich der _migrations-Tabelle. + ### Manuell 1. Installieren Sie PostgreSQL auf Ihrem System @@ -97,3 +129,7 @@ Die Datenbankstruktur ist in verschiedene Bereiche unterteilt, die den Modulen d - Prüfen Sie die Logs auf detaillierte Fehlermeldungen - Migrationen werden nur einmal ausgeführt - Änderungen an bestehenden Migrationen haben keine Auswirkung - Bei schwerwiegenden Problemen kann die `_migrations`-Tabelle manuell bearbeitet werden (nur für Fortgeschrittene) + +## Letztes Update + +2025-07-21 diff --git a/api-gateway-consolidation-plan.md b/api-gateway-consolidation-plan.md new file mode 100644 index 00000000..7ba6375d --- /dev/null +++ b/api-gateway-consolidation-plan.md @@ -0,0 +1,91 @@ +# API Gateway Consolidation Plan + +This document outlines the plan for consolidating the duplicate directory structure in the api-gateway module, specifically merging the `src/main` and `src/jvmMain` directories. + +## 1. File Analysis + +### 1.1 Duplicate Files + +The following files exist in both directories: + +| File | Action | Reasoning | +|------|--------|-----------| +| Application.kt | Merge into src/jvmMain | src/jvmMain version has better configuration handling, but src/main has more complete module setup | +| config/AuthorizationConfig.kt | Keep src/jvmMain version | Assuming identical content | +| config/DatabaseConfig.kt | Keep src/jvmMain version | Assuming identical content | +| config/MonitoringConfig.kt | Keep src/jvmMain version | Confirmed identical content | +| config/OpenApiConfig.kt | Keep src/jvmMain version | Assuming identical content | +| config/SecurityConfig.kt | Keep src/jvmMain version | Assuming identical content | +| config/SerializationConfig.kt | Keep src/jvmMain version | Assuming identical content | +| routing/AuthRoutes.kt | Keep src/jvmMain version | Assuming identical content | + +### 1.2 Files Unique to src/main + +The following files exist only in src/main: + +| File | Action | Reasoning | +|------|--------|-----------| +| auth/AuthorizationHelper.kt | Move to src/jvmMain | Contains important authorization functionality not present in src/jvmMain | +| routing/RoutingConfig.kt | Move to src/jvmMain | Contains critical routing configuration not present in src/jvmMain | +| config/configureSwagger.kt | Check if needed | Not referenced in src/jvmMain, but referenced in src/main Application.kt | + +### 1.3 Files Unique to src/jvmMain + +The following files exist only in src/jvmMain: + +| File | Action | Reasoning | +|------|--------|-----------| +| auth/ApiKeyAuth.kt | Keep | Provides API key authentication | +| auth/JwtAuth.kt | Keep and enhance | Provides JWT authentication, but should be enhanced with functionality from AuthorizationHelper.kt | +| config/MigrationSetup.kt | Keep | Handles database migrations | +| migrations/* (4 files) | Keep | Handle migrations for different contexts | +| module.kt | Merge with Application.kt | Contains module definition but needs to be enhanced with functionality from src/main | +| validation/RequestValidator.kt | Keep | Provides request validation | + +## 2. Implementation Steps + +### 2.1 Merge Application.kt and module.kt + +1. Start with the src/jvmMain Application.kt +2. Incorporate the module configuration from src/main Application.kt +3. Ensure all necessary components are configured: + - Database + - Serialization + - Monitoring + - Security + - OpenAPI/Swagger + - Routing + +### 2.2 Move Unique Files from src/main to src/jvmMain + +1. Move AuthorizationHelper.kt to src/jvmMain/kotlin/at/mocode/gateway/auth/ +2. Move RoutingConfig.kt to src/jvmMain/kotlin/at/mocode/gateway/routing/ +3. Check if configureSwagger.kt is needed and move if necessary + +### 2.3 Update References + +1. Update imports in all files to reflect the new structure +2. Ensure all configuration functions are called in the module function + +### 2.4 Remove src/main Directory + +After all files have been consolidated and the application has been verified to work correctly, remove the src/main directory. + +## 3. Testing + +After consolidation, the following tests should be performed: + +1. Build the project to ensure there are no compilation errors +2. Run the application to ensure it starts correctly +3. Test key functionality to ensure it works as expected: + - Authentication + - Authorization + - API endpoints + - Database operations + +## 4. Documentation Update + +Update the project documentation to reflect the new structure: + +1. Update README.md if it references the old structure +2. Update any other documentation that mentions the directory structure diff --git a/api-gateway/build.gradle.kts b/api-gateway/build.gradle.kts index b2e4761b..a232fb53 100644 --- a/api-gateway/build.gradle.kts +++ b/api-gateway/build.gradle.kts @@ -17,6 +17,7 @@ kotlin { implementation(project(":master-data")) implementation(project(":member-management")) implementation(project(":horse-registry")) + implementation(project(":event-management")) implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.serialization.json) diff --git a/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/config/AuthorizationConfig.kt b/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/config/AuthorizationConfig.kt new file mode 100644 index 00000000..8912de2a --- /dev/null +++ b/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/config/AuthorizationConfig.kt @@ -0,0 +1,330 @@ +package at.mocode.gateway.config + +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.auth.jwt.* +import io.ktor.server.response.* +import io.ktor.http.* +import io.ktor.server.routing.* +import io.ktor.util.pipeline.* +import at.mocode.enums.RolleE +import at.mocode.enums.BerechtigungE + +/** + * Authorization configuration and middleware for role-based access control. + * + * Provides utilities for checking user roles and permissions on protected endpoints. + */ + +/** + * Enum representing user roles in the system. + */ +enum class UserRole { + ADMIN, + VEREINS_ADMIN, + FUNKTIONAER, + REITER, + TRAINER, + RICHTER, + TIERARZT, + ZUSCHAUER, + GAST +} + +/** + * Enum representing permissions in the system. + */ +enum class Permission { + // Person management + PERSON_READ, + PERSON_CREATE, + PERSON_UPDATE, + PERSON_DELETE, + + // Club management + VEREIN_READ, + VEREIN_CREATE, + VEREIN_UPDATE, + VEREIN_DELETE, + + // Event management + VERANSTALTUNG_READ, + VERANSTALTUNG_CREATE, + VERANSTALTUNG_UPDATE, + VERANSTALTUNG_DELETE, + + // Horse management + PFERD_READ, + PFERD_CREATE, + PFERD_UPDATE, + PFERD_DELETE, + + // Master data management + STAMMDATEN_READ, + STAMMDATEN_UPDATE, + + // System administration + SYSTEM_ADMIN, + BENUTZER_VERWALTEN, + ROLLEN_VERWALTEN +} + +/** + * Data class representing user authorization context. + */ +data class UserAuthContext( + val userId: String, + val username: String, + val roles: List, + val permissions: List +) + +/** + * Maps domain role enum to authorization role enum. + */ +private fun mapDomainRoleToUserRole(domainRole: RolleE): UserRole { + return when (domainRole) { + RolleE.ADMIN -> UserRole.ADMIN + RolleE.VEREINS_ADMIN -> UserRole.VEREINS_ADMIN + RolleE.FUNKTIONAER -> UserRole.FUNKTIONAER + RolleE.REITER -> UserRole.REITER + RolleE.TRAINER -> UserRole.TRAINER + RolleE.RICHTER -> UserRole.RICHTER + RolleE.TIERARZT -> UserRole.TIERARZT + RolleE.ZUSCHAUER -> UserRole.ZUSCHAUER + RolleE.GAST -> UserRole.GAST + } +} + +/** + * Maps domain permission enum to authorization permission enum. + */ +private fun mapDomainPermissionToPermission(domainPermission: BerechtigungE): Permission { + return when (domainPermission) { + BerechtigungE.PERSON_READ -> Permission.PERSON_READ + BerechtigungE.PERSON_CREATE -> Permission.PERSON_CREATE + BerechtigungE.PERSON_UPDATE -> Permission.PERSON_UPDATE + BerechtigungE.PERSON_DELETE -> Permission.PERSON_DELETE + BerechtigungE.VEREIN_READ -> Permission.VEREIN_READ + BerechtigungE.VEREIN_CREATE -> Permission.VEREIN_CREATE + BerechtigungE.VEREIN_UPDATE -> Permission.VEREIN_UPDATE + BerechtigungE.VEREIN_DELETE -> Permission.VEREIN_DELETE + BerechtigungE.VERANSTALTUNG_READ -> Permission.VERANSTALTUNG_READ + BerechtigungE.VERANSTALTUNG_CREATE -> Permission.VERANSTALTUNG_CREATE + BerechtigungE.VERANSTALTUNG_UPDATE -> Permission.VERANSTALTUNG_UPDATE + BerechtigungE.VERANSTALTUNG_DELETE -> Permission.VERANSTALTUNG_DELETE + BerechtigungE.PFERD_READ -> Permission.PFERD_READ + BerechtigungE.PFERD_CREATE -> Permission.PFERD_CREATE + BerechtigungE.PFERD_UPDATE -> Permission.PFERD_UPDATE + BerechtigungE.PFERD_DELETE -> Permission.PFERD_DELETE + BerechtigungE.STAMMDATEN_READ -> Permission.STAMMDATEN_READ + BerechtigungE.STAMMDATEN_UPDATE -> Permission.STAMMDATEN_UPDATE + BerechtigungE.SYSTEM_ADMIN -> Permission.SYSTEM_ADMIN + BerechtigungE.BENUTZER_VERWALTEN -> Permission.BENUTZER_VERWALTEN + BerechtigungE.ROLLEN_VERWALTEN -> Permission.ROLLEN_VERWALTEN + } +} + +/** + * Extension function to get user authorization context from JWT principal. + */ +fun JWTPrincipal.getUserAuthContext(): UserAuthContext? { + val userId = getClaim("userId", String::class) ?: return null + val username = getClaim("username", String::class) ?: return null + + // Get roles and permissions from JWT token + val domainRoles = getClaim("roles", Array::class)?.toList() ?: emptyList() + val domainPermissions = getClaim("permissions", Array::class)?.toList() ?: emptyList() + + // Map domain enums to authorization enums + val roles = domainRoles.map { mapDomainRoleToUserRole(it) } + val permissions = domainPermissions.map { mapDomainPermissionToPermission(it) } + + return UserAuthContext( + userId = userId, + username = username, + roles = roles, + permissions = permissions + ) +} + +/** + * Maps roles to their corresponding permissions. + */ +private fun getRolePermissions(roles: List): List { + val permissions = mutableSetOf() + + roles.forEach { role -> + when (role) { + UserRole.ADMIN -> { + permissions.addAll(Permission.values()) + } + UserRole.VEREINS_ADMIN -> { + permissions.addAll(listOf( + Permission.PERSON_READ, Permission.PERSON_CREATE, Permission.PERSON_UPDATE, + Permission.VEREIN_READ, Permission.VEREIN_UPDATE, + Permission.PFERD_READ, Permission.PFERD_CREATE, Permission.PFERD_UPDATE, + Permission.STAMMDATEN_READ + )) + } + UserRole.FUNKTIONAER -> { + permissions.addAll(listOf( + Permission.PERSON_READ, + Permission.VEREIN_READ, + Permission.VERANSTALTUNG_READ, Permission.VERANSTALTUNG_CREATE, Permission.VERANSTALTUNG_UPDATE, + Permission.PFERD_READ, + Permission.STAMMDATEN_READ + )) + } + UserRole.TRAINER -> { + permissions.addAll(listOf( + Permission.PERSON_READ, + Permission.VEREIN_READ, + Permission.VERANSTALTUNG_READ, + Permission.PFERD_READ, + Permission.STAMMDATEN_READ + )) + } + UserRole.REITER -> { + permissions.addAll(listOf( + Permission.PERSON_READ, + Permission.VEREIN_READ, + Permission.VERANSTALTUNG_READ, + Permission.PFERD_READ, + Permission.STAMMDATEN_READ + )) + } + UserRole.RICHTER -> { + permissions.addAll(listOf( + Permission.PERSON_READ, + Permission.VEREIN_READ, + Permission.VERANSTALTUNG_READ, + Permission.PFERD_READ, + Permission.STAMMDATEN_READ + )) + } + UserRole.TIERARZT -> { + permissions.addAll(listOf( + Permission.PERSON_READ, + Permission.PFERD_READ, + Permission.STAMMDATEN_READ + )) + } + UserRole.ZUSCHAUER -> { + permissions.addAll(listOf( + Permission.VERANSTALTUNG_READ, + Permission.STAMMDATEN_READ + )) + } + UserRole.GAST -> { + permissions.addAll(listOf( + Permission.STAMMDATEN_READ + )) + } + } + } + + return permissions.toList() +} + +/** + * Route extension function to require specific roles. + */ +fun Route.requireRoles(vararg roles: UserRole, build: Route.() -> Unit): Route { + val route = createChild(object : RouteSelector() { + override suspend fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation { + return RouteSelectorEvaluation.Constant + } + + override fun toString(): String = "requireRoles(${roles.joinToString()})" + }) + + route.intercept(ApplicationCallPipeline.Call) { + val principal = call.principal() + val authContext = principal?.getUserAuthContext() + + if (authContext == null) { + call.respond(HttpStatusCode.Unauthorized, "Authentication required") + finish() + return@intercept + } + + val hasRequiredRole = roles.any { requiredRole -> + authContext.roles.contains(requiredRole) + } + + if (!hasRequiredRole) { + call.respond( + HttpStatusCode.Forbidden, + "Access denied. Required roles: ${roles.joinToString()}" + ) + finish() + return@intercept + } + } + + route.build() + return route +} + +/** + * Route extension function to require specific permissions. + */ +fun Route.requirePermissions(vararg permissions: Permission, build: Route.() -> Unit): Route { + val route = createChild(object : RouteSelector() { + override suspend fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation { + return RouteSelectorEvaluation.Constant + } + + override fun toString(): String = "requirePermissions(${permissions.joinToString()})" + }) + + route.intercept(ApplicationCallPipeline.Call) { + val principal = call.principal() + val authContext = principal?.getUserAuthContext() + + if (authContext == null) { + call.respond(HttpStatusCode.Unauthorized, "Authentication required") + finish() + return@intercept + } + + val hasAllPermissions = permissions.all { requiredPermission -> + authContext.permissions.contains(requiredPermission) + } + + if (!hasAllPermissions) { + call.respond( + HttpStatusCode.Forbidden, + "Access denied. Required permissions: ${permissions.joinToString()}" + ) + finish() + return@intercept + } + } + + route.build() + return route +} + +/** + * Pipeline context extension to get current user authorization context. + */ +val PipelineContext.userAuthContext: UserAuthContext? + get() = call.principal()?.getUserAuthContext() + +/** + * Application call extension to check if user has specific role. + */ +fun ApplicationCall.hasRole(role: UserRole): Boolean { + val authContext = principal()?.getUserAuthContext() + return authContext?.roles?.contains(role) == true +} + +/** + * Application call extension to check if user has specific permission. + */ +fun ApplicationCall.hasPermission(permission: Permission): Boolean { + val authContext = principal()?.getUserAuthContext() + return authContext?.permissions?.contains(permission) == true +} diff --git a/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/config/DatabaseConfig.kt b/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/config/DatabaseConfig.kt new file mode 100644 index 00000000..5d218771 --- /dev/null +++ b/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/config/DatabaseConfig.kt @@ -0,0 +1,62 @@ +package at.mocode.gateway.config + +import io.ktor.server.application.* +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.transactions.transaction +import org.slf4j.LoggerFactory + +/** + * Database configuration for the API Gateway. + * + * Sets up database connections and schema initialization for all bounded contexts. + */ +fun Application.configureDatabase() { + val log = LoggerFactory.getLogger("DatabaseConfig") + val databaseUrl = environment.config.propertyOrNull("database.url")?.getString() + ?: "jdbc:postgresql://localhost:5432/meldestelle" + val databaseUser = environment.config.propertyOrNull("database.user")?.getString() + ?: "meldestelle_user" + val databasePassword = environment.config.propertyOrNull("database.password")?.getString() + ?: "meldestelle_password" + + // Initialize database connection + Database.connect( + url = databaseUrl, + driver = "org.postgresql.Driver", + user = databaseUser, + password = databasePassword + ) + + // Initialize database schemas for all contexts + transaction { + // Import table definitions from all contexts + try { + // Master Data Context tables + SchemaUtils.createMissingTablesAndColumns( + at.mocode.masterdata.infrastructure.repository.LandTable + ) + + // Member Management Context tables + SchemaUtils.createMissingTablesAndColumns( + at.mocode.members.infrastructure.repository.PersonTable, + at.mocode.members.infrastructure.repository.VereinTable + ) + + // Horse Registry Context tables + SchemaUtils.createMissingTablesAndColumns( + at.mocode.horses.infrastructure.repository.HorseTable + ) + + // Event Management Context tables + SchemaUtils.createMissingTablesAndColumns( + at.mocode.events.infrastructure.repository.VeranstaltungTable + ) + + log.info("Database schemas initialized successfully") + } catch (e: Exception) { + log.error("Failed to initialize database schemas: ${e.message}") + // In production, you might want to fail fast here + } + } +} diff --git a/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/config/MonitoringConfig.kt b/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/config/MonitoringConfig.kt new file mode 100644 index 00000000..413c33d0 --- /dev/null +++ b/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/config/MonitoringConfig.kt @@ -0,0 +1,59 @@ +package at.mocode.gateway.config + +import io.ktor.server.application.* +import io.ktor.server.plugins.calllogging.* +import io.ktor.server.plugins.statuspages.* +import io.ktor.server.request.* +import io.ktor.http.* +import io.ktor.server.response.* +import at.mocode.dto.base.ApiResponse +import org.slf4j.event.Level + +/** + * Monitoring and logging configuration for the API Gateway. + * + * Configures request logging, error handling, and status pages. + */ +fun Application.configureMonitoring() { + install(CallLogging) { + level = Level.INFO + filter { call -> call.request.path().startsWith("/api") } + format { call -> + val status = call.response.status() + val httpMethod = call.request.httpMethod.value + val userAgent = call.request.headers["User-Agent"] + "$status: $httpMethod ${call.request.path()} - $userAgent" + } + } + + install(StatusPages) { + exception { call, cause -> + call.application.log.error("Unhandled exception", cause) + call.respond( + HttpStatusCode.InternalServerError, + ApiResponse.error("Internal server error: ${cause.message}") + ) + } + + status(HttpStatusCode.NotFound) { call, status -> + call.respond( + status, + ApiResponse.error("Endpoint not found: ${call.request.path()}") + ) + } + + status(HttpStatusCode.Unauthorized) { call, status -> + call.respond( + status, + ApiResponse.error("Authentication required") + ) + } + + status(HttpStatusCode.Forbidden) { call, status -> + call.respond( + status, + ApiResponse.error("Access forbidden") + ) + } + } +} diff --git a/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/config/OpenApiConfig.kt b/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/config/OpenApiConfig.kt new file mode 100644 index 00000000..cd4f7fbb --- /dev/null +++ b/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/config/OpenApiConfig.kt @@ -0,0 +1,35 @@ +package at.mocode.gateway.config + +import io.ktor.server.application.* +import io.ktor.server.plugins.openapi.* +import io.ktor.server.plugins.swagger.* +import io.ktor.server.routing.* + +/** + * Configuration for OpenAPI/Swagger documentation. + * + * This module configures the OpenAPI specification generation and Swagger UI + * for the API Gateway, providing comprehensive API documentation. + */ +fun Application.configureOpenApi() { + // Configure OpenAPI using a static file + routing { + // Serve the OpenAPI specification from a file + openAPI(path = "openapi", swaggerFile = "openapi/documentation.yaml") { + // Additional configuration can be added here if needed + } + } +} + +/** + * Configuration for Swagger UI. + * + * Provides an interactive web interface for exploring and testing the API. + */ +fun Application.configureSwagger() { + routing { + swaggerUI(path = "swagger", swaggerFile = "openapi/documentation.yaml") { + version = "4.15.5" + } + } +} diff --git a/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/config/SecurityConfig.kt b/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/config/SecurityConfig.kt new file mode 100644 index 00000000..48ab08a8 --- /dev/null +++ b/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/config/SecurityConfig.kt @@ -0,0 +1,85 @@ +package at.mocode.gateway.config + +import io.ktor.server.application.* +import io.ktor.server.plugins.cors.routing.* +import io.ktor.server.auth.* +import io.ktor.server.auth.jwt.* +import io.ktor.http.* +import com.auth0.jwt.JWT +import com.auth0.jwt.algorithms.Algorithm +import io.ktor.server.response.respond + +/** + * Security configuration for the API Gateway. + * + * Configures CORS, JWT authentication, and other security-related settings. + */ +fun Application.configureSecurity() { + install(CORS) { + allowMethod(HttpMethod.Options) + allowMethod(HttpMethod.Put) + allowMethod(HttpMethod.Delete) + allowMethod(HttpMethod.Patch) + allowHeader(HttpHeaders.Authorization) + allowHeader(HttpHeaders.ContentType) + allowHeader("X-Requested-With") + + // Allow requests from common development origins + allowHost("localhost:3000") + allowHost("localhost:8080") + allowHost("127.0.0.1:3000") + allowHost("127.0.0.1:8080") + + // In production, configure specific allowed origins + anyHost() // This should be restricted in production + } + + // JWT Configuration + val jwtConfig = JwtConfig.fromEnvironment() + + install(Authentication) { + jwt("auth-jwt") { + realm = jwtConfig.realm + verifier( + JWT + .require(Algorithm.HMAC512(jwtConfig.secret)) + .withAudience(jwtConfig.audience) + .withIssuer(jwtConfig.issuer) + .build() + ) + validate { credential -> + if (credential.payload.getClaim("userId").asString() != null) { + JWTPrincipal(credential.payload) + } else { + null + } + } + challenge { defaultScheme, realm -> + call.respond(HttpStatusCode.Unauthorized, "Token is not valid or has expired") + } + } + } +} + +/** + * JWT Configuration data class. + */ +data class JwtConfig( + val secret: String, + val issuer: String, + val audience: String, + val realm: String, + val expirationTime: Long = 3600000L // 1 hour in milliseconds +) { + companion object { + fun fromEnvironment(): JwtConfig { + return JwtConfig( + secret = System.getenv("JWT_SECRET") ?: "default-secret-key-change-in-production", + issuer = System.getenv("JWT_ISSUER") ?: "meldestelle-api", + audience = System.getenv("JWT_AUDIENCE") ?: "meldestelle-users", + realm = System.getenv("JWT_REALM") ?: "Meldestelle API", + expirationTime = System.getenv("JWT_EXPIRATION")?.toLongOrNull() ?: 3600000L + ) + } + } +} diff --git a/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/config/SerializationConfig.kt b/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/config/SerializationConfig.kt new file mode 100644 index 00000000..80864b17 --- /dev/null +++ b/api-gateway/src/jvmMain/kotlin/at/mocode/gateway/config/SerializationConfig.kt @@ -0,0 +1,23 @@ +package at.mocode.gateway.config + +import io.ktor.serialization.kotlinx.json.* +import io.ktor.server.application.* +import io.ktor.server.plugins.contentnegotiation.* +import kotlinx.serialization.json.Json + +/** + * Serialization configuration for the API Gateway. + * + * Configures JSON serialization settings that are consistent across all bounded contexts. + */ +fun Application.configureSerialization() { + install(ContentNegotiation) { + json(Json { + prettyPrint = true + isLenient = true + ignoreUnknownKeys = true + encodeDefaults = true + explicitNulls = false + }) + } +} diff --git a/api-gateway/src/jvmTest/kotlin/at/mocode/gateway/test/AuthenticationAuthorizationTest.kt b/api-gateway/src/jvmTest/kotlin/at/mocode/gateway/test/AuthenticationAuthorizationTest.kt new file mode 100644 index 00000000..8138dbc8 --- /dev/null +++ b/api-gateway/src/jvmTest/kotlin/at/mocode/gateway/test/AuthenticationAuthorizationTest.kt @@ -0,0 +1,276 @@ +package at.mocode.gateway.test + +import kotlinx.coroutines.runBlocking +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse +import java.net.URI +import java.time.Duration +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +/** + * Test class for authentication and authorization functionality. + * + * This test verifies the complete authentication and authorization flow: + * 1. User registration + * 2. User login + * 3. Access to protected endpoints + * 4. Token refresh + * 5. Password change + * 6. Logout + */ +class AuthenticationAuthorizationTest { + + private val baseUrl = "http://localhost:8080" + private val client = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .build() + + @Test + fun testAuthenticationFlow() = runBlocking { + println("🚀 Starting Authentication and Authorization Tests") + println("=" * 60) + + try { + // Test 1: Health Check + println("\n📋 Test 1: API Health Check") + testHealthCheck() + + // Test 2: User Registration + println("\n📝 Test 2: User Registration") + testUserRegistration() + + // Test 3: User Login + println("\n🔐 Test 3: User Login") + val token = testUserLogin() + + if (token != null) { + // Test 4: Access Protected Profile Endpoint + println("\n👤 Test 4: Access Protected Profile") + testProtectedProfile(token) + + // Test 5: Token Refresh + println("\n🔄 Test 5: Token Refresh") + val newToken = testTokenRefresh(token) + + // Test 6: Change Password + println("\n🔑 Test 6: Change Password") + testChangePassword(newToken ?: token) + + // Test 7: Logout + println("\n👋 Test 7: Logout") + testLogout(newToken ?: token) + } + + println("\n✅ All tests completed!") + + } catch (e: Exception) { + println("\n❌ Test failed with error: ${e.message}") + e.printStackTrace() + throw e + } + } + + private suspend fun testHealthCheck() { + val request = HttpRequest.newBuilder() + .uri(URI.create("$baseUrl/health")) + .GET() + .build() + + val response = client.send(request, HttpResponse.BodyHandlers.ofString()) + + if (response.statusCode() == 200) { + println("✅ Health check passed") + println(" Response: ${response.body()}") + assertEquals(200, response.statusCode(), "Health check should return 200 OK") + } else { + println("❌ Health check failed: ${response.statusCode()}") + println(" Response: ${response.body()}") + assertEquals(200, response.statusCode(), "Health check should return 200 OK") + } + } + + private suspend fun testUserRegistration() { + val registrationData = """ + { + "personId": "550e8400-e29b-41d4-a716-446655440000", + "username": "testuser_${System.currentTimeMillis()}", + "email": "test_${System.currentTimeMillis()}@example.com", + "password": "SecurePassword123!" + } + """.trimIndent() + + val request = HttpRequest.newBuilder() + .uri(URI.create("$baseUrl/auth/register")) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(registrationData)) + .build() + + val response = client.send(request, HttpResponse.BodyHandlers.ofString()) + + if (response.statusCode() == 201) { + println("✅ User registration successful") + println(" Response: ${response.body()}") + assertEquals(201, response.statusCode(), "User registration should return 201 Created") + } else { + println("⚠️ User registration response: ${response.statusCode()}") + println(" Response: ${response.body()}") + println(" Note: This might be expected if registration requires existing person ID") + // Don't assert here as registration might fail for valid reasons in test environment + } + } + + private suspend fun testUserLogin(): String? { + // Try to login with a test user (this assumes there's already a user in the system) + val loginData = """ + { + "usernameOrEmail": "admin", + "password": "admin123" + } + """.trimIndent() + + val request = HttpRequest.newBuilder() + .uri(URI.create("$baseUrl/auth/login")) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(loginData)) + .build() + + val response = client.send(request, HttpResponse.BodyHandlers.ofString()) + + if (response.statusCode() == 200) { + println("✅ User login successful") + println(" Response: ${response.body()}") + + // Extract token from response (simplified - in real scenario, parse JSON) + val responseBody = response.body() + val tokenStart = responseBody.indexOf("\"token\":\"") + 9 + val tokenEnd = responseBody.indexOf("\"", tokenStart) + + return if (tokenStart > 8 && tokenEnd > tokenStart) { + val token = responseBody.substring(tokenStart, tokenEnd) + println(" Token extracted: ${token.take(20)}...") + assertNotNull(token, "Token should not be null") + assertTrue(token.isNotEmpty(), "Token should not be empty") + token + } else { + println(" Could not extract token from response") + null + } + } else { + println("⚠️ User login failed: ${response.statusCode()}") + println(" Response: ${response.body()}") + println(" Note: This is expected if no test user exists in the database") + return null + } + } + + private suspend fun testProtectedProfile(token: String) { + val request = HttpRequest.newBuilder() + .uri(URI.create("$baseUrl/auth/profile")) + .header("Authorization", "Bearer $token") + .GET() + .build() + + val response = client.send(request, HttpResponse.BodyHandlers.ofString()) + + if (response.statusCode() == 200) { + println("✅ Protected profile access successful") + println(" Response: ${response.body()}") + assertEquals(200, response.statusCode(), "Protected profile access should return 200 OK") + } else { + println("❌ Protected profile access failed: ${response.statusCode()}") + println(" Response: ${response.body()}") + assertEquals(200, response.statusCode(), "Protected profile access should return 200 OK") + } + } + + private suspend fun testTokenRefresh(token: String): String? { + val request = HttpRequest.newBuilder() + .uri(URI.create("$baseUrl/auth/refresh")) + .header("Authorization", "Bearer $token") + .POST(HttpRequest.BodyPublishers.noBody()) + .build() + + val response = client.send(request, HttpResponse.BodyHandlers.ofString()) + + if (response.statusCode() == 200) { + println("✅ Token refresh successful") + println(" Response: ${response.body()}") + + // Extract new token from response (simplified) + val responseBody = response.body() + val tokenStart = responseBody.indexOf("\"token\":\"") + 9 + val tokenEnd = responseBody.indexOf("\"", tokenStart) + + return if (tokenStart > 8 && tokenEnd > tokenStart) { + val newToken = responseBody.substring(tokenStart, tokenEnd) + println(" New token extracted: ${newToken.take(20)}...") + assertNotNull(newToken, "New token should not be null") + assertTrue(newToken.isNotEmpty(), "New token should not be empty") + newToken + } else { + println(" Could not extract new token from response") + null + } + } else { + println("❌ Token refresh failed: ${response.statusCode()}") + println(" Response: ${response.body()}") + return null + } + } + + private suspend fun testChangePassword(token: String) { + val changePasswordData = """ + { + "currentPassword": "admin123", + "newPassword": "NewSecurePassword123!" + } + """.trimIndent() + + val request = HttpRequest.newBuilder() + .uri(URI.create("$baseUrl/auth/change-password")) + .header("Authorization", "Bearer $token") + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(changePasswordData)) + .build() + + val response = client.send(request, HttpResponse.BodyHandlers.ofString()) + + if (response.statusCode() == 200) { + println("✅ Password change successful") + println(" Response: ${response.body()}") + assertEquals(200, response.statusCode(), "Password change should return 200 OK") + } else { + println("⚠️ Password change response: ${response.statusCode()}") + println(" Response: ${response.body()}") + println(" Note: This might fail if current password is incorrect") + // Don't assert here as password change might fail for valid reasons in test environment + } + } + + private suspend fun testLogout(token: String) { + val request = HttpRequest.newBuilder() + .uri(URI.create("$baseUrl/auth/logout")) + .header("Authorization", "Bearer $token") + .POST(HttpRequest.BodyPublishers.noBody()) + .build() + + val response = client.send(request, HttpResponse.BodyHandlers.ofString()) + + if (response.statusCode() == 200) { + println("✅ Logout successful") + println(" Response: ${response.body()}") + assertEquals(200, response.statusCode(), "Logout should return 200 OK") + } else { + println("❌ Logout failed: ${response.statusCode()}") + println(" Response: ${response.body()}") + assertEquals(200, response.statusCode(), "Logout should return 200 OK") + } + } +} + +// Extension function for string repetition +operator fun String.times(n: Int): String = this.repeat(n) diff --git a/cleanup-summary.md b/cleanup-summary.md new file mode 100644 index 00000000..9726a275 --- /dev/null +++ b/cleanup-summary.md @@ -0,0 +1,121 @@ +# Meldestelle Codebase Cleanup Summary + +This document summarizes the cleanup tasks identified for the Meldestelle project and provides a comprehensive plan for implementation. + +## 1. Issues Identified + +### 1.1 Directory Structure Inconsistencies + +- The api-gateway module has inconsistent directory structure with both `src/main` and `src/jvmMain` directories +- Duplicate files exist in both directories with varying levels of completeness +- Some functionality exists only in one directory or the other + +### 1.2 Test File Organization + +- Standalone test scripts exist in the root directory instead of proper test directories +- Test scripts use ad-hoc testing approaches rather than proper unit test frameworks +- Test naming and organization is inconsistent + +### 1.3 Documentation Issues + +- Documentation is fragmented across multiple files +- Some documentation is outdated or inaccurate +- Redundant documentation exists for the same topics +- Inconsistent naming conventions for documentation files +- Documentation is scattered between root directory and docs directory + +### 1.4 Code Quality Issues + +- Potential unused or redundant code +- Inconsistent naming conventions +- Possible separation of concerns issues + +## 2. Implementation Plans + +Detailed implementation plans have been created for each area: + +### 2.1 API Gateway Consolidation Plan + +The [API Gateway Consolidation Plan](api-gateway-consolidation-plan.md) outlines: + +- Analysis of duplicate and unique files in src/main and src/jvmMain +- Strategy for merging Application.kt and module.kt +- Plan for moving unique files from src/main to src/jvmMain +- Steps for updating references and removing redundant directories + +### 2.2 Test Scripts Conversion Plan + +The [Test Scripts Conversion Plan](test-scripts-conversion-plan.md) outlines: + +- Identification of standalone test scripts and their target directories +- Guidelines for converting scripts to proper unit tests +- Implementation steps for each test script with sample code structures +- Verification steps to ensure converted tests work correctly + +### 2.3 Documentation Consolidation Plan + +The [Documentation Consolidation Plan](documentation-consolidation-plan.md) outlines: + +- Analysis of current documentation files and issues +- Strategy for consolidating documentation into a clear, hierarchical structure +- Content consolidation approach for each topic area +- Implementation steps and verification process + +## 3. Implementation Approach + +The implementation will follow a phased approach: + +### 3.1 Phase 1: Directory Structure and Test Organization + +1. Consolidate api-gateway module directory structure + - Merge Application.kt and module.kt + - Move unique files from src/main to src/jvmMain + - Update references + - Remove src/main directory + +2. Organize test files + - Move standalone test scripts to appropriate test directories + - Convert scripts to proper unit tests + - Ensure consistent test naming and organization + +### 3.2 Phase 2: Documentation and Code Cleanup + +1. Consolidate and update documentation + - Create new directory structure in docs directory + - Consolidate content from existing files + - Update main README.md + - Remove redundant documentation files + +2. Clean up code + - Remove unused or redundant code + - Standardize naming conventions + - Improve separation of concerns + +### 3.3 Phase 3: Verification + +1. Build the project to ensure there are no compilation errors +2. Run tests to verify functionality +3. Review documentation for accuracy and completeness +4. Final check against requirements in the issue description + +## 4. Benefits + +Implementing these cleanup tasks will result in: + +1. **Improved Maintainability**: Consistent directory structure, better organized tests, and clear documentation make the codebase easier to maintain +2. **Enhanced Readability**: Standardized naming conventions and improved separation of concerns make the code easier to understand +3. **Better Developer Experience**: Consolidated documentation and proper test organization improve the developer experience +4. **Reduced Technical Debt**: Removing redundant code and fixing inconsistencies reduces technical debt +5. **Easier Onboarding**: Clear structure and documentation make it easier for new developers to understand the project + +## 5. Next Steps + +1. Review and approve the implementation plans +2. Prioritize tasks based on impact and dependencies +3. Begin implementation following the phased approach +4. Regularly verify changes to ensure they meet requirements +5. Update this summary as implementation progresses + +## Last Updated + +2025-07-21 diff --git a/docker-compose.yml b/docker-compose.yml index ac7f8305..f446810b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -42,29 +42,29 @@ services: ports: # Nur bei Bedarf freigeben, z.B. für lokalen Zugriff - "127.0.0.1:54321:5432" # Host-Port 54321 → Container-Port 5432 - # Optional: PgAdmin Service -# pgadmin: -# image: dpage/pgadmin4:latest # Oder spezifische Version -# container_name: meldestelle-pgadmin -# restart: unless-stopped -# environment: -# # Werte aus .env lesen (oder Defaults nutzen) -# PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-admin@example.com} -# PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-DeinSicheresPgAdminPasswort!} # UNBEDINGT IN .env SETZEN! -# PGADMIN_CONFIG_SERVER_MODE: 'False' -# volumes: -# - pgadmin_data:/var/lib/pgadmin # Benanntes Volume -# ports: -# # Port 5050 auf dem Host (nur localhost) → Port 80 im Container -# - "${PGADMIN_PORT:-127.0.0.1:5050}:80" -# networks: -# - meldestelle-net # <--- Muss zum Netzwerk-Namen passen -# depends_on: # PgAdmin braucht die DB -# - db + # PgAdmin Service + pgadmin: + image: dpage/pgadmin4:latest # Oder spezifische Version + container_name: meldestelle-pgadmin + restart: unless-stopped + environment: + # Werte aus .env lesen (oder Defaults nutzen) + PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-admin@example.com} + PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin_password_change_me} # UNBEDINGT IN .env SETZEN! + PGADMIN_CONFIG_SERVER_MODE: 'False' + volumes: + - pgadmin_data:/var/lib/pgadmin # Benanntes Volume + ports: + # Port 5050 auf dem Host (nur localhost) → Port 80 im Container + - "${PGADMIN_PORT:-127.0.0.1:5050}:80" + networks: + - meldestelle-net # <--- Muss zum Netzwerk-Namen passen + depends_on: # PgAdmin braucht die DB + - db networks: meldestelle-net: driver: bridge volumes: postgres_data: # <--- Konsistenter Name -# pgadmin_data: # <--- Konsistenter Name + pgadmin_data: # <--- Konsistenter Name diff --git a/documentation-consolidation-plan.md b/documentation-consolidation-plan.md new file mode 100644 index 00000000..89fbb8a5 --- /dev/null +++ b/documentation-consolidation-plan.md @@ -0,0 +1,180 @@ +# Documentation Consolidation Plan + +This document outlines the plan for consolidating and updating the documentation in the project to improve clarity, accuracy, and maintainability. + +## 1. Documentation Analysis + +### 1.1 Current Documentation Files + +The project contains numerous documentation files, many of which appear to be redundant or fragmented: + +#### Root Directory Documentation +- API_VALIDATION_IMPLEMENTATION.md +- AUTHENTICATION_AUTHORIZATION_IMPLEMENTATION_SUMMARY.md +- AUTHENTICATION_AUTHORIZATION_SUMMARY.md +- CLEANUP_IMPLEMENTATION_PLAN.md +- CLIENT_VALIDATION_IMPLEMENTATION.md +- DATABASE_INSTALLATION_COMPLETED.md +- DATABASE_SETUP_FIXES.md +- README_API_Implementation.md +- README_CODE_ORGANIZATION.md +- README_CONFIG.md +- README_DATABASE_SETUP.md +- README.md +- fixes_implemented.md +- issues_found.md + +#### Docs Directory Documentation +- API_Documentation.md +- API_DOCUMENTATION.md (duplicate with different case) +- API_IMPLEMENTATION_SUMMARY.md +- API_VERSIONING.md +- bounded-contexts-design.md +- module-structure-design.md +- scs-implementation-completed.md +- scs-implementation-summary.md +- SWAGGER_DOCUMENTATION.md +- Various diagram files + +### 1.2 Documentation Issues + +Based on initial examination, the following issues have been identified: + +1. **Outdated Content**: Some documentation (e.g., README_CODE_ORGANIZATION.md) refers to paths and patterns that don't match the current codebase structure. +2. **Fragmentation**: Related information is spread across multiple files (e.g., multiple files about API implementation). +3. **Redundancy**: Some topics are covered in multiple files with overlapping content. +4. **Inconsistent Naming**: Inconsistent file naming conventions (e.g., mix of uppercase, lowercase, and snake_case). +5. **Lack of Organization**: Documentation is scattered between the root directory and the docs directory. + +## 2. Consolidation Strategy + +### 2.1 Documentation Structure + +Consolidate documentation into a clear, hierarchical structure in the docs directory: + +``` +docs/ +├── architecture/ +│ ├── bounded-contexts.md +│ ├── module-structure.md +│ └── system-overview.md +├── api/ +│ ├── implementation.md +│ ├── validation.md +│ └── versioning.md +├── database/ +│ ├── setup.md +│ └── migrations.md +├── security/ +│ ├── authentication.md +│ └── authorization.md +├── development/ +│ ├── getting-started.md +│ ├── code-organization.md +│ └── testing.md +├── diagrams/ +│ └── [existing diagram files] +└── README.md (index document) +``` + +### 2.2 Content Consolidation + +1. **Architecture Documentation**: + - Consolidate bounded-contexts-design.md and module-structure-design.md into the architecture directory + - Create a new system-overview.md that provides a high-level overview of the system + +2. **API Documentation**: + - Merge API_Documentation.md, API_DOCUMENTATION.md, API_IMPLEMENTATION_SUMMARY.md, and README_API_Implementation.md into api/implementation.md + - Move API_VALIDATION_IMPLEMENTATION.md and CLIENT_VALIDATION_IMPLEMENTATION.md to api/validation.md + - Move API_VERSIONING.md to api/versioning.md + +3. **Database Documentation**: + - Merge DATABASE_INSTALLATION_COMPLETED.md, DATABASE_SETUP_FIXES.md, and README_DATABASE_SETUP.md into database/setup.md + - Create database/migrations.md for database migration information + +4. **Security Documentation**: + - Merge AUTHENTICATION_AUTHORIZATION_IMPLEMENTATION_SUMMARY.md and AUTHENTICATION_AUTHORIZATION_SUMMARY.md into security/authentication.md and security/authorization.md + +5. **Development Documentation**: + - Create development/getting-started.md with setup instructions + - Update and move README_CODE_ORGANIZATION.md to development/code-organization.md + - Create development/testing.md with testing guidelines + +6. **Main README.md**: + - Update to provide a clear overview of the project + - Include links to the detailed documentation in the docs directory + - Keep it concise and focused on getting started quickly + +### 2.3 Content Updates + +For each consolidated document: + +1. **Verify Accuracy**: + - Check that all information is current and accurate + - Update any outdated references to file paths, class names, etc. + - Ensure examples reflect the current codebase + +2. **Improve Clarity**: + - Use consistent terminology throughout + - Add explanatory diagrams where helpful + - Include code examples for common tasks + +3. **Ensure Completeness**: + - Cover all important aspects of each topic + - Include troubleshooting sections for common issues + - Add references to related documentation + +## 3. Implementation Steps + +### 3.1 Create New Directory Structure + +1. Create the new directory structure in the docs directory +2. Create placeholder files for each new document + +### 3.2 Consolidate Content + +For each topic area: + +1. Review all related existing documentation +2. Extract relevant, current information +3. Organize into the new document structure +4. Update references, examples, and paths +5. Add missing information as needed + +### 3.3 Update Main README.md + +1. Create a new version of README.md that: + - Provides a clear project overview + - Explains the project structure + - Includes quick start instructions + - Links to detailed documentation + +### 3.4 Remove Redundant Files + +After consolidation is complete and verified: + +1. Create a list of files to be removed +2. Verify that all valuable content has been preserved +3. Remove the redundant files + +## 4. Verification + +After consolidation: + +1. Review all new documentation for accuracy and completeness +2. Verify that all links between documents work correctly +3. Check that code examples are correct and up-to-date +4. Ensure the documentation accurately reflects the current codebase + +## 5. Future Maintenance + +Establish guidelines for future documentation: + +1. **Single Source of Truth**: Each topic should be documented in exactly one place +2. **Consistent Structure**: Follow the established directory structure +3. **Regular Updates**: Documentation should be updated whenever related code changes +4. **Clear Ownership**: Assign responsibility for maintaining each section of documentation + +## Last Updated + +2025-07-21 diff --git a/horse-registry/build.gradle.kts b/horse-registry/build.gradle.kts index e2b9681d..59562296 100644 --- a/horse-registry/build.gradle.kts +++ b/horse-registry/build.gradle.kts @@ -39,11 +39,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")) } } } diff --git a/horse-registry/src/jsMain/kotlin/Main.kt b/horse-registry/src/jsMain/kotlin/Main.kt new file mode 100644 index 00000000..608a0277 --- /dev/null +++ b/horse-registry/src/jsMain/kotlin/Main.kt @@ -0,0 +1,24 @@ +import at.mocode.horses.ui.components.PferdeListe +import react.create + +/** + * Main entry point for the Horse Registry JavaScript build. + * + * This function serves as the entry point for the Kotlin/JS application. + * It registers the React component as a web component using r2wc. + */ +fun main() { + console.log("Horse Registry 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 React component to Web Component using r2wc + val PferdeListeWebComponent = r2wc(PferdeListe, js("{}")) + + // Register the new component with a custom HTML tag + js("customElements.define('pferde-liste', arguments[0])")(PferdeListeWebComponent) + + console.log("Web component 'pferde-liste' registered successfully!") + console.log("You can now use in your HTML") +} diff --git a/horse-registry/src/jsMain/kotlin/at/mocode/horses/ui/components/PferdeListe.kt b/horse-registry/src/jsMain/kotlin/at/mocode/horses/ui/components/PferdeListe.kt new file mode 100644 index 00000000..c2a48b59 --- /dev/null +++ b/horse-registry/src/jsMain/kotlin/at/mocode/horses/ui/components/PferdeListe.kt @@ -0,0 +1,308 @@ +package at.mocode.horses.ui.components + +import at.mocode.horses.domain.model.DomPferd +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 PferdeListe component + */ +external interface PferdeListeProps : Props + +// Create Ktor client for API calls +private val apiClient = HttpClient { + install(ContentNegotiation) { + json(Json { + ignoreUnknownKeys = true + }) + } +} + +/** + * React component that displays a list of horses (Pferde). + * + * This component loads horse data from the API and renders it as HTML. + * Uses useState for state management and useEffectOnce for data loading. + */ +val PferdeListe = FC { _ -> + // State management with useState + var horses by useState>(emptyList()) + var loading by useState(true) + var error by useState(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/horses") + val loadedHorses: List = response.body() + horses = loadedHorses + } catch (e: Exception) { + error = "Fehler beim Laden der Pferde: ${e.message}" + console.error("Error loading horses:", 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" + } + +"Pferde-Register" + } + + when { + loading -> { + div { + css { + "padding" to "20px" + "textAlign" to "center" + "color" to "#666" + "fontSize" to "18px" + } + +"Lade Pferde..." + } + } + 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!! + } + } + horses.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 Pferde verfügbar" + } + } + else -> { + div { + css { + "display" to "grid" + "gridTemplateColumns" to "repeat(auto-fill, minmax(300px, 1fr))" + "gap" to "20px" + } + horses.forEach { horse -> + 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" + } + +horse.getDisplayName() + } + + // Basic information + p { + span { + +"🐎" + } + +" Geschlecht: ${horse.geschlecht.name}" + } + + horse.geburtsdatum?.let { birthDate -> + p { + span { + +"📅" + } + +" Geburtsdatum: $birthDate" + horse.getAge()?.let { age -> + +" (${age} Jahre alt)" + } + } + } + + horse.rasse?.let { breed -> + p { + span { + +"🏇" + } + +" Rasse: $breed" + } + } + + horse.farbe?.let { color -> + p { + span { + +"🎨" + } + +" Farbe: $color" + } + } + + horse.stockmass?.let { height -> + p { + span { + +"📏" + } + +" Stockmaß: ${height} cm" + } + } + + // Identification numbers + val identificationNumbers = mutableListOf() + horse.lebensnummer?.let { identificationNumbers.add("Lebensnummer: $it") } + horse.chipNummer?.let { identificationNumbers.add("Chip: $it") } + horse.passNummer?.let { identificationNumbers.add("Pass: $it") } + horse.oepsNummer?.let { identificationNumbers.add("OEPS: $it") } + horse.feiNummer?.let { identificationNumbers.add("FEI: $it") } + + if (identificationNumbers.isNotEmpty()) { + p { + span { + +"🆔" + } + +" Identifikation: ${identificationNumbers.joinToString(", ")}" + } + } + + // Pedigree information + val pedigreeInfo = mutableListOf() + horse.vaterName?.let { pedigreeInfo.add("Vater: $it") } + horse.mutterName?.let { pedigreeInfo.add("Mutter: $it") } + horse.mutterVaterName?.let { pedigreeInfo.add("Muttervater: $it") } + + if (pedigreeInfo.isNotEmpty()) { + p { + span { + +"🧬" + } + +" Abstammung: ${pedigreeInfo.joinToString(", ")}" + } + } + + // Breeding information + horse.zuechterName?.let { breeder -> + p { + span { + +"👨‍🌾" + } + +" Züchter: $breeder" + } + } + + horse.zuchtbuchNummer?.let { studbook -> + p { + span { + +"📖" + } + +" Zuchtbuchnummer: $studbook" + } + } + + // Status indicators + val statusList = mutableListOf() + if (horse.istAktiv) statusList.add("Aktiv") else statusList.add("Inaktiv") + if (horse.hasCompleteIdentification()) statusList.add("Vollständig identifiziert") + if (horse.isOepsRegistered()) statusList.add("OEPS registriert") + if (horse.isFeiRegistered()) statusList.add("FEI registriert") + + p { + span { + +"ℹ️" + } + +" Status: ${statusList.joinToString(", ")}" + } + + // Data source + p { + span { + +"📊" + } + +" Datenquelle: ${horse.datenQuelle.name}" + } + + // Notes + horse.bemerkungen?.let { notes -> + p { + span { + +"📝" + } + +" Bemerkungen: $notes" + } + } + + // Creation and update dates + p { + span { + +"📅" + } + +" Erstellt am: ${horse.createdAt}" + } + + p { + span { + +"🔄" + } + +" Zuletzt geändert: ${horse.updatedAt}" + } + } + } + } + } + } + } +} diff --git a/master-data/build.gradle.kts b/master-data/build.gradle.kts index 6e8fe5ea..76b2daf8 100644 --- a/master-data/build.gradle.kts +++ b/master-data/build.gradle.kts @@ -37,11 +37,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")) } } } diff --git a/master-data/src/jsMain/kotlin/Main.kt b/master-data/src/jsMain/kotlin/Main.kt new file mode 100644 index 00000000..78baf0c9 --- /dev/null +++ b/master-data/src/jsMain/kotlin/Main.kt @@ -0,0 +1,24 @@ +import at.mocode.masterdata.ui.components.StammdatenListe +import react.create + +/** + * Main entry point for the Master Data JavaScript build. + * + * This function serves as the entry point for the Kotlin/JS application. + * It registers the React component as a web component using r2wc. + */ +fun main() { + console.log("Master Data 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 React component to Web Component using r2wc + val StammdatenListeWebComponent = r2wc(StammdatenListe, js("{}")) + + // Register the new component with a custom HTML tag + js("customElements.define('stammdaten-liste', arguments[0])")(StammdatenListeWebComponent) + + console.log("Web component 'stammdaten-liste' registered successfully!") + console.log("You can now use in your HTML") +} diff --git a/master-data/src/jsMain/kotlin/at/mocode/masterdata/ui/components/StammdatenListe.kt b/master-data/src/jsMain/kotlin/at/mocode/masterdata/ui/components/StammdatenListe.kt new file mode 100644 index 00000000..ca01475e --- /dev/null +++ b/master-data/src/jsMain/kotlin/at/mocode/masterdata/ui/components/StammdatenListe.kt @@ -0,0 +1,257 @@ +package at.mocode.masterdata.ui.components + +import at.mocode.masterdata.domain.model.LandDefinition +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.h2 +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 StammdatenListe component + */ +external interface StammdatenListeProps : Props + +// Create Ktor client for API calls +private val apiClient = HttpClient { + install(ContentNegotiation) { + json(Json { + ignoreUnknownKeys = true + }) + } +} + +/** + * React component that displays master data (Stammdaten). + * + * This component loads master data from the API and renders it as HTML. + * Currently focuses on countries (LandDefinition) but can be extended for other master data types. + * Uses useState for state management and useEffectOnce for data loading. + */ +val StammdatenListe = FC { _ -> + // State management with useState + var countries by useState>(emptyList()) + var loading by useState(true) + var error by useState(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/masterdata/countries") + val loadedCountries: List = response.body() + countries = loadedCountries + } catch (e: Exception) { + error = "Fehler beim Laden der Stammdaten: ${e.message}" + console.error("Error loading master data:", 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" + } + +"Stammdaten" + } + + h2 { + css { + "color" to "#34495e" + "marginTop" to "20px" + "marginBottom" to "15px" + "fontSize" to "1.5em" + } + +"Länder" + } + + when { + loading -> { + div { + css { + "padding" to "20px" + "textAlign" to "center" + "color" to "#666" + "fontSize" to "18px" + } + +"Lade Stammdaten..." + } + } + 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!! + } + } + countries.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 Länder verfügbar" + } + } + else -> { + div { + css { + "display" to "grid" + "gridTemplateColumns" to "repeat(auto-fill, minmax(300px, 1fr))" + "gap" to "20px" + } + countries.forEach { country -> + 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" + } + +country.nameDeutsch + } + + // ISO codes + p { + span { + +"🌍" + } + +" ISO-Codes: ${country.isoAlpha2Code} / ${country.isoAlpha3Code}" + country.isoNumerischerCode?.let { numCode -> + +" / $numCode" + } + } + + // English name if available + country.nameEnglisch?.let { englishName -> + p { + span { + +"🇬🇧" + } + +" Englischer Name: $englishName" + } + } + + // EU/EWR membership + val membershipInfo = mutableListOf() + country.istEuMitglied?.let { isEuMember -> + if (isEuMember) membershipInfo.add("EU-Mitglied") + } + country.istEwrMitglied?.let { isEwrMember -> + if (isEwrMember) membershipInfo.add("EWR-Mitglied") + } + + if (membershipInfo.isNotEmpty()) { + p { + span { + +"🇪🇺" + } + +" Mitgliedschaft: ${membershipInfo.joinToString(", ")}" + } + } + + // Status + p { + span { + +"ℹ️" + } + +" Status: ${if (country.istAktiv) "Aktiv" else "Inaktiv"}" + } + + // Sort order if available + country.sortierReihenfolge?.let { sortOrder -> + p { + span { + +"🔢" + } + +" Sortierreihenfolge: $sortOrder" + } + } + + // Coat of arms/flag URL if available + country.wappenUrl?.let { flagUrl -> + p { + span { + +"🏴" + } + +" Wappen/Flagge: $flagUrl" + } + } + + // Creation and update dates + p { + span { + +"📅" + } + +" Erstellt am: ${country.createdAt}" + } + + p { + span { + +"🔄" + } + +" Zuletzt geändert: ${country.updatedAt}" + } + } + } + } + } + } + } +} diff --git a/member-management/build.gradle.kts b/member-management/build.gradle.kts index d9f35430..5b1ff9be 100644 --- a/member-management/build.gradle.kts +++ b/member-management/build.gradle.kts @@ -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")) } } } diff --git a/member-management/src/jsMain/kotlin/Main.kt b/member-management/src/jsMain/kotlin/Main.kt new file mode 100644 index 00000000..e82fcc7a --- /dev/null +++ b/member-management/src/jsMain/kotlin/Main.kt @@ -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 and in your HTML") +} diff --git a/member-management/src/jsMain/kotlin/at/mocode/members/ui/components/LoginForm.kt b/member-management/src/jsMain/kotlin/at/mocode/members/ui/components/LoginForm.kt new file mode 100644 index 00000000..a2e09e99 --- /dev/null +++ b/member-management/src/jsMain/kotlin/at/mocode/members/ui/components/LoginForm.kt @@ -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 { props -> + // State management with useState + var username by useState("") + var password by useState("") + var validationErrors by useState>(emptyList()) + var serverError by useState(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" + } + } + } + } +} diff --git a/member-management/src/jsMain/kotlin/at/mocode/members/ui/components/MitgliederListe.kt b/member-management/src/jsMain/kotlin/at/mocode/members/ui/components/MitgliederListe.kt new file mode 100644 index 00000000..7ccde04c --- /dev/null +++ b/member-management/src/jsMain/kotlin/at/mocode/members/ui/components/MitgliederListe.kt @@ -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 { _ -> + // State management with useState + var members by useState>(emptyList()) + var loading by useState(true) + var error by useState(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 = 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() + 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}" + } + } + } + } + } + } + } +} diff --git a/member-management/src/jvmTest/kotlin/at/mocode/members/test/AuthenticationTest.kt b/member-management/src/jvmTest/kotlin/at/mocode/members/test/AuthenticationTest.kt new file mode 100644 index 00000000..7e11e8d3 --- /dev/null +++ b/member-management/src/jvmTest/kotlin/at/mocode/members/test/AuthenticationTest.kt @@ -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 + } + } +} diff --git a/shared-kernel/build.gradle.kts b/shared-kernel/build.gradle.kts index 5b75bbd6..9b51b50d 100644 --- a/shared-kernel/build.gradle.kts +++ b/shared-kernel/build.gradle.kts @@ -32,6 +32,24 @@ kotlin { implementation(libs.postgresql.driver) } + jvmTest.dependencies { + // Ktor server dependencies + implementation(libs.ktor.server.core) + implementation(libs.ktor.server.netty) + implementation(libs.ktor.server.tests) + + // H2 database for testing + implementation(libs.h2.driver) + + // Dependencies on other modules + implementation(project(":api-gateway")) + implementation(project(":master-data")) + implementation(project(":event-management")) + + // Coroutines testing + implementation(libs.kotlinx.coroutines.test) + } + jsMain.dependencies { // Kotlin React dependencies with explicit stable versions (for shared components) implementation("org.jetbrains.kotlin-wrappers:kotlin-react:18.2.0-pre.467") diff --git a/shared-kernel/src/jvmTest/kotlin/at/mocode/validation/test/ValidationTest.kt b/shared-kernel/src/jvmTest/kotlin/at/mocode/validation/test/ValidationTest.kt new file mode 100644 index 00000000..362fd211 --- /dev/null +++ b/shared-kernel/src/jvmTest/kotlin/at/mocode/validation/test/ValidationTest.kt @@ -0,0 +1,105 @@ +package at.mocode.validation.test + +import at.mocode.validation.ApiValidationUtils +import at.mocode.validation.ValidationError +import kotlin.test.Test +import kotlin.test.assertTrue +import kotlin.test.assertFalse +import kotlinx.datetime.LocalDate + +/** + * Test class for API validation utilities. + * + * This test verifies that the validation implementation works correctly + * for all API endpoints. + */ +class ValidationTest { + + @Test + fun testQueryParameterValidation() { + // Test valid parameters + val validErrors = ApiValidationUtils.validateQueryParameters( + limit = "50", + offset = "0", + search = "test" + ) + assertTrue(ApiValidationUtils.isValid(validErrors), "Valid query parameters should pass validation") + + // Test invalid limit + val invalidLimitErrors = ApiValidationUtils.validateQueryParameters( + limit = "invalid" + ) + assertFalse(ApiValidationUtils.isValid(invalidLimitErrors), "Invalid limit parameter should fail validation") + + // Test limit out of range + val outOfRangeLimitErrors = ApiValidationUtils.validateQueryParameters( + limit = "2000" + ) + assertFalse(ApiValidationUtils.isValid(outOfRangeLimitErrors), "Out of range limit should fail validation") + + // Test invalid offset + val invalidOffsetErrors = ApiValidationUtils.validateQueryParameters( + offset = "-1" + ) + assertFalse(ApiValidationUtils.isValid(invalidOffsetErrors), "Invalid offset parameter should fail validation") + } + + @Test + fun testLoginRequestValidation() { + // Test valid login + val validErrors = ApiValidationUtils.validateLoginRequest("user@example.com", "password123") + assertTrue(ApiValidationUtils.isValid(validErrors), "Valid login request should pass validation") + + // Test missing username + val missingUsernameErrors = ApiValidationUtils.validateLoginRequest(null, "password123") + assertFalse(ApiValidationUtils.isValid(missingUsernameErrors), "Missing username should fail validation") + + // Test missing password + val missingPasswordErrors = ApiValidationUtils.validateLoginRequest("user@example.com", null) + assertFalse(ApiValidationUtils.isValid(missingPasswordErrors), "Missing password should fail validation") + } + + @Test + fun testCountryRequestValidation() { + // Test valid country request + val validErrors = ApiValidationUtils.validateCountryRequest("AT", "AUT", "Österreich", "Austria") + assertTrue(ApiValidationUtils.isValid(validErrors), "Valid country request should pass validation") + + // Test missing required fields + val missingFieldsErrors = ApiValidationUtils.validateCountryRequest(null, null, null, null) + assertFalse(ApiValidationUtils.isValid(missingFieldsErrors), "Missing required fields should fail validation") + + // Test invalid ISO codes + val invalidIsoErrors = ApiValidationUtils.validateCountryRequest("INVALID", "INVALID", "Test", "Test") + assertFalse(ApiValidationUtils.isValid(invalidIsoErrors), "Invalid ISO codes should fail validation") + } + + @Test + fun testHorseRequestValidation() { + // Test valid horse request + val validErrors = ApiValidationUtils.validateHorseRequest("Thunder", "123456789", "987654321", "OEPS123", "FEI456") + assertTrue(ApiValidationUtils.isValid(validErrors), "Valid horse request should pass validation") + + // Test missing horse name + val missingNameErrors = ApiValidationUtils.validateHorseRequest(null, "123456789", "987654321", "OEPS123", "FEI456") + assertFalse(ApiValidationUtils.isValid(missingNameErrors), "Missing horse name should fail validation") + } + + @Test + fun testEventRequestValidation() { + val startDate = LocalDate(2024, 6, 1) + val endDate = LocalDate(2024, 6, 3) + + // Test valid event request + val validErrors = ApiValidationUtils.validateEventRequest("Test Event", "Vienna", startDate, endDate, 100) + assertTrue(ApiValidationUtils.isValid(validErrors), "Valid event request should pass validation") + + // Test missing event name + val missingNameErrors = ApiValidationUtils.validateEventRequest(null, "Vienna", startDate, endDate, 100) + assertFalse(ApiValidationUtils.isValid(missingNameErrors), "Missing event name should fail validation") + + // Test invalid date range (end before start) + val invalidDateErrors = ApiValidationUtils.validateEventRequest("Test Event", "Vienna", endDate, startDate, 100) + assertFalse(ApiValidationUtils.isValid(invalidDateErrors), "Invalid date range should fail validation") + } +} diff --git a/test-scripts-conversion-plan.md b/test-scripts-conversion-plan.md new file mode 100644 index 00000000..2765ca39 --- /dev/null +++ b/test-scripts-conversion-plan.md @@ -0,0 +1,309 @@ +# Test Scripts Conversion Plan + +This document outlines the plan for moving standalone test scripts from the root directory to appropriate test directories and converting them to proper unit tests. + +## 1. Standalone Test Scripts + +The following standalone test scripts have been identified in the root directory: + +| File | Target Directory | Test Class Name | +|------|-----------------|-----------------| +| test_authentication.kt | member-management/src/jvmTest/kotlin/at/mocode/members/test/ | AuthenticationTest | +| test_authentication_authorization.kt | api-gateway/src/jvmTest/kotlin/at/mocode/gateway/test/ | AuthenticationAuthorizationTest | +| test_validation.kt | shared-kernel/src/jvmTest/kotlin/at/mocode/validation/test/ | ValidationTest | +| database-integration-test.kt | shared-kernel/src/jvmTest/kotlin/at/mocode/shared/database/test/ | DatabaseIntegrationTest | + +## 2. Conversion Guidelines + +When converting the standalone scripts to proper unit tests, the following guidelines should be followed: + +1. **Add proper test annotations**: + - Use `@Test` for test methods + - Use `@BeforeTest` for setup methods + - Use `@AfterTest` for teardown methods + +2. **Organize tests into test classes**: + - Create a test class with a descriptive name + - Group related tests into methods within the class + - Use descriptive method names that explain what is being tested + +3. **Use proper assertions**: + - Replace `println` statements with proper assertions + - Use `kotlin.test.assertEquals`, `kotlin.test.assertTrue`, etc. + - Add meaningful error messages to assertions + +4. **Set up test dependencies properly**: + - Initialize dependencies in setup methods + - Use mocks or test doubles where appropriate + - Clean up resources in teardown methods + +5. **Add proper package declarations**: + - Use the package that corresponds to the target directory + +## 3. Implementation Steps + +### 3.1 test_authentication.kt → AuthenticationTest + +1. Create the target directory if it doesn't exist +2. Create a new file AuthenticationTest.kt with the following structure: + ```kotlin + 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 kotlin.test.* + + class AuthenticationTest { + private lateinit var userRepository: UserRepositoryImpl + private lateinit var userAuthorizationService: UserAuthorizationService + private lateinit var jwtService: JwtService + + @BeforeTest + fun setup() { + userRepository = UserRepositoryImpl() + val personRolleRepository = PersonRolleRepositoryImpl() + val rolleRepository = RolleRepositoryImpl() + val rolleBerechtigungRepository = RolleBerechtigungRepositoryImpl() + val berechtigungRepository = BerechtigungRepositoryImpl() + + userAuthorizationService = UserAuthorizationService( + userRepository, + personRolleRepository, + rolleRepository, + rolleBerechtigungRepository, + berechtigungRepository + ) + + jwtService = JwtService(userAuthorizationService) + } + + @Test + fun testUserAuthInfo() { + val testUsers = userRepository.getAllUsers() + assertNotEquals(0, testUsers.size, "Should have at least one test user") + + if (testUsers.isNotEmpty()) { + val testUser = testUsers.first() + val authInfo = userAuthorizationService.getUserAuthInfo(testUser.userId) + assertNotNull(authInfo, "Auth info should not be null") + } + } + + @Test + fun testJwtTokenGeneration() { + val testUsers = userRepository.getAllUsers() + if (testUsers.isNotEmpty()) { + val testUser = testUsers.first() + val tokenInfo = jwtService.generateToken(testUser) + assertNotNull(tokenInfo.token, "Token should not be null") + assertNotNull(tokenInfo.expiresAt, "Expiration date should not be null") + } + } + + @Test + fun testTokenValidation() { + val testUsers = userRepository.getAllUsers() + if (testUsers.isNotEmpty()) { + val testUser = testUsers.first() + val tokenInfo = jwtService.generateToken(testUser) + val payload = jwtService.validateToken(tokenInfo.token) + assertNotNull(payload, "Payload should not be null") + } + } + } + ``` + +### 3.2 test_authentication_authorization.kt → AuthenticationAuthorizationTest + +1. Create the target directory if it doesn't exist +2. Create a new file AuthenticationAuthorizationTest.kt with a similar structure to AuthenticationTest.kt +3. Convert the main function to test methods with proper assertions +4. Add setup and teardown methods as needed + +### 3.3 test_validation.kt → ValidationTest + +1. Create the target directory if it doesn't exist +2. Create a new file ValidationTest.kt with a similar structure +3. Convert the main function to test methods with proper assertions +4. Add setup and teardown methods as needed + +### 3.4 database-integration-test.kt → DatabaseIntegrationTest + +1. Create the target directory if it doesn't exist +2. Create a new file DatabaseIntegrationTest.kt with the following structure: + ```kotlin + package at.mocode.shared.database.test + + import at.mocode.gateway.config.configureDatabase + import at.mocode.masterdata.domain.model.LandDefinition + import at.mocode.masterdata.infrastructure.repository.LandRepositoryImpl + import at.mocode.events.domain.model.Veranstaltung + import at.mocode.events.infrastructure.repository.VeranstaltungRepositoryImpl + import at.mocode.enums.SparteE + import com.benasher44.uuid.uuid4 + import io.ktor.server.application.* + import io.ktor.server.engine.* + import io.ktor.server.netty.* + import kotlinx.coroutines.runBlocking + import kotlinx.datetime.Clock + import kotlinx.datetime.LocalDate + import org.jetbrains.exposed.sql.transactions.transaction + import kotlin.test.* + + class DatabaseIntegrationTest { + private lateinit var application: Application + private lateinit var landRepository: LandRepositoryImpl + private lateinit var eventRepository: VeranstaltungRepositoryImpl + + @BeforeTest + fun setup() { + val environment = applicationEngineEnvironment { + config = MapApplicationConfig( + "database.url" to "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE", + "database.user" to "sa", + "database.password" to "" + ) + } + + application = Application(environment) + application.configureDatabase() + + landRepository = LandRepositoryImpl() + eventRepository = VeranstaltungRepositoryImpl() + } + + @Test + fun testMasterDataRepository() = runBlocking { + transaction { + // Create a test country + val testCountry = LandDefinition( + landId = uuid4(), + isoAlpha2Code = "TS", + isoAlpha3Code = "TST", + isoNumerischerCode = "999", + nameDeutsch = "Testland", + nameEnglisch = "Testland", + wappenUrl = null, + istEuMitglied = false, + istEwrMitglied = false, + istAktiv = true, + sortierReihenfolge = 999, + createdAt = Clock.System.now(), + updatedAt = Clock.System.now() + ) + + // Save the test country + val savedCountry = landRepository.save(testCountry) + assertEquals("Testland", savedCountry.nameDeutsch, "Country name should match") + + // Retrieve the test country + val retrievedCountry = landRepository.findByIsoAlpha2Code("TS") + assertNotNull(retrievedCountry, "Retrieved country should not be null") + assertEquals("Testland", retrievedCountry.nameDeutsch, "Retrieved country name should match") + + // Count active countries + val activeCount = landRepository.countActive() + assertTrue(activeCount > 0, "Should have at least one active country") + + // Clean up + landRepository.delete(testCountry.landId) + } + } + + @Test + fun testEventManagementRepository() = runBlocking { + transaction { + // Create a test event + val testEvent = Veranstaltung( + name = "Test Veranstaltung", + beschreibung = "Eine Test-Veranstaltung für die Integration", + startDatum = LocalDate(2024, 8, 15), + endDatum = LocalDate(2024, 8, 17), + ort = "Test-Ort", + veranstalterVereinId = uuid4(), + sparten = listOf(SparteE.DRESSUR, SparteE.SPRINGEN), + istAktiv = true, + istOeffentlich = true, + maxTeilnehmer = 100, + anmeldeschluss = LocalDate(2024, 8, 1) + ) + + // Save the test event + val savedEvent = eventRepository.save(testEvent) + assertEquals("Test Veranstaltung", savedEvent.name, "Event name should match") + + // Retrieve the test event + val retrievedEvent = eventRepository.findById(savedEvent.veranstaltungId) + assertNotNull(retrievedEvent, "Retrieved event should not be null") + assertEquals("Test Veranstaltung", retrievedEvent.name, "Retrieved event name should match") + assertEquals(3, retrievedEvent.getDurationInDays(), "Event duration should be 3 days") + assertTrue(retrievedEvent.isMultiDay(), "Event should be multi-day") + + // Test search functionality + val searchResults = eventRepository.findByName("Test", 10) + assertTrue(searchResults.isNotEmpty(), "Search should return at least one result") + + // Test public events + val publicEvents = eventRepository.findPublicEvents(true) + assertTrue(publicEvents.isNotEmpty(), "Should have at least one public event") + + // Count active events + val activeEventCount = eventRepository.countActive() + assertTrue(activeEventCount > 0, "Should have at least one active event") + + // Clean up event + eventRepository.delete(savedEvent.veranstaltungId) + } + } + } + + /** + * Simple map-based application config for testing + */ + class MapApplicationConfig(private val map: Map) : ApplicationConfig { + constructor(vararg pairs: Pair) : this(pairs.toMap()) + + override fun property(path: String): ApplicationConfigValue { + return MapApplicationConfigValue(map[path]) + } + + override fun propertyOrNull(path: String): ApplicationConfigValue? { + return map[path]?.let { MapApplicationConfigValue(it) } + } + + override fun config(path: String): ApplicationConfig { + return this + } + + override fun configList(path: String): List { + return emptyList() + } + + override fun keys(): Set { + return map.keys + } + } + + class MapApplicationConfigValue(private val value: String?) : ApplicationConfigValue { + override fun getString(): String = value ?: "" + override fun getList(): List = value?.split(",") ?: emptyList() + } + ``` + +## 4. Verification + +After converting each test script: + +1. Build the project to ensure there are no compilation errors +2. Run the tests to ensure they pass +3. Verify that the tests provide the same coverage as the original scripts +4. Remove the original scripts from the root directory + +## 5. Documentation Update + +Update the project documentation to reflect the new test organization: + +1. Update README.md if it references the standalone test scripts +2. Update any other documentation that mentions the test scripts