Files
Rbm.FamStack/docs/ARCHITECTURE.md
T
2026-10-09 18:54:02 +02:00

14 KiB
Raw Blame History

Architektur – Digitaler Familienplaner (FamilyPlanner)

1. Überblick

FamilyPlanner ist eine Progressive Web App für Familien (Einkaufsliste, Essenplan, Aufgaben/Ämtchen, Kalender mit ICS-Abo, Chat). Lokal wird alles über .NET Aspire orchestriert, produktiv läuft es per Docker Compose auf einem Linux-Host.

                  Browser / installierte PWA (Blazor WebAssembly)
                                 │  HTTPS, same-origin, Auth-Cookie
                                 ▼
┌───────────────────────────────────────────────────────────────┐
│ FamilyPlanner.Web  (BFF)                                      │
│  • liefert WASM-App, Manifest, Service Worker (statisch)      │
│  • YARP: /api/**, /hubs/**, /ics/** ──────────────┐            │
│  • Security-Header (CSP, HSTS …)                  │            │
└───────────────────────────────────────────────────┼───────────┘
                                                    ▼
┌───────────────────────────────────────────────────────────────┐
│ FamilyPlanner.Api                                             │
│  • Minimal APIs (Vertical Slices pro Feature)                 │
│  • ASP.NET Core Identity + Cookie-Auth, Policies              │
│  • SignalR Hubs (Chat, Live-Updates)                          │
│  • ICS-Feeds (Token-gesichert), ICS-Import                    │
└───────────────────────────┬───────────────────────────────────┘
                            │ EF Core 10
                            ▼
                     SQL Server (Volume)
                            ▲
┌───────────────────────────┴───────────────────────────────────┐
│ FamilyPlanner.MigrationService (Worker, läuft einmal)          │
│  • wendet EF-Migrations an, seedet Rollen + Initial-Admin      │
└───────────────────────────────────────────────────────────────┘

Siehe ADRs in docs/adr/ für die Begründungen.

2. Projektstruktur

Projekt Zweck
src/FamilyPlanner.AppHost Aspire-Orchestrierung: SQL Server (persistentes Volume), Datenbank, MigrationService, Api, Web; Docker-Compose-Publishing
src/FamilyPlanner.ServiceDefaults OpenTelemetry, Health Checks (/health, /alive), Service Discovery, Resilience
src/FamilyPlanner.Domain Entities, Value Objects, Rollen-/Berechtigungsregeln, reine Domänenlogik (keine Abhängigkeiten)
src/FamilyPlanner.Infrastructure EF Core AppDbContext, Identity, Migrations, Seeding, Interceptors (Audit/Soft Delete)
src/FamilyPlanner.Shared DTOs/Contracts zwischen Api und WASM-Client
src/FamilyPlanner.Api Minimal APIs (Features/<Feature>/…), Auth, SignalR, ICS
src/FamilyPlanner.MigrationService Worker: Migrations + Seeding beim Start, beendet sich danach
src/FamilyPlanner.Web Blazor-Host/BFF: statische Auslieferung der WASM-App + YARP-Proxy
src/FamilyPlanner.Web.Client Blazor WebAssembly (MudBlazor), PWA
tests/FamilyPlanner.UnitTests Domänenlogik, Rollen/Policies, Parser, ICS
tests/FamilyPlanner.IntegrationTests End-to-End über Aspire.Hosting.Testing (echter SQL-Server-Container, flüchtige DB)
deploy/ docker-compose.yml, .env.example, Caddyfile, Backup-/Restore-Skripte
tools/ Hilfsskripte (z. B. make_icons.py zum Erzeugen der PWA-Icons)
tests/FamilyPlanner.Web.Tests bUnit-Komponententests

Vertical Slices in der Api

FamilyPlanner.Api/
  Features/
    Auth/AuthEndpoints.cs          # /api/auth/*
    Users/…                        # /api/admin/users/* (Phase 2)
    Shopping/…                     # /api/shopping/*   (Phase 3)
    Meals/…  Tasks/…  Calendar/…  Chat/…  Dashboard/…
  Hubs/                            # SignalR
  Infrastructure/                  # Auth-Setup, Security, Rate Limiting

Jeder Slice enthält Endpunkte, Validatoren (FluentValidation) und ggf. Handler. Geteilte Regeln liegen im Domain-Projekt.

3. Datenmodell (Ziel, wird phasenweise umgesetzt)

Alle Entitäten erben von AuditableEntity (Id: Guid, CreatedAt, UpdatedAt, CreatedBy, UpdatedBy). Löschbare Entitäten implementieren ISoftDeletable (IsDeleted, DeletedAt) – globaler Query-Filter. Einkaufsliste, Aufgaben und Termine haben eine RowVersion (Optimistic Concurrency). Alle Zeitpunkte werden als UTC (DateTimeOffset/DateTime UTC) gespeichert und in Europe/Berlin angezeigt.

Bereich Entitäten
Identity ✅ AppUser (DisplayName, Color, AvatarUrl, MustChangePassword, IsActive, ExpiresAt, PinHash, LastLoginAt, Soft Delete), AppRole (Admin, Familie, Kind, Gast), UserAvatar (Bild ≤ 1 MB)
Einstellungen ✅ FamilySettings (FamilyName, TimeZoneId, MealPlanDays, MealTypes, TaskCategories, ChorePointsEnabled, ChildTasksRequireApproval, GuestsCanEditShoppingList, GuestChatEnabled)
Einkauf ShoppingList (Name, IsDefault), ShoppingItem (Text, Quantity, Category, IsDone, DoneAt, SortOrder), ShoppingSuggestion (Text, UseCount)
Essen MealPlanEntry (Date, MealType, Dish, Note, RecipeUrl, Ingredients), Dish (Name, Ingredients – für Autovervollständigung), MealWish (von Kindern)
Aufgaben TodoTask (Title, Notes, DueAt, HasTime, Priority, Status, Category, IsPrivate, IsChore, Points, NeedsApproval, RecurrenceRule, SeriesId), TaskAssignment, TaskChecklistItem, TaskCompletion (wer/wann/genehmigt), TaskCategory, PointsLedger
Kalender Calendar (Name, Color), CalendarPermission (Rolle/Benutzer → Read/Write), CalendarEvent (Title, StartUtc, EndUtc, AllDay, Location, Notes, RRule, ExDates, Uid), EventPersonTag, EventException (Overrides), IcsFeedToken (Token-Hash, Scope, Owner, RevokedAt)
Chat ChatChannel (Family / Direct), ChatMember (LastReadAt), ChatMessage (Text, Attachment), ChatReaction
Betrieb AuditLogEntry ✅ (ActorName, Action, TargetType/Id/Name, Details, CreatedAt), PushSubscription (vorbereitet)

4. Rollen- und Berechtigungsmodell

Rollen: Admin, Familie, Kind, Gast (Konstanten in FamilyPlanner.Domain.Roles). Die Rollenmatrix der Anforderungen ist als reine Funktion PermissionMatrix im Domain-Projekt abgebildet (ADR-0008): je Permission und Rolle eine Zugriffsstufe Full (F), Restricted (R) = erreichbar, aber objektbezogene Prüfung im Slice, oder None (–). Die Api registriert je Permission eine Policy Permission:<Name>; Endpunkte nutzen .RequirePermission(…).

Permission Admin Familie Kind Gast Bedeutung von R
DashboardView F F F R Gast: eingeschränkte Kacheln
ShoppingRead F F F F
ShoppingWrite F F F – ¹
ShoppingDelete F F R – nur eigene Einträge
MealPlanRead F F F F
MealPlanEdit F F R – nur Wünsche vorschlagen
TasksRead F F R R Kind: eigene + Familie; Gast: nur zugewiesene
TasksEdit F F R – nur eigene, private Aufgaben
TasksAssign F F – –
TasksCompleteAssigned F F F F
CalendarRead F F F R nur freigegebene Kalender
EventsEdit F F R – nur eigene Termine
FamilyChat F F F – ²
DirectMessages F F R – nur an Familienmitglieder
UserManagement, CalendarManagement, SettingsManagement, AuditLogView F – – –

¹ F, wenn „Gäste dürfen die Einkaufsliste ergänzen/abhaken“ aktiviert ist. ² F, wenn „Gäste dürfen den Familienchat nutzen“ aktiviert ist.

Zusätzlich gibt es grobe Rollen-Policies (Admin, Parent, FamilyMember, AnyRole). Alle Policies – auch die Fallback-Policy für jeden Endpunkt ohne eigene Angabe – verlangen: angemeldet, Konto nicht abgelaufen (Claim fp:expires_at) und kein ausstehender Passwortwechsel. Ein Benutzer mit MustChangePassword = true erreicht nur /api/auth/*.

Das UI bekommt die wirksamen Berechtigungen über /api/auth/me und blendet anhand derselben Policy-Namen aus; es ist nie die einzige Sperre.

5. Authentifizierung

  • ASP.NET Core Identity (Api), Cookie __Host-fp.auth (HttpOnly, Secure, SameSite=Strict, Path=/).
  • Der Browser spricht ausschließlich mit dem Web-Host (same-origin); YARP leitet /api und /hubs an die Api weiter. Dadurch ist das Cookie First-Party, und es werden keine Tokens im Browser gespeichert (BFF-Prinzip, ADR-0002).
  • Bei 401 liefert die Api JSON-Status statt Redirect (OnRedirectToLogin → 401/403).
  • Anti-Forgery: SameSite=Strict + Pflicht-Header X-Requested-With: FamilyPlanner auf allen nicht-GET-Requests (Custom-Header-Check, kann von fremden Origins nicht ohne CORS-Preflight gesetzt werden).
  • Login per Benutzername oder E-Mail, „Angemeldet bleiben“ (persistentes Cookie, 30 Tage gleitend), Lockout (5 Fehlversuche → 15 min), Rate Limiting (Fixed Window pro IP auf /api/auth/login).
  • Seed-Admin aus Konfiguration (Seed:AdminUserName, Seed:AdminPassword, Seed:AdminEmail), MustChangePassword = true.
  • Kalender-Abos (ICS) verwenden geheime Tokens in der URL (nur Hash in DB), da Kalender-Clients keine Cookies können (ADR-0005).

6. API-Übersicht (Ziel, ✅ = umgesetzt)

Bereich Endpunkte
Auth ✅ POST /api/auth/login, POST /api/auth/pin-login (Feature-Flag), POST /api/auth/logout, GET /api/auth/me, POST /api/auth/change-password
Admin – Benutzer ✅ GET/POST /api/admin/users, GET/PUT/DELETE /api/admin/users/{id}, POST …/{id}/reset-password, POST …/{id}/unlock, PUT …/{id}/pin, PUT/DELETE …/{id}/avatar
Admin – Einstellungen ✅ GET /api/settings (alle), PUT /api/admin/settings, GET /api/admin/audit?page=&pageSize=
Benutzerverzeichnis ✅ GET /api/users, GET /api/users/{id}/avatar
Einkauf GET/POST /api/shopping/lists, GET/POST /api/shopping/lists/{id}/items, PATCH/DELETE /api/shopping/items/{id}, POST /api/shopping/lists/{id}/clear-done, GET /api/shopping/suggestions?q=
Essen GET /api/meals?from=&to=, PUT /api/meals/{date}/{mealType}, POST /api/meals/{id}/move, POST /api/meals/{id}/to-shopping, GET/POST /api/meals/wishes
Aufgaben GET/POST /api/tasks, PUT/DELETE /api/tasks/{id}, POST /api/tasks/{id}/complete, POST /api/tasks/{id}/approve, GET /api/tasks/points
Kalender GET /api/calendars, GET /api/events?from=&to=, POST/PUT/DELETE /api/events…, POST /api/calendars/{id}/import, GET/POST/DELETE /api/feeds
ICS-Feed GET /ics/{token}.ics (öffentlich, Token-gesichert, ETag/Last-Modified)
Chat GET /api/chat/channels, GET /api/chat/channels/{id}/messages?before=, POST …/messages, POST …/read
Dashboard GET /api/dashboard
Hubs /hubs/live (Live-Updates: Einkauf, Aufgaben, Termine, Dashboard), /hubs/chat
Betrieb /health, /alive

7. Frontend

  • Blazor Web App, Interactive WebAssembly global, Prerendering aus (ADR-0002). Der Web-Host rendert nur die HTML-Hülle.
  • MudBlazor als Komponentenbibliothek (lokal ausgeliefert, kein CDN), deutsches UI. Lokalisierung vorbereitet: Kultur de-DE, AddLocalization() registriert, alle Globalisierungsdaten geladen; Texte stehen derzeit direkt in den Komponenten und werden bei Bedarf in Resources/*.resx überführt.
  • Ordner Pages/ erfordert per _Imports.razor die Policy PasswordChanged; Public/ enthält Login, Passwortwechsel und 404.
  • Layout: MudLayout mit AppBar, Drawer (Desktop) bzw. Bottom-Navigation (Smartphone), Dark Mode (System-Default, umschaltbar, in localStorage).
  • Eigener AuthenticationStateProvider (ApiAuthenticationStateProvider) holt /api/auth/me und liefert Claims inkl. Rollen.
  • PWA: manifest.webmanifest, Icons 72–512 px + maskable, eigener Service Worker (App-Shell-Cache, Network-first für Navigation, keine Caches für /api//hubs), Offline-Banner. Hintergrund-Sync für die Einkaufsliste über IndexedDB-Outbox ab Phase 3.

8. Echtzeit

SignalR-Hubs in der Api, vom Web-Host per YARP (WebSockets) durchgereicht. Gruppen: family, user:{id}, channel:{id}. Änderungen in Slices publizieren Events (ILiveNotifier), die Clients aktualisieren gezielt ihre Ansichten.

9. Betrieb / Deployment

  • Lokal: aspire run bzw. dotnet run --project src/FamilyPlanner.AppHost.
  • Produktion: deploy/docker-compose.yml (SQL Server + Volume, migrations, api, web, optional Caddy für HTTPS). Images werden mit dem gemeinsamen Dockerfile (Build-Argument PROJECT) gebaut; alle Container laufen als Non-Root (app-User der Microsoft-Images, SQL Server als mssql).
  • Backups: deploy/scripts/backup.sh (SQL-Backup ins Volume sql-backup + Kopie nach deploy/backups/), deploy/scripts/restore.sh.
  • aspire publish mit AddDockerComposeEnvironment("compose") erzeugt alternativ eine Compose-Datei aus dem App-Modell.
  • Konfiguration ausschließlich per Umgebungsvariablen/.env (siehe deploy/.env.example), lokal User Secrets / Aspire-Parameter.
  • Health Checks: /alive (Liveness), /health (Readiness inkl. DB bei der Api) – auch in Produktion gemappt (nur Status-Text, keine Details). Der Web-Host beantwortet /health selbst und leitet es nicht an die Api weiter.
  • Data Protection Keys werden in der Datenbank gespeichert (Api), damit Cookies Container-Neustarts überleben.

10. Querschnitt

  • Nullable, TreatWarningsAsErrors, zentrales Paketmanagement (Directory.Packages.props), .editorconfig.
  • Validierung: FluentValidation in jedem Slice.
  • Logging/Tracing/Metriken: OpenTelemetry (OTLP → Aspire-Dashboard).
  • Zeitzone: TimeProvider + TimeZoneInfo.FindSystemTimeZoneById("Europe/Berlin") (IANA-ID, funktioniert auf Linux und Windows mit ICU).