Email queue + async sending; legal settings and placeholders
- 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>
This commit is contained in:
80
backend/src/lib/legal-placeholders.ts
Normal file
80
backend/src/lib/legal-placeholders.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
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] ?? '';
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user