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

mozilla / blurts-server / 5a9450c3-a9ae-4aa5-b101-c6809a4d3fbe

pending completion
5a9450c3-a9ae-4aa5-b101-c6809a4d3fbe

push

circleci

GitHub
MNTOR-1143 - limit how often users can verify email addresses to 5 minutes, with generic rate limiting for all APIs (#2807)

282 of 1299 branches covered (21.71%)

Branch coverage included in aggregate %.

14 of 14 new or added lines in 3 files covered. (100.0%)

959 of 3504 relevant lines covered (27.37%)

4.44 hits per line

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

0.0
/src/db/tables/email_addresses.js
1
/* This Source Code Form is subject to the terms of the Mozilla Public
2
 * License, v. 2.0. If a copy of the MPL was not distributed with this
3
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4

5
import { v4 as uuidv4 } from 'uuid'
6
import Knex from 'knex'
7
import knexConfig from '../knexfile.js'
8
import mozlog from '../../utils/log.js'
9
import { fluentError } from '../../utils/fluent.js'
10
import { subscribeHash } from '../../utils/hibp.js'
11
import { getSha1 } from '../../utils/fxa.js'
12
import { getSubscriberByEmail, updateFxAData } from './subscribers.js'
13
const knex = Knex(knexConfig)
×
14
const log = mozlog('DB.email_addresses')
×
15

16
async function getEmailByToken (token) {
17
  const res = await knex('email_addresses')
×
18
    .where('verification_token', '=', token)
19

20
  return res[0]
×
21
}
22

23
async function getEmailById (emailAddressId) {
24
  const res = await knex('email_addresses')
×
25
    .where('id', '=', emailAddressId)
26

27
  return res[0]
×
28
}
29

30
async function getEmailAddressRecordByEmail (email) {
31
  const emailAddresses = await knex('email_addresses').where({
×
32
    email, verified: true
33
  })
34
  if (!emailAddresses) {
×
35
    return null
×
36
  }
37
  if (emailAddresses.length > 1) {
×
38
    // TODO: handle multiple emails in separate(?) subscriber accounts?
39
    log.warn('getEmailAddressRecordByEmail', { msg: 'found the same email multiple times' })
×
40
  }
41
  return emailAddresses[0]
×
42
}
43

44
async function addSubscriberUnverifiedEmailHash (user, email) {
45
  const res = await knex('email_addresses').insert({
×
46
    subscriber_id: user.id,
47
    email,
48
    sha1: getSha1(email),
49
    verification_token: uuidv4(),
50
    verified: false
51
  }).returning('*')
52
  return res[0]
×
53
}
54

55
async function resetUnverifiedEmailAddress (emailAddressId) {
56
  const newVerificationToken = uuidv4()
×
57

58
  // Time in ms to require between verification reset.
59
  const verificationWait = 5 * 60 * 1000 // 5 minutes
×
60

61
  const verificationRecentlyUpdated = await knex('email_addresses')
×
62
    .select('id')
63
    .whereRaw('"updated_at" > NOW() - INTERVAL \'1 MILLISECOND\' * ?', verificationWait)
64
    .andWhere('id', emailAddressId)
65
    .first()
66

67
  if (verificationRecentlyUpdated?.id === parseInt(emailAddressId)) {
×
68
    throw fluentError('error-email-validation-pending')
×
69
  }
70

71
  const res = await knex('email_addresses')
×
72
    .update({
73
      verification_token: newVerificationToken,
74
      updated_at: knex.fn.now()
75
    })
76
    .where('id', emailAddressId)
77
    .returning('*')
78
  return res[0]
×
79
}
80

81
async function verifyEmailHash (token) {
82
  const unverifiedEmail = await getEmailByToken(token)
×
83
  if (!unverifiedEmail) {
×
84
    throw fluentError('Error message for this verification email timed out or something went wrong.')
×
85
  }
86
  const verifiedEmail = await _verifyNewEmail(unverifiedEmail)
×
87
  return verifiedEmail[0]
×
88
}
89

90
// TODO: refactor into an upsert? https://jaketrent.com/post/upsert-knexjs/
91
// Used internally, ideally should not be called by consumers.
92
async function _getSha1EntryAndDo (sha1, aFoundCallback, aNotFoundCallback) {
93
  const existingEntries = await knex('subscribers')
×
94
    .where('primary_sha1', sha1)
95

96
  if (existingEntries.length && aFoundCallback) {
×
97
    return await aFoundCallback(existingEntries[0])
×
98
  }
99

100
  if (!existingEntries.length && aNotFoundCallback) {
×
101
    return await aNotFoundCallback()
×
102
  }
103
}
104

105
// Used internally.
106
async function _addEmailHash (sha1, email, signupLanguage, verified = false) {
×
107
  log.debug('_addEmailHash', { sha1, email, signupLanguage, verified })
×
108
  try {
×
109
    return await _getSha1EntryAndDo(sha1, async aEntry => {
×
110
      // Entry existed, patch the email value if supplied.
111
      if (email) {
×
112
        const res = await knex('subscribers')
×
113
          .update({
114
            primary_email: email,
115
            primary_sha1: getSha1(email.toLowerCase()),
116
            primary_verified: verified,
117
            updated_at: knex.fn.now()
118
          })
119
          .where('id', '=', aEntry.id)
120
          .returning('*')
121
        return res[0]
×
122
      }
123

124
      return aEntry
×
125
    }, async () => {
126
      // Always add a verification_token value
127
      const verificationToken = uuidv4()
×
128
      const res = await knex('subscribers')
×
129
        .insert({ primary_sha1: getSha1(email.toLowerCase()), primary_email: email, signup_language: signupLanguage, primary_verification_token: verificationToken, primary_verified: verified })
130
        .returning('*')
131
      return res[0]
×
132
    })
133
  } catch (e) {
134
    log.error(e)
×
135
    throw fluentError('error-could-not-add-email')
×
136
  }
137
}
138

139
/**
140
     * Add a subscriber:
141
     * 1. Add a record to subscribers
142
     * 2. Immediately call _verifySubscriber
143
     * 3. For FxA subscriber, add refresh token and profile data
144
     *
145
     * @param {string} email to add
146
     * @param {string} signupLanguage from Accept-Language
147
     * @param {string} fxaAccessToken from Firefox Account Oauth
148
     * @param {string} fxaRefreshToken from Firefox Account Oauth
149
     * @param {string} fxaProfileData from Firefox Account
150
     * @returns {object} subscriber knex object added to DB
151
     */
152
async function addSubscriber (email, signupLanguage, fxaAccessToken = null, fxaRefreshToken = null, fxaProfileData = null) {
×
153
  console.log({ email })
×
154
  console.log({ signupLanguage })
×
155
  const emailHash = await _addEmailHash(getSha1(email), email, signupLanguage, true)
×
156
  const verified = await _verifySubscriber(emailHash)
×
157
  const verifiedSubscriber = Array.isArray(verified) ? verified[0] : null
×
158
  if (fxaRefreshToken || fxaProfileData) {
×
159
    return updateFxAData(verifiedSubscriber, fxaAccessToken, fxaRefreshToken, fxaProfileData)
×
160
  }
161
  return verifiedSubscriber
×
162
}
163

164
/**
165
     * When an email is verified, convert it into a subscriber:
166
     * 1. Subscribe the hash to HIBP
167
     * 2. Update our subscribers record to verified
168
     * 3. (if opted in) Subscribe the email to Fx newsletter
169
     *
170
     * @param {object} emailHash knex object in DB
171
     * @returns {object} verified subscriber knex object in DB
172
     */
173
async function _verifySubscriber (emailHash) {
174
  await subscribeHash(emailHash.primary_sha1)
×
175

176
  const verifiedSubscriber = await knex('subscribers')
×
177
    .where('primary_email', '=', emailHash.primary_email)
178
    .update({
179
      primary_verified: true,
180
      updated_at: knex.fn.now()
181
    })
182
    .returning('*')
183

184
  return verifiedSubscriber
×
185
}
186

187
// Verifies new emails added by existing users
188
async function _verifyNewEmail (emailHash) {
189
  await subscribeHash(emailHash.sha1)
×
190

191
  const verifiedEmail = await knex('email_addresses')
×
192
    .where('id', '=', emailHash.id)
193
    .update({
194
      verified: true
195
    })
196
    .returning('*')
197

198
  return verifiedEmail
×
199
}
200

201
async function getUserEmails (userId) {
202
  const userEmails = await knex('email_addresses')
×
203
    .where('subscriber_id', '=', userId)
204
    .returning('*')
205

206
  return userEmails
×
207
}
208

209
// This is used by SES callbacks to remove email addresses when recipients
210
// perma-bounce or mark our emails as spam
211
// Removes from either subscribers or email_addresses as necessary
212
async function removeEmail (email) {
213
  const subscriber = await getSubscriberByEmail(email)
×
214
  if (!subscriber) {
×
215
    const emailAddress = await getEmailAddressRecordByEmail(email)
×
216
    if (!emailAddress) {
×
217
      log.warn('removed-subscriber-not-found')
×
218
      return
×
219
    }
220
    await knex('email_addresses')
×
221
      .where({
222
        email,
223
        verified: true
224
      })
225
      .del()
226
    return
×
227
  }
228
  // This can fail if a subscriber has more email_addresses and marks
229
  // a primary email as spam, but we should let it fail so we can see it
230
  // in the logs
231
  await knex('subscribers')
×
232
    .where({
233
      primary_verification_token: subscriber.primary_verification_token,
234
      primary_sha1: subscriber.primary_sha1
235
    })
236
    .del()
237
}
238

239
async function removeOneSecondaryEmail (emailId) {
240
  await knex('email_addresses')
×
241
    .where({
242
      id: emailId
243
    })
244
    .del()
245
}
246

247
async function getEmailAddressesByHashes (hashes) {
248
  return await knex('email_addresses')
×
249
    .join('subscribers', 'email_addresses.subscriber_id', '=', 'subscribers.id')
250
    .whereIn('email_addresses.sha1', hashes)
251
    .andWhere('email_addresses.verified', '=', true)
252
}
253

254
async function deleteEmailAddressesByUid (uid) {
255
  await knex('email_addresses').where({ subscriber_id: uid }).del()
×
256
}
257

258
export {
259
  getEmailByToken,
260
  getEmailById,
261
  getEmailAddressRecordByEmail,
262
  addSubscriberUnverifiedEmailHash,
263
  resetUnverifiedEmailAddress,
264
  verifyEmailHash,
265
  addSubscriber,
266
  getUserEmails,
267
  removeEmail,
268
  removeOneSecondaryEmail,
269
  getEmailAddressesByHashes,
270
  deleteEmailAddressesByUid
271
}
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