email marketing
This commit is contained in:
72
src/lib/marketingEmail.ts
Normal file
72
src/lib/marketingEmail.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import 'server-only';
|
||||
|
||||
import crypto from 'crypto';
|
||||
|
||||
const TOKEN_TTL_MS = 1000 * 60 * 60 * 24 * 365;
|
||||
|
||||
function getSigningSecret(): string {
|
||||
const secret = process.env.EMAIL_UNSUBSCRIBE_SECRET || process.env.NEXTAUTH_SECRET;
|
||||
|
||||
if (!secret) {
|
||||
throw new Error('Set EMAIL_UNSUBSCRIBE_SECRET before sending marketing email.');
|
||||
}
|
||||
|
||||
return secret;
|
||||
}
|
||||
|
||||
function sign(payload: string): string {
|
||||
return crypto.createHmac('sha256', getSigningSecret()).update(payload).digest('base64url');
|
||||
}
|
||||
|
||||
function normalizeEmail(email: string): string {
|
||||
return email.trim().toLowerCase();
|
||||
}
|
||||
|
||||
export function createMarketingUnsubscribeUrl(email: string): string {
|
||||
const payload = Buffer.from(
|
||||
JSON.stringify({ email: normalizeEmail(email), expiresAt: Date.now() + TOKEN_TTL_MS })
|
||||
).toString('base64url');
|
||||
const token = `${payload}.${sign(payload)}`;
|
||||
const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://www.qrmaster.net';
|
||||
|
||||
return `${appUrl}/unsubscribe?token=${encodeURIComponent(token)}`;
|
||||
}
|
||||
|
||||
export function getUnsubscribeEmail(token: string | null | undefined): string | null {
|
||||
if (!token) return null;
|
||||
|
||||
const separator = token.lastIndexOf('.');
|
||||
if (separator <= 0 || separator === token.length - 1) return null;
|
||||
|
||||
const payload = token.slice(0, separator);
|
||||
const providedSignature = token.slice(separator + 1);
|
||||
const expectedSignature = sign(payload);
|
||||
const providedBuffer = Buffer.from(providedSignature);
|
||||
const expectedBuffer = Buffer.from(expectedSignature);
|
||||
|
||||
if (
|
||||
providedBuffer.length !== expectedBuffer.length ||
|
||||
!crypto.timingSafeEqual(providedBuffer, expectedBuffer)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')) as {
|
||||
email?: unknown;
|
||||
expiresAt?: unknown;
|
||||
};
|
||||
|
||||
if (
|
||||
typeof decoded.email !== 'string' ||
|
||||
typeof decoded.expiresAt !== 'number' ||
|
||||
decoded.expiresAt < Date.now()
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return normalizeEmail(decoded.email);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user