mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 10:19:35 +01:00
feat: add Spring Boot ERP application with user management domain
Implement DDD-based architecture with domain, application, infrastructure, and API layers. Includes user/role management with authentication, RBAC permissions, audit logging, Liquibase migrations, and test suite.
This commit is contained in:
parent
a1df32377b
commit
ec9114aa0a
124 changed files with 18208 additions and 0 deletions
677
docs/USER_MANAGEMENT.md
Normal file
677
docs/USER_MANAGEMENT.md
Normal file
|
|
@ -0,0 +1,677 @@
|
|||
# User Management - Dokumentation
|
||||
|
||||
**Bounded Context:** User Management (Generic Subdomain)
|
||||
**Architektur:** DDD-Light + Clean Architecture
|
||||
**Status:** MVP Implementation Complete
|
||||
|
||||
---
|
||||
|
||||
## Übersicht
|
||||
|
||||
User Management ist als **Generic Subdomain** klassifiziert - Commodity-Funktionalität ohne Wettbewerbsvorteil. Daher minimaler DDD-Aufwand:
|
||||
- ✅ Einfache Entities (keine Aggregates)
|
||||
- ✅ Transaction Script Pattern (keine komplexen Domain Services)
|
||||
- ✅ Keine Domain Events
|
||||
- ✅ Standard-Technologien (Spring Security, JWT, BCrypt)
|
||||
|
||||
### Warum wird User Management benötigt?
|
||||
|
||||
- **HACCP-Compliance:** Alle Qualitätsaktionen müssen einem Benutzer zugeordnet sein
|
||||
- **GoBD-Compliance:** Audit Trail für alle geschäftskritischen Aktionen
|
||||
- **Mehrfilialen-Support:** Benutzer müssen Filialen zugeordnet werden
|
||||
- **Berechtigungskonzept:** Unterschiedliche Rollen für Produktion, Qualität, Verkauf
|
||||
|
||||
---
|
||||
|
||||
## Architektur
|
||||
|
||||
### Clean Architecture Layers
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ REST API (Controllers) │
|
||||
│ AuthController, UserController, RoleController │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Application Layer (Use Cases) │
|
||||
│ CreateUser, AuthenticateUser, ChangePassword │
|
||||
│ Transaction Script Pattern │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Domain Layer (DDD-Light) │
|
||||
│ User, Role (Simple Entities) │
|
||||
│ UserId, RoleId, PasswordHash (Value Objects) │
|
||||
│ UserRepository, RoleRepository (Interfaces) │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Infrastructure Layer │
|
||||
│ Spring Security, JWT, BCrypt, JPA, PostgreSQL │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Wichtige Design-Entscheidungen
|
||||
|
||||
| Entscheidung | Begründung |
|
||||
|--------------|------------|
|
||||
| **Eigene Implementation** statt Keycloak | Einfacher für MVP, volle Kontrolle, weniger Komplexität |
|
||||
| **JWT** statt Sessions | Stateless, skalierbar, microservice-ready |
|
||||
| **BCrypt Strength 12** | Sicher (~250ms), nicht zu langsam |
|
||||
| **AuthorizationPort** (ACL) | Ermöglicht spätere Keycloak-Migration ohne BC-Änderungen |
|
||||
| **Async Audit Logging** | Performance - blockiert Use Cases nicht |
|
||||
| **No Domain Events** | Generic Subdomain - minimaler DDD-Aufwand |
|
||||
|
||||
---
|
||||
|
||||
## Domain Model
|
||||
|
||||
### Entities
|
||||
|
||||
#### User
|
||||
```java
|
||||
public class User {
|
||||
private UserId id;
|
||||
private String username;
|
||||
private String email;
|
||||
private PasswordHash passwordHash; // BCrypt
|
||||
private Set<Role> roles;
|
||||
private String branchId; // Filialzuordnung
|
||||
private UserStatus status; // ACTIVE, INACTIVE, LOCKED
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime lastLogin;
|
||||
}
|
||||
```
|
||||
|
||||
#### Role (Reference Data)
|
||||
```java
|
||||
public class Role {
|
||||
private RoleId id;
|
||||
private RoleName name; // Enum: ADMIN, PRODUCTION_MANAGER, ...
|
||||
private Set<Permission> permissions;
|
||||
private String description;
|
||||
}
|
||||
```
|
||||
|
||||
### Value Objects
|
||||
|
||||
- `UserId` - UUID-basiert
|
||||
- `RoleId` - UUID-basiert
|
||||
- `PasswordHash` - BCrypt Hash (60 chars, $2a$12$...)
|
||||
- `Permission` - Enum (RECIPE_READ, BATCH_WRITE, etc.)
|
||||
- `RoleName` - Enum (ADMIN, PRODUCTION_MANAGER, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Vordefinierte Rollen
|
||||
|
||||
| Rolle | Permissions | Zielgruppe |
|
||||
|-------|-------------|------------|
|
||||
| **ADMIN** | Alle (67) | Systemadministrator |
|
||||
| **PRODUCTION_MANAGER** | RECIPE_*, BATCH_*, PRODUCTION_ORDER_*, STOCK_READ | Leiter Produktion |
|
||||
| **PRODUCTION_WORKER** | RECIPE_READ, BATCH_*, LABEL_* | Produktionsmitarbeiter |
|
||||
| **QUALITY_MANAGER** | HACCP_*, TEMPERATURE_LOG_*, CLEANING_RECORD_*, GOODS_INSPECTION_* | Qualitätsbeauftragter |
|
||||
| **QUALITY_INSPECTOR** | TEMPERATURE_LOG_*, GOODS_INSPECTION_* | QM-Mitarbeiter |
|
||||
| **PROCUREMENT_MANAGER** | PURCHASE_ORDER_*, GOODS_RECEIPT_*, SUPPLIER_* | Einkaufsleiter |
|
||||
| **WAREHOUSE_WORKER** | STOCK_*, INVENTORY_COUNT_*, LABEL_* | Lagermitarbeiter |
|
||||
| **SALES_MANAGER** | ORDER_*, INVOICE_*, CUSTOMER_* | Verkaufsleiter |
|
||||
| **SALES_STAFF** | ORDER_READ/WRITE, CUSTOMER_READ, STOCK_READ | Verkaufsmitarbeiter |
|
||||
|
||||
Rollen werden bei DB-Initialisierung aus Seed-Daten geladen (Flyway Migration V002).
|
||||
|
||||
---
|
||||
|
||||
## Authorization Port (ACL für andere BCs)
|
||||
|
||||
### Warum AuthorizationPort?
|
||||
|
||||
**Problem:** Andere BCs brauchen Authentifizierung/Autorisierung, sollen aber nicht direkt von User Management abhängen.
|
||||
|
||||
**Lösung:** `AuthorizationPort` Interface im **Shared Kernel** - typsicher, action-oriented, ermöglicht Keycloak-Migration.
|
||||
|
||||
### Interface
|
||||
|
||||
```java
|
||||
public interface AuthorizationPort {
|
||||
boolean can(Action action);
|
||||
void assertCan(Action action);
|
||||
boolean can(Action action, ResourceId resource);
|
||||
void assertCan(Action action, ResourceId resource);
|
||||
ActorId currentActor();
|
||||
Optional<BranchId> currentBranch();
|
||||
}
|
||||
```
|
||||
|
||||
### Typsichere Actions (Sealed Interface + Enums)
|
||||
|
||||
```java
|
||||
// Shared Kernel - Basis-Interface
|
||||
public interface Action {
|
||||
// Marker interface
|
||||
}
|
||||
|
||||
// Production BC - eigene Actions
|
||||
public enum ProductionAction implements Action {
|
||||
RECIPE_READ, RECIPE_WRITE, RECIPE_DELETE,
|
||||
BATCH_READ, BATCH_WRITE, BATCH_COMPLETE,
|
||||
PRODUCTION_ORDER_READ, PRODUCTION_ORDER_WRITE
|
||||
}
|
||||
|
||||
// Quality BC
|
||||
public enum QualityAction implements Action {
|
||||
TEMPERATURE_LOG_READ, TEMPERATURE_LOG_WRITE,
|
||||
CLEANING_RECORD_READ, CLEANING_RECORD_WRITE,
|
||||
GOODS_INSPECTION_READ, GOODS_INSPECTION_WRITE
|
||||
}
|
||||
```
|
||||
|
||||
### Nutzung in anderen BCs
|
||||
|
||||
```java
|
||||
// Production BC - Rezept erstellen
|
||||
public class CreateRecipe {
|
||||
private final AuthorizationPort authPort;
|
||||
private final RecipeRepository recipeRepository;
|
||||
private final AuditLogger auditLogger;
|
||||
|
||||
public Result<ApplicationError, RecipeDTO> execute(CreateRecipeCommand cmd) {
|
||||
// ✅ Typsichere fachliche Authorization
|
||||
authPort.assertCan(ProductionAction.RECIPE_WRITE);
|
||||
|
||||
// Business logic
|
||||
Recipe recipe = Recipe.create(cmd.name(), cmd.ingredients());
|
||||
recipeRepository.save(recipe);
|
||||
|
||||
// Audit logging mit ActorId
|
||||
auditLogger.log(
|
||||
AuditEvent.RECIPE_CREATED,
|
||||
recipe.id(),
|
||||
authPort.currentActor()
|
||||
);
|
||||
|
||||
return Result.success(RecipeDTO.from(recipe));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Action → Permission Mapping
|
||||
|
||||
```java
|
||||
// Infrastructure Layer - ActionToPermissionMapper
|
||||
@Component
|
||||
public class ActionToPermissionMapper {
|
||||
public Permission mapActionToPermission(Action action) {
|
||||
// Type-safe pattern matching (Java 21)
|
||||
if (action instanceof ProductionAction pa) {
|
||||
return switch (pa) {
|
||||
case RECIPE_READ -> Permission.RECIPE_READ;
|
||||
case RECIPE_WRITE -> Permission.RECIPE_WRITE;
|
||||
// ... exhaustive
|
||||
};
|
||||
}
|
||||
// ... andere BCs
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Vorteile
|
||||
|
||||
1. ✅ **Typsicher** - Compiler prüft Actions, keine Tippfehler
|
||||
2. ✅ **Fachlich** - Jeder BC spricht seine eigene Sprache
|
||||
3. ✅ **Entkoppelt** - BCs kennen keine User/Roles/Permissions
|
||||
4. ✅ **Flexibel** - Action-zu-Permission-Mapping änderbar ohne BC-Änderungen
|
||||
5. ✅ **IDE-Support** - Auto-Completion für Actions
|
||||
6. ✅ **Keycloak-ready** - Actions können auf Keycloak Policies gemappt werden
|
||||
|
||||
---
|
||||
|
||||
## REST API
|
||||
|
||||
### Authentication Endpoints (Public)
|
||||
|
||||
#### Login
|
||||
```http
|
||||
POST /api/auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "admin123"
|
||||
}
|
||||
|
||||
Response 200 OK:
|
||||
{
|
||||
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"tokenType": "Bearer",
|
||||
"expiresIn": 28800,
|
||||
"expiresAt": "2026-02-18T02:00:00",
|
||||
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
}
|
||||
```
|
||||
|
||||
#### Logout
|
||||
```http
|
||||
POST /api/auth/logout
|
||||
Authorization: Bearer {accessToken}
|
||||
|
||||
Response 204 No Content
|
||||
```
|
||||
|
||||
#### Refresh Token
|
||||
```http
|
||||
POST /api/auth/refresh
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
}
|
||||
|
||||
Response 200 OK:
|
||||
{
|
||||
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### User Management Endpoints (Authenticated)
|
||||
|
||||
#### Create User (ADMIN only)
|
||||
```http
|
||||
POST /api/users
|
||||
Authorization: Bearer {accessToken}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "jdoe",
|
||||
"email": "jdoe@example.com",
|
||||
"password": "SecurePass123!",
|
||||
"roleNames": ["PRODUCTION_WORKER"],
|
||||
"branchId": "branch-123"
|
||||
}
|
||||
|
||||
Response 201 Created
|
||||
```
|
||||
|
||||
#### List Users
|
||||
```http
|
||||
GET /api/users
|
||||
Authorization: Bearer {accessToken}
|
||||
|
||||
Response 200 OK:
|
||||
[
|
||||
{
|
||||
"id": "user-123",
|
||||
"username": "jdoe",
|
||||
"email": "jdoe@example.com",
|
||||
"roles": [...],
|
||||
"branchId": "branch-123",
|
||||
"status": "ACTIVE",
|
||||
"createdAt": "2026-02-17T10:00:00",
|
||||
"lastLogin": "2026-02-17T14:30:00"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### Lock/Unlock User (ADMIN only)
|
||||
```http
|
||||
POST /api/users/{id}/lock
|
||||
POST /api/users/{id}/unlock
|
||||
Authorization: Bearer {accessToken}
|
||||
|
||||
Response 200 OK
|
||||
```
|
||||
|
||||
#### Change Password
|
||||
```http
|
||||
PUT /api/users/{id}/password
|
||||
Authorization: Bearer {accessToken}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"currentPassword": "OldPass123!",
|
||||
"newPassword": "NewPass456!"
|
||||
}
|
||||
|
||||
Response 204 No Content
|
||||
```
|
||||
|
||||
### Swagger UI
|
||||
|
||||
Interaktive API-Dokumentation: **http://localhost:8080/swagger-ui/index.html**
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
### JWT Token Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"sub": "user-123",
|
||||
"username": "jdoe",
|
||||
"permissions": ["RECIPE_READ", "BATCH_WRITE", ...],
|
||||
"branchId": "branch-123",
|
||||
"iat": 1708185600,
|
||||
"exp": 1708214400
|
||||
}
|
||||
```
|
||||
|
||||
- **Algorithm:** HS256 (HMAC-SHA256)
|
||||
- **Secret:** Konfiguriert via `jwt.secret` in application.yml
|
||||
- **Expiration:** 8 Stunden (Access Token), 7 Tage (Refresh Token)
|
||||
|
||||
### Password Security
|
||||
|
||||
- **Algorithm:** BCrypt
|
||||
- **Strength:** 12 Rounds (~250ms hashing time)
|
||||
- **Requirements:** Min. 8 Zeichen, 1 Großbuchstabe, 1 Kleinbuchstabe, 1 Zahl, 1 Sonderzeichen
|
||||
|
||||
### Spring Security Configuration
|
||||
|
||||
```yaml
|
||||
# Stateless JWT Authentication
|
||||
- sessionManagement: STATELESS
|
||||
- CSRF: Disabled (safe for stateless JWT)
|
||||
- Public Endpoints: /api/auth/login, /api/auth/refresh
|
||||
- Protected Endpoints: /api/** (require authentication)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Audit Logging (HACCP/GoBD)
|
||||
|
||||
### Compliance Requirements
|
||||
|
||||
- **HACCP:** Wer hat wann welche Temperatur gemessen?
|
||||
- **GoBD:** Unveränderlicher Audit Trail für 10 Jahre
|
||||
- **Retention:** 10 Jahre (gesetzlich), dann archivieren
|
||||
|
||||
### Audit Log Structure
|
||||
|
||||
```sql
|
||||
CREATE TABLE audit_logs (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
event VARCHAR(100) NOT NULL, -- z.B. USER_CREATED, TEMPERATURE_RECORDED
|
||||
entity_id VARCHAR(36), -- z.B. UserId, RecipeId, BatchId
|
||||
performed_by VARCHAR(36), -- ActorId
|
||||
details VARCHAR(2000), -- Zusätzliche Details
|
||||
timestamp TIMESTAMP NOT NULL, -- Wann (Business-Zeit)
|
||||
ip_address VARCHAR(45), -- Woher (Client IP)
|
||||
user_agent VARCHAR(500), -- Womit (Browser/App)
|
||||
created_at TIMESTAMP NOT NULL -- DB-Insert-Zeit
|
||||
);
|
||||
```
|
||||
|
||||
### Audit Events
|
||||
|
||||
- **User Management:** USER_CREATED, LOGIN_SUCCESS, PASSWORD_CHANGED
|
||||
- **Quality BC:** TEMPERATURE_RECORDED, TEMPERATURE_CRITICAL, CLEANING_PERFORMED
|
||||
- **Production BC:** BATCH_CREATED, BATCH_COMPLETED, RECIPE_MODIFIED
|
||||
- **System:** SYSTEM_SETTINGS_CHANGED
|
||||
|
||||
### Performance
|
||||
|
||||
- **Async Logging:** `@Async` - blockiert Use Cases nicht
|
||||
- **Separate Transaction:** `REQUIRES_NEW` - auch bei Business-TX-Rollback
|
||||
- **Batch-Inserts:** Möglich für High-Volume-Szenarien
|
||||
|
||||
---
|
||||
|
||||
## Branch-Zuordnung (Mehrfilialen)
|
||||
|
||||
### User-Branch Beziehung
|
||||
|
||||
```java
|
||||
public class User {
|
||||
private String branchId; // Optional, null = ADMIN (global access)
|
||||
}
|
||||
```
|
||||
|
||||
### Data Filtering
|
||||
|
||||
**Strategie 1: Application Layer Filtering (MVP)**
|
||||
|
||||
```java
|
||||
public class ListStock {
|
||||
private final AuthorizationPort authPort;
|
||||
|
||||
public Result<ApplicationError, List<StockDTO>> execute() {
|
||||
Optional<BranchId> actorBranch = authPort.currentBranch();
|
||||
|
||||
if (actorBranch.isPresent()) {
|
||||
// Normaler User: Nur eigene Filiale
|
||||
return Result.success(stockRepository.findByBranch(actorBranch.get()));
|
||||
} else {
|
||||
// ADMIN: Alle Filialen
|
||||
return Result.success(stockRepository.findAll());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Strategie 2: Database Row-Level Security (Post-MVP)**
|
||||
|
||||
```sql
|
||||
-- PostgreSQL RLS (später)
|
||||
CREATE POLICY user_branch_policy ON stock
|
||||
USING (branch_id = current_setting('app.current_branch_id')::text);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deployment
|
||||
|
||||
### Umgebungsvariablen
|
||||
|
||||
```bash
|
||||
# JWT Configuration
|
||||
JWT_SECRET=YourVerySecretKeyMin256BitsForHS256Algorithm # Min. 256 Bits!
|
||||
|
||||
# Database
|
||||
SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/effigenix
|
||||
SPRING_DATASOURCE_USERNAME=effigenix
|
||||
SPRING_DATASOURCE_PASSWORD=effigenix
|
||||
|
||||
# Flyway
|
||||
SPRING_FLYWAY_ENABLED=true
|
||||
```
|
||||
|
||||
### Docker Compose (PostgreSQL)
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
environment:
|
||||
POSTGRES_DB: effigenix
|
||||
POSTGRES_USER: effigenix
|
||||
POSTGRES_PASSWORD: effigenix
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
```
|
||||
|
||||
### Build & Run
|
||||
|
||||
```bash
|
||||
# Build
|
||||
mvn clean package
|
||||
|
||||
# Run
|
||||
java -jar target/effigenix-erp-0.1.0-SNAPSHOT.jar
|
||||
|
||||
# Run mit Production Profile
|
||||
java -jar target/effigenix-erp-0.1.0-SNAPSHOT.jar --spring.profiles.active=prod
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Entwickler-Guide
|
||||
|
||||
### Neue Rolle hinzufügen
|
||||
|
||||
1. **RoleName Enum erweitern** (`domain/usermanagement/RoleName.java`)
|
||||
2. **Permissions zuweisen** in Flyway Seed-Daten (`V002__seed_roles_and_permissions.sql`)
|
||||
3. **Migration ausführen** oder manuell in DB einfügen
|
||||
|
||||
### AuthorizationPort in neuem BC nutzen
|
||||
|
||||
1. **Action Enum erstellen** (z.B. `NewBCAction`)
|
||||
```java
|
||||
public enum NewBCAction implements Action {
|
||||
ENTITY_READ, ENTITY_WRITE, ENTITY_DELETE
|
||||
}
|
||||
```
|
||||
|
||||
2. **Action in Shared Kernel registrieren** (nur für Dokumentation, nicht sealed)
|
||||
|
||||
3. **Mapping hinzufügen** in `ActionToPermissionMapper`
|
||||
```java
|
||||
if (action instanceof NewBCAction nba) {
|
||||
return switch (nba) {
|
||||
case ENTITY_READ -> Permission.ENTITY_READ;
|
||||
case ENTITY_WRITE -> Permission.ENTITY_WRITE;
|
||||
case ENTITY_DELETE -> Permission.ENTITY_DELETE;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
4. **Permissions zu RoleName Enum hinzufügen**
|
||||
```java
|
||||
public enum Permission {
|
||||
// ... existing
|
||||
ENTITY_READ, ENTITY_WRITE, ENTITY_DELETE
|
||||
}
|
||||
```
|
||||
|
||||
5. **In Use Cases nutzen**
|
||||
```java
|
||||
authPort.assertCan(NewBCAction.ENTITY_WRITE);
|
||||
```
|
||||
|
||||
### Custom Audit Event hinzufügen
|
||||
|
||||
1. **AuditEvent Enum erweitern**
|
||||
```java
|
||||
public enum AuditEvent {
|
||||
// ... existing
|
||||
MY_CUSTOM_EVENT
|
||||
}
|
||||
```
|
||||
|
||||
2. **In Use Case loggen**
|
||||
```java
|
||||
auditLogger.log(AuditEvent.MY_CUSTOM_EVENT, entityId, actorId);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Manuelles Testen (Swagger UI)
|
||||
|
||||
1. **Server starten:** `mvn spring-boot:run`
|
||||
2. **Swagger öffnen:** http://localhost:8080/swagger-ui/index.html
|
||||
3. **Login:** POST /api/auth/login mit `admin` / `admin123` (Seed-Daten erstellen)
|
||||
4. **Token kopieren** aus Response
|
||||
5. **Authorize** Button klicken, Token einfügen: `Bearer {token}`
|
||||
6. **Endpoints testen**
|
||||
|
||||
### curl Beispiele
|
||||
|
||||
```bash
|
||||
# Login
|
||||
curl -X POST http://localhost:8080/api/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","password":"admin123"}'
|
||||
|
||||
# Get Users (mit Token)
|
||||
curl -X GET http://localhost:8080/api/users \
|
||||
-H "Authorization: Bearer {accessToken}"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration zu Keycloak (Zukunft)
|
||||
|
||||
### Warum ACL-Pattern vorbereitet ist
|
||||
|
||||
**Aktuell:** `SpringSecurityAuthorizationAdapter` → User Management BC
|
||||
**Zukunft:** `KeycloakAuthorizationAdapter` → Keycloak
|
||||
|
||||
### Migrations-Schritte
|
||||
|
||||
1. **KeycloakAuthorizationAdapter implementieren**
|
||||
- Implementiert `AuthorizationPort`
|
||||
- Mappt Actions auf Keycloak Policies/Roles
|
||||
|
||||
2. **Spring Configuration umstellen**
|
||||
- Keycloak Adapter in `SecurityConfig` einbinden
|
||||
- JWT Validation auf Keycloak-Token umstellen
|
||||
|
||||
3. **Keine Änderungen in BCs nötig!**
|
||||
- BCs nutzen weiterhin `authPort.assertCan(Action)`
|
||||
- Mapping bleibt gleich
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
| Komponente | Performance | Optimierung |
|
||||
|------------|-------------|-------------|
|
||||
| **Password Hashing** | ~250ms (BCrypt 12) | Akzeptabel für Login, async für bulk operations |
|
||||
| **JWT Validation** | <1ms | Stateless, keine DB-Abfrage |
|
||||
| **Audit Logging** | Async | Blockiert Use Cases nicht |
|
||||
| **Authorization Check** | <5ms | In-Memory Permissions (aus JWT) |
|
||||
|
||||
### Produktions-Empfehlungen
|
||||
|
||||
1. **JWT Secret:** Min. 256 Bits, rotieren alle 90 Tage
|
||||
2. **Refresh Token:** Redis-backed statt In-Memory
|
||||
3. **Audit Logs:** Archivierung nach 10 Jahren (Legal-Hold)
|
||||
4. **Rate Limiting:** Login-Endpunkt schützen (5 Versuche / 15 Min)
|
||||
5. **HTTPS:** Nur HTTPS in Produktion (JWT im Header!)
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Invalid JWT signature"
|
||||
- **Ursache:** JWT Secret geändert oder nicht konfiguriert
|
||||
- **Lösung:** `JWT_SECRET` Umgebungsvariable prüfen
|
||||
|
||||
### "User not found" bei Login
|
||||
- **Ursache:** Seed-Daten nicht geladen
|
||||
- **Lösung:** Flyway Migrations ausführen (`mvn flyway:migrate`)
|
||||
|
||||
### "Access Denied" trotz richtiger Rolle
|
||||
- **Ursache:** Permission fehlt in `ActionToPermissionMapper`
|
||||
- **Lösung:** Mapping hinzufügen und neu deployen
|
||||
|
||||
### Audit Logs werden nicht geschrieben
|
||||
- **Ursache:** Async nicht konfiguriert
|
||||
- **Lösung:** `@EnableAsync` in Application Class prüfen
|
||||
|
||||
---
|
||||
|
||||
## Fazit
|
||||
|
||||
User Management als **Generic Subdomain** mit:
|
||||
|
||||
✅ **Minimaler DDD-Aufwand** - Einfache Entities, Transaction Scripts
|
||||
✅ **Typsichere Authorization** - Action Enums statt String-Permissions
|
||||
✅ **ACL-Pattern** - Ermöglicht Keycloak-Migration
|
||||
✅ **HACCP/GoBD-Compliant** - Audit Logging mit 10-Jahres-Retention
|
||||
✅ **Production-Ready** - JWT, BCrypt, Async, Multi-Branch
|
||||
|
||||
**Nächste Schritte:**
|
||||
1. Seed-Daten für initialen Admin-User erstellen
|
||||
2. Integration Tests schreiben
|
||||
3. Production-Konfiguration (HTTPS, Rate Limiting, Redis)
|
||||
4. Keycloak-Migration evaluieren (wenn OAuth2/SSO benötigt)
|
||||
Loading…
Add table
Add a link
Reference in a new issue