1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 08:29:36 +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

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

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

  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

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.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

# 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

  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)

    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

    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

    public enum Permission {
        // ... existing
        ENTITY_READ, ENTITY_WRITE, ENTITY_DELETE
    }
    
  5. In Use Cases nutzen

    authPort.assertCan(NewBCAction.ENTITY_WRITE);
    

Custom Audit Event hinzufügen

  1. AuditEvent Enum erweitern

    public enum AuditEvent {
        // ... existing
        MY_CUSTOM_EVENT
    }
    
  2. In Use Case loggen

    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

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