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

mozilla / blurts-server / #11786

pending completion
#11786

push

circleci

web-flow
Merge pull request #2751 from mozilla/MNTOR-1025

MNTOR-1025: csrf

282 of 1122 branches covered (25.13%)

Branch coverage included in aggregate %.

4 of 4 new or added lines in 2 files covered. (100.0%)

959 of 3014 relevant lines covered (31.82%)

2.56 hits per line

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

83.16
/db/DB.js
1
'use strict'
2

3
const uuidv4 = require('uuid/v4')
9✔
4
const Knex = require('knex')
9✔
5

6
const { FluentError } = require('../locale-utils')
9✔
7
const AppConstants = require('../app-constants')
9✔
8
const { FXA } = require('../lib/fxa')
9✔
9
const HIBP = require('../hibp')
9✔
10
const getSha1 = require('../sha1-utils')
9✔
11
const mozlog = require('../log')
9✔
12

13
const knexConfig = require('./knexfile')
9✔
14

15
let knex = Knex(knexConfig)
9✔
16

17
const log = mozlog('DB')
9✔
18

19
const DB = {
9✔
20
  async getSubscriberByToken (token) {
21
    const res = await knex('subscribers')
5✔
22
      .where('primary_verification_token', '=', token)
23

24
    return res[0]
5✔
25
  },
26

27
  async getEmailByToken (token) {
28
    const res = await knex('email_addresses')
25✔
29
      .where('verification_token', '=', token)
30

31
    return res[0]
25✔
32
  },
33

34
  async getEmailById (emailAddressId) {
35
    const res = await knex('email_addresses')
9✔
36
      .where('id', '=', emailAddressId)
37

38
    return res[0]
9✔
39
  },
40

41
  async getSubscriberByTokenAndHash (token, emailSha1) {
42
    const res = await knex.table('subscribers')
4✔
43
      .first()
44
      .where({
45
        primary_verification_token: token,
46
        primary_sha1: emailSha1
47
      })
48
    return res
4✔
49
  },
50

51
  async joinEmailAddressesToSubscriber (subscriber) {
52
    if (subscriber) {
41✔
53
      const emailAddressRecords = await knex('email_addresses').where({
35✔
54
        subscriber_id: subscriber.id
55
      })
56
      subscriber.email_addresses = emailAddressRecords.map(
35✔
57
        emailAddress => ({ id: emailAddress.id, email: emailAddress.email })
59✔
58
      )
59
    }
60
    return subscriber
41✔
61
  },
62

63
  async getSubscriberById (id) {
64
    const [subscriber] = await knex('subscribers').where({
4✔
65
      id
66
    })
67
    const subscriberAndEmails = await this.joinEmailAddressesToSubscriber(subscriber)
4✔
68
    return subscriberAndEmails
4✔
69
  },
70

71
  async getSubscriberByFxaUid (uid) {
72
    const [subscriber] = await knex('subscribers').where({
3✔
73
      fxa_uid: uid
74
    })
75
    const subscriberAndEmails = await this.joinEmailAddressesToSubscriber(subscriber)
3✔
76
    return subscriberAndEmails
3✔
77
  },
78

79
  async getSubscriberByEmail (email) {
80
    const [subscriber] = await knex('subscribers').where({
34✔
81
      primary_email: email,
82
      primary_verified: true
83
    })
84
    const subscriberAndEmails = await this.joinEmailAddressesToSubscriber(subscriber)
34✔
85
    return subscriberAndEmails
34✔
86
  },
87

88
  async getEmailAddressRecordByEmail (email) {
89
    const emailAddresses = await knex('email_addresses').where({
3✔
90
      email, verified: true
91
    })
92
    if (!emailAddresses) {
3!
93
      return null
×
94
    }
95
    if (emailAddresses.length > 1) {
3!
96
      // TODO: handle multiple emails in separate(?) subscriber accounts?
97
      log.warn('getEmailAddressRecordByEmail', { msg: 'found the same email multiple times' })
×
98
    }
99
    return emailAddresses[0]
3✔
100
  },
101

102
  async addSubscriberUnverifiedEmailHash (user, email) {
103
    const res = await knex('email_addresses').insert({
8✔
104
      subscriber_id: user.id,
105
      email,
106
      sha1: getSha1(email),
107
      verification_token: uuidv4(),
108
      verified: false
109
    }).returning('*')
110
    return res[0]
8✔
111
  },
112

113
  async resetUnverifiedEmailAddress (emailAddressId) {
114
    const newVerificationToken = uuidv4()
1✔
115
    const res = await knex('email_addresses')
1✔
116
      .update({
117
        verification_token: newVerificationToken,
118
        updated_at: knex.fn.now()
119
      })
120
      .where('id', emailAddressId)
121
      .returning('*')
122
    return res[0]
1✔
123
  },
124

125
  async verifyEmailHash (token) {
126
    const unverifiedEmail = await this.getEmailByToken(token)
5✔
127
    if (!unverifiedEmail) {
5!
128
      throw new FluentError('Error message for this verification email timed out or something went wrong.')
×
129
    }
130
    const verifiedEmail = await this._verifyNewEmail(unverifiedEmail)
5✔
131
    return verifiedEmail[0]
5✔
132
  },
133

134
  // TODO: refactor into an upsert? https://jaketrent.com/post/upsert-knexjs/
135
  // Used internally, ideally should not be called by consumers.
136
  async _getSha1EntryAndDo (sha1, aFoundCallback, aNotFoundCallback) {
137
    const existingEntries = await knex('subscribers')
14✔
138
      .where('primary_sha1', sha1)
139

140
    if (existingEntries.length && aFoundCallback) {
14✔
141
      return await aFoundCallback(existingEntries[0])
1✔
142
    }
143

144
    if (!existingEntries.length && aNotFoundCallback) {
13!
145
      return await aNotFoundCallback()
13✔
146
    }
147
  },
148

149
  // Used internally.
150
  async _addEmailHash (sha1, email, signupLanguage, verified = false) {
×
151
    try {
14✔
152
      return await this._getSha1EntryAndDo(sha1, async aEntry => {
14✔
153
        // Entry existed, patch the email value if supplied.
154
        if (email) {
1!
155
          const res = await knex('subscribers')
1✔
156
            .update({
157
              primary_email: email,
158
              primary_sha1: getSha1(email.toLowerCase()),
159
              primary_verified: verified,
160
              updated_at: knex.fn.now()
161
            })
162
            .where('id', '=', aEntry.id)
163
            .returning('*')
164
          return res[0]
1✔
165
        }
166

167
        return aEntry
×
168
      }, async () => {
169
        // Always add a verification_token value
170
        const verificationToken = uuidv4()
13✔
171
        const res = await knex('subscribers')
13✔
172
          .insert({
173
            primary_sha1: getSha1(email.toLowerCase()),
174
            primary_email: email,
175
            signup_language: signupLanguage,
176
            primary_verification_token: verificationToken,
177
            primary_verified: verified
178
          })
179
          .returning('*')
180
        return res[0]
12✔
181
      })
182
    } catch (e) {
183
      throw new FluentError('error-could-not-add-email')
1✔
184
    }
185
  },
186

187
  /**
188
   * Add a subscriber:
189
   * 1. Add a record to subscribers
190
   * 2. Immediately call _verifySubscriber
191
   * 3. For FxA subscriber, add refresh token and profile data
192
   *
193
   * @param {string} email to add
194
   * @param {string} signupLanguage from Accept-Language
195
   * @param {string} fxaAccessToken from Firefox Account Oauth
196
   * @param {string} fxaRefreshToken from Firefox Account Oauth
197
   * @param {string} fxaProfileData from Firefox Account
198
   * @returns {object} subscriber knex object added to DB
199
   */
200
  async addSubscriber (email, signupLanguage, fxaAccessToken = null, fxaRefreshToken = null, fxaProfileData = null) {
37✔
201
    const emailHash = await this._addEmailHash(getSha1(email), email, signupLanguage, true)
14✔
202
    const verified = await this._verifySubscriber(emailHash)
13✔
203
    const verifiedSubscriber = Array.isArray(verified) ? verified[0] : null
13!
204
    if (fxaRefreshToken || fxaProfileData) {
13✔
205
      return this._updateFxAData(verifiedSubscriber, fxaAccessToken, fxaRefreshToken, fxaProfileData)
2✔
206
    }
207
    return verifiedSubscriber
11✔
208
  },
209

210
  /**
211
   * When an email is verified, convert it into a subscriber:
212
   * 1. Subscribe the hash to HIBP
213
   * 2. Update our subscribers record to verified
214
   * 3. (if opted in) Subscribe the email to Fx newsletter
215
   *
216
   * @param {object} emailHash knex object in DB
217
   * @returns {object} verified subscriber knex object in DB
218
   */
219
  async _verifySubscriber (emailHash) {
220
    // TODO: move this "up" into controllers/users ?
221
    await HIBP.subscribeHash(emailHash.primary_sha1)
13✔
222

223
    const verifiedSubscriber = await knex('subscribers')
13✔
224
      .where('primary_email', '=', emailHash.primary_email)
225
      .update({
226
        primary_verified: true,
227
        updated_at: knex.fn.now()
228
      })
229
      .returning('*')
230

231
    return verifiedSubscriber
13✔
232
  },
233

234
  // Verifies new emails added by existing users
235
  async _verifyNewEmail (emailHash) {
236
    await HIBP.subscribeHash(emailHash.sha1)
5✔
237

238
    const verifiedEmail = await knex('email_addresses')
5✔
239
      .where('id', '=', emailHash.id)
240
      .update({
241
        verified: true
242
      })
243
      .returning('*')
244

245
    return verifiedEmail
5✔
246
  },
247

248
  async getUserEmails (userId) {
249
    const userEmails = await knex('email_addresses')
6✔
250
      .where('subscriber_id', '=', userId)
251
      .returning('*')
252

253
    return userEmails
6✔
254
  },
255

256
  /**
257
   * Update fxa_refresh_token and fxa_profile_json for subscriber
258
   *
259
   * @param {object} subscriber knex object in DB
260
   * @param {string} fxaAccessToken from Firefox Account Oauth
261
   * @param {string} fxaRefreshToken from Firefox Account Oauth
262
   * @param {string} fxaProfileData from Firefox Account
263
   * @returns {object} updated subscriber knex object in DB
264
   */
265
  async _updateFxAData (subscriber, fxaAccessToken, fxaRefreshToken, fxaProfileData) {
266
    const fxaUID = JSON.parse(fxaProfileData).uid
3✔
267
    const updated = await knex('subscribers')
3✔
268
      .where('id', '=', subscriber.id)
269
      .update({
270
        fxa_uid: fxaUID,
271
        fxa_access_token: fxaAccessToken,
272
        fxa_refresh_token: fxaRefreshToken,
273
        fxa_profile_json: fxaProfileData
274
      })
275
      .returning('*')
276
    const updatedSubscriber = Array.isArray(updated) ? updated[0] : null
3!
277
    if (updatedSubscriber) {
3!
278
      FXA.destroyOAuthToken({ refresh_token: subscriber.fxa_refresh_token })
3✔
279
    }
280
    return updatedSubscriber
3✔
281
  },
282

283
  async updateFxAProfileData (subscriber, fxaProfileData) {
284
    await knex('subscribers').where('id', subscriber.id)
×
285
      .update({
286
        fxa_profile_json: fxaProfileData
287
      })
288
    return this.getSubscriberById(subscriber.id)
×
289
  },
290

291
  async setBreachesLastShownNow (subscriber) {
292
    // TODO: turn 2 db queries into a single query (also see #942)
293
    const nowDateTime = new Date()
3✔
294
    const nowTimeStamp = nowDateTime.toISOString()
3✔
295
    await knex('subscribers')
3✔
296
      .where('id', '=', subscriber.id)
297
      .update({
298
        breaches_last_shown: nowTimeStamp
299
      })
300
    return this.getSubscriberByEmail(subscriber.primary_email)
3✔
301
  },
302

303
  async setAllEmailsToPrimary (subscriber, allEmailsToPrimary) {
304
    const updated = await knex('subscribers')
3✔
305
      .where('id', subscriber.id)
306
      .update({
307
        all_emails_to_primary: allEmailsToPrimary
308
      })
309
      .returning('*')
310
    const updatedSubscriber = Array.isArray(updated) ? updated[0] : null
3!
311
    return updatedSubscriber
3✔
312
  },
313

314
  async setBreachesResolved (options) {
315
    const { user, updatedResolvedBreaches } = options
×
316
    await knex('subscribers')
×
317
      .where('id', user.id)
318
      .update({
319
        breaches_resolved: updatedResolvedBreaches
320
      })
321
    return this.getSubscriberByEmail(user.primary_email)
×
322
  },
323

324
  async setWaitlistsJoined (options) {
325
    const { user, updatedWaitlistsJoined } = options
×
326
    await knex('subscribers')
×
327
      .where('id', user.id)
328
      .update({
329
        waitlists_joined: updatedWaitlistsJoined
330
      })
331
    return this.getSubscriberByEmail(user.primary_email)
×
332
  },
333

334
  async removeSubscriber (subscriber) {
335
    await knex('email_addresses').where({ subscriber_id: subscriber.id }).del()
2✔
336
    await knex('subscribers').where({ id: subscriber.id }).del()
2✔
337
  },
338

339
  // This is used by SES callbacks to remove email addresses when recipients
340
  // perma-bounce or mark our emails as spam
341
  // Removes from either subscribers or email_addresses as necessary
342
  async removeEmail (email) {
343
    const subscriber = await this.getSubscriberByEmail(email)
6✔
344
    if (!subscriber) {
6✔
345
      const emailAddress = await this.getEmailAddressRecordByEmail(email)
2✔
346
      if (!emailAddress) {
2✔
347
        log.warn('removed-subscriber-not-found')
1✔
348
        return
1✔
349
      }
350
      await knex('email_addresses')
1✔
351
        .where({
352
          email,
353
          verified: true
354
        })
355
        .del()
356
      return
1✔
357
    }
358
    // If the subscriber has more email_addresses, log the deletion failure
359
    if (subscriber.email_addresses.length !== 0) {
4✔
360
      log.error('removeEmail', {
1✔
361
        msg: `Unable to delete subscriber ${subscriber.id} with ${subscriber.email_addresses.length} additional email(s).`
362
      })
363
      return
1✔
364
    }
365

366
    await knex('subscribers')
3✔
367
      .where({
368
        primary_verification_token: subscriber.primary_verification_token,
369
        primary_sha1: subscriber.primary_sha1
370
      })
371
      .del()
372
  },
373

374
  async removeSubscriberByToken (token, emailSha1) {
375
    const subscriber = await this.getSubscriberByTokenAndHash(token, emailSha1)
4✔
376
    if (!subscriber) {
4✔
377
      return false
3✔
378
    }
379

380
    // Delete subscriber's emails
381
    // TODO issue 2744: Replace this code with a DB migration to add cascading deletion
382
    await knex('email_addresses').where({ subscriber_id: subscriber.id }).del()
1✔
383

384
    // Delete the subscriber
385
    await knex('subscribers')
1✔
386
      .where({
387
        primary_verification_token: subscriber.primary_verification_token,
388
        primary_sha1: subscriber.primary_sha1
389
      })
390
      .del()
391
    return subscriber
1✔
392
  },
393

394
  async removeOneSecondaryEmail (emailId) {
395
    await knex('email_addresses')
2✔
396
      .where({
397
        id: emailId
398
      })
399
      .del()
400
  },
401

402
  async getSubscribersByHashes (hashes) {
403
    return await knex('subscribers').whereIn('primary_sha1', hashes).andWhere('primary_verified', '=', true)
18✔
404
  },
405

406
  async getEmailAddressesByHashes (hashes) {
407
    return await knex('email_addresses')
5✔
408
      .join('subscribers', 'email_addresses.subscriber_id', '=', 'subscribers.id')
409
      .whereIn('email_addresses.sha1', hashes)
410
      .andWhere('email_addresses.verified', '=', true)
411
  },
412

413
  async deleteUnverifiedSubscribers () {
414
    const expiredDateTime = new Date(Date.now() - AppConstants.DELETE_UNVERIFIED_SUBSCRIBERS_TIMER * 1000)
×
415
    const expiredTimeStamp = expiredDateTime.toISOString()
×
416
    const numDeleted = await knex('subscribers')
×
417
      .where('primary_verified', false)
418
      .andWhere('created_at', '<', expiredTimeStamp)
419
      .del()
420
    log.info('deleteUnverifiedSubscribers', { msg: `Deleted ${numDeleted} rows.` })
×
421
  },
422

423
  async deleteSubscriberByFxAUID (fxaUID) {
424
    await knex('subscribers').where('fxa_uid', fxaUID).del()
1✔
425
  },
426

427
  async deleteEmailAddressesByUid (uid) {
428
    await knex('email_addresses').where({ subscriber_id: uid }).del()
×
429
  },
430

431
  async updateBreachStats (id, stats) {
432
    await knex('subscribers')
3✔
433
      .where('id', id)
434
      .update({
435
        breach_stats: stats
436
      })
437
  },
438

439
  async updateMonthlyEmailTimestamp (email) {
440
    const res = await knex('subscribers').update({ monthly_email_at: 'now' })
1✔
441
      .where('primary_email', email)
442
      .returning('monthly_email_at')
443

444
    return res
1✔
445
  },
446

447
  async updateMonthlyEmailOptout (token) {
448
    await knex('subscribers').update('monthly_email_optout', true).where('primary_verification_token', token)
×
449
  },
450

451
  getSubscribersWithUnresolvedBreachesQuery () {
452
    return knex('subscribers')
4✔
453
      .whereRaw('monthly_email_optout IS NOT TRUE')
454
      .whereRaw("greatest(created_at, monthly_email_at) < (now() - interval '30 days')")
455
      .whereRaw("(breach_stats #>> '{numBreaches, numUnresolved}')::int > 0")
456
  },
457

458
  async getSubscribersWithUnresolvedBreaches (limit = 0) {
2✔
459
    let query = this.getSubscribersWithUnresolvedBreachesQuery()
3✔
460
      .select('primary_email', 'primary_verification_token', 'breach_stats', 'signup_language')
461
    if (limit) {
3!
462
      query = query.limit(limit).orderBy('created_at')
×
463
    }
464
    return await query
3✔
465
  },
466

467
  async getSubscribersWithUnresolvedBreachesCount () {
468
    const query = this.getSubscribersWithUnresolvedBreachesQuery()
1✔
469
    const count = parseInt((await query.count({ count: '*' }))[0].count)
1✔
470
    return count
1✔
471
  },
472

473
  async createConnection () {
474
    if (knex === null) {
8!
475
      knex = Knex(knexConfig)
×
476
    }
477
  },
478

479
  async destroyConnection () {
480
    if (knex !== null) {
8!
481
      await knex.destroy()
8✔
482
      knex = null
1✔
483
    }
484
  }
485

486
}
487

488
module.exports = DB
9✔
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

© 2025 Coveralls, Inc