- Privacy page: news opt-in per channel with history, download my data (JSON + PDF), correction and deletion requests - Bulk messages are an Event update or News & announcements; news only reaches people who opted in, with unsubscribe links, one-click List-Unsubscribe headers and "Reply STOP" on WhatsApp - WhatsApp STOP via WAWP's message webhook, checked against a secret in the webhook URL - Sign-up opt-in box; existing accounts kept opted in and asked once - Admin -> Data requests with 30-day due dates and a daily reminder job - Deleting an account is a request; anonymising now covers attendees, form answers, consent details, payer details in provider payloads, etc. - Unit tests (privacy.test.js) and e2e (privacy.spec.ts); docs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012mt6TCv85T9VKRq3cV7rXq
103 lines
3.8 KiB
SQL
103 lines
3.8 KiB
SQL
-- CreateEnum
|
|
CREATE TYPE "DataRequestType" AS ENUM ('access', 'correction', 'deletion');
|
|
|
|
-- CreateEnum
|
|
CREATE TYPE "DataRequestStatus" AS ENUM ('open', 'completed', 'rejected', 'cancelled');
|
|
|
|
-- 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 'data_request_completed';
|
|
ALTER TYPE "AdminAuditAction" ADD VALUE 'data_request_rejected';
|
|
ALTER TYPE "AdminAuditAction" ADD VALUE 'user_anonymised';
|
|
|
|
-- AlterTable
|
|
ALTER TABLE "User" ADD COLUMN "newsAskedAt" TIMESTAMP(3),
|
|
ADD COLUMN "newsEmail" BOOLEAN NOT NULL DEFAULT false,
|
|
ADD COLUMN "newsWhatsApp" BOOLEAN NOT NULL DEFAULT false;
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "CommunicationConsent" (
|
|
"id" TEXT NOT NULL,
|
|
"userId" TEXT,
|
|
"address" TEXT,
|
|
"channel" TEXT NOT NULL,
|
|
"granted" BOOLEAN NOT NULL,
|
|
"source" TEXT NOT NULL,
|
|
"ip" TEXT,
|
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
CONSTRAINT "CommunicationConsent_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "MessagingOptOut" (
|
|
"id" TEXT NOT NULL,
|
|
"channel" TEXT NOT NULL,
|
|
"address" TEXT NOT NULL,
|
|
"source" TEXT NOT NULL,
|
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
CONSTRAINT "MessagingOptOut_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "DataRequest" (
|
|
"id" TEXT NOT NULL,
|
|
"type" "DataRequestType" NOT NULL,
|
|
"status" "DataRequestStatus" NOT NULL DEFAULT 'open',
|
|
"userId" TEXT,
|
|
"name" TEXT NOT NULL,
|
|
"email" TEXT NOT NULL,
|
|
"details" TEXT,
|
|
"response" TEXT,
|
|
"selfService" BOOLEAN NOT NULL DEFAULT false,
|
|
"dueAt" TIMESTAMP(3) NOT NULL,
|
|
"closedAt" TIMESTAMP(3),
|
|
"handledById" TEXT,
|
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
|
|
CONSTRAINT "DataRequest_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "CommunicationConsent_userId_createdAt_idx" ON "CommunicationConsent"("userId", "createdAt");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "CommunicationConsent_address_idx" ON "CommunicationConsent"("address");
|
|
|
|
-- CreateIndex
|
|
CREATE UNIQUE INDEX "MessagingOptOut_channel_address_key" ON "MessagingOptOut"("channel", "address");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "DataRequest_status_dueAt_idx" ON "DataRequest"("status", "dueAt");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "DataRequest_userId_idx" ON "DataRequest"("userId");
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "CommunicationConsent" ADD CONSTRAINT "CommunicationConsent_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "DataRequest" ADD CONSTRAINT "DataRequest_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "DataRequest" ADD CONSTRAINT "DataRequest_handledById_fkey" FOREIGN KEY ("handledById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
|
|
-- Existing accounts: kept opted in to news (WhatsApp only when they have a phone number),
|
|
-- logged as "existing_account", and asked once at their next login (newsAskedAt stays null).
|
|
UPDATE "User" SET "newsEmail" = true, "newsWhatsApp" = ("phoneNumber" IS NOT NULL)
|
|
WHERE "isActive" = true AND "email" NOT LIKE '%@deleted.invalid' AND "email" NOT LIKE '%@deleted.local' AND "email" NOT LIKE '%@guest.local';
|
|
|
|
INSERT INTO "CommunicationConsent" ("id", "userId", "channel", "granted", "source")
|
|
SELECT gen_random_uuid()::text, "id", 'email', true, 'existing_account' FROM "User" WHERE "newsEmail" = true;
|
|
|
|
INSERT INTO "CommunicationConsent" ("id", "userId", "channel", "granted", "source")
|
|
SELECT gen_random_uuid()::text, "id", 'whatsapp', true, 'existing_account' FROM "User" WHERE "newsWhatsApp" = true;
|