Kuma/src/modules/marketing/lib/unsubscribe-token.ts
2026-10-02 17:31:08 +02:00

49 lines
1.8 KiB
TypeScript

import { createHmac, timingSafeEqual } from "crypto";
/**
* Signierter Abmeldelink: Wer eine Werbe-Mail bekommt, soll sich ohne Anmeldung
* abmelden können. Das Token enthält Kunde und Studio und ist mit
* MARKETING_TOKEN_SECRET signiert, damit niemand fremde Einwilligungen
* widerrufen kann. Es läuft bewusst nicht ab - ein Abmeldelink aus einer alten
* Mail muss weiter funktionieren.
*/
function secret(): string {
const value = process.env.MARKETING_TOKEN_SECRET;
if (!value) throw new Error("MARKETING_TOKEN_SECRET ist nicht gesetzt");
return value;
}
function sign(payload: string): string {
return createHmac("sha256", secret()).update(payload).digest("base64url");
}
export function createUnsubscribeToken(userId: string, businessId: string): string {
const payload = `${userId}.${businessId}`;
return `${payload}.${sign(payload)}`;
}
export function verifyUnsubscribeToken(
token: string | null | undefined
): { userId: string; businessId: string } | null {
if (!token) return null;
const parts = token.split(".");
if (parts.length !== 3) return null;
const [userId, businessId, signature] = parts;
if (!userId || !businessId || !signature) return null;
const expected = Buffer.from(sign(`${userId}.${businessId}`));
const actual = Buffer.from(signature);
if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) return null;
return { userId, businessId };
}
export function unsubscribePageUrl(baseUrl: string, token: string): string {
return `${baseUrl}/abmelden?t=${encodeURIComponent(token)}`;
}
/** Ziel für den List-Unsubscribe-Header (Ein-Klick-Abmeldung im Mailprogramm). */
export function unsubscribeApiUrl(baseUrl: string, token: string): string {
return `${baseUrl}/api/marketing/unsubscribe?t=${encodeURIComponent(token)}`;
}