Compare commits

...

2 Commits

Author SHA1 Message Date
865b3831c8 Merge feature/ops-fixes: DataProtection-Key-Persistenz (AR-3) + prod-compose AI-Env (AR-4)
Some checks failed
CI / Backend Tests (.NET) (push) Successful in 52s
CI / Docker Build & Push (push) Has been cancelled
CI / Frontend Tests (Node/Vite) (push) Has been cancelled
- AR-3: AddDataProtection().PersistKeysToFileSystem + compose keys-Volume → Gmail-App-Passwort
  ueberlebt Redeploy (war ephemer → Inbox waere still gebrochen). Dev-Fallback .data-protection-keys.
- AR-4: prod compose.yaml ANTHROPIC_API_KEY → AI__BaseUrl/ApiKey/Model + .env.example aktualisiert.
139/139, has-pending=No, kein Schema-Change. [god-QA: config/Program.cs only, disjunkt]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:03:20 +02:00
1b0d3286db OPS-FIXES-1: AR-3 Data Protection Key-Persistenz + AR-4 AI-Env-Korrekte
AR-3 (P1): PersistKeysToFileSystem + persistentes Volume
  Program.cs: AddDataProtection().PersistKeysToFileSystem(keyRingPath).SetApplicationName(GerbilManager)
  Pfad konfigurierbar via DataProtection:KeyRingPath (env DataProtection__KeyRingPath);
  Fallback = ContentRoot/.data-protection-keys (Aspire-Dev-ephemeral, ok).
  compose.yaml: DataProtection__KeyRingPath: /data/keys + Volume-Mount keys:/data/keys.
  Volumes: neues 'keys' Volume (Bind-Mount auf NAS-Dataset KEYS_PATH=/mnt/SSD/gerbil/keys).
  .gitignore: .data-protection-keys/ ignoriert (Dev-only ephemeral keys).
  Verhindert: Gmail-App-Passwort wird nach Image-Redeploy unlesbar (bisher stilles inbox-fail).

AR-4 (P1): compose.yaml + .env.example: AI__* statt ANTHROPIC_API_KEY
  compose.yaml: ANTHROPIC_API_KEY entfernt (Code liest es nicht). Korrekte Vars:
    AI__BaseUrl: ${AI__BaseUrl:-}
    AI__ApiKey: ${AI__ApiKey:-}
    AI__Model: ${AI__Model:-gemini-2.0-flash}
  .env.example: AI__BaseUrl/ApiKey/Model + KEYS_PATH hinzugefuegt; ANTHROPIC_API_KEY entfernt.
  Quelle: docs/ai-provider.md (war korrekt, compose war falsch).
  Verhindert: alle 4 KI-Features (Verkaufstext, Inbox-Entwurf) blieben in prod stumm.

GATE: 139/139 C#-Tests, build gruen (using Microsoft.AspNetCore.DataProtection; framework-included).
2026-06-06 18:01:57 +02:00
4 changed files with 43 additions and 8 deletions

3
.gitignore vendored
View File

@@ -134,3 +134,6 @@ entities.json
# Runtime photo store (uploaded/imported gerbil photos) — never commit # Runtime photo store (uploaded/imported gerbil photos) — never commit
GerbilManagerWebAPI/photo-storage/ GerbilManagerWebAPI/photo-storage/
# AR-3: Data Protection key ring (dev-only ephemeral keys) — never commit
GerbilManagerWebAPI/.data-protection-keys/

View File

@@ -1,5 +1,6 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using GerbilManagerWebAPI.Endpoints; using GerbilManagerWebAPI.Endpoints;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Scalar.AspNetCore; using Scalar.AspNetCore;
@@ -46,7 +47,17 @@ builder.Services.AddHttpClient<GerbilManagerWebAPI.Inbox.DraftReplyService>(
http => http.Timeout = TimeSpan.FromSeconds(60)); http => http.Timeout = TimeSpan.FromSeconds(60));
// INBOX-0: Gmail inbox. App Password encrypted at rest via Data Protection. // 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<GerbilManagerWebAPI.Inbox.MailSettingsService>(); builder.Services.AddScoped<GerbilManagerWebAPI.Inbox.MailSettingsService>();
builder.Services.AddScoped<GerbilManagerWebAPI.Inbox.IGmailMailReader, GerbilManagerWebAPI.Inbox.GmailMailReader>(); builder.Services.AddScoped<GerbilManagerWebAPI.Inbox.IGmailMailReader, GerbilManagerWebAPI.Inbox.GmailMailReader>();
builder.Services.AddScoped<GerbilManagerWebAPI.Inbox.RequestSyncService>(); builder.Services.AddScoped<GerbilManagerWebAPI.Inbox.RequestSyncService>();

View File

@@ -15,9 +15,15 @@ TAG=latest
PGDATA_PATH=/mnt/SSD/gerbil/pgdata PGDATA_PATH=/mnt/SSD/gerbil/pgdata
PHOTOS_PATH=/mnt/SSD/gerbil/photos PHOTOS_PATH=/mnt/SSD/gerbil/photos
BACKUPS_PATH=/mnt/SSD/gerbil/backups 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-Rotation: Anzahl Tage (Standard: 7)
BACKUP_KEEP_DAYS=7 BACKUP_KEEP_DAYS=7
# Claude-API-Key fuer KI-Verkaufstext (FEAT-12a; leer lassen wenn nicht vorhanden) # KI-Funktionen (Verkaufstext + Posteingang-Entwurf)
ANTHROPIC_API_KEY= # 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

View File

@@ -40,10 +40,17 @@ services:
ConnectionStrings__gerbilmanager: "Host=db;Port=5432;Database=gerbilmanager;Username=postgres;Password=${POSTGRES_PASSWORD}" ConnectionStrings__gerbilmanager: "Host=db;Port=5432;Database=gerbilmanager;Username=postgres;Password=${POSTGRES_PASSWORD}"
# Speicherort der hochgeladenen Fotos (NAS-Dataset gemounted unter /data/photos) # Speicherort der hochgeladenen Fotos (NAS-Dataset gemounted unter /data/photos)
Photos__RootPath: /data/photos Photos__RootPath: /data/photos
# KI-Verkaufstext (FEAT-12a stub; leer lassen wenn kein Key vorhanden) # AR-3: Data Protection Key-Ring (persistiert Gmail-App-Passwort-Verschlüsselung über Redeployments)
ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:-}" 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: volumes:
- photos:/data/photos - photos:/data/photos
- keys:/data/keys
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
@@ -101,6 +108,14 @@ volumes:
type: none type: none
o: bind o: bind
device: "${PHOTOS_PATH:-/mnt/gerbil/photos}" 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: backups:
driver: local driver: local
driver_opts: driver_opts: