@xuda.io/account_module 1.2.2275 → 1.2.2276
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.mjs +219 -1
- package/index_ms.mjs +8 -0
- package/index_msa.mjs +8 -0
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -2583,6 +2583,36 @@ export const onboarding_completed = async function (req, job_id, headers) {
|
|
|
2583
2583
|
}
|
|
2584
2584
|
};
|
|
2585
2585
|
|
|
2586
|
+
// One-time admin heads-up to info@xuda.ai on every new signup. Fired from
|
|
2587
|
+
// maybe_send_welcome_email (the universal once-per-account choke point) so it
|
|
2588
|
+
// covers every signup surface. Prod only (gated by the caller) and idempotent
|
|
2589
|
+
// (admin_signup_notified_ts). Fire-and-forget: never blocks or fails the caller.
|
|
2590
|
+
const notify_admin_new_signup = function (account_doc) {
|
|
2591
|
+
try {
|
|
2592
|
+
const info = account_doc.account_info || {};
|
|
2593
|
+
const name = [info.first_name, info.last_name].filter(Boolean).join(' ') || info.username || 'Unknown';
|
|
2594
|
+
const email = info.email || 'unknown';
|
|
2595
|
+
const body = `
|
|
2596
|
+
<h2 style="margin:0 0 14px">New signup on Xuda</h2>
|
|
2597
|
+
<table cellpadding="6" style="border-collapse:collapse;font-size:14px">
|
|
2598
|
+
<tr><td><strong>Name</strong></td><td>${_.escape(name)}</td></tr>
|
|
2599
|
+
<tr><td><strong>Email</strong></td><td>${_.escape(email)}</td></tr>
|
|
2600
|
+
<tr><td><strong>Username</strong></td><td>${_.escape(info.username || '')}</td></tr>
|
|
2601
|
+
<tr><td><strong>Account ID</strong></td><td>${_.escape(account_doc._id || '')}</td></tr>
|
|
2602
|
+
<tr><td><strong>Signed up (UTC)</strong></td><td>${new Date().toISOString()}</td></tr>
|
|
2603
|
+
</table>`;
|
|
2604
|
+
email_msa.send_email({
|
|
2605
|
+
email: 'info@xuda.ai',
|
|
2606
|
+
subject: `New signup: ${name} (${email})`,
|
|
2607
|
+
body,
|
|
2608
|
+
ref: account_doc._id,
|
|
2609
|
+
app_id: null,
|
|
2610
|
+
});
|
|
2611
|
+
} catch (err) {
|
|
2612
|
+
console.error('[notify_admin_new_signup]', err.message);
|
|
2613
|
+
}
|
|
2614
|
+
};
|
|
2615
|
+
|
|
2586
2616
|
// Sends the welcome_aboard email exactly once per account. Safe to call from
|
|
2587
2617
|
// multiple triggers (avatar-completion, onboarding_completed) and across every
|
|
2588
2618
|
// signup surface (xuda.ai, Google OAuth, xuda.fashion, xuda.network, chat
|
|
@@ -2591,11 +2621,26 @@ export const maybe_send_welcome_email = async function (uid) {
|
|
|
2591
2621
|
try {
|
|
2592
2622
|
const account_doc = await db_module.get_couch_doc_native('xuda_accounts', uid);
|
|
2593
2623
|
if (!account_doc || !account_doc.account_info) return { code: -1, data: 'no account' };
|
|
2624
|
+
|
|
2625
|
+
// Admin new-signup notification (prod only), independent of the user-facing
|
|
2626
|
+
// welcome email so it also covers suppressed ambassadors/mentors. Idempotent
|
|
2627
|
+
// via admin_signup_notified_ts; the flag persists on whichever save below runs.
|
|
2628
|
+
let admin_flag_needs_save = false;
|
|
2629
|
+
if (!_conf.is_debug && !account_doc.admin_signup_notified_ts) {
|
|
2630
|
+
notify_admin_new_signup(account_doc);
|
|
2631
|
+
account_doc.admin_signup_notified_ts = Date.now();
|
|
2632
|
+
admin_flag_needs_save = true;
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2594
2635
|
if (account_doc.account_info.is_xuda_network_ambassador === true) {
|
|
2595
2636
|
console.log('[maybe_send_welcome_email] suppressing for xuda.network ambassador/mentor', uid);
|
|
2637
|
+
if (admin_flag_needs_save) await db_module.save_couch_doc('xuda_accounts', account_doc);
|
|
2596
2638
|
return { code: 0, data: 'ambassador' };
|
|
2597
2639
|
}
|
|
2598
|
-
if (account_doc.welcome_email_sent_ts)
|
|
2640
|
+
if (account_doc.welcome_email_sent_ts) {
|
|
2641
|
+
if (admin_flag_needs_save) await db_module.save_couch_doc('xuda_accounts', account_doc);
|
|
2642
|
+
return { code: 0, data: 'already sent' };
|
|
2643
|
+
}
|
|
2599
2644
|
|
|
2600
2645
|
const host = _conf.is_debug ? process.env.XUDA_HOSTNAME || _conf.domain : _conf.domain;
|
|
2601
2646
|
const username = account_doc.account_info.username || uid;
|
|
@@ -2620,6 +2665,179 @@ export const maybe_send_welcome_email = async function (uid) {
|
|
|
2620
2665
|
}
|
|
2621
2666
|
};
|
|
2622
2667
|
|
|
2668
|
+
// ---- Email bounce lifecycle helpers -----------------------------------------
|
|
2669
|
+
const _bounce_settings_url = () => {
|
|
2670
|
+
const host = _conf.is_debug ? process.env.XUDA_HOSTNAME || _conf.domain : _conf.domain;
|
|
2671
|
+
return `https://${host}/dashboard/settings`;
|
|
2672
|
+
};
|
|
2673
|
+
|
|
2674
|
+
// Alert the user to update their email (banner + SMS per the topic's delivery
|
|
2675
|
+
// methods). SMS is gated/log-only in notification_module until sms_enabled.
|
|
2676
|
+
const _send_bounce_alert = async (account, attempt, max) => {
|
|
2677
|
+
const eb = account.account_email_bounce || {};
|
|
2678
|
+
return await notification_msa.submit_notification({
|
|
2679
|
+
type: 'account',
|
|
2680
|
+
app_id: null,
|
|
2681
|
+
uid_arr: [account._id],
|
|
2682
|
+
topic: 'email_bounce_update_required',
|
|
2683
|
+
params: {
|
|
2684
|
+
recipient_email: eb.recipient || account.account_info?.email || '',
|
|
2685
|
+
settings_url: _bounce_settings_url(),
|
|
2686
|
+
attempt,
|
|
2687
|
+
max,
|
|
2688
|
+
},
|
|
2689
|
+
});
|
|
2690
|
+
};
|
|
2691
|
+
|
|
2692
|
+
// Hard-email-bounce flag. Called (via account_ms RPC) by email_module's
|
|
2693
|
+
// scan_platform_bounces when a platform email to this account's address bounces
|
|
2694
|
+
// permanently (5.x.x). Records the flag and, on the FIRST transition into
|
|
2695
|
+
// flagged, fires alert #1 immediately (attempts 2..max + suspend are driven by
|
|
2696
|
+
// the daily sweep_email_bounces cron). Idempotent: repeated bounces for an
|
|
2697
|
+
// already-flagged/suspended account only refresh last_bounce_ts (no re-alert).
|
|
2698
|
+
export const flag_email_bounce = async function (req) {
|
|
2699
|
+
const { uid, recipient, reason, status_code, message_id, source } = req || {};
|
|
2700
|
+
if (!uid) return { code: -1, data: 'uid required' };
|
|
2701
|
+
try {
|
|
2702
|
+
const acct_ret = await db_module.get_couch_doc('xuda_accounts', uid);
|
|
2703
|
+
if (acct_ret.code < 0) return { code: -1, data: 'account not found' };
|
|
2704
|
+
const account = acct_ret.data;
|
|
2705
|
+
const now = Date.now();
|
|
2706
|
+
const eb = account.account_email_bounce || {};
|
|
2707
|
+
const was_active = eb.status === 'flagged' || eb.status === 'suspended';
|
|
2708
|
+
const max = Number(_conf.bounce_monitor?.suspend_after_alerts || 3);
|
|
2709
|
+
|
|
2710
|
+
eb.recipient = recipient || eb.recipient;
|
|
2711
|
+
eb.reason = reason || eb.reason;
|
|
2712
|
+
eb.status_code = status_code || eb.status_code;
|
|
2713
|
+
eb.last_message_id = message_id || eb.last_message_id;
|
|
2714
|
+
eb.source = source || eb.source || 'dsn';
|
|
2715
|
+
eb.last_bounce_ts = now;
|
|
2716
|
+
eb.bounce_count = (eb.bounce_count || 0) + 1;
|
|
2717
|
+
if (eb.status !== 'suspended') {
|
|
2718
|
+
eb.status = 'flagged';
|
|
2719
|
+
eb.first_bounce_ts = eb.first_bounce_ts || now;
|
|
2720
|
+
if (!was_active) {
|
|
2721
|
+
// Fresh flag -> immediate first alert.
|
|
2722
|
+
eb.attempt_count = 1;
|
|
2723
|
+
eb.first_alert_ts = now;
|
|
2724
|
+
eb.last_attempt_ts = now;
|
|
2725
|
+
} else if (eb.attempt_count == null) {
|
|
2726
|
+
eb.attempt_count = 0;
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
account.account_email_bounce = eb;
|
|
2731
|
+
const save_ret = await db_module.save_couch_doc('xuda_accounts', account);
|
|
2732
|
+
if (save_ret.code < 0) return save_ret;
|
|
2733
|
+
|
|
2734
|
+
if (!was_active) {
|
|
2735
|
+
try { await _send_bounce_alert(account, 1, max); } catch (e) { console.error('flag_email_bounce alert #1 failed:', e.message); }
|
|
2736
|
+
}
|
|
2737
|
+
console.log(`flag_email_bounce: ${uid} <${recipient}> ${status_code || ''} -> status=${eb.status} attempt=${eb.attempt_count} bounce_count=${eb.bounce_count}${!was_active ? ' (alert #1 sent)' : ''}`);
|
|
2738
|
+
return { code: 1, data: { uid, status: eb.status, attempt_count: eb.attempt_count, bounce_count: eb.bounce_count } };
|
|
2739
|
+
} catch (err) {
|
|
2740
|
+
console.error('flag_email_bounce error:', err);
|
|
2741
|
+
return { code: -10, data: err.message };
|
|
2742
|
+
}
|
|
2743
|
+
};
|
|
2744
|
+
|
|
2745
|
+
// Daily retry/escalation sweep for flagged accounts (controller cron). For each
|
|
2746
|
+
// account with account_email_bounce.status === 'flagged':
|
|
2747
|
+
// - if the user changed their email away from the bounced address -> CLEAR;
|
|
2748
|
+
// - else if under the alert cap and >= alert_interval since last -> next alert;
|
|
2749
|
+
// - else (cap reached) -> SUSPEND (account_email_bounce_suspended) + ops alert.
|
|
2750
|
+
// `req` overrides: { interval_ms, max, limit, uids }.
|
|
2751
|
+
export const sweep_email_bounces = async function (req = {}) {
|
|
2752
|
+
const bm = _conf.bounce_monitor || {};
|
|
2753
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
2754
|
+
const interval = Number(req.interval_ms ?? bm.alert_interval_ms ?? DAY_MS);
|
|
2755
|
+
const max = Number(req.max ?? bm.suspend_after_alerts ?? 3);
|
|
2756
|
+
const now = Date.now();
|
|
2757
|
+
const summary = { flagged: 0, cleared: 0, alerted: 0, suspended: 0, skipped: 0 };
|
|
2758
|
+
try {
|
|
2759
|
+
const selector = { 'account_email_bounce.status': 'flagged' };
|
|
2760
|
+
if (Array.isArray(req.uids) && req.uids.length) selector._id = { $in: req.uids };
|
|
2761
|
+
const find_ret = await db_module.find_couch_query('xuda_accounts', {
|
|
2762
|
+
selector,
|
|
2763
|
+
fields: ['_id'],
|
|
2764
|
+
limit: req.limit ?? 99999,
|
|
2765
|
+
});
|
|
2766
|
+
const rows = find_ret?.data?.docs || find_ret?.docs || [];
|
|
2767
|
+
summary.flagged = rows.length;
|
|
2768
|
+
|
|
2769
|
+
for (const row of rows) {
|
|
2770
|
+
const acct_ret = await db_module.get_couch_doc('xuda_accounts', row._id);
|
|
2771
|
+
if (acct_ret.code < 0) continue;
|
|
2772
|
+
const account = acct_ret.data;
|
|
2773
|
+
const eb = account.account_email_bounce;
|
|
2774
|
+
if (!eb || eb.status !== 'flagged') { summary.skipped++; continue; }
|
|
2775
|
+
|
|
2776
|
+
// CLEAR: the user updated their email away from the bounced address.
|
|
2777
|
+
const cur_email = (account.account_info?.email || '').toLowerCase().trim();
|
|
2778
|
+
if (cur_email && cur_email !== (eb.recipient || '').toLowerCase().trim()) {
|
|
2779
|
+
eb.status = 'cleared';
|
|
2780
|
+
eb.cleared_ts = now;
|
|
2781
|
+
account.account_email_bounce = eb;
|
|
2782
|
+
await db_module.save_couch_doc('xuda_accounts', account);
|
|
2783
|
+
summary.cleared++;
|
|
2784
|
+
continue;
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
// Cadence gate.
|
|
2788
|
+
if (eb.last_attempt_ts && now - eb.last_attempt_ts < interval) { summary.skipped++; continue; }
|
|
2789
|
+
|
|
2790
|
+
if ((eb.attempt_count || 0) < max) {
|
|
2791
|
+
const next = (eb.attempt_count || 0) + 1;
|
|
2792
|
+
eb.attempt_count = next;
|
|
2793
|
+
eb.last_attempt_ts = now;
|
|
2794
|
+
account.account_email_bounce = eb;
|
|
2795
|
+
await db_module.save_couch_doc('xuda_accounts', account);
|
|
2796
|
+
try { await _send_bounce_alert(account, next, max); } catch (e) { console.error('bounce alert failed:', e.message); }
|
|
2797
|
+
summary.alerted++;
|
|
2798
|
+
} else {
|
|
2799
|
+
// SUSPEND after the alert cap. Dedicated flag (enforced at
|
|
2800
|
+
// router_module.validate_app_active_status alongside billing
|
|
2801
|
+
// suspension) so we never clobber account_suspension_status.
|
|
2802
|
+
eb.status = 'suspended';
|
|
2803
|
+
eb.suspended_ts = now;
|
|
2804
|
+
account.account_email_bounce = eb;
|
|
2805
|
+
account.account_email_bounce_suspended = 1;
|
|
2806
|
+
await db_module.save_couch_doc('xuda_accounts', account);
|
|
2807
|
+
|
|
2808
|
+
const host = _conf.is_debug ? process.env.XUDA_HOSTNAME || _conf.domain : _conf.domain;
|
|
2809
|
+
const supers = _conf.superuser_account_ids || [];
|
|
2810
|
+
if (supers.length) {
|
|
2811
|
+
try {
|
|
2812
|
+
await notification_msa.submit_notification({
|
|
2813
|
+
type: 'account',
|
|
2814
|
+
app_id: null,
|
|
2815
|
+
uid_arr: supers,
|
|
2816
|
+
topic: 'email_bounce_suspended',
|
|
2817
|
+
system: true,
|
|
2818
|
+
params: {
|
|
2819
|
+
uid: account._id,
|
|
2820
|
+
account_email: account.account_info?.email || eb.recipient || '',
|
|
2821
|
+
recipient: eb.recipient || '',
|
|
2822
|
+
reason: eb.reason || '',
|
|
2823
|
+
status_code: eb.status_code || '',
|
|
2824
|
+
attempts: eb.attempt_count || max,
|
|
2825
|
+
domain: host,
|
|
2826
|
+
},
|
|
2827
|
+
});
|
|
2828
|
+
} catch (e) { console.error('ops bounce-suspend alert failed:', e.message); }
|
|
2829
|
+
}
|
|
2830
|
+
summary.suspended++;
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
console.log('sweep_email_bounces:', JSON.stringify(summary));
|
|
2834
|
+
return { code: 1, data: summary };
|
|
2835
|
+
} catch (err) {
|
|
2836
|
+
console.error('sweep_email_bounces error:', err);
|
|
2837
|
+
return { code: -10, data: err.message };
|
|
2838
|
+
}
|
|
2839
|
+
};
|
|
2840
|
+
|
|
2623
2841
|
// Every-3-days verification nudge for accounts still at stat 1 (unverified).
|
|
2624
2842
|
// Runs from the controller's daily cron (master/dev only); the daily tick plus
|
|
2625
2843
|
// the per-account 3-day gate below yields the every-3-days cadence. Each email
|
package/index_ms.mjs
CHANGED
|
@@ -209,6 +209,14 @@ export const maybe_send_welcome_email = async function (...args) {
|
|
|
209
209
|
return await broker.send_to_queue("maybe_send_welcome_email", ...args);
|
|
210
210
|
};
|
|
211
211
|
|
|
212
|
+
export const flag_email_bounce = async function (...args) {
|
|
213
|
+
return await broker.send_to_queue("flag_email_bounce", ...args);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export const sweep_email_bounces = async function (...args) {
|
|
217
|
+
return await broker.send_to_queue("sweep_email_bounces", ...args);
|
|
218
|
+
};
|
|
219
|
+
|
|
212
220
|
export const send_verification_reminders = async function (...args) {
|
|
213
221
|
return await broker.send_to_queue("send_verification_reminders", ...args);
|
|
214
222
|
};
|
package/index_msa.mjs
CHANGED
|
@@ -209,6 +209,14 @@ export const maybe_send_welcome_email = function (...args) {
|
|
|
209
209
|
broker.send_to_queue_async("maybe_send_welcome_email", ...args);
|
|
210
210
|
};
|
|
211
211
|
|
|
212
|
+
export const flag_email_bounce = function (...args) {
|
|
213
|
+
broker.send_to_queue_async("flag_email_bounce", ...args);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export const sweep_email_bounces = function (...args) {
|
|
217
|
+
broker.send_to_queue_async("sweep_email_bounces", ...args);
|
|
218
|
+
};
|
|
219
|
+
|
|
212
220
|
export const send_verification_reminders = function (...args) {
|
|
213
221
|
broker.send_to_queue_async("send_verification_reminders", ...args);
|
|
214
222
|
};
|