14 KiB
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
/apiund/hubsan 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: FamilyPlannerauf 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 inResources/*.resxüberführt. - Ordner
Pages/erfordert per_Imports.razordie PolicyPasswordChanged;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/meund 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 runbzw.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 gemeinsamenDockerfile(Build-ArgumentPROJECT) gebaut; alle Container laufen als Non-Root (app-User der Microsoft-Images, SQL Server alsmssql). - Backups:
deploy/scripts/backup.sh(SQL-Backup ins Volumesql-backup+ Kopie nachdeploy/backups/),deploy/scripts/restore.sh. aspire publishmitAddDockerComposeEnvironment("compose")erzeugt alternativ eine Compose-Datei aus dem App-Modell.- Konfiguration ausschließlich per Umgebungsvariablen/
.env(siehedeploy/.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/healthselbst 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).