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.
19 KiB
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
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)
public class Role {
private RoleId id;
private RoleName name; // Enum: ADMIN, PRODUCTION_MANAGER, ...
private Set<Permission> permissions;
private String description;
}
Value Objects
UserId- UUID-basiertRoleId- UUID-basiertPasswordHash- 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
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)
// 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
// 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
// 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
- ✅ Typsicher - Compiler prüft Actions, keine Tippfehler
- ✅ Fachlich - Jeder BC spricht seine eigene Sprache
- ✅ Entkoppelt - BCs kennen keine User/Roles/Permissions
- ✅ Flexibel - Action-zu-Permission-Mapping änderbar ohne BC-Änderungen
- ✅ IDE-Support - Auto-Completion für Actions
- ✅ Keycloak-ready - Actions können auf Keycloak Policies gemappt werden
REST API
Authentication Endpoints (Public)
Login
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
POST /api/auth/logout
Authorization: Bearer {accessToken}
Response 204 No Content
Refresh Token
POST /api/auth/refresh
Content-Type: application/json
{
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Response 200 OK:
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
...
}
User Management Endpoints (Authenticated)
Create User (ADMIN only)
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
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)
POST /api/users/{id}/lock
POST /api/users/{id}/unlock
Authorization: Bearer {accessToken}
Response 200 OK
Change Password
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
{
"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.secretin 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
# 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
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
public class User {
private String branchId; // Optional, null = ADMIN (global access)
}
Data Filtering
Strategie 1: Application Layer Filtering (MVP)
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)
-- PostgreSQL RLS (später)
CREATE POLICY user_branch_policy ON stock
USING (branch_id = current_setting('app.current_branch_id')::text);
Deployment
Umgebungsvariablen
# 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)
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
# 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
- RoleName Enum erweitern (
domain/usermanagement/RoleName.java) - Permissions zuweisen in Flyway Seed-Daten (
V002__seed_roles_and_permissions.sql) - Migration ausführen oder manuell in DB einfügen
AuthorizationPort in neuem BC nutzen
-
Action Enum erstellen (z.B.
NewBCAction)public enum NewBCAction implements Action { ENTITY_READ, ENTITY_WRITE, ENTITY_DELETE } -
Action in Shared Kernel registrieren (nur für Dokumentation, nicht sealed)
-
Mapping hinzufügen in
ActionToPermissionMapperif (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; }; } -
Permissions zu RoleName Enum hinzufügen
public enum Permission { // ... existing ENTITY_READ, ENTITY_WRITE, ENTITY_DELETE } -
In Use Cases nutzen
authPort.assertCan(NewBCAction.ENTITY_WRITE);
Custom Audit Event hinzufügen
-
AuditEvent Enum erweitern
public enum AuditEvent { // ... existing MY_CUSTOM_EVENT } -
In Use Case loggen
auditLogger.log(AuditEvent.MY_CUSTOM_EVENT, entityId, actorId);
Testing
Manuelles Testen (Swagger UI)
- Server starten:
mvn spring-boot:run - Swagger öffnen: http://localhost:8080/swagger-ui/index.html
- Login: POST /api/auth/login mit
admin/admin123(Seed-Daten erstellen) - Token kopieren aus Response
- Authorize Button klicken, Token einfügen:
Bearer {token} - Endpoints testen
curl Beispiele
# 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
-
KeycloakAuthorizationAdapter implementieren
- Implementiert
AuthorizationPort - Mappt Actions auf Keycloak Policies/Roles
- Implementiert
-
Spring Configuration umstellen
- Keycloak Adapter in
SecurityConfigeinbinden - JWT Validation auf Keycloak-Token umstellen
- Keycloak Adapter in
-
Keine Änderungen in BCs nötig!
- BCs nutzen weiterhin
authPort.assertCan(Action) - Mapping bleibt gleich
- BCs nutzen weiterhin
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
- JWT Secret: Min. 256 Bits, rotieren alle 90 Tage
- Refresh Token: Redis-backed statt In-Memory
- Audit Logs: Archivierung nach 10 Jahren (Legal-Hold)
- Rate Limiting: Login-Endpunkt schützen (5 Versuche / 15 Min)
- HTTPS: Nur HTTPS in Produktion (JWT im Header!)
Troubleshooting
"Invalid JWT signature"
- Ursache: JWT Secret geändert oder nicht konfiguriert
- Lösung:
JWT_SECRETUmgebungsvariable 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:
@EnableAsyncin 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:
- Seed-Daten für initialen Admin-User erstellen
- Integration Tests schreiben
- Production-Konfiguration (HTTPS, Rate Limiting, Redis)
- Keycloak-Migration evaluieren (wenn OAuth2/SSO benötigt)