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

mozilla / blurts-server / db2946f8-f843-49e7-aa7c-6f7822b22249

pending completion
db2946f8-f843-49e7-aa7c-6f7822b22249

Pull #2807

circleci

Robert Helmer
depend on RateLimitError being thrown to handle errors
Pull Request #2807: MNTOR-1143 - limit how often users can verify email addresses to 5 minutes, with generic rate limiting for all APIs

282 of 1285 branches covered (21.95%)

Branch coverage included in aggregate %.

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

959 of 3467 relevant lines covered (27.66%)

4.49 hits per line

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

0.0
/src/controllers/settings.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 AppConstants from '../app-constants.js'
6

7
import {
8
  getUserEmails,
9
  resetUnverifiedEmailAddress,
10
  addSubscriberUnverifiedEmailHash,
11
  removeOneSecondaryEmail,
12
  getEmailById,
13
  verifyEmailHash
14
} from '../db/tables/email_addresses.js'
15

16
import { setAllEmailsToPrimary } from '../db/tables/subscribers.js'
17

18
import { fluentError, getMessage } from '../utils/fluent.js'
19
import { sendEmail, getVerificationUrl, getUnsubscribeUrl } from '../utils/email.js'
20

21
import { getBreachesForEmail } from '../utils/hibp.js'
22
import { generateToken } from '../utils/csrf.js'
23
import { RateLimitError } from '../utils/error.js'
24

25
import { mainLayout } from '../views/main.js'
26
import { settings } from '../views/partials/settings.js'
27
import { getTemplate } from '../views/email-2022.js'
28
import { verifyPartial } from '../views/partials/email-verify.js'
29

30
async function settingsPage (req, res) {
31
  const emails = await getUserEmails(req.session.user.id)
×
32
  // Add primary subscriber email to the list
33
  emails.push({
×
34
    email: req.session.user.primary_email,
35
    sha1: req.session.user.primary_sha1,
36
    primary: true,
37
    verified: true
38
  })
39

40
  const breachCounts = new Map()
×
41

42
  const allBreaches = req.app.locals.breaches
×
43
  for (const email of emails) {
×
44
    const breaches = await getBreachesForEmail(
×
45
      email.sha1,
46
      allBreaches,
47
      true,
48
      false
49
    )
50
    breachCounts.set(email.email, breaches?.length || 0)
×
51
  }
52

53
  const {
54
    all_emails_to_primary: allEmailsToPrimary,
55
    fxa_profile_json: fxaProfile
56
  } = req.user
×
57

58
  const data = {
×
59
    allEmailsToPrimary,
60
    fxaProfile,
61
    partial: settings,
62
    emails,
63
    breachCounts,
64
    limit: AppConstants.MAX_NUM_ADDRESSES,
65
    csrfToken: generateToken(res)
66
  }
67

68
  res.send(mainLayout(data))
×
69
}
70

71
async function addEmail (req, res) {
72
  const sessionUser = req.user
×
73
  const email = req.body.email
×
74
  // Use the same regex as HTML5 email input type
75
  // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email#basic_validation
76
  const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
×
77

78
  if (!email || !emailRegex.test(email)) {
×
79
    throw fluentError('user-add-invalid-email')
×
80
  }
81

82
  // Total max number of email addresses minus one to account for the primary email
83
  if (sessionUser.email_addresses.length >= AppConstants.MAX_NUM_ADDRESSES - 1) {
×
84
    throw fluentError('user-add-too-many-emails')
×
85
  }
86

87
  checkForDuplicateEmail(sessionUser, email)
×
88

89
  const unverifiedSubscriber = await addSubscriberUnverifiedEmailHash(
×
90
    req.session.user,
91
    email
92
  )
93

94
  await sendVerificationEmail(unverifiedSubscriber.id)
×
95

96
  return res.json({
×
97
    success: true,
98
    status: 200,
99
    message: 'Sent the verification email'
100
  })
101
}
102

103
function checkForDuplicateEmail (sessionUser, email) {
104
  const emailLowerCase = email.toLowerCase()
×
105
  if (emailLowerCase === sessionUser.primary_email.toLowerCase()) {
×
106
    throw fluentError('user-add-duplicate-email')
×
107
  }
108

109
  for (const secondaryEmail of sessionUser.email_addresses) {
×
110
    if (emailLowerCase === secondaryEmail.email.toLowerCase()) {
×
111
      throw fluentError('user-add-duplicate-email')
×
112
    }
113
  }
114
}
115

116
async function removeEmail (req, res) {
117
  const emailId = req.body.emailId
×
118
  const sessionUser = req.user
×
119
  const existingEmail = await getEmailById(emailId)
×
120

121
  if (existingEmail.subscriber_id !== sessionUser.id) {
×
122
    throw fluentError('error-not-subscribed')
×
123
  }
124

125
  removeOneSecondaryEmail(emailId)
×
126
  res.redirect('/user/settings')
×
127
}
128

129
async function resendEmail (req, res) {
130
  const emailId = req.body.emailId
×
131
  const sessionUser = req.user
×
132
  const existingEmail = await getUserEmails(sessionUser.id)
×
133

134
  const filteredEmail = existingEmail.filter(
×
135
    (a) => a.email === emailId && a.subscriber_id === sessionUser.id
×
136
  )
137

138
  if (!filteredEmail) {
×
139
    throw fluentError('user-verify-token-error')
×
140
  }
141

142
  await sendVerificationEmail(emailId)
×
143

144
  return res.json({
×
145
    success: true,
146
    status: 200,
147
    message: 'Sent the verification email'
148
  })
149
}
150

151
async function sendVerificationEmail (emailId) {
152
  try {
×
153
    const unverifiedEmailAddressRecord = await resetUnverifiedEmailAddress(
×
154
      emailId
155
    )
156
    const recipientEmail = unverifiedEmailAddressRecord.email
×
157
    const data = {
×
158
      recipientEmail,
159
      ctaHref: getVerificationUrl(unverifiedEmailAddressRecord),
160
      utmCampaign: 'email_verify',
161
      unsubscribeUrl: getUnsubscribeUrl(
162
        unverifiedEmailAddressRecord,
163
        'account-verification-email'
164
      ),
165
      heading: getMessage('email-verify-heading'),
166
      subheading: getMessage('email-verify-subhead'),
167
      partial: { name: 'verify' }
168
    }
169
    await sendEmail(
×
170
      recipientEmail,
171
      getMessage('email-subject-verify'),
172
      getTemplate(data, verifyPartial(data))
173
    )
174
  } catch (err) {
175
    if (err.message === 'error-email-validation-pending') {
×
176
      throw new RateLimitError('Verification email recently sent, try again later')
×
177
    } else {
178
      throw err
×
179
    }
180
  }
181
}
182

183
async function verifyEmail (req, res) {
184
  const token = req.query.token
×
185
  await verifyEmailHash(token)
×
186

187
  return res.redirect('/user/settings')
×
188
}
189

190
async function updateCommunicationOptions (req, res) {
191
  const sessionUser = req.user
×
192
  // 0 = Send breach alerts to the email address found in brew breach.
193
  // 1 = Send all breach alerts to user's primary email address.
194
  const allEmailsToPrimary = Number(req.body.communicationOption) === 1
×
195
  const updatedSubscriber = await setAllEmailsToPrimary(
×
196
    sessionUser,
197
    allEmailsToPrimary
198
  )
199
  req.session.user = updatedSubscriber
×
200

201
  return res.json({
×
202
    success: true,
203
    status: 200,
204
    message: 'Communications options updated'
205
  })
206
}
207

208
export {
209
  settingsPage,
210
  resendEmail,
211
  addEmail,
212
  removeEmail,
213
  verifyEmail,
214
  updateCommunicationOptions
215
}
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