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

mozilla / blurts-server / #13042

pending completion
#13042

push

circleci

mansaj
Merge branch 'main' into MNTOR-1356

282 of 1601 branches covered (17.61%)

Branch coverage included in aggregate %.

433 of 433 new or added lines in 31 files covered. (100.0%)

959 of 4327 relevant lines covered (22.16%)

1.85 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, 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 { generateToken } from '../utils/csrf.js'
24
import { RateLimitError, UnauthorizedError, UserInputError } from '../utils/error.js'
25

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

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

41
  const breachCounts = new Map()
×
42

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

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

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

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

68
async function addEmail (req, res) {
69
  const sessionUser = req.user
×
70
  const email = req.body.email
×
71
  // Use the same regex as HTML5 email input type
72
  // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email#basic_validation
73
  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])?)*$/
×
74
  const emailCount = 1 + (req.user.email_addresses?.length ?? 0) // primary + verified + unverified emails
×
75

76
  if (!email || !emailRegex.test(email)) {
×
77
    throw new UserInputError(getMessage('user-add-invalid-email'))
×
78
  }
79

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

84
  checkForDuplicateEmail(sessionUser, email)
×
85

86
  const unverifiedSubscriber = await addSubscriberUnverifiedEmailHash(
×
87
    req.session.user,
88
    email
89
  )
90

91
  await sendVerificationEmail(sessionUser, unverifiedSubscriber.id)
×
92

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

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

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

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

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

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

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

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

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

141
  await sendVerificationEmail(sessionUser, emailId)
×
142

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

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

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

182
  return res.redirect('/user/settings')
×
183
}
184

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

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

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