• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

GoodDollar / GoodServer / 13051048639

30 Jan 2025 11:08AM UTC coverage: 49.666% (+0.09%) from 49.574%
13051048639

push

github

web-flow
Age check (#485)

* add: age check

* add: dont allow faceid deletion, wait until expiration

* fix: missing imports

584 of 1457 branches covered (40.08%)

Branch coverage included in aggregate %.

21 of 22 new or added lines in 3 files covered. (95.45%)

15 existing lines in 2 files now uncovered.

1867 of 3478 relevant lines covered (53.68%)

8.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

6.36
/src/server/storage/storageAPI.js
1
// @flow
2
import moment from 'moment'
3
import { Router } from 'express'
4
import passport from 'passport'
5
import fetch from 'cross-fetch'
6
import { first, get, toLower, values } from 'lodash'
7
import { sha3, toChecksumAddress } from 'web3-utils'
8

9
import { type StorageAPI, UserRecord } from '../../imports/types'
10
import { wrapAsync, onlyInEnv } from '../utils/helpers'
11
import { withTimeout } from '../utils/async'
12
import OnGage from '../crm/ongage'
13
import conf from '../server.config'
14
import { addUserToWhiteList, createCRMRecord } from './addUserSteps'
15
import createUserVerifier from './verifier'
16
import stakingModelTasks from '../blockchain/stakingModelTasks'
17
import { cancelDisposalTask, getDisposalTask } from '../verification/cron/taskUtil'
18
import createEnrollmentProcessor from '../verification/processor/EnrollmentProcessor'
19
import requestRateLimiter from '../utils/requestRateLimiter'
20
import { default as AdminWallet } from '../blockchain/MultiWallet'
21
import Logger from '../../imports/logger'
22

23
const { fishManager } = stakingModelTasks
2✔
24

25
const deleteFromAnalytics = (userId, walletAddress, log) => {
2✔
26
  const amplitudePromise = fetch(`https://amplitude.com/api/2/deletions/users`, {
×
27
    headers: { Authorization: `Basic ${conf.amplitudeBasicAuth}`, 'Content-Type': 'application/json' },
28
    method: 'POST',
29

30
    body: JSON.stringify({
31
      user_ids: [toChecksumAddress(userId.toLowerCase())], //amplitude id is case sensitive and is the original address form from user wallet
32
      delete_from_org: 'true',
33
      ignore_invalid_id: 'true'
34
    })
35
  })
36
    .then(_ => _.text())
×
37
    .then(_ => {
38
      log.info('amplitude delete user result', { result: _ })
×
39
      return {
×
40
        amplitude: 'ok'
41
      }
42
    })
43
    .catch(e => {
×
44
      log.warn('amplitude delete user failed', e.message, e, { userId, walletAddress })
45
      return {
×
46
        amplitude: 'failed'
47
      }
48
    })
2✔
49

×
50
  return [amplitudePromise]
51
}
×
52

×
53
const adminAuthenticate = (req, res, next) => {
54
  const { password } = req.body || {}
55

×
56
  if (password !== conf.adminPassword) {
57
    return res.json({ ok: 0 })
58
  }
2✔
59

2✔
60
  next()
61
}
62

63
const setup = (app: Router, storage: StorageAPI) => {
64
  app.use(
×
65
    ['/user/*'],
×
66
    passport.authenticate('jwt', { session: false }),
×
67
    requestRateLimiter(20, 1, 'user'),
68
    wrapAsync(async (req, res, next) => {
×
69
      const { user, body, log } = req
70
      const { loggedInAs } = user
×
71
      const identifier = get(body, 'user.identifier', loggedInAs)
×
72

×
73
      log.trace(`/user/* ${req.baseUrl} auth:`, { user, body })
×
74

75
      if (loggedInAs !== identifier) {
76
        log.warn(`Trying to update other user data! ${loggedInAs}!==${identifier}`)
77
        throw new Error(`Trying to update other user data! ${loggedInAs}!==${identifier}`)
78
      } else next()
79
    })
80
  )
81

82
  /**
83
   * @api {post} /user/add Add user account
84
   * @apiName Add
85
   * @apiGroup Storage
86
   *
87
   * @apiParam {Object} user
2✔
88
   *
89
   * @apiSuccess {Number} ok
90
   * @ignore
×
91
   */
×
92
  app.post(
×
93
    '/user/add',
×
94
    wrapAsync(async (req, res) => {
×
95
      const { env, skipEmailVerification, disableFaceVerification, optionalMobile } = conf
96
      const isNonDevelopMode = env !== 'development'
×
97
      const { cookies, body, log: logger, user: userRecord } = req
×
98
      const { user: userPayload = {} } = body
×
99
      const { __utmzz: utmString = '' } = cookies
×
100

101
      try {
×
102
        logger.debug('new user request:', { data: userPayload, userRecord })
103
        let { email } = userPayload
104
        email = email.toLowerCase()
105

×
106
        const { mobile, inviteCode, fullName, regMethod, torusProvider } = userPayload
107

108
        // if torus, then we first verify the user mobile/email by verifying it matches the torus public key
×
109
        // (torus maps identifier such as email and mobile to private/public key pairs)
110
        const verifier = createUserVerifier(userRecord, userPayload, logger)
×
111

×
112
        //this modifies userRecord with smsValidated/isEmailConfirmed
×
113
        const { emailVerified, mobileVerified } = await verifier.verifySignInIdentifiers()
×
114

×
115
        const isEmailTorusVerified = email && emailVerified
×
116
        const isEmailManuallyVerified = email && userRecord.isEmailConfirmed && userRecord.email === sha3(email)
117
        const isEmailConfirmed = !!('development' === env || isEmailTorusVerified || isEmailManuallyVerified)
×
118
        const isMobileTorusVerified = mobile && mobileVerified
119
        const isMobileManuallyVerified = mobile && userRecord.smsValidated && userRecord.mobile === sha3(mobile)
120
        const isMobileConfirmed = !!('development' === env || isMobileTorusVerified || isMobileManuallyVerified)
121

122
        logger.debug('new user verification result:', {
123
          env,
124
          mobile,
125
          email,
126
          isEmailTorusVerified,
127
          isEmailManuallyVerified,
128
          isMobileTorusVerified,
129
          isMobileManuallyVerified,
130
          isEmailConfirmed,
×
131
          isMobileConfirmed
×
132
        })
×
133
        // check that user email/mobile sent is the same as the ones verified
134
        //in case email/mobile was verified using torus userRecord.mobile/email will be empty
135
        if (['production', 'staging'].includes(env)) {
×
136
          if (optionalMobile === false && isMobileConfirmed === false) {
×
137
            throw new Error('User mobile not verified!')
138
          }
139

140
          if (skipEmailVerification === false && isEmailConfirmed === false) {
×
141
            throw new Error('User email not verified!')
×
142
          }
143
        }
×
144

×
145
        userRecord.isEmailConfirmed = isEmailConfirmed
146
        userRecord.smsValidated = isMobileConfirmed
147

148
        if (userRecord.createdDate) {
×
149
          logger.warn('user already created', { userRecord, userPayload })
150
          // return res.json({ ok: 1 })
151
        }
152

×
153
        const toUpdateUser: UserRecord = {
×
154
          identifier: userRecord.loggedInAs,
155
          regMethod,
156
          torusProvider,
157
          email: email ? sha3(email) : userRecord.email,
×
158
          mobile: mobile ? sha3(mobile) : userRecord.mobile,
159
          fullName,
160
          profilePublickey: userRecord.profilePublickey,
161
          walletAddress: sha3(userRecord.gdAddress.toLowerCase()),
162
          isCompleted: userRecord.isCompleted
163
            ? userRecord.isCompleted
164
            : {
165
                whiteList: false,
166
                topWallet: false
167
              },
×
168
          isEmailConfirmed,
×
169
          smsValidated: isMobileConfirmed
170
        }
×
171

172
        const userRecordWithPII = { ...userRecord, ...toUpdateUser, inviteCode, email, mobile }
×
173
        const signUpPromises = []
174

×
175
        const p1 = storage
×
176
          .updateUser(toUpdateUser)
177
          .then(() => logger.debug('updated new user record', { toUpdateUser }))
×
178
          .catch(e => {
179
            logger.error('failed updating new user record', e.message, e, { toUpdateUser })
180
            throw e
×
181
          })
×
182
        signUpPromises.push(p1)
183

×
184
        // whitelisting user if FR is disabled
×
185
        if (disableFaceVerification) {
186
          const p2 = addUserToWhiteList(userRecord, logger)
187
            .then(isWhitelisted => {
×
188
              logger.debug('addUserToWhiteList result', { isWhitelisted })
×
189
              if (isWhitelisted === false) throw new Error('Failed whitelisting user')
190
            })
×
191
            .catch(e => {
192
              logger.warn('addUserToWhiteList failed', e.message, e, { userRecord })
193
              throw e
×
194
            })
×
195
          signUpPromises.push(p2)
×
196
        }
197

×
198
        let p3 = Promise.resolve()
×
199
        if (isNonDevelopMode) {
200
          p3 = createCRMRecord(userRecordWithPII, utmString, logger)
201
            .then(r => {
×
202
              logger.debug('createCRMRecord success')
×
203
              return r
204
            })
×
205
            .catch(e => {
206
              logger.error('createCRMRecord failed', e.message, e, { userRecordWithPII })
207
              throw new Error('Failed adding user to CRM')
×
208
            })
209
          signUpPromises.push(p3)
210
        }
×
211

212
        const p5 = Promise.all([
×
213
          //TODO: generate email/mobile claims using ceramic
214
        ])
215
          .then(res => logger.info('created did claims: result', { res }))
×
216
          .catch(() => {
217
            logger.warn('create did claims: failed')
218
          })
×
219

220
        signUpPromises.push(p5)
×
221

×
222
        // don't await, if we failed to update its not critical for user.
×
223
        withTimeout(Promise.all(signUpPromises), 30000, 'signup promises timeout')
×
224
          .then(async () => {
×
225
            logger.info('signup promises success')
×
226
            if (isNonDevelopMode) {
×
227
              const crmId = await p3
228
              if (crmId)
229
                await OnGage.updateContact(email, crmId, { signup_completed: true }, logger).catch(exception => {
230
                  const { message } = exception
×
231
                  logger.error('Failed CRM tagging user completed signup', message, exception, { crmId })
232
                })
×
233
            }
234
          })
×
235
          .catch(e => logger.error('signup promises failed', e.message, e))
236

×
237
        logger.debug('signup steps success. adding new user:', { toUpdateUser })
238

239
        await storage.updateUser({
240
          identifier: userRecord.loggedInAs,
×
241
          createdDate: userRecord.createdDate || new Date().toString(),
242
          otp: {} //delete trace of mobile,email
×
243
        })
×
244

245
        res.json({ ok: 1 })
246
      } catch (e) {
247
        logger.warn('user signup failed', e.message, e)
248
        throw e
249
      }
250
    })
251
  )
252

2✔
253
  /**
254
   * we had issues with mautic some users are not in the database
255
   * fix to make sure we have user data in CRM
×
256
   */
×
257
  app.post(
258
    '/user/verifyCRM',
×
259
    wrapAsync(async (req, res) => {
×
260
      const { body, log: logger, user: userRecord } = req
261
      const { user: userPayload = {} } = body
×
262

×
263
      try {
264
        logger.debug('verify crm:', { data: userPayload, userRecord })
×
265

266
        if (userRecord.crmId) {
×
267
          logger.debug('verifyCRM already has crmID', { crmId: userRecord.crmId })
×
268
        } else {
269
          let { email, mobile, fullName } = userPayload
×
270

×
271
          if (!email) {
272
            const error = 'verifyCRM missing user email'
273

×
274
            logger.warn(error, { userPayload, userRecord })
275
            return res.json({ ok: 0, error })
×
276
          }
277

278
          let emailLC = email.toLowerCase()
279

280
          const toCRM = {
281
            identifier: userRecord.loggedInAs,
282
            fullName,
283
            walletAddress: sha3(userRecord.gdAddress.toLowerCase()),
284
            email
285
          }
286

×
287
          // TODO: verify why this is happening on wallet
×
288
          // for some reason some emails were kept with capital letter while from user they arrive lower case
289
          // this line is a patch to handle that case
290

291
          if (
×
292
            (sha3(email) === userRecord.email ||
293
              sha3(emailLC) === userRecord.email ||
294
              sha3(emailLC.charAt(0).toUpperCase() + emailLC.slice(1)) === userRecord.email) === false
×
295
          ) {
×
296
            logger.error('unable to verify user email', { email, hash: sha3(email), recordHash: userRecord.email })
297
          }
298

×
299
          if (mobile && sha3(mobile) === userRecord.mobile) {
300
            toCRM.mobile = mobile
×
301
          }
302

303
          const crmId = await createCRMRecord(toCRM, '', logger)
304

305
          await storage.updateUser({
306
            email: sha3(email),
×
307
            identifier: userRecord.loggedInAs,
308
            crmId
309
          })
×
310

311
          logger.debug('verifyCRM success', { crmId, toCRM })
×
312
        }
×
313

314
        res.json({ ok: 1 })
315
      } catch (e) {
316
        logger.error('createCRMRecord failed', e.message, e)
317
        throw new Error('Failed adding user in verifyCRM')
318
      }
319
    })
320
  )
321

322
  /**
323
   * @api {post} /user/start user starts registration and we have his email
324
   * @apiName Add
325
   * @apiGroup Storage
326
   *
327
   * @apiParam {Object} user
2✔
328
   *
329
   * @apiSuccess {Number} ok
330
   * @ignore
331
   */
×
332
  app.post(
×
333
    '/user/start',
×
334
    onlyInEnv('production', 'staging', 'test'),
335
    wrapAsync(async (req, res) => {
×
336
      const { user } = req.body
×
337
      const { log: logger, user: existingUser } = req
338
      const { __utmzz: utmString = '' } = req.cookies
339

×
340
      if (existingUser.crmId) {
×
341
        return res.json({ ok: 1 })
×
342
      }
343

344
      if (!user.email) {
345
        logger.error('email missing', { user, existingUser })
×
346
        throw new Error('Email is missed')
×
347
      }
348

×
349
      // fire and forget, don't wait for success or failure
350
      createCRMRecord({ ...user, email: user.email.toLowerCase() }, utmString, logger)
351
        .then(() => logger.debug('/user/start createCRMRecord success'))
×
352
        .catch(e => {
353
          logger.error('/user/start createCRMRecord failed', e.message, e, { user })
354
        })
355

356
      res.json({ ok: 1 })
357
    })
358
  )
359

360
  /**
361
   * @api {post} /user/claim collect claim status to crm
362
   * @apiName Add
363
   * @apiGroup Storage
364
   *
365
   * @apiParam {Object} user
2✔
366
   *
2✔
367
   * @apiSuccess {Number} ok
2✔
368
   * @ignore
×
369
   */
×
370
  let updatesQueue = [] // we send updates to ongange in bulk
×
371
  const qLogger = Logger.child({ from: 'OnGageQueue' })
×
372
  const clearOnGageQueue = async () => {
×
373
    if (updatesQueue.length === 0) return
×
374
    const oldQueue = updatesQueue
375
    updatesQueue = []
×
376
    try {
377
      const result = await OnGage.updateContacts(oldQueue, qLogger)
378
      qLogger.debug('/user/claim updateContacts result:', { result, total: oldQueue.length })
2!
379
    } catch (e) {
380
      qLogger.error('/user/claim updateContacts failed', e.message, e, { oldQueue })
2✔
381
    }
382
  }
383
  if (conf.env !== 'test') setInterval(clearOnGageQueue, 60 * 1000)
384

×
385
  app.post(
×
386
    '/user/claim',
387
    onlyInEnv('production', 'staging'),
×
388
    wrapAsync(async (req, res) => {
×
389
      let { last_claim, claim_counter } = req.body
390
      const { log: logger, user } = req
×
391

×
392
      if (!user.crmId) {
×
393
        const error = 'user/claim missing crmId'
394

395
        logger.warn(error, { user, body: req.body })
396
        res.json({ ok: 0, error })
×
397
        return
398
      }
×
399

×
400
      // format date according to OnGage date format
×
401
      last_claim = moment().format('YYYY/MM/DD')
402

403
      updatesQueue.push({ id: user.crmId, last_claim, claim_counter })
×
404
      if (updatesQueue.length > 100) {
405
        clearOnGageQueue()
406
      }
407

408
      res.json({ ok: 1 })
409
    })
410
  )
411

412
  /**
413
   * @api {post} /user/delete Delete user account
414
   * @apiName Delete
415
   * @apiGroup Storage
416
   *
2✔
417
   * @apiSuccess {Number} ok
418
   * @apiSuccess {[Object]} results
NEW
419
   * @ignore
×
420
   */
×
421
  app.post(
422
    '/user/delete',
423
    wrapAsync(async (req, res) => {
×
424
      const { user, log } = req
425
      log.info('delete user', { user })
×
426

×
427
      //first get number of accounts using same crmId before we delete the account
428
      const crmCount = user.crmId
429
        ? await storage.getCountCRMId(user.crmId).catch(e => {
430
            log.warn('getCountCRMId failed:', e.message, e)
×
431
            return 1
×
UNCOV
432
          })
×
433
        : 0
×
434

×
435
      const results = await Promise.all([
436
        (user.identifier ? storage.deleteUser(user) : Promise.reject())
×
437
          .then(() => ({ mongodb: 'ok' }))
438
          .catch(() => ({ mongodb: 'failed' })),
439
        crmCount > 1
×
440
          ? Promise.resolve({ crm: 'okMultiNotDeleted' })
×
441
          : crmCount === 0
442
            ? Promise.resolve({ crm: 'missingId' })
443
            : OnGage.deleteContact(user.crmId, log)
444
                .then(() => ({ crm: 'ok' }))
×
445
                .catch(() => ({ crm: 'failed' })),
×
446
        ...deleteFromAnalytics(user.identifier, user.gdAddress)
447
      ])
448

449
      log.info('delete user results', { user, results })
450
      res.json({ ok: 1, results })
451
    })
452
  )
453

454
  /**
455
   * @api {get} /user/exists return true  if user finished registration
456
   * @apiName Delete
457
   * @apiGroup Storage
458
   *
459
   * @apiSuccess {Number} ok
460
   * @apiSuccess {Boolean} exists
2✔
461
   * @apiSuccess {String} fullName
462

463
   * @ignore
×
464
   */
465
  app.get(
×
466
    '/user/exists',
467
    wrapAsync(async (req, res) => {
468
      const { user } = req
469

470
      res.json({ ok: 1, exists: user.createdDate != null, fullName: user.fullName })
471
    })
472
  )
473

474
  /**
475
   * @api {post} /userExists returns user registration method
476
   * @apiGroup Storage
477
   *
478
   * @apiSuccess {Number} ok
479
   * @apiSuccess {Boolean} exists
480
   * @apiSuccess {String} fullName
481
   * @apiSuccess {String} provider
2✔
482

483

484
   * @ignore
×
485
   */
×
486
  app.post(
×
487
    '/userExists',
488
    wrapAsync(async (req, res) => {
×
489
      const { log, body } = req
×
490
      const toHash = value => (value ? sha3(value) : null)
491
      const sendNotExists = () => res.json({ ok: 1, exists: false })
×
492

×
493
      let { identifier = '', email, mobile } = body
494
      email = email ? email.toLowerCase() : undefined
×
495

496
      const lowerCaseID = identifier ? identifier.toLowerCase() : undefined
497
      const [emailHash, mobileHash] = [email, mobile].map(toHash)
×
498

×
499
      const identityFilters = [
×
500
        // identifier is stored lowercase in the db. we lowercase addresses in the /auth/eth process
501
        { identifier: lowerCaseID },
×
502
        { email: email && emailHash },
×
503
        { mobile: mobile && mobileHash }
×
504
      ].filter(or => !!first(values(or)))
505

×
506
      if (identityFilters.length === 0) {
507
        log.warn('empty data for /userExists', { body })
508
        sendNotExists()
×
509

510
        return
511
      }
512

×
513
      const dateFilters = {
514
        createdDate: { $exists: true }
515
      }
516

517
      const providerFilters = [
×
518
        { regMethod: { $type: 'string', $ne: 'torus' } },
519
        { regMethod: 'torus', torusProvider: { $type: 'string', $ne: '' } }
×
520
      ]
521

522
      const searchFilters = [identityFilters, providerFilters].map(filters => ({ $or: filters }))
523

×
524
      const allFilters = {
525
        $and: [dateFilters, ...searchFilters]
526
      }
527

528
      const projections = {
529
        identifier: 1,
530
        email: 1,
531
        mobile: 1,
532
        createdDate: 1,
533
        torusProvider: 1,
534
        fullName: 1,
535
        regMethod: 1,
×
536
        crmId: 1
537
      }
×
538

539
      // sort by importance, prefer oldest verified account
540
      let ordering = { isVerified: -1, createdDate: 1 }
×
541

542
      if (lowerCaseID) {
543
        // sortBy sorts in ascending order (and keeps existing sort)
544
        // so non-matched by id results would be moved to the end
×
545
        projections.identifierMatches = {
546
          $eq: ['$identifier', lowerCaseID]
547
        }
×
548

549
        ordering = { identifierMatches: -1, ...ordering }
550
      }
551

552
      const existing = await storage.model.aggregate([
553
        { $match: allFilters },
×
554
        { $project: projections },
555
        { $sort: ordering }
×
556
      ])
×
557

558
      log.debug('userExists:', { existing, identifier, identifierLC: lowerCaseID, email, mobile })
559

×
560
      if (!existing.length) {
561
        return sendNotExists()
×
562
      }
563

564
      const bestExisting = first(existing)
565

566
      return res.json({
567
        ok: 1,
568
        exists: true,
×
569
        found: existing.length,
×
570
        fullName: bestExisting.fullName,
571
        provider: bestExisting.torusProvider,
572
        identifier: bestExisting.identifierMatches,
573
        email: email && emailHash === bestExisting.email,
574
        mobile: mobile && mobileHash === bestExisting.mobile,
575
        regMethod: bestExisting.regMethod
2✔
576
      })
577
    })
578
  )
579

×
580
  app.get(
×
581
    '/userWhitelisted/:account',
×
582
    requestRateLimiter(10, 1),
583
    wrapAsync(async (req, res) => {
×
584
      const { params } = req
585
      const { account } = params
586
      const isWhitelisted = await AdminWallet.isVerified(account)
587

2✔
588
      res.json({ ok: 1, isWhitelisted })
589
    })
590
  )
591

×
592
  app.get(
×
593
    '/syncWhitelist/:account',
594
    requestRateLimiter(3, 1),
×
595
    wrapAsync(async (req, res) => {
×
596
      const { params, log } = req
597
      const { account } = params
×
598

×
599
      try {
600
        const whitelisted = await AdminWallet.syncWhitelist(account, log)
×
601

×
602
        log.debug('syncWhitelist success', { account, whitelisted })
603
        res.json({ ok: 1, whitelisted })
604
      } catch (e) {
605
        log.error('failed syncWhitelist', e.message, e, { account })
606
        res.json({ ok: 0, error: e.message })
2✔
607
      }
608
    })
609
  )
610

×
611
  app.post(
×
612
    '/admin/user/get',
×
613
    adminAuthenticate,
×
614
    wrapAsync(async (req, res) => {
×
615
      const { body } = req
×
616
      let user = {}
×
617
      if (body.email)
×
618
        user = await storage.getUsersByEmail(body.email.startsWith('0x') === false ? sha3(body.email) : body.email)
619
      if (body.mobile)
×
620
        user = await storage.getUsersByMobile(body.mobile.startsWith('0x') === false ? sha3(body.mobile) : body.mobile)
621
      if (body.identifier) user = await storage.getUser(body.identifier)
622
      if (body.identifierHash) user = await storage.getByIdentifierHash(body.identifierHash)
623

2✔
624
      res.json({ ok: 1, user })
625
    })
626
  )
×
627

628
  app.post(
629
    '/admin/user/list',
2✔
630
    adminAuthenticate,
631
    wrapAsync(async (_, res) => storage.listUsers(list => res.json(list)))
632
  )
633

×
634
  app.post(
×
635
    '/admin/user/delete',
×
636
    adminAuthenticate,
637
    wrapAsync(async (req, res) => {
×
638
      const { body, log } = req
639
      let user = {}
640
      if (body.email)
641
        user = await storage.getUsersByEmail(body.email.startsWith('0x') === false ? sha3(body.email) : body.email)
2✔
642
      if (body.mobile)
643
        user = await storage.getUsersByMobile(body.mobile.startsWith('0x') === false ? sha3(body.mobile) : body.mobile)
644
      if (body.identifier) user = await storage.getUser(body.identifier)
645
      if (body.identifierHash) user = await storage.getByIdentifierHash(body.identifierHash)
×
646
      if (!user?.length) return res.json({ ok: 0, error: 'User not found' })
×
647
      user = user[0]
×
648
      const crmCount = user.crmId
×
649
        ? await storage.getCountCRMId(user.crmId).catch(e => {
×
650
            log.warn('getCountCRMId failed:', e.message, e)
651
            return 1
×
652
          })
×
653
        : 0
654

×
655
      log.info('delete user', { user, crmCount })
656

657
      const results = await Promise.all([
658
        (user.identifier ? storage.deleteUser(user) : Promise.reject())
2✔
659
          .then(() => ({ mongodb: 'ok' }))
660
          .catch(() => ({ mongodb: 'failed' })),
661
        crmCount === 0
662
          ? Promise.resolve({ crm: 'missingId' })
×
663
          : OnGage.deleteContact(user.crmId, log)
×
664
              .then(() => ({ crm: 'ok' }))
665
              .catch(() => ({ crm: 'failed' })),
×
666
        ...deleteFromAnalytics(user.identifier, user.gdAddress)
667
      ])
×
668

×
669
      log.info('delete user results', { user, results })
670
      res.json({ ok: 1, results })
671
    })
×
672
  )
×
673

674
  app.post(
×
675
    '/admin/model/fish',
×
676
    adminAuthenticate,
677
    wrapAsync(async (req, res) => {
×
678
      const { body, log } = req
679
      const { daysAgo } = body
×
680
      if (!daysAgo) return res.json({ ok: 0, error: 'missing daysAgo' })
×
681
      log.debug('fishing request', { daysAgo })
×
682
      fishManager
683
        .run(daysAgo)
684
        .then(fishResult => log.info('fishing request result:', { fishResult }))
×
685
        .catch(e => log.error('fish request failed', e.message, e, { daysAgo }))
686

687
      res.json({ ok: 1 })
688
    })
2✔
689
  )
690

691
  app.post(
692
    '/admin/verify/face/delete',
×
693
    adminAuthenticate,
×
694
    wrapAsync(async (req, res) => {
695
      const { body, log } = req
×
696
      const { enrollmentIdentifier, walletAddress } = body
×
697

×
698
      try {
×
699
        let removeWhitelistedResult
700
        if (walletAddress) {
×
701
          removeWhitelistedResult = await AdminWallet.removeWhitelisted(walletAddress)
×
702
        }
×
703

×
704
        log.info('admin delete faceid', { enrollmentIdentifier, walletAddress, removeWhitelistedResult })
705
        const processor = createEnrollmentProcessor(storage, log)
706

707
        await processor.dispose(toLower(enrollmentIdentifier), log)
708
        await cancelDisposalTask(storage, enrollmentIdentifier)
709
      } catch (exception) {
710
        const { message } = exception
711

712
        log.error('delete face record failed:', message, exception, { enrollmentIdentifier })
713
        res.status(400).json({ ok: 0, error: message })
714
        return
715
      }
716

717
      res.json({ ok: 1 })
718
    })
719
  )
720

721
  app.post(
722
    '/admin/verify/face/disposal',
723
    adminAuthenticate,
724
    wrapAsync(async (req, res) => {
725
      const { body, log } = req
726
      const { enrollmentIdentifier } = body
727

728
      try {
729
        const record = await getDisposalTask(storage, enrollmentIdentifier)
730
        log.debug('get face disposal task result:', { enrollmentIdentifier, record })
731
        return res.json({ ok: 1, record })
732
      } catch (exception) {
733
        const { message } = exception
734
        log.error('get face disposal task failed:', message, exception, { enrollmentIdentifier })
735
        res.status(400).json({ ok: 0, error: message })
736
        return
737
      }
738
    })
739
  )
740

741
  app.post(
742
    '/admin/user/verifyAge',
743
    adminAuthenticate,
744
    wrapAsync(async (req, res) => {
745
      const { body, log } = req
746
      let result = {}
747
      const identifier = body.identifier || sha3(toChecksumAddress(body.account))
748
      if (identifier) result = await storage.updateUser({ identifier, ageVerified: true })
749
      log.info('admin age verify', body, result)
750
      res.json({ ok: 1, result })
751
    })
752
  )
753
}
754

755
export default setup
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc