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

GoodDollar / GoodServer / 13050841606

14 Jan 2025 12:39PM UTC coverage: 49.666% (+0.09%) from 49.574%
13050841606

push

github

sirpy
fix: remove unused

584 of 1457 branches covered (40.08%)

Branch coverage included in aggregate %.

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

21.84
/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
2✔
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

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

2✔
74
let faucetAddressBlocked = {}
×
75
const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000
×
76
const checkMultiIpAccounts = async (account, ip, logger) => {
×
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
2!
89
}
×
90

91
if (conf.env !== 'test')
×
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
2✔
109
  ) // clear every 1 day (24 hours)
110

111
const setup = (app: Router, verifier: VerificationAPI, storage: StorageAPI) => {
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
   *
2✔
120
   * @ignore
121
   */
122
  app.delete(
123
    '/verify/face/:enrollmentIdentifier',
3✔
124
    passport.authenticate('jwt', { session: false }),
3✔
125
    wrapAsync(async (req, res) => {
3✔
126
      const { params, query, log, user } = req
127
      const { enrollmentIdentifier } = params
3✔
128
      const { fvSigner = '' } = query
3✔
129

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

3✔
133
        const record = await getDisposalTask(storage, enrollmentIdentifier)
2✔
134
        log.debug('get face disposal task result:', { enrollmentIdentifier, record })
135
        if (record == null || record.status === DelayedTaskStatus.Complete) {
1✔
136
          return res.json({ success: true, status: DelayedTaskStatus.Complete })
137
        }
138
        return res.json({
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
   *
2✔
162
   * @ignore
163
   */
164
  app.get(
165
    '/verify/face/:enrollmentIdentifier',
1✔
166
    passport.authenticate('jwt', { session: false }),
1✔
167
    wrapAsync(async (req, res) => {
1✔
168
      const { params, log, user, query } = req
1✔
169
      const { enrollmentIdentifier } = params
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([
178
          processor.isEnqueuedForDisposal(v2Identifier, log),
179
          v1Identifier && processor.isEnqueuedForDisposal(v1Identifier, log)
1✔
180
        ])
181

×
182
        res.json({ success: true, isDisposing: !!isDisposingV2 || !!isDisposingV1 })
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
   *
2✔
197
   * @ignore
198
   */
199
  app.post(
200
    '/verify/face/license/:licenseType',
4✔
201
    passport.authenticate('jwt', { session: false }),
4✔
202
    wrapAsync(async (req, res) => {
203
      const { log, user, params } = req
4✔
204
      const { licenseType } = params
205

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

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

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

3✔
216
        res.json({ success: true, license })
3✔
217
      } catch (exception) {
3✔
218
        const { message } = exception
219
        log.error('getting FaceTec license failed:', message, exception, { user })
220
        res.status(400).json({ success: false, error: message })
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
   *
2✔
230
   * @ignore
231
   */
232
  app.post(
233
    '/verify/face/session',
2✔
234
    passport.authenticate('jwt', { session: false }),
235
    wrapAsync(async (req, res) => {
2✔
236
      const { log, user } = req
237

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

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

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

248
        log.error('generating enrollment session token failed:', message, exception, { user })
249
        res.status(400).json({ success: false, error: message })
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
   *
2✔
262
   * @ignore
263
   */
264
  app.put(
265
    '/verify/face/:enrollmentIdentifier',
11✔
266
    passport.authenticate('jwt', { session: false }),
11✔
267
    wrapAsync(async (req, res) => {
11!
268
      const { user, log, params, body } = req
11✔
269
      const { enrollmentIdentifier } = params
270
      const { chainId, fvSigner = '', ...payload } = body || {} // payload is the facetec data
11✔
271
      const { gdAddress } = user
272

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

11!
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) {
278
        return
279
      }
280

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

11✔
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
11✔
285
      switch (String(chainId)) {
286
        case '122':
287
        case '50':
11✔
288
        case '42220':
289
          user.chainId = chainId
11✔
290
          break
11✔
291
        default:
292
          user.chainId = 42220
293
          break
11!
294
      }
295

11✔
296
      try {
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)
×
302
        const enrollmentProcessor = createEnrollmentProcessor(storage, log)
303

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

11✔
307
        try {
7✔
308
          // if v1, we convert to v2
7✔
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) {
7✔
312
            log.info('v1 identifier found, converting to v2', { v1Identifier, v2Identifier, gdAddress })
1✔
313
            await Promise.all([
1✔
314
              enrollmentProcessor.dispose(v1Identifier, log),
1✔
315
              cancelDisposalTask(storage, v1Identifier)
1!
316
            ])
317
          }
318
          await enrollmentProcessor.validate(user, v2Identifier, payload)
1✔
319
          const wasWhitelisted = await AdminWallet.lastAuthenticated(gdAddress)
320
          const enrollmentResult = await enrollmentProcessor.enroll(user, v2Identifier, payload, log)
321

7!
322
          // fetch duplicate expiration
×
323
          if (enrollmentResult.success === false && enrollmentResult.enrollmentResult?.isDuplicate) {
324
            const dup = enrollmentResult.enrollmentResult.duplicate.identifier
325
            const authenticationPeriod = await AdminWallet.getAuthenticationPeriod()
326
            const record = await getDisposalTask(storage, dup)
327
            const expiration = moment(record?.createdAt || 0)
328
              .add(authenticationPeriod + 1, 'days')
×
329
              .toISOString()
330
            enrollmentResult.enrollmentResult.duplicate.expiration = expiration
×
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
            })
7✔
340
            if (isV1) {
341
              //throw error so we de-whitelist user
4!
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', {
346
            wasWhitelisted,
×
347
            enrollmentResult,
×
348
            gdAddress,
×
349
            v2Identifier
350
          })
×
351
          res.json(enrollmentResult)
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)
4✔
357
            // if new identifier is indexed then dont revoke
4✔
358
            if (!isIndexed) {
359
              const isWhitelisted = await AdminWallet.isVerified(gdAddress)
4!
360
              if (isWhitelisted) await AdminWallet.removeWhitelisted(gdAddress)
×
361
            }
362
            log.error('failed converting v1 identifier', e.message, e, { isIndexed, gdAddress })
4✔
363
          }
364

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

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

377
        res.status(400).json({ success: false, error: message })
378
      }
379
    })
380
  )
2✔
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(
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)
2✔
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(
×
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
    })
2✔
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(
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

2✔
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(
×
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
2✔
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(
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
    })
2✔
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(
×
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)
2✔
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
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 })
2✔
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
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
      })
2✔
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
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

2✔
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
968
   * @ignore
969
   */
970
  app.get('/verify/phase', (_, res) => {
2✔
971
    const { phase } = conf
2✔
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
×
986
   */
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
      }
1075
    })
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