From ab971618d501db7c8edd6a18f9ee02ee2d9989b7 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Thu, 7 May 2026 20:04:02 +0200 Subject: [PATCH] =?UTF-8?q?factory-import:=20--save-as-builtin=20Flag=20+?= =?UTF-8?q?=20README-=C3=9Cberarbeitung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Schließt die Lücke „nach Import landet die ZIP nicht im Image-Default": ./factory-import.sh --save-as-builtin → entpackt die ZIP nach erfolgreichem DB-Import zusätzlich in backend/factory-defaults/ (alter Inhalt vorher aufgeräumt, README.md und .gitkeep bleiben). Beim nächsten Image-Build sind die Defaults drin und seeden frische VMs automatisch. README-Abschnitt „Factory-Defaults" komplett überarbeitet: - Drei Transport-Pfade explizit erklärt (laufende DB / Drop-Box / Image) - HTML-Standardtexte + AppSetting-Whitelist dokumentiert - Auto-Seed-Verhalten + Berechtigungen aktualisiert - Typische Workflows als End-zu-End-Sequenz inkl. scp-Sync Live verifiziert: STALE_FILE.txt im backend/factory-defaults/ wurde beim --save-as-builtin sauber entfernt, README.md blieb erhalten, Subfolder neu befüllt. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 195 +++++++++++++++++++++++++++++++++------------- docs/todo.md | 11 ++- factory-import.sh | 57 ++++++++++++-- 3 files changed, 198 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 0a0ed5ee..eb7e2919 100644 --- a/README.md +++ b/README.md @@ -1066,8 +1066,9 @@ Folgende Felder werden in Audit-Logs gefiltert: ## Factory-Defaults: Stammdaten-Kataloge teilen Das **Factory-Defaults**-System erlaubt den Export und Import von -Stammdaten-Katalogen (Anbieter, Tarife, PDF-Vorlagen usw.) zwischen verschiedenen -OpenCRM-Installationen. Es ist bewusst **streng abgegrenzt** zu Datenbank-Backups: +Stammdaten-Katalogen (Anbieter, Tarife, PDF-Auftragsvorlagen, HTML-Standardtexte) +zwischen verschiedenen OpenCRM-Installationen. Es ist bewusst **streng abgegrenzt** +zu Datenbank-Backups: ### Abgrenzung @@ -1075,64 +1076,117 @@ OpenCRM-Installationen. Es ist bewusst **streng abgegrenzt** zu Datenbank-Backup |---|---|---| | Anbieter, Tarife, Kündigungsfristen, Laufzeiten, Kategorien | ✅ | ✅ | | PDF-Auftragsvorlagen (inkl. Dateien + Feldzuordnungen) | ✅ | ✅ | +| HTML-Standardtexte: Datenschutzerklärung, Impressum, Vollmacht-Vorlage, Website-Datenschutz | ✅ | ✅ | | **Kundendaten, Verträge, Dokumente** | ❌ | ✅ | | **Emails, SMTP-/IMAP-Zugangsdaten** | ❌ | ✅ | -| **System-Einstellungen, Datenschutzerklärungen, Impressum** | ❌ | ✅ | +| **Secrets, JWT, Encryption-Keys, User-Accounts** | ❌ | ✅ | | Zwischen verschiedenen Installationen teilbar | ✅ | ❌ (zu firmen-spezifisch) | -> **Kurz:** Factory-Defaults = reine Kataloge, Backup = alles. +> **Kurz:** Factory-Defaults = generische Stammdaten + rechtliche Standardtexte, +> Backup = die komplette Instanz. -### Export (Installation A → ZIP) +### Drei Wege, eine ZIP zu transportieren +Es gibt drei Pfade, je nachdem wo die ZIP gerade liegen soll: + +| Wo | Pfad | Wann | +|---|---|---| +| **Laufende DB einer Instanz** | UI-Upload oder `./factory-import.sh` | Bestehende Live-Instanz updaten | +| **Drop-Box im Repo** (`factory-exports/`) | `./factory-export.sh` legt ab, `./factory-import.sh` liest | Transfer zwischen dev und prod via `scp` | +| **Werkseinstellung im Image** (`backend/factory-defaults/`) | `./factory-import.sh --save-as-builtin` oder manuell entpacken | Neue VMs sollen die Defaults beim allerersten Start mitbringen | + +Alle drei sind unabhängig, **alle drei zusammen** decken den typischen Workflow ab. + +### Export + +**Variante A – UI:** 1. **Einstellungen** → **Factory-Defaults** öffnen -2. Übersicht prüfen (Anzahl pro Kategorie) -3. Button **„Factory-Defaults exportieren"** klicken -4. ZIP wird als `factory-defaults-YYYY-MM-DD.zip` heruntergeladen +2. Button **„Factory-Defaults exportieren"** klicken +3. ZIP wird als `factory-defaults-YYYY-MM-DD.zip` heruntergeladen + +**Variante B – CLI (für scp-Transfers):** +```bash +./factory-export.sh # → factory-exports/factory-defaults-…zip +OPENCRM_URL=https://crm.prod.example.de \ + OPENCRM_EMAIL=admin@example.de ./factory-export.sh # gegen Prod-Instanz +``` +Ohne `OPENCRM_PASSWORD` wird das Passwort interaktiv abgefragt. Der Zielordner +`factory-exports/` ist gitignored – die ZIPs landen also nicht ins Repo. **ZIP-Struktur:** ``` -factory-defaults-2026-04-23.zip +factory-defaults-2026-05-07-1949.zip ├── manifest.json # Version + Datum + Counts -├── providers/ -│ └── providers.json # Anbieter inkl. zugehöriger Tarife +├── providers/providers.json ├── contract-meta/ -│ ├── cancellation-periods.json # Kündigungsfristen (Code + Beschreibung) -│ ├── contract-durations.json # Laufzeiten (Code + Beschreibung) -│ └── contract-categories.json # Kategorien (Strom, Gas, DSL, ...) -└── pdf-templates/ - ├── pdf-templates.json # Vorlagen-Metadaten + Feldzuordnungen - └── *.pdf # Die eigentlichen PDF-Dateien +│ ├── cancellation-periods.json +│ ├── contract-durations.json +│ └── contract-categories.json +├── pdf-templates/ +│ ├── pdf-templates.json +│ └── *.pdf # Die eigentlichen PDF-Dateien +└── app-settings/ + └── app-settings.json # HTML-Templates (Whitelist-only) ``` -Die ZIP kann an andere Installationen weitergegeben werden -(Partner, Test-System, neue Installation). +### Import -### Import (ZIP → Installation B) +**Variante A – UI:** +1. **Einstellungen** → **Factory-Defaults** +2. Bereich **Import** → **„ZIP hochladen"** → Datei wählen +3. Erfolgs-Box zeigt Counts pro Kategorie -1. ZIP herunterladen bzw. erhalten -2. Inhalt nach `backend/factory-defaults/` entpacken (Unterordnerstruktur beibehalten) -3. Im Backend-Verzeichnis ausführen: - ```bash - npm run seed:defaults - ``` +**Variante B – CLI:** +```bash +./factory-import.sh # nimmt jüngste ZIP aus factory-exports/ +./factory-import.sh ./factory-exports/foo.zip # explizite ZIP +./factory-import.sh --save-as-builtin # zusätzlich ins Image-Default +./factory-import.sh --save-as-builtin ./foo.zip # entpacken (siehe unten) +``` + +Konfigurierbar per ENV: `OPENCRM_URL`, `OPENCRM_EMAIL`, `OPENCRM_PASSWORD`. + +**Variante C – Container-Bare-Metal (für Migration / mehrere ZIPs zusammenführen):** +```bash +# Inhalt der ZIP nach backend/factory-defaults/ entpacken (Unterordner beibehalten) +cd backend && npm run seed:defaults +``` **Beispiel-Output:** ``` -📦 Factory-Defaults werden eingespielt... - - ✓ Anbieter: 7, Tarife: 12 - ✓ Kündigungsfristen: 5 - ✓ Laufzeiten: 4 - ✓ Vertragskategorien: 8 - ✓ PDF-Vorlagen: 3 - -✅ Factory-Defaults erfolgreich eingespielt. +✓ Anbieter: 10 +✓ Tarife: 4 +✓ Kündigungsfristen: 18 +✓ Laufzeiten: 18 +✓ Vertragskategorien: 8 +✓ PDF-Vorlagen: 2 +✓ HTML-Templates: 2 ``` -### Mehrere ZIPs kombinieren +### `--save-as-builtin`: ZIP zur Werkseinstellung machen -Du kannst mehrere Exporte in `backend/factory-defaults/` übereinanderlegen – -JSON-Dateien werden automatisch gemerged: +Mit `--save-as-builtin` entpackt `factory-import.sh` die ZIP nach **erfolgreichem +DB-Import** zusätzlich in `backend/factory-defaults/`. Beim nächsten +`docker-compose up --build` landen die Defaults im Image. Frisch hochgezogene +VMs bringen sie dann beim ersten Start automatisch mit (Auto-Seed-Pfad im +Container-Entrypoint). + +```bash +# typischer Sync prod → dev → Image-Default +ssh prod './factory-export.sh' +scp prod:opencrm/factory-exports/factory-defaults-*.zip factory-exports/ +./factory-import.sh --save-as-builtin +docker-compose up -d --build # neuer Build, neue VMs starten direkt mit Defaults +``` + +Der Inhalt von `backend/factory-defaults/` wird beim `--save-as-builtin` vorher +geleert (außer `README.md` und `.gitkeep`), damit nichts Veraltetes liegen +bleibt. + +### Mehrere ZIPs kombinieren (CLI-only, Variante C) + +`backend/factory-defaults/` darf mehrere `*.json` pro Unterordner haben – +`npm run seed:defaults` merged sie automatisch: ``` backend/factory-defaults/ @@ -1142,40 +1196,73 @@ backend/factory-defaults/ eigene.json # 5 eigene Anbieter ``` -Das Import-Script liest **alle** `*.json` im jeweiligen Unterordner und merged per -unique Key (letzter Eintrag gewinnt). Duplikate sind also unproblematisch. +Bei gleichem Unique-Key gewinnt der zuletzt gelesene Eintrag. Der UI-/HTTP-Import +nimmt nur eine ZIP entgegen – für Merges nutze `npm run seed:defaults`. ### Idempotenz -Das Script nutzt ausschließlich Prisma `upsert`: +Alle Pfade nutzen Prisma `upsert`: - **Neue Einträge** werden angelegt -- **Bestehende Einträge** (per unique Key: `name`, `code`) werden aktualisiert +- **Bestehende Einträge** (per unique Key: `name` / `code` / `key`) werden aktualisiert - Nichts wird gelöscht -Du kannst `npm run seed:defaults` also beliebig oft ausführen, ohne Datenverlust +Du kannst Imports also beliebig oft hintereinander ausführen, ohne Datenverlust oder Duplikate. -### PDF-Dateien beim Import +### PDF-Dateien -Beim Import werden PDF-Vorlagen aus `factory-defaults/pdf-templates/*.pdf` nach -`uploads/pdf-templates/` kopiert und die Pfade in der DB entsprechend gesetzt. -Beim Re-Import wird die alte Datei in `uploads/` entsorgt und durch die neue -ersetzt. +Beim Import werden PDF-Vorlagen aus dem ZIP nach `uploads/pdf-templates/` +kopiert (mit eindeutigem Suffix) und die `templatePath`-Spalte entsprechend +gesetzt. Beim Re-Import wird die alte Datei in `uploads/` entsorgt und durch +die neue ersetzt. + +### AppSettings-Whitelist + +Beim Import werden nur die Keys mit AppSetting-Schreibzugriff gewährt, die auch +exportiert werden – aktuell: + +- `privacyPolicyHtml` +- `imprintHtml` +- `authorizationTemplateHtml` +- `websitePrivacyPolicyHtml` + +Andere Keys (SMTP, JWT, etc.) werden mit einer Warnung ignoriert. Whitelist ist +in [`backend/src/services/factoryDefaults.service.ts`](backend/src/services/factoryDefaults.service.ts) +zentral gepflegt. + +### Auto-Seed beim Erst-Deploy + +Bei einer **frischen** Installation (leere DB) spielt der Container-Entrypoint +nach dem Prisma-Seed automatisch das Built-in-Verzeichnis ein: + +``` +[entrypoint] DB ist leer (User-Count=0) – Auto-Seed wird ausgeführt +[entrypoint] Spiele eingebaute Factory-Defaults ein… + ✓ Anbieter: 10, Tarife: 4 + … +``` + +Bei bestehenden Installs passiert das **nicht** – nur frische DBs. ### Berechtigungen | Aktion | Berechtigung | |--------|--------------| | Factory-Defaults Vorschau | `settings:read` | -| Factory-Defaults Export | `settings:update` | -| Factory-Defaults Import (CLI) | Server-Zugang (SSH/Shell) | +| Factory-Defaults Export (UI/CLI) | `settings:update` | +| Factory-Defaults Import (UI/CLI) | `settings:update` | +| Werkseinstellungen ändern (`--save-as-builtin` / `npm run seed:defaults`) | Server-Zugang (SSH/Shell) | -### Typischer Einsatzzweck +### Typische Einsatzzwecke -- **Neue Installation aufsetzen**: Eine Kollegen-ZIP importieren und sofort mit - gepflegtem Anbieter- und Vorlagenkatalog loslegen +- **Neue VM aufsetzen**: ZIP einmalig nach `backend/factory-defaults/` entpacken + (oder per `--save-as-builtin`), dann `docker-compose up --build` – die + Werkseinstellungen sind beim ersten Start automatisch drin. +- **Prod-Stand zurück nach dev synchronisieren**: `./factory-export.sh` auf prod, + `scp` ins dev, `./factory-import.sh --save-as-builtin` lokal – damit ist + sowohl die dev-DB aktuell als auch der nächste Image-Build. - **Vorlagen-Paket teilen**: Eine ZIP mit nur PDF-Vorlagen weitergeben - (die anderen Ordner einfach aus der ZIP entfernen vor dem Entpacken) + (andere Ordner aus der ZIP entfernen vor dem Entpacken). - **Anbieter-Paket teilen**: ZIP mit nur `providers/` weitergeben - **Versionskontrolle**: Die entpackten JSON-Dateien unter Versionskontrolle stellen (außerhalb von `backend/factory-defaults/`, da der Ordner gitignored ist) diff --git a/docs/todo.md b/docs/todo.md index 79b26dc6..13d8f1f1 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -97,16 +97,19 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung ## ✅ Erledigt -- [x] **🔁 Factory-Defaults Sync-Scripts (dev ↔ prod)** +- [x] **🔁 Factory-Defaults Sync-Scripts (dev ↔ prod ↔ Image)** - `./factory-export.sh` zieht eine ZIP per API in `factory-exports/` (gitignored Drop-Box). - `./factory-import.sh [zip]` lädt die ZIP per API in eine andere Instanz – ohne Argument wählt es die jüngste ZIP automatisch. + - `./factory-import.sh --save-as-builtin` entpackt die ZIP zusätzlich nach + `backend/factory-defaults/` (vorher aufgeräumt). Damit landet sie beim + nächsten `docker-compose up --build` als Werkseinstellung im Image und + seedet frische DBs automatisch. - Konfigurierbar per Env: `OPENCRM_URL`, `OPENCRM_EMAIL`, `OPENCRM_PASSWORD` (sonst interaktive Abfrage). - - Use-Case: Anbieter/Tarife auf prod erweitert? `./factory-export.sh` auf - prod, `scp` ins dev, `./factory-import.sh` lokal – fertig. Geht in - beide Richtungen. + - README-Abschnitt „Factory-Defaults: Stammdaten-Kataloge teilen" + komplett überarbeitet (drei Transport-Pfade, Auto-Seed, Whitelist). - [x] **🚀 Auto-Seed: Werkseinstellungen beim Erst-Deploy** - Inhalt von `backend/factory-defaults/` wird via Dockerfile als diff --git a/factory-import.sh b/factory-import.sh index e1cf3d36..c948a4b1 100755 --- a/factory-import.sh +++ b/factory-import.sh @@ -3,9 +3,13 @@ # Idempotent (upserts pro Kategorie, nichts wird gelöscht). # # Aufruf: -# ./factory-import.sh ./factory-exports/factory-defaults-2026-05-07-1923.zip -# ./factory-import.sh # ohne Argument: nimmt automatisch -# # die jüngste ZIP aus factory-exports/ +# ./factory-import.sh # jüngste ZIP aus factory-exports/ +# ./factory-import.sh ./factory-exports/foo.zip # explizite ZIP +# ./factory-import.sh --save-as-builtin # nach Import auch ins +# ./factory-import.sh --save-as-builtin ./foo.zip # backend/factory-defaults/ +# # entpacken → nächster +# # Image-Build hat sie +# # als Werkseinstellung # # ENV (wie factory-export.sh): # OPENCRM_URL (default http://localhost:3010) @@ -20,8 +24,29 @@ PASSWORD="${OPENCRM_PASSWORD:-}" REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" EXPORT_DIR="$REPO_ROOT/factory-exports" +BUILTIN_DIR="$REPO_ROOT/backend/factory-defaults" + +# Argumente parsen: erlaubt sind --save-as-builtin und 0/1 ZIP-Pfade in +# beliebiger Reihenfolge. +SAVE_AS_BUILTIN=false +ZIP_PATH="" +for arg in "$@"; do + case "$arg" in + --save-as-builtin) SAVE_AS_BUILTIN=true ;; + -h|--help) + sed -n '2,16p' "$0" | sed 's/^# \?//' + exit 0 + ;; + --*) echo "✗ Unbekanntes Flag: $arg"; exit 2 ;; + *) + if [ -n "$ZIP_PATH" ]; then + echo "✗ Mehrere ZIP-Pfade angegeben (nur einer erlaubt)"; exit 2 + fi + ZIP_PATH="$arg" + ;; + esac +done -ZIP_PATH="${1:-}" if [ -z "$ZIP_PATH" ]; then # Jüngste ZIP automatisch wählen ZIP_PATH="$(ls -1t "$EXPORT_DIR"/*.zip 2>/dev/null | head -1 || true)" @@ -64,7 +89,7 @@ RESPONSE="$(curl -sS -X POST "$URL/api/factory-defaults/import" \ -F "zip=@$ZIP_PATH")" # Hübsch ausgeben + auf success prüfen -if printf '%s' "$RESPONSE" | python3 -c ' +if ! printf '%s' "$RESPONSE" | python3 -c ' import json, sys r = json.load(sys.stdin) if not r.get("success"): @@ -91,7 +116,25 @@ if warnings: for w in warnings: print(f" - {w}") '; then - exit 0 -else exit 1 fi + +# --save-as-builtin: ZIP zusätzlich in backend/factory-defaults/ entpacken, +# damit der nächste Image-Build sie als Werkseinstellung mitnimmt. +# Vorher räumen wir auf (außer README.md + .gitkeep), damit nichts Veraltetes +# liegen bleibt. +if [ "$SAVE_AS_BUILTIN" = "true" ]; then + echo + echo "→ --save-as-builtin: aktualisiere $BUILTIN_DIR/" + if [ ! -d "$BUILTIN_DIR" ]; then + mkdir -p "$BUILTIN_DIR" + fi + # Aufräumen: alles außer README.md und .gitkeep löschen + find "$BUILTIN_DIR" -mindepth 1 \ + \! -name 'README.md' \! -name '.gitkeep' \ + -delete + # ZIP entpacken (manifest.json kommt mit, ist aber harmlos) + unzip -q -o "$ZIP_PATH" -d "$BUILTIN_DIR" + echo "✓ Werkseinstellungen aktualisiert. Beim nächsten 'docker-compose up" + echo " --build' landen sie im Image und seeden frische DBs automatisch." +fi