diff --git a/apxtri/models/Notifications.js b/apxtri/models/Notifications.js index 2f67c95..3b5f523 100644 --- a/apxtri/models/Notifications.js +++ b/apxtri/models/Notifications.js @@ -212,6 +212,56 @@ Notifications.sendsms = async (data, tribeId) => { } */ }; + +/** + * Preprod gate shared by every smatchit notification. + * + * Notifications.filternoitif(tribe, tpl, alias, channel, topics) -> true = send, false = simulate + * + * Reads tribe/objects/tribes/itm/{tribe}.json .notify on every call (a require() + * would be cached and the flag could not be toggled without a pm2 restart): + * { "test": true, "aliasprefix": "smatchit" } + * + * A notification is sent when, in order: + * 1. the tribe has no notify block, or notify.test !== true -> production, never filtered + * 2. tpl.nofilterpreprod === true -> transactional (identification, key recovery) + * 3. alias starts with notify.aliasprefix (defaults to tribeId) + * and every topic it would be published on also starts with the prefix. + * + * Missing alias is refused, not sent: an authenticated caller that forgets to + * propagate the alias must not be able to escape the preprod gate. + * Tribes without a notify block (okwo, ndda) are unaffected. + */ +Notifications.filternoitif = (tribe, tpl, alias, channel = "push", topics = []) => { + const file = path.resolve(__dirname, `../../objects/tribes/itm/${tribe}.json`); + let confnotif = null; + try { + confnotif = fs.existsSync(file) ? fs.readJSONSync(file).notify : null; + } catch (err) { + // unreadable conf must not silently disable the gate + console.log(currentmod, "filternoitif confreadfail:", err.message); + return false; + } + if (!confnotif || confnotif.test !== true) return true; + if (tpl && tpl.nofilterpreprod === true) return true; + const prefix = confnotif.aliasprefix || tribe; + if (typeof alias !== "string" || alias === "") { + if (log) + console.log( + currentmod, + `filternoitif REFUSED ${channel} noalias`, + { tribe, template: tpl && tpl.tplname } + ); + return false; + } + const ok = + alias.startsWith(prefix) && + topics.every((t) => typeof t === "string" && t.startsWith(prefix)); + if (!ok && log) + console.log(currentmod, `filternoitif REFUSED ${channel}`, { tribe, alias, topics }); + return ok; +}; + Notifications.manageemail = async ( data, templatename, @@ -263,7 +313,7 @@ Notifications.manageemail = async ( data: { template: tplpath }, }; } - const results = { fail: [], success: [] }; + const results = { fail: [], success: [], simulated: [] }; const emailtosend = {}; // init with common propertie Object.keys(data).forEach((k) => { @@ -324,6 +374,16 @@ Notifications.manageemail = async ( if (email.attachments) { email.attachments.forEach((a) => tplemail.attachments.push(a)); } + // call site 1 of 2 for the preprod gate. The alias is carried by each + // emailsto entry (Jobads.js already builds it) but was never propagated + // into emailtosend, which is what sendmail receives. Assigned (not ||-ed) + // because emailtosend is reused across iterations: an entry without an + // alias must not inherit the previous recipient's. + emailtosend.alias = email.alias; + if (!Notifications.filternoitif(tribe, tplemail, emailtosend.alias, "email")) { + results.simulated.push({ to: emailtosend.to, alias: emailtosend.alias }); + continue; + } //console.log(emailtosend) sendit = await Notifications.sendmail(emailtosend, tribe); //const sendit={status:200} @@ -351,7 +411,9 @@ Notifications.manageemail = async ( data: { emailsucces: results.success.length, emailfail: results.fail.length, + emailsimulated: results.simulated.length, failsdetail: results.fail, + simulateddetail: results.simulated, timems: Date.now() - start, }, };