Reorganize into monorepo layout, move mitarbeiter-app to legacy reference
Consolidates the previously separate omsorgapp and omsorgCore repos (each had their own nested .git with GitHub history) plus the old root-level website/mitarbeiter-app into a single monorepo, matching the structure already documented in the root CLAUDE.md. Also moves the PHP employee app aside as omsorgWeb/mitarbeiter-app-legacy/ to serve as a template for a ground-up rewrite. Fixes .gitignore in the same pass: the config-secrets/uploads/data patterns were unanchored (relative to repo root, not depth-agnostic), so they silently stopped matching once the app moved under omsorgWeb/. Patterns are now **/-prefixed and cover both mitarbeiter-app and mitarbeiter-app-legacy, keeping DB/SMTP credentials and uploaded employee documents out of version control. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
8beb0fcf52
commit
b6c1389c55
@@ -0,0 +1,179 @@
|
||||
# CLAUDE.md — omsorgCore (OMSORG Backend: Core + Engine)
|
||||
|
||||
Gilt zusätzlich zur Root-`CLAUDE.md`. Das Grundgerüst ist angelegt und baut (`dotnet build` läuft grün); der fachliche Umfang ist bewusst noch minimal (Fundament, nicht Vollständigkeit).
|
||||
|
||||
## Auftrag dieses Projekts
|
||||
|
||||
`omsorgCore` ist die gemeinsame Datenbasis + Automatisierungsschicht für die gesamte Plattform (siehe `REQUIREMENTS.md` Abschnitt 2 und 6, Blueprint Kap. 19/20). Es soll schrittweise ersetzen:
|
||||
- die MySQL-Datenhaltung in `omsorgWeb/mitarbeiter-app`
|
||||
- die lokale JSON-Datenhaltung in `omsorgapp`
|
||||
|
||||
Beide bestehenden Projekte sollen künftig gegen dieses Backend sprechen statt eigene, getrennte Datenspeicher zu pflegen. Diese Anbindung ist noch **nicht** erfolgt — aktueller Schritt ist nur das Backend-Fundament selbst.
|
||||
|
||||
## Tech-Stack (umgesetzt)
|
||||
|
||||
- C# / **.NET 8** (LTS)
|
||||
- ASP.NET Core Web API mit **Controllern** (kein Minimal-API-Stil)
|
||||
- **PostgreSQL** über `Npgsql.EntityFrameworkCore.PostgreSQL` 8.0.10 (bewusst auf net8-kompatible Version gepinnt — neuere Paketversionen zielen auf .NET 10)
|
||||
- **JWT Bearer Tokens** für Auth (`Microsoft.AspNetCore.Authentication.JwtBearer`)
|
||||
- Passwort-Hashing über `Microsoft.AspNetCore.Identity.PasswordHasher<T>` (PBKDF2, kein eigenes Krypto-Rad)
|
||||
- **Core + Engine = eine Backend-Komponente**, kein separates Deployable für die Engine — die Event-Schicht läuft im selben Prozess wie die Datenschicht (siehe Root-`CLAUDE.md` und `REQUIREMENTS.md` Abschnitt 1.2)
|
||||
|
||||
## Reale Projektstruktur
|
||||
|
||||
```
|
||||
omsorgCore/
|
||||
OmsorgCore.sln
|
||||
.config/dotnet-tools.json # lokales dotnet-ef Tool (dotnet tool restore)
|
||||
src/
|
||||
OmsorgCore.Domain/ # Entitäten, Enums. Keine Abhängigkeit auf andere Projekte.
|
||||
Common/ # Entity, AuditableEntity (Basisklassen)
|
||||
Enums/ # ModuleType, PermissionAction, PermissionEffect
|
||||
Entities/ # Employee, Facility, Contract, Order, TimeEntry, Invoice,
|
||||
# User, Role, RolePermission, UserPermissionOverride
|
||||
OmsorgCore.Application/ # Business-Logik. Abhängig von Domain.
|
||||
Abstractions/ # Interfaces: IEmployeeRepository, IUserRepository,
|
||||
# IPasswordHasher, IJwtTokenGenerator, ICurrentUserService,
|
||||
# IPermissionService
|
||||
Services/ # PermissionService, AuthService, EmployeeService, SessionAdminService
|
||||
DependencyInjection.cs # AddApplication()
|
||||
OmsorgCore.Infrastructure/ # Technische Umsetzung. Abhängig von Domain + Application.
|
||||
Persistence/
|
||||
OmsorgCoreDbContext.cs
|
||||
Configurations/ # ein IEntityTypeConfiguration<T> pro Entität
|
||||
Migrations/ # EF-Core-Migrationen (InitialCreate bereits erzeugt)
|
||||
Repositories/ # EmployeeRepository, UserRepository, RefreshTokenRepository (implementieren Application-Interfaces)
|
||||
Security/ # PasswordHasher, JwtOptions, JwtTokenGenerator, RefreshTokenOptions, RefreshTokenGenerator
|
||||
DependencyInjection.cs # AddInfrastructure(configuration)
|
||||
OmsorgCore.Engine/ # Event-Schicht. Abhängig von Domain + Application.
|
||||
Events/ # IDomainEvent, IDomainEventHandler<T>, IDomainEventDispatcher,
|
||||
# DomainEventDispatcher (In-Process, kein Message-Bus), Beispiel-Event
|
||||
Handlers/ # Beispiel-Handler (EmployeeCreatedHandler)
|
||||
DependencyInjection.cs # AddEngine()
|
||||
OmsorgCore.Api/ # ASP.NET Core Web API. Abhängig von Application+Infrastructure+Engine.
|
||||
Controllers/ # AuthController, EmployeesController, HealthController, AdminSessionsController
|
||||
Contracts/ # Request-/Response-DTOs (LoginRequest, EmployeeResponse, ...)
|
||||
Security/ # CurrentUserService, RequirePermissionAttribute
|
||||
Program.cs # einziger Ort, an dem alle Schichten verdrahtet werden
|
||||
tests/
|
||||
OmsorgCore.Tests/ # xUnit, referenziert Domain + Application
|
||||
```
|
||||
|
||||
**Konvention: eine Klasse/ein Interface/ein Enum pro Datei.** Ausnahme: keine — auch kleine DTOs (Requests/Responses als `record`) bekommen eine eigene Datei.
|
||||
|
||||
**Abhängigkeitsrichtung ist strikt:** Domain kennt niemanden. Application kennt nur Domain und definiert Interfaces, die Infrastructure implementiert (Ports-and-Adapters). Engine kennt Domain + Application, nicht Infrastructure oder Api. Api verdrahtet alles ausschließlich in `Program.cs` — Controller rufen nur Application-Services und den `IDomainEventDispatcher` (Engine) auf, **niemals** direkt `OmsorgCoreDbContext`/EF Core.
|
||||
|
||||
## Rechtesystem
|
||||
|
||||
Rolle liefert Standard-Rechte (`RolePermission`: Modul × Aktion), ein individueller `UserPermissionOverride` (Grant/Revoke) gewinnt immer gegen den Rollen-Default — siehe `PermissionService.HasPermissionAsync` (`src/OmsorgCore.Application/Services/PermissionService.cs`). Deckt Blueprint 6.5 ("Rolle als Vorlage + individuelle Rechte") ab.
|
||||
|
||||
Die aufgelösten Rechte eines Users (nicht nur eine einzelne Prüfung) liefert `PermissionService.GetGrantedPermissionsAsync` als Liste von `PermissionGrant(Module, Action)`. Exponiert über `GET /api/auth/me` (`AuthController.Me`, `[Authorize]`) als `MeResponse { username, role, permissions: [{ module, action }, ...] }` — der einzige Weg, wie granulare Rechte den Client erreichen (das JWT trägt nur den Rollennamen). `omsorgapp` ruft diesen Endpunkt nach Login/Refresh auf (siehe `omsorgapp/CLAUDE.md`, "Rechtesystem im Client") und trifft UI-Entscheidungen darüber statt über einen Rollennamen-Vergleich.
|
||||
|
||||
Rechteprüfung auf Controller-Actions:
|
||||
```csharp
|
||||
[RequirePermission(ModuleType.Employees, PermissionAction.Create)]
|
||||
```
|
||||
Das Attribut (`src/OmsorgCore.Api/Security/RequirePermissionAttribute.cs`) prüft serverseitig über `IPermissionService` — nicht nur im Client (REQUIREMENTS.md NFR-9). Jeder Controller außer `AuthController` trägt zusätzlich `[Authorize]`.
|
||||
|
||||
## Auth-Flow
|
||||
|
||||
1. `POST /api/auth/login` (`AuthController`) → `AuthService.LoginAsync` prüft Username/Passwort-Hash, widerruft **alle bisherigen aktiven Refresh-Tokens dieses Users und würfelt seinen `SecurityStamp` neu** (`EndOtherSessionsAsync` — ein User hat immer nur eine aktive Session; ältere Sessions werden per Killswitch sofort ungültig, siehe "Session-Killswitch" unten), dann erzeugt `JwtTokenGenerator` ein Access-Token (Claims `sub`/`name`/`role`) + `RefreshTokenGenerator` einen langlebigen Refresh-Token. Response (`LoginResponse`): `accessToken`, `refreshToken`, `expiresAt` (camelCase, Default-JSON-Serialisierung von ASP.NET Core).
|
||||
2. Client sendet Access-Token als `Authorization: Bearer <token>`.
|
||||
3. `Program.cs` validiert das Token gegen `Jwt:Issuer`/`Jwt:Audience`/`Jwt:Secret` aus der Konfiguration.
|
||||
4. `POST /api/auth/refresh` (kein `[Authorize]` — der Refresh-Token selbst ist das Credential): `AuthService.RefreshAsync` prüft den Refresh-Token per Hash-Lookup, **rotiert** ihn (alten Token widerrufen, neuen ausstellen, per `ReplacedByTokenId` verkettet) und liefert ein neues Token-Paar. Erlaubt langlebige Sessions ohne täglichen Passwort-Login (siehe `omsorgapp/CLAUDE.md`).
|
||||
5. `POST /api/auth/logout` widerruft den vorgelegten Refresh-Token (`AuthService.RevokeAsync`, idempotent).
|
||||
|
||||
**Refresh-Token:** kein Rohtoken wird gespeichert, nur sein SHA-256-Hash (`RefreshToken`-Entity, `IRefreshTokenGenerator`). Gültigkeit über `RefreshToken:ExpiryDays` in `appsettings.json` (Default 60 Tage, sliding — jede Nutzung verlängert effektiv die Session), überschreibbar per `RefreshToken__ExpiryDays`.
|
||||
|
||||
**Secret-Handling:** `appsettings.json` enthält nur Issuer/Audience/ExpiryMinutes/RefreshToken:ExpiryDays. `appsettings.Development.json` enthält einen **lokalen Platzhalter** für `Jwt:Secret` und den Connection-String (`CHANGE_ME_...`) — für echte Umgebungen über Umgebungsvariable (`Jwt__Secret`) oder `dotnet user-secrets` überschreiben, nie ein echtes Secret einchecken.
|
||||
|
||||
**Standard-Admin-Seed:** `DbSeeder.SeedDefaultAdminAsync` (`src/OmsorgCore.Infrastructure/Persistence/DbSeeder.cs`) legt beim Start **nur im Development-Modus** (`Program.cs`, `app.Environment.IsDevelopment()`) einen Benutzer `admin`/`abersicher` mit einer neuen Rolle `Administrator` (alle `ModuleType`×`PermissionAction`-Kombinationen als `RolePermission`) an — idempotent, läuft nur wenn noch **kein** `User` existiert. DB-Fehler dabei (z. B. keine Verbindung — Migrationen sind zu diesem Zeitpunkt bereits automatisch angewendet, siehe "Datenbank" unten) sind nicht fatal, werden nur geloggt (`try/catch` um den Seed-Aufruf). Bewusst **nicht** in Produktion aktiv, um kein bekanntes Standard-Passwort auszuliefern — für einen echten Produktivbetrieb muss ein richtiger User-Anlage-Flow her.
|
||||
|
||||
## Session-Killswitch (SecurityStamp)
|
||||
|
||||
Ein Access-Token ist als JWT zustandslos gültig bis zum Ablauf (60 Min) — ein reiner Refresh-Token-Widerruf verhindert nur das *stille Verlängern*, der bereits ausgestellte Access-Token bliebe sonst bis zu 60 Minuten gültig. Für einen echten Not-Aus-Schalter (Debug-Sicht in `omsorgapp`, "alle Nutzer sofort zum Neu-Login zwingen") trägt `User.SecurityStamp` (Guid) einen Wert, der:
|
||||
1. bei jedem Access-Token-Ausstellen als Claim `"sst"` mit eingebettet wird (`JwtTokenGenerator`),
|
||||
2. bei **jedem** authentifizierten Request per `JwtBearerEvents.OnTokenValidated` (`Program.cs`) gegen den aktuellen `User.SecurityStamp` in der DB geprüft wird (`IUserRepository.GetByIdAsync`, schlanker Lookup ohne Includes) — bei Mismatch oder fehlendem Claim `context.Fail(...)`, sofort 401, unabhängig von der Token-Restlaufzeit.
|
||||
|
||||
`ISessionAdminService`/`SessionAdminService` (`src/OmsorgCore.Application/Services/`) kapselt die Admin-Operationen:
|
||||
- `GetActiveSessionsAsync()` — alle nicht widerrufenen/nicht abgelaufenen Refresh-Tokens (`IRefreshTokenRepository.GetAllActiveAsync`) als `SessionInfo` (Id, Username, CreatedAt, ExpiresAt).
|
||||
- `RevokeSessionAsync(sessionId)` — widerruft genau diesen Refresh-Token und würfelt den `SecurityStamp` nur des zugehörigen Users neu (sofortiger Kick für genau diesen Nutzer).
|
||||
- `RevokeAllSessionsAsync()` — widerruft alle aktiven Refresh-Tokens und würfelt `SecurityStamp` **aller** Nutzer neu (globaler Killswitch).
|
||||
|
||||
Exponiert über `AdminSessionsController` (`GET /api/admin/sessions`, `POST /api/admin/sessions/{id}/revoke`, `POST /api/admin/sessions/revoke-all`), geschützt über `[RequirePermission(ModuleType.UserManagement, ...)]` — per Default nur die `Administrator`-Rolle aus dem `DbSeeder`. Im Frontend: `omsorgapp/src/modules/debug/DebugSessionsPage.jsx`, im Sidebar-Menü nur sichtbar, wenn `hasPermission("UserManagement", "View")` (siehe `GET /api/auth/me` unten) — dieselbe Rechteprüfung wie serverseitig, kein reiner Rollennamen-Vergleich mehr im Client.
|
||||
|
||||
**Kosten:** ein zusätzlicher DB-Read pro authentifiziertem Request (`GetByIdAsync`). Für die aktuelle Nutzerzahl vernachlässigbar — bei relevantem Traffic-Wachstum wäre ein Cache (z. B. In-Memory mit kurzer TTL) der nächste Schritt, aber kein Caching ohne echten Bedarf, um die Sofortigkeit des Killswitches nicht zu unterlaufen.
|
||||
|
||||
## Passwort-Reset / E-Mail-Versand
|
||||
|
||||
Vollständig implementiert: `PasswordResetCode`-Entity + `PasswordResetService` (PIN anfordern/verifizieren, Passwort setzen, siehe Auth-Flow-Analogie: Session-Revoke + `SecurityStamp`-Rotation nach erfolgreichem Reset), exponiert über `AuthController` (`POST /api/auth/forgot-password/{request,verify,reset}`).
|
||||
|
||||
**E-Mail-Versand:** `IEmailSender` (`OmsorgCore.Application.Abstractions`) hat zwei Implementierungen in `OmsorgCore.Email`, Auswahl über `Email:Provider` zur Startzeit (`DependencyInjection.AddEmail`):
|
||||
- `"Console"` (Default) — schreibt nur ins Log, kein echter Versand. Sicherer Default für Umgebungen ohne SMTP-Konfiguration.
|
||||
- `"Smtp"` — echter Versand über MailKit (`SmtpEmailSender`). **Ein gemeinsamer SMTP-Server (`Email:Smtp:*`), darüber mehrere benannte Accounts (`Email:Accounts:<Name>:*`)** — z. B. ein Account, der exklusiv für Passwort-Reset-Mails verwendet wird, getrennt von einem künftigen allgemeinen Absender. Welcher Account für den Reset-Flow gilt, steht in `Email:PasswordResetAccount` (Default `"PasswordReset"`); `EmailMessage.FromAccountKey` transportiert die Auswahl vom Aufrufer bis zum `SmtpEmailSender`. Details der Keys: `CONFIGURATION.md`. **Zugangsdaten (Host/Username/Passwort) kommen ausschließlich aus appsettings/Env-Var/user-secrets, nie über eine API — es gibt bewusst keinen Settings-Controller oder UI-Formular dafür.**
|
||||
|
||||
**Templating:** Betreff/Text der Passwort-Reset-Mail sind über `Email:PasswordResetTemplate:{Subject,BodyTemplate}` konfigurierbar, Platzhalter im Format `$NAME$` (`$RESET_PIN$`, `$RESET_PIN_EXPIRY_MINUTES$`), ersetzt von `EmailTemplateRenderer.Render` (pure Funktion, unit-getestet in `EmailTemplateRendererTests`). Die Zusammensetzung passiert in `PasswordResetEmailComposer` (`OmsorgCore.Email`, implementiert `IPasswordResetEmailComposer` aus `Application.Abstractions`) — bewusst nicht direkt im `Engine`-Handler, weil `Engine` nur `Domain`+`Application` kennen darf, nicht `Email` (Abhängigkeitsrichtung, siehe oben). `PasswordResetRequestedHandler` (`Engine/Handlers/`) ruft nur `IPasswordResetEmailComposer.Compose(...)` + `IEmailSender.SendAsync(...)` auf.
|
||||
|
||||
**Testversand ohne Zugangsdaten-Leak:** `AdminEmailController` (`POST /api/admin/email/test-send`, `[RequirePermission(ModuleType.UserManagement, PermissionAction.Edit)]`) verschickt eine feste Testmail über den aktuell konfigurierten `IEmailSender` — Host/Username/Passwort verlassen dabei nie den Server, nur Erfolg/Fehlschlag geht an den Client. Im Frontend: `omsorgapp/src/modules/debug/DebugSessionsPage.jsx`, gleicher Rechteschutz wie die Sessions-Verwaltung dort.
|
||||
|
||||
## Datenbank
|
||||
|
||||
- Connection-String-Key: `ConnectionStrings:OmsorgCore` (Format `Host=...;Port=5432;Database=omsorg_core;Username=...;Password=...`).
|
||||
- `dotnet-ef` ist als lokales Tool eingerichtet (`.config/dotnet-tools.json`) — vor erster Nutzung `dotnet tool restore`. Wird nur noch zum **Erzeugen** neuer Migrationen gebraucht (`dotnet ef migrations add ...`), nicht mehr zum Anwenden.
|
||||
- **`Program.cs` ruft bei jedem Start `db.Database.MigrateAsync()` auf, in jeder Umgebung** (nicht nur Development) — ausstehende Migrationen werden automatisch angewendet, bevor der Server Requests annimmt. Ein manuelles `dotnet ef database update` ist dadurch nur noch zum gezielten Vorab-Prüfen/Debuggen einer Migration nötig, nicht mehr für den normalen Start/Deploy. Schlägt die Migration fehl, crasht der Start bewusst fatal (fail-fast) statt mit einem veralteten Schema weiterzulaufen.
|
||||
- Migrationen `InitialCreate`, `AddRefreshTokens`, `AddUserSecurityStamp`, `AddEmployeeContactFieldConstraints` und `AddAuditableSoftDelete` existieren (`src/OmsorgCore.Infrastructure/Persistence/Migrations/`) und wurden erfolgreich gegen eine echte PostgreSQL-Instanz angewendet.
|
||||
|
||||
## Build- und Run-Befehle
|
||||
|
||||
```bash
|
||||
cd omsorgCore
|
||||
dotnet build # ganze Solution
|
||||
dotnet tool restore # einmalig, für dotnet-ef
|
||||
|
||||
# Neue Migration erzeugen, nachdem sich das Entity-Modell geändert hat (wird NICHT automatisch angewendet):
|
||||
dotnet tool run dotnet-ef migrations add <Name> \
|
||||
--project src/OmsorgCore.Infrastructure/OmsorgCore.Infrastructure.csproj \
|
||||
--startup-project src/OmsorgCore.Api/OmsorgCore.Api.csproj
|
||||
# Angewendet wird sie automatisch beim nächsten Start der API (siehe oben) — kein manueller
|
||||
# `database update`-Schritt im Normalfall mehr nötig.
|
||||
|
||||
# API starten (lädt appsettings.Development.json):
|
||||
ASPNETCORE_ENVIRONMENT=Development dotnet run --project src/OmsorgCore.Api
|
||||
# → GET /api/health, POST /api/auth/login, GET/POST /api/employees (Bearer-Token nötig)
|
||||
# Swagger UI unter /swagger im Development-Modus
|
||||
```
|
||||
|
||||
## Verifiziert
|
||||
|
||||
- `dotnet build` für alle 6 Projekte: grün. `dotnet test`: grün (u. a. `AuthServiceTests` mit Fake-Repositories für Login/Refresh-Rotation/Revoke).
|
||||
- `dotnet tool run dotnet-ef database update` erfolgreich gegen echte PostgreSQL-Instanz ausgeführt (`InitialCreate` + `AddRefreshTokens`).
|
||||
- API-Start mit `ASPNETCORE_ENVIRONMENT=Development`: `GET /api/health` → `{"status":"ok","databaseReachable":true}`. `POST /api/auth/login`/`refresh` mit falschen/unbekannten Credentials → 401, `POST /api/auth/logout` → 204. `GET /api/employees` ohne Token → 401 (Auth-Pipeline greift korrekt).
|
||||
- `omsorgapp`s `authClient.cjs` erfolgreich gegen den laufenden Server getestet (Login/Refresh/Logout-Fehlerfälle).
|
||||
- **Kompletter Login-Flow end-to-end mit echtem Postgres verifiziert:** Login mit `admin`/`abersicher` (Seed) → gültiges Token-Paar; `refresh` rotiert korrekt (neues Paar, alter Refresh-Token danach 401 bei Wiederverwendung); `GET /api/employees` mit frischem Access-Token → 200 (Administrator-Rolle hat volle Rechte über den Seed).
|
||||
- **Session-Killswitch end-to-end verifiziert:** `GET /api/admin/sessions` liefert aktive Sessions; `POST /api/admin/sessions/revoke-all` → 204, danach liefert **derselbe, zuvor gültige Access-Token sofort 401** (nicht erst nach Ablauf) und der zugehörige Refresh-Token liefert bei `POST /api/auth/refresh` ebenfalls 401. Erneuter Login mit `admin`/`abersicher` funktioniert danach wieder normal.
|
||||
|
||||
## Offene Punkte
|
||||
|
||||
- Facility/Contract/Order/TimeEntry/Invoice haben noch keine Controller/Repositories/Services — nur Domain-Entitäten + DB-Konfiguration. Nächste Schritte folgen demselben Muster wie `Employee` (Repository-Interface in Application, Implementierung in Infrastructure, Service in Application, Controller in Api).
|
||||
- Keine E-Mail-Verifizierung bei User-Anlage (Passwort-Reset per E-Mail ist fertig, siehe "Passwort-Reset / E-Mail-Versand" oben).
|
||||
- `omsorgapp` spricht seit Kurzem gegen dieses Backend (Login-Screen + Refresh-Token-Session, siehe `omsorgapp/CLAUDE.md`) — `omsorgWeb` ist noch nicht angebunden.
|
||||
- Kein Docker-/CI-Setup.
|
||||
|
||||
## Die sechs Core-Objekte (Domain-Entitäten)
|
||||
|
||||
Aus `REQUIREMENTS.md` Abschnitt 6 / Blueprint Kap. 19: **Mitarbeiter, Einrichtung, Vertrag, Auftrag, Zeiterfassung, Rechnung** — als `Employee`, `Facility`, `Contract`, `Order`, `TimeEntry`, `Invoice` in `src/OmsorgCore.Domain/Entities/` angelegt, mit Kernfeldern (nicht vollständig ausmodelliert).
|
||||
|
||||
Verbindliche Regeln für das Datenmodell:
|
||||
1. Jede Entität hat eine eindeutige `Guid Id` (siehe `Entity`-Basisklasse).
|
||||
2. Beziehungen ausschließlich über Foreign Keys/IDs, keine redundante Texteingabe verwandter Daten.
|
||||
3. Stammdaten nur an einer Stelle — kein Feld, das auch in `omsorgWeb` oder `omsorgapp` unabhängig gepflegt wird, sobald die Migration dorthin begonnen hat.
|
||||
4. Änderungen an geschäftsrelevanten Daten müssen nachvollziehbar sein — `AuditableEntity` liefert `CreatedAt`/`UpdatedAt`; ein vollständiger Audit-Trail (wer hat was geändert) ist noch nicht gebaut.
|
||||
5. Kein Hard-Delete für sensible/geschäftsrelevante Daten — Soft-Delete/Archivierung (noch nicht implementiert, bei Bedarf einbauen statt Datensätze zu löschen).
|
||||
6. Berechtigungsprüfung auf Daten- und Funktionsebene (siehe Rechtesystem oben und Rechtematrix in `REQUIREMENTS.md` Abschnitt 7).
|
||||
7. Rechnungen entstehen ausschließlich aus freigegebener Zeiterfassung (FR-ZE-3/FR-RE-1) — muss bei Ausbau von `TimeEntry`/`Invoice` auf Domain-/Application-Ebene erzwungen werden, nicht nur als UI-Regel im Client.
|
||||
|
||||
## Event-Schicht (Engine) — Funktionsweise
|
||||
|
||||
`IDomainEventDispatcher` (Singleton, In-Process) löst über den DI-Container alle registrierten `IDomainEventHandler<TEvent>` für ein Event auf und ruft sie auf. Beispiel: `EmployeesController.Create` ruft nach dem Speichern `_dispatcher.DispatchAsync(new EmployeeCreatedEvent(created.Id))` auf, `EmployeeCreatedHandler` reagiert darauf (aktuell nur Logging). Neue Trigger aus `REQUIREMENTS.md` Abschnitt 4.10 (Krankmeldung, fehlender Tätigkeitsnachweis, Vertragsende, ...) folgen demselben Muster: Event-Klasse in `Engine/Events/`, Handler in `Engine/Handlers/`, Registrierung in `Engine/DependencyInjection.cs`, Dispatch-Aufruf an der Stelle im Api-Layer, wo das auslösende Ereignis passiert.
|
||||
|
||||
**Wichtig:** `Application`-Services dispatchen bewusst *nicht* selbst (sie kennen `Engine` nicht, das würde die Abhängigkeitsrichtung verletzen) — das Dispatchen passiert im Api-Layer, der als einziger alle Schichten kennt.
|
||||
Reference in New Issue
Block a user