• 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 {
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

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

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

1✔
247
        res.json({ success: true, sessionToken })
248
      } catch (exception) {
249
        const { message } = exception
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
   *
2✔
262
   * @apiParam {String} enrollmentIdentifier
263
   * @apiParam {String} sessionId
264
   *
265
   * @ignore
11✔
266
   */
11✔
267
  app.put(
11!
268
    '/verify/face/:enrollmentIdentifier',
11✔
269
    passport.authenticate('jwt', { session: false }),
270
    wrapAsync(async (req, res) => {
11✔
271
      const { user, log, params, body } = req
272
      const { enrollmentIdentifier } = params
273
      const { chainId, fvSigner = '', ...payload } = body || {} // payload is the facetec data
274
      const { gdAddress } = user
11!
275

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

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
      }
11✔
283

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

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
11✔
288
      switch (String(chainId)) {
289
        case '122':
11✔
290
        case '50':
11✔
291
        case '42220':
292
          user.chainId = chainId
293
          break
11!
294
        default:
295
          user.chainId = 42220
11✔
296
          break
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
×
302
        try {
303
          await verifyIdentifier(enrollmentIdentifier, gdAddress, chainId)
304
        } catch (e) {
305
          log.warn('verifyIdentifier failed:', e.message, e, {
306
            enrollmentIdentifier,
11✔
307
            fvSigner,
7✔
308
            gdAddress,
7✔
309
            chainId: e.chainId,
310
            rpc: e.rpc
311
          })
7✔
312
          throw e
1✔
313
        }
1✔
314
        log.debug('FV identifier verification success', { enrollmentIdentifier, gdAddress, chainId })
1✔
315
        const { v2Identifier, v1Identifier } = normalizeIdentifiers(enrollmentIdentifier, fvSigner)
1!
316
        const enrollmentProcessor = createEnrollmentProcessor(storage, log)
317
        log.debug('checking if user was previously registered with a v1 identifier before enrolling:', {
318
          v2Identifier,
1✔
319
          v1Identifier
320
        })
321
        // here we check if wallet was registered using a v1 identifier
7!
322
        const isV1 = !!v1Identifier && (await enrollmentProcessor.isIdentifierExists(v1Identifier))
×
323

324
        try {
325
          // if v1, we convert to v2
326
          // delete previous enrollment.
327
          // once user completes FV it will create a new record under his V2 id. update his lastAuthenticated. and enqueue for disposal
328
          if (isV1) {
×
329
            log.info('v1 identifier found, converting to v2', { v1Identifier, v2Identifier, gdAddress })
330
            await Promise.all([
×
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 })
7✔
340
          const enrollmentResult = await enrollmentProcessor.enroll(user, v2Identifier, payload, log)
341

4!
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()
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
          }
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,
4✔
357
              gdAddress,
4✔
358
              v2Identifier
359
            })
4!
360
            if (isV1) {
×
361
              //throw error so we de-whitelist user
362
              throw new Error('User failed to re-authenticate with V1 identifier')
4✔
363
            }
364
          }
365
          log.info(wasWhitelisted > 0 && enrollmentResult.success ? 'user re-authenticated' : 'user enrolled', {
4✔
366
            wasWhitelisted,
367
            enrollmentResult,
368
            gdAddress,
369
            v2Identifier
370
          })
371
          res.json(enrollmentResult)
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
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
378
            if (!isIndexed) {
379
              const isWhitelisted = await AdminWallet.isVerified(gdAddress)
380
              if (isWhitelisted) await AdminWallet.removeWhitelisted(gdAddress)
2✔
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)
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
2✔
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 })
×
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()) {
2✔
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 })
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)) {
2✔
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
            }
×
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) {
2✔
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
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

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

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

2✔
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) => {
×
986
    const { phase } = conf
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

1075
          res.json({ success: true })
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