feat(admin): consentement aux annonces et désabonnement sans connexion
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 5m58s
CI / Deploy production (on server) (push) Successful in 26s

Les comptes déjà créés ne sont pas inscrits. Le choix est facultatif à l’inscription et dans les paramètres. Un lien public demande confirmation avant d’arrêter les actualités, sans toucher aux messages de compte.
This commit is contained in:
Antigravity
2026-09-06 09:23:10 +00:00
parent 6fa12f6e13
commit 360e58e473
33 changed files with 1036 additions and 9 deletions

View File

@@ -0,0 +1,61 @@
-- Additive marketing consent and suppression. No existing user is opted in.
CREATE TABLE "MarketingPreference" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"emailNormalized" TEXT NOT NULL,
"optedIn" BOOLEAN NOT NULL DEFAULT false,
"preferredLanguage" TEXT,
"consentVersion" TEXT,
"consentText" TEXT,
"source" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "MarketingPreference_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "MarketingPreference_userId_key" ON "MarketingPreference"("userId");
CREATE INDEX "MarketingPreference_emailNormalized_idx" ON "MarketingPreference"("emailNormalized");
CREATE INDEX "MarketingPreference_optedIn_idx" ON "MarketingPreference"("optedIn");
ALTER TABLE "MarketingPreference" ADD CONSTRAINT "MarketingPreference_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
CREATE TABLE "MarketingPreferenceEvent" (
"id" TEXT NOT NULL,
"preferenceId" TEXT NOT NULL,
"userId" TEXT,
"emailNormalized" TEXT NOT NULL,
"optedIn" BOOLEAN NOT NULL,
"source" TEXT NOT NULL,
"consentVersion" TEXT,
"consentText" TEXT,
"ip" TEXT,
"userAgent" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "MarketingPreferenceEvent_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "MarketingPreferenceEvent_preferenceId_idx" ON "MarketingPreferenceEvent"("preferenceId");
CREATE INDEX "MarketingPreferenceEvent_userId_createdAt_idx" ON "MarketingPreferenceEvent"("userId", "createdAt");
CREATE INDEX "MarketingPreferenceEvent_emailNormalized_createdAt_idx" ON "MarketingPreferenceEvent"("emailNormalized", "createdAt");
ALTER TABLE "MarketingPreferenceEvent" ADD CONSTRAINT "MarketingPreferenceEvent_preferenceId_fkey" FOREIGN KEY ("preferenceId") REFERENCES "MarketingPreference"("id") ON DELETE CASCADE ON UPDATE CASCADE;
CREATE TABLE "EmailSuppression" (
"id" TEXT NOT NULL,
"emailNormalized" TEXT NOT NULL,
"reason" TEXT NOT NULL,
"source" TEXT NOT NULL,
"userId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "EmailSuppression_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "EmailSuppression_emailNormalized_key" ON "EmailSuppression"("emailNormalized");
CREATE INDEX "EmailSuppression_reason_idx" ON "EmailSuppression"("reason");
CREATE INDEX "EmailSuppression_userId_idx" ON "EmailSuppression"("userId");
ALTER TABLE "EmailSuppression" ADD CONSTRAINT "EmailSuppression_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -54,6 +54,8 @@ model User {
usageLogs UsageLog[]
apiKeys UserAPIKey[]
aiConsentLogs AiConsentLog[]
marketingPreference MarketingPreference?
emailSuppressions EmailSuppression[]
noteClusters NoteCluster[]
bridgeNotes BridgeNote[]
bridgeSuggestions BridgeSuggestion[]
@@ -1124,3 +1126,55 @@ model ErrorLog {
@@index([createdAt])
@@index([resolved])
}
/// Explicit marketing announcements opt-in. Missing row = not opted in.
model MarketingPreference {
id String @id @default(cuid())
userId String @unique
emailNormalized String
optedIn Boolean @default(false)
preferredLanguage String?
consentVersion String?
consentText String? @db.Text
source String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
events MarketingPreferenceEvent[]
@@index([emailNormalized])
@@index([optedIn])
}
model MarketingPreferenceEvent {
id String @id @default(cuid())
preferenceId String
userId String?
emailNormalized String
optedIn Boolean
source String
consentVersion String?
consentText String? @db.Text
ip String?
userAgent String?
createdAt DateTime @default(now())
preference MarketingPreference @relation(fields: [preferenceId], references: [id], onDelete: Cascade)
@@index([preferenceId])
@@index([userId, createdAt])
@@index([emailNormalized, createdAt])
}
/// Blocks campaign mail. Kept after account deletion (userId set null).
model EmailSuppression {
id String @id @default(cuid())
emailNormalized String @unique
reason String
source String
userId String?
createdAt DateTime @default(now())
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
@@index([reason])
@@index([userId])
}