@xuda.io/account_module 1.2.144 → 1.2.146

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 ADDED
@@ -0,0 +1,1239 @@
1
+ import path from 'path';
2
+ import _ from 'lodash';
3
+ import { createRequire } from 'module';
4
+
5
+ // Initialize require for dynamic paths based on environment variables
6
+ const require = createRequire(import.meta.url);
7
+
8
+ // Load Globals and Config
9
+ global._conf = require(path.join(process.env.XUDA_HOME, process.env.XUDA_CONFIG));
10
+
11
+ const _common = require(path.join(process.env.XUDA_HOME, 'common', 'xuda_node_common.js'));
12
+ const _utils = require(path.join(process.env.XUDA_HOME, 'common', 'xuda-cpi-utils.js'));
13
+
14
+ // Module Paths
15
+ const module_path = path.join(process.env.XUDA_HOME, 'cpi') + (!_conf.is_debug ? '/node_modules/@xuda.io' : '');
16
+ const db_module = require(`${module_path}/db_module`);
17
+ const jobs_module = require(`${module_path}/jobs_module`);
18
+
19
+ export const update_account_info = async (req) => {
20
+ const marketplace_module = require(`${module_path}/marketplace_module`);
21
+
22
+ const { uid } = req;
23
+ const data = req;
24
+
25
+ const validate_input = (string, max_length, is_mandatory, is_pass) => {
26
+ if (!/^[a-zA-Z0-9,.@ ]*$/.test(string) && !is_pass) {
27
+ return -1;
28
+ } else if ((is_mandatory && !string) || string.length < 2) {
29
+ return -2;
30
+ } else if (string.length > max_length) {
31
+ // Fixed: string.count -> string.length
32
+ return -3;
33
+ } else {
34
+ return 1;
35
+ }
36
+ };
37
+
38
+ const ret = await db_module.get_couch_doc('xuda_accounts', uid);
39
+ let account_obj = ret.data;
40
+ account_obj.ts = Date.now();
41
+ let change = '';
42
+ let error = {};
43
+
44
+ await marketplace_module.marketplace_save_user({ uid });
45
+
46
+ _.forEach(data, (val, key) => {
47
+ if (account_obj.account_info[key] !== val) {
48
+ change += ' from ' + account_obj.account_info[key] + ' to ' + val;
49
+
50
+ account_obj.account_info[key] = val;
51
+
52
+ if (account_obj.account_info.account_type === 'business') {
53
+ if (key === 'company_name' && validate_input(val, 150, true, false) < 0) {
54
+ error[key] = 'Invalid company';
55
+ }
56
+ }
57
+ if (key === 'first_name' && !validate_input(val, 35, true, false)) {
58
+ error[key] = 'Invalid first name';
59
+ }
60
+ if (key === 'last_name' && !validate_input(val, 35, true, false)) {
61
+ error[key] = 'Invalid last name';
62
+ }
63
+ if (key === 'email' && (!validator.isEmail(val) || !val || val.length < 2)) {
64
+ error[key] = 'Invalid email';
65
+ }
66
+ if (key === 'tel' && (!/^[0-9]{3}-[0-9]{4}-[0-9]{4}$/.test(val) || !val || val.length < 2)) {
67
+ error[key] = 'Invalid phone number';
68
+ }
69
+ if (key === 'address' && !/^[a-z0-9- ]+$/i.test(val)) {
70
+ error[key] = 'Invalid address';
71
+ }
72
+ if (key === 'city' && !validate_input(val, 255, false, false)) {
73
+ error[key] = 'Invalid city';
74
+ }
75
+ if (key === 'state' && !validate_input(val, 50, false, false)) {
76
+ error[key] = 'Invalid state';
77
+ }
78
+ if (key === 'zip' && validate_input(val, 9, false, false) < 0) {
79
+ error[key] = 'Invalid zip';
80
+ }
81
+ if (key === 'country' && !validate_input(val, 55, false, false)) {
82
+ error[key] = 'Invalid country';
83
+ }
84
+ }
85
+ });
86
+
87
+ if (!_.isEmpty(error)) {
88
+ return { code: -1310, data: error };
89
+ }
90
+ if (!change) {
91
+ return { code: 1300, data: 'no change' };
92
+ }
93
+ delete account_obj.account_info.gtp_token;
94
+ delete account_obj.account_info.client_id;
95
+ delete account_obj.account_info.uid;
96
+
97
+ if (!account_obj.account_project_id) {
98
+ const app_module = require(`${module_path}/app_module`);
99
+ const ret = await app_module.create_project({
100
+ token_ret: req.token_ret,
101
+ uid,
102
+ data: {
103
+ app_name: `Account ${uid} main project`,
104
+ is_account_project: true,
105
+ app_plugins_purchased: ['@xuda.io/xuda-dbs-plugin-xuda', '@xuda.io/xuda-framework-plugin-tailwind'],
106
+ },
107
+ });
108
+ if (ret.code > -1) {
109
+ account_obj.account_project_id = ret.data;
110
+ }
111
+ }
112
+
113
+ const ret2 = await db_module.save_couch_doc('xuda_accounts', account_obj);
114
+ if (ret2.code > 0) {
115
+ return { code: 1300, data: 'ok' };
116
+ }
117
+ return ret2;
118
+ };
119
+
120
+ export const update_account_preferences = async (req) => {
121
+ const account_id = req.account_id;
122
+ const data = req;
123
+
124
+ const ret = await db_module.get_couch_doc('xuda_accounts', account_id);
125
+ let account_obj = ret.data;
126
+ account_obj.ts = Date.now();
127
+
128
+ account_obj.preferences = data;
129
+
130
+ const ret1 = await db_module.save_couch_doc('xuda_accounts', account_obj);
131
+ if (ret1.code < 0) {
132
+ return ret1;
133
+ }
134
+ return { code: 1300, data: account_obj.preferences };
135
+ };
136
+
137
+ export const save_admin_presets = async (req) => {
138
+ const ret = await db_module.get_couch_doc('xuda_master', req.app_id);
139
+ if (ret.code < 0) {
140
+ return ret;
141
+ }
142
+ let app_obj = ret.data;
143
+ app_obj.deploy_data.admin_presets = req.admin_presets;
144
+ app_obj.deploy_data.preset_id = req.preset_id;
145
+
146
+ const ret3 = await db_module.save_app_obj(app_obj, null, req.app_id);
147
+ let obj = {};
148
+ if (ret3.code > -1) {
149
+ obj = {
150
+ code: 1,
151
+ data: 'ok',
152
+ app_obj: _common.get_clean_app_obj(app_obj),
153
+ };
154
+ } else {
155
+ obj = ret3;
156
+ }
157
+
158
+ return obj;
159
+ };
160
+
161
+ export const increment_account_usage = async (req) => {
162
+ const { uid } = req;
163
+
164
+ let opt = {};
165
+ if (uid) {
166
+ opt.key = uid;
167
+ }
168
+ const accounts_ret = await db_module.get_couch_view_raw('xuda_accounts', 'all_accounts', opt);
169
+
170
+ const drive_module = require(`${module_path}/drive_module`);
171
+
172
+ const bytesToGB = (bytes) => {
173
+ return bytes / 1073741824; // or bytes / (1024 ** 3)
174
+ };
175
+
176
+ // check if last calculation made at least hour ago
177
+ const interval = 3600 * 1000;
178
+
179
+ const run_account_drive = async (account_data) => {
180
+ try {
181
+ const usage_id = await _common.xuda_get_uuid('usage');
182
+
183
+ const usage_ret = await db_module.get_couch_view_raw('xuda_usage', 'open_drive_usage', {
184
+ key: account_data._id,
185
+ });
186
+
187
+ let doc = {
188
+ _id: usage_id,
189
+ docType: 'user_drive_usage',
190
+ date_created: new Date(),
191
+ stat: 1,
192
+ uid: account_data._id,
193
+ stripe_customer_id: account_data.stripe_customer_id,
194
+ account_name: account_data.account_info.first_name + ' ' + account_data.account_info.last_name,
195
+ size: 0,
196
+ price: 0,
197
+ hours: 0,
198
+ ts: 0,
199
+ subscription_id: account_data.stripe_membership_subscription_id,
200
+ };
201
+
202
+ if (usage_ret?.rows?.[0]) {
203
+ doc = usage_ret.rows[0].value;
204
+ }
205
+
206
+ if (Date.now() - doc.ts >= interval) {
207
+ const plan = _conf.PLAN_OBJ[account_data.membership_plan];
208
+ const drive_size_total = account_data.user_drive_size + account_data.studio_drive_size + account_data.workspace_drive_size + account_data.builds_drive_size + account_data.plugins_drive_size;
209
+ if (bytesToGB(drive_size_total) > plan.features.drive) {
210
+ let price_per_hour = (bytesToGB(drive_size_total) * _conf.PRICE_OBJ.price_per_gb_drive) / _conf.PRICE_OBJ.avg_hours_in_month;
211
+
212
+ doc.hours++;
213
+ doc.size = drive_size_total;
214
+ doc.price += price_per_hour;
215
+
216
+ doc.ts = Date.now();
217
+ await db_module.save_couch_doc('xuda_usage', doc);
218
+ }
219
+ }
220
+ } catch (err) {
221
+ console.error(err.message);
222
+ }
223
+ };
224
+
225
+ const run_account_apps = async (account_data) => {
226
+ const apps_ret = await db_module.get_couch_view('xuda_master', 'user_apps', {
227
+ startkey: [account_data._id, ''],
228
+ endkey: [account_data._id, 'ZZZZZ'],
229
+ include_docs: true,
230
+ });
231
+
232
+ try {
233
+ for await (let app of apps_ret.data.rows) {
234
+ if (app.value.app_type === 'master') {
235
+ let size_ret = await drive_module.get_drive_size({
236
+ drive_type: 'studio',
237
+ app_id: app.id,
238
+ });
239
+ if (size_ret.code > -1) account_data.studio_drive_size += size_ret.data;
240
+
241
+ size_ret = await drive_module.get_drive_size({
242
+ drive_type: 'workspace',
243
+ app_id: app.id,
244
+ });
245
+ if (size_ret.code > -1) account_data.workspace_drive_size += size_ret.data;
246
+
247
+ size_ret = await drive_module.get_drive_size({
248
+ drive_type: 'builds',
249
+ app_id: app.id,
250
+ });
251
+ if (size_ret.code > -1) account_data.builds_drive_size += size_ret.data;
252
+
253
+ size_ret = await drive_module.get_drive_size({
254
+ drive_type: 'plugins',
255
+ app_id: app.id,
256
+ });
257
+ if (size_ret.code > -1) account_data.plugins_drive_size += size_ret.data;
258
+
259
+ // db data
260
+ const couch_info_ret = await db_module.get_couch_app_info(app.id);
261
+ account_data.project_data_size += couch_info_ret?.data?.sizes?.active || 0;
262
+ }
263
+
264
+ const usage_id = await _common.xuda_get_uuid('usage');
265
+
266
+ const usage_ret = await db_module.get_couch_view_raw('xuda_usage', 'open_app_usage', {
267
+ key: [app.id, app.doc?.app_hosting?.app_server_type || app.doc.app_type],
268
+ });
269
+
270
+ let doc = {
271
+ _id: usage_id,
272
+ docType: 'app_usage',
273
+ date_created: new Date(),
274
+ stat: 1,
275
+ uid: account_data._id,
276
+ stripe_customer_id: account_data.stripe_customer_id,
277
+ account_name: account_data.account_info.first_name + ' ' + account_data.account_info.last_name,
278
+ app_id: app.id,
279
+ price: 0,
280
+ hours: 0,
281
+ ts: 0,
282
+ name: app.value.app_name,
283
+ app_type: app.value.app_type,
284
+ subscription_id: account_data.stripe_membership_subscription_id,
285
+ };
286
+
287
+ if (doc.app_type === 'user_group') {
288
+ doc = {
289
+ ...doc,
290
+ ...{
291
+ user_group_members: 0,
292
+ user_group_members_accumulated: 0,
293
+ user_group_members_avg: 0,
294
+ price_user_group: 0,
295
+ },
296
+ };
297
+ }
298
+
299
+ if (usage_ret?.rows?.[0]) {
300
+ doc = usage_ret.rows[0].value;
301
+ }
302
+
303
+ let price_per_hour;
304
+
305
+ // check if last calculation made at least hour ago
306
+ const interval = 6000; // 3600 * 1000;
307
+ if (Date.now() - doc.ts >= interval) {
308
+ doc.hours++;
309
+
310
+ switch (doc.app_type) {
311
+ case 'backup':
312
+ if (app.value.app_hosting) {
313
+ // backup of deployment
314
+ price_per_hour = (app.value.app_hosting.disk * _conf.PRICE_OBJ.price_per_gb_backup) / _conf.PRICE_OBJ.avg_hours_in_month;
315
+ } else {
316
+ // backup of project
317
+ const info_ret = await db_module.get_couch_app_info(doc.app_id);
318
+ if (info_ret.code > 0) {
319
+ const size = info_ret.data.sizes.active / 1000 / 1000 / 1000; //gb
320
+ price_per_hour = (size * _conf.PRICE_OBJ.price_per_gb_backup) / _conf.PRICE_OBJ.avg_hours_in_month;
321
+ }
322
+ }
323
+ break;
324
+
325
+ case 'user_group':
326
+ {
327
+ const team_ret = await db_module.get_couch_view('xuda_team', 'user_group_shares_count', {
328
+ key: app.id,
329
+ reduce: true,
330
+ group_level: 1,
331
+ });
332
+
333
+ const team_members = team_ret.data.rows?.[0]?.value || 0;
334
+ const price_per_hour_team = (team_members * _conf.PRICE_OBJ.price_per_user_group_member) / _conf.PRICE_OBJ.avg_hours_in_month;
335
+ doc.price_user_group += price_per_hour_team;
336
+ doc.user_group_members = team_members;
337
+ doc.user_group_members_accumulated += team_members;
338
+ doc.user_group_members_avg = doc.user_group_members_accumulated / doc.hours;
339
+ }
340
+ break; // Added break
341
+
342
+ default:
343
+ // deployments,vps,master,instances and datacenters
344
+ if (!app.doc.app_hosting) continue;
345
+
346
+ doc.app_hosting = app.doc.app_hosting;
347
+ doc.server_type = app.doc.app_hosting.app_server_type;
348
+ price_per_hour = app.doc.app_hosting.price / _conf.PRICE_OBJ.avg_hours_in_month;
349
+ break;
350
+ }
351
+ doc.price += price_per_hour;
352
+ if (doc.price < 1) {
353
+ doc.price = 1;
354
+ }
355
+
356
+ doc.ts = Date.now();
357
+ await db_module.save_couch_doc('xuda_usage', doc);
358
+ }
359
+ }
360
+ } catch (err) {
361
+ console.error(err.message);
362
+ }
363
+ };
364
+
365
+ // iterate on all status account except deleted accounts
366
+ for await (let val of accounts_ret.rows) {
367
+ let account_data = val.value;
368
+
369
+ const size_ret = await drive_module.get_drive_size({
370
+ uid: account_data._id,
371
+ drive_type: 'user',
372
+ });
373
+
374
+ account_data.user_drive_size = size_ret.data;
375
+ account_data.studio_drive_size = 0;
376
+ account_data.workspace_drive_size = 0;
377
+ account_data.plugins_drive_size = 0;
378
+ account_data.builds_drive_size = 0;
379
+ account_data.project_data_size = 0;
380
+
381
+ await run_account_apps(account_data);
382
+ await run_account_drive(account_data);
383
+
384
+ account_data.total_drive_size = account_data.user_drive_size + account_data.studio_drive_size + account_data.workspace_drive_size + account_data.plugins_drive_size + account_data.builds_drive_size + account_data.project_data_size;
385
+ await db_module.save_couch_doc('xuda_accounts', account_data);
386
+ }
387
+ };
388
+
389
+ export const get_account_data = async (req) => {
390
+ let { uid, enforce_usage } = req;
391
+
392
+ try {
393
+ const { data: acc_obj } = await db_module.get_couch_doc('xuda_accounts', uid);
394
+ let ret = { code: 1 };
395
+ ret.data = {
396
+ _id: acc_obj._id,
397
+ account_info: acc_obj.account_info,
398
+ usage: acc_obj.activity_usage,
399
+ membership_plan: acc_obj.membership_plan,
400
+ plan_changed: acc_obj.plan_changed,
401
+ plan_info: acc_obj.plan_info,
402
+ support_plan: acc_obj.support_plan,
403
+ support_plan_changed: acc_obj.support_plan_changed,
404
+ storage_plan: acc_obj.storage_plan,
405
+ storage_plan_changed: acc_obj.storage_plan_changed,
406
+ account_project_id: acc_obj.account_project_id,
407
+
408
+ prices_info: acc_obj.prices_info,
409
+ activity_usage: acc_obj.activity_usage,
410
+ uid: acc_obj._id,
411
+ stat: acc_obj.stat,
412
+ preferences: acc_obj.preferences || {},
413
+ billing_status: {
414
+ account_suspension_status: acc_obj.account_suspension_status,
415
+ account_suspension_data: acc_obj.account_suspension_data,
416
+ account_billing_hold_status: acc_obj.account_billing_hold_status,
417
+ account_billing_hold_data: acc_obj.account_billing_hold_date,
418
+ account_termination_status: acc_obj.account_termination_status,
419
+ account_termination_data: acc_obj.account_termination_data,
420
+ },
421
+ drive_usage: {
422
+ user_drive_size: acc_obj.user_drive_size || 0,
423
+ studio_drive_size: acc_obj.studio_drive_size || 0,
424
+ workspace_drive_size: acc_obj.workspace_drive_size || 0,
425
+ plugins_drive_size: acc_obj.plugins_drive_size || 0,
426
+ builds_drive_size: acc_obj.builds_drive_size || 0,
427
+ project_data_size: acc_obj.project_data_size || 0,
428
+ total_drive_size: acc_obj.total_drive_size || 0,
429
+ },
430
+ stripe_connect_account_id: acc_obj?.stripe_connect_account_obj?.id,
431
+ stripe_connect_account_status: acc_obj?.stripe_connect_account_status,
432
+ };
433
+
434
+ return ret;
435
+ } catch (error) {
436
+ return { code: -1, data: error };
437
+ }
438
+ };
439
+
440
+ export const get_account_projects = async (req) => {
441
+ const { uid } = req;
442
+ let opt = {
443
+ key: uid,
444
+ };
445
+
446
+ if (_conf.superuser_account_ids.includes(uid) || _conf.support.support_team_account_ids.includes(uid)) {
447
+ opt = {};
448
+ }
449
+
450
+ let ret = await db_module.get_couch_view('xuda_master', 'user_projects', opt);
451
+
452
+ return ret;
453
+ };
454
+
455
+ export const get_account_datacenters = async (req) => {
456
+ return await db_module.get_couch_view('xuda_master', 'user_datacenters', {
457
+ key: req.uid,
458
+ });
459
+ };
460
+
461
+ export const get_account_deployments = async (req) => {
462
+ return await db_module.get_couch_view('xuda_master', 'user_deployments', {
463
+ key: req.uid,
464
+ });
465
+ };
466
+
467
+ export const get_account_instances = async (req) => {
468
+ return await db_module.get_couch_view('xuda_master', 'user_all_instances', {
469
+ key: req.uid,
470
+ });
471
+ };
472
+
473
+ export const get_account_info = async (req) => {
474
+ return await get_account_data({
475
+ uid: req.uid,
476
+ enforce_usage: req.enforce_usage,
477
+ });
478
+ };
479
+
480
+ export const get_account_name = async (req) => {
481
+ const data = await db_module.get_couch_doc('xuda_accounts', req.uid_query);
482
+ if (data.code < 0) {
483
+ return data;
484
+ }
485
+ let obj = {
486
+ first_name: '',
487
+ last_name: '',
488
+ email: '',
489
+ phone: '',
490
+ profile_picture: '',
491
+ username: '',
492
+ };
493
+ if (data.data.account_info) {
494
+ obj = {
495
+ first_name: data.data.account_info.first_name,
496
+ last_name: data.data.account_info.last_name,
497
+ email: data.data.account_info.email,
498
+ phone: data.data.account_info.tel,
499
+ profile_picture: data.data.account_info.profile_picture,
500
+ username: data.data.account_info.username,
501
+ };
502
+ }
503
+ return { code: 1, data: obj };
504
+ };
505
+
506
+ export const account_validate_username = async (req) => {
507
+ const opt = {
508
+ selector: { 'account_info.username': req.username },
509
+ fields: ['_id'],
510
+ limit: 1,
511
+ };
512
+
513
+ let ret = await db_module.find_couch_query('xuda_accounts', opt);
514
+
515
+ if (ret.docs.length) {
516
+ return { code: -400, data: 'User exist' };
517
+ } else {
518
+ return { code: 1, data: 'ok' };
519
+ }
520
+ };
521
+
522
+ export const verify_account = async (req) => {
523
+ const { account_id } = req;
524
+ if (!account_id) {
525
+ return { code: -10, data: 'Missing id' };
526
+ }
527
+
528
+ const ret = await db_module.get_couch_doc('xuda_accounts', account_id);
529
+
530
+ if (ret.code < 0) {
531
+ return { code: -1, data: 'account not found' };
532
+ }
533
+ let obj = ret.data;
534
+ obj.stat = 3;
535
+ const ret_acc = await db_module.save_couch_doc('xuda_accounts', obj);
536
+ if (ret.code < 0) {
537
+ return ret_acc;
538
+ }
539
+ return { code: 1, data: ret_acc.data };
540
+ };
541
+
542
+ export const validate_user_plan = async (req) => {
543
+ const { account_id, app_obj } = req;
544
+
545
+ const apps_ret = await db_module.get_couch_view('xuda_master', 'user_apps_count', {
546
+ startkey: [account_id, ''],
547
+ endkey: [account_id, 'ZZZZZ'],
548
+ reduce: true,
549
+ group_level: 2,
550
+ });
551
+
552
+ let ret_obj = _.reduce(
553
+ apps_ret.data.rows,
554
+ (ret, value) => {
555
+ ret[value.key[1]] = value.value;
556
+ return ret;
557
+ },
558
+ {},
559
+ );
560
+
561
+ const ret = await db_module.get_couch_doc('xuda_accounts', account_id);
562
+ if (ret.code < 0) {
563
+ return ret;
564
+ }
565
+ let plan = _conf.PLAN_OBJ[ret.data.membership_plan];
566
+
567
+ if (!plan) {
568
+ return { code: -7, data: 'error - no plan defined' };
569
+ }
570
+
571
+ // validate number of projects
572
+ if (app_obj.app_type === 'master' && ret_obj.master >= plan.features.projects) {
573
+ return {
574
+ code: -8,
575
+ data: `Number of projects (${ret.data.membership_plan}) exceeds to the ${plan.features.projects} plan limits`,
576
+ };
577
+ }
578
+
579
+ // validate number of team members
580
+ const user_active_app_requests_count_ret = await db_module.get_couch_view('xuda_team', 'user_active_app_requests_count', {
581
+ key: account_id,
582
+ reduce: true,
583
+ // group_level: 2,
584
+ });
585
+
586
+ if (app_obj.app_type === 'master' && user_active_app_requests_count_ret.data?.rows?.[0]?.value >= plan.features.team) {
587
+ return {
588
+ code: -9,
589
+ data: `Number of team shares (${user_active_app_requests_count_ret.data.rows[0].value}) exceeds to the ${plan.features.team} plan limits`,
590
+ };
591
+ }
592
+
593
+ return { code: 1, data: ret.data };
594
+ };
595
+
596
+ export const get_hosting_plan = (req) => {
597
+ const { app_obj } = req;
598
+ if (app_obj.app_hosting) {
599
+ if (_conf.PRICE_OBJ.server_slugs[app_obj.app_hosting.app_server_type]) {
600
+ return _conf.PRICE_OBJ.server_slugs[app_obj.app_hosting.app_server_type];
601
+ }
602
+ // debugger;
603
+ console.error('error: ' + app_obj.app_hosting.app_server_type + ' not found in PRICE_OBJ.server_slugs');
604
+ return { cpu: 0, price: 0 };
605
+ }
606
+ };
607
+
608
+ export const get_cpu = async (req) => {
609
+ const { app_obj } = req;
610
+ let cpu = 1;
611
+ if (app_obj.app_hosting) {
612
+ const hosting_plan = get_hosting_plan({
613
+ app_obj,
614
+ });
615
+
616
+ if (hosting_plan.cpu) {
617
+ cpu = hosting_plan.cpu;
618
+ }
619
+ }
620
+ return cpu;
621
+ };
622
+
623
+ const get_account_log_object = (body, status, service, source, response_data, ip, headers, security) => {
624
+ return {
625
+ uid: body.uid,
626
+ ip,
627
+ method: service,
628
+ req_body: body,
629
+ client_headers: headers,
630
+ security: _conf.cpi_methods?.[service]?.log,
631
+ api_pk: body.api_pk,
632
+ api_sk: body.api_sk,
633
+ dashboard: !body.api_pk && !body.api_sk,
634
+ response_status_code: status,
635
+ response_data,
636
+ source,
637
+ security,
638
+ };
639
+ };
640
+
641
+ export const add_account_log_util = (body, status, service, source, response_data, ip, headers) => {
642
+ if (!_conf.cpi_methods?.[service]) return;
643
+ if (_conf.cpi_methods?.[service]?.private) return;
644
+ const security = _conf.cpi_methods?.[service].security;
645
+ if (!security) {
646
+ if (service.substr(0, 4) === 'get_' && !body.api_pk && !body.api_sk) return;
647
+ }
648
+ const logs_module = require(`${module_path}/logs_module`);
649
+ logs_module.add_account_log(get_account_log_object(body, status, service, source, response_data, ip, headers, security));
650
+ };
651
+
652
+ export const save_ssh_key = async (req) => {
653
+ const { uid, _id, ssh_key_name, ssh_key_content } = req;
654
+
655
+ const isValidSSHPublicKey = (sshPublicKey) => {
656
+ // Regular expression to match SSH public keys
657
+ const sshKeyRegex = /^ssh-(rsa|ed25519|ecdsa-sha2-nistp[0-9]+) [A-Za-z0-9+/]+[=]{0,3}( [^@]+@[^@]+)?$/;
658
+ return sshKeyRegex.test(sshPublicKey);
659
+ };
660
+
661
+ if (!isValidSSHPublicKey(ssh_key_content)) {
662
+ return { code: -1, data: 'invalid ssh key' };
663
+ }
664
+
665
+ let doc = {
666
+ uid,
667
+ docType: 'ssh_key',
668
+ date_created_ts: Date.now(),
669
+ date_created: Date.now(),
670
+ stat: 3,
671
+ };
672
+
673
+ if (_id) {
674
+ const ret = await db_module.get_couch_doc('xuda_ssh_keys', _id);
675
+ if (ret.code < 0) {
676
+ return ret;
677
+ }
678
+ doc = ret.data;
679
+ doc.date_updated_ts = Date.now();
680
+ } else {
681
+ doc._id = await _common.xuda_get_uuid('ssh_key');
682
+ }
683
+
684
+ doc.ssh_key_name = ssh_key_name;
685
+ doc.ssh_key_content = ssh_key_content;
686
+
687
+ const save_ret = await db_module.save_couch_doc('xuda_ssh_keys', doc);
688
+
689
+ return save_ret;
690
+ };
691
+
692
+ export const delete_ssh_key = async (req) => {
693
+ const { ssh_key_id } = req;
694
+
695
+ const ret = await db_module.get_couch_doc('xuda_ssh_keys', ssh_key_id);
696
+ if (ret.code < 0) {
697
+ return ret;
698
+ }
699
+ let doc = ret.data;
700
+ doc.date_updated_ts = Date.now();
701
+ doc.stat = 4;
702
+
703
+ const save_ret = await db_module.save_couch_doc('xuda_ssh_keys', doc);
704
+
705
+ return save_ret;
706
+ };
707
+
708
+ export const get_ssh_keys = async (req) => {
709
+ const { uid, _id } = req;
710
+
711
+ let opt = {
712
+ selector: { docType: 'ssh_key', uid, stat: { $lt: 4 } },
713
+ };
714
+
715
+ if (_id) {
716
+ opt._id = _id;
717
+ }
718
+
719
+ let ret = await db_module.find_couch_query('xuda_ssh_keys', opt);
720
+ return ret;
721
+ };
722
+
723
+ export const search_users = async (req) => {
724
+ try {
725
+ if (!_conf.superuser_account_ids.includes(req.uid)) {
726
+ throw new Error('user is not authorized for this method');
727
+ }
728
+
729
+ let selector = {
730
+ docType: 'account',
731
+ stat: 3,
732
+ };
733
+
734
+ if (req.search) {
735
+ selector = {
736
+ ...selector,
737
+ $or: [
738
+ { 'account_info.first_name': { $regex: `(?i)${req.search}` } },
739
+ { 'account_info.last_name': { $regex: `(?i)${req.search}` } },
740
+ { 'account_info.company_name': { $regex: `(?i)${req.search}` } },
741
+ { 'account_info.tel': { $regex: `(?i)${req.search}` } },
742
+ { 'account_info.email': { $regex: `(?i)${req.search}` } },
743
+ { 'account_info.address': { $regex: `(?i)${req.search}` } },
744
+ { _id: { $regex: `(?i)${req.search}` } },
745
+ ],
746
+ };
747
+ }
748
+
749
+ const opt = {
750
+ selector,
751
+ limit: req.limit ? req.limit : 99999,
752
+ };
753
+
754
+ if (req.skip) {
755
+ opt.skip = req.skip;
756
+ }
757
+
758
+ if (req?.bookmark !== 'nil') {
759
+ opt.bookmark = req.bookmark;
760
+ }
761
+
762
+ try {
763
+ let ret = await db_module.find_couch_query('xuda_accounts', opt);
764
+ return { code: 1, data: ret };
765
+ } catch (e) {
766
+ return { code: -400, data: e };
767
+ }
768
+ } catch (err) {
769
+ return { code: -1, data: err.message };
770
+ }
771
+ };
772
+
773
+ export const read_emails = async (req) => {
774
+ const { uid, email_account_address, email_account_id, accessToken, vendor } = req;
775
+
776
+ const email_module = require(`${module_path}/email_module`);
777
+
778
+ const read_mailbox = async (mailbox_path, mailbox_name, is_sent) => {
779
+ let emails_ret = await db_module.find_couch_query('xuda_emails', {
780
+ selector: {
781
+ docType: 'email',
782
+ uid,
783
+ email_account_address,
784
+ email_account_id,
785
+ mailbox_path,
786
+ },
787
+ fields: ['message.seq'],
788
+ limit: 1,
789
+ sort: [{ 'message.seq': 'desc' }],
790
+ });
791
+
792
+ let imap_ret;
793
+
794
+ const seq = emails_ret?.docs?.[0]?.message?.seq || 1;
795
+ imap_ret = await email_module.query_imap({
796
+ email_account_address,
797
+ type: 'all',
798
+ boxLock: mailbox_path,
799
+ seq,
800
+ accessToken,
801
+ });
802
+
803
+ let senders = {};
804
+ let recipient = {};
805
+ let docs = [];
806
+
807
+ for await (let message of imap_ret) {
808
+ delete message.modseq;
809
+ if (message.seq > seq) {
810
+ docs.push({
811
+ _id: await _common.xuda_get_uuid('email'),
812
+ docType: 'email',
813
+ uid,
814
+ email_account_address,
815
+ email_account_id,
816
+ message,
817
+ mailbox_path,
818
+ mailbox_name,
819
+ });
820
+ if (is_sent) {
821
+ recipient[message.envelope.to[0].address] = message.envelope.to[0].name;
822
+ } else {
823
+ senders[message.envelope.sender[0].address] = message.envelope.sender[0].name;
824
+ }
825
+ }
826
+ }
827
+
828
+ if (docs.length) {
829
+ await db_module.save_couch_bulk_docs('xuda_emails', docs);
830
+
831
+ for await (let [email, name] of Object.entries(is_sent ? recipient : senders)) {
832
+ await add_contact({ uid, name, email });
833
+ }
834
+ }
835
+ };
836
+
837
+ const mailboxes_ret = await email_module.list_mailboxes({
838
+ email_account_address,
839
+ email_account_id,
840
+ accessToken,
841
+ });
842
+
843
+ for await (let mailbox_obj of mailboxes_ret) {
844
+ switch (vendor) {
845
+ case 'gmail':
846
+ if (!['[Gmail]/Starred', '[Gmail]/Drafts', '[Gmail]/All Mail', '[Gmail]/Spam', '[Gmail]/Trash', '[Gmail]', '[Gmail]/Important'].includes(mailbox_obj.path)) {
847
+ await read_mailbox(mailbox_obj.path, mailbox_obj.name, mailbox_obj.path === '[Gmail]/Sent Mail');
848
+ }
849
+ break;
850
+
851
+ default:
852
+ if (['INBOX', 'Sent Mail'].includes(mailbox_obj.path)) {
853
+ await read_mailbox(mailbox_obj.path, mailbox_obj.name);
854
+ }
855
+ break;
856
+ }
857
+ }
858
+ };
859
+
860
+ export const get_email_accounts = async (req) => {
861
+ const { uid, stat } = req;
862
+
863
+ const db_module = require(`${module_path}/db_module`);
864
+ let email_account_ret = await db_module.find_couch_query('xuda_emails', {
865
+ selector: {
866
+ docType: 'email_account',
867
+ uid,
868
+ stat: stat || 3,
869
+ },
870
+ });
871
+ return { code: 1, data: email_account_ret };
872
+ };
873
+
874
+ const generate_gmail_access_token = async (email_account) => {
875
+ const response = await fetch('https://oauth2.googleapis.com/token', {
876
+ method: 'POST',
877
+ headers: { 'Content-Type': 'application/json' },
878
+ body: JSON.stringify({
879
+ refresh_token: email_account.authorization_data.refresh_token,
880
+ client_id: _conf.gmail.clientId,
881
+ client_secret: _conf.gmail.clientSecret,
882
+ grant_type: 'refresh_token',
883
+ }),
884
+ });
885
+ return await response.json();
886
+ };
887
+
888
+ export const find_contact_duplicates = async (req) => {
889
+ const { uid, name } = req;
890
+ let opt = {
891
+ selector: {
892
+ docType: 'contact',
893
+ stat: { $lt: 4 },
894
+ uid,
895
+ },
896
+ limit: 999999,
897
+ };
898
+
899
+ if (typeof name !== 'undefined') {
900
+ opt.selector.name = name;
901
+ }
902
+ const contact_ret = await db_module.find_couch_query('xuda_contacts', opt);
903
+ let duplicates = {};
904
+ for (let contact of contact_ret.docs) {
905
+ if (!duplicates[contact.name]) {
906
+ duplicates[contact.name] = [];
907
+ }
908
+ if (contact.stat === 3) {
909
+ contact.primary = true;
910
+ }
911
+ duplicates[contact.name].push(contact);
912
+ }
913
+
914
+ // filter results
915
+ for await (let [name, val] of Object.entries(duplicates)) {
916
+ if (val.length < 2) {
917
+ delete duplicates[name];
918
+ }
919
+ }
920
+
921
+ return { code: 1, data: duplicates };
922
+ };
923
+
924
+ export const merge_contact = async (req) => {
925
+ const { uid, name } = req;
926
+ let duplicate_ret = await find_contact_duplicates({
927
+ uid,
928
+ name,
929
+ });
930
+ if (duplicate_ret.code < 0) {
931
+ return duplicate_ret;
932
+ }
933
+
934
+ let duplicate_arr = duplicate_ret?.data?.[name] || [];
935
+ if (!duplicate_arr.length) {
936
+ return { code: 1, data: 'no merge performed' };
937
+ }
938
+ let contact_to_merge;
939
+ let contact_to_delete = [];
940
+ for (let contact of duplicate_arr) {
941
+ if (contact.primary) {
942
+ contact_to_merge = contact._id;
943
+ continue;
944
+ }
945
+ contact_to_delete.push(contact._id);
946
+ }
947
+
948
+ if (!contact_to_merge) {
949
+ // find best match for primary
950
+ for (let [i, contact_id] of Object.entries(contact_to_delete)) {
951
+ let contact_obj = duplicate_arr.find((e) => e._id === contact_id);
952
+
953
+ if (contact_obj.name && !['spam', 'newsletter', 'noreply', 'no-reply'].includes(contact_obj.email?.[0]?.toLowerCase())) {
954
+ contact_to_merge = contact_id;
955
+ contact_to_delete = contact_to_delete.splice(i, 1);
956
+ break;
957
+ }
958
+ }
959
+
960
+ if (!contact_to_merge) {
961
+ contact_to_merge = contact_to_delete[0];
962
+ contact_to_delete = contact_to_delete.slice(1);
963
+ }
964
+ }
965
+
966
+ // get the primary account
967
+ const primary_contact_ret = await db_module.get_couch_doc('xuda_contacts', contact_to_merge);
968
+ if (primary_contact_ret.code < 0) {
969
+ return primary_contact_ret;
970
+ }
971
+ let primary_contact_doc = primary_contact_ret.data;
972
+
973
+ primary_contact_doc.stat_ts = Date.now();
974
+ primary_contact_doc.stat_reason = 'merge';
975
+
976
+ for (let contact_id of contact_to_delete) {
977
+ let email_address = duplicate_arr.find((e) => e._id === contact_id).email[0];
978
+
979
+ primary_contact_doc.email.push(email_address);
980
+ }
981
+
982
+ const primary_contact_save_ret = await db_module.save_couch_doc('xuda_contacts', primary_contact_doc);
983
+
984
+ for (let contact_id of contact_to_delete) {
985
+ const contact_ret = await db_module.get_couch_doc('xuda_contacts', contact_id);
986
+ if (contact_ret.code < 0) {
987
+ continue;
988
+ }
989
+
990
+ let contact_doc = contact_ret.data;
991
+ contact_doc.stat = 4;
992
+ contact_doc.stat_ts = Date.now();
993
+ contact_doc.stat_reason = 'merge';
994
+ await db_module.save_couch_doc('xuda_contacts', contact_doc);
995
+ }
996
+
997
+ return primary_contact_save_ret;
998
+ };
999
+
1000
+ export const get_contacts = async (req) => {
1001
+ const { uid, name, limit, skip, bookmark } = req;
1002
+ let opt = {
1003
+ selector: {
1004
+ docType: 'contact',
1005
+ uid,
1006
+ stat: { $lt: 4 },
1007
+ },
1008
+ limit: limit ? limit : 99999,
1009
+ };
1010
+
1011
+ if (typeof name !== 'undefined') {
1012
+ opt.selector.name = { $regex: `(?i)${name}` };
1013
+ }
1014
+
1015
+ if (skip) {
1016
+ opt.skip = skip;
1017
+ }
1018
+
1019
+ if (bookmark !== 'nil') {
1020
+ opt.bookmark = bookmark;
1021
+ }
1022
+ const contacts_ret = await db_module.find_couch_query('xuda_contacts', opt);
1023
+
1024
+ return contacts_ret;
1025
+ };
1026
+
1027
+ export const delete_contact = async (req) => {
1028
+ const { _id } = req;
1029
+
1030
+ const contact_ret = await db_module.get_couch_doc('xuda_contacts', _id);
1031
+ if (contact_ret.code < 0) {
1032
+ return contact_ret;
1033
+ }
1034
+ let contact_doc = contact_ret.data;
1035
+
1036
+ contact_doc.stat_ts = Date.now();
1037
+ contact_doc.stat_reason = 'deleted';
1038
+
1039
+ const contact_save_ret = await db_module.save_couch_doc('xuda_contacts', contact_doc);
1040
+
1041
+ return contact_save_ret;
1042
+ };
1043
+
1044
+ export const add_contact = async (req) => {
1045
+ const { uid, email, name, stat, contact_uid, req_id } = req;
1046
+ const contact_ret = await db_module.get_couch_view_raw('xuda_contacts', 'contacts_by_email_address', { key: [uid, email] });
1047
+
1048
+ if (!contact_ret.rows.length) {
1049
+ const ret = await db_module.save_couch_doc('xuda_contacts', {
1050
+ _id: await _common.xuda_get_uuid('contact'),
1051
+ email: [email],
1052
+ name,
1053
+ stat: stat || 1,
1054
+ date_created: Date.now(),
1055
+ docType: 'contact',
1056
+ contact_uid,
1057
+ uid,
1058
+ req_id,
1059
+ });
1060
+ return ret;
1061
+ }
1062
+ return { code: -1, data: 'contact already exist active or deleted' };
1063
+ };
1064
+
1065
+ export const update_contact = async (req) => {
1066
+ const { uid, email, contact_uid, stat, req_id } = req;
1067
+ let contact_ret;
1068
+ if (email) {
1069
+ contact_ret = await db_module.get_couch_view_raw('xuda_contacts', 'contacts_by_email_address', { key: [uid, email] });
1070
+ } else if (contact_uid) {
1071
+ contact_ret = await db_module.get_couch_view_raw('xuda_contacts', 'contacts_by_contact_uid', { key: [uid, contact_uid] });
1072
+ }
1073
+ if (contact_ret.rows.length) {
1074
+ let doc = contact_ret.rows[0].value;
1075
+ if (stat) {
1076
+ doc.stat = stat;
1077
+ doc.stat_ts = Date.now();
1078
+ }
1079
+ if (req_id) {
1080
+ doc.req_id = req_id;
1081
+ }
1082
+ return await db_module.save_couch_doc('xuda_contacts', doc);
1083
+ }
1084
+ return { code: -1, data: 'contact not found' };
1085
+ };
1086
+
1087
+ export const invite_user = async (req) => {
1088
+ const { email, name, uid } = req;
1089
+
1090
+ const team_module = require(`${module_path}/team_module`);
1091
+ const ret = await team_module.create_team_request({
1092
+ email,
1093
+ access_type: 'contact',
1094
+ uid,
1095
+ });
1096
+ };
1097
+
1098
+ export const get_account_rt_info = async (uid) => {
1099
+ const doc_ret = await db_module.get_couch_doc('xuda_accounts', uid);
1100
+ if (doc_ret.code < 0) {
1101
+ console.error('get_account_rt_info', doc_ret);
1102
+ return {};
1103
+ }
1104
+ let account_info = doc_ret.data.account_info;
1105
+
1106
+ delete account_info.user_id;
1107
+
1108
+ account_info.uid = doc_ret.data._id;
1109
+ return account_info;
1110
+ };
1111
+
1112
+ const get_prices = (uid, item) => {
1113
+ let price;
1114
+
1115
+ switch (item) {
1116
+ case 'user_group':
1117
+ case 'pwa':
1118
+ case 'preview':
1119
+ case 'android':
1120
+ case 'ios':
1121
+ case 'macos':
1122
+ case 'emitter':
1123
+ price = _conf.PRICE_OBJ.deployments[item];
1124
+ break;
1125
+
1126
+ case 'drive_ocr':
1127
+ price = _conf.PRICE_OBJ.drive_addons.ocr.price;
1128
+ break;
1129
+
1130
+ case 'drive_edit_image':
1131
+ price = _conf.PRICE_OBJ.drive_addons.edit_image.price;
1132
+ break;
1133
+
1134
+ case 'auto_backup':
1135
+ price = _conf.PRICE_OBJ.auto_backup_per_month;
1136
+ break;
1137
+
1138
+ case 'deployment_offline':
1139
+ price = _conf.PRICE_OBJ.deployment_offline_per_month;
1140
+ break;
1141
+
1142
+ case 'xuda_subdomain':
1143
+ price = _conf.PRICE_OBJ.xuda_subdomain_per_month;
1144
+ break;
1145
+
1146
+ case 'custom_domain':
1147
+ price = _conf.PRICE_OBJ.custom_domain_per_month;
1148
+ break;
1149
+
1150
+ case 'user_monitor':
1151
+ price = _conf.PRICE_OBJ.user_assist_per_month;
1152
+ break;
1153
+
1154
+ case 'branding':
1155
+ price = _conf.PRICE_OBJ.branding_per_month;
1156
+ break;
1157
+
1158
+ case 'utility_screen':
1159
+ price = _conf.PRICE_OBJ.utility_screen_per_month;
1160
+ break;
1161
+
1162
+ default:
1163
+ price = 0;
1164
+ break;
1165
+ }
1166
+
1167
+ if (_conf.PRICE_OBJ.server_slugs[item]) {
1168
+ price = _conf.PRICE_OBJ.server_slugs[item].price;
1169
+ }
1170
+
1171
+ return price;
1172
+ };
1173
+
1174
+ export const validate_account_topup = async (uid, items = []) => {
1175
+ const stripe_module = require(`${module_path}/stripe_module`);
1176
+
1177
+ const { code, data } = await stripe_module.get_billing_metrics({ uid });
1178
+ if (code < 0) {
1179
+ throw new Error(data);
1180
+ }
1181
+ let topup = 0;
1182
+ for (const item of items) {
1183
+ topup += get_prices(uid, item);
1184
+ }
1185
+
1186
+ if (!data?.customer_obj?.default_source) {
1187
+ let err = new Error('no card on file');
1188
+ err.code = -90;
1189
+ err.billing_problem = true;
1190
+ throw err;
1191
+ }
1192
+
1193
+ if (data?.balance?.past_due) {
1194
+ let err = new Error(`past_due ${data.balance.past_due}`);
1195
+ err.code = -91;
1196
+ err.billing_problem = true;
1197
+ err.topup = topup + data.balance.past_due;
1198
+ err.open_invoices = data?.open_invoices;
1199
+ throw err;
1200
+ }
1201
+
1202
+ const balance = data?.balance?.account || 0; // minus balance represents positive balance
1203
+
1204
+ if (balance > 0) {
1205
+ let err = new Error(`negative balance ${-balance}`);
1206
+ err.code = -92;
1207
+ err.billing_problem = true;
1208
+ err.topup = topup - balance; // ask for whole balance
1209
+ throw err;
1210
+ }
1211
+
1212
+ if (topup + balance > 0) {
1213
+ let err = new Error(`not enough balance ${topup + balance}`);
1214
+ err.code = -93;
1215
+ err.billing_problem = true;
1216
+ err.topup = topup;
1217
+ throw err;
1218
+ }
1219
+
1220
+ return true;
1221
+ };
1222
+
1223
+ export const create_account_oauth_link = async (req, job_id) => {
1224
+ const link = `https://accounts.google.com/o/oauth2/v2/auth?scope=https://mail.google.com/&access_type=offline&include_granted_scopes=true&response_type=code&state=${req.uid}&redirect_uri=https://${_conf.is_debug ? 'dev.' : ''}xuda.io/oauth&client_id=${_conf.gmail.clientId}`;
1225
+ await jobs_module.update_job(
1226
+ {
1227
+ job_id,
1228
+ current_step_name: 'creating link',
1229
+ response: link,
1230
+ },
1231
+ global[`_account_module_ch`],
1232
+ );
1233
+ await _utils.delay(20000);
1234
+ // debugger;
1235
+ return {
1236
+ code: 1,
1237
+ data: link,
1238
+ };
1239
+ };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@xuda.io/account_module",
3
- "version": "1.2.144",
3
+ "version": "1.2.146",
4
4
  "description": "Xuda Account Server Module",
5
- "main": "index.js",
5
+ "main": "index.mjs",
6
6
  "dependencies": {
7
7
  "lodash": "^4.17.21",
8
8
  "validator": "^13.1.1"
File without changes