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

GoodDollar / GoodServer / 18878690898

28 Oct 2025 02:46PM UTC coverage: 49.137% (-0.4%) from 49.515%
18878690898

push

github

gooddollar-techadmin
chore: release qa version 1.63.1-0 [skip build]

616 of 1530 branches covered (40.26%)

Branch coverage included in aggregate %.

1888 of 3566 relevant lines covered (52.94%)

7.28 hits per line

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

21.89
/src/server/verification/verificationAPI.js
1
// @flow
2
import crypto from 'crypto'
3
import { Router } from 'express'
4
import passport from 'passport'
5
import { get, defaults, memoize, omit } from 'lodash'
6
import { sha3, keccak256, toWei } from 'web3-utils'
7
import web3Abi from 'web3-eth-abi'
8
import requestIp from 'request-ip'
9
import moment from 'moment'
10
import type { LoggedUser, StorageAPI, UserRecord, VerificationAPI } from '../../imports/types'
11
import { default as AdminWallet } from '../blockchain/MultiWallet'
12
import { findFaucetAbuse, findGDTx } from '../blockchain/explorer'
13
import { onlyInEnv, wrapAsync } from '../utils/helpers'
14
import requestRateLimiter, { userRateLimiter } from '../utils/requestRateLimiter'
15
import OTP from '../../imports/otp'
16
import conf from '../server.config'
17
import OnGage from '../crm/ongage'
18
import { sendTemplateEmail } from '../aws-ses/aws-ses'
19
import fetch from 'cross-fetch'
20
import createEnrollmentProcessor from './processor/EnrollmentProcessor.js'
21
import createIdScanProcessor from './processor/IdScanProcessor'
22

23
import { cancelDisposalTask, getDisposalTask } from './cron/taskUtil'
24
import { recoverPublickey, verifyIdentifier } from '../utils/eth'
25
import { shouldLogVerificaitonError } from './utils/logger'
26
import { syncUserEmail } from '../storage/addUserSteps'
27
import { normalizeIdentifiers } from './utils/utils.js'
28

29
import ipcache from '../db/mongo/ipcache-provider.js'
30
import { DelayedTaskStatus } from '../db/mongo/models/delayed-task.js'
31

32
// currently faces stay until they expire, no option for user to delete their face record
33
/*
34
export const deleteFaceId = async (fvSigner, enrollmentIdentifier, user, storage, log) => {
35
  const { gdAddress } = user
36
  log.debug('delete face request:', { fvSigner, enrollmentIdentifier, user })
37
  const processor = createEnrollmentProcessor(storage, log)
38

39
  // for v2 identifier - verify that identifier is for the address we are going to whitelist
40
  await verifyIdentifier(enrollmentIdentifier, gdAddress)
41

42
  const { v2Identifier, v1Identifier } = normalizeIdentifiers(enrollmentIdentifier, fvSigner)
43

44
  // here we check if wallet was registered using v1 of v2 identifier
45
  const [isV2, isV1] = await Promise.all([
46
    processor.isIdentifierExists(v2Identifier),
47
    v1Identifier && processor.isIdentifierExists(v1Identifier)
48
  ])
49

50
  if (isV2) {
51
    //in v2 we expect the enrollmentidentifier to be the whole signature, so we cut it down to 42
52
    await processor.enqueueDisposal(user, v2Identifier, log)
53
  }
54

55
  if (isV1) {
56
    await processor.enqueueDisposal(user, v1Identifier, log)
57
  }
58
}
59
*/
60

61
// try to cache responses from faucet abuse to prevent 500 errors from server
62
// if same user keep requesting.
63
const cachedFindFaucetAbuse = memoize(findFaucetAbuse)
2✔
64
const clearMemoizedFaucetAbuse = async () => {
2✔
65
  if (cachedFindFaucetAbuse.cache) {
×
66
    cachedFindFaucetAbuse.cache.clear()
×
67
    console.log('clearMemoizedFaucetAbuse done')
×
68
    return
×
69
  }
70
  console.log('clearMemoizedFaucetAbuse failed')
×
71
}
72
if (conf.env !== 'test') setInterval(clearMemoizedFaucetAbuse, 60 * 60 * 1000) // clear every 1 hour
2!
73

74
let faucetAddressBlocked = {}
2✔
75
const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000
2✔
76
const checkMultiIpAccounts = async (account, ip, logger) => {
2✔
77
  const record = await ipcache.updateAndGet(ip.toLowerCase(), account.toLowerCase())
×
78
  logger.debug('checkMultiIpAccounts:', { record })
×
79
  const { accounts } = record
×
80
  if (accounts.length >= 5) {
×
81
    logger.debug('checkMultiIpAccounts:', { ip, account, accounts })
×
82
    accounts.forEach(addr => (faucetAddressBlocked[addr] = faucetAddressBlocked[addr] || Date.now()))
×
83
    return accounts
×
84
  }
85
  if (faucetAddressBlocked[account]) {
×
86
    return true
×
87
  }
88
  return false
×
89
}
90

91
if (conf.env !== 'test')
2!
92
  setInterval(
×
93
    () => {
94
      let cleared = 0
×
95
      Object.keys(faucetAddressBlocked).forEach(addr => {
×
96
        if (faucetAddressBlocked[addr] + SEVEN_DAYS <= Date.now()) {
×
97
          cleared += 1
×
98
          delete faucetAddressBlocked[addr]
×
99
        }
100
      })
101
      console.log(
×
102
        'cleaning faucetAddressBlocked after 7 days. total addresses:',
103
        Object.keys(faucetAddressBlocked).length,
104
        'cleared:',
105
        cleared
106
      )
107
    },
108
    24 * 60 * 60 * 1000
109
  ) // clear every 1 day (24 hours)
110

111
const setup = (app: Router, verifier: VerificationAPI, storage: StorageAPI) => {
2✔
112
  /**
113
   * @api {delete} /verify/face/:enrollmentIdentifier Enqueue user's face snapshot for disposal since 24h
114
   * @apiName Dispose Face
115
   * @apiGroup Verification
116
   *
117
   * @apiParam {String} enrollmentIdentifier
118
   * @apiParam {String} signature
119
   *
120
   * @ignore
121
   */
122
  app.delete(
2✔
123
    '/verify/face/:enrollmentIdentifier',
124
    passport.authenticate('jwt', { session: false }),
125
    wrapAsync(async (req, res) => {
126
      const { params, query, log, user } = req
3✔
127
      const { enrollmentIdentifier } = params
3✔
128
      const { fvSigner = '' } = query
3✔
129

130
      try {
3✔
131
        const authenticationPeriod = await AdminWallet.getAuthenticationPeriod()
3✔
132

133
        const record = await getDisposalTask(storage, enrollmentIdentifier)
3✔
134
        log.debug('get face disposal task result:', { enrollmentIdentifier, record })
3✔
135
        if (record == null || record.status === DelayedTaskStatus.Complete) {
3✔
136
          return res.json({ success: true, status: DelayedTaskStatus.Complete })
2✔
137
        }
138
        return res.json({
1✔
139
          success: true,
140
          status: record.status,
141
          executeAt: moment(record.createdAt)
142
            .add(authenticationPeriod + 1, 'days')
143
            .toISOString()
144
        })
145
      } catch (exception) {
146
        const { message } = exception
×
147

148
        log.error('delete face record failed:', message, exception, { enrollmentIdentifier, fvSigner, user })
×
149
        res.status(400).json({ success: false, error: message })
×
150
        return
×
151
      }
152
    })
153
  )
154

155
  /**
156
   * @api {get} /verify/face/:enrollmentIdentifier Checks is face snapshot enqueued for disposal. Return disposal state
157
   * @apiName Check face disposal state
158
   * @apiGroup Verification
159
   *
160
   * @apiParam {String} enrollmentIdentifier
161
   *
162
   * @ignore
163
   */
164
  app.get(
2✔
165
    '/verify/face/:enrollmentIdentifier',
166
    passport.authenticate('jwt', { session: false }),
167
    wrapAsync(async (req, res) => {
168
      const { params, log, user, query } = req
1✔
169
      const { enrollmentIdentifier } = params
1✔
170
      const { fvSigner = '' } = query
1✔
171
      log.debug('check face status request:', { fvSigner, enrollmentIdentifier, user })
1✔
172

173
      try {
1✔
174
        const { v2Identifier, v1Identifier } = normalizeIdentifiers(enrollmentIdentifier, fvSigner)
1✔
175

176
        const processor = createEnrollmentProcessor(storage, log)
1✔
177
        const [isDisposingV2, isDisposingV1] = await Promise.all([
1✔
178
          processor.isEnqueuedForDisposal(v2Identifier, log),
179
          v1Identifier && processor.isEnqueuedForDisposal(v1Identifier, log)
1!
180
        ])
181

182
        res.json({ success: true, isDisposing: !!isDisposingV2 || !!isDisposingV1 })
1✔
183
      } catch (exception) {
184
        const { message } = exception
×
185

186
        log.error('face record disposing check failed:', message, exception, { enrollmentIdentifier, fvSigner, user })
×
187
        res.status(400).json({ success: false, error: message })
×
188
      }
189
    })
190
  )
191

192
  /**
193
   * @api {post} /verify/face/license Retrieves FaceTec license key for a new enrollment session
194
   * @apiName Retrieves FaceTec license key text
195
   * @apiGroup Verification
196
   *
197
   * @ignore
198
   */
199
  app.post(
2✔
200
    '/verify/face/license/:licenseType',
201
    passport.authenticate('jwt', { session: false }),
202
    wrapAsync(async (req, res) => {
203
      const { log, user, params } = req
4✔
204
      const { licenseType } = params
4✔
205

206
      log.debug('license face request:', { licenseType, user })
4✔
207

208
      try {
4✔
209
        if (!conf.zoomProductionMode) {
4✔
210
          throw new Error('Cannot obtain production license running non-production mode.')
1✔
211
        }
212

213
        const processor = createEnrollmentProcessor(storage, log)
3✔
214
        const license = await processor.getLicenseKey(licenseType, log)
3✔
215

216
        res.json({ success: true, license })
1✔
217
      } catch (exception) {
218
        const { message } = exception
3✔
219
        log.error('getting FaceTec license failed:', message, exception, { user })
3✔
220
        res.status(400).json({ success: false, error: message })
3✔
221
      }
222
    })
223
  )
224

225
  /**
226
   * @api {post} /verify/face/session Issues session token for a new enrollment session
227
   * @apiName Issue enrollment session token
228
   * @apiGroup Verification
229
   *
230
   * @ignore
231
   */
232
  app.post(
2✔
233
    '/verify/face/session',
234
    passport.authenticate('jwt', { session: false }),
235
    wrapAsync(async (req, res) => {
236
      const { log, user } = req
2✔
237

238
      log.debug('session face request:', { user })
2✔
239

240
      try {
2✔
241
        const processor = createEnrollmentProcessor(storage, log)
2✔
242
        const sessionToken = await processor.issueSessionToken(log)
2✔
243

244
        res.json({ success: true, sessionToken })
1✔
245
      } catch (exception) {
246
        const { message } = exception
1✔
247

248
        log.error('generating enrollment session token failed:', message, exception, { user })
1✔
249
        res.status(400).json({ success: false, error: message })
1✔
250
      }
251
    })
252
  )
253

254
  /**
255
   * @api {put} /verify/:enrollmentIdentifier Verify users face
256
   * @apiName Face Verification
257
   * @apiGroup Verification
258
   *
259
   * @apiParam {String} enrollmentIdentifier
260
   * @apiParam {String} sessionId
261
   *
262
   * @ignore
263
   */
264
  app.put(
2✔
265
    '/verify/face/:enrollmentIdentifier',
266
    passport.authenticate('jwt', { session: false }),
267
    wrapAsync(async (req, res) => {
268
      const { user, log, params, body } = req
11✔
269
      const { enrollmentIdentifier } = params
11✔
270
      const { chainId, fvSigner = '', ...payload } = body || {} // payload is the facetec data
11!
271
      const { gdAddress } = user
11✔
272

273
      log.debug('enroll face request:', { fvSigner, enrollmentIdentifier, chainId, user })
11✔
274

275
      // checking if request aborted to handle cases when connection is slow
276
      // and facemap / images were uploaded more that 30sec causing timeout
277
      if (req.aborted) {
11!
278
        return
×
279
      }
280

281
      // user.chainId = chainId || conf.defaultWhitelistChainId
282

283
      //currently we force all new users to be marked as registered first on celo or xdc if specified
284
      //this is relevant for the invite rewards
285
      switch (String(chainId)) {
11!
286
        case '122':
287
        case '50':
288
        case '42220':
289
          user.chainId = chainId
×
290
          break
×
291
        default:
292
          user.chainId = 42220
11✔
293
          break
11✔
294
      }
295

296
      try {
11✔
297
        // for v2 identifier - verify that identifier is for the address we are going to whitelist
298
        // for v1 this will do nothing
299
        await verifyIdentifier(enrollmentIdentifier, gdAddress, chainId)
11✔
300

301
        const { v2Identifier, v1Identifier } = normalizeIdentifiers(enrollmentIdentifier, fvSigner)
11✔
302
        const enrollmentProcessor = createEnrollmentProcessor(storage, log)
11✔
303

304
        // here we check if wallet was registered using v1 of v2 identifier
305
        const isV1 = !!v1Identifier && (await enrollmentProcessor.isIdentifierExists(v1Identifier))
11!
306

307
        try {
11✔
308
          // if v1, we convert to v2
309
          // delete previous enrollment.
310
          // once user completes FV it will create a new record under his V2 id. update his lastAuthenticated. and enqueue for disposal
311
          if (isV1) {
11!
312
            log.info('v1 identifier found, converting to v2', { v1Identifier, v2Identifier, gdAddress })
×
313
            await Promise.all([
×
314
              enrollmentProcessor.dispose(v1Identifier, log),
315
              cancelDisposalTask(storage, v1Identifier)
316
            ])
317
          }
318
          await enrollmentProcessor.validate(user, v2Identifier, payload)
11✔
319
          const wasWhitelisted = await AdminWallet.lastAuthenticated(gdAddress)
7✔
320
          const enrollmentResult = await enrollmentProcessor.enroll(user, v2Identifier, payload, log)
7✔
321

322
          // fetch duplicate expiration
323
          if (enrollmentResult.success === false && enrollmentResult.enrollmentResult?.isDuplicate) {
7✔
324
            const dup = enrollmentResult.enrollmentResult.duplicate.identifier
1✔
325
            const authenticationPeriod = await AdminWallet.getAuthenticationPeriod()
1✔
326
            const record = await getDisposalTask(storage, dup)
1✔
327
            const expiration = moment(record?.createdAt || 0)
1!
328
              .add(authenticationPeriod + 1, 'days')
329
              .toISOString()
330
            enrollmentResult.enrollmentResult.duplicate.expiration = expiration
1✔
331
          }
332
          // log warn if user was whitelisted but unable to pass FV again
333
          if (wasWhitelisted > 0 && enrollmentResult.success === false) {
7!
334
            log.warn('user failed to re-authenticate', {
×
335
              wasWhitelisted,
336
              enrollmentResult,
337
              gdAddress,
338
              v2Identifier
339
            })
340
            if (isV1) {
×
341
              //throw error so we de-whitelist user
342
              throw new Error('User failed to re-authenticate with V1 identifier')
×
343
            }
344
          }
345
          log.info(wasWhitelisted > 0 && enrollmentResult.success ? 'user re-authenticated' : 'user enrolled', {
7!
346
            wasWhitelisted,
347
            enrollmentResult,
348
            gdAddress,
349
            v2Identifier
350
          })
351
          res.json(enrollmentResult)
7✔
352
        } catch (e) {
353
          if (isV1) {
4!
354
            // if we deleted the user record but had an error in whitelisting, then we must revoke his whitelisted status
355
            // since we might not have his record enrolled
356
            const isIndexed = await enrollmentProcessor.isIdentifierIndexed(v2Identifier)
×
357
            // if new identifier is indexed then dont revoke
358
            if (!isIndexed) {
×
359
              const isWhitelisted = await AdminWallet.isVerified(gdAddress)
×
360
              if (isWhitelisted) await AdminWallet.removeWhitelisted(gdAddress)
×
361
            }
362
            log.error('failed converting v1 identifier', e.message, e, { isIndexed, gdAddress })
×
363
          }
364

365
          throw e
4✔
366
        }
367
      } catch (exception) {
368
        const { message } = exception
4✔
369
        const logArgs = ['Face verification error:', message, exception, { enrollmentIdentifier, fvSigner, gdAddress }]
4✔
370

371
        if (shouldLogVerificaitonError(exception)) {
4!
372
          log.error(...logArgs)
×
373
        } else {
374
          log.warn(...logArgs)
4✔
375
        }
376

377
        res.status(400).json({ success: false, error: message })
4✔
378
      }
379
    })
380
  )
381

382
  /**
383
   * @api {put} /verify/idscan:enrollmentIdentifier Verify id matches face and does OCR
384
   * @apiName IDScan
385
   * @apiGroup IDScanq
386
   *
387
   * @apiParam {String} enrollmentIdentifier
388
   * @apiParam {String} sessionId
389
   *
390
   * @ignore
391
   */
392
  app.put(
2✔
393
    '/verify/idscan/:enrollmentIdentifier',
394
    passport.authenticate('jwt', { session: false }),
395
    wrapAsync(async (req, res) => {
396
      const { user, log, params, body } = req
×
397
      const { enrollmentIdentifier } = params
×
398
      const { chainId, ...payload } = body || {} // payload is the facetec data
×
399
      const { gdAddress } = user
×
400

401
      log.debug('idscan request:', { user, payloadFields: Object.keys(payload) })
×
402

403
      // checking if request aborted to handle cases when connection is slow
404
      // and facemap / images were uploaded more that 30sec causing timeout
405
      if (req.aborted) {
×
406
        return
×
407
      }
408

409
      try {
×
410
        // for v2 identifier - verify that identifier is for the address we are going to whitelist
411
        // for v1 this will do nothing
412
        await verifyIdentifier(enrollmentIdentifier, gdAddress, chainId)
×
413

414
        const { v2Identifier } = normalizeIdentifiers(enrollmentIdentifier)
×
415

416
        const idscanProcessor = createIdScanProcessor(storage, log)
×
417

418
        let { isMatch, scanResultBlob, ...scanResult } = await idscanProcessor.verify(user, v2Identifier, payload)
×
419
        scanResult = omit(scanResult, ['externalDatabaseRefID', 'ocrResults', 'serverInfo', 'callData']) //remove unrequired fields
×
420
        log.debug('idscan results:', { isMatch, scanResult })
×
421
        const toSign = { success: true, isMatch, gdAddress, scanResult, timestamp: Date.now() }
×
422
        const { sig: signature } = await AdminWallet.signMessage(JSON.stringify(toSign))
×
423
        res.json({ scanResult: { ...toSign, signature }, scanResultBlob })
×
424
      } catch (exception) {
425
        const { message } = exception
×
426
        const logArgs = ['idscan error:', message, exception, { enrollmentIdentifier, gdAddress }]
×
427

428
        if (shouldLogVerificaitonError(exception)) {
×
429
          log.error(...logArgs)
×
430
        } else {
431
          log.warn(...logArgs)
×
432
        }
433

434
        res.status(400).json({ success: false, error: message })
×
435
      }
436
    })
437
  )
438

439
  /**
440
   * demo for verifying that idscan was created by us
441
   * trigger defender autotask
442
   */
443
  app.post(
2✔
444
    '/verify/idscan',
445
    wrapAsync(async (req, res) => {
446
      const { log, body } = req
×
447
      const { scanResult } = body || {} // payload is the facetec data
×
448

449
      log.debug('idscan submit:', { payloadFields: Object.keys(scanResult) })
×
450

451
      try {
×
452
        const { signature, ...signed } = scanResult
×
453
        const { gdAddress } = signed
×
454
        const mHash = keccak256(JSON.stringify(signed))
×
455
        log.debug('idscan submit verifying...:', { mHash, signature })
×
456
        const publicKey = recoverPublickey(signature, mHash, '')
×
457
        const data = web3Abi.encodeFunctionCall(
×
458
          {
459
            name: 'addMember',
460
            type: 'function',
461
            inputs: [
462
              {
463
                type: 'address',
464
                name: 'member'
465
              }
466
            ]
467
          },
468
          [gdAddress]
469
        )
470
        const relayer = await AdminWallet.signer.relayer.getRelayer()
×
471

472
        log.debug('idscan submit verified...:', { publicKey, relayer })
×
473
        if (publicKey !== relayer.address.toLowerCase()) {
×
474
          throw new Error('invalid signer: ' + publicKey)
×
475
        }
476

477
        const addTx = await AdminWallet.signer.sendTx({
×
478
          to: '0x163a99a51fE32eEaC7407687522B5355354a1a37',
479
          data,
480
          gasLimit: '300000'
481
        })
482

483
        log.debug('idscan adding member:', addTx)
×
484

485
        res.json({ ok: 1, txHash: addTx.hash })
×
486
      } catch (exception) {
487
        const { message } = exception
×
488

489
        log.error('idscan submit failed:', message, exception)
×
490

491
        res.status(400).json({ ok: 0, error: message })
×
492
      }
493
    })
494
  )
495
  /**
496
   * @api {post} /verify/sendotp Sends OTP
497
   * @apiName Send OTP
498
   * @apiGroup Verification
499
   *
500
   * @apiParam {UserRecord} user
501
   *
502
   * @apiSuccess {Number} ok
503
   * @ignore
504
   */
505
  app.post(
2✔
506
    '/verify/sendotp',
507
    passport.authenticate('jwt', { session: false }),
508
    userRateLimiter(2, 1), // 1 req / 1min, should be applied AFTER auth to have req.user been set
509
    // also no need for reqRateLimiter as user limiter falls back to the ip (e.g. works as default limiter if no user)
510
    wrapAsync(async (req, res) => {
511
      const { user, body } = req
×
512
      const log = req.log
×
513

514
      log.info('otp request:', { user, body })
×
515

516
      const onlyCheckAlreadyVerified = body.onlyCheckAlreadyVerified
×
517

518
      const mobile = decodeURIComponent(body.user.mobile || user.otp.mobile) //fix in case input is %2B instead of +
×
519
      const hashedMobile = sha3(mobile)
×
520
      let userRec: UserRecord = defaults(body.user, user, { identifier: user.loggedInAs })
×
521

522
      const savedMobile = user.mobile
×
523

524
      if (conf.allowDuplicateUserData === false && (await storage.isDupUserData({ mobile: hashedMobile }))) {
×
525
        return res.json({ ok: 0, error: 'mobile_already_exists' })
×
526
      }
527

528
      if (!userRec.smsValidated || hashedMobile !== savedMobile) {
×
529
        if (!onlyCheckAlreadyVerified) {
×
530
          log.debug('sending otp:', user.loggedInAs)
×
531
          if (['production', 'staging'].includes(conf.env)) {
×
532
            const clientIp = requestIp.getClientIp(req)
×
533
            const sendResult = await OTP.sendOTP(mobile, get(body, 'user.otpChannel', 'sms'), clientIp)
×
534

535
            log.info('otp sent:', mobile, { user: user.loggedInAs, sendResult })
×
536
          }
537

538
          await storage.updateUser({
×
539
            identifier: user.loggedInAs,
540
            otp: {
541
              ...(userRec.otp || {}),
×
542
              mobile
543
            }
544
          })
545
        }
546
      }
547

548
      res.json({ ok: 1, alreadyVerified: hashedMobile === savedMobile && user.smsValidated })
×
549
    })
550
  )
551

552
  /**
553
   * @api {post} /verify/mobile Verify mobile data code
554
   * @apiName OTP Code
555
   * @apiGroup Verification
556
   *
557
   * @apiParam {Object} verificationData
558
   *
559
   * @apiSuccess {Number} ok
560
   * @apiSuccess {Claim} attestation
561
   * @ignore
562
   */
563
  app.post(
2✔
564
    '/verify/mobile',
565
    requestRateLimiter(10, 1),
566
    passport.authenticate('jwt', { session: false }),
567
    onlyInEnv('production', 'staging'),
568
    wrapAsync(async (req, res) => {
569
      const log = req.log
×
570
      const { user, body } = req
×
571
      const verificationData: { otp: string } = body.verificationData
×
572
      const { mobile } = user.otp || {}
×
573

574
      if (!mobile) {
×
575
        log.warn('mobile to verify not found or missing', 'mobile missing', new Error('mobile missing'), {
×
576
          user,
577
          verificationData
578
        })
579

580
        return res.status(400).json({ ok: 0, error: 'MOBILE MISSING' })
×
581
      }
582

583
      const hashedNewMobile = mobile && sha3(mobile)
×
584
      const currentMobile = user.mobile
×
585

586
      log.debug('mobile verified', { user, verificationData, hashedNewMobile })
×
587

588
      if (!user.smsValidated || currentMobile !== hashedNewMobile) {
×
589
        try {
×
590
          const clientIp = requestIp.getClientIp(req)
×
591
          await verifier.verifyMobile({ identifier: user.loggedInAs, mobile }, verificationData, clientIp)
×
592
        } catch (e) {
593
          log.warn('mobile verification failed:', e.message, { user, mobile, verificationData })
×
594
          return res.status(400).json({ ok: 0, error: 'OTP FAILED', message: e.message })
×
595
        }
596

597
        let updIndexPromise
598
        const { crmId } = user
×
599

600
        if (currentMobile && currentMobile !== hashedNewMobile) {
×
601
          updIndexPromise = Promise.all([
×
602
            //TODO: generate ceramic claim
603
          ])
604
        }
605

606
        if (crmId) {
×
607
          //fire and forget
608
          OnGage.updateContact(null, crmId, { mobile }, log).catch(e =>
×
609
            log.error('Error updating CRM contact', e.message, e, { crmId, mobile })
×
610
          )
611
        }
612

613
        await Promise.all([
×
614
          updIndexPromise,
615
          storage.updateUser({
616
            identifier: user.loggedInAs,
617
            smsValidated: true,
618
            mobile: hashedNewMobile
619
          }),
620
          user.createdDate && //keep temporary field if user is signing up
×
621
            storage.model.updateOne({ identifier: user.loggedInAs }, { $unset: { 'otp.mobile': true } })
622
        ])
623
      }
624

625
      //TODO: replace with ceramic
626
      let signedMobile
627
      res.json({ ok: 1, attestation: signedMobile })
×
628
    })
629
  )
630

631
  /**
632
   * @api {post} /verify/registration Verify user registration status
633
   * @apiName Verify Registration Status
634
   * @apiGroup Verification
635
   * @apiSuccess {Number} ok
636
   * @ignore
637
   */
638
  app.post(
2✔
639
    '/verify/registration',
640
    passport.authenticate('jwt', { session: false }),
641
    wrapAsync(async (req, res) => {
642
      const user = req.user
×
643
      res.json({ ok: user && user.createdDate ? 1 : 0 })
×
644
    })
645
  )
646
  /**
647
   * @api {post} /verify/topwallet Tops Users Wallet if needed
648
   * @apiName Top Wallet
649
   * @apiGroup Verification
650
   *
651
   * @apiParam {LoggedUser} user
652
   *
653
   * @apiSuccess {Number} ok
654
   * @ignore
655
   */
656
  app.post(
2✔
657
    '/verify/topwallet',
658
    requestRateLimiter(3, 1),
659
    passport.authenticate(['jwt', 'anonymous'], { session: false }),
660
    wrapAsync(async (req, res) => {
661
      const log = req.log
×
662
      const { origin, host } = req.headers
×
663
      const { account, chainId = 42220 } = req.body || {}
×
664
      const user: LoggedUser = req.user || { gdAddress: account }
×
665
      const clientIp = requestIp.getClientIp(req)
×
666

667
      const gdContract = AdminWallet.walletsMap[chainId]?.tokenContract?._address
×
668
      const faucetContract = AdminWallet.walletsMap[chainId]?.faucetContract?._address
×
669
      log.debug('topwallet tx request:', {
×
670
        address: user.gdAddress,
671
        chainId,
672
        gdContract,
673
        faucetContract,
674
        user: req.user,
675
        origin,
676
        host,
677
        clientIp
678
      })
679

680
      if (!faucetContract) {
×
681
        log.warn('topWallet unsupported chain', { chainId })
×
682
        return res.json({ ok: -1, error: 'unsupported chain' })
×
683
      }
684

685
      if (conf.env === 'production') {
×
686
        if (
×
687
          !user.isEmailConfirmed &&
×
688
          !user.smsValidated &&
689
          !(await AdminWallet.isVerified(user.gdAddress)) &&
690
          !(gdContract && (await findGDTx(user.gdAddress, chainId, gdContract)))
×
691
        ) {
692
          log.warn('topwallet denied, not registered user nor whitelisted nor did gd tx lately', {
×
693
            address: user.gdAddress,
694
            origin,
695
            chainId,
696
            clientIp
697
          })
698
          return res.json({ ok: -1, error: 'not whitelisted' })
×
699
        }
700
      }
701
      if (!user.gdAddress) {
×
702
        throw new Error('missing wallet address to top')
×
703
      }
704

705
      // check for faucet abuse
706
      const foundAbuse = await cachedFindFaucetAbuse(user.gdAddress, chainId).catch(e => {
×
707
        log.error('findFaucetAbuse failed', e.message, e, { address: user.gdAddress, chainId })
×
708
        return
×
709
      })
710

711
      if (foundAbuse) {
×
712
        log.warn('faucet abuse found:', foundAbuse)
×
713
        return res.json({ ok: -1, error: 'faucet abuse: ' + foundAbuse.hash })
×
714
      }
715

716
      const foundMultiIpAccounts = await checkMultiIpAccounts(user.gdAddress, clientIp, log)
×
717
      if (foundMultiIpAccounts) {
×
718
        log.warn('faucet multiip abuse found:', foundMultiIpAccounts.length, new Error('faucet multiip abuse'), {
×
719
          foundMultiIpAccounts,
720
          clientIp,
721
          account: user.gdAddress
722
        })
723
        AdminWallet.banInFaucet(foundMultiIpAccounts, 'all', log).catch(e => {
×
724
          log.error('findFaucetAbuse banInFaucet failed:', e.message, e, {
×
725
            address: user.gdAddress,
726
            foundMultiIpAccounts
×
727
          })
728
        })
729
        return res.json({ ok: -1, error: 'faucet multi abuse' })
×
730
      }
×
731

732
      try {
×
733
        let txPromise = AdminWallet.topWallet(user.gdAddress, chainId, log)
×
734
          .then(tx => {
735
            log.debug('topwallet tx', { walletaddress: user.gdAddress, tx })
736
            return { ok: 1 }
×
737
          })
×
738
          .catch(async exception => {
739
            const { message } = exception
×
740
            log.warn('Failed topwallet tx', message, exception, { walletaddress: user.gdAddress }) //errors are already logged in adminwallet so jsut warn
741

742
            return { ok: -1, error: message }
×
743
          })
744

×
745
        const txRes = await txPromise
746

747
        log.info('topwallet tx done', {
748
          txRes,
749
          loggedInAs: user.loggedInAs
×
750
        })
751

×
752
        res.json(txRes)
×
753
      } catch (e) {
754
        log.error('topwallet timeout or unexpected', e.message, e, { walletaddress: user.gdAddress })
755
        res.json({ ok: -1, error: e.message })
756
      }
757
    })
758
  )
759

760
  /**
761
   * @api {post} /verify/swaphelper trigger a non custodial swap
762
   * @apiName Swaphelper
763
   * @apiGroup Verification
764
   *
765
   * @apiParam {account} user
766
   *
767
   * @apiSuccess {Number} ok
2✔
768
   * @ignore
769
   */
770
  app.post(
771
    '/verify/swaphelper',
772
    requestRateLimiter(3, 1),
×
773
    passport.authenticate(['jwt', 'anonymous'], { session: false }),
×
774
    wrapAsync(async (req, res) => {
×
775
      const log = req.log
776
      const { account, chainId = 42220 } = req.body || {}
×
777
      const gdAddress = account || get(req.user, 'gdAddress')
×
778

×
779
      log.info('swaphelper request:', { gdAddress, chainId })
780
      if (!gdAddress) {
781
        throw new Error('missing user account')
×
782
      }
783

784
      try {
×
785
        //verify target helper address has funds
×
786

787
        const tx = await AdminWallet.walletsMap[chainId].swaphelper(gdAddress, log)
×
788
        log.info('swaphelper request done:', { gdAddress, chainId, tx })
789

×
790
        res.json({ ok: 1, hash: tx.transactionHash })
×
791
      } catch (e) {
792
        log.error('swaphelper timeout or unexpected', e.message, e, { walletaddress: gdAddress, chainId })
793
        res.json({ ok: -1, error: e.message })
794
      }
795
    })
796
  )
797

798
  /**
799
   * @api {post} /verify/email Send verification email endpoint
800
   * @apiName Send Email
801
   * @apiGroup Verification
802
   *
803
   * @apiParam {UserRecord} user
804
   *
805
   * @apiSuccess {Number} ok
2✔
806
   * @ignore
807
   */
808
  app.post(
809
    '/verify/sendemail',
810
    requestRateLimiter(2, 1),
×
811
    passport.authenticate('jwt', { session: false }),
×
812
    wrapAsync(async (req, res) => {
×
813
      let runInEnv = ['production', 'staging', 'test'].includes(conf.env)
814
      const log = req.log
×
815
      const { user, body } = req
×
816

817
      let { email } = body.user
×
818
      email = email.toLowerCase()
×
819

×
820
      if (!email || !user) {
821
        log.warn('email verification email or user record not found:', { email, user })
822
        return res.json({ ok: 0, error: 'email or user missing' })
823
      }
×
824

×
825
      //merge user details
×
826
      const { email: currentEmail } = user
827
      let userRec: UserRecord = defaults(body.user, user)
828
      const isEmailChanged = currentEmail && currentEmail !== sha3(email)
×
829

830
      let code
×
831
      log.debug('email verification request:', { email, currentEmail, isEmailChanged, body, user })
×
832

833
      if (runInEnv === true && conf.skipEmailVerification === false) {
×
834
        code = OTP.generateOTP(6)
×
835

×
836
        if (!user.isEmailConfirmed || isEmailChanged) {
837
          try {
×
838
            const { fullName } = userRec
×
839

×
840
            if (!code || !fullName || !email) {
841
              log.error('missing input for sending verification email', { code, fullName, email })
842
              throw new Error('missing input for sending verification email')
×
843
            }
844

845
            const templateData = {
846
              firstname: fullName,
847
              code: parseInt(code)
×
848
            }
849

×
850
            const sesResponse = await sendTemplateEmail(email, templateData)
851

852
            log.debug('sent new user email validation code', {
853
              email,
854
              code,
855
              sesResponse: get(sesResponse, '$response.httpResponse.statusCode'),
856
              sesId: get(sesResponse, 'MessageId'),
857
              sesError: get(sesResponse, '$response.error.message')
×
858
            })
×
859
          } catch (e) {
860
            log.error('failed sending email verification to user:', e.message, e, { userRec, code })
861
            throw e
862
          }
863
        }
864
      }
×
865

866
      // updates/adds user with the emailVerificationCode to be used for verification later
867
      await storage.updateUser({
868
        identifier: user.identifier,
×
869
        emailVerificationCode: code,
870
        otp: {
871
          ...(userRec.otp || {}),
872
          email
873
        }
×
874
      })
875

876
      res.json({ ok: 1, alreadyVerified: isEmailChanged === false && user.isEmailConfirmed })
877
    })
878
  )
879

880
  /**
881
   * @api {post} /verify/email Verify email code
882
   * @apiName Email
883
   * @apiGroup Verification
884
   *
885
   * @apiParam {Object} verificationData
886
   * @apiParam {String} verificationData.code
887
   *
888
   * @apiSuccess {Number} ok
889
   * @apiSuccess {Claim} attestation
2✔
890
   * @ignore
891
   */
892
  app.post(
893
    '/verify/email',
×
894
    passport.authenticate('jwt', { session: false }),
×
895
    wrapAsync(async (req, res) => {
×
896
      let runInEnv = ['production', 'staging', 'test'].includes(conf.env)
×
897
      const { __utmzz: utmString = '' } = req.cookies
×
898
      const log = req.log
899
      const { user, body } = req
×
900
      const verificationData: { code: string } = body.verificationData
×
901

902
      let { email } = user.otp || {}
×
903
      email = email && email.toLowerCase()
×
904

905
      const hashedNewEmail = email ? sha3(email) : null
×
906
      const currentEmail = user.email
907

908
      log.debug('email verification request', {
909
        user,
910
        body,
911
        email,
912
        verificationData,
913
        currentEmail,
914
        hashedNewEmail
×
915
      })
×
916

×
917
      if (!email) {
918
        log.error('email address to verify is missing')
919
        throw new Error('email address to verify is missing')
×
920
      }
921

922
      if (!user.isEmailConfirmed || currentEmail !== hashedNewEmail) {
×
923
        let signedEmail
×
924

×
925
        if (runInEnv && conf.skipEmailVerification === false) {
926
          try {
×
927
            await verifier.verifyEmail({ identifier: user.loggedInAs }, verificationData)
×
928
          } catch (e) {
929
            log.warn('email verification failed:', e.message, { user, email, verificationData })
930
            return res.status(400).json({ ok: 0, error: e.message })
931
          }
×
932
        }
933

934
        storage.updateUser({
935
          identifier: user.loggedInAs,
936
          isEmailConfirmed: true,
937
          email: hashedNewEmail
×
938
        })
×
939

940
        if (runInEnv) {
941
          storage.model.updateOne({ identifier: user.loggedInAs }, { $unset: { 'otp.email': 1 } })
×
942

×
943
          // fire and forget
944
          syncUserEmail(user, email, utmString, log).catch(e =>
945
            log.error('Error updating CRM contact', e.message, e, {
946
              crmId: user.crmId,
947
              currentEmail,
948
              email
949
            })
950
          )
951
        }
×
952

953
        //TODO: sign using ceramic did
954
        return res.json({ ok: 1, attestation: signedEmail })
×
955
      }
956

957
      return res.json({ ok: 0, error: 'nothing to do' })
958
    })
959
  )
960

961
  /**
962
   * @api {get} /verify/phase get release/phase version number
963
   * @apiName Get Phase VErsion Number
964
   * @apiGroup Verification
965
   *
966
   * @apiSuccess {Number} phase
967
   * @apiSuccess {Boolean} success
2✔
968
   * @ignore
×
969
   */
970
  app.get('/verify/phase', (_, res) => {
×
971
    const { phase } = conf
972

973
    res.json({ success: true, phase })
974
  })
975

976
  /**
977
   * @depracated now using goodcfverify cloudflare worker
978
   * @api {post} /verify/recaptcha verify recaptcha token
979
   * @apiName Recaptcha
980
   * @apiGroup Verification
981
   *
982
   * @apiParam {string} token
983
   *
984
   * @apiSuccess {Number} ok
985
   * @ignore
2✔
986
   */
2✔
987

988
  const visitorsCounter = {}
989
  app.post(
990
    '/verify/recaptcha',
×
991
    requestRateLimiter(10, 1),
×
992
    wrapAsync(async (req, res) => {
×
993
      const log = req.log
×
994
      const { payload: token = '', ipv6 = '', captchaType = '', fingerprint = {} } = req.body
×
995
      const clientIp = requestIp.getClientIp(req)
×
996
      const xForwardedFor = (req.headers || {})['x-forwarded-for']
×
997
      const { visitorId } = fingerprint
998
      let kvStorageIpKey = clientIp
×
999
      let parsedRes = {}
×
1000

×
1001
      try {
1002
        if (ipv6 && ipv6 !== clientIp) {
×
1003
          kvStorageIpKey = ipv6
×
1004
        }
×
1005
        let visitsCounter = 0
×
1006
        if (visitorId) {
×
1007
          visitsCounter = visitorsCounter[visitorId] || 0
1008
          visitsCounter++
1009
          visitorsCounter[visitorId] = visitsCounter
×
1010
        }
1011

1012
        log.debug('Verifying recaptcha', {
1013
          token: token.slice(0, 10),
1014
          ipv6,
1015
          clientIp,
1016
          kvStorageIpKey,
1017
          xForwardedFor,
1018
          captchaType,
1019
          visitorId,
1020
          visitsCounter
1021
        })
×
1022

×
1023
        //hcaptcha verify
1024
        if (captchaType === 'hcaptcha') {
×
1025
          if (!visitorId) {
1026
            //we use fingerprint only for web with hcaptcha at the moment
1027
            throw new Error('missing visitorId')
×
1028
          }
1029

1030
          const recaptchaRes = await fetch('https://hcaptcha.com/siteverify', {
1031
            method: 'POST',
1032
            headers: {
1033
              'Content-Type': 'application/x-www-form-urlencoded'
1034
            },
1035
            body: new URLSearchParams({
1036
              secret: conf.hcaptchaSecretKey,
1037
              response: token
×
1038
            })
1039
          })
×
1040
          parsedRes = await recaptchaRes.json()
1041
        } else {
×
1042
          const url = `https://www.google.com/recaptcha/api/siteverify?secret=${conf.recaptchaSecretKey}&response=${token}&remoteip=${clientIp}`
1043

1044
          const recaptchaRes = await fetch(url, {
1045
            method: 'POST',
1046
            headers: {
1047
              'Content-Type': 'application/json',
1048
              Accept: '*/*'
1049
            }
×
1050
          })
1051

1052
          parsedRes = await recaptchaRes.json()
×
1053
        }
×
1054

1055
        if (parsedRes.success) {
×
1056
          const verifyResult = await OTP.verifyCaptcha(kvStorageIpKey, captchaType)
1057

×
1058
          log.debug('Recaptcha verified', { verifyResult, parsedRes })
1059

×
1060
          res.json({ success: true })
1061
        } else {
1062
          throw new Error('user failed captcha')
×
1063
        }
×
1064
      } catch (exception) {
×
1065
        const { message } = exception
1066
        const logFunc = ['user failed captcha', 'missing visitorId'].includes(message) ? 'warn' : 'error'
1067
        log[logFunc]('Recaptcha verification failed', message, exception, {
1068
          clientIp,
1069
          token: token.slice(0, 10),
1070
          captchaType,
×
1071
          parsedRes
1072
        })
1073
        res.status(400).json({ success: false, error: message })
1074
      }
2✔
1075
    })
2✔
1076
  )
1077
  const payouts = new Set()
1078
  app.get(
×
1079
    '/verify/offerwall',
×
1080
    wrapAsync(async (req, res) => {
1081
      const { log, query } = req
1082
      const { user_id, value, token, signature } = query
×
1083

1084
      // Secret key (replace with your actual secret key)
1085
      const secretKey = conf.offerwallSecret
×
1086

1087
      // Concatenate the inputs with "."
1088
      const concatenatedString = `${secretKey}.${user_id}.${parseInt(value)}.${token}`
×
1089

×
1090
      // Generate MD5 hash
×
1091
      const calculatedSignature = crypto.createHash('md5').update(concatenatedString).digest('hex')
1092
      log.debug('offerwall payout request:', { user_id, value, token, signature, calculatedSignature })
×
1093
      try {
×
1094
        // Compare the calculated signature with the provided signature
1095
        if (calculatedSignature !== signature) {
×
1096
          throw new Error('Invalid signature')
×
1097
        }
1098
        if (payouts.has(token)) {
×
1099
          throw new Error('Already paid')
×
1100
        }
×
1101
        const tx = await AdminWallet.walletsMap[42220].transferWalletGoodDollars(user_id, toWei(String(value)), log)
×
1102
        payouts.add(token)
1103
        log.info('offerwall payout success:', { user_id, value, token, tx: tx.transactionHash })
×
1104
        res.json({ success: true })
1105
      } catch (exception) {
×
1106
        const { message } = exception
×
1107

1108
        log.error('offerwall payout request failed:', message, exception, { user_id, value, token, signature })
1109
        res.status(400).json({ success: false, error: message })
1110
      }
1111
    })
1112
  )
1113
}
1114

1115
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