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

mozilla / blurts-server / #13275

pending completion
#13275

push

circleci

rhelmer
MNTOR-1452 - use UPDATE FOR to lock table and prevent race condition when adding email

282 of 1679 branches covered (16.8%)

Branch coverage included in aggregate %.

3 of 3 new or added lines in 1 file covered. (100.0%)

959 of 4548 relevant lines covered (21.09%)

1.76 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 '../appConstants.js'
6

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

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

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

21
import { getBreachesForEmail } from '../utils/hibp.js'
22
import { getSha1 } from '../utils/fxa.js'
23
import { validateEmailAddress } from '../utils/emailAddress.js'
24
import { generateToken } from '../utils/csrf.js'
25
import { RateLimitError, UnauthorizedError, UserInputError } from '../utils/error.js'
26

27
import { mainLayout } from '../views/mainLayout.js'
28
import { settings } from '../views/partials/settings.js'
29
import { getTemplate } from '../views/emails/email2022.js'
30
import { verifyPartial } from '../views/emails/emailVerify.js'
31

32
async function settingsPage (req, res) {
33
  /** @type {Array<import('../db/tables/emailAddresses.js').EmailRow>} */
34
  const emails = await getUserEmails(req.session.user.id)
×
35
  // Add primary subscriber email to the list
36
  emails.push({
×
37
    email: req.session.user.primary_email,
38
    sha1: req.session.user.primary_sha1,
39
    primary: true,
40
    verified: true
41
  })
42

43
  const breachCounts = new Map()
×
44

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

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

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

66
  res.send(mainLayout(data))
×
67
}
68

69
async function addEmail (req, res) {
70
  const sessionUser = req.user
×
71
  const emailCount = 1 + (req.user.email_addresses?.length ?? 0) // primary + verified + unverified emails
×
72
  const validatedEmail = validateEmailAddress(req.body.email)
×
73

74
  if (validatedEmail === null) {
×
75
    throw new UserInputError(getMessage('user-add-invalid-email'))
×
76
  }
77

78
  if (emailCount >= AppConstants.MAX_NUM_ADDRESSES) {
×
79
    throw new UserInputError(getMessage('user-add-too-many-emails'))
×
80
  }
81

82
  checkForDuplicateEmail(sessionUser, validatedEmail.email)
×
83

84
  const unverifiedSubscriber = await addSubscriberUnverifiedEmailHash(
×
85
    req.session.user,
86
    validatedEmail.email
87
  )
88

89
  await sendVerificationEmail(sessionUser, unverifiedSubscriber.id)
×
90

91
  return res.json({
×
92
    success: true,
93
    status: 200,
94
    newEmailCount: emailCount + 1,
95
    message: 'Sent the verification email'
96
  })
97
}
98

99
function checkForDuplicateEmail (sessionUser, email) {
100
  const emailLowerCase = email.toLowerCase()
×
101
  if (emailLowerCase === sessionUser.primary_email.toLowerCase()) {
×
102
    throw new UserInputError(getMessage('user-add-duplicate-email'))
×
103
  }
104

105
  for (const secondaryEmail of sessionUser.email_addresses) {
×
106
    if (emailLowerCase === secondaryEmail.email.toLowerCase()) {
×
107
      throw new UserInputError(getMessage('user-add-duplicate-email'))
×
108
    }
109
  }
110
}
111

112
async function removeEmail (req, res) {
113
  const emailId = req.body.emailId
×
114
  const sessionUser = req.user
×
115
  const existingEmail = await getEmailById(emailId)
×
116

117
  if (existingEmail?.subscriber_id !== sessionUser.id) {
×
118
    throw new UserInputError(getMessage('error-not-subscribed'))
×
119
  }
120

121
  removeOneSecondaryEmail(emailId)
×
122
  deleteResolutionsWithEmail(existingEmail.subscriber_id, existingEmail.email)
×
123
  res.redirect('/user/settings')
×
124
}
125

126
async function resendEmail (req, res) {
127
  const emailId = req.body.emailId
×
128
  const sessionUser = req.user
×
129
  const existingEmail = await getUserEmails(sessionUser.id)
×
130

131
  const filteredEmail = existingEmail.filter(
×
132
    (a) => a.email === emailId && a.subscriber_id === sessionUser.id
×
133
  )
134

135
  if (!filteredEmail) {
×
136
    throw new UnauthorizedError(getMessage('user-verify-token-error'))
×
137
  }
138

139
  await sendVerificationEmail(sessionUser, emailId)
×
140

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

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

176
async function verifyEmail (req, res) {
177
  const token = req.query.token
×
178
  await verifyEmailHash(token)
×
179

180
  return res.redirect('/user/settings')
×
181
}
182

183
async function updateCommunicationOptions (req, res) {
184
  const sessionUser = req.user
×
185
  // 0 = Send breach alerts to the email address found in brew breach.
186
  // 1 = Send all breach alerts to user's primary email address.
187
  const allEmailsToPrimary = Number(req.body.communicationOption) === 1
×
188
  const updatedSubscriber = await setAllEmailsToPrimary(
×
189
    sessionUser,
190
    allEmailsToPrimary
191
  )
192
  req.session.user = updatedSubscriber
×
193

194
  return res.json({
×
195
    success: true,
196
    status: 200,
197
    message: 'Communications options updated'
198
  })
199
}
200

201
export {
202
  settingsPage,
203
  resendEmail,
204
  addEmail,
205
  removeEmail,
206
  verifyEmail,
207
  updateCommunicationOptions
208
}
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