Files
Florian Wilde 2a80f15e20 Phase B2: mehrere Domains fuer Kurz-URLs
Eine Domain kann mehreren Gruppen dienen (n:m ueber DomainGrant), und der
Slug eines Codes ist nur noch je Domain eindeutig - derselbe sprechende
Slug darf also auf zwei Domains liegen.

Sicherheitsrelevanter Kern: die Weiterleitung liest den Hostnamen aus
X-Forwarded-Host bzw. Host und prueft ihn gegen die Domain-Tabelle. Ein
unbekannter Hostname endet mit 404, faellt also auf die Standard-Domain
zurueck und findet dort nichts. Ueber einen gefaelschten Header laesst sich
damit keine fremde Domain ansprechen. Auch der Hostname fuer die Vorschau
kommt aus der Datenbank, nicht aus der Anfrage.

- Migration legt die Spalte erst nullable an, fuellt sie mit der
  Standard-Domain und macht sie danach verpflichtend; der globale
  Slug-Index weicht dem zusammengesetzten aus (domainId, slug).
- Verwaltung -> Domains: registrieren, Gruppen zuteilen, Voreinstellung je
  Gruppe, abschalten. Domains mit Codes lassen sich weder loeschen noch
  einer Gruppe entziehen, die noch Codes darunter hat - beides wuerde
  gedruckte Codes ins Leere laufen lassen.

Ein Fehler dabei gefunden und behoben: die Migration kennt BASE_URL nicht
und legt die Standard-Domain mit einem Platzhalter-Hostnamen an. Korrigiert
wurde er bisher nur beim Oeffnen der Domain-Verwaltung - auf einer frischen
Installation waeren also Kurz-URLs auf 'standard.invalid' entstanden.
ensureDefaultDomain() laeuft jetzt vor jeder Verwendung.

Geprueft: 34 neue Pruefungen fuer Host-Aufloesung, Spoofing, Slug je Domain
und Zuteilung; dazu 280 bestehende bei ausgeschalteter Anmeldung.
2026-08-14 13:10:12 +02:00

87 lines
3.0 KiB
SQL

-- Mehrere Domains für Kurz-URLs.
--
-- Zwei Dinge sind hier heikel und deshalb ausdrücklich schrittweise gelöst:
--
-- 1. `QrCode.domainId` ist verpflichtend, es gibt aber schon Codes. Die
-- Spalte entsteht zuerst ohne Einschränkung, wird gefüllt und erst danach
-- auf NOT NULL gesetzt.
-- 2. `slug` war global eindeutig und ist es künftig nur noch je Domain. Der
-- alte Index muss weichen, bevor der neue greifen kann.
-- CreateTable
CREATE TABLE "Domain" (
"id" TEXT NOT NULL,
"hostname" TEXT NOT NULL,
"label" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Domain_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "DomainGrant" (
"id" TEXT NOT NULL,
"domainId" TEXT NOT NULL,
"groupId" TEXT NOT NULL,
"isDefault" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "DomainGrant_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Domain_hostname_key" ON "Domain"("hostname");
-- CreateIndex
CREATE INDEX "DomainGrant_groupId_idx" ON "DomainGrant"("groupId");
-- CreateIndex
CREATE UNIQUE INDEX "DomainGrant_domainId_groupId_key" ON "DomainGrant"("domainId", "groupId");
-- Standard-Domain. Der Hostname ist zunächst ein Platzhalter: die
-- Migration kennt BASE_URL nicht. Beim ersten Start ersetzt
-- ensureDefaultDomain() ihn durch den Hostnamen aus BASE_URL.
INSERT INTO "Domain" ("id", "hostname", "label", "isActive", "createdAt", "updatedAt")
VALUES (
'dom_standard',
'standard.invalid',
'Standard-Domain',
true,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
);
-- Alle bestehenden Gruppen bekommen die Standard-Domain als Voreinstellung.
INSERT INTO "DomainGrant" ("id", "domainId", "groupId", "isDefault", "createdAt")
SELECT 'dg_std_' || "Group"."id", 'dom_standard', "Group"."id", true, CURRENT_TIMESTAMP
FROM "Group";
-- AlterTable: erst nullable …
ALTER TABLE "QrCode" ADD COLUMN "domainId" TEXT;
-- … bestehende Codes der Standard-Domain zuordnen …
UPDATE "QrCode" SET "domainId" = 'dom_standard' WHERE "domainId" IS NULL;
-- … und erst jetzt verpflichtend machen.
ALTER TABLE "QrCode" ALTER COLUMN "domainId" SET NOT NULL;
-- Eindeutigkeit des Slugs von global auf „je Domain“ umstellen.
DROP INDEX "QrCode_slug_key";
-- CreateIndex
CREATE UNIQUE INDEX "QrCode_domainId_slug_key" ON "QrCode"("domainId", "slug");
-- CreateIndex
CREATE INDEX "QrCode_domainId_idx" ON "QrCode"("domainId");
-- AddForeignKey
ALTER TABLE "DomainGrant" ADD CONSTRAINT "DomainGrant_domainId_fkey" FOREIGN KEY ("domainId") REFERENCES "Domain"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DomainGrant" ADD CONSTRAINT "DomainGrant_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "Group"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "QrCode" ADD CONSTRAINT "QrCode_domainId_fkey" FOREIGN KEY ("domainId") REFERENCES "Domain"("id") ON DELETE RESTRICT ON UPDATE CASCADE;