- Retention policies (Admin -> Privacy -> Retention) applied by a nightly
`retention` job: inactive accounts anonymised after 5 years with a
30-day warning (on by default; never staff roles or people with an
upcoming registration), form answers, login history, payer details in
payment notifications, audit/access logs
- User.lastLoginAt (set on login, backfilled) and inactivityWarnedAt
- Breach register (SecurityBreach) with drafts for the people affected
and the Information Regulator
- Readiness checklist with a score
- Children's/medical flag in the access log; {retention} placeholder in
the privacy notice
- Unit tests (retention.test.js), e2e (privacy-retention.spec.ts); docs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012mt6TCv85T9VKRq3cV7rXq
58 lines
2.1 KiB
SQL
58 lines
2.1 KiB
SQL
-- CreateEnum
|
|
CREATE TYPE "BreachStatus" AS ENUM ('open', 'contained', 'closed');
|
|
|
|
-- AlterEnum
|
|
-- This migration adds more than one value to an enum.
|
|
-- With PostgreSQL versions 11 and earlier, this is not possible
|
|
-- in a single migration. This can be worked around by creating
|
|
-- multiple migrations, each migration adding only one value to
|
|
-- the enum.
|
|
|
|
|
|
ALTER TYPE "AdminAuditAction" ADD VALUE 'retention_policies_updated';
|
|
ALTER TYPE "AdminAuditAction" ADD VALUE 'retention_run';
|
|
ALTER TYPE "AdminAuditAction" ADD VALUE 'breach_recorded';
|
|
ALTER TYPE "AdminAuditAction" ADD VALUE 'breach_updated';
|
|
|
|
-- AlterTable
|
|
ALTER TABLE "DataAccessLog" ADD COLUMN "sensitive" BOOLEAN NOT NULL DEFAULT false;
|
|
|
|
-- AlterTable
|
|
ALTER TABLE "User" ADD COLUMN "inactivityWarnedAt" TIMESTAMP(3),
|
|
ADD COLUMN "lastLoginAt" TIMESTAMP(3);
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "SecurityBreach" (
|
|
"id" TEXT NOT NULL,
|
|
"title" TEXT NOT NULL,
|
|
"status" "BreachStatus" NOT NULL DEFAULT 'open',
|
|
"discoveredAt" TIMESTAMP(3) NOT NULL,
|
|
"occurredAt" TIMESTAMP(3),
|
|
"description" TEXT NOT NULL,
|
|
"dataAffected" TEXT NOT NULL,
|
|
"peopleAffected" INTEGER,
|
|
"whoAffected" TEXT,
|
|
"consequences" TEXT,
|
|
"actionsTaken" TEXT,
|
|
"adviceToPeople" TEXT,
|
|
"causedBy" TEXT,
|
|
"regulatorNotifiedAt" TIMESTAMP(3),
|
|
"peopleNotifiedAt" TIMESTAMP(3),
|
|
"createdById" TEXT,
|
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
|
|
CONSTRAINT "SecurityBreach_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "SecurityBreach_status_discoveredAt_idx" ON "SecurityBreach"("status", "discoveredAt");
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "SecurityBreach" ADD CONSTRAINT "SecurityBreach_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
|
|
-- Last login, from the login history (which the retention job will start expiring).
|
|
UPDATE "User" u SET "lastLoginAt" = l.at
|
|
FROM (SELECT "userId", MAX("createdAt") AS at FROM "SecurityEvent" WHERE "type" = 'login' AND "userId" IS NOT NULL GROUP BY "userId") l
|
|
WHERE u."id" = l."userId";
|