Merge current features with security hardening and automatic safe initialization

This commit is contained in:
t.indorf
2026-10-09 23:29:19 +02:00
85 changed files with 13522 additions and 1326 deletions
+7
View File
@@ -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
View File
@@ -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
+4 -2
View File
@@ -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.
+7 -4
View File
@@ -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"
+160
View File
@@ -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;
+37
View File
@@ -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;
+286
View File
@@ -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;