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

mozilla / blurts-server / ea160b21-282b-46f8-8bfe-f53fd3b88c61

pending completion
ea160b21-282b-46f8-8bfe-f53fd3b88c61

push

circleci

GitHub
Merge pull request #2848 from mozilla/MNTOR-1188/breaches-email-count

282 of 1374 branches covered (20.52%)

Branch coverage included in aggregate %.

12 of 12 new or added lines in 4 files covered. (100.0%)

959 of 3725 relevant lines covered (25.74%)

4.18 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/main.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

12
async function breachesPage (req, res) {
13
  // TODO: remove: to test out getBreaches call with JSON returns
14
  const breachesData = await getAllEmailsAndBreaches(req.user, req.app.locals.breaches)
×
15
  const emailVerifiedCount = breachesData.verifiedEmails?.length ?? 0
×
16
  const emailTotalCount = emailVerifiedCount + (breachesData.unverifiedEmails?.length ?? 0)
×
17
  appendBreachResolutionChecklist(breachesData)
×
18

19
  const data = {
×
20
    breachesData,
21
    emailVerifiedCount,
22
    emailTotalCount,
23
    partial: breaches,
24
    csrfToken: generateToken(res),
25
    fxaProfile: req.user.fxa_profile_json,
26
    nonce: res.locals.nonce
27
  }
28

29
  res.send(mainLayout(data))
×
30
}
31

32
/**
33
 * Get breaches from the database and return a JSON object
34
 * TODO: Takes in additional query parameters:
35
 *
36
 * status: enum (resolved, unresolved)
37
 * email: string
38
 * @param {object} req
39
 * @param {object} res
40
 */
41
async function getBreaches (req, res) {
42
  const allBreaches = req.app.locals.breaches
×
43
  const sessionUser = req.user
×
44
  const resp = await getAllEmailsAndBreaches(sessionUser, allBreaches)
×
45
  return res.json(resp)
×
46
}
47

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

65
  // check if current user's emails array contain affectedEmail
66
  if (!affectedEmailAsSubscriber && !affectedEmailInEmailAddresses) {
×
67
    return res.json('Error: affectedEmail is not valid for this subscriber')
×
68
  }
69

70
  // check if breach id is a part of affectEmail's breaches
71
  const allBreaches = req.app.locals.breaches
×
72
  const { verifiedEmails } = await getAllEmailsAndBreaches(req.session.user, allBreaches)
×
73
  let currentEmail
74
  if (affectedEmailAsSubscriber) {
×
75
    currentEmail = verifiedEmails.find(ve => ve.email === affectedEmailAsSubscriber)
×
76
  } else {
77
    currentEmail = verifiedEmails.find(ve => ve.email === affectedEmailInEmailAddresses)
×
78
  }
79
  const currentBreaches = currentEmail?.breaches?.filter(b => b.Id === breachIdNumber)
×
80
  if (!currentBreaches) {
×
81
    return res.json('Error: breachId provided does not exist')
×
82
  }
83

84
  // check if resolutionsChecked array is a subset of the breaches' datatypes
85
  const isSubset = resolutionsChecked.every(val => currentBreaches[0].DataClasses.includes(val))
×
86
  if (!isSubset) {
×
87
    return res.json(`Error: the resolutionChecked param contains more than allowed data types: ${resolutionsChecked}`)
×
88
  }
89

90
  /* new JsonB:
91
  {
92
    email_id: {
93
      recency_index: {
94
        resolutions: ['email', ...],
95
        isResolved: true
96
      }
97
    }
98
  }
99
  */
100

101
  const currentBreachDataTypes = currentBreaches[0].DataClasses // get this from existing breaches
×
102
  const currentBreachResolution = req.user.breach_resolution || {} // get this from existing breach resolution if available
×
103
  const isResolved = resolutionsChecked.length === currentBreachDataTypes.length
×
104
  currentBreachResolution[affectedEmail] = {
×
105
    ...(currentBreachResolution[affectedEmail] || {}),
×
106
    ...{
107
      [breachIdNumber]: {
108
        resolutionsChecked,
109
        isResolved
110
      }
111
    }
112
  }
113

114
  // set useBreachId to mark latest version of breach resolution
115
  // without this line, the get call might assume recency index
116
  currentBreachResolution.useBreachId = true
×
117

118
  const updatedSubscriber = await setBreachResolution(sessionUser, currentBreachResolution)
×
119

120
  req.session.user = updatedSubscriber
×
121

122
  const userBreachStats = breachStatsV1(verifiedEmails)
×
123

124
  await updateBreachStats(sessionUser.id, userBreachStats)
×
125

126
  res.json(updatedSubscriber.breach_resolution)
×
127
}
128

129
// PRIVATE
130

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

168
  // combine the breaches for each account, breach duplicates are ok
169
  // since the user may have multiple accounts with different emails
170
  verifiedEmails.forEach(email => {
×
171
    email.breaches.forEach(breach => {
×
172
      if (breach.IsResolved) {
×
173
        breachStats.numBreaches.numResolved++
×
174
      }
175

176
      const dataClasses = breach.DataClasses
×
177
      if (dataClasses.includes('passwords')) {
×
178
        breachStats.passwords.count++
×
179
        if (breach.IsResolved) {
×
180
          breachStats.passwords.numResolved++
×
181
        }
182
      }
183
    })
184
    foundBreaches = [...foundBreaches, ...email.breaches]
×
185
  })
186

187
  // total number of verified emails being monitored
188
  breachStats.monitoredEmails.count = verifiedEmails.length
×
189

190
  // total number of breaches across all emails
191
  breachStats.numBreaches.count = foundBreaches.length
×
192

193
  breachStats.numBreaches.numUnresolved = breachStats.numBreaches.count - breachStats.numBreaches.numResolved
×
194

195
  return breachStats
×
196
}
197

198
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