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

mozilla / blurts-server / 17fce790-ebdd-469f-a243-e5224a22b5b4

pending completion
17fce790-ebdd-469f-a243-e5224a22b5b4

Pull #2790

circleci

Florian Zia
merge: main -> MNTOR-1056-Migrate-breach-alert-email
Pull Request #2790: MNTOR-1056: Migrate breach alert email

282 of 1293 branches covered (21.81%)

Branch coverage included in aggregate %.

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

959 of 3510 relevant lines covered (27.32%)

4.44 hits per line

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

0.0
/src/client/js/partials/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
const breachesPartial = document.querySelector("[data-partial='breaches']")
×
6
const state = new Proxy({
×
7
  selectedEmail: null,
8
  selectedStatus: 'unresolved'
9
}, {
10
  set (target, key, value) {
11
    if (target[key] !== value) {
×
12
      target[key] = value
×
13
      render()
×
14
    }
15

16
    return true
×
17
  }
18
})
19

20
let breachesTable, breachRows, emailSelect, statusFilter, resolvedCountOutput, unresolvedCountOutput
21

22
function init () {
23
  breachesTable = breachesPartial.querySelector('.breaches-table')
×
24
  breachRows = breachesTable.querySelectorAll('.breach-row')
×
25
  emailSelect = breachesPartial.querySelector('.breaches-header custom-select')
×
26
  statusFilter = breachesPartial.querySelector('.breaches-filter')
×
27
  resolvedCountOutput = statusFilter.querySelector("label[for='breaches-resolved'] output")
×
28
  unresolvedCountOutput = statusFilter.querySelector("label[for='breaches-unresolved'] output")
×
29

30
  state.selectedEmail = emailSelect.value // triggers render
×
31

32
  emailSelect.addEventListener('change', handleEvent)
×
33
  statusFilter.addEventListener('change', handleEvent)
×
34
  breachesTable.addEventListener('change', handleEvent)
×
35
}
36

37
function handleEvent (e) {
38
  switch (true) {
×
39
    case e.target.matches('custom-select[name="email-account"]'):
40
      state.selectedEmail = e.target.value
×
41
      break
×
42
    case e.target.matches('input[name="breaches-status"]'):
43
      state.selectedStatus = e.target.value
×
44
      break
×
45
    case e.target.matches('.resolve-list-item [type="checkbox"]'):
46
      updateBreachStatus(e.target)
×
47
      break
×
48
  }
49
}
50

51
async function updateBreachStatus (input) {
52
  const affectedEmail = state.selectedEmail
×
53
  const breachId = input.name
×
54
  const checkedInputs = Array.from(input.closest('.resolve-list').querySelectorAll('input:checked'))
×
55

56
  try {
×
57
    const res = await fetch('/api/v1/user/breaches', {
×
58
      method: 'PUT',
59
      headers: {
60
        'Content-Type': 'application/json',
61
        'x-csrf-token': breachesTable.dataset.token
62
      },
63
      body: JSON.stringify({
64
        affectedEmail,
65
        breachId,
66
        resolutionsChecked: checkedInputs.map(input => input.value)
×
67
      })
68
    })
69

70
    if (!res.ok) throw new Error('Bad fetch response')
×
71

72
    const data = await res.json()
×
73
    input.closest('.breach-row').dataset.status = data[affectedEmail][breachId].isResolved ? 'resolved' : 'unresolved'
×
74
    renderResolvedCounts()
×
75
  } catch (e) {
76
    console.error('Could not update user breach resolve status:', e)
×
77
  }
78
}
79

80
function renderResolvedCounts () {
81
  resolvedCountOutput.textContent = breachesPartial.querySelectorAll(`[data-status='resolved'][data-email='${state.selectedEmail}']`).length
×
82
  unresolvedCountOutput.textContent = breachesPartial.querySelectorAll(`[data-status='unresolved'][data-email='${state.selectedEmail}']`).length
×
83
}
84

85
function renderBreachRows () {
86
  let delay = 0
×
87
  let hidden
88

89
  breachRows.forEach(breach => {
×
90
    hidden = (breach.dataset.email !== state.selectedEmail) || (breach.dataset.status !== state.selectedStatus)
×
91
    breach.toggleAttribute('hidden', hidden)
×
92
    breach.removeAttribute('open')
×
93
    if (!hidden) {
×
94
      breach.style.setProperty('--delay', `${delay}ms`)
×
95
      delay += 50
×
96
    }
97
  })
98
}
99

100
function render () {
101
  // render split into separate functions to allow independent trigger
102
  // e.g. if user marks all steps resolved – update the count, but leave the breach in place for further user interaction
103
  renderResolvedCounts()
×
104
  renderBreachRows()
×
105
}
106

107
const sendAlertTestButton = document.getElementById('sendAlertTestButton')
×
108
sendAlertTestButton.addEventListener('click', () => sendBreachAlertEmail())
×
109
async function sendBreachAlertEmail () {
110
  try {
×
111
    const res = await fetch('/api/v1/hibp/notify', {
×
112
      method: 'POST',
113
      headers: {
114
        Authorization: 'Bearer <HIBP_KANON_API_TOKEN>',
115
        'Content-Type': 'application/json',
116
        'x-csrf-token': breachesTable.dataset.token
117
      },
118
      body: JSON.stringify({
119
        breachName: 'Adobe',
120
        hashPrefix: '365050',
121
        hashSuffixes: ['53cbb89874fc738c0512daf12bc4d91765']
122
      })
123
    })
124

125
    if (!res.ok) throw new Error('Bad fetch response')
×
126

127
    console.log('Sent breach alert email')
×
128
  } catch (e) {
129
    console.error('Could not send breach alert email:', e)
×
130
  }
131
}
132

133
if (breachesPartial) init()
×
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