dbgate-api-premium 6.5.4 → 6.5.6

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dbgate-api-premium",
3
3
  "main": "src/index.js",
4
- "version": "6.5.4",
4
+ "version": "6.5.6",
5
5
  "homepage": "https://dbgate.org/",
6
6
  "repository": {
7
7
  "type": "git",
@@ -30,10 +30,10 @@
30
30
  "compare-versions": "^3.6.0",
31
31
  "cors": "^2.8.5",
32
32
  "cross-env": "^6.0.3",
33
- "dbgate-datalib": "^6.5.4",
33
+ "dbgate-datalib": "^6.5.6",
34
34
  "dbgate-query-splitter": "^4.11.5",
35
- "dbgate-sqltree": "^6.5.4",
36
- "dbgate-tools": "^6.5.4",
35
+ "dbgate-sqltree": "^6.5.6",
36
+ "dbgate-tools": "^6.5.6",
37
37
  "debug": "^4.3.4",
38
38
  "diff": "^5.0.0",
39
39
  "diff2html": "^3.4.13",
@@ -85,7 +85,7 @@
85
85
  "devDependencies": {
86
86
  "@types/fs-extra": "^9.0.11",
87
87
  "@types/lodash": "^4.14.149",
88
- "dbgate-types": "^6.5.4",
88
+ "dbgate-types": "^6.5.6",
89
89
  "env-cmd": "^10.1.0",
90
90
  "jsdoc-to-markdown": "^9.0.5",
91
91
  "node-loader": "^1.0.2",
@@ -11,7 +11,7 @@ const logger = getLogger('authProvider');
11
11
  class AuthProviderBase {
12
12
  amoid = 'none';
13
13
 
14
- async login(login, password, options = undefined) {
14
+ async login(login, password, options = undefined, req = undefined) {
15
15
  return {
16
16
  accessToken: jwt.sign(
17
17
  {
@@ -23,7 +23,7 @@ class AuthProviderBase {
23
23
  };
24
24
  }
25
25
 
26
- oauthToken(params) {
26
+ oauthToken(params, req) {
27
27
  return {};
28
28
  }
29
29
 
@@ -21,6 +21,8 @@ const axios = require('axios');
21
21
  const _ = require('lodash');
22
22
  const { authProxyGetTokenFromCode, authProxyGetRedirectUrl } = require('../utility/authProxy');
23
23
  const { decryptUser } = require('../utility/crypting');
24
+ const { sendToAuditLog } = require('../utility/auditlog');
25
+ const { isLoginLicensed, LOGIN_LIMIT_ERROR, markTokenAsLoggedIn } = require('../utility/loginchecker');
24
26
 
25
27
  async function loadPermissionsForUserId(userId) {
26
28
  const rolePermissions = sortPermissionsFromTheSameLevel(await storageReadUserRolePermissions(userId));
@@ -50,21 +52,39 @@ class StorageProviderBase extends AuthProviderBase {
50
52
  }
51
53
 
52
54
  class AnonymousProvider extends StorageProviderBase {
53
- async login(login, password, options = undefined) {
55
+ async login(login, password, options = undefined, req = undefined) {
54
56
  const permissions = await [
55
57
  ...getPredefinedPermissions('anonymous-user'),
56
58
  ...(await storageReadRolePermissions(-1)),
57
59
  ];
58
60
 
61
+ if (!(await isLoginLicensed(req, `anonymous`))) {
62
+ return { error: LOGIN_LIMIT_ERROR };
63
+ }
64
+
65
+ sendToAuditLog(req, {
66
+ category: 'auth',
67
+ component: 'AnonymousProvider',
68
+ action: 'login',
69
+ event: 'login.anonymous',
70
+ severity: 'info',
71
+ message: 'Anonymous login',
72
+ });
73
+
74
+ const licenseUid = 'anonymous';
75
+ const accessToken = jwt.sign(
76
+ {
77
+ amoid: this.amoid,
78
+ permissions,
79
+ licenseUid,
80
+ },
81
+ getTokenSecret(),
82
+ { expiresIn: getTokenLifetime() }
83
+ );
84
+ markTokenAsLoggedIn(licenseUid, accessToken);
85
+
59
86
  return {
60
- accessToken: jwt.sign(
61
- {
62
- amoid: this.amoid,
63
- permissions,
64
- },
65
- getTokenSecret(),
66
- { expiresIn: getTokenLifetime() }
67
- ),
87
+ accessToken,
68
88
  };
69
89
  }
70
90
  }
@@ -73,7 +93,7 @@ class LocalAuthProvider extends StorageProviderBase {
73
93
  constructor(config) {
74
94
  super(config);
75
95
  }
76
- async login(login, password) {
96
+ async login(login, password, _options, req) {
77
97
  const rows = await storageSelectFmt('select * from ~users where login = %v', login);
78
98
  if (rows.length == 0) {
79
99
  return { error: 'Login not allowed' };
@@ -83,19 +103,49 @@ class LocalAuthProvider extends StorageProviderBase {
83
103
  const userId = row.id;
84
104
  const permissions = await loadPermissionsForUserId(userId);
85
105
 
106
+ if (!(await isLoginLicensed(req, `local:${login}`))) {
107
+ return { error: LOGIN_LIMIT_ERROR };
108
+ }
109
+
110
+ sendToAuditLog(req, {
111
+ category: 'auth',
112
+ component: 'LocalAuthProvider',
113
+ action: 'login',
114
+ event: 'login.local',
115
+ severity: 'info',
116
+ detail: { login },
117
+ message: 'Local login successful',
118
+ });
119
+
120
+ const licenseUid = `local:${login}`;
121
+ const accessToken = jwt.sign(
122
+ {
123
+ amoid: this.amoid,
124
+ login,
125
+ permissions,
126
+ userId,
127
+ licenseUid,
128
+ },
129
+ getTokenSecret(),
130
+ { expiresIn: getTokenLifetime() }
131
+ );
132
+ markTokenAsLoggedIn(licenseUid, accessToken);
133
+
86
134
  return {
87
- accessToken: jwt.sign(
88
- {
89
- amoid: this.amoid,
90
- login,
91
- permissions,
92
- userId,
93
- },
94
- getTokenSecret(),
95
- { expiresIn: getTokenLifetime() }
96
- ),
135
+ accessToken,
97
136
  };
98
137
  }
138
+
139
+ sendToAuditLog(req, {
140
+ category: 'auth',
141
+ component: 'LocalAuthProvider',
142
+ action: 'loginFail',
143
+ event: 'login.localFailed',
144
+ severity: 'warn',
145
+ detail: { login },
146
+ message: 'Local login failed',
147
+ });
148
+
99
149
  return { error: 'Invalid credentials' };
100
150
  }
101
151
 
@@ -121,7 +171,7 @@ class OauthProvider extends StorageProviderBase {
121
171
  return this.config.oauthLogout;
122
172
  }
123
173
 
124
- async oauthToken(params) {
174
+ async oauthToken(params, req) {
125
175
  const { redirectUri, code } = params;
126
176
 
127
177
  const scopeParam = this.config.oauthScope ? `&scope=${this.config.oauthScope}` : '';
@@ -151,6 +201,16 @@ class OauthProvider extends StorageProviderBase {
151
201
  const permissions = await loadPermissionsForUserId(loginRows[0]?.id ?? -1);
152
202
 
153
203
  if (this.config.oauthOnlyDefinedLogins == 1 && loginRows.length == 0) {
204
+ sendToAuditLog(req, {
205
+ category: 'auth',
206
+ component: 'OauthProvider',
207
+ action: 'loginFail',
208
+ event: 'login.oauthNotAllowed',
209
+ severity: 'warn',
210
+ detail: { login },
211
+ message: `Username ${login} not allowed to log in`,
212
+ });
213
+
154
214
  return { error: `Username ${login} not allowed to log in` };
155
215
  }
156
216
 
@@ -171,21 +231,50 @@ class OauthProvider extends StorageProviderBase {
171
231
  if (this.config.oauthOnlyDefinedGroups == 1) {
172
232
  const roleRows = await storageSelectFmt('select * from ~roles where ~name in (%,v)', groups);
173
233
  if (roleRows.length == 0) {
234
+ sendToAuditLog(req, {
235
+ category: 'auth',
236
+ component: 'OauthProvider',
237
+ action: 'loginFail',
238
+ event: 'login.groupNotAllowed',
239
+ severity: 'warn',
240
+ detail: { login },
241
+ message: `Groups ${groups.join(', ')} not allowed to log in`,
242
+ });
243
+
174
244
  return { error: `Groups ${groups.join(', ')} not allowed to log in` };
175
245
  }
176
246
  }
177
247
 
248
+ if (!(await isLoginLicensed(req, `oauth:${login}`))) {
249
+ return { error: LOGIN_LIMIT_ERROR };
250
+ }
251
+
252
+ sendToAuditLog(req, {
253
+ category: 'auth',
254
+ component: 'OauthProvider',
255
+ action: 'login',
256
+ event: 'login.oauth',
257
+ severity: 'info',
258
+ detail: { login, userId: loginRows[0]?.id },
259
+ message: `User ${login} logged in via OAUTH`,
260
+ });
261
+
262
+ const licenseUid = `oauth:${login}`;
263
+ const accessToken = jwt.sign(
264
+ {
265
+ amoid: this.amoid,
266
+ login,
267
+ permissions,
268
+ userId: loginRows[0]?.id,
269
+ licenseUid,
270
+ },
271
+ getTokenSecret(),
272
+ { expiresIn: getTokenLifetime() }
273
+ );
274
+ markTokenAsLoggedIn(licenseUid, accessToken);
275
+
178
276
  return {
179
- accessToken: jwt.sign(
180
- {
181
- amoid: this.amoid,
182
- login,
183
- permissions,
184
- userId: loginRows[0]?.id,
185
- },
186
- getTokenSecret(),
187
- { expiresIn: getTokenLifetime() }
188
- ),
277
+ accessToken,
189
278
  };
190
279
  }
191
280
 
@@ -214,7 +303,7 @@ class ADProvider extends StorageProviderBase {
214
303
  super(config);
215
304
  }
216
305
 
217
- async login(login, password) {
306
+ async login(login, password, _options, req) {
218
307
  const adConfig = {
219
308
  url: this.config.adUrl,
220
309
  baseDN: this.config.adBaseDN,
@@ -225,6 +314,16 @@ class ADProvider extends StorageProviderBase {
225
314
  try {
226
315
  const res = await ad.authenticate(login, password);
227
316
  if (!res) {
317
+ sendToAuditLog(req, {
318
+ category: 'auth',
319
+ component: 'OauthProvider',
320
+ action: 'loginFail',
321
+ event: 'login.adRejected',
322
+ severity: 'warn',
323
+ detail: { login },
324
+ message: `AD rejected login for ${login}`,
325
+ });
326
+
228
327
  return { error: `Login failed, AD rejected login ${login}` };
229
328
  }
230
329
 
@@ -237,18 +336,47 @@ class ADProvider extends StorageProviderBase {
237
336
  }
238
337
  }
239
338
 
339
+ if (!(await isLoginLicensed(req, `ad:${login}`))) {
340
+ return { error: LOGIN_LIMIT_ERROR };
341
+ }
342
+
343
+ sendToAuditLog(req, {
344
+ category: 'auth',
345
+ component: 'ADProvider',
346
+ action: 'login',
347
+ event: 'login.ad',
348
+ severity: 'info',
349
+ detail: { login },
350
+ message: `User ${login} logged in via AD`,
351
+ });
352
+
353
+ const licenseUid = `ad:${login}`;
354
+ const accessToken = jwt.sign(
355
+ {
356
+ amoid: this.amoid,
357
+ login,
358
+ permissions,
359
+ licenseUid,
360
+ },
361
+ getTokenSecret(),
362
+ { expiresIn: getTokenLifetime() }
363
+ );
364
+ markTokenAsLoggedIn(licenseUid, accessToken);
365
+
240
366
  return {
241
- accessToken: jwt.sign(
242
- {
243
- amoid: this.amoid,
244
- login,
245
- permissions,
246
- },
247
- getTokenSecret(),
248
- { expiresIn: getTokenLifetime() }
249
- ),
367
+ accessToken,
250
368
  };
251
369
  } catch (e) {
370
+ sendToAuditLog(req, {
371
+ category: 'auth',
372
+ component: 'OauthProvider',
373
+ action: 'loginFail',
374
+ event: 'login.adError',
375
+ severity: 'warn',
376
+ detail: { login, error: e.message },
377
+ message: `AD login error for ${login}`,
378
+ });
379
+
252
380
  return { error: `Login failed: ${e.message}` };
253
381
  }
254
382
  }
@@ -276,26 +404,56 @@ class DatabaseProvider extends StorageProviderBase {
276
404
  }));
277
405
  }
278
406
 
279
- async login(login, password, options) {
407
+ async login(login, password, options, req) {
280
408
  const { conid } = options;
281
409
  const rows = login ? await storageSelectFmt('select * from ~users where ~login = %v', login) : [];
282
410
  if (this.config.dbloginOnlyDefinedLogins == 1 && rows.length == 0) {
283
- return { error: 'Login not allowed' };
411
+ sendToAuditLog(req, {
412
+ category: 'auth',
413
+ component: 'DatabaseProvider',
414
+ action: 'loginFail',
415
+ event: 'login.dbLoginNotAllowed',
416
+ severity: 'warn',
417
+ detail: { login },
418
+ message: `Login ${login} not allowed to log in`,
419
+ });
420
+
421
+ return { error: 'Login not allowed', login };
284
422
  }
285
423
  const userId = rows[0]?.id;
286
424
  const permissions = await loadPermissionsForUserId(userId ?? -1);
425
+
426
+ if (!(await isLoginLicensed(req, `db:${login}`))) {
427
+ return { error: LOGIN_LIMIT_ERROR };
428
+ }
429
+
430
+ sendToAuditLog(req, {
431
+ category: 'auth',
432
+ component: 'DatabaseProvider',
433
+ action: 'login',
434
+ event: 'login.dbLogin',
435
+ severity: 'info',
436
+ detail: { login },
437
+ message: `User ${login} logged in via database`,
438
+ });
439
+
440
+ const licenseUid = `db:${login}`;
441
+ const accessToken = jwt.sign(
442
+ {
443
+ amoid: this.amoid,
444
+ login,
445
+ permissions,
446
+ userId,
447
+ conid,
448
+ licenseUid,
449
+ },
450
+ getTokenSecret(),
451
+ { expiresIn: getTokenLifetime() }
452
+ );
453
+ markTokenAsLoggedIn(licenseUid, accessToken);
454
+
287
455
  return {
288
- accessToken: jwt.sign(
289
- {
290
- amoid: this.amoid,
291
- login,
292
- permissions,
293
- userId,
294
- conid,
295
- },
296
- getTokenSecret(),
297
- { expiresIn: getTokenLifetime() }
298
- ),
456
+ accessToken,
299
457
  };
300
458
  }
301
459
 
@@ -320,7 +478,7 @@ class MsEntraProvider extends StorageProviderBase {
320
478
  return null;
321
479
  }
322
480
 
323
- async oauthToken(params) {
481
+ async oauthToken(params, req) {
324
482
  const { sid, code } = params;
325
483
 
326
484
  const token = await authProxyGetTokenFromCode({ sid, code });
@@ -336,26 +494,65 @@ class MsEntraProvider extends StorageProviderBase {
336
494
 
337
495
  if (this.config.msentraOnlyDefinedLogins == 1) {
338
496
  if (loginRows.length == 0) {
497
+ sendToAuditLog(req, {
498
+ category: 'auth',
499
+ component: 'MsEntraProvider',
500
+ action: 'loginFail',
501
+ event: 'login.msentraNotAllowed',
502
+ severity: 'warn',
503
+ detail: { email },
504
+ message: `Email ${email} not allowed to log in`,
505
+ });
506
+
339
507
  return { error: `Email ${email} not allowed to log in` };
340
508
  }
341
509
  }
342
510
 
343
511
  if (token) {
512
+ if (!(await isLoginLicensed(req, `msentra:${payload.unique_name}`))) {
513
+ return { error: LOGIN_LIMIT_ERROR };
514
+ }
515
+
516
+ sendToAuditLog(req, {
517
+ category: 'auth',
518
+ component: 'MsEntraProvider',
519
+ action: 'login',
520
+ event: 'login.msentra',
521
+ severity: 'info',
522
+ detail: { login: payload.unique_name, userId: loginRows[0]?.id },
523
+ message: `User ${payload.unique_name} logged in via MS Entra`,
524
+ });
525
+
526
+ const licenseUid = `msentra:${payload.unique_name}`;
527
+ const accessToken = jwt.sign(
528
+ {
529
+ amoid: this.amoid,
530
+ permissions,
531
+ msentraToken: token,
532
+ userId: loginRows[0]?.id,
533
+ login: payload.unique_name,
534
+ licenseUid,
535
+ },
536
+ getTokenSecret(),
537
+ { expiresIn: getTokenLifetime() }
538
+ );
539
+ markTokenAsLoggedIn(licenseUid, accessToken);
540
+
344
541
  return {
345
- accessToken: jwt.sign(
346
- {
347
- amoid: this.amoid,
348
- permissions,
349
- msentraToken: token,
350
- userId: loginRows[0]?.id,
351
- login: payload.unique_name,
352
- },
353
- getTokenSecret(),
354
- { expiresIn: getTokenLifetime() }
355
- ),
542
+ accessToken,
356
543
  };
357
544
  }
358
545
 
546
+ sendToAuditLog(req, {
547
+ category: 'auth',
548
+ component: 'MsEntraProvider',
549
+ action: 'loginFail',
550
+ event: 'login.msentraNotFound',
551
+ severity: 'warn',
552
+ detail: { email },
553
+ message: `Token not found for email ${email}`,
554
+ });
555
+
359
556
  return { error: 'Token not found' };
360
557
  }
361
558
 
@@ -13,8 +13,21 @@ const {
13
13
  } = require('../auth/authProvider');
14
14
  const storage = require('./storage');
15
15
  const { decryptPasswordString } = require('../utility/crypting');
16
- const { createDbGateIdentitySession, startCloudTokenChecking } = require('../utility/cloudIntf');
16
+ const {
17
+ createDbGateIdentitySession,
18
+ startCloudTokenChecking,
19
+ readCloudTokenHolder,
20
+ readCloudTestTokenHolder,
21
+ } = require('../utility/cloudIntf');
17
22
  const socket = require('../utility/socket');
23
+ const { sendToAuditLog } = require('../utility/auditlog');
24
+ const {
25
+ isLoginLicensed,
26
+ LOGIN_LIMIT_ERROR,
27
+ markTokenAsLoggedIn,
28
+ markUserAsActive,
29
+ markLoginAsLoggedOut,
30
+ } = require('../utility/loginchecker');
18
31
 
19
32
  const logger = getLogger('auth');
20
33
 
@@ -54,6 +67,11 @@ function authMiddleware(req, res, next) {
54
67
 
55
68
  // const isAdminPage = req.headers['x-is-admin-page'] == 'true';
56
69
 
70
+ if (process.env.SKIP_ALL_AUTH) {
71
+ // API is not authorized for basic auth
72
+ return next();
73
+ }
74
+
57
75
  if (process.env.BASIC_AUTH) {
58
76
  // API is not authorized for basic auth
59
77
  return next();
@@ -72,6 +90,8 @@ function authMiddleware(req, res, next) {
72
90
  try {
73
91
  const decoded = jwt.verify(token, getTokenSecret());
74
92
  req.user = decoded;
93
+ markUserAsActive(decoded.licenseUid, token);
94
+
75
95
  return next();
76
96
  } catch (err) {
77
97
  if (skipAuth) {
@@ -87,12 +107,12 @@ function authMiddleware(req, res, next) {
87
107
 
88
108
  module.exports = {
89
109
  oauthToken_meta: true,
90
- async oauthToken(params) {
110
+ async oauthToken(params, req) {
91
111
  const { amoid } = params;
92
- return getAuthProviderById(amoid).oauthToken(params);
112
+ return getAuthProviderById(amoid).oauthToken(params, req);
93
113
  },
94
114
  login_meta: true,
95
- async login(params) {
115
+ async login(params, req) {
96
116
  const { amoid, login, password, isAdminPage } = params;
97
117
 
98
118
  if (isAdminPage) {
@@ -102,25 +122,52 @@ module.exports = {
102
122
  adminPassword = decryptPasswordString(adminConfig?.adminPassword);
103
123
  }
104
124
  if (adminPassword && adminPassword == password) {
125
+ if (!(await isLoginLicensed(req, `superadmin`))) {
126
+ return { error: LOGIN_LIMIT_ERROR };
127
+ }
128
+
129
+ sendToAuditLog(req, {
130
+ category: 'auth',
131
+ component: 'AuthController',
132
+ action: 'login',
133
+ event: 'login.admin',
134
+ severity: 'info',
135
+ message: 'Administration login successful',
136
+ });
137
+
138
+ const licenseUid = `superadmin`;
139
+ const accessToken = jwt.sign(
140
+ {
141
+ login: 'superadmin',
142
+ permissions: await storage.loadSuperadminPermissions(),
143
+ roleId: -3,
144
+ licenseUid,
145
+ },
146
+ getTokenSecret(),
147
+ {
148
+ expiresIn: getTokenLifetime(),
149
+ }
150
+ );
151
+ markTokenAsLoggedIn(licenseUid, accessToken);
152
+
105
153
  return {
106
- accessToken: jwt.sign(
107
- {
108
- login: 'superadmin',
109
- permissions: await storage.loadSuperadminPermissions(),
110
- roleId: -3,
111
- },
112
- getTokenSecret(),
113
- {
114
- expiresIn: getTokenLifetime(),
115
- }
116
- ),
154
+ accessToken,
117
155
  };
118
156
  }
119
157
 
158
+ sendToAuditLog(req, {
159
+ category: 'auth',
160
+ component: 'AuthController',
161
+ action: 'loginFail',
162
+ event: 'login.adminFailed',
163
+ severity: 'warn',
164
+ message: 'Administraton login failed',
165
+ });
166
+
120
167
  return { error: 'Login failed' };
121
168
  }
122
169
 
123
- return getAuthProviderById(amoid).login(login, password);
170
+ return getAuthProviderById(amoid).login(login, password, undefined, req);
124
171
  },
125
172
 
126
173
  getProviders_meta: true,
@@ -138,8 +185,8 @@ module.exports = {
138
185
  },
139
186
 
140
187
  createCloudLoginSession_meta: true,
141
- async createCloudLoginSession({ client }) {
142
- const res = await createDbGateIdentitySession(client);
188
+ async createCloudLoginSession({ client, redirectUri }) {
189
+ const res = await createDbGateIdentitySession(client, redirectUri);
143
190
  startCloudTokenChecking(res.sid, tokenHolder => {
144
191
  socket.emit('got-cloud-token', tokenHolder);
145
192
  socket.emitChanged('cloud-content-changed');
@@ -148,5 +195,29 @@ module.exports = {
148
195
  return res;
149
196
  },
150
197
 
198
+ cloudLoginRedirected_meta: true,
199
+ async cloudLoginRedirected({ sid }) {
200
+ const tokenHolder = await readCloudTokenHolder(sid);
201
+ return tokenHolder;
202
+ },
203
+
204
+ cloudTestLogin_meta: true,
205
+ async cloudTestLogin({ email }) {
206
+ const tokenHolder = await readCloudTestTokenHolder(email);
207
+ return tokenHolder;
208
+ },
209
+
210
+ logoutAdmin_meta: true,
211
+ async logoutAdmin() {
212
+ await markLoginAsLoggedOut('superadmin');
213
+ return true;
214
+ },
215
+
216
+ logoutUser_meta: true,
217
+ async logoutUser({}, req) {
218
+ await markLoginAsLoggedOut(req?.user?.licenseUid);
219
+ return true;
220
+ },
221
+
151
222
  authMiddleware,
152
223
  };
@@ -258,4 +258,22 @@ module.exports = {
258
258
  await fs.writeFile(filePath, content);
259
259
  return true;
260
260
  },
261
+
262
+ folderUsers_meta: true,
263
+ async folderUsers({ folid }) {
264
+ const resp = await callCloudApiGet(`content-folders/users/${folid}`);
265
+ return resp;
266
+ },
267
+
268
+ setFolderUserRole_meta: true,
269
+ async setFolderUserRole({ folid, email, role }) {
270
+ const resp = await callCloudApiPost(`content-folders/set-user-role/${folid}`, { email, role });
271
+ return resp;
272
+ },
273
+
274
+ removeFolderUser_meta: true,
275
+ async removeFolderUser({ folid, email }) {
276
+ const resp = await callCloudApiPost(`content-folders/remove-user/${folid}`, { email });
277
+ return resp;
278
+ },
261
279
  };