• 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 {
13
  findFaucetAbuse
14
  // findGDTx
15
} from '../blockchain/explorer'
16
import { onlyInEnv, wrapAsync } from '../utils/helpers'
17
import requestRateLimiter, { userRateLimiter } from '../utils/requestRateLimiter'
18
import OTP from '../../imports/otp'
19
import conf from '../server.config'
20
import OnGage from '../crm/ongage'
21
import { sendTemplateEmail } from '../aws-ses/aws-ses'
22
import fetch from 'cross-fetch'
23
import createEnrollmentProcessor from './processor/EnrollmentProcessor.js'
24
import createIdScanProcessor from './processor/IdScanProcessor'
25

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

32
import ipcache from '../db/mongo/ipcache-provider.js'
33
import { DelayedTaskStatus } from '../db/mongo/models/delayed-task.js'
34

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

42
  // for v2 identifier - verify that identifier is for the address we are going to whitelist
43
  await verifyIdentifier(enrollmentIdentifier, gdAddress)
44

45
  const { v2Identifier, v1Identifier } = normalizeIdentifiers(enrollmentIdentifier, fvSigner)
46

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

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

58
  if (isV1) {
59
    await processor.enqueueDisposal(user, v1Identifier, log)
60
  }
61
}
62
*/
63

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

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

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

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

133
      try {
3✔
134
        const authenticationPeriod = await AdminWallet.getAuthenticationPeriod()
3✔
135

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

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

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

176
      try {
1✔
177
        const { v2Identifier, v1Identifier } = normalizeIdentifiers(enrollmentIdentifier, fvSigner)
1✔
178

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

×
185
        res.json({ success: true, isDisposing: !!isDisposingV2 || !!isDisposingV1 })
186
      } catch (exception) {
×
187
        const { message } = exception
×
188

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

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

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

1✔
211
      try {
212
        if (!conf.zoomProductionMode) {
213
          throw new Error('Cannot obtain production license running non-production mode.')
3✔
214
        }
3✔
215

216
        const processor = createEnrollmentProcessor(storage, log)
1✔
217
        const license = await processor.getLicenseKey(licenseType, log)
218

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

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

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

2✔
243
      try {
244
        const processor = createEnrollmentProcessor(storage, log)
1✔
245
        const sessionToken = await processor.issueSessionToken(log)
246

1✔
247
        res.json({ success: true, sessionToken })
248
      } catch (exception) {
1✔
249
        const { message } = exception
1✔
250

251
        log.error('generating enrollment session token failed:', message, exception, { user })
252
        res.status(400).json({ success: false, error: message })
253
      }
254
    })
255
  )
256

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

276
      log.debug('enroll face request:', { fvSigner, enrollmentIdentifier, chainId, user })
277

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

284
      // user.chainId = chainId || conf.defaultWhitelistChainId
285

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

299
      try {
11✔
300
        // for v2 identifier - verify that identifier is for the address we are going to whitelist
301
        // for v1 this will do nothing
11✔
302
        try {
11✔
303
          await verifyIdentifier(enrollmentIdentifier, gdAddress, chainId)
304
        } catch (e) {
305
          log.warn('verifyIdentifier failed:', e.message, e, {
11!
306
            enrollmentIdentifier,
307
            fvSigner,
11✔
308
            gdAddress,
309
            chainId: e.chainId,
310
            rpc: e.rpc
311
          })
11!
312
          throw e
×
313
        }
×
314
        log.debug('FV identifier verification success', { enrollmentIdentifier, gdAddress, chainId })
315
        const { v2Identifier, v1Identifier } = normalizeIdentifiers(enrollmentIdentifier, fvSigner)
316
        const enrollmentProcessor = createEnrollmentProcessor(storage, log)
317
        log.debug('checking if user was previously registered with a v1 identifier before enrolling:', {
318
          v2Identifier,
11✔
319
          v1Identifier
7✔
320
        })
7✔
321
        // here we check if wallet was registered using a v1 identifier
322
        const isV1 = !!v1Identifier && (await enrollmentProcessor.isIdentifierExists(v1Identifier))
323

7✔
324
        try {
1✔
325
          // if v1, we convert to v2
1✔
326
          // delete previous enrollment.
1✔
327
          // once user completes FV it will create a new record under his V2 id. update his lastAuthenticated. and enqueue for disposal
1!
328
          if (isV1) {
329
            log.info('v1 identifier found, converting to v2', { v1Identifier, v2Identifier, gdAddress })
330
            await Promise.all([
1✔
331
              enrollmentProcessor.dispose(v1Identifier, log),
332
              cancelDisposalTask(storage, v1Identifier)
333
            ])
7!
334
          }
×
335
          log.debug('starting enrollment process:', { v2Identifier, v1Identifier, isV1 })
336
          await enrollmentProcessor.validate(user, v2Identifier, payload)
337
          log.debug('enrollment validation success:', { v2Identifier, v1Identifier, isV1 })
338
          const wasWhitelisted = await AdminWallet.lastAuthenticated(gdAddress)
339
          log.debug('user last authenticated:', { wasWhitelisted, gdAddress })
340
          const enrollmentResult = await enrollmentProcessor.enroll(user, v2Identifier, payload, log)
×
341

342
          // fetch duplicate expiration
×
343
          if (enrollmentResult.success === false && enrollmentResult.enrollmentResult?.isDuplicate) {
344
            const dup = enrollmentResult.enrollmentResult.duplicate.identifier
345
            const authenticationPeriod = await AdminWallet.getAuthenticationPeriod()
7!
346
            const record = await getDisposalTask(storage, dup)
347
            const expiration = moment(record?.createdAt || 0)
348
              .add(authenticationPeriod + 1, 'days')
349
              .toISOString()
350
            enrollmentResult.enrollmentResult.duplicate.expiration = expiration
351
          }
7✔
352
          // log warn if user was whitelisted but unable to pass FV again
353
          if (wasWhitelisted > 0 && enrollmentResult.success === false) {
4!
354
            log.warn('user failed to re-authenticate', {
355
              wasWhitelisted,
356
              enrollmentResult,
×
357
              gdAddress,
358
              v2Identifier
×
359
            })
×
360
            if (isV1) {
×
361
              //throw error so we de-whitelist user
362
              throw new Error('User failed to re-authenticate with V1 identifier')
×
363
            }
364
          }
365
          log.info(wasWhitelisted > 0 && enrollmentResult.success ? 'user re-authenticated' : 'user enrolled', {
4✔
366
            wasWhitelisted,
367
            enrollmentResult,
368
            gdAddress,
4✔
369
            v2Identifier
4✔
370
          })
371
          res.json(enrollmentResult)
4!
372
        } catch (e) {
×
373
          if (isV1) {
374
            // if we deleted the user record but had an error in whitelisting, then we must revoke his whitelisted status
4✔
375
            // since we might not have his record enrolled
376
            const isIndexed = await enrollmentProcessor.isIdentifierIndexed(v2Identifier)
377
            // if new identifier is indexed then dont revoke
4✔
378
            if (!isIndexed) {
379
              const isWhitelisted = await AdminWallet.isVerified(gdAddress)
380
              if (isWhitelisted) await AdminWallet.removeWhitelisted(gdAddress)
381
            }
382
            log.error('failed converting v1 identifier', e.message, e, { isIndexed, gdAddress })
383
          }
384

385
          throw e
386
        }
387
      } catch (exception) {
388
        const { message } = exception
389
        const logArgs = ['Face verification error:', message, exception, { enrollmentIdentifier, fvSigner, gdAddress }]
390

391
        if (shouldLogVerificaitonError(exception)) {
392
          log.error(...logArgs)
2✔
393
        } else {
394
          log.warn(...logArgs)
395
        }
396

×
397
        res.status(400).json({ success: false, error: message })
×
398
      }
×
399
    })
×
400
  )
401

×
402
  /**
403
   * @api {put} /verify/idscan:enrollmentIdentifier Verify id matches face and does OCR
404
   * @apiName IDScan
405
   * @apiGroup IDScanq
×
406
   *
×
407
   * @apiParam {String} enrollmentIdentifier
408
   * @apiParam {String} sessionId
409
   *
×
410
   * @ignore
411
   */
412
  app.put(
×
413
    '/verify/idscan/:enrollmentIdentifier',
414
    passport.authenticate('jwt', { session: false }),
×
415
    wrapAsync(async (req, res) => {
416
      const { user, log, params, body } = req
×
417
      const { enrollmentIdentifier } = params
418
      const { chainId, ...payload } = body || {} // payload is the facetec data
×
419
      const { gdAddress } = user
×
420

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

×
423
      // checking if request aborted to handle cases when connection is slow
×
424
      // and facemap / images were uploaded more that 30sec causing timeout
425
      if (req.aborted) {
×
426
        return
×
427
      }
428

×
429
      try {
×
430
        // for v2 identifier - verify that identifier is for the address we are going to whitelist
431
        // for v1 this will do nothing
×
432
        await verifyIdentifier(enrollmentIdentifier, gdAddress, chainId)
433

434
        const { v2Identifier } = normalizeIdentifiers(enrollmentIdentifier)
×
435

436
        const idscanProcessor = createIdScanProcessor(storage, log)
437

438
        let { isMatch, scanResultBlob, ...scanResult } = await idscanProcessor.verify(user, v2Identifier, payload)
439
        scanResult = omit(scanResult, ['externalDatabaseRefID', 'ocrResults', 'serverInfo', 'callData']) //remove unrequired fields
440
        log.debug('idscan results:', { isMatch, scanResult })
441
        const toSign = { success: true, isMatch, gdAddress, scanResult, timestamp: Date.now() }
442
        const { sig: signature } = await AdminWallet.signMessage(JSON.stringify(toSign))
443
        res.json({ scanResult: { ...toSign, signature }, scanResultBlob })
2✔
444
      } catch (exception) {
445
        const { message } = exception
446
        const logArgs = ['idscan error:', message, exception, { enrollmentIdentifier, gdAddress }]
×
447

×
448
        if (shouldLogVerificaitonError(exception)) {
449
          log.error(...logArgs)
×
450
        } else {
451
          log.warn(...logArgs)
×
452
        }
×
453

×
454
        res.status(400).json({ success: false, error: message })
×
455
      }
×
456
    })
×
457
  )
×
458

459
  /**
460
   * demo for verifying that idscan was created by us
461
   * trigger defender autotask
462
   */
463
  app.post(
464
    '/verify/idscan',
465
    wrapAsync(async (req, res) => {
466
      const { log, body } = req
467
      const { scanResult } = body || {} // payload is the facetec data
468

469
      log.debug('idscan submit:', { payloadFields: Object.keys(scanResult) })
470

×
471
      try {
472
        const { signature, ...signed } = scanResult
×
473
        const { gdAddress } = signed
×
474
        const mHash = keccak256(JSON.stringify(signed))
×
475
        log.debug('idscan submit verifying...:', { mHash, signature })
476
        const publicKey = recoverPublickey(signature, mHash, '')
477
        const data = web3Abi.encodeFunctionCall(
×
478
          {
479
            name: 'addMember',
480
            type: 'function',
481
            inputs: [
482
              {
483
                type: 'address',
×
484
                name: 'member'
485
              }
×
486
            ]
487
          },
×
488
          [gdAddress]
489
        )
×
490
        const relayer = await AdminWallet.signer.relayer.getRelayer()
491

×
492
        log.debug('idscan submit verified...:', { publicKey, relayer })
493
        if (publicKey !== relayer.address.toLowerCase()) {
494
          throw new Error('invalid signer: ' + publicKey)
495
        }
496

497
        const addTx = await AdminWallet.signer.sendTx({
498
          to: '0x163a99a51fE32eEaC7407687522B5355354a1a37',
499
          data,
500
          gasLimit: '300000'
501
        })
502

503
        log.debug('idscan adding member:', addTx)
504

505
        res.json({ ok: 1, txHash: addTx.hash })
2✔
506
      } catch (exception) {
507
        const { message } = exception
508

509
        log.error('idscan submit failed:', message, exception)
510

511
        res.status(400).json({ ok: 0, error: message })
×
512
      }
×
513
    })
514
  )
×
515
  /**
516
   * @api {post} /verify/sendotp Sends OTP
×
517
   * @apiName Send OTP
518
   * @apiGroup Verification
×
519
   *
×
520
   * @apiParam {UserRecord} user
×
521
   *
522
   * @apiSuccess {Number} ok
×
523
   * @ignore
524
   */
×
525
  app.post(
×
526
    '/verify/sendotp',
527
    passport.authenticate('jwt', { session: false }),
528
    userRateLimiter(2, 1), // 1 req / 1min, should be applied AFTER auth to have req.user been set
×
529
    // also no need for reqRateLimiter as user limiter falls back to the ip (e.g. works as default limiter if no user)
×
530
    wrapAsync(async (req, res) => {
×
531
      const { user, body } = req
×
532
      const log = req.log
×
533

×
534
      log.info('otp request:', { user, body })
535

×
536
      const onlyCheckAlreadyVerified = body.onlyCheckAlreadyVerified
537

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

×
542
      const savedMobile = user.mobile
543

544
      if (conf.allowDuplicateUserData === false && (await storage.isDupUserData({ mobile: hashedMobile }))) {
545
        return res.json({ ok: 0, error: 'mobile_already_exists' })
546
      }
547

548
      if (!userRec.smsValidated || hashedMobile !== savedMobile) {
×
549
        if (!onlyCheckAlreadyVerified) {
550
          log.debug('sending otp:', user.loggedInAs)
551
          if (['production', 'staging'].includes(conf.env)) {
552
            const clientIp = requestIp.getClientIp(req)
553
            const sendResult = await OTP.sendOTP(mobile, get(body, 'user.otpChannel', 'sms'), clientIp)
554

555
            log.info('otp sent:', mobile, { user: user.loggedInAs, sendResult })
556
          }
557

558
          await storage.updateUser({
559
            identifier: user.loggedInAs,
560
            otp: {
561
              ...(userRec.otp || {}),
562
              mobile
563
            }
2✔
564
          })
565
        }
566
      }
567

568
      res.json({ ok: 1, alreadyVerified: hashedMobile === savedMobile && user.smsValidated })
569
    })
×
570
  )
×
571

×
572
  /**
×
573
   * @api {post} /verify/mobile Verify mobile data code
574
   * @apiName OTP Code
×
575
   * @apiGroup Verification
×
576
   *
577
   * @apiParam {Object} verificationData
578
   *
579
   * @apiSuccess {Number} ok
580
   * @apiSuccess {Claim} attestation
×
581
   * @ignore
582
   */
583
  app.post(
×
584
    '/verify/mobile',
×
585
    requestRateLimiter(10, 1),
586
    passport.authenticate('jwt', { session: false }),
×
587
    onlyInEnv('production', 'staging'),
588
    wrapAsync(async (req, res) => {
×
589
      const log = req.log
×
590
      const { user, body } = req
×
591
      const verificationData: { otp: string } = body.verificationData
×
592
      const { mobile } = user.otp || {}
593

×
594
      if (!mobile) {
×
595
        log.warn('mobile to verify not found or missing', 'mobile missing', new Error('mobile missing'), {
596
          user,
597
          verificationData
598
        })
×
599

600
        return res.status(400).json({ ok: 0, error: 'MOBILE MISSING' })
×
601
      }
×
602

603
      const hashedNewMobile = mobile && sha3(mobile)
604
      const currentMobile = user.mobile
605

606
      log.debug('mobile verified', { user, verificationData, hashedNewMobile })
×
607

608
      if (!user.smsValidated || currentMobile !== hashedNewMobile) {
×
609
        try {
×
610
          const clientIp = requestIp.getClientIp(req)
611
          await verifier.verifyMobile({ identifier: user.loggedInAs, mobile }, verificationData, clientIp)
612
        } catch (e) {
613
          log.warn('mobile verification failed:', e.message, { user, mobile, verificationData })
×
614
          return res.status(400).json({ ok: 0, error: 'OTP FAILED', message: e.message })
615
        }
616

617
        let updIndexPromise
618
        const { crmId } = user
619

620
        if (currentMobile && currentMobile !== hashedNewMobile) {
×
621
          updIndexPromise = Promise.all([
622
            //TODO: generate ceramic claim
623
          ])
624
        }
625

626
        if (crmId) {
627
          //fire and forget
×
628
          OnGage.updateContact(null, crmId, { mobile }, log).catch(e =>
629
            log.error('Error updating CRM contact', e.message, e, { crmId, mobile })
630
          )
631
        }
632

633
        await Promise.all([
634
          updIndexPromise,
635
          storage.updateUser({
636
            identifier: user.loggedInAs,
637
            smsValidated: true,
638
            mobile: hashedNewMobile
2✔
639
          }),
640
          user.createdDate && //keep temporary field if user is signing up
641
            storage.model.updateOne({ identifier: user.loggedInAs }, { $unset: { 'otp.mobile': true } })
642
        ])
×
643
      }
×
644

645
      //TODO: replace with ceramic
646
      let signedMobile
647
      res.json({ ok: 1, attestation: signedMobile })
648
    })
649
  )
650

651
  /**
652
   * @api {post} /verify/registration Verify user registration status
653
   * @apiName Verify Registration Status
654
   * @apiGroup Verification
655
   * @apiSuccess {Number} ok
656
   * @ignore
2✔
657
   */
658
  app.post(
659
    '/verify/registration',
660
    passport.authenticate('jwt', { session: false }),
661
    wrapAsync(async (req, res) => {
×
662
      const user = req.user
×
663
      res.json({ ok: user && user.createdDate ? 1 : 0 })
×
664
    })
×
665
  )
×
666
  /**
667
   * @api {post} /verify/topwallet Tops Users Wallet if needed
×
668
   * @apiName Top Wallet
×
669
   * @apiGroup Verification
×
670
   *
671
   * @apiParam {LoggedUser} user
672
   *
673
   * @apiSuccess {Number} ok
674
   * @ignore
675
   */
676
  app.post(
677
    '/verify/topwallet',
678
    requestRateLimiter(3, 1),
679
    passport.authenticate(['jwt', 'anonymous'], { session: false }),
680
    wrapAsync(async (req, res) => {
×
681
      const log = req.log
×
682
      const { origin, host } = req.headers
×
683
      const { account, chainId = 42220 } = req.body || {}
684
      const user: LoggedUser = req.user || { gdAddress: account }
685
      const clientIp = requestIp.getClientIp(req)
×
686

×
687
      const gdContract = AdminWallet.walletsMap[chainId]?.tokenContract?._address
×
688
      const faucetContract = AdminWallet.walletsMap[chainId]?.faucetContract?._address
689
      log.debug('topwallet tx request:', {
690
        address: user.gdAddress,
×
691
        chainId,
692
        gdContract,
×
693
        faucetContract,
694
        user: req.user,
695
        origin,
696
        host,
697
        clientIp
698
      })
×
699

700
      if (!faucetContract) {
701
        log.warn('topWallet unsupported chain', { chainId })
×
702
        return res.json({ ok: -1, error: 'unsupported chain' })
×
703
      }
704

705
      if (conf.env === 'production') {
706
        if (!(await AdminWallet.isConnected(user.gdAddress))) {
×
707
          log.warn('topwallet denied user not whitelisted', {
×
708
            address: user.gdAddress,
×
709
            origin,
710
            chainId,
711
            clientIp
×
712
          })
×
713
          return res.json({ ok: -1, error: 'not whitelisted' })
×
714
        }
715
      }
716
      if (!user.gdAddress) {
×
717
        throw new Error('missing wallet address to top')
×
718
      }
×
719

720
      // check for faucet abuse
721
      const foundAbuse = await cachedFindFaucetAbuse(user.gdAddress, chainId).catch(e => {
722
        log.error('findFaucetAbuse failed', e.message, e, { address: user.gdAddress, chainId })
723
        return
×
724
      })
×
725

726
      if (foundAbuse) {
×
727
        log.warn('faucet abuse found:', foundAbuse)
728
        return res.json({ ok: -1, error: 'faucet abuse: ' + foundAbuse.hash })
729
      }
×
730

×
731
      const foundMultiIpAccounts = await checkMultiIpAccounts(user.gdAddress, clientIp, log)
732
      if (foundMultiIpAccounts) {
×
733
        log.warn('faucet multiip abuse found:', foundMultiIpAccounts.length, new Error('faucet multiip abuse'), {
×
734
          foundMultiIpAccounts,
735
          clientIp,
736
          account: user.gdAddress
×
737
        })
×
738
        AdminWallet.banInFaucet(foundMultiIpAccounts, 'all', log).catch(e => {
739
          log.error('findFaucetAbuse banInFaucet failed:', e.message, e, {
×
740
            address: user.gdAddress,
741
            foundMultiIpAccounts
742
          })
×
743
        })
744
        return res.json({ ok: -1, error: 'faucet multi abuse' })
×
745
      }
746

747
      try {
748
        let txPromise = AdminWallet.topWallet(user.gdAddress, chainId, log)
749
          .then(tx => {
×
750
            log.debug('topwallet tx', { walletaddress: user.gdAddress, tx })
751
            return { ok: 1 }
×
752
          })
×
753
          .catch(async exception => {
754
            const { message } = exception
755
            log.warn('Failed topwallet tx', message, exception, { walletaddress: user.gdAddress }) //errors are already logged in adminwallet so jsut warn
756

757
            return { ok: -1, error: message }
758
          })
759

760
        const txRes = await txPromise
761

762
        log.info('topwallet tx done', {
763
          txRes,
764
          loggedInAs: user.loggedInAs
765
        })
766

767
        res.json(txRes)
2✔
768
      } catch (e) {
769
        log.error('topwallet timeout or unexpected', e.message, e, { walletaddress: user.gdAddress })
770
        res.json({ ok: -1, error: e.message })
771
      }
772
    })
×
773
  )
×
774

×
775
  /**
776
   * @api {post} /verify/swaphelper trigger a non custodial swap
×
777
   * @apiName Swaphelper
×
778
   * @apiGroup Verification
×
779
   *
780
   * @apiParam {account} user
781
   *
×
782
   * @apiSuccess {Number} ok
783
   * @ignore
784
   */
×
785
  app.post(
×
786
    '/verify/swaphelper',
787
    requestRateLimiter(3, 1),
×
788
    passport.authenticate(['jwt', 'anonymous'], { session: false }),
789
    wrapAsync(async (req, res) => {
×
790
      const log = req.log
×
791
      const { account, chainId = 42220 } = req.body || {}
792
      const gdAddress = account || get(req.user, 'gdAddress')
793

794
      log.info('swaphelper request:', { gdAddress, chainId })
795
      if (!gdAddress) {
796
        throw new Error('missing user account')
797
      }
798

799
      try {
800
        //verify target helper address has funds
801

802
        const tx = await AdminWallet.walletsMap[chainId].swaphelper(gdAddress, log)
803
        log.info('swaphelper request done:', { gdAddress, chainId, tx })
804

805
        res.json({ ok: 1, hash: tx.transactionHash })
2✔
806
      } catch (e) {
807
        log.error('swaphelper timeout or unexpected', e.message, e, { walletaddress: gdAddress, chainId })
808
        res.json({ ok: -1, error: e.message })
809
      }
810
    })
×
811
  )
×
812

×
813
  /**
814
   * @api {post} /verify/email Send verification email endpoint
×
815
   * @apiName Send Email
×
816
   * @apiGroup Verification
817
   *
×
818
   * @apiParam {UserRecord} user
×
819
   *
×
820
   * @apiSuccess {Number} ok
821
   * @ignore
822
   */
823
  app.post(
×
824
    '/verify/sendemail',
×
825
    requestRateLimiter(2, 1),
×
826
    passport.authenticate('jwt', { session: false }),
827
    wrapAsync(async (req, res) => {
828
      let runInEnv = ['production', 'staging', 'test'].includes(conf.env)
×
829
      const log = req.log
830
      const { user, body } = req
×
831

×
832
      let { email } = body.user
833
      email = email.toLowerCase()
×
834

×
835
      if (!email || !user) {
×
836
        log.warn('email verification email or user record not found:', { email, user })
837
        return res.json({ ok: 0, error: 'email or user missing' })
×
838
      }
×
839

×
840
      //merge user details
841
      const { email: currentEmail } = user
842
      let userRec: UserRecord = defaults(body.user, user)
×
843
      const isEmailChanged = currentEmail && currentEmail !== sha3(email)
844

845
      let code
846
      log.debug('email verification request:', { email, currentEmail, isEmailChanged, body, user })
847

×
848
      if (runInEnv === true && conf.skipEmailVerification === false) {
849
        code = OTP.generateOTP(6)
×
850

851
        if (!user.isEmailConfirmed || isEmailChanged) {
852
          try {
853
            const { fullName } = userRec
854

855
            if (!code || !fullName || !email) {
856
              log.error('missing input for sending verification email', { code, fullName, email })
857
              throw new Error('missing input for sending verification email')
×
858
            }
×
859

860
            const templateData = {
861
              firstname: fullName,
862
              code: parseInt(code)
863
            }
864

×
865
            const sesResponse = await sendTemplateEmail(email, templateData)
866

867
            log.debug('sent new user email validation code', {
868
              email,
×
869
              code,
870
              sesResponse: get(sesResponse, '$response.httpResponse.statusCode'),
871
              sesId: get(sesResponse, 'MessageId'),
872
              sesError: get(sesResponse, '$response.error.message')
873
            })
×
874
          } catch (e) {
875
            log.error('failed sending email verification to user:', e.message, e, { userRec, code })
876
            throw e
877
          }
878
        }
879
      }
880

881
      // updates/adds user with the emailVerificationCode to be used for verification later
882
      await storage.updateUser({
883
        identifier: user.identifier,
884
        emailVerificationCode: code,
885
        otp: {
886
          ...(userRec.otp || {}),
887
          email
888
        }
889
      })
2✔
890

891
      res.json({ ok: 1, alreadyVerified: isEmailChanged === false && user.isEmailConfirmed })
892
    })
893
  )
×
894

×
895
  /**
×
896
   * @api {post} /verify/email Verify email code
×
897
   * @apiName Email
×
898
   * @apiGroup Verification
899
   *
×
900
   * @apiParam {Object} verificationData
×
901
   * @apiParam {String} verificationData.code
902
   *
×
903
   * @apiSuccess {Number} ok
×
904
   * @apiSuccess {Claim} attestation
905
   * @ignore
×
906
   */
907
  app.post(
908
    '/verify/email',
909
    passport.authenticate('jwt', { session: false }),
910
    wrapAsync(async (req, res) => {
911
      let runInEnv = ['production', 'staging', 'test'].includes(conf.env)
912
      const { __utmzz: utmString = '' } = req.cookies
913
      const log = req.log
914
      const { user, body } = req
×
915
      const verificationData: { code: string } = body.verificationData
×
916

×
917
      let { email } = user.otp || {}
918
      email = email && email.toLowerCase()
919

×
920
      const hashedNewEmail = email ? sha3(email) : null
921
      const currentEmail = user.email
922

×
923
      log.debug('email verification request', {
×
924
        user,
×
925
        body,
926
        email,
×
927
        verificationData,
×
928
        currentEmail,
929
        hashedNewEmail
930
      })
931

×
932
      if (!email) {
933
        log.error('email address to verify is missing')
934
        throw new Error('email address to verify is missing')
935
      }
936

937
      if (!user.isEmailConfirmed || currentEmail !== hashedNewEmail) {
×
938
        let signedEmail
×
939

940
        if (runInEnv && conf.skipEmailVerification === false) {
941
          try {
×
942
            await verifier.verifyEmail({ identifier: user.loggedInAs }, verificationData)
×
943
          } catch (e) {
944
            log.warn('email verification failed:', e.message, { user, email, verificationData })
945
            return res.status(400).json({ ok: 0, error: e.message })
946
          }
947
        }
948

949
        storage.updateUser({
950
          identifier: user.loggedInAs,
951
          isEmailConfirmed: true,
×
952
          email: hashedNewEmail
953
        })
954

×
955
        if (runInEnv) {
956
          storage.model.updateOne({ identifier: user.loggedInAs }, { $unset: { 'otp.email': 1 } })
957

958
          // fire and forget
959
          syncUserEmail(user, email, utmString, log).catch(e =>
960
            log.error('Error updating CRM contact', e.message, e, {
961
              crmId: user.crmId,
962
              currentEmail,
963
              email
964
            })
965
          )
966
        }
967

2✔
968
        //TODO: sign using ceramic did
×
969
        return res.json({ ok: 1, attestation: signedEmail })
970
      }
×
971

972
      return res.json({ ok: 0, error: 'nothing to do' })
973
    })
974
  )
975

976
  /**
977
   * @api {get} /verify/phase get release/phase version number
978
   * @apiName Get Phase VErsion Number
979
   * @apiGroup Verification
980
   *
981
   * @apiSuccess {Number} phase
982
   * @apiSuccess {Boolean} success
983
   * @ignore
984
   */
985
  app.get('/verify/phase', (_, res) => {
2✔
986
    const { phase } = conf
2✔
987

988
    res.json({ success: true, phase })
989
  })
990

×
991
  /**
×
992
   * @depracated now using goodcfverify cloudflare worker
×
993
   * @api {post} /verify/recaptcha verify recaptcha token
×
994
   * @apiName Recaptcha
×
995
   * @apiGroup Verification
×
996
   *
×
997
   * @apiParam {string} token
998
   *
×
999
   * @apiSuccess {Number} ok
×
1000
   * @ignore
×
1001
   */
1002

×
1003
  const visitorsCounter = {}
×
1004
  app.post(
×
1005
    '/verify/recaptcha',
×
1006
    requestRateLimiter(10, 1),
×
1007
    wrapAsync(async (req, res) => {
1008
      const log = req.log
1009
      const { payload: token = '', ipv6 = '', captchaType = '', fingerprint = {} } = req.body
×
1010
      const clientIp = requestIp.getClientIp(req)
1011
      const xForwardedFor = (req.headers || {})['x-forwarded-for']
1012
      const { visitorId } = fingerprint
1013
      let kvStorageIpKey = clientIp
1014
      let parsedRes = {}
1015

1016
      try {
1017
        if (ipv6 && ipv6 !== clientIp) {
1018
          kvStorageIpKey = ipv6
1019
        }
1020
        let visitsCounter = 0
1021
        if (visitorId) {
×
1022
          visitsCounter = visitorsCounter[visitorId] || 0
×
1023
          visitsCounter++
1024
          visitorsCounter[visitorId] = visitsCounter
×
1025
        }
1026

1027
        log.debug('Verifying recaptcha', {
×
1028
          token: token.slice(0, 10),
1029
          ipv6,
1030
          clientIp,
1031
          kvStorageIpKey,
1032
          xForwardedFor,
1033
          captchaType,
1034
          visitorId,
1035
          visitsCounter
1036
        })
1037

×
1038
        //hcaptcha verify
1039
        if (captchaType === 'hcaptcha') {
×
1040
          if (!visitorId) {
1041
            //we use fingerprint only for web with hcaptcha at the moment
×
1042
            throw new Error('missing visitorId')
1043
          }
1044

1045
          const recaptchaRes = await fetch('https://hcaptcha.com/siteverify', {
1046
            method: 'POST',
1047
            headers: {
1048
              'Content-Type': 'application/x-www-form-urlencoded'
1049
            },
×
1050
            body: new URLSearchParams({
1051
              secret: conf.hcaptchaSecretKey,
1052
              response: token
×
1053
            })
×
1054
          })
1055
          parsedRes = await recaptchaRes.json()
×
1056
        } else {
1057
          const url = `https://www.google.com/recaptcha/api/siteverify?secret=${conf.recaptchaSecretKey}&response=${token}&remoteip=${clientIp}`
×
1058

1059
          const recaptchaRes = await fetch(url, {
×
1060
            method: 'POST',
1061
            headers: {
1062
              'Content-Type': 'application/json',
×
1063
              Accept: '*/*'
×
1064
            }
×
1065
          })
1066

1067
          parsedRes = await recaptchaRes.json()
1068
        }
1069

1070
        if (parsedRes.success) {
×
1071
          const verifyResult = await OTP.verifyCaptcha(kvStorageIpKey, captchaType)
1072

1073
          log.debug('Recaptcha verified', { verifyResult, parsedRes })
1074

2✔
1075
          res.json({ success: true })
2✔
1076
        } else {
1077
          throw new Error('user failed captcha')
1078
        }
×
1079
      } catch (exception) {
×
1080
        const { message } = exception
1081
        const logFunc = ['user failed captcha', 'missing visitorId'].includes(message) ? 'warn' : 'error'
1082
        log[logFunc]('Recaptcha verification failed', message, exception, {
×
1083
          clientIp,
1084
          token: token.slice(0, 10),
1085
          captchaType,
×
1086
          parsedRes
1087
        })
1088
        res.status(400).json({ success: false, error: message })
×
1089
      }
×
1090
    })
×
1091
  )
1092
  const payouts = new Set()
×
1093
  app.get(
×
1094
    '/verify/offerwall',
1095
    wrapAsync(async (req, res) => {
×
1096
      const { log, query } = req
×
1097
      const { user_id, value, token, signature } = query
1098

×
1099
      // Secret key (replace with your actual secret key)
×
1100
      const secretKey = conf.offerwallSecret
×
1101

×
1102
      // Concatenate the inputs with "."
1103
      const concatenatedString = `${secretKey}.${user_id}.${parseInt(value)}.${token}`
×
1104

1105
      // Generate MD5 hash
×
1106
      const calculatedSignature = crypto.createHash('md5').update(concatenatedString).digest('hex')
×
1107
      log.debug('offerwall payout request:', { user_id, value, token, signature, calculatedSignature })
1108
      try {
1109
        // Compare the calculated signature with the provided signature
1110
        if (calculatedSignature !== signature) {
1111
          throw new Error('Invalid signature')
1112
        }
1113
        if (payouts.has(token)) {
1114
          throw new Error('Already paid')
1115
        }
1116
        const tx = await AdminWallet.walletsMap[42220].transferWalletGoodDollars(user_id, toWei(String(value)), log)
1117
        payouts.add(token)
1118
        log.info('offerwall payout success:', { user_id, value, token, tx: tx.transactionHash })
1119
        res.json({ success: true })
1120
      } catch (exception) {
1121
        const { message } = exception
1122

1123
        log.error('offerwall payout request failed:', message, exception, { user_id, value, token, signature })
1124
        res.status(400).json({ success: false, error: message })
1125
      }
1126
    })
1127
  )
1128
}
1129

1130
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