1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 18:49:59 +01:00
effigenix/backend/docs/USER_MANAGEMENT.md
Sebastian Frick c2c48a03e8 refactor: restructure repository with separate backend and frontend directories
- Move Java backend to backend/ directory
- Create frontend/ directory for TypeScript TUI and future WebUI
- Update .gitignore for Node.js and worktrees
- Update README.md with new repository structure
- Copy documentation to backend/
2026-02-17 22:08:51 +01:00

677 lines
19 KiB
Markdown

# 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)