fix(settings): support Persian and flexible SMS variable names and improve save persistence

This commit is contained in:
2026-08-15 22:03:22 +03:30
parent d2d4ee719b
commit d3190a8729
2 changed files with 18 additions and 11 deletions
+10 -7
View File
@@ -103,9 +103,9 @@ const saveSettings = async (body = {}) => {
? Object.fromEntries(incoming.map((item) => [item.key, item])) ? Object.fromEntries(incoming.map((item) => [item.key, item]))
: incoming; : incoming;
const existing = await Setting.findOne({ key: SETTINGS_KEY }); const existing = await Setting.findOne({ key: SETTINGS_KEY }).lean();
const storedMap = existing ? readStoredMap(existing) : emptyTemplateMap(); const storedMap = existing ? readStoredMap(existing) : emptyTemplateMap();
const nextMap = emptyTemplateMap(); const nextMap = {};
for (const def of SMS_TEMPLATE_DEFS) { for (const def of SMS_TEMPLATE_DEFS) {
const storedEntry = normalizeStoredEntry(storedMap[def.key], def); const storedEntry = normalizeStoredEntry(storedMap[def.key], def);
@@ -119,7 +119,7 @@ const saveSettings = async (body = {}) => {
if (incomingEntry.templateId !== undefined) { if (incomingEntry.templateId !== undefined) {
const sanitized = sanitizeTemplateId(incomingEntry.templateId); const sanitized = sanitizeTemplateId(incomingEntry.templateId);
if (sanitized === null) { if (sanitized === null) {
throw new AppError('VALIDATION_FAILED', { field: def.key }, `Invalid SMS template ID for ${def.key}`); throw new AppError('VALIDATION_FAILED', { field: def.key }, `شناسه قالب پیامک برای ${def.label} نامعتبر است`);
} }
templateId = sanitized; templateId = sanitized;
} }
@@ -138,16 +138,19 @@ const saveSettings = async (body = {}) => {
const slot = String(item.slot || '').trim(); const slot = String(item.slot || '').trim();
if (!slot) continue; if (!slot) continue;
const rawName = String(item.name || '').trim(); const rawName = String(item.name || '').trim();
if (!rawName) continue;
const sanitized = sanitizeVariableName(rawName); const sanitized = sanitizeVariableName(rawName);
if (sanitized === null) { if (sanitized === null) {
throw new AppError( throw new AppError(
'VALIDATION_FAILED', 'VALIDATION_FAILED',
{ field: `${def.key}.${slot}` }, { field: `${def.key}.${slot}` },
`Invalid SMS variable name "${rawName}" for ${def.key}` `نام متغیر «${rawName}» برای ${def.label} نامعتبر است`
); );
} }
validVars.push({ slot, name: sanitized }); const slotDef = (def?.slots || []).find((s) => s.key === slot);
validVars.push({
slot,
name: sanitized || slotDef?.defaultName || slot
});
} }
variables = validVars; variables = validVars;
} }
@@ -165,7 +168,7 @@ const saveSettings = async (body = {}) => {
{ key: SETTINGS_KEY }, { key: SETTINGS_KEY },
{ $set: { smsTemplates: nextMap } }, { $set: { smsTemplates: nextMap } },
{ upsert: true, new: true, setDefaultsOnInsert: true } { upsert: true, new: true, setDefaultsOnInsert: true }
); ).lean();
return { return {
smsTemplates: toPublicTemplates(readStoredMap(doc)) smsTemplates: toPublicTemplates(readStoredMap(doc))
+8 -4
View File
@@ -60,15 +60,17 @@ const sanitizeTemplateId = (value) => {
if (value == null) return ''; if (value == null) return '';
const digits = String(value).trim(); const digits = String(value).trim();
if (!digits) return ''; if (!digits) return '';
if (!/^\d{1,20}$/.test(digits)) return null; if (!/^\d{1,30}$/.test(digits)) return null;
return digits; return digits;
}; };
const sanitizeVariableName = (value) => { const sanitizeVariableName = (value) => {
if (value == null) return ''; if (value == null) return '';
const name = String(value).trim().replace(/^#+|#+$/g, ''); const name = String(value).trim().replace(/^#+|#+$/g, '').trim();
if (!name) return ''; if (!name) return '';
if (!/^[A-Za-z][A-Za-z0-9_]{0,49}$/.test(name)) return null; // Check for dangerous injection or control chars
if (/[\r\n\t\0<>"'`]/.test(name)) return null;
if (name.length > 100) return null;
return name; return name;
}; };
@@ -144,7 +146,9 @@ const resolveVariablesList = (def, storedVariables) => {
const slot = String(item.slot || '').trim(); const slot = String(item.slot || '').trim();
if (!slot) return null; if (!slot) return null;
const slotDef = (def?.slots || []).find((s) => s.key === slot); const slotDef = (def?.slots || []).find((s) => s.key === slot);
const rawName = item.name !== undefined ? item.name : slotDef?.defaultName || ''; const rawName = item.name !== undefined && String(item.name).trim() !== ''
? String(item.name).trim()
: slotDef?.defaultName || '';
const sanitized = sanitizeVariableName(rawName); const sanitized = sanitizeVariableName(rawName);
return { return {
slot, slot,