Merge current features with security hardening and automatic safe initialization
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
# Deployment und Mailversand
|
||||
|
||||
Die aktuelle Anleitung steht in [README.md](README.md), die Konfigurationsvorlage in [production.env.example](production.env.example).
|
||||
|
||||
Der Compose-Dienst `migrate` initialisiert das Schema über versionierte Migrationen und stellt den separaten Runtime-Zugang bereit. Die App startet erst nach erfolgreicher Initialisierung. `prisma db push`, Demo-Seeding und historische SQL-Upgrades sind im Produktionsstart nicht vorgesehen.
|
||||
|
||||
Für Resend müssen Versanddomain, `RESEND_API_KEY` und `EMAIL_FROM` gültig konfiguriert sein. `ADMIN_EMAIL` ist das Ziel für das Kontaktformular. Die drei öffentlichen Basis-URLs müssen übereinstimmen, damit Einladungs- und Bestätigungslinks auf die richtige Anwendung zeigen. Der Notdienst-Worker startet im App-Container mit `NOTDIENST_JOB_SECRET`; kein zusätzlicher Cron-Dienst ist erforderlich.
|
||||
+13
-11
@@ -4,12 +4,18 @@ Die Anwendung startet ohne Schemaänderungen und ohne Demo-Seeding. Datenbankmig
|
||||
|
||||
## Konfiguration
|
||||
|
||||
Werte aus `production.env.example` als Hosting-Secrets hinterlegen. `APP_DATABASE_URL` enthält die eingeschränkte Rolle; `MIGRATION_DATABASE_URL` den Datenbankeigentümer. Für lokale CLI-Befehle heißt die jeweils benötigte URL `DATABASE_URL`. `APP_DATABASE_PASSWORD` ist das separate Runtime-Passwort (24–256 Zeichen); in URLs muss es URL-kodiert werden. Keine echten Secrets im Repository speichern.
|
||||
Werte aus `production.env.example` als Hosting-Secrets hinterlegen. Im Compose-Stack erzeugt der Dienst `migrate` automatisch ein separates Runtime-Passwort im privaten Secret-Volume, wendet die Migrationen an, vergibt die eingeschränkten DB-Rechte und bereitet die Upload-/Cache-Volumes vor. Danach startet `app` mit UID/GID 1001. Das Eigentümerpasswort steht nur PostgreSQL und dem Initialisierungsdienst zur Verfügung. Für manuelle CLI-Befehle außerhalb von Compose bleibt `DATABASE_URL` die jeweilige administrative oder eingeschränkte URL. Keine echten Secrets im Repository speichern.
|
||||
|
||||
`AUTH_URL`, `NEXTAUTH_URL` und `NEXT_PUBLIC_SITE_URL` müssen dieselbe kanonische HTTPS-Origin verwenden. Der Reverse Proxy terminiert TLS, routet intern auf App-Port 3000 und überschreibt weitergeleitete Host-/IP-Header. `TRUSTED_CLIENT_IP_HEADER` nur setzen, wenn der Proxy den gewählten Header (`x-real-ip` oder `cf-connecting-ip`) zuverlässig überschreibt. Ohne diese Einstellung gelten weiterhin gemeinsame globale und identitätsbezogene Datenbanklimits. Proxyseitig Body-Limit von höchstens 26 MiB, Timeouts und zusätzliche Request-Limits setzen.
|
||||
|
||||
Compose startet PostgreSQL standardmäßig intern. Bei einer bereits extern verwalteten Coolify-Datenbank den Stack entsprechend anpassen und deren interne URL verwenden; niemals versehentlich einen neuen leeren Datenbankdienst als Ersatz anschließen. pgAdmin ist nur im Profil `maintenance` und nur auf Loopback verfügbar; Zugriff über SSH-Tunnel.
|
||||
|
||||
## Neustart mit leerer Datenbank
|
||||
|
||||
Wenn vorhandene Daten ausdrücklich löschbare Testdaten sind: ausschließlich die zu dieser Installation gehörenden Datenbank- und Upload-Volumes zurücksetzen. Danach genügt ein normales Deployment mit `docker compose -f docker-compose.coolify.yml up -d --build app`. Es sind weder eine Baseline-Markierung noch manuelle Schema-Upgrades oder Demo-Seeding erforderlich. Der Initialisierungsdienst läuft vor der App; ein Fehler verhindert deren Start. Bestehende produktive Daten erfordern dagegen die unten beschriebene Vorbereitung.
|
||||
|
||||
Im Normalbetrieb bleiben Datenbank, Uploads, Cache und Runtime-Secret-Volume bei Deployments bestehen. Das Runtime-Passwort wird bei einem normalen Neustart wiederverwendet. Niemals die Datenbank als Startnebenwirkung löschen.
|
||||
|
||||
## Bestehende Installation aktualisieren
|
||||
|
||||
1. Datenbank **und** Upload-Volume sichern und Wiederherstellung überprüfen. Wartungsfenster ankündigen und alte App stoppen: das neue Schema und der neue Code gehören zusammen.
|
||||
@@ -24,24 +30,20 @@ Compose-Beispiele (Environment-Secrets vorher bereitstellen):
|
||||
```sh
|
||||
docker compose -f docker-compose.coolify.yml build app migrate
|
||||
# NUR bei bestehender, zuvor geprüfter Baseline-Datenbank:
|
||||
docker compose -f docker-compose.coolify.yml --profile maintenance run --rm migrate npx prisma migrate resolve --applied 20261009000000_baseline
|
||||
# Bei neuen und bestehenden Datenbanken:
|
||||
docker compose -f docker-compose.coolify.yml --profile maintenance run --rm migrate npm run db:migrate:deploy
|
||||
docker compose -f docker-compose.coolify.yml --profile maintenance run --rm migrate npm run db:provision-runtime
|
||||
docker compose -f docker-compose.coolify.yml --profile maintenance run --rm migrate npm run uploads:backfill
|
||||
# Nur auf dem vorgesehenen App-Upload-Volume:
|
||||
docker compose -f docker-compose.coolify.yml --profile maintenance run --rm migrate chown -R 1001:1001 /app/uploads
|
||||
# Falls ein bereits vorhandenes Cache-Volume root gehört: dessen Eigentümer separat korrigieren.
|
||||
docker compose -f docker-compose.coolify.yml run --rm migrate npx prisma migrate resolve --applied 20261009000000_baseline
|
||||
# Initialisierung läuft automatisch vor der App:
|
||||
docker compose -f docker-compose.coolify.yml up -d app
|
||||
```
|
||||
|
||||
Die Baseline-Markierung benötigt bei einem manuellen Aufruf eine administrative `DATABASE_URL`; für eine leere Datenbank entfällt sie vollständig. Vorhandene Uploads müssen vor dem Start kontrolliert und ihre Größen unter administrativem Zugang nachgetragen werden. Für bestehende root-eigene Unterverzeichnisse ist eine gezielte Eigentümerkorrektur nötig; die Initialisierung bereitet die Basisverzeichnisse vor.
|
||||
|
||||
Bei einem Rollback nach Migration zuerst Kompatibilität prüfen; das alte Image allein zurückzusetzen reicht wegen neuer Pflichtfelder nicht aus. Im Zweifel das geprüfte Backup von Datenbank und Dateien gemeinsam wiederherstellen.
|
||||
|
||||
## Auswirkungen auf bestehende Konten
|
||||
|
||||
Alle JWT-Sitzungen vor diesem Release werden ungültig; erneute Anmeldung ist erforderlich. Administrative Sperren entwerten auch bei späterer Entsperrung dauerhaft die alten Sitzungen. Gründerkonten ohne Kita und ohne echte E-Mail-Prüfung registrieren sich mit derselben Adresse erneut und bestätigen den Link. Bereits bestehende Kita-Gründer behalten ihre bisherige Verifizierung; deren historische Adressen werden dadurch nicht nachträglich als echte Mailbox-Bestätigung nachgewiesen.
|
||||
|
||||
Neue Registrierungen bestätigen zuerst die E-Mail-Adresse, wählen anschließend ihr Passwort und erstellen erst danach eine Kita. Beim Ändern einer Elternadresse werden Passwort und bisherige Sitzungen entwertet; die neue Adresse erhält eine Einladung zum Festlegen eines neuen Passworts. Kita-Admins können die Einladung erneut senden. Neue Registrierungslinks gelten 30 Minuten, Einladungen sieben Tage und Notdienst-Bestätigungslinks höchstens 24 Stunden sowie nur für offene, noch relevante Einsätze. Bestehende UUID-Links bleiben innerhalb ihrer Laufzeit kompatibel. Zwei-Faktor-Anmeldung ist auf ausdrücklichen Wunsch vorerst nicht eingeführt.
|
||||
Neue Registrierungen bestätigen zuerst die E-Mail-Adresse, wählen anschließend ihr Passwort und erstellen erst danach eine Kita. Beim Ändern einer Elternadresse werden Passwort und bisherige Sitzungen entwertet; die neue Adresse erhält eine Einladung zum Festlegen eines neuen Passworts. Kita-Admins können die Einladung erneut senden. Neue Registrierungslinks gelten 30 Minuten, Einladungen sieben Tage und Notdienst-Rückmeldelinks bis zum Ende des jeweiligen Einsatzes; eine gemeinsame Haushaltsantwort kann nur einmal gespeichert werden. Sperren entwerten vorhandene Rückmeldelinks dauerhaft. Die öffentlichen Token-Antworten zeigen nur die benötigten Einsatzangaben. Bestehende UUID-Links bleiben innerhalb ihrer Laufzeit kompatibel. Zwei-Faktor-Anmeldung ist auf ausdrücklichen Wunsch vorerst nicht eingeführt.
|
||||
|
||||
## Dateien und Wartung
|
||||
|
||||
@@ -51,7 +53,7 @@ Grenzen: 10 MiB pro Datei, fünf Dateien und 25 MiB pro Nachricht; standardmäß
|
||||
|
||||
`npm run uploads:cleanup` zeigt zunächst nur verwaiste Dateien mit mindestens 24 Stunden Alter. Nach Prüfung entfernt `npm run uploads:cleanup -- --delete` diese Dateien. Backups, Speicherüberwachung und eine Aufbewahrungs-/Löschstrategie für Backups separat einrichten. Dateien gelöschter Datensätze werden auch unmittelbar bestmöglich entfernt.
|
||||
|
||||
Die SQL-Migrationen ergänzen **31 zusammengesetzte Tenant-Fremdschlüssel**, die Prisma wegen der vorhandenen optionalen Cascade-/SetNull-Beziehungen nicht vollständig abbildet. Zukünftige generierte Migrationen auf unbeabsichtigtes Entfernen dieser Constraints prüfen. Kein `prisma db push` auf verwalteten Datenbanken einsetzen. Runtime-Grants nach zukünftigen Migrationen erneut bereitstellen.
|
||||
Die SQL-Migrationen ergänzen **41 zusammengesetzte Tenant-Fremdschlüssel**, die Prisma wegen der vorhandenen optionalen Cascade-/SetNull-Beziehungen nicht vollständig abbildet. Zukünftige generierte Migrationen auf unbeabsichtigtes Entfernen dieser Constraints prüfen. Kein `prisma db push` auf verwalteten Datenbanken einsetzen. Runtime-Grants nach zukünftigen Migrationen erneut bereitstellen.
|
||||
|
||||
## Verifikation und Updates
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
# Production-Checkliste
|
||||
|
||||
## Vor dem Rollout
|
||||
## Vor dem Rollout mit bestehenden Daten
|
||||
|
||||
Bei einem ausdrücklich genehmigten Reset löschbarer Testdaten entfallen Backup-Übernahme, Baseline-Markierung und Upload-Backfill. Nach dem gezielten Reset initialisiert das normale Deployment den leeren Stand.
|
||||
|
||||
- [ ] Datenbank und Uploads gesichert; Wiederherstellung geprüft.
|
||||
- [ ] Alte App gestoppt; Wartungsfenster und erneute Anmeldung angekündigt.
|
||||
- [ ] Bestehendes Schema mit Baseline verglichen; Tenant-Preflight ohne Befunde.
|
||||
- [ ] Baseline nur bei bestehender passender DB markiert; anschließend alle Migrationen angewendet.
|
||||
- [ ] Runtime-Rolle mit separatem Passwort bereitgestellt; App verwendet `APP_DATABASE_URL`.
|
||||
- [ ] Initialisierungsdienst erfolgreich; App verwendet automatisch die eingeschränkte Runtime-Rolle.
|
||||
- [ ] Upload-Größen nachgetragen; gemeldete fehlende Dateien geprüft.
|
||||
- [ ] Upload- und Cache-Volumes für UID/GID 1001 beschreibbar; Upload-Pfad unverändert.
|
||||
|
||||
|
||||
@@ -11,10 +11,9 @@ POSTGRES_DB="kita_planer"
|
||||
POSTGRES_USER="kita_planer"
|
||||
POSTGRES_PASSWORD="[GENERATE_ME]"
|
||||
|
||||
# Unterschiedliche DB-Rollen für Runtime und Migration. URL-Passwörter müssen URL-kodiert sein.
|
||||
APP_DATABASE_PASSWORD="[GENERATE_ME]"
|
||||
APP_DATABASE_URL="postgresql://kita_runtime:[URL_ENCODED_APP_PASSWORD]@postgres:5432/kita_planer?schema=public"
|
||||
MIGRATION_DATABASE_URL="postgresql://kita_planer:[URL_ENCODED_OWNER_PASSWORD]@postgres:5432/kita_planer?schema=public"
|
||||
# Der Initialisierungsdienst erzeugt automatisch einen separaten Runtime-Zugang.
|
||||
# Keine zusätzlichen APP_DATABASE_URL-/Passwort-Secrets nötig.
|
||||
# Passwort wird nur im privaten kita_planer_runtime_secrets-Volume gespeichert.
|
||||
|
||||
# pgAdmin im Coolify-Compose-Stack
|
||||
PGADMIN_DEFAULT_EMAIL="[admin@deine-domain.de]"
|
||||
@@ -41,6 +40,10 @@ RESEND_API_KEY="re_[PRODUCTION_API_KEY]"
|
||||
EMAIL_FROM="Kita-Planer <[absender@deine-domain.de]>"
|
||||
ADMIN_EMAIL="[kontakt@deine-domain.de]"
|
||||
|
||||
# Automatische Notdienst-Fristpruefung im App-Container.
|
||||
# Eigenes Secret, mindestens 32 Zeichen; generieren mit: openssl rand -hex 32
|
||||
NOTDIENST_JOB_SECRET="[GENERATE_ME]"
|
||||
|
||||
# Persistenter Upload-Speicher fuer Anhaenge vom Schwarzen Brett.
|
||||
# Muss auf dem Server dauerhaft erhalten bleiben und darf nicht im Repo liegen.
|
||||
UPLOAD_DIR="/var/lib/kita-planer/uploads/announcements"
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
-- Additives Upgrade für einen Personalbereich einschließlich vorhandener Mail-Erweiterung.
|
||||
-- Einmalig vor dem Start der neuen Anwendung ausführen, sofern nicht prisma db push verwendet wird.
|
||||
BEGIN;
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "NotdienstAlarmStatus" AS ENUM ('PENDING', 'CONFIRMED', 'DECLINED', 'LATE_CONFIRMATION', 'REVIEW_REQUIRED', 'CANCELLED');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "notdienst_alerts" ADD COLUMN "alarmId" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "notdienst_alarms" (
|
||||
"id" TEXT NOT NULL,
|
||||
"kitaId" TEXT NOT NULL,
|
||||
"assignmentId" TEXT,
|
||||
"activeAssignmentId" TEXT,
|
||||
"familyId" TEXT NOT NULL,
|
||||
"familyName" TEXT NOT NULL,
|
||||
"childName" TEXT NOT NULL,
|
||||
"date" DATE NOT NULL,
|
||||
"wholeDay" BOOLEAN NOT NULL DEFAULT true,
|
||||
"startMinute" INTEGER,
|
||||
"endMinute" INTEGER,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"deadline" TIMESTAMP(3) NOT NULL,
|
||||
"status" "NotdienstAlarmStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"triggeredById" TEXT,
|
||||
"triggeredAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"respondedById" TEXT,
|
||||
"respondedAt" TIMESTAMP(3),
|
||||
"responseSource" TEXT,
|
||||
"escalatedAt" TIMESTAMP(3),
|
||||
"educatorId" TEXT,
|
||||
"notes" TEXT,
|
||||
"cancelledAt" TIMESTAMP(3),
|
||||
"cancelReason" TEXT,
|
||||
|
||||
CONSTRAINT "notdienst_alarms_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "notdienst_alarm_recipients" (
|
||||
"id" TEXT NOT NULL,
|
||||
"alarmId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"token" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "notdienst_alarm_recipients_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "notdienst_alarm_notifications" (
|
||||
"id" TEXT NOT NULL,
|
||||
"alarmId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"eventKey" TEXT NOT NULL,
|
||||
"message" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"readAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "notdienst_alarm_notifications_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "notdienst_alarm_emails" (
|
||||
"id" TEXT NOT NULL,
|
||||
"alarmId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"eventKey" TEXT NOT NULL,
|
||||
"kind" TEXT NOT NULL,
|
||||
"subject" TEXT NOT NULL,
|
||||
"message" TEXT NOT NULL,
|
||||
"path" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"sentAt" TIMESTAMP(3),
|
||||
"skippedAt" TIMESTAMP(3),
|
||||
"leaseUntil" TIMESTAMP(3),
|
||||
"failed" BOOLEAN NOT NULL DEFAULT false,
|
||||
"attempts" INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
CONSTRAINT "notdienst_alarm_emails_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "notdienst_alarm_history" (
|
||||
"id" TEXT NOT NULL,
|
||||
"alarmId" TEXT NOT NULL,
|
||||
"actorId" TEXT,
|
||||
"action" TEXT NOT NULL,
|
||||
"details" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "notdienst_alarm_history_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "notdienst_alarms_activeAssignmentId_key" ON "notdienst_alarms"("activeAssignmentId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "notdienst_alarms_kitaId_date_idx" ON "notdienst_alarms"("kitaId", "date");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "notdienst_alarms_status_deadline_idx" ON "notdienst_alarms"("status", "deadline");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "notdienst_alarm_recipients_token_key" ON "notdienst_alarm_recipients"("token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "notdienst_alarm_recipients_alarmId_userId_key" ON "notdienst_alarm_recipients"("alarmId", "userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "notdienst_alarm_notifications_userId_readAt_idx" ON "notdienst_alarm_notifications"("userId", "readAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "notdienst_alarm_notifications_alarmId_userId_eventKey_key" ON "notdienst_alarm_notifications"("alarmId", "userId", "eventKey");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "notdienst_alarm_emails_sentAt_skippedAt_createdAt_idx" ON "notdienst_alarm_emails"("sentAt", "skippedAt", "createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "notdienst_alarm_emails_alarmId_userId_eventKey_key" ON "notdienst_alarm_emails"("alarmId", "userId", "eventKey");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "notdienst_alarm_history_alarmId_createdAt_idx" ON "notdienst_alarm_history"("alarmId", "createdAt");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "notdienst_alarms" ADD CONSTRAINT "notdienst_alarms_kitaId_fkey" FOREIGN KEY ("kitaId") REFERENCES "kitas"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "notdienst_alarms" ADD CONSTRAINT "notdienst_alarms_assignmentId_fkey" FOREIGN KEY ("assignmentId") REFERENCES "notdienst_assignments"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "notdienst_alarms" ADD CONSTRAINT "notdienst_alarms_familyId_fkey" FOREIGN KEY ("familyId") REFERENCES "families"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "notdienst_alarm_recipients" ADD CONSTRAINT "notdienst_alarm_recipients_alarmId_fkey" FOREIGN KEY ("alarmId") REFERENCES "notdienst_alarms"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "notdienst_alarm_recipients" ADD CONSTRAINT "notdienst_alarm_recipients_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "notdienst_alarm_notifications" ADD CONSTRAINT "notdienst_alarm_notifications_alarmId_fkey" FOREIGN KEY ("alarmId") REFERENCES "notdienst_alarms"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "notdienst_alarm_notifications" ADD CONSTRAINT "notdienst_alarm_notifications_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "notdienst_alarm_emails" ADD CONSTRAINT "notdienst_alarm_emails_alarmId_fkey" FOREIGN KEY ("alarmId") REFERENCES "notdienst_alarms"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "notdienst_alarm_emails" ADD CONSTRAINT "notdienst_alarm_emails_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "notdienst_alarm_history" ADD CONSTRAINT "notdienst_alarm_history_alarmId_fkey" FOREIGN KEY ("alarmId") REFERENCES "notdienst_alarms"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "notdienst_alerts" ADD CONSTRAINT "notdienst_alerts_alarmId_fkey" FOREIGN KEY ("alarmId") REFERENCES "notdienst_alarms"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,37 @@
|
||||
-- Nur E-Mail-Erweiterung für einen bereits mit 043fed2 eingeführten Personalbereich.
|
||||
-- Alternative zu prisma db push, nicht zusätzlich oder mehrfach ausführen.
|
||||
BEGIN;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "users" ADD COLUMN "staffDecisionEmails" BOOLEAN NOT NULL DEFAULT true;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "staff_emails" (
|
||||
"id" TEXT NOT NULL,
|
||||
"profileId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"toAdmins" BOOLEAN NOT NULL DEFAULT false,
|
||||
"subject" TEXT NOT NULL,
|
||||
"message" TEXT NOT NULL,
|
||||
"path" TEXT NOT NULL,
|
||||
"inviteToken" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"sentAt" TIMESTAMP(3),
|
||||
"skippedAt" TIMESTAMP(3),
|
||||
"leaseUntil" TIMESTAMP(3),
|
||||
"attempts" INTEGER NOT NULL DEFAULT 0,
|
||||
"failed" BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
CONSTRAINT "staff_emails_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "staff_emails_profileId_sentAt_skippedAt_idx" ON "staff_emails"("profileId", "sentAt", "skippedAt");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "staff_emails" ADD CONSTRAINT "staff_emails_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "staff_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "staff_emails" ADD CONSTRAINT "staff_emails_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,286 @@
|
||||
-- Einmalige Einführung des vollständigen Personalbereichs inklusive E-Mails ab ec3f4f9.
|
||||
-- Alternative zu prisma db push, nicht zusätzlich oder mehrfach ausführen.
|
||||
BEGIN;
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "StaffRequestStatus" AS ENUM ('PENDING', 'APPROVED', 'REJECTED', 'CANCEL_REQUESTED', 'CANCELLED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "StaffLeaveKind" AS ENUM ('VACATION', 'TIME_OFF', 'SICKNESS');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "StaffWeekStatus" AS ENUM ('DRAFT', 'SUBMITTED', 'APPROVED');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "users" ADD COLUMN "staffDecisionEmails" BOOLEAN NOT NULL DEFAULT true;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "staff_profiles" (
|
||||
"id" TEXT NOT NULL,
|
||||
"kitaId" TEXT NOT NULL,
|
||||
"educatorId" TEXT NOT NULL,
|
||||
"userId" TEXT,
|
||||
"employmentStart" DATE NOT NULL,
|
||||
"employmentEnd" DATE,
|
||||
"trackingStart" DATE NOT NULL,
|
||||
"openingMinutes" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "staff_profiles_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "staff_emails" (
|
||||
"id" TEXT NOT NULL,
|
||||
"profileId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"toAdmins" BOOLEAN NOT NULL DEFAULT false,
|
||||
"subject" TEXT NOT NULL,
|
||||
"message" TEXT NOT NULL,
|
||||
"path" TEXT NOT NULL,
|
||||
"inviteToken" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"sentAt" TIMESTAMP(3),
|
||||
"skippedAt" TIMESTAMP(3),
|
||||
"leaseUntil" TIMESTAMP(3),
|
||||
"attempts" INTEGER NOT NULL DEFAULT 0,
|
||||
"failed" BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
CONSTRAINT "staff_emails_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "staff_work_patterns" (
|
||||
"id" TEXT NOT NULL,
|
||||
"profileId" TEXT NOT NULL,
|
||||
"effectiveFrom" DATE NOT NULL,
|
||||
"dailyMinutes" INTEGER[],
|
||||
|
||||
CONSTRAINT "staff_work_patterns_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "staff_day_overrides" (
|
||||
"id" TEXT NOT NULL,
|
||||
"profileId" TEXT NOT NULL,
|
||||
"date" DATE NOT NULL,
|
||||
"minutes" INTEGER NOT NULL,
|
||||
"reason" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "staff_day_overrides_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "staff_holidays" (
|
||||
"id" TEXT NOT NULL,
|
||||
"kitaId" TEXT NOT NULL,
|
||||
"date" DATE NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "staff_holidays_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "staff_leave_accounts" (
|
||||
"id" TEXT NOT NULL,
|
||||
"profileId" TEXT NOT NULL,
|
||||
"year" INTEGER NOT NULL,
|
||||
"entitlementUnits" INTEGER NOT NULL,
|
||||
"carryUnits" INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
CONSTRAINT "staff_leave_accounts_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "staff_leaves" (
|
||||
"id" TEXT NOT NULL,
|
||||
"profileId" TEXT NOT NULL,
|
||||
"kind" "StaffLeaveKind" NOT NULL,
|
||||
"status" "StaffRequestStatus" NOT NULL DEFAULT 'PENDING',
|
||||
"startDate" DATE NOT NULL,
|
||||
"endDate" DATE NOT NULL,
|
||||
"halfDay" BOOLEAN NOT NULL DEFAULT false,
|
||||
"minutes" INTEGER,
|
||||
"note" TEXT,
|
||||
"decisionNote" TEXT,
|
||||
"requestedById" TEXT NOT NULL,
|
||||
"reviewedById" TEXT,
|
||||
"reviewedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "staff_leaves_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "staff_leave_days" (
|
||||
"id" TEXT NOT NULL,
|
||||
"leaveId" TEXT NOT NULL,
|
||||
"date" DATE NOT NULL,
|
||||
"vacationUnits" INTEGER NOT NULL DEFAULT 0,
|
||||
"creditMinutes" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "staff_leave_days_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "staff_time_entries" (
|
||||
"id" TEXT NOT NULL,
|
||||
"profileId" TEXT NOT NULL,
|
||||
"date" DATE NOT NULL,
|
||||
"startMinute" INTEGER NOT NULL,
|
||||
"endMinute" INTEGER NOT NULL,
|
||||
"breakMinutes" INTEGER NOT NULL DEFAULT 0,
|
||||
"note" TEXT,
|
||||
|
||||
CONSTRAINT "staff_time_entries_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "staff_weeks" (
|
||||
"id" TEXT NOT NULL,
|
||||
"profileId" TEXT NOT NULL,
|
||||
"weekStart" DATE NOT NULL,
|
||||
"status" "StaffWeekStatus" NOT NULL DEFAULT 'DRAFT',
|
||||
"submittedAt" TIMESTAMP(3),
|
||||
"reviewedAt" TIMESTAMP(3),
|
||||
"reviewedById" TEXT,
|
||||
"decisionNote" TEXT,
|
||||
"dailySnapshot" JSONB,
|
||||
|
||||
CONSTRAINT "staff_weeks_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "staff_adjustments" (
|
||||
"id" TEXT NOT NULL,
|
||||
"profileId" TEXT NOT NULL,
|
||||
"date" DATE NOT NULL,
|
||||
"minutes" INTEGER NOT NULL,
|
||||
"reason" TEXT NOT NULL,
|
||||
"actorId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "staff_adjustments_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "staff_history" (
|
||||
"id" TEXT NOT NULL,
|
||||
"profileId" TEXT NOT NULL,
|
||||
"actorId" TEXT NOT NULL,
|
||||
"action" TEXT NOT NULL,
|
||||
"details" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "staff_history_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "staff_notifications" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"profileId" TEXT NOT NULL,
|
||||
"message" TEXT NOT NULL,
|
||||
"readAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "staff_notifications_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "staff_profiles_educatorId_key" ON "staff_profiles"("educatorId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "staff_profiles_userId_key" ON "staff_profiles"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "staff_profiles_kitaId_idx" ON "staff_profiles"("kitaId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "staff_emails_profileId_sentAt_skippedAt_idx" ON "staff_emails"("profileId", "sentAt", "skippedAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "staff_work_patterns_profileId_effectiveFrom_key" ON "staff_work_patterns"("profileId", "effectiveFrom");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "staff_day_overrides_profileId_date_key" ON "staff_day_overrides"("profileId", "date");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "staff_holidays_kitaId_date_key" ON "staff_holidays"("kitaId", "date");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "staff_leave_accounts_profileId_year_key" ON "staff_leave_accounts"("profileId", "year");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "staff_leaves_profileId_status_startDate_idx" ON "staff_leaves"("profileId", "status", "startDate");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "staff_leave_days_leaveId_date_key" ON "staff_leave_days"("leaveId", "date");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "staff_time_entries_profileId_date_idx" ON "staff_time_entries"("profileId", "date");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "staff_weeks_profileId_weekStart_key" ON "staff_weeks"("profileId", "weekStart");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "staff_adjustments_profileId_date_idx" ON "staff_adjustments"("profileId", "date");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "staff_history_profileId_createdAt_idx" ON "staff_history"("profileId", "createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "staff_notifications_userId_readAt_createdAt_idx" ON "staff_notifications"("userId", "readAt", "createdAt");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "staff_profiles" ADD CONSTRAINT "staff_profiles_kitaId_fkey" FOREIGN KEY ("kitaId") REFERENCES "kitas"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "staff_profiles" ADD CONSTRAINT "staff_profiles_educatorId_fkey" FOREIGN KEY ("educatorId") REFERENCES "educators"("id") ON DELETE NO ACTION ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "staff_profiles" ADD CONSTRAINT "staff_profiles_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "staff_emails" ADD CONSTRAINT "staff_emails_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "staff_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "staff_emails" ADD CONSTRAINT "staff_emails_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "staff_work_patterns" ADD CONSTRAINT "staff_work_patterns_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "staff_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "staff_day_overrides" ADD CONSTRAINT "staff_day_overrides_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "staff_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "staff_holidays" ADD CONSTRAINT "staff_holidays_kitaId_fkey" FOREIGN KEY ("kitaId") REFERENCES "kitas"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "staff_leave_accounts" ADD CONSTRAINT "staff_leave_accounts_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "staff_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "staff_leaves" ADD CONSTRAINT "staff_leaves_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "staff_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "staff_leave_days" ADD CONSTRAINT "staff_leave_days_leaveId_fkey" FOREIGN KEY ("leaveId") REFERENCES "staff_leaves"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "staff_time_entries" ADD CONSTRAINT "staff_time_entries_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "staff_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "staff_weeks" ADD CONSTRAINT "staff_weeks_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "staff_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "staff_adjustments" ADD CONSTRAINT "staff_adjustments_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "staff_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "staff_history" ADD CONSTRAINT "staff_history_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "staff_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "staff_notifications" ADD CONSTRAINT "staff_notifications_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "staff_notifications" ADD CONSTRAINT "staff_notifications_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "staff_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
COMMIT;
|
||||
Reference in New Issue
Block a user