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

mozilla / blurts-server / #13412

pending completion
#13412

push

circleci

Vinnl
Add unique page titles

282 of 1792 branches covered (15.74%)

Branch coverage included in aggregate %.

959 of 4671 relevant lines covered (20.53%)

1.71 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 { getMessage } from '../utils/fluent.js'
9
import { appendBreachResolutionChecklist } from '../utils/breachResolution.js'
10
import { generateToken } from '../utils/csrf.js'
11
import { getAllEmailsAndBreaches } from '../utils/breaches.js'
12
import { getCountryCode } from '../utils/countryCode.js'
13

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

25
  const data = {
×
26
    breachesData,
27
    breachLogos: req.app.locals.breachLogoMap,
28
    emailVerifiedCount,
29
    emailTotalCount,
30
    selectedEmailIndex,
31
    partial: breaches,
32
    csrfToken: generateToken(res),
33
    fxaProfile: req.user.fxa_profile_json,
34
    meta: {
35
      title: getMessage('breach-meta-title')
36
    }
37
  }
38

39
  res.send(mainLayout(data))
×
40
}
41

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

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

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

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

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

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

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

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

129
  const updatedSubscriber = await setBreachResolution(sessionUser, currentBreachResolution)
×
130

131
  req.session.user = updatedSubscriber
×
132

133
  const userBreachStats = breachStatsV1(verifiedEmails)
×
134

135
  await updateBreachStats(sessionUser.id, userBreachStats)
×
136

137
  res.json(updatedSubscriber.breach_resolution)
×
138
}
139

140
// PRIVATE
141

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

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

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

199
  // total number of verified emails being monitored
200
  breachStats.monitoredEmails.count = verifiedEmails.length
×
201

202
  // total number of breaches across all emails
203
  breachStats.numBreaches.count = foundBreaches.length
×
204

205
  breachStats.numBreaches.numUnresolved = breachStats.numBreaches.count - breachStats.numBreaches.numResolved
×
206

207
  return breachStats
×
208
}
209

210
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