@xuda.io/account_module 1.2.2276 → 1.2.2277

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 CHANGED
@@ -54,6 +54,8 @@ const team_ms = await import(`${module_path}/team_module/index_ms.mjs`);
54
54
  const email_ms = await import(`${module_path}/email_module/index_ms.mjs`);
55
55
  const marketplace_ms = await import(`${module_path}/marketplace_module/index_ms.mjs`);
56
56
  const stripe_ms = await import(`${module_path}/stripe_module/index_ms.mjs`);
57
+ // Awaitable app handle for the ops termination flow (power off / destroy).
58
+ const app_ms = await import(`${module_path}/app_module/index_ms.mjs`);
57
59
 
58
60
  const ws_dashboard_msa = await import(`${module_path}/ws_dashboard_module/index_msa.mjs`);
59
61
  const drive_msa = await import(`${module_path}/drive_module/index_msa.mjs`);
@@ -1118,6 +1120,192 @@ export const get_account_info = async function (req) {
1118
1120
  });
1119
1121
  };
1120
1122
 
1123
+ // ===================================================================
1124
+ // OPS ACCOUNT TERMINATION FLOW (manual, superuser-only)
1125
+ // -------------------------------------------------------------------
1126
+ // Escalation ladder for non-payment, driven manually from the ops
1127
+ // dashboard (never automatic): suspend (services OFF) -> terminate
1128
+ // (services OFF + 14-day appeal window) -> finalize (permanent delete)
1129
+ // -> or reinstate. Every step notifies ops. `req.uid` is ALWAYS the
1130
+ // authenticated ops caller (the HTTP layer overwrites it from the
1131
+ // session), so the TARGET account is passed as `account_uid`.
1132
+ // ===================================================================
1133
+ const _OPS_SERVER_TYPES = ['datacenter', 'deployment', 'vps', 'managed_vps', 'instance', 'master'];
1134
+ const _ops_is_super = (req) => !!(req && req.uid && Array.isArray(_conf.superuser_account_ids) && _conf.superuser_account_ids.includes(req.uid));
1135
+ const _ops_host = () => (_conf.is_debug ? process.env.XUDA_HOSTNAME || _conf.domain : _conf.domain) || 'xuda.ai';
1136
+ const _ops_appeal_days = () => Number(_conf.termination?.appeal_days) || 14;
1137
+
1138
+ const _ops_enum_apps = async (uid) => {
1139
+ const ret = await db_module.find_couch_query('xuda_master', { selector: { app_uId: uid, docType: 'app' }, limit: 99999 }, true);
1140
+ return ret.docs || [];
1141
+ };
1142
+
1143
+ // Turn every server-type service for an account OFF (or back ON). Read the
1144
+ // power state via the canonical app_ms wrappers so DO/proxmox teardown is
1145
+ // handled correctly. Returns a small tally.
1146
+ const _ops_services_power = async (uid, on) => {
1147
+ const servers = (await _ops_enum_apps(uid)).filter((a) => _OPS_SERVER_TYPES.includes(a.app_type));
1148
+ let ok = 0, err = 0;
1149
+ for (const a of servers) {
1150
+ try {
1151
+ const r = on ? await app_ms.turn_app_on({ app_id: a._id }) : await app_ms.turn_app_off({ app_id: a._id });
1152
+ if (!r || r.code < 0) err++; else ok++;
1153
+ } catch (e) { err++; console.error('[ops power]', a._id, e.message); }
1154
+ }
1155
+ return { total: servers.length, ok, err };
1156
+ };
1157
+
1158
+ // OPS = the superuser accounts + info@xuda.ai. Sends both an email to info@ and
1159
+ // a system notification to every superuser. `ref` is shown in a small footer.
1160
+ const _ops_notify_ops = (subject, rows, ref) => {
1161
+ try {
1162
+ const esc = (v) => String(v == null ? '' : v).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1163
+ const footer = ref ? `<p style="margin:16px 0 0 0;font-size:11px;color:#999">Ref: ${esc(ref)}</p>` : '';
1164
+ const body = `<h2>${esc(subject)}</h2><table cellpadding="6" style="border-collapse:collapse;font-size:14px">${rows.map(([k, v]) => `<tr><td><strong>${esc(k)}</strong></td><td>${esc(v)}</td></tr>`).join('')}</table>${footer}`;
1165
+ email_msa.send_email({ email: 'info@xuda.ai', subject: `[OPS] ${subject}`, body });
1166
+ const supers = _conf.superuser_account_ids || [];
1167
+ if (supers.length) notification_msa.submit_notification({ type: 'system', uid_arr: supers, system: true, subject: `[OPS] ${subject}`, body, delivery_method: ['banner', 'email'], display_type: 'warning' });
1168
+ } catch (e) { console.error('[ops notify]', e.message); }
1169
+ };
1170
+
1171
+ // Step 1a — SUSPEND: services off, no data loss.
1172
+ export const ops_suspend_account = async function (req = {}) {
1173
+ try {
1174
+ if (!_ops_is_super(req)) return { code: -403, data: 'superuser only' };
1175
+ const account_uid = req.account_uid;
1176
+ if (!account_uid) return { code: -1, data: 'account_uid is required' };
1177
+ const ar = await db_module.get_couch_doc('xuda_accounts', account_uid);
1178
+ if (ar.code < 0 || !ar.data) return { code: -1, data: 'account not found' };
1179
+ const account = ar.data;
1180
+ account.account_suspension_status = 1;
1181
+ account.account_suspension_date = Date.now();
1182
+ account.suspension_reason = req.reason || '';
1183
+ account.suspension_by_uid = req.uid;
1184
+ const sr = await db_module.save_couch_doc('xuda_accounts', account);
1185
+ if (sr.code < 0) return { code: -1, data: sr.data };
1186
+ const power = await _ops_services_power(account_uid, false);
1187
+ const host = _ops_host();
1188
+ notification_msa.submit_notification({ type: 'account', uid_arr: [account_uid], topic: 'health_account_suspended', params: { billing_url: `https://${host}/dashboard/settings/billing` } });
1189
+ _ops_notify_ops(`Account suspended: ${account.account_info?.email || account_uid}`, [['Account', account_uid], ['Email', account.account_info?.email], ['By', req.uid], ['Reason', req.reason || ''], ['Services off', `${power.ok}/${power.total}`]], 'A2');
1190
+ logs_msa.add_account_log({ uid: account_uid, method: 'ops_suspend_account', source: 'ops dashboard', response_status_code: 200, response_data: `suspended by ${req.uid}; services off ${power.ok}/${power.total}` });
1191
+ return { code: 1, data: { suspended: account_uid, services: power } };
1192
+ } catch (err) { return { code: -1, data: err.message }; }
1193
+ };
1194
+
1195
+ // Step 1b — TERMINATE: services off + start the appeal window. No deletion.
1196
+ export const ops_terminate_account = async function (req = {}) {
1197
+ try {
1198
+ if (!_ops_is_super(req)) return { code: -403, data: 'superuser only' };
1199
+ const account_uid = req.account_uid;
1200
+ if (!account_uid) return { code: -1, data: 'account_uid is required' };
1201
+ const ar = await db_module.get_couch_doc('xuda_accounts', account_uid);
1202
+ if (ar.code < 0 || !ar.data) return { code: -1, data: 'account not found' };
1203
+ const account = ar.data;
1204
+ const appeal_days = _ops_appeal_days();
1205
+ account.account_termination_status = 1;
1206
+ account.account_termination_date = Date.now();
1207
+ account.termination_reason = req.reason || '';
1208
+ account.termination_by_uid = req.uid;
1209
+ account.termination_appeal_deadline = Date.now() + appeal_days * 86400000;
1210
+ account.appeal_status = 'none';
1211
+ account.termination_finalized_ts = null;
1212
+ const sr = await db_module.save_couch_doc('xuda_accounts', account);
1213
+ if (sr.code < 0) return { code: -1, data: sr.data };
1214
+ const power = await _ops_services_power(account_uid, false);
1215
+ const host = _ops_host();
1216
+ notification_msa.submit_notification({ type: 'account', uid_arr: [account_uid], topic: 'health_account_terminated', params: { appeal_days, appeal_url: `https://${host}/dashboard/support` } });
1217
+ _ops_notify_ops(`Account terminated (${appeal_days}d appeal): ${account.account_info?.email || account_uid}`, [['Account', account_uid], ['Email', account.account_info?.email], ['By', req.uid], ['Reason', req.reason || ''], ['Services off', `${power.ok}/${power.total}`], ['Appeal deadline', new Date(account.termination_appeal_deadline).toISOString()]], 'A3');
1218
+ logs_msa.add_account_log({ uid: account_uid, method: 'ops_terminate_account', source: 'ops dashboard', response_status_code: 200, response_data: `terminated by ${req.uid}; appeal until ${new Date(account.termination_appeal_deadline).toISOString()}; services off ${power.ok}/${power.total}` });
1219
+ return { code: 1, data: { terminated: account_uid, appeal_deadline: account.termination_appeal_deadline, services: power } };
1220
+ } catch (err) { return { code: -1, data: err.message }; }
1221
+ };
1222
+
1223
+ // Step 3 — FINALIZE: NEVER destroys anything. It compiles the list of resources
1224
+ // that should be deleted for a terminated account and REPORTS it to ops, who
1225
+ // then delete manually. Superuser + terminated required.
1226
+ export const ops_finalize_termination = async function (req = {}) {
1227
+ try {
1228
+ if (!_ops_is_super(req)) return { code: -403, data: 'superuser only' };
1229
+ const account_uid = req.account_uid;
1230
+ if (!account_uid) return { code: -1, data: 'account_uid is required' };
1231
+ const ar = await db_module.get_couch_doc('xuda_accounts', account_uid);
1232
+ if (ar.code < 0 || !ar.data) return { code: -1, data: 'account not found' };
1233
+ const account = ar.data;
1234
+ if (account.account_termination_status !== 1) return { code: -1, data: 'account is not in a terminated state' };
1235
+
1236
+ const apps = await _ops_enum_apps(account_uid);
1237
+ const resources = apps.map((a) => ({
1238
+ app_id: a._id,
1239
+ app_type: a.app_type,
1240
+ app_name: a.app_name || a._id,
1241
+ ip: a.app_hosting_server?.ip || '',
1242
+ region: a.app_hosting_server?.region_name || a.app_hosting_server?.provider || '',
1243
+ }));
1244
+ const appeal_window_elapsed = !!(account.termination_appeal_deadline && Date.now() >= account.termination_appeal_deadline);
1245
+ account.termination_reported_ts = Date.now();
1246
+ account.termination_reported_by_uid = req.uid;
1247
+ await db_module.save_couch_doc('xuda_accounts', account);
1248
+
1249
+ const rows = [
1250
+ ['Account', account_uid], ['Email', account.account_info?.email], ['Requested by', req.uid],
1251
+ ['Appeal window elapsed', appeal_window_elapsed ? 'yes' : 'NO — still within appeal period'],
1252
+ ['Resources to delete', resources.length],
1253
+ ...resources.map((r, i) => [`#${i + 1} ${r.app_type}`, `${r.app_name} ${r.ip}`.trim()]),
1254
+ ];
1255
+ _ops_notify_ops(`Resources to delete (manual) for terminated account: ${account.account_info?.email || account_uid}`, rows, 'OPS-DELETE-LIST');
1256
+ logs_msa.add_account_log({ uid: account_uid, method: 'ops_finalize_termination', security: true, source: 'ops dashboard', response_status_code: 200, response_data: `reported ${resources.length} resource(s) for manual deletion by ${req.uid}; appeal_elapsed=${appeal_window_elapsed}` });
1257
+ return { code: 1, data: { account_uid, appeal_window_elapsed, resources } };
1258
+ } catch (err) { return { code: -1, data: err.message }; }
1259
+ };
1260
+
1261
+ // Reinstate (appeal accepted): clear suspension/termination, services back on.
1262
+ export const ops_reinstate_account = async function (req = {}) {
1263
+ try {
1264
+ if (!_ops_is_super(req)) return { code: -403, data: 'superuser only' };
1265
+ const account_uid = req.account_uid;
1266
+ if (!account_uid) return { code: -1, data: 'account_uid is required' };
1267
+ const ar = await db_module.get_couch_doc('xuda_accounts', account_uid);
1268
+ if (ar.code < 0 || !ar.data) return { code: -1, data: 'account not found' };
1269
+ const account = ar.data;
1270
+ if (account.termination_finalized_ts) return { code: -1, data: 'resources already deleted; cannot reinstate' };
1271
+ account.account_suspension_status = 0;
1272
+ account.account_termination_status = 0;
1273
+ account.appeal_status = 'accepted';
1274
+ account.reinstated_ts = Date.now();
1275
+ account.reinstated_by_uid = req.uid;
1276
+ const sr = await db_module.save_couch_doc('xuda_accounts', account);
1277
+ if (sr.code < 0) return { code: -1, data: sr.data };
1278
+ const power = await _ops_services_power(account_uid, true);
1279
+ notification_msa.submit_notification({ type: 'account', uid_arr: [account_uid], subject: 'Your account has been reinstated', body: '<h2>Your account has been reinstated</h2><p>Your account is active again and your services are being restored. Thank you.</p><p style="margin:16px 0 0 0;font-size:11px;color:#999">Ref: OPS-REINST</p>', delivery_method: ['banner', 'email'], display_type: 'info' });
1280
+ _ops_notify_ops(`Account reinstated: ${account.account_info?.email || account_uid}`, [['Account', account_uid], ['Email', account.account_info?.email], ['By', req.uid], ['Services on', `${power.ok}/${power.total}`]], 'OPS-REINST');
1281
+ logs_msa.add_account_log({ uid: account_uid, method: 'ops_reinstate_account', source: 'ops dashboard', response_status_code: 200, response_data: `reinstated by ${req.uid}; services on ${power.ok}/${power.total}` });
1282
+ return { code: 1, data: { reinstated: account_uid, services: power } };
1283
+ } catch (err) { return { code: -1, data: err.message }; }
1284
+ };
1285
+
1286
+ // Ops dashboard queue: accounts currently suspended or terminated.
1287
+ export const ops_list_terminations = async function (req = {}) {
1288
+ try {
1289
+ if (!_ops_is_super(req)) return { code: -403, data: 'superuser only' };
1290
+ const ret = await db_module.find_couch_query('xuda_accounts', { selector: { docType: 'account', $or: [{ account_suspension_status: 1 }, { account_termination_status: 1 }] }, limit: 9999 }, true);
1291
+ const rows = (ret.docs || []).map((a) => ({
1292
+ account_uid: a._id,
1293
+ email: a.account_info?.email,
1294
+ name: `${a.account_info?.first_name || ''} ${a.account_info?.last_name || ''}`.trim(),
1295
+ suspended: a.account_suspension_status === 1,
1296
+ suspension_date: a.account_suspension_date,
1297
+ terminated: a.account_termination_status === 1,
1298
+ termination_date: a.account_termination_date,
1299
+ reason: a.termination_reason || a.suspension_reason || '',
1300
+ by_uid: a.termination_by_uid || a.suspension_by_uid || '',
1301
+ appeal_deadline: a.termination_appeal_deadline || null,
1302
+ appeal_status: a.appeal_status || 'none',
1303
+ finalized: !!a.termination_finalized_ts,
1304
+ }));
1305
+ return { code: 1, data: { rows } };
1306
+ } catch (err) { return { code: -1, data: err.message }; }
1307
+ };
1308
+
1121
1309
  const TIPS_FILE_PATH = path.join(process.env.XUDA_HOME, 'dist', 'tips', 'tips.json');
1122
1310
  let _did_you_know_tips_cache = null;
1123
1311
  let _did_you_know_tips_cache_mtime = 0;
package/index_ms.mjs CHANGED
@@ -89,6 +89,26 @@ export const get_account_info = async function (...args) {
89
89
  return await broker.send_to_queue("get_account_info", ...args);
90
90
  };
91
91
 
92
+ export const ops_suspend_account = async function (...args) {
93
+ return await broker.send_to_queue("ops_suspend_account", ...args);
94
+ };
95
+
96
+ export const ops_terminate_account = async function (...args) {
97
+ return await broker.send_to_queue("ops_terminate_account", ...args);
98
+ };
99
+
100
+ export const ops_finalize_termination = async function (...args) {
101
+ return await broker.send_to_queue("ops_finalize_termination", ...args);
102
+ };
103
+
104
+ export const ops_reinstate_account = async function (...args) {
105
+ return await broker.send_to_queue("ops_reinstate_account", ...args);
106
+ };
107
+
108
+ export const ops_list_terminations = async function (...args) {
109
+ return await broker.send_to_queue("ops_list_terminations", ...args);
110
+ };
111
+
92
112
  export const did_you_know_tips = async function (...args) {
93
113
  return await broker.send_to_queue("did_you_know_tips", ...args);
94
114
  };
package/index_msa.mjs CHANGED
@@ -89,6 +89,26 @@ export const get_account_info = function (...args) {
89
89
  broker.send_to_queue_async("get_account_info", ...args);
90
90
  };
91
91
 
92
+ export const ops_suspend_account = function (...args) {
93
+ broker.send_to_queue_async("ops_suspend_account", ...args);
94
+ };
95
+
96
+ export const ops_terminate_account = function (...args) {
97
+ broker.send_to_queue_async("ops_terminate_account", ...args);
98
+ };
99
+
100
+ export const ops_finalize_termination = function (...args) {
101
+ broker.send_to_queue_async("ops_finalize_termination", ...args);
102
+ };
103
+
104
+ export const ops_reinstate_account = function (...args) {
105
+ broker.send_to_queue_async("ops_reinstate_account", ...args);
106
+ };
107
+
108
+ export const ops_list_terminations = function (...args) {
109
+ broker.send_to_queue_async("ops_list_terminations", ...args);
110
+ };
111
+
92
112
  export const did_you_know_tips = function (...args) {
93
113
  broker.send_to_queue_async("did_you_know_tips", ...args);
94
114
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xuda.io/account_module",
3
- "version": "1.2.2276",
3
+ "version": "1.2.2277",
4
4
  "description": "Xuda Account Server Module",
5
5
  "main": "index.mjs",
6
6
  "dependencies": {