Document production rollout and verify complete legacy data migration
Some checks failed
check / verify (push) Failing after 1m22s
Some checks failed
check / verify (push) Failing after 1m22s
This commit is contained in:
parent
94d29850c6
commit
007802d56e
@ -1,3 +1,4 @@
|
||||
POSTGRES_PASSWORD=REPLACE_DATABASE_PASSWORD
|
||||
DATABASE_URL=postgresql://taskmanager:REPLACE_DATABASE_PASSWORD@localhost:5432/taskmanager
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
NEXTAUTH_SECRET=
|
||||
|
||||
6
.github/workflows/check.yml
vendored
6
.github/workflows/check.yml
vendored
@ -31,6 +31,12 @@ jobs:
|
||||
- run: npx prisma migrate deploy
|
||||
- run: npm test
|
||||
- run: npm run test:integration
|
||||
- name: Verify original-schema upgrade
|
||||
run: |
|
||||
docker exec ${{ job.services.postgres.id }} createdb -U review taskmanager_upgrade_test
|
||||
npx tsx --test tests/upgrade.test.ts
|
||||
env:
|
||||
DATABASE_URL: postgresql://review:review-local-only@localhost:55432/taskmanager_upgrade_test
|
||||
- run: npm run build
|
||||
- run: npx playwright install --with-deps chromium
|
||||
- run: npx playwright test
|
||||
|
||||
@ -53,7 +53,7 @@ Die alte Role-Spalte bleibt ausschließlich als Migrationshilfe erhalten. Startr
|
||||
|
||||
Die Browser-/API-Tests prüfen Gruppenisolation, Eskalation, sofortigen Rechteentzug, deaktivierte Konten, private Downloads, letzten Administrator, CSRF, Änderungskonflikte, Archiv/Excel, Kalenderwiderruf, einmaligen Reset, Teampool und die SSO-Passwortsperre. Es gab beim schnellen Seitenwechsel zwei Servermeldungen über früh geschlossene Streams; die UI-Assertions und Tests bestanden.
|
||||
|
||||
Die Upload-Migration bewahrt Dateiinhalte, entfernt die öffentliche Kopie und ist wiederholbar. Ein separater Upgrade-Versuch mit Bestandsdaten prüfte E-Mail-Normalisierung, Rollenübernahme, Session-Entwertung, Bestandsgruppe und idempotente Ausführung. Tests verwenden lokale Testdatenbanken. Die separate Demo-Instanz läuft jetzt unter http://localhost:3110 mit Produktionsimages; siehe TESTBETRIEB.md. Der Host hat Node 20, das unterstützte Deployment und CI verwenden Node 22.
|
||||
Die Upload-Migration bewahrt Dateiinhalte und ist wiederholbar. Originaldateien bleiben seit dem Produktions-Upgrade-Paket im privaten Volume erhalten; die alte statische Auslieferung muss gesperrt sein. Ein separater Upgrade-Versuch mit Bestandsdaten prüfte E-Mail-Normalisierung, Rollenübernahme, Session-Entwertung, Bestandsgruppe und idempotente Ausführung. Tests verwenden lokale Testdatenbanken. Die separate Demo-Instanz läuft jetzt unter http://localhost:3110 mit Produktionsimages; siehe TESTBETRIEB.md. Der Host hat Node 20, das unterstützte Deployment und CI verwenden Node 22.
|
||||
|
||||
## Offene Punkte und Grenzen
|
||||
|
||||
@ -78,3 +78,8 @@ Docker-Demo geprüft: Anmeldung, private PDF-Speicherung/-Download, Aufgaben- un
|
||||
## Überarbeitung der Oberfläche (06.10.2026)
|
||||
|
||||
Die Arbeitsoberfläche nutzt durchgehend shadcn/Radix-Dialoge, Sheets, Tabs, Tabellen, Auswahlfelder, Bestätigungen und Lucide-Icons. Aufgaben, Detailansicht, Benutzer, Gruppen/Rollen und Integrationen wurden überarbeitet; Gruppen-/Rollenlöschung ist mit serverseitigen Schutzregeln ergänzt. Die Prüfergebnisse und der erhaltene Funktionsumfang stehen in [UX-ABNAHME.md](UX-ABNAHME.md). Die aktualisierten Produktionsimages laufen weiterhin in der isolierten lokalen Demo.
|
||||
|
||||
|
||||
## Migration bestehender Produktivdaten
|
||||
|
||||
README.md und UPGRADE.md beschreiben den Betrieb und das Upgrade anhand einer separaten Restore-Kopie. Compose migriert Schema, Rollen und alle referenzierten Dateien vor dem Webstart. Kopien werden per SHA-256 verglichen; Originale bleiben erhalten. tests/upgrade.test.ts reproduziert das Originalschema und kontrolliert Konten, Aufgabenfelder, Kommentare, Anhangsbytes, Rollen und Wiederholbarkeit. Der Test sowie beide Integrationstests bestehen. Echte Produktivdaten wurden nicht angefasst.
|
||||
|
||||
@ -1,53 +1,9 @@
|
||||
# Betrieb und Upgrade
|
||||
# Docker-Betrieb
|
||||
|
||||
## Lokale Vorführung und Betriebsprüfungen
|
||||
Die vollständige und aktuelle Anleitung steht in [README.md](README.md): Neuinstallation, Konfiguration, HTTPS, Dienste, Monitoring, Backup, Tests und Fehlerbehebung.
|
||||
|
||||
Eine vollständig getrennte Testinstanz mit Produktionsimages lässt sich über `scripts/start-demo.ps1` starten; Zugang und Bedienung siehe [TESTBETRIEB.md](TESTBETRIEB.md). Für den öffentlichen Betrieb die Demo-Konfiguration nicht übernehmen.
|
||||
Für bestehende produktive Installationen gilt [UPGRADE.md](UPGRADE.md). Sie beschreibt Backup, Wiederherstellung in eine getrennte Zielinstallation, Schema-/RBAC-/Anhangsmigration, Datenvergleich und Rollback. Keine Neuinitialisierung und kein Seed auf Bestandsdaten.
|
||||
|
||||
`/api/health` prüft die Datenbankverbindung. Der Web-Container besitzt einen Healthcheck, der Worker meldet erfolgreiche Durchläufe über einen lokalen Heartbeat. `unhealthy` muss vom Monitoring alarmiert werden; dieser Status allein löst keinen Docker-Neustart aus. Der Web-Container verwendet ein schreibgeschütztes Root-Dateisystem, ein privates Upload-Volume, ein begrenztes temporäres Dateisystem und keine Linux-Capabilities. Web- und operations-Container verwenden UID 1001.
|
||||
Die Compose-Startkette migriert inzwischen auch Anhänge und prüft deren Vollständigkeit. Originaldateien bleiben im privaten Speicher erhalten; alte öffentliche `/uploads/`-Freigaben müssen gesperrt werden.
|
||||
|
||||
`scripts/backup.ps1` erstellt bei kurz angehaltenen App-/Worker-Diensten einen PostgreSQL-Dump und ein Upload-Archiv einschließlich Prüfsummen. Vorher laufende Dienste werden auch im Fehlerfall wieder gestartet. Backups und Secrets extern verschlüsselt aufbewahren. Wiederherstellung zunächst in einer separaten Datenbank und einem separaten Volume mit `pg_restore --exit-on-error` und `tar` üben, Dateiinhalte und Objektanzahlen prüfen. Niemals probeweise die laufende Produktionsdatenbank überschreiben.
|
||||
|
||||
Ein HTTPS-/Proxy-Beispiel mit Login-Ratenlimit, Upload-Sperre und unterdrückten Kalenderlink-Logs liegt in `deploy/nginx.conf.example`. Domain und Zertifikatspfade müssen angepasst werden. Aktuelle Sicherheitsgrenzen stehen in [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Produktion mit Docker
|
||||
|
||||
1. `.env` anhand `.env.example` erstellen. DATABASE_URL muss im Compose-Netz auf `db:5432` zeigen, beispielsweise `postgresql://taskmanager:<URL-kodiertes-Passwort>@db:5432/taskmanager`. POSTGRES_PASSWORD zusätzlich in `.env` setzen. Keine Zugangsdaten committen.
|
||||
2. NEXTAUTH_URL auf die tatsächliche HTTPS-Adresse setzen; NEXTAUTH_SECRET zufällig generieren. Hinter dem Reverse Proxy HTTPS erzwingen, Upload-Requestlimit z.B. 11 MB setzen und `/uploads/` auch dort sperren. TRUST_PROXY nur aktivieren, wenn der Proxy x-real-ip selbst setzt.
|
||||
3. `docker compose build` und `docker compose up -d db`.
|
||||
4. `docker compose run --rm migrate` führt Schema- und RBAC-Datenmigration aus.
|
||||
5. Bei Neuinstallation einmalig `docker compose run --rm migrate npm run prisma:seed` mit gesetzten Bootstrap-Werten ausführen. Anschließend Bootstrap-Werte entfernen.
|
||||
6. `docker compose up -d app worker`. Genau eine Worker-Instanz betreiben. App-Port ist nur an 127.0.0.1 gebunden; öffentliche Auslieferung über HTTPS-Proxy.
|
||||
|
||||
Der Web-Container läuft als UID/GID 1001. Anhänge liegen auf einem privaten Volume `/app/private-uploads`, nicht unter `public`. Das Runtime-Image enthält die Next.js-Standalone-Ausgabe; Migrations- und Worker-Werkzeuge befinden sich im separaten operations-Target. Der Web-Start erzeugt keine Benutzer und führt kein Seed aus.
|
||||
|
||||
## Upgrade einer vorhandenen Installation
|
||||
|
||||
1. Wartungsfenster: alte App und Worker stoppen, PostgreSQL und das bisherige Upload-Volume sichern. Die Backups müssen zusammenpassen. Rückweg vor Beginn anhand eines Restores testen.
|
||||
2. Neues Image bauen. Schema-Migration und anschließend `scripts/migrate-access.ts` ausführen. Das Skript normalisiert E-Mails, bricht bei Kollisionen ab, legt die Bestandsgruppe an, ordnet vorhandene Rollen zu und macht alte JWTs ungültig. Mehrfache Ausführung ist idempotent.
|
||||
3. ADMIN wird Systemadministrator/GLOBAL. BEARBEITER wird Bearbeiter/ASSIGNED. PFLEGER wird Koordinator/GLOBAL plus Bearbeiter/ASSIGNED. VORGESETZTER wird Teamleitung/GLOBAL und erhält nicht automatisch Rechteverwaltung. Anschließend globale Altzuweisungen organisatorisch auf Gruppen eingrenzen.
|
||||
4. Altes Upload-Volume zusätzlich in den operations-Container einhängen, z.B. unter `/legacy-uploads`; neues privates Volume nach `/app/private-uploads`. LEGACY_UPLOAD_DIR und UPLOAD_DIR entsprechend setzen und `npx tsx scripts/migrate-uploads.ts` ausführen. Das Skript prüft die tatsächlichen Dateitypen, kopiert in privaten Speicher, aktualisiert DB-Pfade und entfernt erfolgreich migrierte Altdateien. Bei ungültigen/vermissten Dateien bricht es ab; diese Fälle vor dem Start bearbeiten. Nicht referenzierte Altdateien separat prüfen. Kein automatisches rekursives Löschen alter Verzeichnisse.
|
||||
5. Neue App starten. Sicherstellen, dass der Proxy alte `/uploads/`-Links nicht selbst statisch bedient. Stichprobe: erneuter Login, eigene/fremde Aufgaben, Download nach Rollenentzug, Änderungskonflikt und Archivwiederherstellung.
|
||||
|
||||
Nach Migration nicht einfach die alte Anwendung weiterbetreiben: deren Rechteprüfungen und öffentliche Uploadpfade sind unsicher. Rollback erfordert Wiederherstellung des zusammengehörigen DB-/Datei-Backups und eine Entscheidung über währenddessen entstandene Daten.
|
||||
|
||||
## E-Mail, OIDC und Webhooks
|
||||
|
||||
- SMTP_URL und MAIL_FROM aktivieren Einladung, Reset und Erinnerungen. Passwortlinks sind eine Stunde gültig und einmal verwendbar. Tokens stehen im URL-Fragment und nicht im HTTP-Zugriffslog.
|
||||
- OIDC_ISSUER, OIDC_CLIENT_ID und OIDC_CLIENT_SECRET setzen. Callback: `/api/auth/callback/oidc`. Benutzer werden nicht automatisch per E-Mail verknüpft; den exakten Provider-Subject in der Benutzerverwaltung hinterlegen. Bei verknüpften Konten ist die lokale Passwortanmeldung gesperrt. MFA beim Identity Provider erzwingen; optional OIDC_REQUIRED_ACR setzen, dann wird der zurückgelieferte ACR-Wert geprüft. Echte Provider-Anmeldung erfordert Konfiguration und Abnahme am Zielsystem.
|
||||
- WEBHOOK_ALLOWED_HOSTS enthält ausschließlich exakte freigegebene öffentliche HTTPS-Hosts. Der Worker lehnt private IPs und Redirects ab. Payload enthält Ereignis-ID, Aktion, Objekt-ID und Zeitpunkt, keine Geheimnisse oder vollständigen Aufgabeninhalte. Signatur: HMAC-SHA256 über `<timestamp>.<body>`, Header X-Taskmanager-Timestamp und X-Taskmanager-Signature. Empfänger müssen Timestamp und Ereignis-ID gegen Replay/Duplikate prüfen.
|
||||
- Kalenderlinks sind widerrufbare Geheimnisse mit 90 Tagen Laufzeit. `/api/calendar/*` im Reverse-Proxy-Log redigieren, Links nicht weitergeben. Alternativ lesenden API-Token im Authorization-Header verwenden.
|
||||
|
||||
## Worker und Aufbewahrung
|
||||
|
||||
Der Worker läuft im Minutenabstand: Erinnerungen, Eskalationen, Wiederholungsaufgaben, E-Mail, Dateibereinigung, Webhookzustellung und Ablaufbereinigung. Wiederholungsaufgaben werden je Ausgangsaufgabe genau einmal angelegt. Reminder werden je Aufgabe/Version/Benutzer/Tag dedupliziert. E-Mail/Webhooks haben At-least-once-Semantik; ein Prozessabsturz nach Versand kann Duplikate erzeugen. Webhooks erhalten höchstens fünf Versuche. Monitoring muss Worker-Ausfälle und fehlgeschlagene Zustellungen erfassen.
|
||||
|
||||
ARCHIVE_RETENTION_DAYS=0 bewahrt Archive unbegrenzt. Erst ein ausdrücklich gesetzter Wert ab 30 aktiviert endgültige automatische Löschung archivierter, erledigter Aufgaben. Die Dateibereinigung arbeitet über wiederholbare Cleanup-Jobs. Audit-Ereignisse werden dadurch nicht gelöscht.
|
||||
|
||||
Datenbank, private Anhänge und die verwendeten Secrets regelmäßig sichern; Restore testen. Das Audit-Log ist anwendungsseitig nur lesbar, aber kein manipulationssicheres externes Archiv. Malware-Scanning und externe Log-/Backup-Dienste sind Infrastrukturentscheidungen und nicht automatisch aktiviert.
|
||||
|
||||
## Grenzen und Abnahme
|
||||
|
||||
CSV/XLSX-Import: maximal 100 Zeilen/100 KB, eine Tabelle, keine Excel-Formeln. Batchaktionen verarbeiten maximal 50 Aufgaben und melden Teilerfolge pro Aufgabe; bereits erfolgreiche Zeilen werden nicht zurückgerollt. Export: maximal 5000 Aufgaben. Kommentare im Detail: letzte 100 (weitere Seiten über API); Audit im Detail: letzte 50. Berichte mitteln maximal die letzten 1000 abgeschlossenen Aufgaben. Diese Grenzen sind keine Berechtigungsgrenzen.
|
||||
|
||||
Die PWA bleibt online: vertrauliche Aufgaben werden nicht als Offline-Kopie im Service Worker gespeichert. Native lokale MFA ist nicht implementiert; MFA erfolgt über den optionalen OIDC-Provider. Produktive SMTP-/OIDC-/Webhook-Zustellung und Last-/Backup-Tests müssen mit der Zielinfrastruktur abgenommen werden.
|
||||
Lokale Vorführung: [TESTBETRIEB.md](TESTBETRIEB.md). Proxy-Vorlage: [deploy/nginx.conf.example](deploy/nginx.conf.example). Sicherheitsgrenzen: [SECURITY.md](SECURITY.md).
|
||||
|
||||
241
README.md
241
README.md
@ -1,78 +1,211 @@
|
||||
# Aufgabenplaner
|
||||
# Aufgabenplaner – Installation, Produktivbetrieb und Upgrade
|
||||
|
||||
Next.js 16, React, PostgreSQL und Prisma. Die Anwendung verwaltet Aufgaben mit Benutzern, Gruppen und konfigurierbarem RBAC.
|
||||
Aufgabenverwaltung mit Next.js, PostgreSQL, privaten Anhängen und RBAC aus Benutzern, Gruppen, Rollen und einzelnen Berechtigungen. Die Oberfläche nutzt shadcn/Radix und Lucide; Aufgaben sind als Liste, Kanban oder Monatsagenda verfügbar.
|
||||
|
||||
**Eine bestehende Installation wird aktualisiert, nicht neu initialisiert.** Benutzer, Passwort-Hashes, Aufgaben, Status, Zuweisungen, Beauftragungsdaten, Kommentare und Anhänge werden übernommen. Die neue Version darf erst nach erfolgreicher Migration und Abnahme öffentlich erreichbar sein. Für den Umstieg einer produktiven Altinstallation zuerst die vollständige [Upgrade-Anleitung](UPGRADE.md) lesen.
|
||||
|
||||
## Inhalt
|
||||
|
||||
- [Bestehende Installation übernehmen](UPGRADE.md)
|
||||
- [Voraussetzungen](#voraussetzungen)
|
||||
- [Neuinstallation](#neuinstallation)
|
||||
- [Konfiguration](#konfiguration)
|
||||
- [Berechtigungen](#berechtigungen)
|
||||
- [Betrieb und Datensicherung](#betrieb-und-datensicherung)
|
||||
- [Abnahme und Fehlerbehebung](#abnahme-und-fehlerbehebung)
|
||||
- [Entwicklung und Tests](#entwicklung-und-tests)
|
||||
|
||||
## Funktionen
|
||||
|
||||
- Gruppen, Rollen und einzelne Permissions mit Geltungsbereichen ASSIGNED, GROUP und GLOBAL
|
||||
- Serverseitige Rechteprüfung einschließlich Downloads, Statistiken, Kalender und Unterressourcen
|
||||
- Benutzer anlegen/deaktivieren, sichere Einladungen und Passwort-Reset, optional OIDC mit MFA-Vorgabe
|
||||
- Suche, Filter, Prioritäten, Tags, Projekte, Standorte, Listen, Kanban und Kalenderagenda
|
||||
- Teampool, Zuweisungen, Checklisten, Unteraufgaben und zyklusfreie Abhängigkeiten
|
||||
- Wiederkehrende Aufgaben, persönliche Vorlagen, gespeicherte Filter, Stapelaktionen
|
||||
- Versionierte Änderungen, Archiv/Wiederherstellung und optional zeitgesteuerte Aufbewahrung
|
||||
- Private Anhänge mit Inhaltsprüfung, 10-MB-Grenze, Quoten und Cleanup
|
||||
- Audit-Protokoll, Erinnerungen und Eskalationen an berechtigte Teammitglieder
|
||||
- CSV-/XLSX-Import mit Vorschau, CSV-/XLSX-/ICS-Export, widerrufbare Kalenderabonnements
|
||||
- Eingeschränkte API-Tokens und signierte Webhooks mit Wiederholungsversuchen
|
||||
- Responsive Oberfläche und Web-App-Manifest
|
||||
Aufgaben mit Suche, Filtern, Prioritäten, Tags, Projekten, Standorten, Checklisten, Kommentaren, Unteraufgaben, Abhängigkeiten und Wiederholungen. Zuweisung an Personen oder Teampool, Archiv/Wiederherstellung, Stapelaktionen, Vorlagen und gespeicherte Ansichten. CSV-/XLSX-Import mit Vorschau, CSV-/XLSX-/ICS-Export, widerrufbare Kalenderlinks, API-Tokens, signierte Webhooks, Audit und Erinnerungen. Benutzer und Gruppen lassen sich verwalten; Löschungen erfordern Bestätigung und unterliegen serverseitigen Schutzregeln.
|
||||
|
||||
## Entwicklung
|
||||
## Voraussetzungen
|
||||
|
||||
Voraussetzungen: Node.js 22+, PostgreSQL 16. `.env.example` nach `.env` kopieren und Werte setzen. Ein zufälliges NEXTAUTH_SECRET mit mindestens 32 Zeichen verwenden.
|
||||
- Linux-Server mit Docker Engine und Docker Compose v2; ausreichend Speicher für PostgreSQL, Anhänge, Builds und mindestens eine vollständige Sicherung. Kapazität mit dem eigenen Datenbestand prüfen.
|
||||
- PostgreSQL 16 (im Compose enthalten). **Kein gleichzeitiger PostgreSQL-Major-Versionswechsel beim Anwendungsupgrade.**
|
||||
- Öffentlich erreichbare HTTPS-Domain und Reverse Proxy. Beispiel: [deploy/nginx.conf.example](deploy/nginx.conf.example).
|
||||
- Repository-Zugriff und administrativer Zugriff auf die eigenen Docker-Volumes/Backups.
|
||||
- Für lokale Entwicklung: Node.js 22+, npm und PostgreSQL 16. Docker nutzt Node 22.
|
||||
|
||||
```sh
|
||||
Die folgenden Serverbefehle verwenden Bash. Alle Befehle im Checkout ausführen. `.env`, Backups und Produktivdaten gehören nicht ins Git. Das öffentliche TLS-Zertifikat und die Proxy-Konfiguration sind betreiberspezifisch.
|
||||
|
||||
## Neuinstallation
|
||||
|
||||
**Nur für eine leere Installation. Bei vorhandenen Daten stattdessen [UPGRADE.md](UPGRADE.md) verwenden.**
|
||||
|
||||
```bash
|
||||
git clone https://git.jfritzsche.de/jf/taskmanager.git
|
||||
cd taskmanager
|
||||
git switch feature/security-rbac
|
||||
cp .env.example .env
|
||||
chmod 600 .env
|
||||
openssl rand -hex 32 # als NEXTAUTH_SECRET eintragen
|
||||
openssl rand -hex 24 # als Datenbankpasswort verwenden
|
||||
```
|
||||
|
||||
`.env` bearbeiten, mindestens:
|
||||
|
||||
```dotenv
|
||||
POSTGRES_PASSWORD=HIER_ZUFAELLIGES_DATENBANKPASSWORT
|
||||
DATABASE_URL=postgresql://taskmanager:HIER_ZUFAELLIGES_DATENBANKPASSWORT@db:5432/taskmanager
|
||||
NEXTAUTH_URL=https://aufgaben.example.org
|
||||
NEXTAUTH_SECRET=HIER_ZUFAELLIGES_SECRET_MIT_MINDESTENS_32_ZEICHEN
|
||||
ALLOW_LOCAL_HTTP=false
|
||||
APP_TIMEZONE=Europe/Berlin
|
||||
ARCHIVE_RETENTION_DAYS=0
|
||||
BOOTSTRAP_EMAIL=admin@example.org
|
||||
BOOTSTRAP_PASSWORD=HIER_EINMALPASSWORT_MIT_MINDESTENS_12_ZEICHEN
|
||||
```
|
||||
|
||||
Passwörter in `DATABASE_URL` müssen URL-kodiert sein; die oben generierten Hexwerte benötigen keine Sonderbehandlung. `POSTGRES_PASSWORD` ist der unveränderte Passwortwert. Variablen mit `$` in `.env` passend einfach quotieren, um Compose-Interpolation zu verhindern.
|
||||
|
||||
```bash
|
||||
docker compose -p taskmanager build
|
||||
docker compose -p taskmanager up -d --wait db
|
||||
docker compose -p taskmanager run --rm migrate
|
||||
docker compose -p taskmanager run --rm --no-deps migrate npm run prisma:seed
|
||||
```
|
||||
|
||||
Bootstrap-Werte danach aus `.env` entfernen. Seed legt ausschließlich bei fehlendem Administrator einen neuen an; keine Standardzugänge. Das Startpasswort muss bei der ersten Anmeldung geändert werden.
|
||||
|
||||
```bash
|
||||
docker compose -p taskmanager up -d --wait app worker
|
||||
curl --fail http://127.0.0.1:3000/api/health
|
||||
```
|
||||
|
||||
Proxy auf `127.0.0.1:3000` richten, Domain/Zertifikate einrichten, `/uploads/` sperren, HTTPS erzwingen. Wenn der Proxy selbst in Docker läuft, sein Netz und Upstream ausdrücklich anpassen: dessen `127.0.0.1` ist nicht der Host. Erst nach der Abnahme unten freigeben.
|
||||
|
||||
## Konfiguration
|
||||
|
||||
| Variable | Bedeutung / Produktionswert |
|
||||
| --- | --- |
|
||||
| `DATABASE_URL` | PostgreSQL-Verbindung; in Compose `db:5432`, außerhalb die tatsächliche Adresse. |
|
||||
| `POSTGRES_PASSWORD` | Passwort des Compose-DB-Benutzers `taskmanager`. Bei vorhandenem Volume muss es zum tatsächlich gesetzten DB-Passwort passen. Eine Änderung der Variable allein ändert das DB-Passwort nicht. |
|
||||
| `POSTGRES_DB` | Optional, Standard `taskmanager`; auch DATABASE_URL anpassen. |
|
||||
| `NEXTAUTH_URL` | Externe HTTPS-Adresse, keine interne Containeradresse. |
|
||||
| `NEXTAUTH_SECRET` | Zufällig, mindestens 32 Zeichen; sichern. Wechsel meldet Sitzungen ab. |
|
||||
| `APP_TIMEZONE` | Gültige Zeitzone, Standard `Europe/Berlin`. |
|
||||
| `UPLOAD_DIR` | Compose setzt `/app/private-uploads`. Bei Betrieb ohne Docker absoluten privaten Pfad verwenden. |
|
||||
| `LEGACY_UPLOAD_DIR` | Nur Migration. Compose verwendet dasselbe private Volume als Wurzel der übernommenen alten Upload-Verzeichnisstruktur. Ohne Compose explizit auf das alte `public/uploads` setzen. |
|
||||
| `GROUP_UPLOAD_QUOTA_MB` | Upload-Quota pro Gruppe; Standard 1024 MB. An Bestandsvolumen anpassen. |
|
||||
| `TRUST_PROXY` | Nur `true`, wenn der vertrauenswürdige Proxy `x-real-ip` ersetzt und direkter externer Zugriff ausgeschlossen ist. |
|
||||
| `ALLOW_LOCAL_HTTP` | Produktion `false`; nur lokale Tests dürfen Loopback-HTTP freischalten. |
|
||||
| `ARCHIVE_RETENTION_DAYS` | `0` bewahrt Archive unbegrenzt. Ab 30 werden archivierte erledigte Aufgaben nach Frist endgültig gelöscht. |
|
||||
| `SMTP_URL`, `MAIL_FROM` | Gemeinsam setzen, um Einladung, Passwort-Reset und Benachrichtigungen zu aktivieren. Ohne SMTP Konten mit Einmalpasswort anlegen. |
|
||||
| `OIDC_ISSUER`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET` | Optional gemeinsam konfigurieren. Callback: `https://DOMAIN/api/auth/callback/oidc`. |
|
||||
| `OIDC_REQUIRED_ACR` | Optional erwarteter ACR-Wert; MFA zusätzlich im Identity Provider erzwingen. |
|
||||
| `WEBHOOK_ALLOWED_HOSTS` | Kommagetrennte exakte öffentliche HTTPS-Hosts; keine Wildcards. |
|
||||
| `BOOTSTRAP_EMAIL`, `BOOTSTRAP_PASSWORD` | Ausschließlich einmalig bei Neuinstallation. Passwort 12 Zeichen bis 72 UTF-8-Bytes. |
|
||||
| `TASKMANAGER_ENV_FILE` | Optional anderer Pfad statt `.env` für die Container. Für Compose-Variablen zusätzlich `--env-file PFAD` verwenden. |
|
||||
|
||||
Die alte `Role`-Spalte bleibt als Migrationsinformation erhalten; sie steuert keine Berechtigungen mehr. OIDC-Konten werden ausdrücklich über den Provider-Subject verknüpft, nicht automatisch über gleiche E-Mail-Adressen. Danach ist ihre lokale Passwortanmeldung gesperrt.
|
||||
|
||||
## Berechtigungen
|
||||
|
||||
- **ASSIGNED:** Zugriff auf dem Benutzer zugewiesene Aufgaben.
|
||||
- **GROUP:** Zugriff auf Aufgaben der jeweiligen Gruppe; direkte Gruppenrollen benötigen eine aktive Mitgliedschaft.
|
||||
- **GLOBAL:** Zugriff unabhängig von der Gruppe. Verwaltungsrechte sind ausschließlich global zulässig.
|
||||
|
||||
Rollen bündeln Permissions; Benutzer erhalten sie direkt oder über Gruppen. Fehlende Rechte werden serverseitig abgewiesen, auch bei Dateien und Exporten. Systemadministrator wird ausschließlich direkt vergeben; mindestens ein aktiver Administrator muss erhalten bleiben. Benutzerpflege verleiht nicht automatisch Rechteverwaltung.
|
||||
|
||||
Für den Teampool einer Gruppe „Teampool lesen und übernehmen“ mit GROUP und „Bearbeiter“ mit ASSIGNED zuweisen. Koordinatoren erhalten für die Bearbeitung eigener Aufgaben zusätzlich „Bearbeiter“. Gruppen mit bestehenden Aufgaben lassen sich nicht löschen; Aufgaben vorher umordnen oder die Gruppe deaktivieren.
|
||||
|
||||
Die genaue Zuordnung alter Rollen und die erforderliche organisatorische Nachprüfung stehen in [UPGRADE.md](UPGRADE.md).
|
||||
|
||||
## Betrieb und Datensicherung
|
||||
|
||||
### Dienste
|
||||
|
||||
- `db`: persistentes PostgreSQL-Volume `postgres_data`.
|
||||
- `migrate`: Schema-, Rollen- und Anhangsmigration; beendet sich bei Erfolg mit Exitcode 0. Bei Fehler startet `app` nicht.
|
||||
- `app`: Next.js, UID/GID 1001, schreibgeschütztes Root-Dateisystem, privates `uploads`-Volume; Port nur am Host-Loopback.
|
||||
- `worker`: genau **eine** Instanz für Erinnerungen, Wiederholungen, Zustellung und Bereinigung. Gleiches privates Upload-Volume wie `app` und `migrate`.
|
||||
|
||||
```bash
|
||||
docker compose -p taskmanager ps -a
|
||||
docker compose -p taskmanager logs --tail=100 migrate app worker
|
||||
```
|
||||
|
||||
`/api/health` prüft die DB-Verbindung. Worker-Heartbeat prüft, ob Durchläufe stattfinden. `unhealthy` extern alarmieren: Docker startet allein aufgrund dieses Status nicht neu. Auf freien Speicher, PostgreSQL-Backups, Zustellfehler und Backup-Alter überwachen. Genau eine Migration gleichzeitig ausführen; bei Updates App und Worker vorher stoppen.
|
||||
|
||||
### Konsistentes Backup (Bash)
|
||||
|
||||
Projektname an die tatsächliche Installation anpassen; vorab `docker compose ... ps` kontrollieren. Der kurze Stopp verhindert auseinanderlaufende Datenbank-/Dateisicherungen. Folgender Ablauf setzt normalerweise laufende App und Worker voraus:
|
||||
|
||||
```bash
|
||||
mkdir -p backups
|
||||
BACKUP_DIR="$(pwd)/backups/$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
mkdir -m 700 "$BACKUP_DIR"
|
||||
docker compose -p taskmanager stop -t 60 app worker
|
||||
# Bei JEDEM Fehler hier angehalten lassen, Fehler prüfen und Dienste bewusst wieder starten.
|
||||
docker compose -p taskmanager exec -T db sh -c 'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc' > "$BACKUP_DIR/database.dump"
|
||||
docker compose -p taskmanager run --rm --no-deps --user 0:0 \
|
||||
-v "$BACKUP_DIR:/backup" worker tar -czf /backup/uploads.tar.gz -C /app/private-uploads .
|
||||
cp .env "$BACKUP_DIR/environment.env"
|
||||
(cd "$BACKUP_DIR" && sha256sum database.dump uploads.tar.gz environment.env > SHA256SUMS)
|
||||
docker compose -p taskmanager start app worker
|
||||
```
|
||||
|
||||
Jeden Exitcode prüfen, insbesondere vor Neustart; Skripte mit `set -euo pipefail` ausführen. PowerShell-Alternative: `scripts/backup.ps1 -Project taskmanager -EnvFile .env`; diese startet vorher laufende Dienste auch bei Fehlern wieder. `.env` zusätzlich geschützt sichern. Backups verschlüsselt außerhalb des Servers lagern, Zugriffsrechte begrenzen und regelmäßig in einer separaten Instanz wiederherstellen. SHA-256 prüft Integrität, ersetzt keine Verschlüsselung.
|
||||
|
||||
### Updates und Wiederherstellung
|
||||
|
||||
Den getesteten Commit festhalten, Images bauen, Wartungsfenster aktivieren, konsistentes Backup erstellen, Dienste stoppen, `docker compose ... run --rm migrate`, anschließend `up -d --wait app worker`. **Niemals `prisma migrate reset`, `db push --accept-data-loss` oder `docker compose down -v` auf Bestandsdaten verwenden.**
|
||||
|
||||
Restore und Rollback stehen mit konkreten Befehlen in [UPGRADE.md](UPGRADE.md). Schemaänderungen werden nicht durch Start eines älteren Images rückgängig gemacht. Nach Freigabe entstandene Änderungen beim Rollback gesondert berücksichtigen.
|
||||
|
||||
## Abnahme und Fehlerbehebung
|
||||
|
||||
Vor Freigabe prüfen:
|
||||
|
||||
1. Migration Exitcode 0, `db`, `app`, `worker` healthy; HTTPS und richtige externe URL.
|
||||
2. Anzahl Benutzer/Aufgaben/Kommentare/Dateien mit dem Backup vergleichen; alte Beauftragungsdaten und Anhänge stichprobenartig prüfen.
|
||||
3. Bestehender Administrator und Bearbeiter können sich nach erneuter Anmeldung anmelden. Rollen und Gruppenzugriff prüfen.
|
||||
4. Fremde Aufgaben/Dateien sind ohne Berechtigung gesperrt; Rechteentzug greift sofort.
|
||||
5. Aufgabe erstellen, bearbeiten, zuweisen, erledigen, archivieren und wiederherstellen; Kommentar sowie Upload/Download prüfen.
|
||||
6. `/uploads/...` liefert keine Altdateien; Proxy bedient das Verzeichnis ebenfalls nicht statisch.
|
||||
7. SMTP, OIDC/MFA und Webhooks bei Verwendung mit echten Zielsystemen separat prüfen. Restore erfolgreich üben.
|
||||
|
||||
| Problem | Vorgehen |
|
||||
| --- | --- |
|
||||
| `P3005` / vorhandene DB ohne Migrationshistorie | Baseline nur nach Schemaabgleich, siehe Upgrade-Anleitung. Nicht zurücksetzen. |
|
||||
| Doppelte normalisierte E-Mails | Beide Konten erhalten und eindeutige echte Adressen festlegen; kein automatisches Zusammenführen. Danach Migration erneut starten. |
|
||||
| `ENOENT`, Größen-/Inhaltsprüfung eines Anhangs | Volume/Verzeichnisstruktur und Originalbackup prüfen. Fehlende Bytes können nicht rekonstruiert werden. Keine File-Zeilen löschen, um die Prüfung zu umgehen. |
|
||||
| `EACCES` | Eigentümer/Rechte des privaten Volumes für UID/GID 1001 prüfen. Nicht pauschal 777 vergeben. |
|
||||
| Leere Daten nach Umstieg | Projekt-/Volume-Namen und DATABASE_URL vergleichen; häufig wurde ein neues leeres Volume eingebunden. Nicht seeden, bevor die Quelle geklärt ist. |
|
||||
| Login-Schleife / Cookie fehlt | NEXTAUTH_URL, HTTPS und Proxy prüfen; nach Migration alte Sitzung abmelden. |
|
||||
| Worker unhealthy | Logs, DB-Erreichbarkeit, Platz, Konfiguration und Heartbeat prüfen. |
|
||||
|
||||
## Grenzen
|
||||
|
||||
Anhänge: PDF/JPEG/PNG/DOCX/XLSX, maximal 10 MB, tatsächlicher Inhalt wird geprüft. Nicht erkannte oder früher fälschlich akzeptierte Bestandsdateien bleiben erhalten und stoppen die Übernahme bis zur Klärung. Nicht referenzierte Altdateien bleiben im privaten Volume und werden nicht als Aufgabenanhang erfunden.
|
||||
|
||||
CSV/XLSX: 100 Zeilen/100 KB, keine Formeln; Batch: 50 Aufgaben mit möglichen Teilerfolgen; Export: 5000 Aufgaben. Kalenderseite ist eine Monatsagenda. Keine Offline-Datenkopien und keine native lokale MFA. E-Mail/Webhooks können nach einem Absturz doppelt zugestellt werden; Empfänger müssen deduplizieren. Audit ist kein externes manipulationssicheres Archiv. Malware-Scanning, TLS, externe Sicherung und Monitoring sind Betreiberaufgaben. Aktuelle Abhängigkeitsbefunde: [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Entwicklung und Tests
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
npx prisma generate
|
||||
npx prisma migrate deploy
|
||||
npx tsx scripts/migrate-access.ts
|
||||
npm run prisma:seed
|
||||
npx tsx scripts/migrate-uploads.ts
|
||||
npm run dev
|
||||
```
|
||||
|
||||
`prisma:seed` benötigt einmalig BOOTSTRAP_EMAIL und BOOTSTRAP_PASSWORD (mindestens 12 Zeichen, maximal 72 UTF-8-Bytes). Es legt nur dann einen Administrator an, wenn noch keiner vorhanden ist. Das Startpasswort muss beim ersten Login geändert werden. Bootstrap-Werte anschließend entfernen. Kein Standardpasswort ist eingebaut.
|
||||
|
||||
Der Worker wird separat gestartet:
|
||||
|
||||
```sh
|
||||
# separat:
|
||||
npm run worker
|
||||
# Ein einzelner Durchlauf:
|
||||
npm run worker -- --once
|
||||
```
|
||||
|
||||
## Berechtigungen
|
||||
Bei einer leeren Entwicklungsdatenbank einmalig Bootstrap-Werte setzen und `npm run prisma:seed` ausführen. Für Checks:
|
||||
|
||||
Gruppen bilden Teams ab. Rollen bündeln Permissions. Zuweisungen bestimmen den Geltungsbereich:
|
||||
|
||||
- ASSIGNED: Aufgabe ist dem Benutzer zugewiesen.
|
||||
- GROUP: Aufgabe gehört zur Gruppe der Zuweisung. Direkte GROUP-Rollen benötigen zusätzlich eine aktive Mitgliedschaft.
|
||||
- GLOBAL: Zugriff unabhängig von der Gruppe.
|
||||
|
||||
Fehlende Rechte bedeuten Ablehnung. Verwaltungsrechte sind nur GLOBAL erlaubt. Benutzerpflege berechtigt nicht automatisch zur Rechtevergabe. Die Systemadministrator-Rolle ist geschützt und wird ausschließlich direkt vergeben; mindestens ein aktiver Systemadministrator muss bestehen bleiben.
|
||||
|
||||
Für einen Teampool die Rolle „Teampool lesen und übernehmen“ an die Gruppe mit GROUP vergeben und „Bearbeiter“ mit ASSIGNED zuweisen. Mitglieder dürfen dann Aufgaben im Pool übernehmen und anschließend ihre eigenen Aufgaben bearbeiten. „Koordinator“ kann Aufgaben anlegen/zuweisen; für Statusänderungen eigener Aufgaben zusätzlich „Bearbeiter“ vergeben.
|
||||
|
||||
Die alte Role-Spalte bleibt ausschließlich zur verlustarmen Migration erhalten. Sie ist keine Autorisierungsquelle mehr.
|
||||
|
||||
## Prüfen
|
||||
|
||||
```sh
|
||||
```bash
|
||||
npm run typecheck
|
||||
npm run lint
|
||||
npm test
|
||||
npm run build
|
||||
npm audit --omit=dev
|
||||
```
|
||||
|
||||
Integrationstests ändern ausschließlich die ausdrücklich konfigurierte Datenbank `taskmanager_review`. Andere Datenbanknamen werden abgewiesen:
|
||||
Integration und Playwright nur gegen dedizierte `taskmanager_review`-DB, niemals Produktion: `npm run test:integration`, `npx playwright install chromium`, `npx playwright test`. Playwright nutzt Port 3107. Bei Proxy `NO_PROXY=localhost,127.0.0.1` setzen.
|
||||
|
||||
```sh
|
||||
npx prisma migrate deploy
|
||||
npm run test:integration
|
||||
npx playwright install chromium
|
||||
npx playwright test
|
||||
```
|
||||
Der reproduzierbare Altversions-Upgrade-Test verwendet ausschließlich eine **leere** Datenbank `taskmanager_upgrade_test`: `DATABASE_URL=... npx tsx --test tests/upgrade.test.ts`. Er legt das ursprüngliche Schema und Bestandsdaten an, prüft alle alten Aufgabenfelder, Konten, Kommentare, Anhangsbytes, Rollen und Wiederholbarkeit. Er lässt die Testdatenbank zur Kontrolle stehen; ein weiterer Lauf benötigt erneut eine leere Testdatenbank.
|
||||
|
||||
Playwright startet eine lokale Testinstanz auf Port 3107. In Umgebungen mit HTTP-Proxy muss `NO_PROXY=localhost,127.0.0.1` gesetzt sein.
|
||||
|
||||
Deployment und Upgrade: [README.Docker.md](README.Docker.md). API: [API_DOCUMENTATION.md](API_DOCUMENTATION.md). Umsetzung und Prüfstand: [IMPLEMENTATION.md](IMPLEMENTATION.md).
|
||||
Lokale Vorführung: [TESTBETRIEB.md](TESTBETRIEB.md). API: [API_DOCUMENTATION.md](API_DOCUMENTATION.md). Oberfläche und Abnahme: [UX-ABNAHME.md](UX-ABNAHME.md). Technischer Prüfstand: [IMPLEMENTATION.md](IMPLEMENTATION.md).
|
||||
|
||||
186
UPGRADE.md
Normal file
186
UPGRADE.md
Normal file
@ -0,0 +1,186 @@
|
||||
# Upgrade einer bestehenden Produktivinstallation
|
||||
|
||||
Diese Anleitung betrifft den ursprünglichen Stand mit den Tabellen `User`, `Task`, `Comment`, `File` und den Rollen ADMIN, VORGESETZTER, PFLEGER, BEARBEITER. Ausgangsschema: `prisma/migrations/20251111164012_init/migration.sql`, ursprünglicher Commit `c2e00ff13a90041c43bdbdbebff51362f557ac40`.
|
||||
|
||||
**Empfohlener Weg: konsistente Sicherung der gestoppten Altanwendung in eine separate neue Installation wiederherstellen, dort migrieren, prüfen und erst dann umschalten.** Das alte Datenbank- und Upload-Volume bleibt unangetastet. Eine frühere Probe mit einer Backupkopie ersetzt nicht die erneute Sicherung im eigentlichen Wartungsfenster.
|
||||
|
||||
Befehle sind Bash-Beispiele für den ursprünglichen Docker-Betrieb. Container-, Projekt-, DB- und Volume-Namen vor Ausführung mit der eigenen Installation vergleichen. Nicht ungeprüft aus einem anderen Checkout ausführen. Jeder nicht erfolgreiche Schritt beendet den Upgradeversuch; keine Freigabe bei Warnungen über fehlende Daten.
|
||||
|
||||
## 1. Quelle aufnehmen und Probe vorbereiten
|
||||
|
||||
```bash
|
||||
docker ps -a --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
|
||||
docker inspect taskmanager-db --format '{{json .Mounts}}'
|
||||
docker inspect taskmanager-app --format '{{json .Mounts}}'
|
||||
```
|
||||
|
||||
Beim Original-Compose heißen Container `taskmanager-db` und `taskmanager-app`. Tatsächliche Volume-Namen hängen vom Compose-Projektnamen ab. **Keine Annahme, dass ein Volume einfach `uploads` heißt.** Den Mount für `/app/public/uploads` und den für PostgreSQL notieren. Bei Bind-Mounts stattdessen deren Verzeichnisse sichern. Bei nicht persistenten Uploads zuerst das gesamte `/app/public/uploads` aus dem alten Container sichern; Container nicht löschen.
|
||||
|
||||
Festhalten: laufender Git-Commit/Image-ID, Compose-Konfiguration, verschlüsselte Kopie der Secrets, DB-Version, Benutzername/DB-Name, Proxy-Routing, Zeitzone und Mengen der vier Tabellen. App-spezifische lokale Änderungen am Schema mit dem genannten Ausgangsschema vergleichen. Unbekannte Fork-Schemata benötigen eine angepasste Migration; keine pauschale Baseline setzen.
|
||||
|
||||
Neue Version in **separatem** Verzeichnis bauen:
|
||||
|
||||
```bash
|
||||
git clone --branch feature/security-rbac https://git.jfritzsche.de/jf/taskmanager.git taskmanager-next
|
||||
cd taskmanager-next
|
||||
git rev-parse HEAD # Release-Commit im Änderungsprotokoll festhalten
|
||||
cp .env.example .env
|
||||
chmod 600 .env
|
||||
# .env nach README konfigurieren, ohne BOOTSTRAP-Werte und ohne DEMO_MODE.
|
||||
docker compose -p taskmanager-next build
|
||||
```
|
||||
|
||||
`DATABASE_URL` zeigt in der neuen Compose-Installation auf `db:5432/taskmanager`. Neue sichere DB-Zugangsdaten sind möglich, weil eine neue DB aus Dump aufgebaut wird. NEXTAUTH_URL bleibt die endgültige öffentliche URL; SMTP/OIDC/Webhooks für die Probe nicht aktivieren, Worker nicht starten. `ARCHIVE_RETENTION_DAYS=0` belassen. Für eine zeitgleiche Probe App-Port über ein separates Compose-Override auf einen freien Loopback-Port ändern; die Produktionsdomain noch nicht umschalten.
|
||||
|
||||
## 2. Wartungsfenster und zusammengehöriges Backup
|
||||
|
||||
Proxy in Wartungsmodus setzen. Alte Anwendung und gegebenenfalls separate alte Worker stoppen; DB läuft zum Sichern weiter. Nutzeränderungen ab diesem Zeitpunkt verhindern.
|
||||
|
||||
```bash
|
||||
set -euo pipefail
|
||||
BACKUP_DIR="$(pwd)/backups/pre-upgrade-$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
chmod 700 "$BACKUP_DIR"
|
||||
docker stop -t 60 taskmanager-app
|
||||
# Etwaige zusätzliche alte Worker ebenfalls anhalten.
|
||||
docker exec taskmanager-db pg_dump -U taskmanager -d taskmanager -Fc > "$BACKUP_DIR/database.dump"
|
||||
```
|
||||
|
||||
Altes Upload-Volume sichern; den folgenden Namen durch den in Schritt 1 **nachgewiesenen** Namen ersetzen:
|
||||
|
||||
```bash
|
||||
OLD_UPLOAD_VOLUME='TATSAECHLICHER_ALTER_UPLOAD_VOLUME_NAME'
|
||||
docker volume inspect "$OLD_UPLOAD_VOLUME"
|
||||
docker run --rm --network none \
|
||||
-v "$OLD_UPLOAD_VOLUME:/source:ro" -v "$BACKUP_DIR:/backup" \
|
||||
alpine:3.22 tar -czf /backup/uploads.tar.gz -C /source .
|
||||
(cd "$BACKUP_DIR" && sha256sum database.dump uploads.tar.gz > SHA256SUMS)
|
||||
```
|
||||
|
||||
Bei Bind-Mount `-v /absoluter/alter/uploadpfad:/source:ro` verwenden. Bei ausschließlich im Container gespeicherten Anhängen `docker cp taskmanager-app:/app/public/uploads "$BACKUP_DIR/uploads"` und dieses Verzeichnis mit seiner **inneren Struktur** archivieren. Im Archiv müssen unmittelbar Aufgaben-Unterverzeichnisse bzw. Dateien liegen, nicht noch ein zusätzliches `uploads/`-Verzeichnis.
|
||||
|
||||
Alte `.env`, Compose-/Proxy-Konfiguration und Image-ID ebenfalls geschützt sichern. Dump mit `pg_restore --list` prüfen und in der Probe tatsächlich wiederherstellen. Backups extern kopieren, bevor Altcontainer oder Images entfernt werden. **Keine alten Volumes löschen.**
|
||||
|
||||
Bestandsmengen für den späteren Vergleich:
|
||||
|
||||
```bash
|
||||
docker exec taskmanager-db psql -U taskmanager -d taskmanager -c \
|
||||
'SELECT (SELECT count(*) FROM "User") AS users, (SELECT count(*) FROM "Task") AS tasks, (SELECT count(*) FROM "Comment") AS comments, (SELECT count(*) FROM "File") AS files;'
|
||||
```
|
||||
|
||||
## 3. Neue, leere Zielinstallation wiederherstellen
|
||||
|
||||
Projekt `taskmanager-next` muss separat und leer sein. Existiert es bereits aus einer Probe, eine neue Zielinstallation mit neuem Projektnamen erstellen; nicht blind `--clean`, `down -v` oder bestehende Daten überschreiben. Alle nachfolgenden `-p`-Angaben konsistent ersetzen.
|
||||
|
||||
```bash
|
||||
(cd "$BACKUP_DIR" && sha256sum -c SHA256SUMS)
|
||||
docker compose -p taskmanager-next up -d --wait db
|
||||
docker compose -p taskmanager-next exec -T db \
|
||||
pg_restore -U taskmanager -d taskmanager --no-owner --no-privileges --exit-on-error < "$BACKUP_DIR/database.dump"
|
||||
docker compose -p taskmanager-next run --rm --no-deps --user 0:0 \
|
||||
-v "$BACKUP_DIR:/backup:ro" migrate sh -c \
|
||||
'tar -xzf /backup/uploads.tar.gz -C /app/private-uploads && chown -R 1001:1001 /app/private-uploads'
|
||||
```
|
||||
|
||||
Die Eigentümerkorrektur betrifft ausschließlich das **neue private** Upload-Volume. Normale Dienste laufen anschließend wieder als UID 1001. Nicht auf ein unkontrolliertes Host-Verzeichnis anwenden. Das alte Volume wird hier weder eingebunden noch verändert.
|
||||
|
||||
Eine externe PostgreSQL-Installation erfordert entsprechend angepasste DATABASE_URL und Compose-Abhängigkeiten. Ihre DB nicht durch den hier enthaltenen lokalen `db`-Dienst ersetzen. Die Datenmigration selbst bleibt dieselbe.
|
||||
|
||||
## 4. Migrationshistorie prüfen und Migration ausführen
|
||||
|
||||
```bash
|
||||
docker compose -p taskmanager-next run --rm --no-deps migrate npx prisma migrate status
|
||||
```
|
||||
|
||||
Noch ausstehende Migrationen sind auf diesem Stand erwartbar. Eine mit Prisma-Migrationen betriebene Altinstallation enthält `_prisma_migrations`; diese Tabelle ist Teil des Dumps und muss erhalten bleiben.
|
||||
|
||||
**Sonderfall: ursprüngliches Schema wurde früher nur mit `prisma db push` angelegt.** Prisma meldet dann eine nicht leere DB ohne Baseline (P3005). Nur wenn nachweislich das Schema der ursprünglichen Init-Migration vorliegt und diese Änderungen bereits vorhanden sind, die Init-Migration als angewandt markieren:
|
||||
|
||||
```bash
|
||||
docker compose -p taskmanager-next run --rm --no-deps migrate \
|
||||
npx prisma migrate resolve --applied 20251111164012_init
|
||||
```
|
||||
|
||||
Bei Schemaabweichungen stoppen und abgleichen. Niemals die RBAC-Migration als angewandt markieren, um einen Fehler zu übergehen. Fehlgeschlagene SQL-Migrationen anhand ihres konkreten Fehlers reparieren, bevor `resolve` verwendet wird.
|
||||
|
||||
Nun eigentliche Migration:
|
||||
|
||||
```bash
|
||||
docker compose -p taskmanager-next run --rm migrate
|
||||
```
|
||||
|
||||
Der Befehl führt nacheinander aus:
|
||||
|
||||
1. `prisma migrate deploy`: additive Schemaänderungen und Indizes. Keine Neuinitialisierung.
|
||||
2. `scripts/migrate-access.ts`: eindeutige E-Mail-Normalisierung, Bestandsmitgliedschaften, alte Rollen nach RBAC, Entwertung alter Sitzungen. Transaktional und über einen Migrationsmarker wiederholbar.
|
||||
3. `scripts/migrate-uploads.ts`: **alle** referenzierten Dateien auf Vorhandensein, Größe und zugelassenen Inhalt prüfen; alte `/uploads/...`-Pfade in zufällige private Speicherschlüssel umstellen und kopierte Inhalte per SHA-256 vergleichen. Originaldateien bleiben als Rückfallkopie im privaten Volume erhalten. Bereits migrierte Dateien werden beim erneuten Lauf geprüft, nicht erneut kopiert.
|
||||
|
||||
App hängt vom erfolgreichen Migrationsdienst ab. Kein Start bei Fehlern. Die Dateiübernahme ist pro Datei wiederaufnehmbar, nicht eine globale Dateisystemtransaktion. Nach einem Abbruch Ursache beheben und erneut ausführen. Keine parallelen Migratoren und keine schreibenden alten/neuen Apps währenddessen betreiben.
|
||||
|
||||
### Welche Daten bleiben erhalten?
|
||||
|
||||
| Bestand | Verhalten |
|
||||
| --- | --- |
|
||||
| Benutzer-ID, Name, Passwort-Hash, alte Rolle | Erhalten; vorhandene Passwörter funktionieren weiter. E-Mail wird getrimmt und kleingeschrieben; kollidierende Adressen stoppen die Migration. |
|
||||
| Aufgaben-ID, Beschreibung, Standort, Fälligkeit, Status, Ersteller/Zuweisung, Zeitstempel | Erhalten; alle Bestandsaufgaben gehören zunächst zur Gruppe `Bestand` (`legacy`). |
|
||||
| `beauftragtAm`, `firmaBeauftragt` | Unverändert in der Datenbank/API erhalten. In der aktuellen Aufgabenmaske gibt es dafür keine eigenen Eingabefelder. |
|
||||
| Kommentare, Autoren, Zuordnungen, Zeitstempel | Unverändert erhalten. |
|
||||
| Dateieinträge, IDs, Namen, Zuordnungen, Originalbytes | Erhalten. Nur Speicherpfad und ermittelter MIME-Typ ändern sich. |
|
||||
| Neue Felder | Sichere Standardwerte, z.B. keine Archivierung, keine Wiederholung, leere Checklisten/Tags. Ein historisches Abschlussdatum wird nicht erfunden. |
|
||||
| Sitzungen | Absichtlich ungültig; erneute Anmeldung erforderlich. |
|
||||
| Nicht referenzierte Altdateien | Physisch im privaten Volume erhalten, nicht automatisch einer Aufgabe zugeordnet. |
|
||||
|
||||
Fehlende oder ungültige Anhänge werden **nicht** übersprungen oder gelöscht. Anhand des Backups korrigieren bzw. fachlich prüfen. Der ursprüngliche Upload prüfte nur den behaupteten MIME-Typ; die neue Inhaltsprüfung kann deshalb alte Problemdateien erkennen. Ohne Zugriff auf reale Bestandsdaten kann keine pauschale Garantie für beschädigte oder fehlende Dateien gegeben werden.
|
||||
|
||||
### Alte Rollen werden übernommen
|
||||
|
||||
| Alte Rolle | Neue direkte Rollen |
|
||||
| --- | --- |
|
||||
| ADMIN | Systemadministrator / GLOBAL |
|
||||
| BEARBEITER | Bearbeiter / ASSIGNED |
|
||||
| PFLEGER | Koordinator / GLOBAL und Bearbeiter / ASSIGNED |
|
||||
| VORGESETZTER | Teamleitung / GLOBAL |
|
||||
|
||||
Alle Bestandsbenutzer erhalten Mitgliedschaft in `Bestand`. Alte globale Rechte anschließend organisatorisch prüfen und passende Gruppen anlegen. Teamleitung erhält nicht automatisch Rechteverwaltung. Keine Gruppen werden aus Namen oder Aufgabenorten erraten. Es erfolgt keine automatische Kontenzusammenführung und kein Seed überschreibt alte Konten.
|
||||
|
||||
## 5. Abnahme vor Umschalten
|
||||
|
||||
Zuerst Mengen vergleichen:
|
||||
|
||||
```bash
|
||||
docker compose -p taskmanager-next exec -T db psql -U taskmanager -d taskmanager -c \
|
||||
'SELECT (SELECT count(*) FROM "User") AS users, (SELECT count(*) FROM "Task") AS tasks, (SELECT count(*) FROM "Comment") AS comments, (SELECT count(*) FROM "File") AS files;'
|
||||
docker compose -p taskmanager-next exec -T db psql -U taskmanager -d taskmanager -c \
|
||||
'SELECT count(*) AS unmigrated_files FROM "File" WHERE path LIKE '\''/uploads/%'\'';'
|
||||
docker compose -p taskmanager-next up -d --wait app
|
||||
curl --fail http://127.0.0.1:3000/api/health
|
||||
```
|
||||
|
||||
`unmigrated_files` muss 0 sein. Datei-Migration prüft jeden DB-Anhang; zusätzlich repräsentative alte Dateien im Browser herunterladen und mit Originalen vergleichen. Vorhandene Konten, Kommentare, Zuweisungen, erledigte Aufgaben und Beauftragungsdaten prüfen. Admin- und Bearbeiter-Login sowie fehlende Rechte, Uploads, Statuswechsel und Gruppenverwaltung testen. Neue private Dateien dürfen nicht über alte `/uploads`-URLs erreichbar sein. Ein Reverse Proxy darf alte Uploads auch nicht selbst statisch ausliefern.
|
||||
|
||||
Nach Abnahme produktive externe Dienste konfigurieren, genau einen Worker starten und erst dann Wartungsmodus aufheben:
|
||||
|
||||
```bash
|
||||
docker compose -p taskmanager-next up -d --wait app worker
|
||||
docker compose -p taskmanager-next ps -a
|
||||
```
|
||||
|
||||
Der Projektname `taskmanager-next` bleibt danach der Name dieser Installation, auch für Backups und Updates. Beide Installationen niemals gleichzeitig öffentlich beschreibbar betreiben. Alte Installation und gesicherte Images zunächst als Rückfallbasis behalten; alte öffentliche Uploadfreigaben sperren. Originaldateien im neuen privaten Volume erst nach gesonderter Prüfung und gesichertem Restore entfernen; keine pauschale rekursive Bereinigung.
|
||||
|
||||
## 6. Rollback
|
||||
|
||||
**Vor öffentlicher Freigabe:** neue App/Worker stoppen, Proxy auf alten Stand zurückstellen, alte App mit ihrer ursprünglichen DB und ihrem ursprünglichen Upload-Volume starten. Diese wurden durch den empfohlenen Kopie-Workflow nicht verändert. Das Wartungsfenster erst nach geprüftem alten Login/Download beenden. Rückkehr zum Altstand bedeutet auch Rückkehr zu dessen bekannten Sicherheitsgrenzen.
|
||||
|
||||
```bash
|
||||
docker compose -p taskmanager-next stop -t 60 app worker
|
||||
docker start taskmanager-app
|
||||
# Proxy-Routing und Wartungsmodus bewusst zurückstellen.
|
||||
```
|
||||
|
||||
**Nach Freigabe:** zuerst neue Installation konsistent sichern. Seit dem Umstieg entstandene Aufgaben/Kommentare/Dateien wären in der alten DB nicht enthalten. Fachlich entscheiden, ob vorwärts repariert oder diese Änderungen ausdrücklich abgeglichen werden. Nicht einfach umschalten und damit Änderungen verlieren.
|
||||
|
||||
**Bei In-place-Upgrade:** alter Code allein genügt nicht. DB-Dump in eine leere Ersatzdatenbank mit `pg_restore --exit-on-error --no-owner --no-privileges` einspielen, dazu das zugehörige Upload-Archiv in ein leeres Ersatzvolume entpacken, alte Konfiguration/Images mit diesen Ersatzressourcen verbinden und prüfen. Kein Test-Restore in eine laufende Produktivdatenbank.
|
||||
|
||||
## Nachweis und Grenzen
|
||||
|
||||
`tests/upgrade.test.ts` baut eine leere Testdatenbank aus dem ursprünglichen SQL-Schema auf, fügt alle vier alten Rollen sowie Aufgabe mit Beauftragungsdaten, Kommentar und verschachteltem Altanhang ein, migriert und vergleicht Bestandsfelder/Bytes. Zweiter Lauf prüft Idempotenz; fehlende private Datei wird als Fehler erkannt. Reale Produktivdaten sind damit nicht geprüft: die Restore-Probe und fachliche Abnahme der eigenen Sicherung bleiben vor dem Umstieg notwendig.
|
||||
@ -17,7 +17,12 @@ services:
|
||||
context: .
|
||||
target: operations
|
||||
env_file: ${TASKMANAGER_ENV_FILE:-.env}
|
||||
command: ["sh", "-c", "npx prisma migrate deploy && npx tsx scripts/migrate-access.ts"]
|
||||
command: ["sh", "-c", "npx prisma migrate deploy && npx tsx scripts/migrate-access.ts && npx tsx scripts/migrate-uploads.ts"]
|
||||
environment:
|
||||
UPLOAD_DIR: /app/private-uploads
|
||||
LEGACY_UPLOAD_DIR: /app/private-uploads
|
||||
volumes:
|
||||
- uploads:/app/private-uploads
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
@ -60,7 +60,7 @@ async function main() {
|
||||
data: { users: users.length },
|
||||
},
|
||||
});
|
||||
});
|
||||
}, { timeout: 120000 });
|
||||
console.log(
|
||||
"RBAC-Datenmigration abgeschlossen. Globale Altzuweisungen in der Verwaltung prüfen.",
|
||||
);
|
||||
|
||||
@ -1,32 +1,44 @@
|
||||
import "dotenv/config";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import path from "node:path";
|
||||
import { readFile, unlink } from "node:fs/promises";
|
||||
import { saveFile, removeFile } from "../src/lib/storage";
|
||||
import { readFile, realpath } from "node:fs/promises";
|
||||
import { createHash } from "node:crypto";
|
||||
import { fileTypeFromBuffer } from "file-type";
|
||||
import { saveFile, removeFile, storagePath, MAX_UPLOAD } from "../src/lib/storage";
|
||||
const db = new PrismaClient();
|
||||
const sha = (bytes: Uint8Array) => createHash("sha256").update(bytes).digest("hex");
|
||||
async function main() {
|
||||
const root = path.resolve(process.env.LEGACY_UPLOAD_DIR || "public/uploads");
|
||||
const files = await db.file.findMany({
|
||||
where: { path: { startsWith: "/uploads/" } },
|
||||
});
|
||||
const files = await db.file.findMany();
|
||||
async function source(file: typeof files[number]) {
|
||||
if (!file.path.startsWith("/uploads/")) return storagePath(file.path);
|
||||
const base = await realpath(root);
|
||||
const resolved = await realpath(path.resolve(base, file.path.slice("/uploads/".length)));
|
||||
if (!resolved.startsWith(base + path.sep)) throw new Error(`Anhang ${file.id}: Pfad außerhalb des Upload-Verzeichnisses`);
|
||||
return resolved;
|
||||
}
|
||||
// Validate every referenced file before converting any paths. Never silently skip data.
|
||||
for (const file of files) {
|
||||
const old = path.resolve(root, file.path.slice("/uploads/".length));
|
||||
if (!old.startsWith(root + path.sep))
|
||||
throw new Error("Pfad außerhalb des Upload-Verzeichnisses");
|
||||
const saved = await saveFile(await readFile(old));
|
||||
const bytes = await readFile(await source(file));
|
||||
if (bytes.length !== file.size) throw new Error(`Anhang ${file.id}: Dateigröße stimmt nicht mit der Datenbank überein`);
|
||||
const type = await fileTypeFromBuffer(bytes);
|
||||
if (!bytes.length || bytes.length > MAX_UPLOAD || !type || !["pdf", "jpg", "png", "docx", "xlsx"].includes(type.ext))
|
||||
throw new Error(`Anhang ${file.id}: Inhalt oder Größe nicht zulässig; Original bleibt erhalten`);
|
||||
}
|
||||
let migrated = 0;
|
||||
for (const file of files.filter(f => f.path.startsWith("/uploads/"))) {
|
||||
const bytes = await readFile(await source(file));
|
||||
const saved = await saveFile(bytes);
|
||||
try {
|
||||
await db.file.update({
|
||||
where: { id: file.id },
|
||||
data: { path: saved.key, mimeType: saved.mime },
|
||||
});
|
||||
if (sha(await readFile(storagePath(saved.key))) !== sha(bytes)) throw new Error(`Anhang ${file.id}: Prüfsummenabweichung`);
|
||||
await db.file.update({ where: { id: file.id }, data: { path: saved.key, mimeType: saved.mime } });
|
||||
} catch (e) {
|
||||
await removeFile(saved.key);
|
||||
throw e;
|
||||
}
|
||||
await unlink(old);
|
||||
// Keep originals for recovery. The volume is private and never served statically.
|
||||
migrated++;
|
||||
}
|
||||
console.log(
|
||||
`${files.length} Anhänge migriert. Nicht referenzierte Altdateien separat prüfen.`,
|
||||
);
|
||||
console.log(`${files.length} Anhänge geprüft, ${migrated} migriert. Originaldateien bleiben erhalten; öffentliche /uploads/-Auslieferung sperren.`);
|
||||
}
|
||||
main().finally(() => db.$disconnect());
|
||||
main().catch(error => { console.error(error); process.exitCode = 1; }).finally(() => db.$disconnect());
|
||||
|
||||
76
tests/upgrade.test.ts
Normal file
76
tests/upgrade.test.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { mkdtemp, mkdir, writeFile, readFile, rm } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
const exec = promisify(execFile);
|
||||
|
||||
test("original production schema upgrades without losing users, tasks, comments or file contents", async () => {
|
||||
if (!process.env.DATABASE_URL || new URL(process.env.DATABASE_URL).pathname !== "/taskmanager_upgrade_test")
|
||||
throw new Error("Dedicated EMPTY taskmanager_upgrade_test database required");
|
||||
const db = new PrismaClient();
|
||||
const root = await mkdtemp(path.join(tmpdir(), "taskmanager-upgrade-"));
|
||||
const legacy = path.join(root, "legacy");
|
||||
const target = path.join(root, "private");
|
||||
const env = { ...process.env, LEGACY_UPLOAD_DIR: legacy, UPLOAD_DIR: target };
|
||||
const prisma = (...args: string[]) => exec(process.execPath, ["node_modules/prisma/build/index.js", ...args], { env });
|
||||
const script = (name: string) => exec(process.execPath, ["--import", "tsx", `scripts/${name}.ts`], { env });
|
||||
try {
|
||||
const tables = await db.$queryRaw<{ count: bigint }[]>`SELECT count(*) FROM information_schema.tables WHERE table_schema='public'`;
|
||||
assert.equal(Number(tables[0].count), 0, "Refuse to modify a non-empty database");
|
||||
await prisma("db", "execute", "--file", "prisma/migrations/20251111164012_init/migration.sql", "--schema", "prisma/schema.prisma");
|
||||
await prisma("migrate", "resolve", "--applied", "20251111164012_init");
|
||||
for (const [id, role] of [["admin", "ADMIN"], ["worker", "BEARBEITER"], ["coordinator", "PFLEGER"], ["leader", "VORGESETZTER"]]) {
|
||||
await db.$executeRaw`INSERT INTO "User" (id,email,name,password,role,"updatedAt") VALUES (${id},${id === "admin" ? " ADMIN@Example.test " : `${id}@example.test`},${id},'unchanged-password-hash',${role}::"Role",'2025-01-02')`;
|
||||
}
|
||||
await db.$executeRaw`INSERT INTO "Task" (id,wo,was,"bisWann",status,"beauftragtAm","firmaBeauftragt","creatorId","assigneeId","updatedAt") VALUES ('task','Gebäude Ä','Bestehende Wartung','2025-11-15','ERLEDIGT','2025-11-10','Fachfirma GmbH','admin','worker','2025-11-16')`;
|
||||
await db.$executeRaw`INSERT INTO "Comment" (id,content,"taskId","authorId") VALUES ('comment','Bestehender Kommentar','task','leader')`;
|
||||
const bytes = Buffer.from("%PDF-1.4\n1 0 obj\n<<>>\nendobj\n%%EOF");
|
||||
await mkdir(path.join(legacy, "task"), { recursive: true });
|
||||
await writeFile(path.join(legacy, "task", "original.pdf"), bytes);
|
||||
await db.$executeRaw`INSERT INTO "File" (id,name,path,"mimeType",size,"taskId") VALUES ('file','Original.pdf','/uploads/task/original.pdf','application/pdf',${bytes.length},'task')`;
|
||||
const beforeUsers = await db.$queryRaw<Record<string, unknown>[]>`SELECT * FROM "User"`;
|
||||
const beforeFiles = await db.$queryRaw<Record<string, unknown>[]>`SELECT * FROM "File"`;
|
||||
const beforeTask = await db.$queryRaw<Record<string, unknown>[]>`SELECT * FROM "Task"`;
|
||||
const beforeComment = await db.$queryRaw<Record<string, unknown>[]>`SELECT * FROM "Comment"`;
|
||||
await prisma("migrate", "deploy");
|
||||
await script("migrate-access");
|
||||
await script("migrate-uploads");
|
||||
const afterTask = await db.task.findUniqueOrThrow({ where: { id: "task" } });
|
||||
for (const [key, value] of Object.entries(beforeTask[0])) assert.deepEqual(afterTask[key as keyof typeof afterTask], value, `Task.${key}`);
|
||||
assert.deepEqual(await db.comment.findMany(), beforeComment);
|
||||
for (const old of beforeUsers) {
|
||||
const current = await db.user.findUniqueOrThrow({ where: { id: String(old.id) } });
|
||||
for (const [key, value] of Object.entries(old)) {
|
||||
if (key === "updatedAt") continue;
|
||||
assert.deepEqual(current[key as keyof typeof current], key === "email" ? String(value).trim().toLowerCase() : value, `User.${key}`);
|
||||
}
|
||||
}
|
||||
assert.equal(await db.user.count(), 4);
|
||||
assert.equal(await db.userGroup.count(), 4);
|
||||
assert.equal(await db.userRole.count(), 5);
|
||||
const admin = await db.user.findUniqueOrThrow({ where: { id: "admin" } });
|
||||
assert.equal(admin.email, "admin@example.test");
|
||||
assert.equal(admin.password, "unchanged-password-hash");
|
||||
assert.equal(admin.sessionVersion, 2);
|
||||
const migrated = await db.file.findUniqueOrThrow({ where: { id: "file" } });
|
||||
for (const [key, value] of Object.entries(beforeFiles[0])) {
|
||||
if (key !== "path" && key !== "mimeType") assert.deepEqual(migrated[key as keyof typeof migrated], value, `File.${key}`);
|
||||
}
|
||||
assert.deepEqual(await readFile(path.join(target, migrated.path)), bytes);
|
||||
assert.deepEqual(await readFile(path.join(legacy, "task", "original.pdf")), bytes);
|
||||
await script("migrate-access");
|
||||
await script("migrate-uploads");
|
||||
assert.equal((await db.user.findUniqueOrThrow({ where: { id: "admin" } })).sessionVersion, 2);
|
||||
assert.equal(await db.userRole.count(), 5);
|
||||
assert.equal((await db.file.findUniqueOrThrow({ where: { id: "file" } })).path, migrated.path);
|
||||
await rm(path.join(target, migrated.path));
|
||||
await assert.rejects(script("migrate-uploads"), /ENOENT/);
|
||||
} finally {
|
||||
await db.$disconnect();
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@ -1,13 +1,13 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { mkdtemp, mkdir, writeFile, readFile, access, rm } from "node:fs/promises";
|
||||
import { mkdtemp, mkdir, writeFile, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
test("legacy upload migration preserves content, removes public copy and is repeatable", async () => {
|
||||
test("legacy upload migration preserves content, retains original recovery copy and is repeatable", async () => {
|
||||
if (!process.env.DATABASE_URL || new URL(process.env.DATABASE_URL).pathname !== "/taskmanager_review")
|
||||
throw new Error("Dedicated taskmanager_review database required");
|
||||
const db = new PrismaClient();
|
||||
@ -29,7 +29,7 @@ test("legacy upload migration preserves content, removes public copy and is repe
|
||||
const file = await db.file.findUniqueOrThrow({ where: { id } });
|
||||
assert.match(file.path, /^[a-f0-9-]{36}\.pdf$/);
|
||||
assert.deepEqual(await readFile(path.join(target, file.path)), bytes);
|
||||
await assert.rejects(access(path.join(legacy, "fixture.pdf")));
|
||||
assert.deepEqual(await readFile(path.join(legacy, "fixture.pdf")), bytes);
|
||||
await run();
|
||||
assert.equal((await db.file.findUniqueOrThrow({ where: { id } })).path, file.path);
|
||||
} finally {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user