(fix) Umbau zu SCS
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
<login-form onloginsuccess="handleLoginSuccess"></login-form>
|
||||
```
|
||||
|
||||
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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -222,3 +222,7 @@ val data = call.safeReceive<Artikel>() ?: 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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<UserRole>,
|
||||
val permissions: List<Permission>
|
||||
)
|
||||
|
||||
/**
|
||||
* 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<RolleE>::class)?.toList() ?: emptyList()
|
||||
val domainPermissions = getClaim("permissions", Array<BerechtigungE>::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<UserRole>): List<Permission> {
|
||||
val permissions = mutableSetOf<Permission>()
|
||||
|
||||
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<JWTPrincipal>()
|
||||
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<JWTPrincipal>()
|
||||
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<Unit, ApplicationCall>.userAuthContext: UserAuthContext?
|
||||
get() = call.principal<JWTPrincipal>()?.getUserAuthContext()
|
||||
|
||||
/**
|
||||
* Application call extension to check if user has specific role.
|
||||
*/
|
||||
fun ApplicationCall.hasRole(role: UserRole): Boolean {
|
||||
val authContext = principal<JWTPrincipal>()?.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<JWTPrincipal>()?.getUserAuthContext()
|
||||
return authContext?.permissions?.contains(permission) == true
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Throwable> { call, cause ->
|
||||
call.application.log.error("Unhandled exception", cause)
|
||||
call.respond(
|
||||
HttpStatusCode.InternalServerError,
|
||||
ApiResponse.error<Any>("Internal server error: ${cause.message}")
|
||||
)
|
||||
}
|
||||
|
||||
status(HttpStatusCode.NotFound) { call, status ->
|
||||
call.respond(
|
||||
status,
|
||||
ApiResponse.error<Any>("Endpoint not found: ${call.request.path()}")
|
||||
)
|
||||
}
|
||||
|
||||
status(HttpStatusCode.Unauthorized) { call, status ->
|
||||
call.respond(
|
||||
status,
|
||||
ApiResponse.error<Any>("Authentication required")
|
||||
)
|
||||
}
|
||||
|
||||
status(HttpStatusCode.Forbidden) { call, status ->
|
||||
call.respond(
|
||||
status,
|
||||
ApiResponse.error<Any>("Access forbidden")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
+276
@@ -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)
|
||||
@@ -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
|
||||
+20
-20
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <pferde-liste></pferde-liste> in your HTML")
|
||||
}
|
||||
@@ -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<PferdeListeProps> { _ ->
|
||||
// State management with useState
|
||||
var horses by useState<List<DomPferd>>(emptyList())
|
||||
var loading by useState(true)
|
||||
var error by useState<String?>(null)
|
||||
|
||||
// Data loading with useEffectOnce hook
|
||||
useEffectOnce {
|
||||
val scope = MainScope()
|
||||
scope.launch {
|
||||
try {
|
||||
loading = true
|
||||
error = null
|
||||
// Load data with Ktor client
|
||||
val response = apiClient.get("http://localhost:8080/api/horses")
|
||||
val loadedHorses: List<DomPferd> = 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<String>()
|
||||
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<String>()
|
||||
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<String>()
|
||||
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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <stammdaten-liste></stammdaten-liste> in your HTML")
|
||||
}
|
||||
@@ -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<StammdatenListeProps> { _ ->
|
||||
// State management with useState
|
||||
var countries by useState<List<LandDefinition>>(emptyList())
|
||||
var loading by useState(true)
|
||||
var error by useState<String?>(null)
|
||||
|
||||
// Data loading with useEffectOnce hook
|
||||
useEffectOnce {
|
||||
val scope = MainScope()
|
||||
scope.launch {
|
||||
try {
|
||||
loading = true
|
||||
error = null
|
||||
// Load data with Ktor client
|
||||
val response = apiClient.get("http://localhost:8080/api/masterdata/countries")
|
||||
val loadedCountries: List<LandDefinition> = 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<String>()
|
||||
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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,11 +38,22 @@ kotlin {
|
||||
jsMain.dependencies {
|
||||
// Kotlin React dependencies with explicit stable versions
|
||||
implementation("org.jetbrains.kotlin-wrappers:kotlin-react:18.2.0-pre.467")
|
||||
implementation("org.jetbrains.kotlin-wrappers:kotlin-react-dom:18.2.0-pre.467")
|
||||
implementation("org.jetbrains.kotlin-wrappers:kotlin-emotion:11.10.5-pre.467")
|
||||
|
||||
// Ktor client dependencies for API calls
|
||||
implementation(libs.ktor.client.core)
|
||||
implementation(libs.ktor.client.js)
|
||||
implementation(libs.ktor.client.contentNegotiation)
|
||||
implementation(libs.ktor.client.serializationKotlinxJson)
|
||||
|
||||
// Coroutines for async operations
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
|
||||
// NPM dependencies
|
||||
implementation(npm("react", "18.2.0"))
|
||||
implementation(npm("react-dom", "18.2.0"))
|
||||
implementation(npm("@r2wc/react-to-web-component", "2.0.4"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import at.mocode.members.ui.components.MitgliederListe
|
||||
import at.mocode.members.ui.components.LoginForm
|
||||
import react.create
|
||||
|
||||
/**
|
||||
* Main entry point for the Member Management JavaScript build.
|
||||
*
|
||||
* This function serves as the entry point for the Kotlin/JS application.
|
||||
* It registers the React components as web components using r2wc.
|
||||
*/
|
||||
fun main() {
|
||||
console.log("Member Management JS module loaded successfully!")
|
||||
|
||||
// Import r2wc function from @r2wc/react-to-web-component npm package
|
||||
val r2wc = js("require('@r2wc/react-to-web-component')")
|
||||
|
||||
// Convert MitgliederListe React component to Web Component using r2wc
|
||||
val MitgliederListeWebComponent = r2wc(MitgliederListe, js("{}"))
|
||||
|
||||
// Register the MitgliederListe component with a custom HTML tag
|
||||
js("customElements.define('mitglieder-liste', arguments[0])")(MitgliederListeWebComponent)
|
||||
|
||||
console.log("Web component 'mitglieder-liste' registered successfully!")
|
||||
|
||||
// Convert LoginForm React component to Web Component using r2wc
|
||||
// Define props configuration for the LoginForm component
|
||||
val loginFormProps = js("{}")
|
||||
js("loginFormProps.onLoginSuccess = { type: Function }")
|
||||
|
||||
val LoginFormWebComponent = r2wc(LoginForm, loginFormProps)
|
||||
|
||||
// Register the LoginForm component with a custom HTML tag
|
||||
js("customElements.define('login-form', arguments[0])")(LoginFormWebComponent)
|
||||
|
||||
console.log("Web component 'login-form' registered successfully!")
|
||||
console.log("You can now use <mitglieder-liste></mitglieder-liste> and <login-form></login-form> in your HTML")
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
package at.mocode.members.ui.components
|
||||
|
||||
import at.mocode.validation.ApiValidationUtils
|
||||
import at.mocode.validation.ValidationError
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import react.*
|
||||
import react.dom.html.InputType
|
||||
import react.dom.html.ReactHTML.button
|
||||
import react.dom.html.ReactHTML.div
|
||||
import react.dom.html.ReactHTML.form
|
||||
import react.dom.html.ReactHTML.h2
|
||||
import react.dom.html.ReactHTML.input
|
||||
import react.dom.html.ReactHTML.label
|
||||
import react.dom.html.ReactHTML.p
|
||||
import react.dom.html.ReactHTML.span
|
||||
import emotion.react.css
|
||||
|
||||
/**
|
||||
* Props for the LoginForm component
|
||||
*/
|
||||
external interface LoginFormProps : Props {
|
||||
var onLoginSuccess: (String) -> Unit
|
||||
}
|
||||
|
||||
/**
|
||||
* Request body for login API
|
||||
*/
|
||||
@Serializable
|
||||
private data class LoginRequest(
|
||||
val username: String,
|
||||
val password: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Response from login API
|
||||
*/
|
||||
@Serializable
|
||||
private data class LoginResponse(
|
||||
val token: String,
|
||||
val username: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Error response from API
|
||||
*/
|
||||
@Serializable
|
||||
private data class ErrorResponse(
|
||||
val message: String,
|
||||
val status: String
|
||||
)
|
||||
|
||||
// Create Ktor client for API calls
|
||||
private val apiClient = HttpClient {
|
||||
install(ContentNegotiation) {
|
||||
json(Json {
|
||||
ignoreUnknownKeys = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* React component that displays a login form with client-side validation.
|
||||
*
|
||||
* This component demonstrates how to use the existing validation utilities
|
||||
* for client-side validation before submitting the form to the server.
|
||||
*/
|
||||
val LoginForm = FC<LoginFormProps> { props ->
|
||||
// State management with useState
|
||||
var username by useState("")
|
||||
var password by useState("")
|
||||
var validationErrors by useState<List<ValidationError>>(emptyList())
|
||||
var serverError by useState<String?>(null)
|
||||
var isLoading by useState(false)
|
||||
|
||||
// Function to handle login
|
||||
val handleLogin = {
|
||||
// Clear previous errors
|
||||
validationErrors = emptyList()
|
||||
serverError = null
|
||||
|
||||
// Perform client-side validation
|
||||
val errors = ApiValidationUtils.validateLoginRequest(username, password)
|
||||
|
||||
if (errors.isNotEmpty()) {
|
||||
// If validation fails, update the validationErrors state
|
||||
validationErrors = errors
|
||||
} else {
|
||||
// If validation passes, submit the form
|
||||
isLoading = true
|
||||
|
||||
val scope = MainScope()
|
||||
scope.launch {
|
||||
try {
|
||||
val response = apiClient.post("http://localhost:8080/auth/login") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(LoginRequest(username, password))
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
val loginResponse: LoginResponse = response.body()
|
||||
props.onLoginSuccess(loginResponse.token)
|
||||
} else {
|
||||
val errorResponse: ErrorResponse = response.body()
|
||||
serverError = errorResponse.message
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
serverError = "Login failed: ${e.message}"
|
||||
console.error("Login error:", e)
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get validation error for a field
|
||||
val getFieldError = { fieldName: String ->
|
||||
validationErrors.find { it.field == fieldName }?.message
|
||||
}
|
||||
|
||||
// Render the form
|
||||
div {
|
||||
css {
|
||||
"maxWidth" to "400px"
|
||||
"margin" to "0 auto"
|
||||
"padding" to "20px"
|
||||
"backgroundColor" to "#f9f9f9"
|
||||
"borderRadius" to "8px"
|
||||
"boxShadow" to "0 2px 4px rgba(0,0,0,0.1)"
|
||||
}
|
||||
|
||||
h2 {
|
||||
css {
|
||||
"textAlign" to "center"
|
||||
"color" to "#2c3e50"
|
||||
"marginBottom" to "20px"
|
||||
}
|
||||
+"Login"
|
||||
}
|
||||
|
||||
// Display server error if any
|
||||
serverError?.let {
|
||||
div {
|
||||
css {
|
||||
"backgroundColor" to "#fdeaea"
|
||||
"color" to "#e74c3c"
|
||||
"padding" to "10px"
|
||||
"borderRadius" to "4px"
|
||||
"marginBottom" to "15px"
|
||||
"textAlign" to "center"
|
||||
}
|
||||
+it
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
// No onSubmit handler, using button click instead
|
||||
|
||||
// Username field
|
||||
div {
|
||||
css {
|
||||
"marginBottom" to "15px"
|
||||
}
|
||||
|
||||
label {
|
||||
css {
|
||||
"display" to "block"
|
||||
"marginBottom" to "5px"
|
||||
"fontWeight" to "bold"
|
||||
}
|
||||
htmlFor = "username"
|
||||
+"Username or Email"
|
||||
}
|
||||
|
||||
input {
|
||||
css {
|
||||
"width" to "100%"
|
||||
"padding" to "8px"
|
||||
"borderRadius" to "4px"
|
||||
"border" to if (getFieldError("username") != null) "1px solid #e74c3c" else "1px solid #ddd"
|
||||
}
|
||||
type = InputType.text
|
||||
id = "username"
|
||||
value = username
|
||||
onChange = { event -> username = event.target.value }
|
||||
disabled = isLoading
|
||||
required = true
|
||||
}
|
||||
|
||||
// Display validation error for username if any
|
||||
getFieldError("username")?.let {
|
||||
p {
|
||||
css {
|
||||
"color" to "#e74c3c"
|
||||
"fontSize" to "12px"
|
||||
"margin" to "5px 0 0 0"
|
||||
}
|
||||
+it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Password field
|
||||
div {
|
||||
css {
|
||||
"marginBottom" to "20px"
|
||||
}
|
||||
|
||||
label {
|
||||
css {
|
||||
"display" to "block"
|
||||
"marginBottom" to "5px"
|
||||
"fontWeight" to "bold"
|
||||
}
|
||||
htmlFor = "password"
|
||||
+"Password"
|
||||
}
|
||||
|
||||
input {
|
||||
css {
|
||||
"width" to "100%"
|
||||
"padding" to "8px"
|
||||
"borderRadius" to "4px"
|
||||
"border" to if (getFieldError("password") != null) "1px solid #e74c3c" else "1px solid #ddd"
|
||||
}
|
||||
type = InputType.password
|
||||
id = "password"
|
||||
value = password
|
||||
onChange = { event -> password = event.target.value }
|
||||
disabled = isLoading
|
||||
required = true
|
||||
}
|
||||
|
||||
// Display validation error for password if any
|
||||
getFieldError("password")?.let {
|
||||
p {
|
||||
css {
|
||||
"color" to "#e74c3c"
|
||||
"fontSize" to "12px"
|
||||
"margin" to "5px 0 0 0"
|
||||
}
|
||||
+it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Submit button
|
||||
button {
|
||||
css {
|
||||
"width" to "100%"
|
||||
"padding" to "10px"
|
||||
"backgroundColor" to "#3498db"
|
||||
"color" to "white"
|
||||
"border" to "none"
|
||||
"borderRadius" to "4px"
|
||||
"cursor" to if (isLoading) "not-allowed" else "pointer"
|
||||
"opacity" to if (isLoading) "0.7" else "1"
|
||||
"transition" to "background-color 0.3s"
|
||||
"hover" to {
|
||||
"backgroundColor" to if (!isLoading) "#2980b9" else "#3498db"
|
||||
}
|
||||
}
|
||||
type = react.dom.html.ButtonType.button
|
||||
disabled = isLoading
|
||||
onClick = { handleLogin() }
|
||||
|
||||
if (isLoading) {
|
||||
+"Logging in..."
|
||||
} else {
|
||||
+"Login"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
package at.mocode.members.ui.components
|
||||
|
||||
import at.mocode.members.domain.model.DomUser
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
import react.*
|
||||
import react.dom.html.ReactHTML.div
|
||||
import react.dom.html.ReactHTML.h1
|
||||
import react.dom.html.ReactHTML.h3
|
||||
import react.dom.html.ReactHTML.p
|
||||
import react.dom.html.ReactHTML.span
|
||||
import emotion.react.css
|
||||
|
||||
/**
|
||||
* Props for the MitgliederListe component
|
||||
*/
|
||||
external interface MitgliederListeProps : Props
|
||||
|
||||
// Create Ktor client for API calls
|
||||
private val apiClient = HttpClient {
|
||||
install(ContentNegotiation) {
|
||||
json(Json {
|
||||
ignoreUnknownKeys = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* React component that displays a list of members (Mitglieder).
|
||||
*
|
||||
* This component loads member data from the API and renders it as HTML.
|
||||
* Uses useState for state management and useEffectOnce for data loading.
|
||||
*/
|
||||
val MitgliederListe = FC<MitgliederListeProps> { _ ->
|
||||
// State management with useState
|
||||
var members by useState<List<DomUser>>(emptyList())
|
||||
var loading by useState(true)
|
||||
var error by useState<String?>(null)
|
||||
|
||||
// Data loading with useEffectOnce hook
|
||||
useEffectOnce {
|
||||
val scope = MainScope()
|
||||
scope.launch {
|
||||
try {
|
||||
loading = true
|
||||
error = null
|
||||
// Load data with Ktor client
|
||||
val response = apiClient.get("http://localhost:8080/api/members")
|
||||
val loadedMembers: List<DomUser> = response.body()
|
||||
members = loadedMembers
|
||||
} catch (e: Exception) {
|
||||
error = "Fehler beim Laden der Mitglieder: ${e.message}"
|
||||
console.error("Error loading members:", e)
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render HTML with React DOM elements
|
||||
div {
|
||||
css {
|
||||
// Basic styling for the main container
|
||||
"padding" to "20px"
|
||||
"fontFamily" to "Arial, sans-serif"
|
||||
"maxWidth" to "1200px"
|
||||
"margin" to "0 auto"
|
||||
}
|
||||
|
||||
h1 {
|
||||
css {
|
||||
"color" to "#2c3e50"
|
||||
"borderBottom" to "2px solid #3498db"
|
||||
"paddingBottom" to "10px"
|
||||
"marginBottom" to "20px"
|
||||
}
|
||||
+"Mitglieder"
|
||||
}
|
||||
|
||||
when {
|
||||
loading -> {
|
||||
div {
|
||||
css {
|
||||
"padding" to "20px"
|
||||
"textAlign" to "center"
|
||||
"color" to "#666"
|
||||
"fontSize" to "18px"
|
||||
}
|
||||
+"Lade Mitglieder..."
|
||||
}
|
||||
}
|
||||
error != null -> {
|
||||
div {
|
||||
css {
|
||||
"padding" to "20px"
|
||||
"textAlign" to "center"
|
||||
"color" to "#e74c3c"
|
||||
"backgroundColor" to "#fdeaea"
|
||||
"border" to "1px solid #e74c3c"
|
||||
"borderRadius" to "8px"
|
||||
"margin" to "20px 0"
|
||||
}
|
||||
+error!!
|
||||
}
|
||||
}
|
||||
members.isEmpty() -> {
|
||||
div {
|
||||
css {
|
||||
"padding" to "20px"
|
||||
"textAlign" to "center"
|
||||
"color" to "#666"
|
||||
"backgroundColor" to "#f8f9fa"
|
||||
"border" to "1px solid #e0e0e0"
|
||||
"borderRadius" to "8px"
|
||||
"margin" to "20px 0"
|
||||
}
|
||||
+"Keine Mitglieder verfügbar"
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
div {
|
||||
css {
|
||||
"display" to "grid"
|
||||
"gridTemplateColumns" to "repeat(auto-fill, minmax(300px, 1fr))"
|
||||
"gap" to "20px"
|
||||
}
|
||||
members.forEach { member ->
|
||||
div {
|
||||
css {
|
||||
"border" to "1px solid #e0e0e0"
|
||||
"borderRadius" to "8px"
|
||||
"padding" to "15px"
|
||||
"backgroundColor" to "#f9f9f9"
|
||||
"boxShadow" to "0 2px 4px rgba(0,0,0,0.1)"
|
||||
"transition" to "transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out"
|
||||
"hover" to {
|
||||
"transform" to "translateY(-5px)"
|
||||
"boxShadow" to "0 5px 15px rgba(0,0,0,0.1)"
|
||||
}
|
||||
}
|
||||
h3 {
|
||||
css {
|
||||
"color" to "#3498db"
|
||||
"marginTop" to "0"
|
||||
"marginBottom" to "10px"
|
||||
"borderBottom" to "1px solid #e0e0e0"
|
||||
"paddingBottom" to "5px"
|
||||
}
|
||||
+member.username
|
||||
}
|
||||
|
||||
p {
|
||||
span {
|
||||
+"📧"
|
||||
}
|
||||
+" E-Mail: ${member.email}"
|
||||
}
|
||||
|
||||
p {
|
||||
span {
|
||||
+"🆔"
|
||||
}
|
||||
+" Person-ID: ${member.personId}"
|
||||
}
|
||||
|
||||
// Status indicators
|
||||
val statusList = mutableListOf<String>()
|
||||
if (member.istAktiv) statusList.add("Aktiv") else statusList.add("Inaktiv")
|
||||
if (member.istEmailVerifiziert) statusList.add("E-Mail verifiziert")
|
||||
if (member.isLocked()) statusList.add("Gesperrt")
|
||||
if (member.canLogin()) statusList.add("Kann sich anmelden")
|
||||
|
||||
p {
|
||||
span {
|
||||
+"ℹ️"
|
||||
}
|
||||
+" Status: ${statusList.joinToString(", ")}"
|
||||
}
|
||||
|
||||
// Failed login attempts
|
||||
if (member.fehlgeschlageneAnmeldungen > 0) {
|
||||
p {
|
||||
span {
|
||||
+"⚠️"
|
||||
}
|
||||
+" Fehlgeschlagene Anmeldungen: ${member.fehlgeschlageneAnmeldungen}"
|
||||
}
|
||||
}
|
||||
|
||||
// Last login
|
||||
member.letzteAnmeldung?.let { lastLogin ->
|
||||
p {
|
||||
span {
|
||||
+"🔐"
|
||||
}
|
||||
+" Letzte Anmeldung: $lastLogin"
|
||||
}
|
||||
}
|
||||
|
||||
// Creation date
|
||||
p {
|
||||
span {
|
||||
+"📅"
|
||||
}
|
||||
+" Erstellt am: ${member.createdAt}"
|
||||
}
|
||||
|
||||
// Last update
|
||||
p {
|
||||
span {
|
||||
+"🔄"
|
||||
}
|
||||
+" Zuletzt geändert: ${member.updatedAt}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package at.mocode.members.test
|
||||
|
||||
import at.mocode.members.domain.service.UserAuthorizationService
|
||||
import at.mocode.members.domain.service.JwtService
|
||||
import at.mocode.members.domain.service.AuthenticationService
|
||||
import at.mocode.members.infrastructure.repository.*
|
||||
import com.benasher44.uuid.uuid4
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/**
|
||||
* Test class for the authentication system.
|
||||
*
|
||||
* This test verifies that the authentication services can be created
|
||||
* and basic authentication operations work correctly.
|
||||
*/
|
||||
class AuthenticationTest {
|
||||
|
||||
@Test
|
||||
fun testAuthenticationSystem() = runBlocking {
|
||||
println("[DEBUG_LOG] Testing Authentication System")
|
||||
|
||||
try {
|
||||
// Try to create the services
|
||||
val userRepository = UserRepositoryImpl()
|
||||
val personRolleRepository = PersonRolleRepositoryImpl()
|
||||
val rolleRepository = RolleRepositoryImpl()
|
||||
val rolleBerechtigungRepository = RolleBerechtigungRepositoryImpl()
|
||||
val berechtigungRepository = BerechtigungRepositoryImpl()
|
||||
|
||||
val userAuthorizationService = UserAuthorizationService(
|
||||
userRepository,
|
||||
personRolleRepository,
|
||||
rolleRepository,
|
||||
rolleBerechtigungRepository,
|
||||
berechtigungRepository
|
||||
)
|
||||
|
||||
val jwtService = JwtService(userAuthorizationService)
|
||||
|
||||
println("[DEBUG_LOG] Services created successfully")
|
||||
|
||||
// Try to get user auth info for a test user
|
||||
val testUsers = userRepository.getAllUsers()
|
||||
println("[DEBUG_LOG] Found ${testUsers.size} test users")
|
||||
|
||||
if (testUsers.isNotEmpty()) {
|
||||
val testUser = testUsers.first()
|
||||
println("[DEBUG_LOG] Testing with user: ${testUser.username}")
|
||||
|
||||
val authInfo = userAuthorizationService.getUserAuthInfo(testUser.userId)
|
||||
println("[DEBUG_LOG] Auth info for test user: $authInfo")
|
||||
assertNotNull(authInfo, "Auth info should not be null")
|
||||
|
||||
// Test JWT token generation
|
||||
val token = jwtService.createToken(testUser)
|
||||
println("[DEBUG_LOG] Generated JWT token: ${token}")
|
||||
assertNotNull(token, "JWT token should not be null")
|
||||
assertTrue(token.isNotEmpty(), "JWT token should not be empty")
|
||||
|
||||
// Test token validation
|
||||
val payload = jwtService.validateToken(token)
|
||||
println("[DEBUG_LOG] Token validation result: $payload")
|
||||
assertNotNull(payload, "Token validation payload should not be null")
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
println("[DEBUG_LOG] Error testing authentication system: ${e.message}")
|
||||
e.printStackTrace()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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<String, String>) : ApplicationConfig {
|
||||
constructor(vararg pairs: Pair<String, String>) : 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<ApplicationConfig> {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override fun keys(): Set<String> {
|
||||
return map.keys
|
||||
}
|
||||
}
|
||||
|
||||
class MapApplicationConfigValue(private val value: String?) : ApplicationConfigValue {
|
||||
override fun getString(): String = value ?: ""
|
||||
override fun getList(): List<String> = 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
|
||||
Reference in New Issue
Block a user