diff --git a/internal-packages/database/prisma/migrations/20260428112409_admin_user_deletion/migration.sql b/internal-packages/database/prisma/migrations/20260428112409_admin_user_deletion/migration.sql new file mode 100644 index 000000000..d5d827ecb --- /dev/null +++ b/internal-packages/database/prisma/migrations/20260428112409_admin_user_deletion/migration.sql @@ -0,0 +1,35 @@ +-- DropForeignKey +ALTER TABLE "public"."MfaBackupCode" DROP CONSTRAINT "MfaBackupCode_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."PersonalAccessToken" DROP CONSTRAINT "PersonalAccessToken_userId_fkey"; + +-- CreateTable +CREATE TABLE "public"."UserDeletionAuditLog" ( + "id" TEXT NOT NULL, + "adminUserId" TEXT NOT NULL, + "adminEmail" TEXT NOT NULL, + "targetUserId" TEXT NOT NULL, + "targetEmail" TEXT NOT NULL, + "softDeletedOrgIds" TEXT[], + "reason" TEXT, + "ipAddress" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "UserDeletionAuditLog_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "UserDeletionAuditLog_adminUserId_idx" ON "public"."UserDeletionAuditLog"("adminUserId"); + +-- CreateIndex +CREATE INDEX "UserDeletionAuditLog_targetUserId_idx" ON "public"."UserDeletionAuditLog"("targetUserId"); + +-- CreateIndex +CREATE INDEX "UserDeletionAuditLog_createdAt_idx" ON "public"."UserDeletionAuditLog"("createdAt"); + +-- AddForeignKey +ALTER TABLE "public"."MfaBackupCode" ADD CONSTRAINT "MfaBackupCode_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."PersonalAccessToken" ADD CONSTRAINT "PersonalAccessToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/internal-packages/database/prisma/schema.prisma b/internal-packages/database/prisma/schema.prisma index 9ccf2495d..3737f828d 100644 --- a/internal-packages/database/prisma/schema.prisma +++ b/internal-packages/database/prisma/schema.prisma @@ -74,7 +74,7 @@ model MfaBackupCode { /// Hash of the actual code code String - user User @relation(fields: [userId], references: [id]) + user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) userId String usedAt DateTime? @@ -131,7 +131,7 @@ model PersonalAccessToken { /// This is used to find the token in the database hashedToken String @unique - user User @relation(fields: [userId], references: [id]) + user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) userId String revokedAt DateTime? @@ -2590,6 +2590,32 @@ model ImpersonationAuditLog { @@index([createdAt]) } +model UserDeletionAuditLog { + id String @id @default(cuid()) + + /// Denormalized — audit row must survive after the target user is deleted. + /// No FKs to User; store IDs and emails as plain text. + adminUserId String + adminEmail String + targetUserId String + targetEmail String + + /// Organization IDs that were soft-deleted as part of this user delete + /// (orgs where the deleted user was the sole member). + softDeletedOrgIds String[] + + /// Optional: ticket link / GDPR request id / free-text SE note. + reason String? + + ipAddress String? + + createdAt DateTime @default(now()) + + @@index([adminUserId]) + @@index([targetUserId]) + @@index([createdAt]) +} + enum CustomerQuerySource { DASHBOARD API