- Add in-memory email queue with rate limiting (MAX_EMAILS_PER_HOUR) - Bulk send to event attendees now queues and returns immediately - Frontend shows 'Emails are being sent in the background' - Legal pages, settings, and placeholders updates Co-authored-by: Cursor <cursoragent@cursor.com>
81 lines
2.4 KiB
TypeScript
81 lines
2.4 KiB
TypeScript
import { getLegalSettingsValues } from '../routes/legal-settings.js';
|
|
|
|
/**
|
|
* Strict whitelist of supported placeholders.
|
|
* Only these placeholders will be replaced in legal page content.
|
|
* Unknown placeholders remain unchanged.
|
|
*/
|
|
const SUPPORTED_PLACEHOLDERS = new Set([
|
|
'COMPANY_NAME',
|
|
'LEGAL_ENTITY_NAME',
|
|
'RUC_NUMBER',
|
|
'COMPANY_ADDRESS',
|
|
'COMPANY_CITY',
|
|
'COMPANY_COUNTRY',
|
|
'SUPPORT_EMAIL',
|
|
'LEGAL_EMAIL',
|
|
'GOVERNING_LAW',
|
|
'JURISDICTION_CITY',
|
|
'CURRENT_YEAR',
|
|
'LAST_UPDATED_DATE',
|
|
]);
|
|
|
|
/**
|
|
* Replace legal placeholders in content using strict whitelist mapping.
|
|
*
|
|
* Rules:
|
|
* - Only supported placeholders are replaced
|
|
* - Unknown placeholders remain unchanged
|
|
* - Missing values are replaced with empty string
|
|
* - No code execution or dynamic evaluation
|
|
* - Replacement is pure string substitution
|
|
*
|
|
* @param content - The markdown/text content containing {{PLACEHOLDER}} tokens
|
|
* @param updatedAt - The page's updated_at timestamp (for LAST_UPDATED_DATE)
|
|
* @returns Content with placeholders replaced
|
|
*/
|
|
export async function replaceLegalPlaceholders(
|
|
content: string,
|
|
updatedAt?: string
|
|
): Promise<string> {
|
|
if (!content) return content;
|
|
|
|
// Fetch legal settings values from DB
|
|
const settingsValues = await getLegalSettingsValues();
|
|
|
|
// Build the full replacement map
|
|
const replacements: Record<string, string> = { ...settingsValues };
|
|
|
|
// Dynamic values
|
|
replacements['CURRENT_YEAR'] = new Date().getFullYear().toString();
|
|
|
|
if (updatedAt) {
|
|
try {
|
|
const date = new Date(updatedAt);
|
|
if (!isNaN(date.getTime())) {
|
|
replacements['LAST_UPDATED_DATE'] = date.toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
});
|
|
} else {
|
|
replacements['LAST_UPDATED_DATE'] = updatedAt;
|
|
}
|
|
} catch {
|
|
replacements['LAST_UPDATED_DATE'] = updatedAt;
|
|
}
|
|
}
|
|
|
|
// Replace only whitelisted placeholders using a single regex pass
|
|
// Matches {{PLACEHOLDER_NAME}} where PLACEHOLDER_NAME is uppercase letters and underscores
|
|
return content.replace(/\{\{([A-Z_]+)\}\}/g, (match, placeholderName) => {
|
|
// Only replace if the placeholder is in the whitelist
|
|
if (!SUPPORTED_PLACEHOLDERS.has(placeholderName)) {
|
|
return match; // Unknown placeholder - leave unchanged
|
|
}
|
|
|
|
// Return the value or empty string if missing
|
|
return replacements[placeholderName] ?? '';
|
|
});
|
|
}
|