feat(nextcloud-files): Modul Dateien mit Nextcloud-Adresse, Verbindungsprüfung und gesicherter Verbindungsschicht

- Migration 20261008180000: NextcloudFilesConfig (Organisation) und NextcloudFilesAccount
  (Mandant UND Benutzer per Zeilenschutz, nur verschlüsseltes App-Passwort)
- Transportschicht ncRequest: feste Pfadanfänge, Segmentcodierung, keine Weiterleitungen,
  keine Cookies, Zeitgrenzen; Aufrufsperre pausiert den Ursprung nach jedem 429 und
  sperrt Zugangsschlüssel nach dem ersten 401
- Einstellungen: Adresse speichern/prüfen, Adresswechsel läuft alle Konten ab (mit Bestätigung)
- Controller mit Verwalten nur auf Einstellungen, Seed, Modul, Registrierung in Web
  (Ladefunktion, Symbol folder, Navigation, Layout) und Reiter Einstellungen
- RLS-Inventar fortgeschrieben, Testaufbau (tessera-nc-test) und E2E-Skripte

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 17:41:46 +02:00
parent 83b842b72c
commit 960696745f
35 changed files with 3282 additions and 2 deletions
@@ -0,0 +1,105 @@
-- 261008-mzu — Modul "Nextcloud-Dateien" (Etappe 1).
--
-- Zweck: zwei Tabellen und zwei Aufzaehlungen.
-- * "NextcloudFilesConfig": Einstellungen der Organisation (Singleton), genau
-- eine Nextcloud-Adresse (`baseUrl`, Normalform von normalizeCloudUrl).
-- * "NextcloudFilesAccount": das persoenliche Konto eines Benutzers in dieser
-- Nextcloud. Gespeichert wird NUR das App-Passwort, AES-256-GCM-verschluesselt
-- ueber den CryptoService (`encryptedAppPassword`); das echte Passwort des
-- Benutzers wird nie abgelegt. `ncUserId` ist die Kennung in der Nextcloud
-- (nicht der eingegebene Anmeldename), `connectedVia` merkt sich den Weg
-- (Passwort oder Login Flow v2 fuer Zwei-Faktor-Konten).
--
-- Warum das Konto die Adresse mitfuehrt ("baseUrl"): ein App-Passwort gilt nur
-- fuer die Nextcloud, die es ausgestellt hat. Es wird nie an einen anderen Host
-- geschickt; wechselt die Adresse der Organisation, laufen alle Konten ab
-- (status = EXPIRED, oder Konto-baseUrl weicht von der Einstellung ab) und die
-- Benutzer verbinden neu.
--
-- Der DDL-Teil stammt aus `prisma migrate diff`, damit der Stand ohne Abweichung
-- zum Schema passt; die Zeilenschutz-Regeln sind von Hand ergaenzt.
--
-- Zeilenschutz (Pflicht — sonst schlaegt rls-coverage.spec.ts fehl):
-- "NextcloudFilesConfig" — tenant_isolation_policy nur ueber den Mandanten
-- (Verwaltungsdaten der Organisation, wie "DomainsConfig").
-- "NextcloudFilesAccount" — tenant_isolation_policy ueber Mandant UND Benutzer
-- (Form aus 20260929140000_reminder): ohne gesetzten Benutzer gilt nur der
-- Mandant, mit Benutzer zusaetzlich "userId". Genau das haelt einen Benutzer
-- davon ab, je das Konto eines anderen zu lesen. KEINE system_read_policy:
-- es gibt keinen Hintergrunddienst, der ueber alle Mandanten liest.
--
-- Faellt ein Benutzer weg, faellt sein Konto mit (ON DELETE CASCADE). Das
-- App-Passwort bleibt dann in der Nextcloud bestehen (Geraeteliste dort) — das
-- ist dokumentiert und fuer Etappe 1 bewusst nicht aufgeraeumt.
--
-- Rechte fuer die Anwendungsrolle tessera_app: kommen ueber ALTER DEFAULT
-- PRIVILEGES aus 20260909130000_rls_app_role automatisch — hier nichts zu tun.
--
-- WICHTIG: wie alle RLS-Regeln dieses Schemas wirken diese erst, wenn die
-- Anwendung als Rolle ohne Umgehungsrecht verbindet (Schalter heute AUS, siehe
-- docs/mandantentrennung-datenbankrolle.md). Bis dahin tragen die
-- Anwendungspruefungen im Dienst den Schutz allein.
-- CreateEnum
CREATE TYPE "NextcloudFilesAccountStatus" AS ENUM ('ACTIVE', 'EXPIRED');
-- CreateEnum
CREATE TYPE "NextcloudFilesConnectMethod" AS ENUM ('PASSWORD', 'LOGIN_FLOW');
-- CreateTable
CREATE TABLE "NextcloudFilesConfig" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"baseUrl" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "NextcloudFilesConfig_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "NextcloudFilesAccount" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"baseUrl" TEXT NOT NULL,
"ncUserId" TEXT NOT NULL,
"ncDisplayName" TEXT,
"encryptedAppPassword" TEXT NOT NULL,
"status" "NextcloudFilesAccountStatus" NOT NULL DEFAULT 'ACTIVE',
"connectedVia" "NextcloudFilesConnectMethod" NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "NextcloudFilesAccount_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "NextcloudFilesConfig_tenantId_key" ON "NextcloudFilesConfig"("tenantId");
-- CreateIndex
CREATE INDEX "NextcloudFilesConfig_tenantId_idx" ON "NextcloudFilesConfig"("tenantId");
-- CreateIndex
CREATE INDEX "NextcloudFilesAccount_tenantId_idx" ON "NextcloudFilesAccount"("tenantId");
-- CreateIndex
CREATE UNIQUE INDEX "NextcloudFilesAccount_tenantId_userId_key" ON "NextcloudFilesAccount"("tenantId", "userId");
-- AddForeignKey
ALTER TABLE "NextcloudFilesAccount" ADD CONSTRAINT "NextcloudFilesAccount_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- Zeilenschutz: nur Mandant (Organisations-Einstellung)
ALTER TABLE "NextcloudFilesConfig" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "NextcloudFilesConfig" FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_policy ON "NextcloudFilesConfig"
USING ("tenantId" = current_tenant_id());
-- Zeilenschutz: Mandant UND Benutzer (Muster 20260929140000_reminder)
ALTER TABLE "NextcloudFilesAccount" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "NextcloudFilesAccount" FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_policy ON "NextcloudFilesAccount"
USING (
"tenantId" = current_tenant_id()
AND (current_user_id() IS NULL OR "userId" = current_user_id())
);