add cron and breach stats lookup/update

This commit is contained in:
Amri Toufali 2022-08-21 23:02:37 -07:00
Родитель bf3ad9f27a
Коммит 7961f820ae
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 75269D7487754F5D
12 изменённых файлов: 303 добавлений и 3033 удалений

Просмотреть файл

@ -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: '',

29
cron.js Normal file
Просмотреть файл

@ -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 }

Просмотреть файл

@ -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)

3184
package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -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>