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

mozilla / blurts-server / 048696f1-fa28-4793-bfad-4cbf2d44b467

pending completion
048696f1-fa28-4793-bfad-4cbf2d44b467

push

circleci

GitHub
Merge pull request #2794 from mozilla/MNTOR-978-2

282 of 1227 branches covered (22.98%)

Branch coverage included in aggregate %.

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

959 of 3296 relevant lines covered (29.1%)

4.72 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
  const emailCount = 1 + (req.user.email_addresses?.length || 0) // +1 because user.email_addresses does not include primary
×
14
  // TODO: remove: to test out getBreaches call with JSON returns
15
  const breachesData = await getAllEmailsAndBreaches(req.user, req.app.locals.breaches)
×
16
  appendBreachResolutionChecklist(breachesData)
×
17

18
  const data = {
×
19
    breachesData,
20
    emailCount,
21
    partial: breaches,
22
    csrfToken: generateToken(res),
23
    fxaProfile: req.user.fxa_profile_json
24
  }
25

26
  res.send(mainLayout(data))
×
27
}
28

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

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

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

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

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

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

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

106
  // set useBreachId to mark latest version of breach resolution
107
  // without this line, the get call might assume recency index
108
  currentBreachResolution.useBreachId = true
×
109

110
  const updatedSubscriber = await setBreachResolution(sessionUser, currentBreachResolution)
×
111

112
  req.session.user = updatedSubscriber
×
113

114
  const userBreachStats = breachStatsV1(verifiedEmails)
×
115

116
  await updateBreachStats(sessionUser.id, userBreachStats)
×
117

118
  res.json(updatedSubscriber.breach_resolution)
×
119
}
120

121
// PRIVATE
122

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

160
  // combine the breaches for each account, breach duplicates are ok
161
  // since the user may have multiple accounts with different emails
162
  verifiedEmails.forEach(email => {
×
163
    email.breaches.forEach(breach => {
×
164
      if (breach.IsResolved) {
×
165
        breachStats.numBreaches.numResolved++
×
166
      }
167

168
      const dataClasses = breach.DataClasses
×
169
      if (dataClasses.includes('passwords')) {
×
170
        breachStats.passwords.count++
×
171
        if (breach.IsResolved) {
×
172
          breachStats.passwords.numResolved++
×
173
        }
174
      }
175
    })
176
    foundBreaches = [...foundBreaches, ...email.breaches]
×
177
  })
178

179
  // total number of verified emails being monitored
180
  breachStats.monitoredEmails.count = verifiedEmails.length
×
181

182
  // total number of breaches across all emails
183
  breachStats.numBreaches.count = foundBreaches.length
×
184

185
  breachStats.numBreaches.numUnresolved = breachStats.numBreaches.count - breachStats.numBreaches.numResolved
×
186

187
  return breachStats
×
188
}
189

190
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