fix(settings): support Persian and flexible SMS variable names and improve save persistence
This commit is contained in:
@@ -103,9 +103,9 @@ const saveSettings = async (body = {}) => {
|
||||
? Object.fromEntries(incoming.map((item) => [item.key, item]))
|
||||
: 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 nextMap = emptyTemplateMap();
|
||||
const nextMap = {};
|
||||
|
||||
for (const def of SMS_TEMPLATE_DEFS) {
|
||||
const storedEntry = normalizeStoredEntry(storedMap[def.key], def);
|
||||
@@ -119,7 +119,7 @@ const saveSettings = async (body = {}) => {
|
||||
if (incomingEntry.templateId !== undefined) {
|
||||
const sanitized = sanitizeTemplateId(incomingEntry.templateId);
|
||||
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;
|
||||
}
|
||||
@@ -138,16 +138,19 @@ const saveSettings = async (body = {}) => {
|
||||
const slot = String(item.slot || '').trim();
|
||||
if (!slot) continue;
|
||||
const rawName = String(item.name || '').trim();
|
||||
if (!rawName) continue;
|
||||
const sanitized = sanitizeVariableName(rawName);
|
||||
if (sanitized === null) {
|
||||
throw new AppError(
|
||||
'VALIDATION_FAILED',
|
||||
{ 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;
|
||||
}
|
||||
@@ -165,7 +168,7 @@ const saveSettings = async (body = {}) => {
|
||||
{ key: SETTINGS_KEY },
|
||||
{ $set: { smsTemplates: nextMap } },
|
||||
{ upsert: true, new: true, setDefaultsOnInsert: true }
|
||||
);
|
||||
).lean();
|
||||
|
||||
return {
|
||||
smsTemplates: toPublicTemplates(readStoredMap(doc))
|
||||
|
||||
@@ -60,15 +60,17 @@ const sanitizeTemplateId = (value) => {
|
||||
if (value == null) return '';
|
||||
const digits = String(value).trim();
|
||||
if (!digits) return '';
|
||||
if (!/^\d{1,20}$/.test(digits)) return null;
|
||||
if (!/^\d{1,30}$/.test(digits)) return null;
|
||||
return digits;
|
||||
};
|
||||
|
||||
const sanitizeVariableName = (value) => {
|
||||
if (value == null) return '';
|
||||
const name = String(value).trim().replace(/^#+|#+$/g, '');
|
||||
const name = String(value).trim().replace(/^#+|#+$/g, '').trim();
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -144,7 +146,9 @@ const resolveVariablesList = (def, storedVariables) => {
|
||||
const slot = String(item.slot || '').trim();
|
||||
if (!slot) return null;
|
||||
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);
|
||||
return {
|
||||
slot,
|
||||
|
||||
Reference in New Issue
Block a user