diff --git a/.gitignore b/.gitignore index f1653ef..816fcb1 100644 --- a/.gitignore +++ b/.gitignore @@ -131,6 +131,9 @@ $RECYCLE.BIN/ # MemPalace per-project files (issue #185) mempalace.yaml entities.json - -# Runtime photo store (uploaded/imported gerbil photos) — never commit -GerbilManagerWebAPI/photo-storage/ + +# Runtime photo store (uploaded/imported gerbil photos) — never commit +GerbilManagerWebAPI/photo-storage/ + +# AR-3: Data Protection key ring (dev-only ephemeral keys) — never commit +GerbilManagerWebAPI/.data-protection-keys/ diff --git a/GerbilManagerWebAPI/Program.cs b/GerbilManagerWebAPI/Program.cs index e2a47b0..b7ee181 100644 --- a/GerbilManagerWebAPI/Program.cs +++ b/GerbilManagerWebAPI/Program.cs @@ -1,5 +1,6 @@ using System.Text.Json.Serialization; using GerbilManagerWebAPI.Endpoints; +using Microsoft.AspNetCore.DataProtection; using Microsoft.EntityFrameworkCore; using Scalar.AspNetCore; @@ -46,7 +47,17 @@ builder.Services.AddHttpClient( http => http.Timeout = TimeSpan.FromSeconds(60)); // INBOX-0: Gmail inbox. App Password encrypted at rest via Data Protection. -builder.Services.AddDataProtection(); +// AR-3: persist the key ring so encrypted passwords survive image redeployments. +// In prod the path is mounted to a persistent volume (compose DataProtection__KeyRingPath). +// In dev (Aspire) keys live in the content root — ephemeral, which is fine there. +{ + var keyRingPath = builder.Configuration["DataProtection:KeyRingPath"] + ?? Path.Combine(builder.Environment.ContentRootPath, ".data-protection-keys"); + Directory.CreateDirectory(keyRingPath); + builder.Services.AddDataProtection() + .PersistKeysToFileSystem(new DirectoryInfo(keyRingPath)) + .SetApplicationName("GerbilManager"); +} builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/deploy/truenas/.env.example b/deploy/truenas/.env.example index f005d37..1fe057b 100644 --- a/deploy/truenas/.env.example +++ b/deploy/truenas/.env.example @@ -15,9 +15,15 @@ TAG=latest PGDATA_PATH=/mnt/SSD/gerbil/pgdata PHOTOS_PATH=/mnt/SSD/gerbil/photos BACKUPS_PATH=/mnt/SSD/gerbil/backups +# AR-3: Data Protection Key-Ring (Gmail-App-Passwort-Verschlüsselung) +KEYS_PATH=/mnt/SSD/gerbil/keys # Backup-Rotation: Anzahl Tage (Standard: 7) BACKUP_KEEP_DAYS=7 -# Claude-API-Key fuer KI-Verkaufstext (FEAT-12a; leer lassen wenn nicht vorhanden) -ANTHROPIC_API_KEY= +# KI-Funktionen (Verkaufstext + Posteingang-Entwurf) +# Beliebiger OpenAI-kompatibler Anbieter — Optionen in docs/ai-provider.md +# Leer lassen = KI deaktiviert (kein Fehler, nur 503 AiKeyMissing) +AI__BaseUrl= +AI__ApiKey= +AI__Model=gemini-2.0-flash diff --git a/deploy/truenas/compose.yaml b/deploy/truenas/compose.yaml index 6810c8b..4c19904 100644 --- a/deploy/truenas/compose.yaml +++ b/deploy/truenas/compose.yaml @@ -40,10 +40,17 @@ services: ConnectionStrings__gerbilmanager: "Host=db;Port=5432;Database=gerbilmanager;Username=postgres;Password=${POSTGRES_PASSWORD}" # Speicherort der hochgeladenen Fotos (NAS-Dataset gemounted unter /data/photos) Photos__RootPath: /data/photos - # KI-Verkaufstext (FEAT-12a stub; leer lassen wenn kein Key vorhanden) - ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:-}" + # AR-3: Data Protection Key-Ring (persistiert Gmail-App-Passwort-Verschlüsselung über Redeployments) + DataProtection__KeyRingPath: /data/keys + # AR-4: KI-Funktionen (Verkaufstext + Posteingang-Entwurf, Sektion AI; beliebiger OpenAI-kompatibler Anbieter) + # Anbieter-Optionen und Schlüssel-Beispiele: docs/ai-provider.md + # Leer lassen = KI deaktiviert (503 AiKeyMissing statt Fehler) + AI__BaseUrl: "${AI__BaseUrl:-}" + AI__ApiKey: "${AI__ApiKey:-}" + AI__Model: "${AI__Model:-gemini-2.0-flash}" volumes: - photos:/data/photos + - keys:/data/keys depends_on: db: condition: service_healthy @@ -101,6 +108,14 @@ volumes: type: none o: bind device: "${PHOTOS_PATH:-/mnt/gerbil/photos}" + # AR-3: Data Protection key ring — persistiert Gmail-App-Passwort-Verschlüsselung. + # Muss ein persistentes NAS-Dataset sein (nicht dasselbe wie photos). + keys: + driver: local + driver_opts: + type: none + o: bind + device: "${KEYS_PATH:-/mnt/gerbil/keys}" backups: driver: local driver_opts: