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

mozilla / blurts-server / 45ba77d7-fd3c-4064-90eb-157d6aa10611

pending completion
45ba77d7-fd3c-4064-90eb-157d6aa10611

push

circleci

Robert Helmer
update the queue adr

282 of 1631 branches covered (17.29%)

Branch coverage included in aggregate %.

959 of 4375 relevant lines covered (21.92%)

3.65 hits per line

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

0.0
/src/controllers/breaches.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 { mainLayout } from '../views/mainLayout.js'
6
import { breaches } from '../views/partials/breaches.js'
7
import { setBreachResolution, updateBreachStats } from '../db/tables/subscribers.js'
8
import { appendBreachResolutionChecklist } from '../utils/breach-resolution.js'
9
import { generateToken } from '../utils/csrf.js'
10
import { getAllEmailsAndBreaches } from '../utils/breaches.js'
11
import { getCountryCode } from '../utils/country-code.js'
12

13
async function breachesPage (req, res) {
14
  // TODO: remove: to test out getBreaches call with JSON returns
15
  const breachesData = await getAllEmailsAndBreaches(req.user, req.app.locals.breaches)
×
16
  const emailVerifiedCount = breachesData.verifiedEmails?.length ?? 0
×
17
  const emailTotalCount = emailVerifiedCount + (breachesData.unverifiedEmails?.length ?? 0)
×
18
  appendBreachResolutionChecklist(breachesData, { countryCode: getCountryCode(req) })
×
19
  const cookies = req.cookies
×
20
  const selectedEmailIndex = typeof cookies['monitor.selected-email-index'] !== 'undefined'
×
21
    ? Number.parseInt(cookies['monitor.selected-email-index'], 10)
22
    : 0
23

24
  const data = {
×
25
    breachesData,
26
    breachLogos: req.app.locals.breachLogoMap,
27
    emailVerifiedCount,
28
    emailTotalCount,
29
    selectedEmailIndex,
30
    partial: breaches,
31
    csrfToken: generateToken(res),
32
    fxaProfile: req.user.fxa_profile_json,
33
    nonce: res.locals.nonce
34
  }
35

36
  res.send(mainLayout(data))
×
37
}
38

39
/**
40
 * Get breaches from the database and return a JSON object
41
 * TODO: Takes in additional query parameters:
42
 *
43
 * status: enum (resolved, unresolved)
44
 * email: string
45
 *
46
 * @param {object} req
47
 * @param {object} res
48
 */
49
async function getBreaches (req, res) {
50
  const allBreaches = req.app.locals.breaches
×
51
  const sessionUser = req.user
×
52
  const resp = await getAllEmailsAndBreaches(sessionUser, allBreaches)
×
53
  return res.json(resp)
×
54
}
55

56
/**
57
 * Modify breach resolution for a user
58
 *
59
 * @param {object} req containing {user, body: {affectedEmail, breachId, resolutionsChecked}}
60
 *
61
 * breachId: id of the breach in the `breaches` table
62
 *
63
 * resolutionsChecked: has the following structure [DataTypes]
64
 * @param {object} res JSON object containing the updated breach resolution
65
 */
66
async function putBreachResolution (req, res) {
67
  const sessionUser = req.user
×
68
  const { affectedEmail, breachId, resolutionsChecked } = req.body
×
69
  const breachIdNumber = Number(breachId)
×
70
  const affectedEmailAsSubscriber = sessionUser.primary_email === affectedEmail ? sessionUser.primary_email : false
×
71
  const affectedEmailInEmailAddresses = sessionUser.email_addresses.find(ea => ea.email === affectedEmail)?.email || false
×
72

73
  // check if current user's emails array contain affectedEmail
74
  if (!affectedEmailAsSubscriber && !affectedEmailInEmailAddresses) {
×
75
    return res.json('Error: affectedEmail is not valid for this subscriber')
×
76
  }
77

78
  // check if breach id is a part of affectEmail's breaches
79
  const allBreaches = req.app.locals.breaches
×
80
  const { verifiedEmails } = await getAllEmailsAndBreaches(req.session.user, allBreaches)
×
81
  let currentEmail
82
  if (affectedEmailAsSubscriber) {
×
83
    currentEmail = verifiedEmails.find(ve => ve.email === affectedEmailAsSubscriber)
×
84
  } else {
85
    currentEmail = verifiedEmails.find(ve => ve.email === affectedEmailInEmailAddresses)
×
86
  }
87
  const currentBreaches = currentEmail?.breaches?.filter(b => b.Id === breachIdNumber)
×
88
  if (!currentBreaches) {
×
89
    return res.json('Error: breachId provided does not exist')
×
90
  }
91

92
  // check if resolutionsChecked array is a subset of the breaches' datatypes
93
  const isSubset = resolutionsChecked.every(val => currentBreaches[0].DataClasses.includes(val))
×
94
  if (!isSubset) {
×
95
    return res.json(`Error: the resolutionChecked param contains more than allowed data types: ${resolutionsChecked}`)
×
96
  }
97

98
  /* new JsonB:
99
  {
100
    email_id: {
101
      recency_index: {
102
        resolutions: ['email', ...],
103
        isResolved: true
104
      }
105
    }
106
  }
107
  */
108

109
  const currentBreachDataTypes = currentBreaches[0].DataClasses // get this from existing breaches
×
110
  const currentBreachResolution = req.user.breach_resolution || {} // get this from existing breach resolution if available
×
111
  const isResolved = resolutionsChecked.length === currentBreachDataTypes.length
×
112
  currentBreachResolution[affectedEmail] = {
×
113
    ...(currentBreachResolution[affectedEmail] || {}),
×
114
    ...{
115
      [breachIdNumber]: {
116
        resolutionsChecked,
117
        isResolved
118
      }
119
    }
120
  }
121

122
  // set useBreachId to mark latest version of breach resolution
123
  // without this line, the get call might assume recency index
124
  currentBreachResolution.useBreachId = true
×
125

126
  const updatedSubscriber = await setBreachResolution(sessionUser, currentBreachResolution)
×
127

128
  req.session.user = updatedSubscriber
×
129

130
  const userBreachStats = breachStatsV1(verifiedEmails)
×
131

132
  await updateBreachStats(sessionUser.id, userBreachStats)
×
133

134
  res.json(updatedSubscriber.breach_resolution)
×
135
}
136

137
// PRIVATE
138

139
/**
140
 * TODO: DEPRECATE
141
 * This utiliy function is maintained to keep backwards compatibility with V1.
142
 * After v2 is launched, we will deprecate this function
143
 *
144
 * @param {object} verifiedEmails [{breaches: [isResolved: true/false, dataClasses: []]}]
145
 * @returns {object} breachStats
146
 * {
147
 *    monitoredEmails: {
148
      count: 0
149
    },
150
    numBreaches: {
151
      count: 0,
152
      numResolved: 0
153
      numUnresolved: 0
154
    },
155
    passwords: {
156
      count: 0,
157
      numResolved: 0
158
    }
159
  }
160
 */
161
function breachStatsV1 (verifiedEmails) {
162
  const breachStats = {
×
163
    monitoredEmails: {
164
      count: 0
165
    },
166
    numBreaches: {
167
      count: 0,
168
      numResolved: 0
169
    },
170
    passwords: {
171
      count: 0,
172
      numResolved: 0
173
    }
174
  }
175
  let foundBreaches = []
×
176

177
  // combine the breaches for each account, breach duplicates are ok
178
  // since the user may have multiple accounts with different emails
179
  verifiedEmails.forEach(email => {
×
180
    email.breaches.forEach(breach => {
×
181
      if (breach.IsResolved) {
×
182
        breachStats.numBreaches.numResolved++
×
183
      }
184

185
      const dataClasses = breach.DataClasses
×
186
      if (dataClasses.includes('passwords')) {
×
187
        breachStats.passwords.count++
×
188
        if (breach.IsResolved) {
×
189
          breachStats.passwords.numResolved++
×
190
        }
191
      }
192
    })
193
    foundBreaches = [...foundBreaches, ...email.breaches]
×
194
  })
195

196
  // total number of verified emails being monitored
197
  breachStats.monitoredEmails.count = verifiedEmails.length
×
198

199
  // total number of breaches across all emails
200
  breachStats.numBreaches.count = foundBreaches.length
×
201

202
  breachStats.numBreaches.numUnresolved = breachStats.numBreaches.count - breachStats.numBreaches.numResolved
×
203

204
  return breachStats
×
205
}
206

207
export { breachesPage, putBreachResolution, getBreaches }
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