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

GoodDollar / GoodServer / 14493819276

16 Apr 2025 01:21PM UTC coverage: 49.419% (-0.05%) from 49.468%
14493819276

push

github

sirpy
fix: offerwall hash

592 of 1477 branches covered (40.08%)

Branch coverage included in aggregate %.

1 of 8 new or added lines in 1 file covered. (12.5%)

1 existing line in 1 file now uncovered.

1873 of 3511 relevant lines covered (53.35%)

7.35 hits per line

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

21.28
/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'
2✔
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
  }
2✔
61
}
2✔
62
*/
×
63

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

×
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]) {
2!
89
    return true
×
90
  }
91
  return false
×
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
2✔
109
      )
110
    },
111
    24 * 60 * 60 * 1000
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
   *
2✔
120
   * @apiParam {String} enrollmentIdentifier
121
   * @apiParam {String} signature
122
   *
123
   * @ignore
3✔
124
   */
3✔
125
  app.delete(
3✔
126
    '/verify/face/:enrollmentIdentifier',
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

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

1✔
136
        const record = await getDisposalTask(storage, enrollmentIdentifier)
137
        log.debug('get face disposal task result:', { enrollmentIdentifier, record })
138
        if (record == null || record.status === DelayedTaskStatus.Complete) {
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
2✔
162
   *
163
   * @apiParam {String} enrollmentIdentifier
164
   *
165
   * @ignore
1✔
166
   */
1✔
167
  app.get(
1✔
168
    '/verify/face/:enrollmentIdentifier',
1✔
169
    passport.authenticate('jwt', { session: false }),
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)
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)
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
2✔
197
   * @apiName Retrieves FaceTec license key text
198
   * @apiGroup Verification
199
   *
200
   * @ignore
4✔
201
   */
4✔
202
  app.post(
203
    '/verify/face/license/:licenseType',
4✔
204
    passport.authenticate('jwt', { session: false }),
205
    wrapAsync(async (req, res) => {
4✔
206
      const { log, user, params } = req
4✔
207
      const { licenseType } = params
1✔
208

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

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

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

219
        res.json({ success: true, license })
220
      } catch (exception) {
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
2✔
230
   * @apiName Issue enrollment session token
231
   * @apiGroup Verification
232
   *
233
   * @ignore
2✔
234
   */
235
  app.post(
2✔
236
    '/verify/face/session',
237
    passport.authenticate('jwt', { session: false }),
2✔
238
    wrapAsync(async (req, res) => {
2✔
239
      const { log, user } = req
2✔
240
      const clientIp = requestIp.getClientIp(req)
241

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

1✔
244
      try {
245
        const foundMultiIpAccounts = await checkMultiIpAccounts(user.gdAddress, clientIp, log)
1✔
246
        if (foundMultiIpAccounts) {
1✔
247
          log.warn('session token denied:', foundMultiIpAccounts.length, new Error('session token denied'), {
248
            foundMultiIpAccounts,
249
            clientIp,
250
            account: user.gdAddress
251
          })
252
          throw new Error('session token denied')
253
        }
254
        const processor = createEnrollmentProcessor(storage, log)
255
        const sessionToken = await processor.issueSessionToken(log)
256

257
        res.json({ success: true, sessionToken })
258
      } catch (exception) {
259
        const { message } = exception
260

261
        log.error('generating enrollment session token failed:', message, exception, { user })
2✔
262
        res.status(400).json({ success: false, error: message })
263
      }
264
    })
265
  )
11✔
266

11✔
267
  /**
11!
268
   * @api {put} /verify/:enrollmentIdentifier Verify users face
11✔
269
   * @apiName Face Verification
270
   * @apiGroup Verification
11✔
271
   *
272
   * @apiParam {String} enrollmentIdentifier
273
   * @apiParam {String} sessionId
274
   *
11!
275
   * @ignore
×
276
   */
277
  app.put(
278
    '/verify/face/:enrollmentIdentifier',
279
    passport.authenticate('jwt', { session: false }),
280
    wrapAsync(async (req, res) => {
281
      const { user, log, params, body } = req
282
      const { enrollmentIdentifier } = params
11✔
283
      const { chainId, fvSigner = '', ...payload } = body || {} // payload is the facetec data
284
      const { gdAddress } = user
11✔
285
      const clientIp = requestIp.getClientIp(req)
286

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

289
      // checking if request aborted to handle cases when connection is slow
11✔
290
      // and facemap / images were uploaded more that 30sec causing timeout
11✔
291
      if (req.aborted) {
292
        return
293
      }
11!
294

295
      // user.chainId = chainId || conf.defaultWhitelistChainId
11✔
296

297
      //currently we force all new users to be marked as registered first on celo or xdc if specified
298
      //this is relevant for the invite rewards
299
      switch (String(chainId)) {
11!
300
        case '122':
×
301
        case '50':
×
302
        case '42220':
303
          user.chainId = chainId
304
          break
305
        default:
306
          user.chainId = 42220
11✔
307
          break
7✔
308
      }
7✔
309

310
      try {
311
        const foundMultiIpAccounts = await checkMultiIpAccounts(user.gdAddress, clientIp, log)
7✔
312
        if (foundMultiIpAccounts) {
1✔
313
          log.warn('fv session denied:', foundMultiIpAccounts.length, new Error('fv session denied'), {
1✔
314
            foundMultiIpAccounts,
1✔
315
            clientIp,
1!
316
            account: user.gdAddress
317
          })
318
          throw new Error('fv session denied')
1✔
319
        }
320

321
        // for v2 identifier - verify that identifier is for the address we are going to whitelist
7!
322
        // for v1 this will do nothing
×
323
        try {
324
          await verifyIdentifier(enrollmentIdentifier, gdAddress, chainId)
325
        } catch (e) {
326
          log.warn('verifyIdentifier failed:', e.message, e, {
327
            enrollmentIdentifier,
328
            fvSigner,
×
329
            gdAddress,
330
            chainId: e.chainId,
×
331
            rpc: e.rpc
332
          })
333
          throw e
7!
334
        }
335
        log.debug('FV identifier verification success', { enrollmentIdentifier, gdAddress, chainId })
336
        const { v2Identifier, v1Identifier } = normalizeIdentifiers(enrollmentIdentifier, fvSigner)
337
        const enrollmentProcessor = createEnrollmentProcessor(storage, log)
338
        log.debug('checking if user was previously registered with a v1 identifier before enrolling:', {
339
          v2Identifier,
7✔
340
          v1Identifier
341
        })
4!
342
        // here we check if wallet was registered using a v1 identifier
343
        const isV1 = !!v1Identifier && (await enrollmentProcessor.isIdentifierExists(v1Identifier))
344

×
345
        try {
346
          // if v1, we convert to v2
×
347
          // delete previous enrollment.
×
348
          // once user completes FV it will create a new record under his V2 id. update his lastAuthenticated. and enqueue for disposal
×
349
          if (isV1) {
350
            log.info('v1 identifier found, converting to v2', { v1Identifier, v2Identifier, gdAddress })
×
351
            await Promise.all([
352
              enrollmentProcessor.dispose(v1Identifier, log),
353
              cancelDisposalTask(storage, v1Identifier)
4✔
354
            ])
355
          }
356
          log.debug('starting enrollment process:', { v2Identifier, v1Identifier, isV1 })
4✔
357
          await enrollmentProcessor.validate(user, v2Identifier, payload)
4✔
358
          log.debug('enrollment validation success:', { v2Identifier, v1Identifier, isV1 })
359
          const wasWhitelisted = await AdminWallet.lastAuthenticated(gdAddress)
4!
360
          log.debug('user last authenticated:', { wasWhitelisted, gdAddress })
×
361
          const enrollmentResult = await enrollmentProcessor.enroll(user, v2Identifier, payload, log)
362

4✔
363
          // fetch duplicate expiration
364
          if (enrollmentResult.success === false && enrollmentResult.enrollmentResult?.isDuplicate) {
365
            const dup = enrollmentResult.enrollmentResult.duplicate.identifier
4✔
366
            const authenticationPeriod = await AdminWallet.getAuthenticationPeriod()
367
            const record = await getDisposalTask(storage, dup)
368
            const expiration = moment(record?.createdAt || 0)
369
              .add(authenticationPeriod + 1, 'days')
370
              .toISOString()
371
            enrollmentResult.enrollmentResult.duplicate.expiration = expiration
372
          }
373
          // log warn if user was whitelisted but unable to pass FV again
374
          if (wasWhitelisted > 0 && enrollmentResult.success === false) {
375
            log.warn('user failed to re-authenticate', {
376
              wasWhitelisted,
377
              enrollmentResult,
378
              gdAddress,
379
              v2Identifier
380
            })
2✔
381
            if (isV1) {
382
              //throw error so we de-whitelist user
383
              throw new Error('User failed to re-authenticate with V1 identifier')
384
            }
×
385
          }
×
386
          log.info(wasWhitelisted > 0 && enrollmentResult.success ? 'user re-authenticated' : 'user enrolled', {
×
387
            wasWhitelisted,
×
388
            enrollmentResult,
389
            gdAddress,
×
390
            v2Identifier
391
          })
392
          res.json(enrollmentResult)
393
        } catch (e) {
×
394
          if (isV1) {
×
395
            // if we deleted the user record but had an error in whitelisting, then we must revoke his whitelisted status
396
            // since we might not have his record enrolled
397
            const isIndexed = await enrollmentProcessor.isIdentifierIndexed(v2Identifier)
×
398
            // if new identifier is indexed then dont revoke
399
            if (!isIndexed) {
400
              const isWhitelisted = await AdminWallet.isVerified(gdAddress)
×
401
              if (isWhitelisted) await AdminWallet.removeWhitelisted(gdAddress)
402
            }
×
403
            log.error('failed converting v1 identifier', e.message, e, { isIndexed, gdAddress })
404
          }
×
405

406
          throw e
×
407
        }
×
408
      } catch (exception) {
×
409
        const { message } = exception
×
410
        const logArgs = ['Face verification error:', message, exception, { enrollmentIdentifier, fvSigner, gdAddress }]
×
411

×
412
        if (shouldLogVerificaitonError(exception)) {
413
          log.error(...logArgs)
×
414
        } else {
×
415
          log.warn(...logArgs)
416
        }
×
417

×
418
        res.status(400).json({ success: false, error: message })
419
      }
×
420
    })
421
  )
422

×
423
  /**
424
   * @api {put} /verify/idscan:enrollmentIdentifier Verify id matches face and does OCR
425
   * @apiName IDScan
426
   * @apiGroup IDScanq
427
   *
428
   * @apiParam {String} enrollmentIdentifier
429
   * @apiParam {String} sessionId
430
   *
431
   * @ignore
2✔
432
   */
433
  app.put(
434
    '/verify/idscan/:enrollmentIdentifier',
×
435
    passport.authenticate('jwt', { session: false }),
×
436
    wrapAsync(async (req, res) => {
437
      const { user, log, params, body } = req
×
438
      const { enrollmentIdentifier } = params
439
      const { chainId, ...payload } = body || {} // payload is the facetec data
×
440
      const { gdAddress } = user
×
441

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

×
444
      // checking if request aborted to handle cases when connection is slow
×
445
      // and facemap / images were uploaded more that 30sec causing timeout
×
446
      if (req.aborted) {
447
        return
448
      }
449

450
      try {
451
        // for v2 identifier - verify that identifier is for the address we are going to whitelist
452
        // for v1 this will do nothing
453
        await verifyIdentifier(enrollmentIdentifier, gdAddress, chainId)
454

455
        const { v2Identifier } = normalizeIdentifiers(enrollmentIdentifier)
456

457
        const idscanProcessor = createIdScanProcessor(storage, log)
458

×
459
        let { isMatch, scanResultBlob, ...scanResult } = await idscanProcessor.verify(user, v2Identifier, payload)
460
        scanResult = omit(scanResult, ['externalDatabaseRefID', 'ocrResults', 'serverInfo', 'callData']) //remove unrequired fields
×
461
        log.debug('idscan results:', { isMatch, scanResult })
×
462
        const toSign = { success: true, isMatch, gdAddress, scanResult, timestamp: Date.now() }
×
463
        const { sig: signature } = await AdminWallet.signMessage(JSON.stringify(toSign))
464
        res.json({ scanResult: { ...toSign, signature }, scanResultBlob })
465
      } catch (exception) {
×
466
        const { message } = exception
467
        const logArgs = ['idscan error:', message, exception, { enrollmentIdentifier, gdAddress }]
468

469
        if (shouldLogVerificaitonError(exception)) {
470
          log.error(...logArgs)
471
        } else {
×
472
          log.warn(...logArgs)
473
        }
×
474

475
        res.status(400).json({ success: false, error: message })
×
476
      }
477
    })
×
478
  )
479

×
480
  /**
481
   * demo for verifying that idscan was created by us
482
   * trigger defender autotask
483
   */
484
  app.post(
485
    '/verify/idscan',
486
    wrapAsync(async (req, res) => {
487
      const { log, body } = req
488
      const { scanResult } = body || {} // payload is the facetec data
489

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

492
      try {
493
        const { signature, ...signed } = scanResult
2✔
494
        const { gdAddress } = signed
495
        const mHash = keccak256(JSON.stringify(signed))
496
        log.debug('idscan submit verifying...:', { mHash, signature })
497
        const publicKey = recoverPublickey(signature, mHash, '')
498
        const data = web3Abi.encodeFunctionCall(
499
          {
×
500
            name: 'addMember',
×
501
            type: 'function',
502
            inputs: [
×
503
              {
504
                type: 'address',
×
505
                name: 'member'
506
              }
×
507
            ]
×
508
          },
×
509
          [gdAddress]
510
        )
×
511
        const relayer = await AdminWallet.signer.relayer.getRelayer()
512

×
513
        log.debug('idscan submit verified...:', { publicKey, relayer })
×
514
        if (publicKey !== relayer.address.toLowerCase()) {
515
          throw new Error('invalid signer: ' + publicKey)
516
        }
×
517

×
518
        const addTx = await AdminWallet.signer.sendTx({
×
519
          to: '0x163a99a51fE32eEaC7407687522B5355354a1a37',
×
520
          data,
×
521
          gasLimit: '300000'
×
522
        })
523

×
524
        log.debug('idscan adding member:', addTx)
525

526
        res.json({ ok: 1, txHash: addTx.hash })
×
527
      } catch (exception) {
528
        const { message } = exception
529

×
530
        log.error('idscan submit failed:', message, exception)
531

532
        res.status(400).json({ ok: 0, error: message })
533
      }
534
    })
535
  )
536
  /**
×
537
   * @api {post} /verify/sendotp Sends OTP
538
   * @apiName Send OTP
539
   * @apiGroup Verification
540
   *
541
   * @apiParam {UserRecord} user
542
   *
543
   * @apiSuccess {Number} ok
544
   * @ignore
545
   */
546
  app.post(
547
    '/verify/sendotp',
548
    passport.authenticate('jwt', { session: false }),
549
    userRateLimiter(2, 1), // 1 req / 1min, should be applied AFTER auth to have req.user been set
550
    // also no need for reqRateLimiter as user limiter falls back to the ip (e.g. works as default limiter if no user)
551
    wrapAsync(async (req, res) => {
2✔
552
      const { user, body } = req
553
      const log = req.log
554

555
      log.info('otp request:', { user, body })
556

557
      const onlyCheckAlreadyVerified = body.onlyCheckAlreadyVerified
×
558

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

×
563
      const savedMobile = user.mobile
×
564

565
      if (conf.allowDuplicateUserData === false && (await storage.isDupUserData({ mobile: hashedMobile }))) {
566
        return res.json({ ok: 0, error: 'mobile_already_exists' })
567
      }
568

×
569
      if (!userRec.smsValidated || hashedMobile !== savedMobile) {
570
        if (!onlyCheckAlreadyVerified) {
571
          log.debug('sending otp:', user.loggedInAs)
×
572
          if (['production', 'staging'].includes(conf.env)) {
×
573
            const clientIp = requestIp.getClientIp(req)
574
            const sendResult = await OTP.sendOTP(mobile, get(body, 'user.otpChannel', 'sms'), clientIp)
×
575

576
            log.info('otp sent:', mobile, { user: user.loggedInAs, sendResult })
×
577
          }
×
578

×
579
          await storage.updateUser({
×
580
            identifier: user.loggedInAs,
581
            otp: {
×
582
              ...(userRec.otp || {}),
×
583
              mobile
584
            }
585
          })
586
        }
×
587
      }
588

×
589
      res.json({ ok: 1, alreadyVerified: hashedMobile === savedMobile && user.smsValidated })
×
590
    })
591
  )
592

593
  /**
594
   * @api {post} /verify/mobile Verify mobile data code
×
595
   * @apiName OTP Code
596
   * @apiGroup Verification
×
597
   *
×
598
   * @apiParam {Object} verificationData
599
   *
600
   * @apiSuccess {Number} ok
601
   * @apiSuccess {Claim} attestation
×
602
   * @ignore
603
   */
604
  app.post(
605
    '/verify/mobile',
606
    requestRateLimiter(10, 1),
607
    passport.authenticate('jwt', { session: false }),
608
    onlyInEnv('production', 'staging'),
×
609
    wrapAsync(async (req, res) => {
610
      const log = req.log
611
      const { user, body } = req
612
      const verificationData: { otp: string } = body.verificationData
613
      const { mobile } = user.otp || {}
614

615
      if (!mobile) {
×
616
        log.warn('mobile to verify not found or missing', 'mobile missing', new Error('mobile missing'), {
617
          user,
618
          verificationData
619
        })
620

621
        return res.status(400).json({ ok: 0, error: 'MOBILE MISSING' })
622
      }
623

624
      const hashedNewMobile = mobile && sha3(mobile)
625
      const currentMobile = user.mobile
626

2✔
627
      log.debug('mobile verified', { user, verificationData, hashedNewMobile })
628

629
      if (!user.smsValidated || currentMobile !== hashedNewMobile) {
630
        try {
×
631
          const clientIp = requestIp.getClientIp(req)
×
632
          await verifier.verifyMobile({ identifier: user.loggedInAs, mobile }, verificationData, clientIp)
633
        } catch (e) {
634
          log.warn('mobile verification failed:', e.message, { user, mobile, verificationData })
635
          return res.status(400).json({ ok: 0, error: 'OTP FAILED', message: e.message })
636
        }
637

638
        let updIndexPromise
639
        const { crmId } = user
640

641
        if (currentMobile && currentMobile !== hashedNewMobile) {
642
          updIndexPromise = Promise.all([
643
            //TODO: generate ceramic claim
644
          ])
2✔
645
        }
646

647
        if (crmId) {
648
          //fire and forget
649
          OnGage.updateContact(null, crmId, { mobile }, log).catch(e =>
×
650
            log.error('Error updating CRM contact', e.message, e, { crmId, mobile })
×
651
          )
×
652
        }
×
653

×
654
        await Promise.all([
655
          updIndexPromise,
×
656
          storage.updateUser({
×
657
            identifier: user.loggedInAs,
×
658
            smsValidated: true,
659
            mobile: hashedNewMobile
660
          }),
661
          user.createdDate && //keep temporary field if user is signing up
662
            storage.model.updateOne({ identifier: user.loggedInAs }, { $unset: { 'otp.mobile': true } })
663
        ])
664
      }
665

666
      //TODO: replace with ceramic
667
      let signedMobile
668
      res.json({ ok: 1, attestation: signedMobile })
×
669
    })
×
670
  )
×
671

672
  /**
673
   * @api {post} /verify/registration Verify user registration status
×
674
   * @apiName Verify Registration Status
×
675
   * @apiGroup Verification
×
676
   * @apiSuccess {Number} ok
677
   * @ignore
678
   */
×
679
  app.post(
680
    '/verify/registration',
×
681
    passport.authenticate('jwt', { session: false }),
682
    wrapAsync(async (req, res) => {
683
      const user = req.user
684
      res.json({ ok: user && user.createdDate ? 1 : 0 })
685
    })
686
  )
×
687
  /**
688
   * @api {post} /verify/topwallet Tops Users Wallet if needed
689
   * @apiName Top Wallet
×
690
   * @apiGroup Verification
×
691
   *
692
   * @apiParam {LoggedUser} user
693
   *
694
   * @apiSuccess {Number} ok
×
695
   * @ignore
×
696
   */
×
697
  app.post(
698
    '/verify/topwallet',
699
    requestRateLimiter(3, 1),
×
700
    passport.authenticate(['jwt', 'anonymous'], { session: false }),
×
701
    wrapAsync(async (req, res) => {
×
702
      const log = req.log
703
      const { origin, host } = req.headers
704
      const { account, chainId = 42220 } = req.body || {}
×
705
      const user: LoggedUser = req.user || { gdAddress: account }
×
706
      const clientIp = requestIp.getClientIp(req)
×
707

708
      const gdContract = AdminWallet.walletsMap[chainId]?.tokenContract?._address
709
      const faucetContract = AdminWallet.walletsMap[chainId]?.faucetContract?._address
710
      log.debug('topwallet tx request:', {
711
        address: user.gdAddress,
×
712
        chainId,
713
        gdContract,
714
        faucetContract,
×
715
        user: req.user,
×
716
        origin,
717
        host,
×
718
        clientIp
×
719
      })
720

721
      if (!faucetContract) {
×
722
        log.warn('topWallet unsupported chain', { chainId })
×
723
        return res.json({ ok: -1, error: 'unsupported chain' })
724
      }
×
725

726
      if (conf.env === 'production') {
727
        if (!(await AdminWallet.isConnected(user.gdAddress))) {
×
728
          log.warn('topwallet denied user not whitelisted', {
729
            address: user.gdAddress,
×
730
            origin,
731
            chainId,
732
            clientIp
733
          })
734
          return res.json({ ok: -1, error: 'not whitelisted' })
×
735
        }
736
      }
×
737
      if (!user.gdAddress) {
×
738
        throw new Error('missing wallet address to top')
739
      }
740

741
      // check for faucet abuse
742
      const foundAbuse = await cachedFindFaucetAbuse(user.gdAddress, chainId).catch(e => {
743
        log.error('findFaucetAbuse failed', e.message, e, { address: user.gdAddress, chainId })
744
        return
745
      })
746

747
      if (foundAbuse) {
748
        log.warn('faucet abuse found:', foundAbuse)
749
        return res.json({ ok: -1, error: 'faucet abuse: ' + foundAbuse.hash })
750
      }
751

752
      const foundMultiIpAccounts = await checkMultiIpAccounts(user.gdAddress, clientIp, log)
2✔
753
      if (foundMultiIpAccounts) {
754
        log.warn('faucet multiip abuse found:', foundMultiIpAccounts.length, new Error('faucet multiip abuse'), {
755
          foundMultiIpAccounts,
756
          clientIp,
757
          account: user.gdAddress
×
758
        })
×
759
        AdminWallet.banInFaucet(foundMultiIpAccounts, 'all', log).catch(e => {
×
760
          log.error('findFaucetAbuse banInFaucet failed:', e.message, e, {
761
            address: user.gdAddress,
×
762
            foundMultiIpAccounts
×
763
          })
×
764
        })
765
        return res.json({ ok: -1, error: 'faucet multi abuse' })
766
      }
×
767

768
      try {
769
        let txPromise = AdminWallet.topWallet(user.gdAddress, chainId, log)
×
770
          .then(tx => {
×
771
            log.debug('topwallet tx', { walletaddress: user.gdAddress, tx })
772
            return { ok: 1 }
×
773
          })
774
          .catch(async exception => {
×
775
            const { message } = exception
×
776
            log.warn('Failed topwallet tx', message, exception, { walletaddress: user.gdAddress }) //errors are already logged in adminwallet so jsut warn
777

778
            return { ok: -1, error: message }
779
          })
780

781
        const txRes = await txPromise
782

783
        log.info('topwallet tx done', {
784
          txRes,
785
          loggedInAs: user.loggedInAs
786
        })
787

788
        res.json(txRes)
789
      } catch (e) {
790
        log.error('topwallet timeout or unexpected', e.message, e, { walletaddress: user.gdAddress })
2✔
791
        res.json({ ok: -1, error: e.message })
792
      }
793
    })
794
  )
795

×
796
  /**
×
797
   * @api {post} /verify/swaphelper trigger a non custodial swap
×
798
   * @apiName Swaphelper
799
   * @apiGroup Verification
×
800
   *
×
801
   * @apiParam {account} user
802
   *
×
803
   * @apiSuccess {Number} ok
×
804
   * @ignore
×
805
   */
806
  app.post(
807
    '/verify/swaphelper',
808
    requestRateLimiter(3, 1),
×
809
    passport.authenticate(['jwt', 'anonymous'], { session: false }),
×
810
    wrapAsync(async (req, res) => {
×
811
      const log = req.log
812
      const { account, chainId = 42220 } = req.body || {}
813
      const gdAddress = account || get(req.user, 'gdAddress')
×
814

815
      log.info('swaphelper request:', { gdAddress, chainId })
×
816
      if (!gdAddress) {
×
817
        throw new Error('missing user account')
818
      }
×
819

×
820
      try {
×
821
        //verify target helper address has funds
822

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

826
        res.json({ ok: 1, hash: tx.transactionHash })
827
      } catch (e) {
×
828
        log.error('swaphelper timeout or unexpected', e.message, e, { walletaddress: gdAddress, chainId })
829
        res.json({ ok: -1, error: e.message })
830
      }
831
    })
832
  )
×
833

834
  /**
×
835
   * @api {post} /verify/email Send verification email endpoint
836
   * @apiName Send Email
837
   * @apiGroup Verification
838
   *
839
   * @apiParam {UserRecord} user
840
   *
841
   * @apiSuccess {Number} ok
842
   * @ignore
×
843
   */
×
844
  app.post(
845
    '/verify/sendemail',
846
    requestRateLimiter(2, 1),
847
    passport.authenticate('jwt', { session: false }),
848
    wrapAsync(async (req, res) => {
849
      let runInEnv = ['production', 'staging', 'test'].includes(conf.env)
×
850
      const log = req.log
851
      const { user, body } = req
852

853
      let { email } = body.user
×
854
      email = email.toLowerCase()
855

856
      if (!email || !user) {
857
        log.warn('email verification email or user record not found:', { email, user })
858
        return res.json({ ok: 0, error: 'email or user missing' })
×
859
      }
860

861
      //merge user details
862
      const { email: currentEmail } = user
863
      let userRec: UserRecord = defaults(body.user, user)
864
      const isEmailChanged = currentEmail && currentEmail !== sha3(email)
865

866
      let code
867
      log.debug('email verification request:', { email, currentEmail, isEmailChanged, body, user })
868

869
      if (runInEnv === true && conf.skipEmailVerification === false) {
870
        code = OTP.generateOTP(6)
871

872
        if (!user.isEmailConfirmed || isEmailChanged) {
873
          try {
874
            const { fullName } = userRec
2✔
875

876
            if (!code || !fullName || !email) {
877
              log.error('missing input for sending verification email', { code, fullName, email })
878
              throw new Error('missing input for sending verification email')
×
879
            }
×
880

×
881
            const templateData = {
×
882
              firstname: fullName,
×
883
              code: parseInt(code)
884
            }
×
885

×
886
            const sesResponse = await sendTemplateEmail(email, templateData)
887

×
888
            log.debug('sent new user email validation code', {
×
889
              email,
890
              code,
×
891
              sesResponse: get(sesResponse, '$response.httpResponse.statusCode'),
892
              sesId: get(sesResponse, 'MessageId'),
893
              sesError: get(sesResponse, '$response.error.message')
894
            })
895
          } catch (e) {
896
            log.error('failed sending email verification to user:', e.message, e, { userRec, code })
897
            throw e
898
          }
899
        }
×
900
      }
×
901

×
902
      // updates/adds user with the emailVerificationCode to be used for verification later
903
      await storage.updateUser({
904
        identifier: user.identifier,
×
905
        emailVerificationCode: code,
906
        otp: {
907
          ...(userRec.otp || {}),
×
908
          email
×
909
        }
×
910
      })
911

×
912
      res.json({ ok: 1, alreadyVerified: isEmailChanged === false && user.isEmailConfirmed })
×
913
    })
914
  )
915

916
  /**
×
917
   * @api {post} /verify/email Verify email code
918
   * @apiName Email
919
   * @apiGroup Verification
920
   *
921
   * @apiParam {Object} verificationData
922
   * @apiParam {String} verificationData.code
×
923
   *
×
924
   * @apiSuccess {Number} ok
925
   * @apiSuccess {Claim} attestation
926
   * @ignore
×
927
   */
×
928
  app.post(
929
    '/verify/email',
930
    passport.authenticate('jwt', { session: false }),
931
    wrapAsync(async (req, res) => {
932
      let runInEnv = ['production', 'staging', 'test'].includes(conf.env)
933
      const { __utmzz: utmString = '' } = req.cookies
934
      const log = req.log
935
      const { user, body } = req
936
      const verificationData: { code: string } = body.verificationData
×
937

938
      let { email } = user.otp || {}
939
      email = email && email.toLowerCase()
×
940

941
      const hashedNewEmail = email ? sha3(email) : null
942
      const currentEmail = user.email
943

944
      log.debug('email verification request', {
945
        user,
946
        body,
947
        email,
948
        verificationData,
949
        currentEmail,
950
        hashedNewEmail
951
      })
952

2✔
953
      if (!email) {
×
954
        log.error('email address to verify is missing')
955
        throw new Error('email address to verify is missing')
×
956
      }
957

958
      if (!user.isEmailConfirmed || currentEmail !== hashedNewEmail) {
959
        let signedEmail
960

961
        if (runInEnv && conf.skipEmailVerification === false) {
962
          try {
963
            await verifier.verifyEmail({ identifier: user.loggedInAs }, verificationData)
964
          } catch (e) {
965
            log.warn('email verification failed:', e.message, { user, email, verificationData })
966
            return res.status(400).json({ ok: 0, error: e.message })
967
          }
968
        }
969

970
        storage.updateUser({
2✔
971
          identifier: user.loggedInAs,
2✔
972
          isEmailConfirmed: true,
973
          email: hashedNewEmail
974
        })
975

×
976
        if (runInEnv) {
×
977
          storage.model.updateOne({ identifier: user.loggedInAs }, { $unset: { 'otp.email': 1 } })
×
978

×
979
          // fire and forget
×
980
          syncUserEmail(user, email, utmString, log).catch(e =>
×
981
            log.error('Error updating CRM contact', e.message, e, {
×
982
              crmId: user.crmId,
983
              currentEmail,
×
984
              email
×
985
            })
×
986
          )
987
        }
×
988

×
989
        //TODO: sign using ceramic did
×
990
        return res.json({ ok: 1, attestation: signedEmail })
×
991
      }
×
992

993
      return res.json({ ok: 0, error: 'nothing to do' })
994
    })
×
995
  )
996

997
  /**
998
   * @api {get} /verify/phase get release/phase version number
999
   * @apiName Get Phase VErsion Number
1000
   * @apiGroup Verification
1001
   *
1002
   * @apiSuccess {Number} phase
1003
   * @apiSuccess {Boolean} success
1004
   * @ignore
1005
   */
1006
  app.get('/verify/phase', (_, res) => {
×
1007
    const { phase } = conf
×
1008

1009
    res.json({ success: true, phase })
×
1010
  })
1011

1012
  /**
×
1013
   * @depracated now using goodcfverify cloudflare worker
1014
   * @api {post} /verify/recaptcha verify recaptcha token
1015
   * @apiName Recaptcha
1016
   * @apiGroup Verification
1017
   *
1018
   * @apiParam {string} token
1019
   *
1020
   * @apiSuccess {Number} ok
1021
   * @ignore
1022
   */
×
1023

1024
  const visitorsCounter = {}
×
1025
  app.post(
1026
    '/verify/recaptcha',
×
1027
    requestRateLimiter(10, 1),
1028
    wrapAsync(async (req, res) => {
1029
      const log = req.log
1030
      const { payload: token = '', ipv6 = '', captchaType = '', fingerprint = {} } = req.body
1031
      const clientIp = requestIp.getClientIp(req)
1032
      const xForwardedFor = (req.headers || {})['x-forwarded-for']
1033
      const { visitorId } = fingerprint
1034
      let kvStorageIpKey = clientIp
×
1035
      let parsedRes = {}
1036

1037
      try {
×
1038
        if (ipv6 && ipv6 !== clientIp) {
×
1039
          kvStorageIpKey = ipv6
1040
        }
×
1041
        let visitsCounter = 0
1042
        if (visitorId) {
×
1043
          visitsCounter = visitorsCounter[visitorId] || 0
1044
          visitsCounter++
×
1045
          visitorsCounter[visitorId] = visitsCounter
1046
        }
1047

×
1048
        log.debug('Verifying recaptcha', {
×
1049
          token: token.slice(0, 10),
×
1050
          ipv6,
1051
          clientIp,
1052
          kvStorageIpKey,
1053
          xForwardedFor,
1054
          captchaType,
1055
          visitorId,
×
1056
          visitsCounter
1057
        })
1058

1059
        //hcaptcha verify
2✔
1060
        if (captchaType === 'hcaptcha') {
2✔
1061
          if (!visitorId) {
1062
            //we use fingerprint only for web with hcaptcha at the moment
1063
            throw new Error('missing visitorId')
×
1064
          }
×
1065

×
1066
          const recaptchaRes = await fetch('https://hcaptcha.com/siteverify', {
1067
            method: 'POST',
×
1068
            headers: {
×
1069
              'Content-Type': 'application/x-www-form-urlencoded'
1070
            },
1071
            body: new URLSearchParams({
×
1072
              secret: conf.hcaptchaSecretKey,
1073
              response: token
NEW
1074
            })
×
1075
          })
1076
          parsedRes = await recaptchaRes.json()
1077
        } else {
×
1078
          const url = `https://www.google.com/recaptcha/api/siteverify?secret=${conf.recaptchaSecretKey}&response=${token}&remoteip=${clientIp}`
1079

1080
          const recaptchaRes = await fetch(url, {
×
1081
            method: 'POST',
×
1082
            headers: {
NEW
1083
              'Content-Type': 'application/json',
×
NEW
1084
              Accept: '*/*'
×
1085
            }
NEW
1086
          })
×
NEW
1087

×
NEW
1088
          parsedRes = await recaptchaRes.json()
×
UNCOV
1089
        }
×
1090

1091
        if (parsedRes.success) {
×
1092
          const verifyResult = await OTP.verifyCaptcha(kvStorageIpKey, captchaType)
NEW
1093

×
1094
          log.debug('Recaptcha verified', { verifyResult, parsedRes })
×
1095

1096
          res.json({ success: true })
1097
        } else {
1098
          throw new Error('user failed captcha')
1099
        }
1100
      } catch (exception) {
1101
        const { message } = exception
1102
        const logFunc = ['user failed captcha', 'missing visitorId'].includes(message) ? 'warn' : 'error'
1103
        log[logFunc]('Recaptcha verification failed', message, exception, {
1104
          clientIp,
1105
          token: token.slice(0, 10),
1106
          captchaType,
1107
          parsedRes
1108
        })
1109
        res.status(400).json({ success: false, error: message })
1110
      }
1111
    })
1112
  )
1113
  const payouts = new Set()
1114
  app.get(
1115
    '/verify/offerwall',
1116
    wrapAsync(async (req, res) => {
1117
      const { log, query } = req
1118
      const { user_id, value, token, signature } = query
1119

1120
      // Secret key (replace with your actual secret key)
1121
      const secretKey = conf.offerwallSecret
1122

1123
      // Concatenate the inputs with "."
1124
      const concatenatedString = `${secretKey}.${user_id}.${parseInt(value)}.${token}`
1125

1126
      // Generate MD5 hash
1127
      const calculatedSignature = crypto.createHash('md5').update(concatenatedString).digest('hex')
1128
      log.debug('offerwall payout request:', { user_id, value, token, signature, calculatedSignature })
1129
      try {
1130
        // Compare the calculated signature with the provided signature
1131
        if (calculatedSignature !== signature) {
1132
          throw new Error('Invalid signature')
1133
        }
1134
        if (payouts.has(token)) {
1135
          throw new Error('Already paid')
1136
        }
1137
        const tx = await AdminWallet.walletsMap[42220].transferWalletGoodDollars(user_id, toWei(String(value)), log)
1138
        payouts.add(token)
1139
        log.info('offerwall payout success:', { user_id, value, token, tx: tx.transactionHash })
1140
        res.json({ success: true })
1141
      } catch (exception) {
1142
        const { message } = exception
1143

1144
        log.error('offerwall payout request failed:', message, exception, { user_id, value, token, signature })
1145
        res.status(400).json({ success: false, error: message })
1146
      }
1147
    })
1148
  )
1149
}
1150

1151
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