Fundament Benutzer-/Rollenverwaltung: Schema, Passwort-Policy, Force-Change

Vorbereitung für die Benutzer- & Rollenverwaltung (Pakete A–C) ohne E-Mail-Flow.

- Schema: User.mustChangePassword (erzwungener Wechsel), User.mfaEnrolledAt +
  recoveryCodes (optionale Nutzer-MFA, Paket C); neues Singleton PlatformSetting
  mit mfaRequired (Default aus → MFA optional). Migration additiv + Grant/Seed.
- RBAC: neue Permission role:manage (Rollenverwaltung), dem Mandanten-Admin
  zugewiesen; für den bestehenden Demo-Mandanten nachgezogen (neue Tenants via
  provision/seed automatisch).
- Passwort-Policy: lib/password-policy.ts (client-safe Validierung/Beschreibung,
  Defaults + Ableitung aus TenantSettings.securityPolicy) und server/password.ts
  (Argon2id-Hash + policy-konformer Einmal-Passwort-Generator).
- Force-Change-Flow: /change-password (außerhalb der (app)-Shell) + Self-Service-
  Action changeOwnPassword (policy-geprüft, Audit). (app)-Layout erzwingt den
  Wechsel autoritativ aus der DB und sperrt deaktivierte Konten (Abmelden-Screen).

tsc + lint + build + Guard-Check grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 09:18:05 +02:00
co-authored by Claude Opus 4.8
parent e07dcd9f2a
commit db36c795bd
10 changed files with 262 additions and 2 deletions
@@ -0,0 +1,22 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "mfa_enrolled_at" TIMESTAMP(3),
ADD COLUMN "must_change_password" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "recovery_codes" JSONB NOT NULL DEFAULT '[]';
-- CreateTable
CREATE TABLE "platform_settings" (
"id" TEXT NOT NULL DEFAULT 'singleton',
"mfa_required" BOOLEAN NOT NULL DEFAULT false,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "platform_settings_pkey" PRIMARY KEY ("id")
);
-- platform_settings ist plattformweit (kein tenant_id) → RLS nicht aktiviert.
GRANT SELECT, INSERT, UPDATE, DELETE ON "platform_settings" TO isms_app;
-- Singleton-Zeile anlegen (MFA-Pflicht standardmäßig aus).
INSERT INTO "platform_settings" ("id", "mfa_required", "updated_at")
VALUES ('singleton', false, now())
ON CONFLICT ("id") DO NOTHING;