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

mozilla / blurts-server / #11898

pending completion
#11898

push

circleci

web-flow
Merge pull request #2770 from mozilla/license

Add license headers in source files

282 of 1138 branches covered (24.78%)

Branch coverage included in aggregate %.

959 of 3049 relevant lines covered (31.45%)

2.55 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 { getUserEmails } from '../db/tables/email_addresses.js'
9
import { getBreachesForEmail, filterBreaches } from '../utils/hibp.js'
10
import { filterBreachDataTypes, appendBreachResolutionChecklist } from '../utils/breach-resolution.js'
11
import { getSha1 } from '../utils/fxa.js'
12

13
async function breachesPage (req, res) {
14
  const emailCount = 1 + (req.user.email_addresses?.length || 0) // +1 because user.email_addresses does not include primary
×
15
  // TODO: remove: to test out getBreaches call with JSON returns
16
  const breachesData = await getAllEmailsAndBreaches(req.user, req.app.locals.breaches)
×
17
  appendBreachResolutionChecklist(breachesData)
×
18
  const data = {
×
19
    breachesData,
20
    emailCount,
21
    partial: breaches
22
  }
23

24
  res.send(mainLayout(data))
×
25
}
26

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

43
/**
44
 * Modify breach resolution for a user
45
 * @param {object} req containing {user, body: {affectedEmail, recencyIndex, resolutionsChecked}}
46
 *
47
 * recencyIndex: corresponds to the relevant breach from HIBP
48
 *
49
 * resolutionsChecked: has the following structure [DataTypes]
50
 *
51
 * @param {object} res JSON object containing the updated breach resolution
52
 */
53
async function putBreachResolution (req, res) {
54
  const sessionUser = req.user
×
55
  const { affectedEmail, recencyIndex, resolutionsChecked } = req.body
×
56
  const recencyIndexNumber = Number(recencyIndex)
×
57
  const affectedEmailIsSubscriberRecord = sessionUser.primary_email === affectedEmail
×
58
  const affectedEmailInEmailAddresses = sessionUser.email_addresses.filter(ea => ea.email === affectedEmail)
×
59

60
  // check if current user's emails array contain affectedEmail
61
  if (!affectedEmailIsSubscriberRecord && !affectedEmailInEmailAddresses) {
×
62
    return res.json('Error: affectedEmail is not valid for this subscriber')
×
63
  }
64

65
  // check if recency index is a part of affectEmail's breaches
66
  const allBreaches = req.app.locals.breaches
×
67
  const { verifiedEmails } = await getAllEmailsAndBreaches(req.session.user, allBreaches)
×
68
  const currentEmail = verifiedEmails.find(ve => ve.email === affectedEmailInEmailAddresses[0].email)
×
69
  const currentBreaches = currentEmail.breaches?.filter(b => b.recencyIndex === recencyIndexNumber)
×
70
  if (!currentBreaches) {
×
71
    return res.json('Error: the recencyIndex provided does not exist')
×
72
  }
73

74
  // check if resolutionsChecked array is a subset of the breaches' datatypes
75
  const isSubset = resolutionsChecked.every(val => currentBreaches[0].DataClasses.includes(val))
×
76
  if (!isSubset) {
×
77
    return res.json(`Error: the resolutionChecked param contains more than allowed data types: ${resolutionsChecked}`)
×
78
  }
79

80
  /* new JsonB:
81
  {
82
    email_id: {
83
      recency_index: {
84
        resolutions: ['email', ...],
85
        isResolved: true
86
      }
87
    }
88
  }
89
  */
90

91
  const currentBreachDataTypes = currentBreaches[0].DataClasses // get this from existing breaches
×
92
  const currentBreachResolution = req.user.breach_resolution || {} // get this from existing breach resolution if available
×
93
  const isResolved = resolutionsChecked.length === currentBreachDataTypes.length
×
94
  currentBreachResolution[affectedEmail] = {
×
95
    ...(currentBreachResolution[affectedEmail] || {}),
×
96
    ...{
97
      [recencyIndexNumber]: {
98
        resolutionsChecked,
99
        isResolved
100
      }
101
    }
102
  }
103

104
  const updatedSubscriber = await setBreachResolution(sessionUser, currentBreachResolution)
×
105

106
  req.session.user = updatedSubscriber
×
107

108
  const userBreachStats = breachStatsV1(verifiedEmails)
×
109

110
  await updateBreachStats(sessionUser.id, userBreachStats)
×
111

112
  res.json(updatedSubscriber.breach_resolution)
×
113
}
114

115
// PRIVATE
116

117
/**
118
 * TODO: deprecate
119
 * Get all emails and breaches for a user via app.locals
120
 * This function will be replaced after 'breaches" table is created
121
 * and all records can be retrieved from the one table
122
 * @param {*} user
123
 * @param {*} allBreaches
124
 * @returns
125
 */
126
async function getAllEmailsAndBreaches (user, allBreaches) {
127
  const monitoredEmails = await getUserEmails(user.id)
×
128
  const verifiedEmails = []
×
129
  const unverifiedEmails = []
×
130
  verifiedEmails.push(await bundleVerifiedEmails({ user, email: user.primary_email, recordId: user.id, recordVerified: user.primary_verified, allBreaches }))
×
131
  for (const email of monitoredEmails) {
×
132
    if (email.verified) {
×
133
      verifiedEmails.push(await bundleVerifiedEmails({ user, email: email.email, recordId: email.id, recordVerified: email.verified, allBreaches }))
×
134
    } else {
135
      unverifiedEmails.push(email)
×
136
    }
137
  }
138

139
  // get new breaches since last shown
140
  for (const emailEntry of verifiedEmails) {
×
141
    const newBreachesForEmail = emailEntry.breaches.filter(breach => breach.AddedDate >= user.breaches_last_shown)
×
142

143
    for (const newBreachForEmail of newBreachesForEmail) {
×
144
      newBreachForEmail.NewBreach = true // add "NewBreach" property to the new breach.
×
145
      emailEntry.hasNewBreaches = newBreachesForEmail.length // add the number of new breaches to the email
×
146
    }
147
  }
148

149
  return { verifiedEmails, unverifiedEmails }
×
150
}
151

152
function addRecencyIndex (foundBreaches) {
153
  const annotatedBreaches = []
×
154
  // slice() the array to make a copy so before reversing so we don't
155
  // reverse foundBreaches in-place
156
  const oldestToNewestFoundBreaches = foundBreaches.slice().reverse()
×
157
  oldestToNewestFoundBreaches.forEach((annotatingBreach, index) => {
×
158
    const foundBreach = foundBreaches.find(foundBreach => foundBreach.Name === annotatingBreach.Name)
×
159
    annotatedBreaches.push(Object.assign({ recencyIndex: index }, foundBreach))
×
160
  })
161
  return annotatedBreaches.reverse()
×
162
}
163

164
async function bundleVerifiedEmails (options) {
165
  const { user, email, recordId, recordVerified, allBreaches } = options
×
166
  const lowerCaseEmailSha = getSha1(email.toLowerCase())
×
167

168
  // find all breaches relevant to the current email
169
  const foundBreaches = await getBreachesForEmail(lowerCaseEmailSha, allBreaches, true, false)
×
170

171
  // adding index to breaches based on recency
172
  const foundBreachesWithRecency = addRecencyIndex(foundBreaches)
×
173

174
  // get v1 "breaches_resolved" object
175
  const resolvedBreachesV1 = user.breaches_resolved
×
176
    ? user.breaches_resolved[email] ? user.breaches_resolved[email] : []
×
177
    : []
178

179
  // get v2 "breach_resolution" object
180
  const breachResolutionV2 = user.breach_resolution
×
181
    ? user.breach_resolution[email] ? user.breach_resolution[email] : {}
×
182
    : []
183

184
  for (const breach of foundBreachesWithRecency) {
×
185
    // if either v1 or v2 is marked as resolved, breach is resolved
186
    breach.IsResolved = !!resolvedBreachesV1.includes(breach.recencyIndex) || !!breachResolutionV2[breach.recencyIndex]?.isResolved
×
187
    breach.ResolutionsChecked = breachResolutionV2[breach.recencyIndex]?.resolutionsChecked || []
×
188

189
    // filter breach types based on the 13 types we care about
190
    breach.DataClasses = filterBreachDataTypes(breach.DataClasses)
×
191
  }
192

193
  // filter out irrelevant breaches based on HIBP
194
  const filteredAnnotatedFoundBreaches = filterBreaches(foundBreachesWithRecency)
×
195

196
  const emailEntry = {
×
197
    email,
198
    breaches: filteredAnnotatedFoundBreaches,
199
    primary: email === user.primary_email,
200
    id: recordId,
201
    verified: recordVerified
202
  }
203

204
  return emailEntry
×
205
}
206

207
/**
208
 * TODO: DEPRECATE
209
 * This utiliy function is maintained to keep backwards compatibility with V1.
210
 * After v2 is launched, we will deprecate this function
211
 * @param {object} verifiedEmails [{breaches: [isResolved: true/false, dataClasses: []]}]
212
 * @returns {object} breachStats
213
 * {
214
 *    monitoredEmails: {
215
      count: 0
216
    },
217
    numBreaches: {
218
      count: 0,
219
      numResolved: 0
220
      numUnresolved: 0
221
    },
222
    passwords: {
223
      count: 0,
224
      numResolved: 0
225
    }
226
  }
227
 */
228
function breachStatsV1 (verifiedEmails) {
229
  const breachStats = {
×
230
    monitoredEmails: {
231
      count: 0
232
    },
233
    numBreaches: {
234
      count: 0,
235
      numResolved: 0
236
    },
237
    passwords: {
238
      count: 0,
239
      numResolved: 0
240
    }
241
  }
242
  let foundBreaches = []
×
243

244
  // combine the breaches for each account, breach duplicates are ok
245
  // since the user may have multiple accounts with different emails
246
  verifiedEmails.forEach(email => {
×
247
    email.breaches.forEach(breach => {
×
248
      if (breach.IsResolved) {
×
249
        breachStats.numBreaches.numResolved++
×
250
      }
251

252
      const dataClasses = breach.DataClasses
×
253
      if (dataClasses.includes('passwords')) {
×
254
        breachStats.passwords.count++
×
255
        if (breach.IsResolved) {
×
256
          breachStats.passwords.numResolved++
×
257
        }
258
      }
259
    })
260
    foundBreaches = [...foundBreaches, ...email.breaches]
×
261
  })
262

263
  // total number of verified emails being monitored
264
  breachStats.monitoredEmails.count = verifiedEmails.length
×
265

266
  // total number of breaches across all emails
267
  breachStats.numBreaches.count = foundBreaches.length
×
268

269
  breachStats.numBreaches.numUnresolved = breachStats.numBreaches.count - breachStats.numBreaches.numResolved
×
270

271
  return breachStats
×
272
}
273

274
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