add cron and breach stats lookup/update
This commit is contained in:
Родитель
bf3ad9f27a
Коммит
7961f820ae
|
@ -56,7 +56,8 @@ const optionalEnvVars = [
|
|||
'VPN_PROMO_BLOCKED_LOCALES',
|
||||
'EDUCATION_VIDEO_URL_RELAY',
|
||||
'EDUCATION_VIDEO_URL_VPN',
|
||||
'ADMINS'
|
||||
'ADMINS',
|
||||
'MONTHLY_CRON_ENABLED'
|
||||
]
|
||||
|
||||
const AppConstants = { }
|
||||
|
|
|
@ -7,6 +7,7 @@ const { readdir } = require('fs/promises')
|
|||
const partialDir = path.join(path.dirname(require.main.filename), '/views/partials/email_partials')
|
||||
const { LocaleUtils } = require('./../locale-utils')
|
||||
const DB = require('../db/DB')
|
||||
const { initMinuteCron } = require('../cron')
|
||||
|
||||
let partialFilenames
|
||||
|
||||
|
@ -111,30 +112,46 @@ function notFound (req, res) {
|
|||
}
|
||||
|
||||
async function previewEmail2022 (req, res) {
|
||||
const breachStats = await DB.getBreachStats(req.user)
|
||||
const breachStats = await DB.getBreachStats(req.user.id)
|
||||
|
||||
res.render('layouts/email-2022-mockup', {
|
||||
layout: 'email-2022-mockup',
|
||||
whichPartial: 'email_partials/email-monthly-unresolved',
|
||||
csrfToken: req.csrfToken(),
|
||||
recipientEmail: req.user.primary_email,
|
||||
primaryEmail: req.user.primary_email,
|
||||
breachStats
|
||||
})
|
||||
}
|
||||
|
||||
function sendTestEmail (data) {
|
||||
return async function (req, res, next) {
|
||||
await EmailUtils.sendEmail(req.body.recipientEmail, LocaleUtils.fluentFormat(req.supportedLocales, data.subjectId), data.layout,
|
||||
{
|
||||
recipientEmail: req.body.recipientEmail,
|
||||
supportedLocales: req.supportedLocales,
|
||||
whichPartial: data.whichPartial
|
||||
}
|
||||
)
|
||||
return async function (req, res) {
|
||||
const breachStats = await DB.getBreachStats(req.user.id)
|
||||
const subject = LocaleUtils.fluentFormat(req.supportedLocales, data.subjectId)
|
||||
const context = {
|
||||
whichPartial: data.whichPartial,
|
||||
supportedLocales: req.supportedLocales,
|
||||
primaryEmail: req.user.primary_email,
|
||||
breachStats
|
||||
}
|
||||
|
||||
async function sendEmail () {
|
||||
await EmailUtils.sendEmail(req.body.recipientEmail, subject, data.layout, context)
|
||||
}
|
||||
|
||||
if (req.body.delay === 'on') {
|
||||
initMinuteCron(sendEmail)
|
||||
|
||||
return res.send(`
|
||||
<h2>Email scheduled to send 1 minute from now!</h2>
|
||||
<a href='/email-l10n/email-2022-mockup'>Go Back</a> | <a href='/user/logout'>Sign Out</a>
|
||||
`)
|
||||
}
|
||||
|
||||
await sendEmail()
|
||||
|
||||
res.send(`
|
||||
<h2>Email sent!</h2>
|
||||
<a href='/user/logout'>Sign Out</a>
|
||||
<a href='/email-l10n/email-2022-mockup'>Go Back</a> | <a href='/user/logout'>Sign Out</a>
|
||||
`)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -238,7 +238,11 @@ async function getDashboard (req, res) {
|
|||
|
||||
const adUnitNum = setAdUnitCookie(req, res)
|
||||
|
||||
DB.updateBreachStats(user, resultsSummary(verifiedEmails)) // TODO: find a better place to update DB e.g. scan-results.js
|
||||
if (!req.session.statsUpdated) {
|
||||
// update user's breach stats in DB once per session without blocking render
|
||||
DB.updateBreachStats(user.id, resultsSummary(verifiedEmails))
|
||||
req.session.statsUpdated = true
|
||||
}
|
||||
|
||||
res.render('dashboards', {
|
||||
title: req.fluentFormat('Firefox Monitor'),
|
||||
|
@ -424,6 +428,8 @@ async function postResolveBreach (req, res) {
|
|||
const numTotalBreaches = userBreachStats.numBreaches.count
|
||||
const numResolvedBreaches = userBreachStats.numBreaches.numResolved
|
||||
|
||||
DB.updateBreachStats(sessionUser.id, userBreachStats)
|
||||
|
||||
const localizedModalStrings = {
|
||||
headline: '',
|
||||
progressMessage: '',
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
const cron = require('node-cron')
|
||||
|
||||
function initMonthlyCron (...jobs) {
|
||||
cron.schedule('* * 1 * *', () => {
|
||||
jobs.forEach(job => job())
|
||||
})
|
||||
}
|
||||
|
||||
// For testing node-cron
|
||||
function initMinuteCron (job) {
|
||||
console.log('running single cron job 1 minute from now')
|
||||
let killCron = false
|
||||
const cronMinute = cron.schedule('* * * * *', () => {
|
||||
console.log('running 1 minute cron job')
|
||||
killCron = true
|
||||
job()
|
||||
})
|
||||
|
||||
const interval = setInterval(() => {
|
||||
console.log('interval')
|
||||
if (killCron) {
|
||||
console.log('stopping cron')
|
||||
cronMinute.stop()
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, 10000)
|
||||
}
|
||||
|
||||
module.exports = { initMonthlyCron, initMinuteCron }
|
21
db/DB.js
21
db/DB.js
|
@ -419,21 +419,30 @@ const DB = {
|
|||
await knex('email_addresses').where({ subscriber_id: uid }).del()
|
||||
},
|
||||
|
||||
async updateBreachStats (subscriber, stats) {
|
||||
await knex('subscribers').where('id', subscriber.id)
|
||||
async updateBreachStats (id, stats) {
|
||||
await knex('subscribers')
|
||||
.where('id', id)
|
||||
.update({
|
||||
breach_stats: stats
|
||||
})
|
||||
},
|
||||
|
||||
async getBreachStats (subscriber) {
|
||||
const res = await knex.table('subscribers').first('breach_stats').where('id', subscriber.id).whereNotNull('breach_stats')
|
||||
const breachStats = res.breach_stats
|
||||
async getBreachStats (id) {
|
||||
const { breach_stats: breachStats } = await knex('subscribers')
|
||||
.where({ id })
|
||||
.first('breach_stats')
|
||||
|
||||
breachStats.numBreaches.numUnresolved = breachStats.numBreaches.count - breachStats.numBreaches.numResolved
|
||||
return breachStats
|
||||
},
|
||||
|
||||
async getSubscribersWithUnresolvedBreaches () {
|
||||
const res = await knex('subscribers')
|
||||
.select('primary_email', 'breach_stats', 'signup_language')
|
||||
.whereJsonPath('breach_stats', '$.numBreaches.numUnresolved', '>', 0)
|
||||
|
||||
return res
|
||||
},
|
||||
|
||||
async createConnection () {
|
||||
if (knex === null) {
|
||||
knex = Knex(knexConfig)
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -26,9 +26,10 @@
|
|||
"helmet": "4.2.0",
|
||||
"intl-pluralrules": "1.2.1",
|
||||
"isemail": "3.2.0",
|
||||
"knex": "0.21.12",
|
||||
"knex": "^2.2.0",
|
||||
"knex-paginate": "1.2.2",
|
||||
"mozlog": "3.0.2",
|
||||
"node-cron": "^3.0.2",
|
||||
"nodemailer": "^6.6.5",
|
||||
"nodemailer-express-handlebars": "^4.0.0",
|
||||
"pg": "^8.7.1",
|
||||
|
|
|
@ -144,6 +144,9 @@ function resultsSummary (verifiedEmails) {
|
|||
|
||||
// total number of breaches across all emails
|
||||
breachStats.numBreaches.count = foundBreaches.length
|
||||
|
||||
breachStats.numBreaches.numUnresolved = breachStats.numBreaches.count - breachStats.numBreaches.numResolved
|
||||
|
||||
return breachStats
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
const DB = require('../db/DB')
|
||||
const EmailUtils = require('../email-utils')
|
||||
const { LocaleUtils } = require('../locale-utils')
|
||||
|
||||
async function sendUnresolvedBreachEmails () {
|
||||
const subscribers = await DB.getSubscribersWithUnresolvedBreaches()
|
||||
|
||||
subscribers.forEach(async subscriber => {
|
||||
const supportedLocales = [subscriber.signup_language, 'en'].filter(Boolean) // filter potential nullish signup_language
|
||||
const subject = LocaleUtils.fluentFormat(supportedLocales, 'email-unresolved-heading')
|
||||
|
||||
await EmailUtils.sendEmail(subscriber.primary_email, subject, 'email-2022',
|
||||
{
|
||||
whichPartial: 'email_partials/email-monthly-unresolved',
|
||||
supportedLocales,
|
||||
primaryEmail: subscriber.primary_email,
|
||||
breachStats: subscriber.breach_stats
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { sendUnresolvedBreachEmails }
|
|
@ -34,6 +34,8 @@ const EmailUtils = require('./email-utils')
|
|||
const HBSHelpers = require('./template-helpers/')
|
||||
const HIBP = require('./hibp')
|
||||
const IpLocationService = require('./ip-location-service')
|
||||
const { initMonthlyCron } = require('./cron')
|
||||
const { sendUnresolvedBreachEmails } = require('./scripts/send-email-to-unresolved-breach-subscribers')
|
||||
|
||||
const {
|
||||
addRequestToResponse, pickLanguage, logErrors, localizeErrorMessages,
|
||||
|
@ -250,3 +252,8 @@ EmailUtils.init().then(() => {
|
|||
}).catch(error => {
|
||||
log.error('try-initialize-email-error', { error })
|
||||
})
|
||||
|
||||
if (AppConstants.MONTHLY_CRON_ENABLED && AppConstants.MONTHLY_CRON_ENABLED !== 'false') {
|
||||
console.log('Starting monthly cron job.')
|
||||
initMonthlyCron(sendUnresolvedBreachEmails)
|
||||
}
|
||||
|
|
|
@ -13,10 +13,16 @@
|
|||
<strong>Monthly Email: Unresolved Breaches</strong>
|
||||
<form method="POST" action="send-email-2022">
|
||||
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
||||
<input type="email" name="recipientEmail" placeholder="Recipient email">
|
||||
<button type="submit"
|
||||
style="background-color: #666; color: white; border-radius: 6px; padding: 12px; border: 0;">Send test
|
||||
email</button>
|
||||
<div style="display: flex; gap: 12px;">
|
||||
<span style="font-size: 14px; display: flex; flex-flow: column; gap: 3px; align-items: end;">
|
||||
<input type="email" name="recipientEmail" placeholder="Recipient email"
|
||||
style="text-align: right; padding: 6px; border-radius: 6px; border: 2px solid #808080;">
|
||||
<label>1 minute delay <input type="checkbox" name="delay"></label>
|
||||
</span>
|
||||
<button type="submit"
|
||||
style="background-color: #666; color: white; border-radius: 6px; padding: 12px; border: 0; cursor: pointer">Send test
|
||||
email</button>
|
||||
</div>
|
||||
</form>
|
||||
</header>
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<tr>
|
||||
<td>
|
||||
<p>
|
||||
<strong>{{{ getString "email-is-affected" email-address=recipientEmail }}}</strong>
|
||||
<strong>{{{ getString "email-is-affected" email-address=primaryEmail }}}</strong>
|
||||
</p>
|
||||
<p>{{ getString "email-more-detail" }}</p>
|
||||
</td>
|
||||
|
|
Загрузка…
Ссылка в новой задаче