add ESLint auto-fixable changes (quotes, semi...)
This commit is contained in:
Родитель
7112d08b01
Коммит
344e394bb0
|
@ -1,13 +1,13 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
// See https://jestjs.io/docs/en/manual-mocks
|
||||
function MessageValidator() {}
|
||||
function MessageValidator () {}
|
||||
MessageValidator.prototype.validate = function (hash, cb) {
|
||||
if (hash.Signature === "invalid") {
|
||||
cb(new Error("The message signature is invalid."));
|
||||
return;
|
||||
if (hash.Signature === 'invalid') {
|
||||
cb(new Error('The message signature is invalid.'))
|
||||
return
|
||||
}
|
||||
cb(null, hash);
|
||||
};
|
||||
cb(null, hash)
|
||||
}
|
||||
|
||||
module.exports = MessageValidator;
|
||||
module.exports = MessageValidator
|
||||
|
|
120
app-constants.js
120
app-constants.js
|
@ -1,79 +1,79 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
/* eslint-disable no-process-env */
|
||||
|
||||
const path = require("path");
|
||||
require("dotenv").config({path: path.join(__dirname, ".env")});
|
||||
const path = require('path')
|
||||
require('dotenv').config({ path: path.join(__dirname, '.env') })
|
||||
|
||||
const requiredEnvVars = [
|
||||
"NODE_ENV",
|
||||
"SERVER_URL",
|
||||
"PORT",
|
||||
"LOGOS_ORIGIN",
|
||||
"COOKIE_SECRET",
|
||||
"SESSION_DURATION_HOURS",
|
||||
"SMTP_URL",
|
||||
"EMAIL_FROM",
|
||||
"SES_CONFIG_SET",
|
||||
"SES_NOTIFICATION_LOG_ONLY",
|
||||
"BREACH_RESOLUTION_ENABLED",
|
||||
"FXA_ENABLED",
|
||||
"FXA_SETTINGS_URL",
|
||||
"FX_REMOTE_SETTINGS_WRITER_SERVER",
|
||||
"FX_REMOTE_SETTINGS_WRITER_USER",
|
||||
"FX_REMOTE_SETTINGS_WRITER_PASS",
|
||||
"MOZLOG_FMT",
|
||||
"MOZLOG_LEVEL",
|
||||
"OAUTH_AUTHORIZATION_URI",
|
||||
"OAUTH_TOKEN_URI",
|
||||
"OAUTH_PROFILE_URI",
|
||||
"OAUTH_CLIENT_ID",
|
||||
"OAUTH_CLIENT_SECRET",
|
||||
"HIBP_KANON_API_ROOT",
|
||||
"HIBP_KANON_API_TOKEN",
|
||||
"HIBP_API_ROOT",
|
||||
"HIBP_RELOAD_BREACHES_TIMER",
|
||||
"HIBP_THROTTLE_DELAY",
|
||||
"HIBP_THROTTLE_MAX_TRIES",
|
||||
"HIBP_NOTIFY_TOKEN",
|
||||
"DATABASE_URL",
|
||||
"PAGE_TOKEN_TIMER",
|
||||
"PRODUCT_PROMOS_ENABLED",
|
||||
"REDIS_URL",
|
||||
"SENTRY_DSN",
|
||||
"DELETE_UNVERIFIED_SUBSCRIBERS_TIMER",
|
||||
"EXPERIMENT_ACTIVE",
|
||||
"MAX_NUM_ADDRESSES",
|
||||
];
|
||||
'NODE_ENV',
|
||||
'SERVER_URL',
|
||||
'PORT',
|
||||
'LOGOS_ORIGIN',
|
||||
'COOKIE_SECRET',
|
||||
'SESSION_DURATION_HOURS',
|
||||
'SMTP_URL',
|
||||
'EMAIL_FROM',
|
||||
'SES_CONFIG_SET',
|
||||
'SES_NOTIFICATION_LOG_ONLY',
|
||||
'BREACH_RESOLUTION_ENABLED',
|
||||
'FXA_ENABLED',
|
||||
'FXA_SETTINGS_URL',
|
||||
'FX_REMOTE_SETTINGS_WRITER_SERVER',
|
||||
'FX_REMOTE_SETTINGS_WRITER_USER',
|
||||
'FX_REMOTE_SETTINGS_WRITER_PASS',
|
||||
'MOZLOG_FMT',
|
||||
'MOZLOG_LEVEL',
|
||||
'OAUTH_AUTHORIZATION_URI',
|
||||
'OAUTH_TOKEN_URI',
|
||||
'OAUTH_PROFILE_URI',
|
||||
'OAUTH_CLIENT_ID',
|
||||
'OAUTH_CLIENT_SECRET',
|
||||
'HIBP_KANON_API_ROOT',
|
||||
'HIBP_KANON_API_TOKEN',
|
||||
'HIBP_API_ROOT',
|
||||
'HIBP_RELOAD_BREACHES_TIMER',
|
||||
'HIBP_THROTTLE_DELAY',
|
||||
'HIBP_THROTTLE_MAX_TRIES',
|
||||
'HIBP_NOTIFY_TOKEN',
|
||||
'DATABASE_URL',
|
||||
'PAGE_TOKEN_TIMER',
|
||||
'PRODUCT_PROMOS_ENABLED',
|
||||
'REDIS_URL',
|
||||
'SENTRY_DSN',
|
||||
'DELETE_UNVERIFIED_SUBSCRIBERS_TIMER',
|
||||
'EXPERIMENT_ACTIVE',
|
||||
'MAX_NUM_ADDRESSES'
|
||||
]
|
||||
|
||||
const optionalEnvVars = [
|
||||
"RECRUITMENT_BANNER_LINK",
|
||||
"RECRUITMENT_BANNER_TEXT",
|
||||
"GEOIP_GEOLITE2_PATH",
|
||||
"GEOIP_GEOLITE2_CITY_FILENAME",
|
||||
"GEOIP_GEOLITE2_COUNTRY_FILENAME",
|
||||
"VPN_PROMO_BLOCKED_LOCALES",
|
||||
"EDUCATION_VIDEO_URL_RELAY",
|
||||
"EDUCATION_VIDEO_URL_VPN",
|
||||
];
|
||||
'RECRUITMENT_BANNER_LINK',
|
||||
'RECRUITMENT_BANNER_TEXT',
|
||||
'GEOIP_GEOLITE2_PATH',
|
||||
'GEOIP_GEOLITE2_CITY_FILENAME',
|
||||
'GEOIP_GEOLITE2_COUNTRY_FILENAME',
|
||||
'VPN_PROMO_BLOCKED_LOCALES',
|
||||
'EDUCATION_VIDEO_URL_RELAY',
|
||||
'EDUCATION_VIDEO_URL_VPN'
|
||||
]
|
||||
|
||||
const AppConstants = { };
|
||||
const AppConstants = { }
|
||||
|
||||
if (!process.env.SERVER_URL && process.env.NODE_ENV === "heroku") {
|
||||
process.env.SERVER_URL = `https://${process.env.HEROKU_APP_NAME}.herokuapp.com`;
|
||||
if (!process.env.SERVER_URL && process.env.NODE_ENV === 'heroku') {
|
||||
process.env.SERVER_URL = `https://${process.env.HEROKU_APP_NAME}.herokuapp.com`
|
||||
}
|
||||
|
||||
for (const v of requiredEnvVars) {
|
||||
if (process.env[v] === undefined) {
|
||||
throw new Error(`Required environment variable was not set: ${v}`);
|
||||
throw new Error(`Required environment variable was not set: ${v}`)
|
||||
}
|
||||
AppConstants[v] = process.env[v];
|
||||
AppConstants[v] = process.env[v]
|
||||
}
|
||||
|
||||
optionalEnvVars.forEach(key => {
|
||||
if (key in process.env) AppConstants[key] = process.env[key];
|
||||
});
|
||||
if (key in process.env) AppConstants[key] = process.env[key]
|
||||
})
|
||||
|
||||
AppConstants.VPN_PROMO_BLOCKED_LOCALES = AppConstants.VPN_PROMO_BLOCKED_LOCALES?.split(",");
|
||||
AppConstants.VPN_PROMO_BLOCKED_LOCALES = AppConstants.VPN_PROMO_BLOCKED_LOCALES?.split(',')
|
||||
|
||||
module.exports = Object.freeze(AppConstants);
|
||||
module.exports = Object.freeze(AppConstants)
|
||||
|
|
|
@ -1,64 +1,64 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const HIBP = require("../hibp");
|
||||
const DB = require("../db/DB");
|
||||
const { changePWLinks } = require("../lib/changePWLinks");
|
||||
const { getAllEmailsAndBreaches } = require("./user");
|
||||
const HIBP = require('../hibp')
|
||||
const DB = require('../db/DB')
|
||||
const { changePWLinks } = require('../lib/changePWLinks')
|
||||
const { getAllEmailsAndBreaches } = require('./user')
|
||||
|
||||
async function getBreachDetail(req, res) {
|
||||
const allBreaches = req.app.locals.breaches;
|
||||
const breachName = req.params.breachName;
|
||||
const featuredBreach = HIBP.getBreachByName(allBreaches, breachName);
|
||||
async function getBreachDetail (req, res) {
|
||||
const allBreaches = req.app.locals.breaches
|
||||
const breachName = req.params.breachName
|
||||
const featuredBreach = HIBP.getBreachByName(allBreaches, breachName)
|
||||
|
||||
if (!featuredBreach) {
|
||||
return res.redirect("/");
|
||||
return res.redirect('/')
|
||||
}
|
||||
|
||||
const affectedEmails = [];
|
||||
const affectedEmails = []
|
||||
|
||||
if (req.session && req.session.user) {
|
||||
const user = await DB.getSubscriberById(req.session.user.id);
|
||||
req.session.user = user;
|
||||
const user = await DB.getSubscriberById(req.session.user.id)
|
||||
req.session.user = user
|
||||
|
||||
const allEmailsAndBreaches = await getAllEmailsAndBreaches(req.session.user, allBreaches);
|
||||
const allEmailsAndBreaches = await getAllEmailsAndBreaches(req.session.user, allBreaches)
|
||||
for (const verifiedEmail of allEmailsAndBreaches.verifiedEmails) {
|
||||
for (const breach of verifiedEmail.breaches) {
|
||||
if (breach.Name === breachName) {
|
||||
affectedEmails.push({
|
||||
emailAddress: verifiedEmail.email,
|
||||
recencyIndex: breach.recencyIndex,
|
||||
isResolved: breach.IsResolved,
|
||||
});
|
||||
isResolved: breach.IsResolved
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const changePWLink = getChangePWLink(featuredBreach);
|
||||
res.render("breach-detail", {
|
||||
title: req.fluentFormat("home-title"),
|
||||
const changePWLink = getChangePWLink(featuredBreach)
|
||||
res.render('breach-detail', {
|
||||
title: req.fluentFormat('home-title'),
|
||||
featuredBreach,
|
||||
changePWLink,
|
||||
affectedEmails,
|
||||
});
|
||||
affectedEmails
|
||||
})
|
||||
}
|
||||
|
||||
function getChangePWLink(breach) {
|
||||
if (!breach.DataClasses.includes("passwords")) {
|
||||
return "";
|
||||
function getChangePWLink (breach) {
|
||||
if (!breach.DataClasses.includes('passwords')) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (changePWLinks.hasOwnProperty(breach.Name)) {
|
||||
return changePWLinks[breach.Name];
|
||||
return changePWLinks[breach.Name]
|
||||
}
|
||||
|
||||
if (breach.Domain) {
|
||||
return "https://www." + breach.Domain;
|
||||
return 'https://www.' + breach.Domain
|
||||
}
|
||||
|
||||
return "";
|
||||
return ''
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getBreachDetail,
|
||||
};
|
||||
getBreachDetail
|
||||
}
|
||||
|
|
|
@ -1,58 +1,55 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const AppConstants = require("../app-constants");
|
||||
const mozlog = require("../log");
|
||||
const {version, homepage, supportedLocales} = require("../package.json");
|
||||
const AppConstants = require('../app-constants')
|
||||
const mozlog = require('../log')
|
||||
const { version, homepage, supportedLocales } = require('../package.json')
|
||||
|
||||
|
||||
const log = mozlog("controllers.dockerflow");
|
||||
const versionJsonPath = path.join(__dirname, "..", "version.json");
|
||||
const log = mozlog('controllers.dockerflow')
|
||||
const versionJsonPath = path.join(__dirname, '..', 'version.json')
|
||||
|
||||
// If the version.json file already exists (e.g., created by circle + docker),
|
||||
// don't need to generate it
|
||||
if (!fs.existsSync(versionJsonPath)) {
|
||||
log.info("generating");
|
||||
let commit;
|
||||
log.info('generating')
|
||||
let commit
|
||||
try {
|
||||
commit = require("git-rev-sync").short();
|
||||
commit = require('git-rev-sync').short()
|
||||
} catch (err) {
|
||||
log.error("generating", {err: err});
|
||||
log.error('generating', { err })
|
||||
}
|
||||
|
||||
const versionJson = {
|
||||
commit,
|
||||
source: homepage,
|
||||
version,
|
||||
languages: supportedLocales,
|
||||
};
|
||||
languages: supportedLocales
|
||||
}
|
||||
|
||||
fs.writeFileSync(versionJsonPath, JSON.stringify(versionJson, null, 2) + "\n");
|
||||
fs.writeFileSync(versionJsonPath, JSON.stringify(versionJson, null, 2) + '\n')
|
||||
}
|
||||
|
||||
|
||||
function vers (req, res) {
|
||||
if (AppConstants.NODE_ENV === "heroku") {
|
||||
if (AppConstants.NODE_ENV === 'heroku') {
|
||||
/* eslint-disable no-process-env */
|
||||
return res.json({
|
||||
commit: process.env.HEROKU_SLUG_COMMIT,
|
||||
version: process.env.HEROKU_SLUG_COMMIT,
|
||||
source: homepage,
|
||||
languages: "*",
|
||||
});
|
||||
languages: '*'
|
||||
})
|
||||
/* eslint-enable no-process-env */
|
||||
}
|
||||
return res.sendFile(versionJsonPath);
|
||||
return res.sendFile(versionJsonPath)
|
||||
}
|
||||
|
||||
|
||||
function heartbeat (req, res) {
|
||||
return res.send("OK");
|
||||
return res.send('OK')
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
vers,
|
||||
heartbeat,
|
||||
};
|
||||
heartbeat
|
||||
}
|
||||
|
|
|
@ -1,114 +1,114 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const EmailUtils = require("../email-utils");
|
||||
const AppConstants = require("../app-constants");
|
||||
const path = require("path");
|
||||
const { readdir } = require("fs/promises");
|
||||
const partialDir = path.join(path.dirname(require.main.filename), "/views/partials/email_partials");
|
||||
const EmailUtils = require('../email-utils')
|
||||
const AppConstants = require('../app-constants')
|
||||
const path = require('path')
|
||||
const { readdir } = require('fs/promises')
|
||||
const partialDir = path.join(path.dirname(require.main.filename), '/views/partials/email_partials')
|
||||
|
||||
let partialFilenames;
|
||||
let partialFilenames
|
||||
|
||||
async function getPartialFilenames() {
|
||||
async function getPartialFilenames () {
|
||||
try {
|
||||
partialFilenames = await readdir(partialDir);
|
||||
partialFilenames = await readdir(partialDir)
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
partialFilenames = [];
|
||||
console.error(e)
|
||||
partialFilenames = []
|
||||
}
|
||||
|
||||
return partialFilenames;
|
||||
return partialFilenames
|
||||
}
|
||||
|
||||
async function getEmailMockUps(req, res) {
|
||||
const email = "example@email.com";
|
||||
const partials = partialFilenames || await getPartialFilenames();
|
||||
async function getEmailMockUps (req, res) {
|
||||
const email = 'example@email.com'
|
||||
const partials = partialFilenames || await getPartialFilenames()
|
||||
|
||||
if (!["dev", "heroku"].includes(AppConstants.NODE_ENV)) return notFound(req, res);
|
||||
if (!['dev', 'heroku'].includes(AppConstants.NODE_ENV)) return notFound(req, res)
|
||||
|
||||
if (!req.query.partial) {
|
||||
req.query.partial = "email_verify";
|
||||
req.query.type = "email_verify";
|
||||
req.query.partial = 'email_verify'
|
||||
req.query.type = 'email_verify'
|
||||
}
|
||||
|
||||
if (!partials.includes(`${req.query.partial}.hbs`)) return notFound(req, res);
|
||||
if (!partials.includes(`${req.query.partial}.hbs`)) return notFound(req, res)
|
||||
|
||||
if (["breachAlert", "pre-fxa", "singleBreach", "multipleBreaches", "noBreaches", "email_verify"].indexOf(req.query.type) === -1) {
|
||||
return res.redirect("/email-l10n");
|
||||
if (['breachAlert', 'pre-fxa', 'singleBreach', 'multipleBreaches', 'noBreaches', 'email_verify'].indexOf(req.query.type) === -1) {
|
||||
return res.redirect('/email-l10n')
|
||||
}
|
||||
|
||||
const unsafeBreachesForEmail = [];
|
||||
["Dropbox", "Apollo", "Adobe"].forEach(name => {
|
||||
unsafeBreachesForEmail.push(req.app.locals.breaches.filter(breach => breach.Name === name)[0]);
|
||||
});
|
||||
['Dropbox', 'Apollo', 'Adobe'].forEach(name => {
|
||||
unsafeBreachesForEmail.push(req.app.locals.breaches.filter(breach => breach.Name === name)[0])
|
||||
})
|
||||
|
||||
const emailContent = ((req) => {
|
||||
switch(req.query.type) {
|
||||
case "pre-fxa":
|
||||
switch (req.query.type) {
|
||||
case 'pre-fxa':
|
||||
return {
|
||||
emailSubject: req.fluentFormat("pre-fxa-subject"),
|
||||
emailSubject: req.fluentFormat('pre-fxa-subject'),
|
||||
preFxaEmail: true,
|
||||
breachAlert: null,
|
||||
};
|
||||
case "noBreaches":
|
||||
breachAlert: null
|
||||
}
|
||||
case 'noBreaches':
|
||||
return {
|
||||
emailSubject: req.fluentFormat("email-subject-no-breaches"),
|
||||
emailSubject: req.fluentFormat('email-subject-no-breaches'),
|
||||
breachAlert: null,
|
||||
unsafeBreachesForEmail: [],
|
||||
};
|
||||
case "breachAlert":
|
||||
unsafeBreachesForEmail: []
|
||||
}
|
||||
case 'breachAlert':
|
||||
return {
|
||||
emailSubject: req.fluentFormat("breach-alert-subject"),
|
||||
breachAlert: req.app.locals.breaches.filter(breach => breach.Name === "LinkedIn")[0],
|
||||
emailSubject: req.fluentFormat('breach-alert-subject'),
|
||||
breachAlert: req.app.locals.breaches.filter(breach => breach.Name === 'LinkedIn')[0],
|
||||
unsafeBreachesForEmail: null,
|
||||
preFxaSubscriber: true,
|
||||
};
|
||||
case "email_verify":
|
||||
preFxaSubscriber: true
|
||||
}
|
||||
case 'email_verify':
|
||||
return {
|
||||
emailSubject: req.fluentFormat("email-subject-verify"),
|
||||
emailSubject: req.fluentFormat('email-subject-verify'),
|
||||
breachAlert: null,
|
||||
unsafeBreachesForEmail: null,
|
||||
};
|
||||
case "multipleBreaches":
|
||||
unsafeBreachesForEmail: null
|
||||
}
|
||||
case 'multipleBreaches':
|
||||
return {
|
||||
emailSubject: req.fluentFormat("email-subject-found-breaches"),
|
||||
unsafeBreachesForEmail: unsafeBreachesForEmail,
|
||||
breachAlert: null,
|
||||
};
|
||||
emailSubject: req.fluentFormat('email-subject-found-breaches'),
|
||||
unsafeBreachesForEmail,
|
||||
breachAlert: null
|
||||
}
|
||||
default:
|
||||
return {
|
||||
emailSubject: req.fluentFormat("email-subject-found-breaches"),
|
||||
emailSubject: req.fluentFormat('email-subject-found-breaches'),
|
||||
unsafeBreachesForEmail: unsafeBreachesForEmail.slice(0, 1),
|
||||
breachAlert: null,
|
||||
};
|
||||
breachAlert: null
|
||||
}
|
||||
}
|
||||
})(req);
|
||||
})(req)
|
||||
|
||||
res.render("email_l10n", {
|
||||
layout: "email_l10n_mockups.hbs",
|
||||
res.render('email_l10n', {
|
||||
layout: 'email_l10n_mockups.hbs',
|
||||
unsafeBreachesForEmail: emailContent.unsafeBreachesForEmail,
|
||||
supportedLocales: req.supportedLocales,
|
||||
whichPartial: `email_partials/${req.query.partial}`,
|
||||
breachedEmail: "breachedEmail@testing.com",
|
||||
recipientEmail: "recipientEmail@testing.com",
|
||||
breachedEmail: 'breachedEmail@testing.com',
|
||||
recipientEmail: 'recipientEmail@testing.com',
|
||||
breachAlert: emailContent.breachAlert,
|
||||
emailSubject: emailContent.emailSubject,
|
||||
preFxaSubscriber: emailContent.preFxaSubscriber,
|
||||
email,
|
||||
preFxaEmail: emailContent.preFxaEmail,
|
||||
ctaHref: EmailUtils.getEmailCtaHref("breach-alert", "go-to-dashboard"),
|
||||
});
|
||||
ctaHref: EmailUtils.getEmailCtaHref('breach-alert', 'go-to-dashboard')
|
||||
})
|
||||
}
|
||||
|
||||
function notFound(req, res) {
|
||||
res.status(404);
|
||||
res.render("subpage", {
|
||||
analyticsID: "error",
|
||||
headline: req.fluentFormat("error-headline"),
|
||||
subhead: req.fluentFormat("home-not-found"),
|
||||
});
|
||||
function notFound (req, res) {
|
||||
res.status(404)
|
||||
res.render('subpage', {
|
||||
analyticsID: 'error',
|
||||
headline: req.fluentFormat('error-headline'),
|
||||
subhead: req.fluentFormat('home-not-found')
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getEmailMockUps,
|
||||
notFound,
|
||||
};
|
||||
notFound
|
||||
}
|
||||
|
|
|
@ -1,106 +1,104 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const { negotiateLanguages, acceptedLanguages } = require("fluent-langneg");
|
||||
const { negotiateLanguages, acceptedLanguages } = require('fluent-langneg')
|
||||
|
||||
const AppConstants = require("../app-constants");
|
||||
const DB = require("../db/DB");
|
||||
const EmailUtils = require("../email-utils");
|
||||
const HIBP = require("../hibp");
|
||||
const { LocaleUtils } = require ("../locale-utils");
|
||||
const mozlog = require("../log");
|
||||
|
||||
|
||||
const log = mozlog("controllers.hibp");
|
||||
const AppConstants = require('../app-constants')
|
||||
const DB = require('../db/DB')
|
||||
const EmailUtils = require('../email-utils')
|
||||
const HIBP = require('../hibp')
|
||||
const { LocaleUtils } = require('../locale-utils')
|
||||
const mozlog = require('../log')
|
||||
|
||||
const log = mozlog('controllers.hibp')
|
||||
|
||||
// Get addresses and language from either subscribers or
|
||||
// email_addresses fields
|
||||
|
||||
function getAddressesAndLanguageForEmail(recipient) {
|
||||
const preFxaSubscriber = !recipient.fxa_uid ? true : false;
|
||||
const signupLanguage = recipient.signup_language;
|
||||
if (recipient.hasOwnProperty("email") && recipient.email) {
|
||||
function getAddressesAndLanguageForEmail (recipient) {
|
||||
const preFxaSubscriber = !recipient.fxa_uid
|
||||
const signupLanguage = recipient.signup_language
|
||||
if (recipient.hasOwnProperty('email') && recipient.email) {
|
||||
// email_addresses record, check all_emails_to_primary
|
||||
if (recipient.all_emails_to_primary) {
|
||||
return {
|
||||
recipientEmail: recipient.primary_email,
|
||||
breachedEmail: recipient.email,
|
||||
signupLanguage,
|
||||
preFxaSubscriber,
|
||||
};
|
||||
preFxaSubscriber
|
||||
}
|
||||
}
|
||||
return {
|
||||
recipientEmail: recipient.email,
|
||||
breachedEmail: recipient.email,
|
||||
signupLanguage,
|
||||
preFxaSubscriber,
|
||||
};
|
||||
preFxaSubscriber
|
||||
}
|
||||
}
|
||||
return {
|
||||
recipientEmail: recipient.primary_email,
|
||||
breachedEmail: recipient.primary_email,
|
||||
signupLanguage,
|
||||
preFxaSubscriber,
|
||||
};
|
||||
preFxaSubscriber
|
||||
}
|
||||
}
|
||||
|
||||
async function notify (req, res) {
|
||||
if (!req.token || req.token !== AppConstants.HIBP_NOTIFY_TOKEN) {
|
||||
const errorMessage = "HIBP notify endpoint requires valid authorization token.";
|
||||
throw new Error(errorMessage);
|
||||
const errorMessage = 'HIBP notify endpoint requires valid authorization token.'
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
if(!["breachName", "hashPrefix", "hashSuffixes"].every(req.body.hasOwnProperty, req.body)) {
|
||||
throw new Error("Breach notification requires breachName, hashPrefix, and hashSuffixes.");
|
||||
if (!['breachName', 'hashPrefix', 'hashSuffixes'].every(req.body.hasOwnProperty, req.body)) {
|
||||
throw new Error('Breach notification requires breachName, hashPrefix, and hashSuffixes.')
|
||||
}
|
||||
|
||||
const reqBreachName = req.body.breachName.toLowerCase();
|
||||
const reqHashPrefix = req.body.hashPrefix.toLowerCase();
|
||||
let breachAlert = HIBP.getBreachByName(req.app.locals.breaches, reqBreachName);
|
||||
const reqBreachName = req.body.breachName.toLowerCase()
|
||||
const reqHashPrefix = req.body.hashPrefix.toLowerCase()
|
||||
let breachAlert = HIBP.getBreachByName(req.app.locals.breaches, reqBreachName)
|
||||
|
||||
if (!breachAlert) {
|
||||
// If breach isn't found, try to reload breaches from HIBP
|
||||
await HIBP.loadBreachesIntoApp(req.app);
|
||||
breachAlert = HIBP.getBreachByName(req.app.locals.breaches, reqBreachName);
|
||||
await HIBP.loadBreachesIntoApp(req.app)
|
||||
breachAlert = HIBP.getBreachByName(req.app.locals.breaches, reqBreachName)
|
||||
if (!breachAlert) {
|
||||
// If breach *still* isn't found, we have a real error
|
||||
throw new Error("Unrecognized breach: " + reqBreachName);
|
||||
throw new Error('Unrecognized breach: ' + reqBreachName)
|
||||
}
|
||||
}
|
||||
|
||||
if (breachAlert.IsSpamList || breachAlert.IsFabricated || !breachAlert.IsVerified || breachAlert.Domain === "") {
|
||||
log.info(`${breachAlert.Name} is fabricated, a spam list, not associated with a website, or unverified. \n Breach Alert not sent.`);
|
||||
if (breachAlert.IsSpamList || breachAlert.IsFabricated || !breachAlert.IsVerified || breachAlert.Domain === '') {
|
||||
log.info(`${breachAlert.Name} is fabricated, a spam list, not associated with a website, or unverified. \n Breach Alert not sent.`)
|
||||
return res.status(200).json(
|
||||
{info: "Breach loaded into database. Subscribers not notified."}
|
||||
);
|
||||
{ info: 'Breach loaded into database. Subscribers not notified.' }
|
||||
)
|
||||
}
|
||||
|
||||
const hashes = req.body.hashSuffixes.map(suffix=>reqHashPrefix + suffix.toLowerCase());
|
||||
const subscribers = await DB.getSubscribersByHashes(hashes);
|
||||
const emailAddresses = await DB.getEmailAddressesByHashes(hashes);
|
||||
const recipients = subscribers.concat(emailAddresses);
|
||||
log.info("notification", { length: recipients.length, breachAlertName: breachAlert.Name });
|
||||
const hashes = req.body.hashSuffixes.map(suffix => reqHashPrefix + suffix.toLowerCase())
|
||||
const subscribers = await DB.getSubscribersByHashes(hashes)
|
||||
const emailAddresses = await DB.getEmailAddressesByHashes(hashes)
|
||||
const recipients = subscribers.concat(emailAddresses)
|
||||
log.info('notification', { length: recipients.length, breachAlertName: breachAlert.Name })
|
||||
|
||||
const utmID = "breach-alert";
|
||||
const notifiedRecipients = [];
|
||||
const utmID = 'breach-alert'
|
||||
const notifiedRecipients = []
|
||||
|
||||
for (const recipient of recipients) {
|
||||
log.info("notify", {recipient});
|
||||
log.info('notify', { recipient })
|
||||
// Get subscriber ID from "subscriber_id" property (if email_addresses record)
|
||||
// or from "id" property (if subscribers record)
|
||||
const subscriberId = recipient.subscriber_id || recipient.id;
|
||||
const { recipientEmail, breachedEmail, signupLanguage, preFxaSubscriber } = getAddressesAndLanguageForEmail(recipient);
|
||||
const campaignId = "go-to-dashboard-link";
|
||||
const ctaHref = EmailUtils.getEmailCtaHref(utmID, campaignId, subscriberId);
|
||||
const subscriberId = recipient.subscriber_id || recipient.id
|
||||
const { recipientEmail, breachedEmail, signupLanguage, preFxaSubscriber } = getAddressesAndLanguageForEmail(recipient)
|
||||
const campaignId = 'go-to-dashboard-link'
|
||||
const ctaHref = EmailUtils.getEmailCtaHref(utmID, campaignId, subscriberId)
|
||||
|
||||
const requestedLanguage = signupLanguage ? acceptedLanguages(signupLanguage) : "";
|
||||
const requestedLanguage = signupLanguage ? acceptedLanguages(signupLanguage) : ''
|
||||
const supportedLocales = negotiateLanguages(
|
||||
requestedLanguage,
|
||||
req.app.locals.AVAILABLE_LANGUAGES,
|
||||
{defaultLocale: "en"}
|
||||
);
|
||||
{ defaultLocale: 'en' }
|
||||
)
|
||||
|
||||
const subject = LocaleUtils.fluentFormat(supportedLocales, "breach-alert-subject");
|
||||
const template = "default_email";
|
||||
const subject = LocaleUtils.fluentFormat(supportedLocales, 'breach-alert-subject')
|
||||
const template = 'default_email'
|
||||
if (!notifiedRecipients.includes(breachedEmail)) {
|
||||
await EmailUtils.sendEmail(
|
||||
recipientEmail, subject, template,
|
||||
|
@ -112,30 +110,27 @@ async function notify (req, res) {
|
|||
breachAlert,
|
||||
SERVER_URL: AppConstants.SERVER_URL,
|
||||
unsubscribeUrl: EmailUtils.getUnsubscribeUrl(recipient, utmID),
|
||||
ctaHref: ctaHref,
|
||||
whichPartial: "email_partials/report",
|
||||
preFxaSubscriber,
|
||||
ctaHref,
|
||||
whichPartial: 'email_partials/report',
|
||||
preFxaSubscriber
|
||||
}
|
||||
);
|
||||
notifiedRecipients.push(breachedEmail);
|
||||
)
|
||||
notifiedRecipients.push(breachedEmail)
|
||||
}
|
||||
}
|
||||
log.info("notified", { length: notifiedRecipients.length });
|
||||
res.status(200);
|
||||
log.info('notified', { length: notifiedRecipients.length })
|
||||
res.status(200)
|
||||
res.json(
|
||||
{info: "Notified subscribers of breach."}
|
||||
);
|
||||
{ info: 'Notified subscribers of breach.' }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
async function breaches (req, res, next) {
|
||||
res.append("Last-Modified", req.app.locals.mostRecentBreachDateTime);
|
||||
res.json(req.app.locals.breaches);
|
||||
res.append('Last-Modified', req.app.locals.mostRecentBreachDateTime)
|
||||
res.json(req.app.locals.breaches)
|
||||
}
|
||||
|
||||
|
||||
|
||||
module.exports = {
|
||||
notify,
|
||||
breaches,
|
||||
};
|
||||
breaches
|
||||
}
|
||||
|
|
|
@ -1,160 +1,158 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const AppConstants = require("../app-constants");
|
||||
const DB = require("../db/DB");
|
||||
const HIBP = require("../hibp");
|
||||
const { scanResult } = require("../scan-results");
|
||||
const AppConstants = require('../app-constants')
|
||||
const DB = require('../db/DB')
|
||||
const HIBP = require('../hibp')
|
||||
const { scanResult } = require('../scan-results')
|
||||
const {
|
||||
generatePageToken,
|
||||
getExperimentFlags,
|
||||
getUTMContents,
|
||||
hasUserSignedUpForWaitlist,
|
||||
} = require("./utils");
|
||||
hasUserSignedUpForWaitlist
|
||||
} = require('./utils')
|
||||
|
||||
const EXPERIMENTS_ENABLED = (AppConstants.EXPERIMENT_ACTIVE === "1");
|
||||
const EXPERIMENTS_ENABLED = (AppConstants.EXPERIMENT_ACTIVE === '1')
|
||||
|
||||
function _getFeaturedBreach(allBreaches, breachQueryValue) {
|
||||
function _getFeaturedBreach (allBreaches, breachQueryValue) {
|
||||
if (!breachQueryValue) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const lowercaseBreachValue = breachQueryValue.toLowerCase();
|
||||
return HIBP.getBreachByName(allBreaches, lowercaseBreachValue);
|
||||
const lowercaseBreachValue = breachQueryValue.toLowerCase()
|
||||
return HIBP.getBreachByName(allBreaches, lowercaseBreachValue)
|
||||
}
|
||||
|
||||
async function home(req, res) {
|
||||
async function home (req, res) {
|
||||
const formTokens = {
|
||||
pageToken: AppConstants.PAGE_TOKEN_TIMER > 0 ? generatePageToken(req) : "",
|
||||
csrfToken: req.csrfToken(),
|
||||
};
|
||||
pageToken: AppConstants.PAGE_TOKEN_TIMER > 0 ? generatePageToken(req) : '',
|
||||
csrfToken: req.csrfToken()
|
||||
}
|
||||
|
||||
let featuredBreach = null;
|
||||
let scanFeaturedBreach = false;
|
||||
let featuredBreach = null
|
||||
let scanFeaturedBreach = false
|
||||
|
||||
if (req.session.user && !req.query.breach) {
|
||||
return res.redirect("/user/dashboard");
|
||||
return res.redirect('/user/dashboard')
|
||||
}
|
||||
|
||||
// Rewrites the /share/{COLOR} links to /
|
||||
if (req.session.redirectHome) {
|
||||
req.session.redirectHome = false;
|
||||
return res.redirect("/");
|
||||
req.session.redirectHome = false
|
||||
return res.redirect('/')
|
||||
}
|
||||
|
||||
// Note - If utmOverrides get set, they are unenrolled from the experiment
|
||||
const utmOverrides = getUTMContents(req);
|
||||
const experimentFlags = getExperimentFlags(req, EXPERIMENTS_ENABLED);
|
||||
const utmOverrides = getUTMContents(req)
|
||||
const experimentFlags = getExperimentFlags(req, EXPERIMENTS_ENABLED)
|
||||
|
||||
if (req.params && req.params.breach) {
|
||||
req.query.breach = req.params.breach;
|
||||
req.query.breach = req.params.breach
|
||||
}
|
||||
|
||||
if (req.query.breach) {
|
||||
|
||||
featuredBreach = _getFeaturedBreach(req.app.locals.breaches, req.query.breach);
|
||||
featuredBreach = _getFeaturedBreach(req.app.locals.breaches, req.query.breach)
|
||||
|
||||
if (!featuredBreach) {
|
||||
return notFound(req, res);
|
||||
return notFound(req, res)
|
||||
}
|
||||
|
||||
const scanRes = await scanResult(req);
|
||||
const scanRes = await scanResult(req)
|
||||
|
||||
if (scanRes.doorhangerScan) {
|
||||
return res.render("scan", Object.assign(scanRes, formTokens));
|
||||
return res.render('scan', Object.assign(scanRes, formTokens))
|
||||
}
|
||||
scanFeaturedBreach = true;
|
||||
scanFeaturedBreach = true
|
||||
|
||||
return res.render("monitor", {
|
||||
title: req.fluentFormat("home-title"),
|
||||
featuredBreach: featuredBreach,
|
||||
return res.render('monitor', {
|
||||
title: req.fluentFormat('home-title'),
|
||||
featuredBreach,
|
||||
scanFeaturedBreach,
|
||||
pageToken: formTokens.pageToken,
|
||||
csrfToken: formTokens.csrfToken,
|
||||
experimentFlags,
|
||||
utmOverrides,
|
||||
});
|
||||
utmOverrides
|
||||
})
|
||||
}
|
||||
|
||||
res.render("monitor", {
|
||||
title: req.fluentFormat("home-title"),
|
||||
featuredBreach: featuredBreach,
|
||||
res.render('monitor', {
|
||||
title: req.fluentFormat('home-title'),
|
||||
featuredBreach,
|
||||
scanFeaturedBreach,
|
||||
pageToken: formTokens.pageToken,
|
||||
csrfToken: formTokens.csrfToken,
|
||||
experimentFlags,
|
||||
utmOverrides,
|
||||
});
|
||||
utmOverrides
|
||||
})
|
||||
}
|
||||
|
||||
function removeMyData(req, res) {
|
||||
const user = req.user;
|
||||
const userHasSignedUpForRemoveData = hasUserSignedUpForWaitlist(user, "remove_data");
|
||||
return res.render("remove-data", {
|
||||
title: req.fluentFormat("home-title"),
|
||||
userHasSignedUpForRemoveData,
|
||||
});
|
||||
function removeMyData (req, res) {
|
||||
const user = req.user
|
||||
const userHasSignedUpForRemoveData = hasUserSignedUpForWaitlist(user, 'remove_data')
|
||||
return res.render('remove-data', {
|
||||
title: req.fluentFormat('home-title'),
|
||||
userHasSignedUpForRemoveData
|
||||
})
|
||||
}
|
||||
|
||||
function getAllBreaches(req, res) {
|
||||
return res.render("top-level-page", {
|
||||
title: "Firefox Monitor",
|
||||
whichPartial: "top-level/all-breaches",
|
||||
});
|
||||
function getAllBreaches (req, res) {
|
||||
return res.render('top-level-page', {
|
||||
title: 'Firefox Monitor',
|
||||
whichPartial: 'top-level/all-breaches'
|
||||
})
|
||||
}
|
||||
|
||||
function getSecurityTips(req, res) {
|
||||
return res.render("top-level-page", {
|
||||
title: req.fluentFormat("home-title"),
|
||||
whichPartial: "top-level/security-tips",
|
||||
});
|
||||
function getSecurityTips (req, res) {
|
||||
return res.render('top-level-page', {
|
||||
title: req.fluentFormat('home-title'),
|
||||
whichPartial: 'top-level/security-tips'
|
||||
})
|
||||
}
|
||||
|
||||
function getAboutPage(req, res) {
|
||||
return res.render("about",{
|
||||
title: req.fluentFormat("about-firefox-monitor"),
|
||||
});
|
||||
function getAboutPage (req, res) {
|
||||
return res.render('about', {
|
||||
title: req.fluentFormat('about-firefox-monitor')
|
||||
})
|
||||
}
|
||||
|
||||
function getBentoStrings(req, res) {
|
||||
function getBentoStrings (req, res) {
|
||||
const localizedBentoStrings = {
|
||||
bentoButtonTitle: req.fluentFormat("bento-button-title"),
|
||||
bentoHeadline: req.fluentFormat("fx-makes-tech"),
|
||||
bentoBottomLink: req.fluentFormat("made-by-mozilla"),
|
||||
fxDesktop: req.fluentFormat("fx-desktop"),
|
||||
fxMobile: req.fluentFormat("fx-mobile"),
|
||||
fxMonitor: req.fluentFormat("fx-monitor"),
|
||||
pocket: req.fluentFormat("pocket"),
|
||||
mozVPN: "Mozilla VPN",
|
||||
mobileCloseBentoButtonTitle: req.fluentFormat("mobile-close-bento-button-title"),
|
||||
};
|
||||
return res.json(localizedBentoStrings);
|
||||
bentoButtonTitle: req.fluentFormat('bento-button-title'),
|
||||
bentoHeadline: req.fluentFormat('fx-makes-tech'),
|
||||
bentoBottomLink: req.fluentFormat('made-by-mozilla'),
|
||||
fxDesktop: req.fluentFormat('fx-desktop'),
|
||||
fxMobile: req.fluentFormat('fx-mobile'),
|
||||
fxMonitor: req.fluentFormat('fx-monitor'),
|
||||
pocket: req.fluentFormat('pocket'),
|
||||
mozVPN: 'Mozilla VPN',
|
||||
mobileCloseBentoButtonTitle: req.fluentFormat('mobile-close-bento-button-title')
|
||||
}
|
||||
return res.json(localizedBentoStrings)
|
||||
}
|
||||
|
||||
function _addToWaitlistsJoined(user, waitlist) {
|
||||
function _addToWaitlistsJoined (user, waitlist) {
|
||||
if (!user.waitlists_joined) {
|
||||
return {[waitlist]: {"notified": false} };
|
||||
return { [waitlist]: { notified: false } }
|
||||
}
|
||||
user.waitlists_joined[waitlist] = {"notified": false };
|
||||
return user.waitlists_joined;
|
||||
user.waitlists_joined[waitlist] = { notified: false }
|
||||
return user.waitlists_joined
|
||||
}
|
||||
|
||||
function addEmailToWaitlist(req, res) {
|
||||
function addEmailToWaitlist (req, res) {
|
||||
if (!req.user) {
|
||||
return res.redirect("/");
|
||||
return res.redirect('/')
|
||||
}
|
||||
const user = req.user;
|
||||
const updatedWaitlistsJoined = _addToWaitlistsJoined(user, "remove_data");
|
||||
DB.setWaitlistsJoined({user, updatedWaitlistsJoined});
|
||||
return res.json("email-added");
|
||||
const user = req.user
|
||||
const updatedWaitlistsJoined = _addToWaitlistsJoined(user, 'remove_data')
|
||||
DB.setWaitlistsJoined({ user, updatedWaitlistsJoined })
|
||||
return res.json('email-added')
|
||||
}
|
||||
|
||||
|
||||
function notFound(req, res) {
|
||||
res.status(404);
|
||||
res.render("subpage", {
|
||||
analyticsID: "error",
|
||||
headline: req.fluentFormat("error-headline"),
|
||||
subhead: req.fluentFormat("home-not-found"),
|
||||
});
|
||||
function notFound (req, res) {
|
||||
res.status(404)
|
||||
res.render('subpage', {
|
||||
analyticsID: 'error',
|
||||
headline: req.fluentFormat('error-headline'),
|
||||
subhead: req.fluentFormat('home-not-found')
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
@ -165,5 +163,5 @@ module.exports = {
|
|||
getSecurityTips,
|
||||
home,
|
||||
notFound,
|
||||
removeMyData,
|
||||
};
|
||||
removeMyData
|
||||
}
|
||||
|
|
|
@ -1,40 +1,40 @@
|
|||
// IP location data includes GeoLite2 data created by MaxMind, available from https://www.maxmind.com.
|
||||
// For testing, you can compare IPs to the corresponding json, eg: https://github.com/maxmind/MaxMind-DB/blob/main/source-data/GeoLite2-City-Test.json
|
||||
|
||||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const AppConstants = require("../app-constants");
|
||||
const { readLocationData } = require("../ip-location-service");
|
||||
const AppConstants = require('../app-constants')
|
||||
const { readLocationData } = require('../ip-location-service')
|
||||
|
||||
async function getIpLocation(req, res) {
|
||||
let clientIp;
|
||||
async function getIpLocation (req, res) {
|
||||
let clientIp
|
||||
|
||||
switch(AppConstants.NODE_ENV){
|
||||
case "dev":
|
||||
clientIp = req.headers["test-ip"] || "216.160.83.56"; // fallback an IP that exists in limited/local GeoLite2 test DB
|
||||
break;
|
||||
case "heroku":
|
||||
case "stage":
|
||||
clientIp = req.headers["test-ip"] || req.ip; // Use "test-ip" header if available, fallback to original IP
|
||||
break;
|
||||
switch (AppConstants.NODE_ENV) {
|
||||
case 'dev':
|
||||
clientIp = req.headers['test-ip'] || '216.160.83.56' // fallback an IP that exists in limited/local GeoLite2 test DB
|
||||
break
|
||||
case 'heroku':
|
||||
case 'stage':
|
||||
clientIp = req.headers['test-ip'] || req.ip // Use "test-ip" header if available, fallback to original IP
|
||||
break
|
||||
default:
|
||||
clientIp = req.ip;
|
||||
clientIp = req.ip
|
||||
}
|
||||
|
||||
if (clientIp === req.session.locationData?.clientIp) {
|
||||
return res.status(200).json(req.session.locationData); // return cached data
|
||||
return res.status(200).json(req.session.locationData) // return cached data
|
||||
}
|
||||
|
||||
const locationData = await readLocationData(clientIp, req.supportedLocales);
|
||||
const locationData = await readLocationData(clientIp, req.supportedLocales)
|
||||
|
||||
if (locationData) {
|
||||
req.session.locationData = Object.assign(locationData, { clientIp }); // cache new location data, adding clientIP key
|
||||
return res.status(200).json(req.session.locationData);
|
||||
req.session.locationData = Object.assign(locationData, { clientIp }) // cache new location data, adding clientIP key
|
||||
return res.status(200).json(req.session.locationData)
|
||||
}
|
||||
|
||||
return res.status(200).json({ clientIp });
|
||||
return res.status(200).json({ clientIp })
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getIpLocation,
|
||||
};
|
||||
getIpLocation
|
||||
}
|
||||
|
|
|
@ -1,107 +1,105 @@
|
|||
"use strict";
|
||||
const { URL } = require("url");
|
||||
'use strict'
|
||||
const { URL } = require('url')
|
||||
|
||||
const crypto = require("crypto");
|
||||
const crypto = require('crypto')
|
||||
|
||||
const AppConstants = require("../app-constants");
|
||||
const DB = require("../db/DB");
|
||||
const EmailUtils = require("../email-utils");
|
||||
const {FXA, FxAOAuthClient} = require("../lib/fxa");
|
||||
const { FluentError } = require("../locale-utils");
|
||||
const HIBP = require("../hibp");
|
||||
const mozlog = require("../log");
|
||||
const sha1 = require("../sha1-utils");
|
||||
const AppConstants = require('../app-constants')
|
||||
const DB = require('../db/DB')
|
||||
const EmailUtils = require('../email-utils')
|
||||
const { FXA, FxAOAuthClient } = require('../lib/fxa')
|
||||
const { FluentError } = require('../locale-utils')
|
||||
const HIBP = require('../hibp')
|
||||
const mozlog = require('../log')
|
||||
const sha1 = require('../sha1-utils')
|
||||
|
||||
const log = mozlog("controllers.oauth");
|
||||
const log = mozlog('controllers.oauth')
|
||||
|
||||
function init(req, res, next, client = FxAOAuthClient) {
|
||||
function init (req, res, next, client = FxAOAuthClient) {
|
||||
// Set a random state string in a cookie so that we can verify
|
||||
// the user when they're redirected back to us after auth.
|
||||
const state = crypto.randomBytes(40).toString("hex");
|
||||
req.session.state = state;
|
||||
const url = new URL(client.code.getUri({state}));
|
||||
const fxaParams = new URL(req.url, AppConstants.SERVER_URL);
|
||||
const state = crypto.randomBytes(40).toString('hex')
|
||||
req.session.state = state
|
||||
const url = new URL(client.code.getUri({ state }))
|
||||
const fxaParams = new URL(req.url, AppConstants.SERVER_URL)
|
||||
|
||||
req.session.utmContents = {};
|
||||
req.session.utmContents = {}
|
||||
|
||||
url.searchParams.append("access_type", "offline");
|
||||
url.searchParams.append("action", "email");
|
||||
url.searchParams.append('access_type', 'offline')
|
||||
url.searchParams.append('action', 'email')
|
||||
|
||||
for (const param of fxaParams.searchParams.keys()) {
|
||||
url.searchParams.append(param, fxaParams.searchParams.get(param));
|
||||
url.searchParams.append(param, fxaParams.searchParams.get(param))
|
||||
}
|
||||
|
||||
res.redirect(url);
|
||||
res.redirect(url)
|
||||
}
|
||||
|
||||
|
||||
async function confirmed(req, res, next, client = FxAOAuthClient) {
|
||||
async function confirmed (req, res, next, client = FxAOAuthClient) {
|
||||
if (!req.session.state) {
|
||||
throw new FluentError("oauth-invalid-session");
|
||||
throw new FluentError('oauth-invalid-session')
|
||||
}
|
||||
|
||||
if (req.session.state !== req.query.state) {
|
||||
throw new FluentError("oauth-invalid-session");
|
||||
throw new FluentError('oauth-invalid-session')
|
||||
}
|
||||
|
||||
const fxaUser = await client.code.getToken(req.originalUrl, { state: req.session.state });
|
||||
const fxaUser = await client.code.getToken(req.originalUrl, { state: req.session.state })
|
||||
// Clear the session.state to clean up and avoid any replays
|
||||
req.session.state = null;
|
||||
log.debug("fxa-confirmed-fxaUser", fxaUser);
|
||||
const fxaProfileData = await FXA.getProfileData(fxaUser.accessToken);
|
||||
log.debug("fxa-confirmed-profile-data", fxaProfileData);
|
||||
const email = JSON.parse(fxaProfileData).email;
|
||||
req.session.state = null
|
||||
log.debug('fxa-confirmed-fxaUser', fxaUser)
|
||||
const fxaProfileData = await FXA.getProfileData(fxaUser.accessToken)
|
||||
log.debug('fxa-confirmed-profile-data', fxaProfileData)
|
||||
const email = JSON.parse(fxaProfileData).email
|
||||
|
||||
const existingUser = await DB.getSubscriberByEmail(email);
|
||||
req.session.user = existingUser;
|
||||
const existingUser = await DB.getSubscriberByEmail(email)
|
||||
req.session.user = existingUser
|
||||
|
||||
const returnURL = new URL("/user/dashboard", AppConstants.SERVER_URL);
|
||||
const returnURL = new URL('/user/dashboard', AppConstants.SERVER_URL)
|
||||
|
||||
// Check if user is signing up or signing in,
|
||||
// then add new users to db and send email.
|
||||
if (!existingUser || existingUser.fxa_refresh_token === null) {
|
||||
// req.session.newUser determines whether or not we show "fxa_new_user_bar" in template
|
||||
req.session.newUser = true;
|
||||
const signupLanguage = req.headers["accept-language"];
|
||||
const verifiedSubscriber = await DB.addSubscriber(email, signupLanguage, fxaUser.accessToken, fxaUser.refreshToken, fxaProfileData);
|
||||
req.session.newUser = true
|
||||
const signupLanguage = req.headers['accept-language']
|
||||
const verifiedSubscriber = await DB.addSubscriber(email, signupLanguage, fxaUser.accessToken, fxaUser.refreshToken, fxaProfileData)
|
||||
|
||||
// duping some of user/verify for now
|
||||
let unsafeBreachesForEmail = [];
|
||||
let unsafeBreachesForEmail = []
|
||||
|
||||
unsafeBreachesForEmail = await HIBP.getBreachesForEmail(
|
||||
sha1(email),
|
||||
req.app.locals.breaches,
|
||||
true
|
||||
);
|
||||
|
||||
const utmID = "report";
|
||||
const reportSubject = EmailUtils.getReportSubject(unsafeBreachesForEmail, req);
|
||||
)
|
||||
|
||||
const utmID = 'report'
|
||||
const reportSubject = EmailUtils.getReportSubject(unsafeBreachesForEmail, req)
|
||||
|
||||
await EmailUtils.sendEmail(
|
||||
email,
|
||||
reportSubject,
|
||||
"default_email",
|
||||
'default_email',
|
||||
{
|
||||
supportedLocales: req.supportedLocales,
|
||||
breachedEmail: email,
|
||||
recipientEmail: email,
|
||||
date: req.fluentFormat(new Date()),
|
||||
unsafeBreachesForEmail: unsafeBreachesForEmail,
|
||||
ctaHref: EmailUtils.getEmailCtaHref(utmID, "go-to-dashboard-link"),
|
||||
unsafeBreachesForEmail,
|
||||
ctaHref: EmailUtils.getEmailCtaHref(utmID, 'go-to-dashboard-link'),
|
||||
unsubscribeUrl: EmailUtils.getUnsubscribeUrl(verifiedSubscriber, utmID),
|
||||
whichPartial: "email_partials/report",
|
||||
whichPartial: 'email_partials/report'
|
||||
}
|
||||
);
|
||||
req.session.user = verifiedSubscriber;
|
||||
return res.redirect(returnURL.pathname + returnURL.search);
|
||||
)
|
||||
req.session.user = verifiedSubscriber
|
||||
return res.redirect(returnURL.pathname + returnURL.search)
|
||||
}
|
||||
// Update existing user's FxA data
|
||||
await DB._updateFxAData(existingUser, fxaUser.accessToken, fxaUser.refreshToken, fxaProfileData);
|
||||
res.redirect(returnURL.pathname + returnURL.search);
|
||||
await DB._updateFxAData(existingUser, fxaUser.accessToken, fxaUser.refreshToken, fxaProfileData)
|
||||
res.redirect(returnURL.pathname + returnURL.search)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init,
|
||||
confirmed,
|
||||
};
|
||||
confirmed
|
||||
}
|
||||
|
|
|
@ -1,49 +1,48 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const crypto = require("crypto");
|
||||
const crypto = require('crypto')
|
||||
|
||||
const AppConstants = require("../app-constants");
|
||||
const { FluentError } = require("../locale-utils");
|
||||
const AppConstants = require('../app-constants')
|
||||
const { FluentError } = require('../locale-utils')
|
||||
// const { generatePageToken } = require("./utils");
|
||||
const mozlog = require("../log");
|
||||
const { scanResult } = require("../scan-results");
|
||||
const sha1 = require("../sha1-utils");
|
||||
const mozlog = require('../log')
|
||||
const { scanResult } = require('../scan-results')
|
||||
const sha1 = require('../sha1-utils')
|
||||
|
||||
const log = mozlog("controllers.scan");
|
||||
const log = mozlog('controllers.scan')
|
||||
|
||||
function _decryptPageToken(encryptedPageToken) {
|
||||
let decipher;
|
||||
function _decryptPageToken (encryptedPageToken) {
|
||||
let decipher
|
||||
|
||||
if (encryptedPageToken.slice(24, 25) === ".") {
|
||||
if (encryptedPageToken.slice(24, 25) === '.') {
|
||||
// iv becomes 24 characters long when represented as base64: ceil(16 / 3) * 4 = 24
|
||||
const iv = Buffer.from(encryptedPageToken.slice(0, 24), "base64");
|
||||
const key = crypto.pbkdf2Sync(AppConstants.COOKIE_SECRET, iv.toString(), 10000, 32, "sha512");
|
||||
const iv = Buffer.from(encryptedPageToken.slice(0, 24), 'base64')
|
||||
const key = crypto.pbkdf2Sync(AppConstants.COOKIE_SECRET, iv.toString(), 10000, 32, 'sha512')
|
||||
|
||||
decipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
|
||||
encryptedPageToken = encryptedPageToken.slice(25);
|
||||
decipher = crypto.createDecipheriv('aes-256-cbc', key, iv)
|
||||
encryptedPageToken = encryptedPageToken.slice(25)
|
||||
} else {
|
||||
decipher = crypto.createDecipher("aes-256-cbc", AppConstants.COOKIE_SECRET);
|
||||
decipher = crypto.createDecipher('aes-256-cbc', AppConstants.COOKIE_SECRET)
|
||||
}
|
||||
|
||||
const decryptedPageToken = Buffer.concat([
|
||||
decipher.update(Buffer.from(encryptedPageToken, "base64")),
|
||||
decipher.final(),
|
||||
]).toString("utf8");
|
||||
decipher.update(Buffer.from(encryptedPageToken, 'base64')),
|
||||
decipher.final()
|
||||
]).toString('utf8')
|
||||
|
||||
return JSON.parse(decryptedPageToken);
|
||||
return JSON.parse(decryptedPageToken)
|
||||
}
|
||||
|
||||
|
||||
function _validatePageToken(pageToken, req) {
|
||||
const requestIP = req.headers["x-real-ip"] || req.ip;
|
||||
const pageTokenIP = pageToken.ip;
|
||||
function _validatePageToken (pageToken, req) {
|
||||
const requestIP = req.headers['x-real-ip'] || req.ip
|
||||
const pageTokenIP = pageToken.ip
|
||||
if (pageToken.ip !== requestIP) {
|
||||
log.error("_validatePageToken", {msg: "IP mis-match", pageTokenIP, requestIP});
|
||||
return false;
|
||||
log.error('_validatePageToken', { msg: 'IP mis-match', pageTokenIP, requestIP })
|
||||
return false
|
||||
}
|
||||
if (Date.now() - new Date(pageToken.date) >= AppConstants.PAGE_TOKEN_TIMER * 1000) {
|
||||
log.error("_validatePageToken", {msg: "expired pageToken"});
|
||||
return false;
|
||||
log.error('_validatePageToken', { msg: 'expired pageToken' })
|
||||
return false
|
||||
}
|
||||
/* TODO: block on scans-per-ip instead of scans-per-timespan
|
||||
if (req.session.scans.length > 5) {
|
||||
|
@ -55,52 +54,49 @@ function _validatePageToken(pageToken, req) {
|
|||
req.session.scans.push(emailHash);
|
||||
}
|
||||
*/
|
||||
return pageToken;
|
||||
return pageToken
|
||||
}
|
||||
|
||||
|
||||
async function post (req, res) {
|
||||
const emailHash = req.body.emailHash;
|
||||
const encryptedPageToken = req.body.pageToken;
|
||||
let validPageToken = false;
|
||||
const emailHash = req.body.emailHash
|
||||
const encryptedPageToken = req.body.pageToken
|
||||
let validPageToken = false
|
||||
|
||||
// for #688: use a page token to check that scans come from real pages
|
||||
if (AppConstants.PAGE_TOKEN_TIMER > 0) {
|
||||
if (!encryptedPageToken) {
|
||||
throw new FluentError("error-scan-page-token");
|
||||
throw new FluentError('error-scan-page-token')
|
||||
}
|
||||
const decryptedPageToken = _decryptPageToken(encryptedPageToken);
|
||||
validPageToken = _validatePageToken(decryptedPageToken, req);
|
||||
const decryptedPageToken = _decryptPageToken(encryptedPageToken)
|
||||
validPageToken = _validatePageToken(decryptedPageToken, req)
|
||||
|
||||
if (!validPageToken) {
|
||||
throw new FluentError("error-scan-page-token");
|
||||
throw new FluentError('error-scan-page-token')
|
||||
}
|
||||
}
|
||||
|
||||
if (!emailHash || emailHash === sha1("")) {
|
||||
return res.redirect("/");
|
||||
if (!emailHash || emailHash === sha1('')) {
|
||||
return res.redirect('/')
|
||||
}
|
||||
|
||||
const scanRes = await scanResult(req);
|
||||
const scanRes = await scanResult(req)
|
||||
|
||||
const formTokens = {
|
||||
pageToken: encryptedPageToken,
|
||||
csrfToken: req.csrfToken(),
|
||||
};
|
||||
csrfToken: req.csrfToken()
|
||||
}
|
||||
|
||||
if (req.session.user && scanRes.selfScan && !req.body.featuredBreach) {
|
||||
return res.redirect("/user/dashboard");
|
||||
return res.redirect('/user/dashboard')
|
||||
}
|
||||
res.render("scan", Object.assign(scanRes, formTokens));
|
||||
res.render('scan', Object.assign(scanRes, formTokens))
|
||||
}
|
||||
|
||||
|
||||
function get (req, res) {
|
||||
res.redirect("/");
|
||||
res.redirect('/')
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
post,
|
||||
get,
|
||||
};
|
||||
get
|
||||
}
|
||||
|
|
|
@ -1,97 +1,88 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const MessageValidator = require("sns-validator");
|
||||
const MessageValidator = require('sns-validator')
|
||||
|
||||
const DB = require("../db/DB");
|
||||
const DB = require('../db/DB')
|
||||
|
||||
const mozlog = require("../log");
|
||||
const mozlog = require('../log')
|
||||
|
||||
const validator = new MessageValidator()
|
||||
const log = mozlog('controllers.ses')
|
||||
|
||||
const validator = new MessageValidator();
|
||||
const log = mozlog("controllers.ses");
|
||||
|
||||
|
||||
async function notification(req, res) {
|
||||
const message = JSON.parse(req.body);
|
||||
async function notification (req, res) {
|
||||
const message = JSON.parse(req.body)
|
||||
return new Promise((resolve, reject) => {
|
||||
validator.validate(message, async (err, message) => {
|
||||
if (err) {
|
||||
log.error("notification", { err: err });
|
||||
const body = "Access denied. " + err.message;
|
||||
res.status(401).send(body);
|
||||
return reject(body);
|
||||
log.error('notification', { err })
|
||||
const body = 'Access denied. ' + err.message
|
||||
res.status(401).send(body)
|
||||
return reject(body)
|
||||
}
|
||||
|
||||
await handleNotification(message);
|
||||
await handleNotification(message)
|
||||
|
||||
res.status(200).json(
|
||||
{status: "OK"}
|
||||
);
|
||||
return resolve("OK");
|
||||
});
|
||||
});
|
||||
{ status: 'OK' }
|
||||
)
|
||||
return resolve('OK')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
async function handleNotification(notification) {
|
||||
log.info("received-SNS", { id: notification.MessageId });
|
||||
const message = JSON.parse(notification.Message);
|
||||
if (message.hasOwnProperty("eventType")) {
|
||||
await handleSESMessage(message);
|
||||
async function handleNotification (notification) {
|
||||
log.info('received-SNS', { id: notification.MessageId })
|
||||
const message = JSON.parse(notification.Message)
|
||||
if (message.hasOwnProperty('eventType')) {
|
||||
await handleSESMessage(message)
|
||||
}
|
||||
if (message.hasOwnProperty("event")) {
|
||||
await handleFxAMessage(message);
|
||||
if (message.hasOwnProperty('event')) {
|
||||
await handleFxAMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function handleFxAMessage(message) {
|
||||
async function handleFxAMessage (message) {
|
||||
switch (message.event) {
|
||||
case "delete":
|
||||
await handleDeleteMessage(message);
|
||||
break;
|
||||
case 'delete':
|
||||
await handleDeleteMessage(message)
|
||||
break
|
||||
default:
|
||||
log.info("unhandled-event", { event: message.event });
|
||||
log.info('unhandled-event', { event: message.event })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function handleDeleteMessage(message) {
|
||||
await DB.deleteSubscriberByFxAUID(message.uid);
|
||||
async function handleDeleteMessage (message) {
|
||||
await DB.deleteSubscriberByFxAUID(message.uid)
|
||||
}
|
||||
|
||||
|
||||
async function handleSESMessage(message) {
|
||||
async function handleSESMessage (message) {
|
||||
switch (message.eventType) {
|
||||
case "Bounce":
|
||||
await handleBounceMessage(message);
|
||||
break;
|
||||
case "Complaint":
|
||||
await handleComplaintMessage(message);
|
||||
break;
|
||||
case 'Bounce':
|
||||
await handleBounceMessage(message)
|
||||
break
|
||||
case 'Complaint':
|
||||
await handleComplaintMessage(message)
|
||||
break
|
||||
default:
|
||||
log.info("unhandled-eventType", { type: message.eventType });
|
||||
log.info('unhandled-eventType', { type: message.eventType })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function handleBounceMessage(message) {
|
||||
const bounce = message.bounce;
|
||||
if (bounce.bounceType === "Permanent") {
|
||||
return await removeSubscribersFromDB(bounce.bouncedRecipients);
|
||||
async function handleBounceMessage (message) {
|
||||
const bounce = message.bounce
|
||||
if (bounce.bounceType === 'Permanent') {
|
||||
return await removeSubscribersFromDB(bounce.bouncedRecipients)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function handleComplaintMessage(message) {
|
||||
const complaint = message.complaint;
|
||||
return await removeSubscribersFromDB(complaint.complainedRecipients);
|
||||
async function handleComplaintMessage (message) {
|
||||
const complaint = message.complaint
|
||||
return await removeSubscribersFromDB(complaint.complainedRecipients)
|
||||
}
|
||||
|
||||
|
||||
async function removeSubscribersFromDB(recipients) {
|
||||
async function removeSubscribersFromDB (recipients) {
|
||||
for (const recipient of recipients) {
|
||||
await DB.removeEmail(recipient.emailAddress);
|
||||
await DB.removeEmail(recipient.emailAddress)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,5 +91,5 @@ module.exports = {
|
|||
handleNotification,
|
||||
handleBounceMessage,
|
||||
handleComplaintMessage,
|
||||
removeSubscribersFromDB,
|
||||
};
|
||||
removeSubscribersFromDB
|
||||
}
|
||||
|
|
|
@ -1,534 +1,519 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const AppConstants = require("../app-constants");
|
||||
const isemail = require("isemail");
|
||||
const AppConstants = require('../app-constants')
|
||||
const isemail = require('isemail')
|
||||
|
||||
const DB = require("../db/DB");
|
||||
const EmailUtils = require("../email-utils");
|
||||
const { FluentError } = require("../locale-utils");
|
||||
const { FXA } = require("../lib/fxa");
|
||||
const HIBP = require("../hibp");
|
||||
const { resultsSummary } = require("../scan-results");
|
||||
const sha1 = require("../sha1-utils");
|
||||
const DB = require('../db/DB')
|
||||
const EmailUtils = require('../email-utils')
|
||||
const { FluentError } = require('../locale-utils')
|
||||
const { FXA } = require('../lib/fxa')
|
||||
const HIBP = require('../hibp')
|
||||
const { resultsSummary } = require('../scan-results')
|
||||
const sha1 = require('../sha1-utils')
|
||||
|
||||
const EXPERIMENTS_ENABLED = (AppConstants.EXPERIMENT_ACTIVE === "1");
|
||||
const EXPERIMENTS_ENABLED = (AppConstants.EXPERIMENT_ACTIVE === '1')
|
||||
const {
|
||||
getExperimentFlags,
|
||||
getUTMContents,
|
||||
hasUserSignedUpForWaitlist,
|
||||
} = require("./utils");
|
||||
hasUserSignedUpForWaitlist
|
||||
} = require('./utils')
|
||||
|
||||
const FXA_MONITOR_SCOPE = "https://identity.mozilla.com/apps/monitor";
|
||||
const FXA_MONITOR_SCOPE = 'https://identity.mozilla.com/apps/monitor'
|
||||
|
||||
async function removeEmail(req, res) {
|
||||
const emailId = req.body.emailId;
|
||||
const sessionUser = req.user;
|
||||
const existingEmail = await DB.getEmailById(emailId);
|
||||
async function removeEmail (req, res) {
|
||||
const emailId = req.body.emailId
|
||||
const sessionUser = req.user
|
||||
const existingEmail = await DB.getEmailById(emailId)
|
||||
if (existingEmail.subscriber_id !== sessionUser.id) {
|
||||
throw new FluentError("error-not-subscribed");
|
||||
throw new FluentError('error-not-subscribed')
|
||||
}
|
||||
|
||||
DB.removeOneSecondaryEmail(emailId);
|
||||
res.redirect("/user/preferences");
|
||||
DB.removeOneSecondaryEmail(emailId)
|
||||
res.redirect('/user/preferences')
|
||||
}
|
||||
|
||||
async function resendEmail(req, res) {
|
||||
const emailId = req.body.emailId;
|
||||
const sessionUser = req.user;
|
||||
const existingEmail = await DB.getEmailById(emailId);
|
||||
async function resendEmail (req, res) {
|
||||
const emailId = req.body.emailId
|
||||
const sessionUser = req.user
|
||||
const existingEmail = await DB.getEmailById(emailId)
|
||||
|
||||
if (!existingEmail || !existingEmail.subscriber_id) {
|
||||
throw new FluentError("user-verify-token-error");
|
||||
throw new FluentError('user-verify-token-error')
|
||||
}
|
||||
|
||||
if (existingEmail.subscriber_id !== sessionUser.id) {
|
||||
// TODO: more specific error message?
|
||||
throw new FluentError("user-verify-token-error");
|
||||
throw new FluentError('user-verify-token-error')
|
||||
}
|
||||
|
||||
const unverifiedEmailAddressRecord = await DB.resetUnverifiedEmailAddress(emailId);
|
||||
const unverifiedEmailAddressRecord = await DB.resetUnverifiedEmailAddress(emailId)
|
||||
|
||||
const email = unverifiedEmailAddressRecord.email;
|
||||
const email = unverifiedEmailAddressRecord.email
|
||||
await EmailUtils.sendEmail(
|
||||
email,
|
||||
req.fluentFormat("email-subject-verify"),
|
||||
"default_email",
|
||||
{ recipientEmail: email,
|
||||
req.fluentFormat('email-subject-verify'),
|
||||
'default_email',
|
||||
{
|
||||
recipientEmail: email,
|
||||
supportedLocales: req.supportedLocales,
|
||||
ctaHref: EmailUtils.getVerificationUrl(unverifiedEmailAddressRecord),
|
||||
unsubscribeUrl: EmailUtils.getUnsubscribeUrl(unverifiedEmailAddressRecord, "account-verification-email"),
|
||||
whichPartial: "email_partials/email_verify",
|
||||
unsubscribeUrl: EmailUtils.getUnsubscribeUrl(unverifiedEmailAddressRecord, 'account-verification-email'),
|
||||
whichPartial: 'email_partials/email_verify'
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
// TODO: what should we return to the client?
|
||||
return res.json("Resent the email");
|
||||
return res.json('Resent the email')
|
||||
}
|
||||
|
||||
async function updateCommunicationOptions(req, res) {
|
||||
const sessionUser = req.user;
|
||||
async function updateCommunicationOptions (req, res) {
|
||||
const sessionUser = req.user
|
||||
// 0 = Send breach alerts to the email address found in brew breach.
|
||||
// 1 = Send all breach alerts to user's primary email address.
|
||||
const allEmailsToPrimary = (Number(req.body.communicationOption) === 1) ? true : false;
|
||||
const updatedSubscriber = await DB.setAllEmailsToPrimary(sessionUser, allEmailsToPrimary);
|
||||
req.session.user = updatedSubscriber;
|
||||
const allEmailsToPrimary = (Number(req.body.communicationOption) === 1)
|
||||
const updatedSubscriber = await DB.setAllEmailsToPrimary(sessionUser, allEmailsToPrimary)
|
||||
req.session.user = updatedSubscriber
|
||||
|
||||
return res.json("Comm options updated");
|
||||
return res.json('Comm options updated')
|
||||
}
|
||||
|
||||
|
||||
async function resolveBreach(req, res) {
|
||||
const sessionUser = req.user;
|
||||
async function resolveBreach (req, res) {
|
||||
const sessionUser = req.user
|
||||
// TODO: verify that req.body.emailAddressId belongs to sessionUser
|
||||
const updatedSubscriber = await DB.setResolvedBreach({
|
||||
subscriber: sessionUser,
|
||||
emailAddresses: req.body.emailAddressId,
|
||||
recencyIndex: req.body.recencyIndex,
|
||||
});
|
||||
req.session.user = updatedSubscriber;
|
||||
return res.json("Breach marked as resolved.");
|
||||
recencyIndex: req.body.recencyIndex
|
||||
})
|
||||
req.session.user = updatedSubscriber
|
||||
return res.json('Breach marked as resolved.')
|
||||
}
|
||||
|
||||
|
||||
function _checkForDuplicateEmail(sessionUser, email) {
|
||||
email = email.toLowerCase();
|
||||
function _checkForDuplicateEmail (sessionUser, email) {
|
||||
email = email.toLowerCase()
|
||||
if (email === sessionUser.primary_email.toLowerCase()) {
|
||||
throw new FluentError("user-add-duplicate-email");
|
||||
throw new FluentError('user-add-duplicate-email')
|
||||
}
|
||||
for (const secondaryEmail of sessionUser.email_addresses) {
|
||||
if (email === secondaryEmail.email.toLowerCase()) {
|
||||
throw new FluentError("user-add-duplicate-email");
|
||||
throw new FluentError('user-add-duplicate-email')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function add(req, res) {
|
||||
const sessionUser = await req.user;
|
||||
const email = req.body.email;
|
||||
async function add (req, res) {
|
||||
const sessionUser = await req.user
|
||||
const email = req.body.email
|
||||
if (!email || !isemail.validate(email)) {
|
||||
throw new FluentError("user-add-invalid-email");
|
||||
throw new FluentError('user-add-invalid-email')
|
||||
}
|
||||
|
||||
if (sessionUser.email_addresses.length >= AppConstants.MAX_NUM_ADDRESSES) {
|
||||
throw new FluentError("user-add-too-many-emails");
|
||||
throw new FluentError('user-add-too-many-emails')
|
||||
}
|
||||
_checkForDuplicateEmail(sessionUser, email);
|
||||
_checkForDuplicateEmail(sessionUser, email)
|
||||
|
||||
const unverifiedSubscriber = await DB.addSubscriberUnverifiedEmailHash(
|
||||
req.session.user, email
|
||||
);
|
||||
|
||||
)
|
||||
|
||||
await EmailUtils.sendEmail(
|
||||
email,
|
||||
req.fluentFormat("email-subject-verify"),
|
||||
"default_email",
|
||||
{ breachedEmail: email,
|
||||
req.fluentFormat('email-subject-verify'),
|
||||
'default_email',
|
||||
{
|
||||
breachedEmail: email,
|
||||
recipientEmail: email,
|
||||
supportedLocales: req.supportedLocales,
|
||||
ctaHref: EmailUtils.getVerificationUrl(unverifiedSubscriber),
|
||||
unsubscribeUrl: EmailUtils.getUnsubscribeUrl(unverifiedSubscriber, "account-verification-email"),
|
||||
whichPartial: "email_partials/email_verify",
|
||||
unsubscribeUrl: EmailUtils.getUnsubscribeUrl(unverifiedSubscriber, 'account-verification-email'),
|
||||
whichPartial: 'email_partials/email_verify'
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
// send users coming from the dashboard back to the dashboard
|
||||
// and set req.session.lastAddedEmail to show a confirmation message.
|
||||
if (req.headers.referer.endsWith("/dashboard")) {
|
||||
req.session.lastAddedEmail = email;
|
||||
return res.redirect("/user/dashboard");
|
||||
if (req.headers.referer.endsWith('/dashboard')) {
|
||||
req.session.lastAddedEmail = email
|
||||
return res.redirect('/user/dashboard')
|
||||
}
|
||||
|
||||
res.redirect("/user/preferences");
|
||||
res.redirect('/user/preferences')
|
||||
}
|
||||
|
||||
|
||||
function getResolvedBreachesForEmail(user, email) {
|
||||
function getResolvedBreachesForEmail (user, email) {
|
||||
if (user.breaches_resolved === null) {
|
||||
return [];
|
||||
return []
|
||||
}
|
||||
return user.breaches_resolved.hasOwnProperty(email) ? user.breaches_resolved[email] : [];
|
||||
return user.breaches_resolved.hasOwnProperty(email) ? user.breaches_resolved[email] : []
|
||||
}
|
||||
|
||||
|
||||
function addResolvedOrNot(foundBreaches, resolvedBreaches) {
|
||||
const annotatedBreaches = [];
|
||||
if (AppConstants.BREACH_RESOLUTION_ENABLED !== "1") {
|
||||
return foundBreaches;
|
||||
function addResolvedOrNot (foundBreaches, resolvedBreaches) {
|
||||
const annotatedBreaches = []
|
||||
if (AppConstants.BREACH_RESOLUTION_ENABLED !== '1') {
|
||||
return foundBreaches
|
||||
}
|
||||
for (const breach of foundBreaches) {
|
||||
const IsResolved = resolvedBreaches.includes(breach.recencyIndex) ? true : false;
|
||||
annotatedBreaches.push(Object.assign({IsResolved}, breach));
|
||||
const IsResolved = !!resolvedBreaches.includes(breach.recencyIndex)
|
||||
annotatedBreaches.push(Object.assign({ IsResolved }, breach))
|
||||
}
|
||||
return annotatedBreaches;
|
||||
return annotatedBreaches
|
||||
}
|
||||
|
||||
|
||||
function addRecencyIndex(foundBreaches) {
|
||||
const annotatedBreaches = [];
|
||||
function addRecencyIndex (foundBreaches) {
|
||||
const annotatedBreaches = []
|
||||
// slice() the array to make a copy so before reversing so we don't
|
||||
// reverse foundBreaches in-place
|
||||
const oldestToNewestFoundBreaches = foundBreaches.slice().reverse();
|
||||
oldestToNewestFoundBreaches.forEach( (annotatingBreach, index) => {
|
||||
const foundBreach = foundBreaches.find( foundBreach => foundBreach.Name === annotatingBreach.Name);
|
||||
annotatedBreaches.push(Object.assign({recencyIndex: index}, foundBreach));
|
||||
});
|
||||
return annotatedBreaches.reverse();
|
||||
const oldestToNewestFoundBreaches = foundBreaches.slice().reverse()
|
||||
oldestToNewestFoundBreaches.forEach((annotatingBreach, index) => {
|
||||
const foundBreach = foundBreaches.find(foundBreach => foundBreach.Name === annotatingBreach.Name)
|
||||
annotatedBreaches.push(Object.assign({ recencyIndex: index }, foundBreach))
|
||||
})
|
||||
return annotatedBreaches.reverse()
|
||||
}
|
||||
|
||||
|
||||
async function bundleVerifiedEmails(options) {
|
||||
const { user, email, recordId, recordVerified, allBreaches} = options;
|
||||
const lowerCaseEmailSha = sha1(email.toLowerCase());
|
||||
const foundBreaches = await HIBP.getBreachesForEmail(lowerCaseEmailSha, allBreaches, true, false);
|
||||
const foundBreachesWithRecency = addRecencyIndex(foundBreaches);
|
||||
const resolvedBreaches = getResolvedBreachesForEmail(user, email);
|
||||
const foundBreachesWithResolutions = addResolvedOrNot(foundBreachesWithRecency, resolvedBreaches);
|
||||
const filteredAnnotatedFoundBreaches = HIBP.filterBreaches(foundBreachesWithResolutions);
|
||||
async function bundleVerifiedEmails (options) {
|
||||
const { user, email, recordId, recordVerified, allBreaches } = options
|
||||
const lowerCaseEmailSha = sha1(email.toLowerCase())
|
||||
const foundBreaches = await HIBP.getBreachesForEmail(lowerCaseEmailSha, allBreaches, true, false)
|
||||
const foundBreachesWithRecency = addRecencyIndex(foundBreaches)
|
||||
const resolvedBreaches = getResolvedBreachesForEmail(user, email)
|
||||
const foundBreachesWithResolutions = addResolvedOrNot(foundBreachesWithRecency, resolvedBreaches)
|
||||
const filteredAnnotatedFoundBreaches = HIBP.filterBreaches(foundBreachesWithResolutions)
|
||||
|
||||
const emailEntry = {
|
||||
"email": email,
|
||||
"breaches": filteredAnnotatedFoundBreaches,
|
||||
"primary": email === user.primary_email,
|
||||
"id": recordId,
|
||||
"verified": recordVerified,
|
||||
};
|
||||
email,
|
||||
breaches: filteredAnnotatedFoundBreaches,
|
||||
primary: email === user.primary_email,
|
||||
id: recordId,
|
||||
verified: recordVerified
|
||||
}
|
||||
|
||||
return emailEntry;
|
||||
return emailEntry
|
||||
}
|
||||
|
||||
async function getAllEmailsAndBreaches(user, allBreaches) {
|
||||
const monitoredEmails = await DB.getUserEmails(user.id);
|
||||
let verifiedEmails = [];
|
||||
const unverifiedEmails = [];
|
||||
verifiedEmails.push(await bundleVerifiedEmails({user, email: user.primary_email, recordId: user.id, recordVerified: user.primary_verified, allBreaches}));
|
||||
async function getAllEmailsAndBreaches (user, allBreaches) {
|
||||
const monitoredEmails = await DB.getUserEmails(user.id)
|
||||
let verifiedEmails = []
|
||||
const unverifiedEmails = []
|
||||
verifiedEmails.push(await bundleVerifiedEmails({ user, email: user.primary_email, recordId: user.id, recordVerified: user.primary_verified, allBreaches }))
|
||||
for (const email of monitoredEmails) {
|
||||
if (email.verified) {
|
||||
verifiedEmails.push(await bundleVerifiedEmails({user, email: email.email, recordId: email.id, recordVerified: email.verified, allBreaches}));
|
||||
verifiedEmails.push(await bundleVerifiedEmails({ user, email: email.email, recordId: email.id, recordVerified: email.verified, allBreaches }))
|
||||
} else {
|
||||
unverifiedEmails.push(email);
|
||||
unverifiedEmails.push(email)
|
||||
}
|
||||
}
|
||||
verifiedEmails = getNewBreachesForEmailEntriesSinceDate(verifiedEmails, user.breaches_last_shown);
|
||||
return { verifiedEmails, unverifiedEmails };
|
||||
verifiedEmails = getNewBreachesForEmailEntriesSinceDate(verifiedEmails, user.breaches_last_shown)
|
||||
return { verifiedEmails, unverifiedEmails }
|
||||
}
|
||||
|
||||
|
||||
function getNewBreachesForEmailEntriesSinceDate(emailEntries, date) {
|
||||
function getNewBreachesForEmailEntriesSinceDate (emailEntries, date) {
|
||||
for (const emailEntry of emailEntries) {
|
||||
const newBreachesForEmail = emailEntry.breaches.filter(breach => breach.AddedDate >= date);
|
||||
const newBreachesForEmail = emailEntry.breaches.filter(breach => breach.AddedDate >= date)
|
||||
|
||||
for (const newBreachForEmail of newBreachesForEmail) {
|
||||
newBreachForEmail["NewBreach"] = true; // add "NewBreach" property to the new breach.
|
||||
emailEntry["hasNewBreaches"] = newBreachesForEmail.length; // add the number of new breaches to the email
|
||||
newBreachForEmail.NewBreach = true // add "NewBreach" property to the new breach.
|
||||
emailEntry.hasNewBreaches = newBreachesForEmail.length // add the number of new breaches to the email
|
||||
}
|
||||
}
|
||||
return emailEntries;
|
||||
return emailEntries
|
||||
}
|
||||
|
||||
async function getDashboard (req, res) {
|
||||
const user = req.user
|
||||
const allBreaches = req.app.locals.breaches
|
||||
const { verifiedEmails, unverifiedEmails } = await getAllEmailsAndBreaches(user, allBreaches)
|
||||
const utmOverrides = getUTMContents(req)
|
||||
const supportedLocalesIncludesEnglish = req.supportedLocales.includes('en')
|
||||
const userHasSignedUpForRemoveData = hasUserSignedUpForWaitlist(user, 'remove_data')
|
||||
|
||||
async function getDashboard(req, res) {
|
||||
const user = req.user;
|
||||
const allBreaches = req.app.locals.breaches;
|
||||
const { verifiedEmails, unverifiedEmails } = await getAllEmailsAndBreaches(user, allBreaches);
|
||||
const utmOverrides = getUTMContents(req);
|
||||
const supportedLocalesIncludesEnglish = req.supportedLocales.includes("en");
|
||||
const userHasSignedUpForRemoveData = hasUserSignedUpForWaitlist(user, "remove_data");
|
||||
const experimentFlags = getExperimentFlags(req, EXPERIMENTS_ENABLED)
|
||||
|
||||
const experimentFlags = getExperimentFlags(req, EXPERIMENTS_ENABLED);
|
||||
let lastAddedEmail = null
|
||||
|
||||
let lastAddedEmail = null;
|
||||
|
||||
req.session.user = await DB.setBreachesLastShownNow(user);
|
||||
req.session.user = await DB.setBreachesLastShownNow(user)
|
||||
if (req.session.lastAddedEmail) {
|
||||
lastAddedEmail = req.session.lastAddedEmail;
|
||||
req.session["lastAddedEmail"] = null;
|
||||
lastAddedEmail = req.session.lastAddedEmail
|
||||
req.session.lastAddedEmail = null
|
||||
}
|
||||
|
||||
res.render("dashboards", {
|
||||
title: req.fluentFormat("Firefox Monitor"),
|
||||
res.render('dashboards', {
|
||||
title: req.fluentFormat('Firefox Monitor'),
|
||||
csrfToken: req.csrfToken(),
|
||||
lastAddedEmail,
|
||||
verifiedEmails,
|
||||
unverifiedEmails,
|
||||
userHasSignedUpForRemoveData,
|
||||
supportedLocalesIncludesEnglish,
|
||||
whichPartial: "dashboards/breaches-dash",
|
||||
whichPartial: 'dashboards/breaches-dash',
|
||||
experimentFlags,
|
||||
utmOverrides,
|
||||
});
|
||||
utmOverrides
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
async function _verify(req) {
|
||||
const verifiedEmailHash = await DB.verifyEmailHash(req.query.token);
|
||||
let unsafeBreachesForEmail = [];
|
||||
async function _verify (req) {
|
||||
const verifiedEmailHash = await DB.verifyEmailHash(req.query.token)
|
||||
let unsafeBreachesForEmail = []
|
||||
unsafeBreachesForEmail = await HIBP.getBreachesForEmail(
|
||||
sha1(verifiedEmailHash.email.toLowerCase()),
|
||||
req.app.locals.breaches,
|
||||
true
|
||||
);
|
||||
|
||||
const utmID = "report";
|
||||
const emailSubject = EmailUtils.getReportSubject(unsafeBreachesForEmail, req);
|
||||
)
|
||||
|
||||
const utmID = 'report'
|
||||
const emailSubject = EmailUtils.getReportSubject(unsafeBreachesForEmail, req)
|
||||
|
||||
await EmailUtils.sendEmail(
|
||||
verifiedEmailHash.email,
|
||||
emailSubject,
|
||||
"default_email",
|
||||
'default_email',
|
||||
{
|
||||
breachedEmail: verifiedEmailHash.email,
|
||||
recipientEmail: verifiedEmailHash.email,
|
||||
supportedLocales: req.supportedLocales,
|
||||
unsafeBreachesForEmail: unsafeBreachesForEmail,
|
||||
ctaHref: EmailUtils.getEmailCtaHref(utmID, "go-to-dashboard-link"),
|
||||
unsafeBreachesForEmail,
|
||||
ctaHref: EmailUtils.getEmailCtaHref(utmID, 'go-to-dashboard-link'),
|
||||
unsubscribeUrl: EmailUtils.getUnsubscribeUrl(verifiedEmailHash, utmID),
|
||||
whichPartial: "email_partials/report",
|
||||
whichPartial: 'email_partials/report'
|
||||
}
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
async function verify(req, res) {
|
||||
async function verify (req, res) {
|
||||
if (!req.query.token) {
|
||||
throw new FluentError("user-verify-token-error");
|
||||
throw new FluentError('user-verify-token-error')
|
||||
}
|
||||
const existingEmail = await DB.getEmailByToken(req.query.token);
|
||||
const existingEmail = await DB.getEmailByToken(req.query.token)
|
||||
|
||||
if (!existingEmail) {
|
||||
throw new FluentError("error-not-subscribed");
|
||||
throw new FluentError('error-not-subscribed')
|
||||
}
|
||||
|
||||
const sessionUser = await req.user;
|
||||
const sessionUser = await req.user
|
||||
if (sessionUser && existingEmail.subscriber_id !== sessionUser.id) {
|
||||
// TODO: more specific error message?
|
||||
// e.g., "This email verification token is not valid for this account"
|
||||
throw new FluentError("user-verify-token-error");
|
||||
throw new FluentError('user-verify-token-error')
|
||||
}
|
||||
|
||||
if (!existingEmail.verified) {
|
||||
await _verify(req);
|
||||
await _verify(req)
|
||||
}
|
||||
|
||||
if (sessionUser) {
|
||||
res.redirect("/user/dashboard");
|
||||
return;
|
||||
res.redirect('/user/dashboard')
|
||||
return
|
||||
}
|
||||
res.render("subpage", {
|
||||
title: "Email Verified",
|
||||
whichPartial: "subpages/confirm",
|
||||
email: existingEmail.email,
|
||||
});
|
||||
res.render('subpage', {
|
||||
title: 'Email Verified',
|
||||
whichPartial: 'subpages/confirm',
|
||||
email: existingEmail.email
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// legacy /user/unsubscribe controller for pre-FxA unsubscribe links
|
||||
async function getUnsubscribe(req, res) {
|
||||
async function getUnsubscribe (req, res) {
|
||||
if (!req.query.token) {
|
||||
throw new FluentError("user-unsubscribe-token-error");
|
||||
throw new FluentError('user-unsubscribe-token-error')
|
||||
}
|
||||
|
||||
const subscriber = await DB.getSubscriberByToken(req.query.token);
|
||||
const subscriber = await DB.getSubscriberByToken(req.query.token)
|
||||
if (subscriber && subscriber.fxa_profile_json !== null) {
|
||||
// Token is for a primary email address of an FxA subscriber:
|
||||
// redirect to preferences to remove Firefox Monitor
|
||||
return res.redirect("/user/preferences");
|
||||
return res.redirect('/user/preferences')
|
||||
}
|
||||
|
||||
const emailAddress = await DB.getEmailByToken(req.query.token);
|
||||
const emailAddress = await DB.getEmailByToken(req.query.token)
|
||||
if (!subscriber && !emailAddress) {
|
||||
// Unknown token:
|
||||
// throw error
|
||||
throw new FluentError("error-not-subscribed");
|
||||
throw new FluentError('error-not-subscribed')
|
||||
}
|
||||
|
||||
// Token is for an old pre-FxA subscriber, or a secondary email address:
|
||||
// render the unsubscribe page
|
||||
res.render("subpage", {
|
||||
title: req.fluentFormat("user-unsubscribe-title"),
|
||||
whichPartial: "subpages/unsubscribe",
|
||||
res.render('subpage', {
|
||||
title: req.fluentFormat('user-unsubscribe-title'),
|
||||
whichPartial: 'subpages/unsubscribe',
|
||||
token: req.query.token,
|
||||
hash: req.query.hash,
|
||||
});
|
||||
hash: req.query.hash
|
||||
})
|
||||
}
|
||||
|
||||
async function getRemoveFxm (req, res) {
|
||||
const sessionUser = req.user
|
||||
|
||||
async function getRemoveFxm(req, res) {
|
||||
const sessionUser = req.user;
|
||||
|
||||
res.render("subpage", {
|
||||
title: req.fluentFormat("remove-fxm"),
|
||||
res.render('subpage', {
|
||||
title: req.fluentFormat('remove-fxm'),
|
||||
subscriber: sessionUser,
|
||||
whichPartial: "subpages/remove_fxm",
|
||||
csrfToken: req.csrfToken(),
|
||||
});
|
||||
whichPartial: 'subpages/remove_fxm',
|
||||
csrfToken: req.csrfToken()
|
||||
})
|
||||
}
|
||||
|
||||
async function postRemoveFxm (req, res) {
|
||||
const sessionUser = req.user
|
||||
await DB.removeSubscriber(sessionUser)
|
||||
await FXA.revokeOAuthTokens(sessionUser)
|
||||
|
||||
async function postRemoveFxm(req, res) {
|
||||
const sessionUser = req.user;
|
||||
await DB.removeSubscriber(sessionUser);
|
||||
await FXA.revokeOAuthTokens(sessionUser);
|
||||
|
||||
req.session.destroy();
|
||||
res.redirect("/");
|
||||
req.session.destroy()
|
||||
res.redirect('/')
|
||||
}
|
||||
|
||||
function _updateResolvedBreaches(options) {
|
||||
function _updateResolvedBreaches (options) {
|
||||
const {
|
||||
user,
|
||||
affectedEmail,
|
||||
isResolved,
|
||||
recencyIndexNumber,
|
||||
} = options;
|
||||
recencyIndexNumber
|
||||
} = options
|
||||
// TODO: clarify the logic here. maybe change the endpoint to PUT /breach-resolution
|
||||
// with the new resolution value ?
|
||||
const userBreachesResolved = user.breaches_resolved === null ? {} : user.breaches_resolved;
|
||||
if (isResolved === "false") {
|
||||
const userBreachesResolved = user.breaches_resolved === null ? {} : user.breaches_resolved
|
||||
if (isResolved === 'false') {
|
||||
if (Array.isArray(userBreachesResolved[affectedEmail])) {
|
||||
userBreachesResolved[affectedEmail].push(recencyIndexNumber);
|
||||
return userBreachesResolved;
|
||||
userBreachesResolved[affectedEmail].push(recencyIndexNumber)
|
||||
return userBreachesResolved
|
||||
}
|
||||
userBreachesResolved[affectedEmail] = [recencyIndexNumber];
|
||||
return userBreachesResolved;
|
||||
userBreachesResolved[affectedEmail] = [recencyIndexNumber]
|
||||
return userBreachesResolved
|
||||
}
|
||||
userBreachesResolved[affectedEmail] = userBreachesResolved[affectedEmail].filter( el => el !== recencyIndexNumber );
|
||||
return userBreachesResolved;
|
||||
userBreachesResolved[affectedEmail] = userBreachesResolved[affectedEmail].filter(el => el !== recencyIndexNumber)
|
||||
return userBreachesResolved
|
||||
}
|
||||
|
||||
|
||||
async function postResolveBreach(req, res) {
|
||||
const sessionUser = req.user;
|
||||
const { affectedEmail, recencyIndex, isResolved } = req.body;
|
||||
const recencyIndexNumber = Number(recencyIndex);
|
||||
const affectedEmailIsSubscriberRecord = sessionUser.primary_email === affectedEmail;
|
||||
const affectedEmailInEmailAddresses = sessionUser.email_addresses.filter( ea => {
|
||||
ea.email === affectedEmail;
|
||||
});
|
||||
async function postResolveBreach (req, res) {
|
||||
const sessionUser = req.user
|
||||
const { affectedEmail, recencyIndex, isResolved } = req.body
|
||||
const recencyIndexNumber = Number(recencyIndex)
|
||||
const affectedEmailIsSubscriberRecord = sessionUser.primary_email === affectedEmail
|
||||
const affectedEmailInEmailAddresses = sessionUser.email_addresses.filter(ea => {
|
||||
ea.email === affectedEmail
|
||||
})
|
||||
|
||||
if (!affectedEmailIsSubscriberRecord && !affectedEmailInEmailAddresses) {
|
||||
return res.json("Error: affectedEmail is not valid for this subscriber");
|
||||
return res.json('Error: affectedEmail is not valid for this subscriber')
|
||||
}
|
||||
|
||||
const updatedResolvedBreaches = _updateResolvedBreaches({
|
||||
user: sessionUser,
|
||||
affectedEmail,
|
||||
isResolved,
|
||||
recencyIndexNumber,
|
||||
});
|
||||
recencyIndexNumber
|
||||
})
|
||||
|
||||
const updatedSubscriber = await DB.setBreachesResolved(
|
||||
{ user: sessionUser, updatedResolvedBreaches }
|
||||
);
|
||||
req.session.user = updatedSubscriber;
|
||||
)
|
||||
req.session.user = updatedSubscriber
|
||||
// return res.json("Breach marked as resolved.");
|
||||
// Currently we're sending { affectedEmail, recencyIndex, isResolved, passwordsExposed } in req.body
|
||||
// Not sure if we need all of these or need to send other/additional values?
|
||||
|
||||
if (isResolved === "true") {
|
||||
if (isResolved === 'true') {
|
||||
// the user clicked "Undo" so mark the breach as unresolved
|
||||
return res.redirect("/");
|
||||
return res.redirect('/')
|
||||
}
|
||||
|
||||
const allBreaches = req.app.locals.breaches;
|
||||
const { verifiedEmails } = await getAllEmailsAndBreaches(req.session.user, allBreaches);
|
||||
const allBreaches = req.app.locals.breaches
|
||||
const { verifiedEmails } = await getAllEmailsAndBreaches(req.session.user, allBreaches)
|
||||
|
||||
const userBreachStats = resultsSummary(verifiedEmails);
|
||||
const numTotalBreaches = userBreachStats.numBreaches.count;
|
||||
const numResolvedBreaches = userBreachStats.numBreaches.numResolved;
|
||||
const userBreachStats = resultsSummary(verifiedEmails)
|
||||
const numTotalBreaches = userBreachStats.numBreaches.count
|
||||
const numResolvedBreaches = userBreachStats.numBreaches.numResolved
|
||||
|
||||
const localizedModalStrings = {
|
||||
headline: "",
|
||||
progressMessage: "",
|
||||
progressStatus: req.fluentFormat( "progress-status", {
|
||||
numResolvedBreaches: numResolvedBreaches,
|
||||
numTotalBreaches: numTotalBreaches }
|
||||
headline: '',
|
||||
progressMessage: '',
|
||||
progressStatus: req.fluentFormat('progress-status', {
|
||||
numResolvedBreaches,
|
||||
numTotalBreaches
|
||||
}
|
||||
),
|
||||
headlineClassName: "",
|
||||
};
|
||||
headlineClassName: ''
|
||||
}
|
||||
|
||||
switch (numResolvedBreaches) {
|
||||
case 1:
|
||||
localizedModalStrings.headline = req.fluentFormat("confirmation-1-subhead");
|
||||
localizedModalStrings.progressMessage = req.fluentFormat("confirmation-1-body");
|
||||
localizedModalStrings.headlineClassName = "overlay-resolved-first-breach";
|
||||
break;
|
||||
localizedModalStrings.headline = req.fluentFormat('confirmation-1-subhead')
|
||||
localizedModalStrings.progressMessage = req.fluentFormat('confirmation-1-body')
|
||||
localizedModalStrings.headlineClassName = 'overlay-resolved-first-breach'
|
||||
break
|
||||
|
||||
case 2:
|
||||
localizedModalStrings.headline = req.fluentFormat("confirmation-2-subhead");
|
||||
localizedModalStrings.progressMessage = req.fluentFormat("confirmation-2-body");
|
||||
localizedModalStrings.headlineClassName = "overlay-take-that-hackers";
|
||||
break;
|
||||
localizedModalStrings.headline = req.fluentFormat('confirmation-2-subhead')
|
||||
localizedModalStrings.progressMessage = req.fluentFormat('confirmation-2-body')
|
||||
localizedModalStrings.headlineClassName = 'overlay-take-that-hackers'
|
||||
break
|
||||
|
||||
case 3:
|
||||
localizedModalStrings.headline = req.fluentFormat("confirmation-3-subhead");
|
||||
localizedModalStrings.headline = req.fluentFormat('confirmation-3-subhead')
|
||||
// TO CONSIDER: The "confirmation-3-body" string contains nested markup.
|
||||
// We'll either have to remove it (requiring a string change), or we will have
|
||||
// to inject it into the template using innerHTML (scaryish).
|
||||
// Defaulting to the generic progressMessage for now.
|
||||
localizedModalStrings.progressMessage = req.fluentFormat("generic-confirmation-message", {
|
||||
numUnresolvedBreaches: numTotalBreaches-numResolvedBreaches,
|
||||
});
|
||||
localizedModalStrings.headlineClassName = "overlay-another-breach-resolved";
|
||||
break;
|
||||
localizedModalStrings.progressMessage = req.fluentFormat('generic-confirmation-message', {
|
||||
numUnresolvedBreaches: numTotalBreaches - numResolvedBreaches
|
||||
})
|
||||
localizedModalStrings.headlineClassName = 'overlay-another-breach-resolved'
|
||||
break
|
||||
|
||||
case numTotalBreaches:
|
||||
localizedModalStrings.headline = req.fluentFormat("confirmation-2-subhead");
|
||||
localizedModalStrings.progressMessage = req.fluentFormat("progress-complete");
|
||||
localizedModalStrings.headlineClassName = "overlay-marked-as-resolved";
|
||||
break;
|
||||
localizedModalStrings.headline = req.fluentFormat('confirmation-2-subhead')
|
||||
localizedModalStrings.progressMessage = req.fluentFormat('progress-complete')
|
||||
localizedModalStrings.headlineClassName = 'overlay-marked-as-resolved'
|
||||
break
|
||||
|
||||
default:
|
||||
if (numResolvedBreaches > 3) {
|
||||
localizedModalStrings.headline = req.fluentFormat("confirmation-2-subhead");
|
||||
localizedModalStrings.progressMessage = req.fluentFormat("confirmation-2-body");
|
||||
localizedModalStrings.headlineClassName = "overlay-marked-as-resolved";
|
||||
localizedModalStrings.headline = req.fluentFormat('confirmation-2-subhead')
|
||||
localizedModalStrings.progressMessage = req.fluentFormat('confirmation-2-body')
|
||||
localizedModalStrings.headlineClassName = 'overlay-marked-as-resolved'
|
||||
}
|
||||
break;
|
||||
break
|
||||
}
|
||||
|
||||
res.json(localizedModalStrings);
|
||||
res.json(localizedModalStrings)
|
||||
}
|
||||
|
||||
async function postUnsubscribe(req, res) {
|
||||
const { token, emailHash } = req.body;
|
||||
async function postUnsubscribe (req, res) {
|
||||
const { token, emailHash } = req.body
|
||||
|
||||
if (!token || !emailHash) {
|
||||
throw new FluentError("user-unsubscribe-token-email-error");
|
||||
throw new FluentError('user-unsubscribe-token-email-error')
|
||||
}
|
||||
|
||||
// legacy unsubscribe link page uses removeSubscriberByToken
|
||||
const unsubscribedUser = await DB.removeSubscriberByToken(token, emailHash);
|
||||
const unsubscribedUser = await DB.removeSubscriberByToken(token, emailHash)
|
||||
if (!unsubscribedUser) {
|
||||
const emailAddress = await DB.getEmailByToken(token);
|
||||
const emailAddress = await DB.getEmailByToken(token)
|
||||
if (!emailAddress) {
|
||||
throw new FluentError("error-not-subscribed");
|
||||
throw new FluentError('error-not-subscribed')
|
||||
}
|
||||
await DB.removeOneSecondaryEmail(emailAddress.id);
|
||||
return res.redirect("/user/preferences");
|
||||
await DB.removeOneSecondaryEmail(emailAddress.id)
|
||||
return res.redirect('/user/preferences')
|
||||
}
|
||||
await FXA.revokeOAuthTokens(unsubscribedUser);
|
||||
req.session.destroy();
|
||||
res.redirect("/");
|
||||
await FXA.revokeOAuthTokens(unsubscribedUser)
|
||||
req.session.destroy()
|
||||
res.redirect('/')
|
||||
}
|
||||
|
||||
async function getPreferences (req, res) {
|
||||
const user = req.user
|
||||
const allBreaches = req.app.locals.breaches
|
||||
const { verifiedEmails, unverifiedEmails } = await getAllEmailsAndBreaches(user, allBreaches)
|
||||
|
||||
async function getPreferences(req, res) {
|
||||
const user = req.user;
|
||||
const allBreaches = req.app.locals.breaches;
|
||||
const { verifiedEmails, unverifiedEmails } = await getAllEmailsAndBreaches(user, allBreaches);
|
||||
|
||||
res.render("dashboards", {
|
||||
title: "Firefox Monitor",
|
||||
whichPartial: "dashboards/preferences",
|
||||
res.render('dashboards', {
|
||||
title: 'Firefox Monitor',
|
||||
whichPartial: 'dashboards/preferences',
|
||||
csrfToken: req.csrfToken(),
|
||||
verifiedEmails, unverifiedEmails,
|
||||
});
|
||||
verifiedEmails,
|
||||
unverifiedEmails
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// This endpoint returns breach stats for Firefox clients to display
|
||||
// in about:protections
|
||||
//
|
||||
|
@ -536,65 +521,63 @@ async function getPreferences(req, res) {
|
|||
// with the FXA verification endpoint before we return breach stats.
|
||||
//
|
||||
// To test this endpoint, see the "Test Firefox Integration" section of the README.
|
||||
async function getBreachStats(req, res) {
|
||||
async function getBreachStats (req, res) {
|
||||
if (!req.token) {
|
||||
return res.status(401).json({
|
||||
errorMessage: "User breach stats requires an FXA OAuth token passed in the Authorization header.",
|
||||
});
|
||||
errorMessage: 'User breach stats requires an FXA OAuth token passed in the Authorization header.'
|
||||
})
|
||||
}
|
||||
const fxaResponse = await FXA.verifyOAuthToken(req.token);
|
||||
if (fxaResponse.name === "HTTPError") {
|
||||
const fxaResponse = await FXA.verifyOAuthToken(req.token)
|
||||
if (fxaResponse.name === 'HTTPError') {
|
||||
return res.status(fxaResponse.response.statusCode).json({
|
||||
errorMessage: "Could not verify FXA OAuth token. FXA returned message: " + fxaResponse.response.statusMessage,
|
||||
});
|
||||
errorMessage: 'Could not verify FXA OAuth token. FXA returned message: ' + fxaResponse.response.statusMessage
|
||||
})
|
||||
}
|
||||
if (!fxaResponse.body.scope.includes(FXA_MONITOR_SCOPE)) {
|
||||
return res.status(401).json({
|
||||
errorMessage: "The provided token does not include Monitor scope.",
|
||||
});
|
||||
errorMessage: 'The provided token does not include Monitor scope.'
|
||||
})
|
||||
}
|
||||
const user = await DB.getSubscriberByFxaUid(fxaResponse.body.user);
|
||||
const user = await DB.getSubscriberByFxaUid(fxaResponse.body.user)
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
errorMessage: "Cannot find Monitor subscriber for that FXA.",
|
||||
});
|
||||
errorMessage: 'Cannot find Monitor subscriber for that FXA.'
|
||||
})
|
||||
}
|
||||
const allBreaches = req.app.locals.breaches;
|
||||
const { verifiedEmails } = await getAllEmailsAndBreaches(user, allBreaches);
|
||||
const breachStats = resultsSummary(verifiedEmails);
|
||||
const allBreaches = req.app.locals.breaches
|
||||
const { verifiedEmails } = await getAllEmailsAndBreaches(user, allBreaches)
|
||||
const breachStats = resultsSummary(verifiedEmails)
|
||||
const baseStats = {
|
||||
monitoredEmails: breachStats.monitoredEmails.count,
|
||||
numBreaches: breachStats.numBreaches.count,
|
||||
passwords: breachStats.passwords.count,
|
||||
};
|
||||
passwords: breachStats.passwords.count
|
||||
}
|
||||
const resolvedStats = {
|
||||
numBreachesResolved: breachStats.numBreaches.numResolved,
|
||||
passwordsResolved: breachStats.passwords.numResolved,
|
||||
};
|
||||
const returnStats = (req.query.includeResolved === "true") ? Object.assign(baseStats, resolvedStats) : baseStats;
|
||||
return res.json(returnStats);
|
||||
passwordsResolved: breachStats.passwords.numResolved
|
||||
}
|
||||
const returnStats = (req.query.includeResolved === 'true') ? Object.assign(baseStats, resolvedStats) : baseStats
|
||||
return res.json(returnStats)
|
||||
}
|
||||
|
||||
|
||||
function logout(req, res) {
|
||||
function logout (req, res) {
|
||||
// Growth Experiment
|
||||
if (EXPERIMENTS_ENABLED && req.session.experimentFlags) {
|
||||
// Persist experimentBranch across session reset
|
||||
const sessionExperimentFlags = req.session.experimentFlags;
|
||||
const sessionExperimentFlags = req.session.experimentFlags
|
||||
req.session.destroy(() => {
|
||||
req.session = {experimentFlags: sessionExperimentFlags};
|
||||
});
|
||||
req.session = { experimentFlags: sessionExperimentFlags }
|
||||
})
|
||||
|
||||
// Return
|
||||
res.redirect("/");
|
||||
return;
|
||||
res.redirect('/')
|
||||
return
|
||||
}
|
||||
|
||||
req.session.destroy();
|
||||
res.redirect("/");
|
||||
req.session.destroy()
|
||||
res.redirect('/')
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
FXA_MONITOR_SCOPE,
|
||||
getPreferences,
|
||||
|
@ -612,5 +595,5 @@ module.exports = {
|
|||
removeEmail,
|
||||
resendEmail,
|
||||
updateCommunicationOptions,
|
||||
resolveBreach,
|
||||
};
|
||||
resolveBreach
|
||||
}
|
||||
|
|
|
@ -1,26 +1,25 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const crypto = require("crypto");
|
||||
const uuidv4 = require("uuid/v4");
|
||||
const mozlog = require("../log");
|
||||
const log = mozlog("controllers.utils");
|
||||
const crypto = require('crypto')
|
||||
const uuidv4 = require('uuid/v4')
|
||||
const mozlog = require('../log')
|
||||
const log = mozlog('controllers.utils')
|
||||
|
||||
const AppConstants = require("../app-constants");
|
||||
const AppConstants = require('../app-constants')
|
||||
|
||||
function generatePageToken (req) {
|
||||
const pageToken = { ip: req.ip, date: new Date(), nonce: uuidv4() }
|
||||
|
||||
function generatePageToken(req) {
|
||||
const pageToken = { ip: req.ip, date: new Date(), nonce: uuidv4() };
|
||||
|
||||
const iv = crypto.randomBytes(16); // AES uses block sizes of 16 bytes
|
||||
const key = crypto.pbkdf2Sync(AppConstants.COOKIE_SECRET, iv.toString(), 10000, 32, "sha512");
|
||||
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
|
||||
const iv = crypto.randomBytes(16) // AES uses block sizes of 16 bytes
|
||||
const key = crypto.pbkdf2Sync(AppConstants.COOKIE_SECRET, iv.toString(), 10000, 32, 'sha512')
|
||||
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv)
|
||||
|
||||
const encryptedPageToken = Buffer.concat([
|
||||
cipher.update(JSON.stringify(pageToken)),
|
||||
cipher.final(),
|
||||
]);
|
||||
cipher.final()
|
||||
])
|
||||
|
||||
return iv.toString("base64") + "." + encryptedPageToken.toString("base64");
|
||||
return iv.toString('base64') + '.' + encryptedPageToken.toString('base64')
|
||||
|
||||
/* TODO: block on scans-per-ip instead of scans-per-timespan
|
||||
if (req.session.scans === undefined){
|
||||
|
@ -31,204 +30,198 @@ function generatePageToken(req) {
|
|||
*/
|
||||
}
|
||||
|
||||
function hasUserSignedUpForWaitlist(user, waitlist) {
|
||||
function hasUserSignedUpForWaitlist (user, waitlist) {
|
||||
if (!user.waitlists_joined) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
if (user.waitlists_joined.hasOwnProperty(waitlist)) {
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
function getTotalPercentage(variations) {
|
||||
let percentage = 0;
|
||||
// calculate and store total percentage of variations
|
||||
for (const v in variations) {
|
||||
if (variations.hasOwnProperty(v) && typeof variations[v] === "number") {
|
||||
// multiply by 100 to allow for percentages to the hundredth
|
||||
// (and avoid floating point math errors)
|
||||
percentage += (variations[v] * 100);
|
||||
}
|
||||
function getTotalPercentage (variations) {
|
||||
let percentage = 0
|
||||
// calculate and store total percentage of variations
|
||||
for (const v in variations) {
|
||||
if (variations.hasOwnProperty(v) && typeof variations[v] === 'number') {
|
||||
// multiply by 100 to allow for percentages to the hundredth
|
||||
// (and avoid floating point math errors)
|
||||
percentage += (variations[v] * 100)
|
||||
}
|
||||
}
|
||||
|
||||
const totalPercentage = percentage / 100;
|
||||
const totalPercentage = percentage / 100
|
||||
|
||||
// Make sure totalPercent is between 0 and 100
|
||||
if (totalPercentage === 0 || totalPercentage > 100) {
|
||||
throw new Error(`The total percentage ${totalPercentage} is out of bounds!`);
|
||||
}
|
||||
// Make sure totalPercent is between 0 and 100
|
||||
if (totalPercentage === 0 || totalPercentage > 100) {
|
||||
throw new Error(`The total percentage ${totalPercentage} is out of bounds!`)
|
||||
}
|
||||
|
||||
return percentage;
|
||||
return percentage
|
||||
}
|
||||
|
||||
function chooseVariation(variations, sorterNum){
|
||||
|
||||
const totalPercentage = getTotalPercentage(variations);
|
||||
function chooseVariation (variations, sorterNum) {
|
||||
const totalPercentage = getTotalPercentage(variations)
|
||||
|
||||
// make sure random number falls in the distribution range
|
||||
let runningTotal;
|
||||
let choice;
|
||||
let runningTotal
|
||||
let choice
|
||||
|
||||
if (sorterNum <= totalPercentage) {
|
||||
runningTotal = 0;
|
||||
runningTotal = 0
|
||||
|
||||
// loop through all variations
|
||||
for (const v in variations) {
|
||||
// check if random number falls within current variation range
|
||||
if (sorterNum <= (variations[v] + runningTotal)) {
|
||||
// if so, we have found our variation
|
||||
choice = v;
|
||||
break;
|
||||
}
|
||||
|
||||
// tally variation percentages for the next loop iteration
|
||||
runningTotal += variations[v];
|
||||
// loop through all variations
|
||||
for (const v in variations) {
|
||||
// check if random number falls within current variation range
|
||||
if (sorterNum <= (variations[v] + runningTotal)) {
|
||||
// if so, we have found our variation
|
||||
choice = v
|
||||
break
|
||||
}
|
||||
|
||||
// tally variation percentages for the next loop iteration
|
||||
runningTotal += variations[v]
|
||||
}
|
||||
|
||||
return choice;
|
||||
|
||||
}
|
||||
|
||||
function unEnrollSession(session) {
|
||||
session.excludeFromExperiment = true;
|
||||
session.experimentBranch = false;
|
||||
session.treatmentBranch = false;
|
||||
session.controlBranch = false;
|
||||
}
|
||||
|
||||
function setBranchVariable(branch, sessionExperimentFlags) {
|
||||
if (branch === "va") {
|
||||
sessionExperimentFlags.controlBranch = true;
|
||||
sessionExperimentFlags.treatmentBranch = false;
|
||||
}
|
||||
if (branch === "vb") {
|
||||
sessionExperimentFlags.treatmentBranch = true;
|
||||
sessionExperimentFlags.controlBranch = false;
|
||||
|
||||
return choice
|
||||
}
|
||||
|
||||
function unEnrollSession (session) {
|
||||
session.excludeFromExperiment = true
|
||||
session.experimentBranch = false
|
||||
session.treatmentBranch = false
|
||||
session.controlBranch = false
|
||||
}
|
||||
|
||||
function setBranchVariable (branch, sessionExperimentFlags) {
|
||||
if (branch === 'va') {
|
||||
sessionExperimentFlags.controlBranch = true
|
||||
sessionExperimentFlags.treatmentBranch = false
|
||||
}
|
||||
if (branch === 'vb') {
|
||||
sessionExperimentFlags.treatmentBranch = true
|
||||
sessionExperimentFlags.controlBranch = false
|
||||
}
|
||||
}
|
||||
|
||||
function getExperimentBranch(req, sorterNum = false, language = false, variations) {
|
||||
|
||||
const sessionExperimentFlags = req.session.experimentFlags;
|
||||
function getExperimentBranch (req, sorterNum = false, language = false, variations) {
|
||||
const sessionExperimentFlags = req.session.experimentFlags
|
||||
|
||||
if (sessionExperimentFlags.excludeFromExperiment && !req.query.experimentBranch) {
|
||||
log.debug("This session has already been excluded from the experiment");
|
||||
unEnrollSession(sessionExperimentFlags);
|
||||
return false;
|
||||
log.debug('This session has already been excluded from the experiment')
|
||||
unEnrollSession(sessionExperimentFlags)
|
||||
return false
|
||||
}
|
||||
|
||||
// If we cannot parse req.headers["accept-language"], we should not
|
||||
// enroll users in the experiment.
|
||||
if (language && !req.headers || language && !req.headers["accept-language"]){
|
||||
log.debug("No headers or accept-language information present.");
|
||||
unEnrollSession(sessionExperimentFlags);
|
||||
return false;
|
||||
if (language && !req.headers || language && !req.headers['accept-language']) {
|
||||
log.debug('No headers or accept-language information present.')
|
||||
unEnrollSession(sessionExperimentFlags)
|
||||
return false
|
||||
}
|
||||
|
||||
// If the user doesn't have the requested variant langauge selected as their primary language,
|
||||
// we do not enroll them in the experiment.
|
||||
if (language) {
|
||||
|
||||
if (!Array.isArray(language)) {
|
||||
throw new Error("The language param is not an array");
|
||||
throw new Error('The language param is not an array')
|
||||
}
|
||||
const lang = req.headers["accept-language"].split(",");
|
||||
const lang = req.headers['accept-language'].split(',')
|
||||
|
||||
// Check to make sure one of the experiment langauge(s) is the top-preferred language.
|
||||
const firstLangMatch = (element) => lang[0].includes(element);
|
||||
const firstLangMatch = (element) => lang[0].includes(element)
|
||||
if (language && !language.some(firstLangMatch)) {
|
||||
log.debug(`Preferred language is not [${language}] variant: ${lang[0]}`);
|
||||
unEnrollSession(sessionExperimentFlags);
|
||||
return false;
|
||||
log.debug(`Preferred language is not [${language}] variant: ${lang[0]}`)
|
||||
unEnrollSession(sessionExperimentFlags)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// If URL param has experimentBranch entry, use that branch;
|
||||
if (req.query.experimentBranch) {
|
||||
if (!Object.keys(variations).includes(req.query.experimentBranch)) {
|
||||
log.debug("The requested branch is unknown: ", req.query.experimentBranch);
|
||||
unEnrollSession(sessionExperimentFlags);
|
||||
return false;
|
||||
log.debug('The requested branch is unknown: ', req.query.experimentBranch)
|
||||
unEnrollSession(sessionExperimentFlags)
|
||||
return false
|
||||
}
|
||||
log.debug("This session has been set to the requested branch: ", req.query.experimentBranch);
|
||||
sessionExperimentFlags.excludeFromExperiment = false;
|
||||
sessionExperimentFlags.experimentBranch = req.query.experimentBranch;
|
||||
setBranchVariable(sessionExperimentFlags.experimentBranch, sessionExperimentFlags);
|
||||
return req.query.experimentBranch;
|
||||
log.debug('This session has been set to the requested branch: ', req.query.experimentBranch)
|
||||
sessionExperimentFlags.excludeFromExperiment = false
|
||||
sessionExperimentFlags.experimentBranch = req.query.experimentBranch
|
||||
setBranchVariable(sessionExperimentFlags.experimentBranch, sessionExperimentFlags)
|
||||
return req.query.experimentBranch
|
||||
}
|
||||
|
||||
// If user was already assigned a branch, stay in that branch;
|
||||
if (sessionExperimentFlags.experimentBranch) {
|
||||
log.debug("This session has already been assigned: ", sessionExperimentFlags.experimentBranch);
|
||||
setBranchVariable(sessionExperimentFlags.experimentBranch, sessionExperimentFlags);
|
||||
return sessionExperimentFlags.experimentBranch;
|
||||
log.debug('This session has already been assigned: ', sessionExperimentFlags.experimentBranch)
|
||||
setBranchVariable(sessionExperimentFlags.experimentBranch, sessionExperimentFlags)
|
||||
return sessionExperimentFlags.experimentBranch
|
||||
}
|
||||
|
||||
if (sorterNum === false) {
|
||||
sorterNum = Math.floor(Math.random() * 10000) + 1;
|
||||
sorterNum = sorterNum/100;
|
||||
sorterNum = Math.floor(Math.random() * 10000) + 1
|
||||
sorterNum = sorterNum / 100
|
||||
// sorterNum = Math.floor(Math.random() * 100);
|
||||
log.debug("No coinflip number provided. Coinflip number is ", sorterNum);
|
||||
log.debug('No coinflip number provided. Coinflip number is ', sorterNum)
|
||||
} else {
|
||||
log.debug("Coinflip number provided. Coinflip number is ", sorterNum);
|
||||
log.debug('Coinflip number provided. Coinflip number is ', sorterNum)
|
||||
}
|
||||
|
||||
const assignedCohort = chooseVariation(variations, sorterNum);
|
||||
const assignedCohort = chooseVariation(variations, sorterNum)
|
||||
|
||||
if (!assignedCohort) {
|
||||
log.debug("This session has randomly been removed from the experiment");
|
||||
sessionExperimentFlags.excludeFromExperiment = true;
|
||||
return false;
|
||||
log.debug('This session has randomly been removed from the experiment')
|
||||
sessionExperimentFlags.excludeFromExperiment = true
|
||||
return false
|
||||
}
|
||||
|
||||
log.debug(`This session has been randomly assigned to the ${assignedCohort} cohort.`);
|
||||
sessionExperimentFlags.experimentBranch = assignedCohort;
|
||||
setBranchVariable(sessionExperimentFlags.experimentBranch, sessionExperimentFlags);
|
||||
return assignedCohort;
|
||||
|
||||
log.debug(`This session has been randomly assigned to the ${assignedCohort} cohort.`)
|
||||
sessionExperimentFlags.experimentBranch = assignedCohort
|
||||
setBranchVariable(sessionExperimentFlags.experimentBranch, sessionExperimentFlags)
|
||||
return assignedCohort
|
||||
}
|
||||
|
||||
function getUTMContents(req) {
|
||||
function getUTMContents (req) {
|
||||
if (!req) {
|
||||
throw new Error("No request available");
|
||||
throw new Error('No request available')
|
||||
}
|
||||
|
||||
// If UTMs are set previously, set them again.
|
||||
if (req.session.utmOverrides) {
|
||||
return req.session.utmOverrides;
|
||||
return req.session.utmOverrides
|
||||
}
|
||||
|
||||
req.session.utmOverrides = false;
|
||||
return false;
|
||||
req.session.utmOverrides = false
|
||||
return false
|
||||
}
|
||||
|
||||
function getExperimentFlags(req, EXPERIMENTS_ENABLED) {
|
||||
function getExperimentFlags (req, EXPERIMENTS_ENABLED) {
|
||||
if (!req) {
|
||||
throw new Error("No request available");
|
||||
throw new Error('No request available')
|
||||
}
|
||||
|
||||
if (req.session.experimentFlags && EXPERIMENTS_ENABLED) {
|
||||
return req.session.experimentFlags;
|
||||
return req.session.experimentFlags
|
||||
}
|
||||
|
||||
const experimentFlags = {
|
||||
experimentBranch: false,
|
||||
treatmentBranch: false,
|
||||
controlBranch: false,
|
||||
excludeFromExperiment: false,
|
||||
};
|
||||
excludeFromExperiment: false
|
||||
}
|
||||
|
||||
req.session.experimentFlags = experimentFlags;
|
||||
return experimentFlags;
|
||||
req.session.experimentFlags = experimentFlags
|
||||
return experimentFlags
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
generatePageToken,
|
||||
hasUserSignedUpForWaitlist,
|
||||
getExperimentBranch,
|
||||
getExperimentFlags,
|
||||
getUTMContents,
|
||||
};
|
||||
getUTMContents
|
||||
}
|
||||
|
|
509
db/DB.js
509
db/DB.js
|
@ -1,193 +1,189 @@
|
|||
"use strict";
|
||||
|
||||
'use strict'
|
||||
|
||||
// eslint-disable-next-line node/no-extraneous-require
|
||||
const uuidv4 = require("uuid/v4");
|
||||
const Knex = require("knex");
|
||||
const { attachPaginate } = require("knex-paginate");
|
||||
const uuidv4 = require('uuid/v4')
|
||||
const Knex = require('knex')
|
||||
const { attachPaginate } = require('knex-paginate')
|
||||
|
||||
const { FluentError } = require("../locale-utils");
|
||||
const AppConstants = require("../app-constants");
|
||||
const { FXA } = require("../lib/fxa");
|
||||
const HIBP = require("../hibp");
|
||||
const getSha1 = require("../sha1-utils");
|
||||
const mozlog = require("../log");
|
||||
const { FluentError } = require('../locale-utils')
|
||||
const AppConstants = require('../app-constants')
|
||||
const { FXA } = require('../lib/fxa')
|
||||
const HIBP = require('../hibp')
|
||||
const getSha1 = require('../sha1-utils')
|
||||
const mozlog = require('../log')
|
||||
|
||||
const knexConfig = require("./knexfile");
|
||||
const knexConfig = require('./knexfile')
|
||||
|
||||
let knex = Knex(knexConfig);
|
||||
attachPaginate();
|
||||
|
||||
const log = mozlog("DB");
|
||||
let knex = Knex(knexConfig)
|
||||
attachPaginate()
|
||||
|
||||
const log = mozlog('DB')
|
||||
|
||||
const DB = {
|
||||
async getSubscriberByToken(token) {
|
||||
const res = await knex("subscribers")
|
||||
.where("primary_verification_token", "=", token);
|
||||
async getSubscriberByToken (token) {
|
||||
const res = await knex('subscribers')
|
||||
.where('primary_verification_token', '=', token)
|
||||
|
||||
return res[0];
|
||||
return res[0]
|
||||
},
|
||||
|
||||
async getEmailByToken(token) {
|
||||
const res = await knex("email_addresses")
|
||||
.where("verification_token", "=", token);
|
||||
async getEmailByToken (token) {
|
||||
const res = await knex('email_addresses')
|
||||
.where('verification_token', '=', token)
|
||||
|
||||
return res[0];
|
||||
return res[0]
|
||||
},
|
||||
|
||||
async getEmailById(emailAddressId) {
|
||||
const res = await knex("email_addresses")
|
||||
.where("id", "=", emailAddressId);
|
||||
async getEmailById (emailAddressId) {
|
||||
const res = await knex('email_addresses')
|
||||
.where('id', '=', emailAddressId)
|
||||
|
||||
return res[0];
|
||||
return res[0]
|
||||
},
|
||||
|
||||
async getSubscriberByTokenAndHash(token, emailSha1) {
|
||||
const res = await knex.table("subscribers")
|
||||
async getSubscriberByTokenAndHash (token, emailSha1) {
|
||||
const res = await knex.table('subscribers')
|
||||
.first()
|
||||
.where({
|
||||
"primary_verification_token": token,
|
||||
"primary_sha1": emailSha1,
|
||||
});
|
||||
return res;
|
||||
primary_verification_token: token,
|
||||
primary_sha1: emailSha1
|
||||
})
|
||||
return res
|
||||
},
|
||||
|
||||
async joinEmailAddressesToSubscriber(subscriber) {
|
||||
async joinEmailAddressesToSubscriber (subscriber) {
|
||||
if (subscriber) {
|
||||
const emailAddressRecords = await knex("email_addresses").where({
|
||||
"subscriber_id": subscriber.id,
|
||||
});
|
||||
const emailAddressRecords = await knex('email_addresses').where({
|
||||
subscriber_id: subscriber.id
|
||||
})
|
||||
subscriber.email_addresses = emailAddressRecords.map(
|
||||
emailAddress=>({id: emailAddress.id, email: emailAddress.email})
|
||||
);
|
||||
emailAddress => ({ id: emailAddress.id, email: emailAddress.email })
|
||||
)
|
||||
}
|
||||
return subscriber;
|
||||
return subscriber
|
||||
},
|
||||
|
||||
async getSubscriberById(id) {
|
||||
const [subscriber] = await knex("subscribers").where({
|
||||
"id": id,
|
||||
});
|
||||
const subscriberAndEmails = await this.joinEmailAddressesToSubscriber(subscriber);
|
||||
return subscriberAndEmails;
|
||||
async getSubscriberById (id) {
|
||||
const [subscriber] = await knex('subscribers').where({
|
||||
id
|
||||
})
|
||||
const subscriberAndEmails = await this.joinEmailAddressesToSubscriber(subscriber)
|
||||
return subscriberAndEmails
|
||||
},
|
||||
|
||||
async getSubscriberByFxaUid(uid) {
|
||||
const [subscriber] = await knex("subscribers").where({
|
||||
"fxa_uid": uid,
|
||||
});
|
||||
const subscriberAndEmails = await this.joinEmailAddressesToSubscriber(subscriber);
|
||||
return subscriberAndEmails;
|
||||
async getSubscriberByFxaUid (uid) {
|
||||
const [subscriber] = await knex('subscribers').where({
|
||||
fxa_uid: uid
|
||||
})
|
||||
const subscriberAndEmails = await this.joinEmailAddressesToSubscriber(subscriber)
|
||||
return subscriberAndEmails
|
||||
},
|
||||
|
||||
async getPreFxaSubscribersPage(pagination) {
|
||||
return await knex("subscribers")
|
||||
async getPreFxaSubscribersPage (pagination) {
|
||||
return await knex('subscribers')
|
||||
.whereRaw("(fxa_uid = '') IS NOT FALSE")
|
||||
.paginate(pagination);
|
||||
|
||||
.paginate(pagination)
|
||||
},
|
||||
|
||||
async getSubscriberByEmail(email) {
|
||||
const [subscriber] = await knex("subscribers").where({
|
||||
"primary_email": email,
|
||||
"primary_verified": true,
|
||||
});
|
||||
const subscriberAndEmails = await this.joinEmailAddressesToSubscriber(subscriber);
|
||||
return subscriberAndEmails;
|
||||
async getSubscriberByEmail (email) {
|
||||
const [subscriber] = await knex('subscribers').where({
|
||||
primary_email: email,
|
||||
primary_verified: true
|
||||
})
|
||||
const subscriberAndEmails = await this.joinEmailAddressesToSubscriber(subscriber)
|
||||
return subscriberAndEmails
|
||||
},
|
||||
|
||||
async getEmailAddressRecordByEmail(email) {
|
||||
const emailAddresses = await knex("email_addresses").where({
|
||||
"email": email, verified: true,
|
||||
});
|
||||
async getEmailAddressRecordByEmail (email) {
|
||||
const emailAddresses = await knex('email_addresses').where({
|
||||
email, verified: true
|
||||
})
|
||||
if (!emailAddresses) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
if (emailAddresses.length > 1) {
|
||||
// TODO: handle multiple emails in separate(?) subscriber accounts?
|
||||
log.warn("getEmailAddressRecordByEmail", {msg: "found the same email multiple times"});
|
||||
log.warn('getEmailAddressRecordByEmail', { msg: 'found the same email multiple times' })
|
||||
}
|
||||
return emailAddresses[0];
|
||||
return emailAddresses[0]
|
||||
},
|
||||
|
||||
|
||||
async addSubscriberUnverifiedEmailHash(user, email) {
|
||||
const res = await knex("email_addresses").insert({
|
||||
async addSubscriberUnverifiedEmailHash (user, email) {
|
||||
const res = await knex('email_addresses').insert({
|
||||
subscriber_id: user.id,
|
||||
email: email,
|
||||
email,
|
||||
sha1: getSha1(email),
|
||||
verification_token: uuidv4(),
|
||||
verified: false,
|
||||
}).returning("*");
|
||||
return res[0];
|
||||
verified: false
|
||||
}).returning('*')
|
||||
return res[0]
|
||||
},
|
||||
|
||||
async resetUnverifiedEmailAddress(emailAddressId) {
|
||||
const newVerificationToken = uuidv4();
|
||||
const res = await knex("email_addresses")
|
||||
async resetUnverifiedEmailAddress (emailAddressId) {
|
||||
const newVerificationToken = uuidv4()
|
||||
const res = await knex('email_addresses')
|
||||
.update({
|
||||
verification_token: newVerificationToken,
|
||||
updated_at: knex.fn.now(),
|
||||
updated_at: knex.fn.now()
|
||||
})
|
||||
.where("id", emailAddressId)
|
||||
.returning("*");
|
||||
return res[0];
|
||||
.where('id', emailAddressId)
|
||||
.returning('*')
|
||||
return res[0]
|
||||
},
|
||||
|
||||
async verifyEmailHash(token) {
|
||||
const unverifiedEmail = await this.getEmailByToken(token);
|
||||
async verifyEmailHash (token) {
|
||||
const unverifiedEmail = await this.getEmailByToken(token)
|
||||
if (!unverifiedEmail) {
|
||||
throw new FluentError("Error message for this verification email timed out or something went wrong.");
|
||||
throw new FluentError('Error message for this verification email timed out or something went wrong.')
|
||||
}
|
||||
const verifiedEmail = await this._verifyNewEmail(unverifiedEmail);
|
||||
return verifiedEmail[0];
|
||||
const verifiedEmail = await this._verifyNewEmail(unverifiedEmail)
|
||||
return verifiedEmail[0]
|
||||
},
|
||||
|
||||
// TODO: refactor into an upsert? https://jaketrent.com/post/upsert-knexjs/
|
||||
// Used internally, ideally should not be called by consumers.
|
||||
async _getSha1EntryAndDo(sha1, aFoundCallback, aNotFoundCallback) {
|
||||
const existingEntries = await knex("subscribers")
|
||||
.where("primary_sha1", sha1);
|
||||
async _getSha1EntryAndDo (sha1, aFoundCallback, aNotFoundCallback) {
|
||||
const existingEntries = await knex('subscribers')
|
||||
.where('primary_sha1', sha1)
|
||||
|
||||
if (existingEntries.length && aFoundCallback) {
|
||||
return await aFoundCallback(existingEntries[0]);
|
||||
return await aFoundCallback(existingEntries[0])
|
||||
}
|
||||
|
||||
if (!existingEntries.length && aNotFoundCallback) {
|
||||
return await aNotFoundCallback();
|
||||
return await aNotFoundCallback()
|
||||
}
|
||||
},
|
||||
|
||||
// Used internally.
|
||||
async _addEmailHash(sha1, email, signup_language, verified = false) {
|
||||
async _addEmailHash (sha1, email, signup_language, verified = false) {
|
||||
try {
|
||||
return await this._getSha1EntryAndDo(sha1, async aEntry => {
|
||||
// Entry existed, patch the email value if supplied.
|
||||
if (email) {
|
||||
const res = await knex("subscribers")
|
||||
const res = await knex('subscribers')
|
||||
.update({
|
||||
primary_email: email,
|
||||
primary_sha1: getSha1(email.toLowerCase()),
|
||||
primary_verified: verified,
|
||||
updated_at: knex.fn.now(),
|
||||
updated_at: knex.fn.now()
|
||||
})
|
||||
.where("id", "=", aEntry.id)
|
||||
.returning("*");
|
||||
return res[0];
|
||||
.where('id', '=', aEntry.id)
|
||||
.returning('*')
|
||||
return res[0]
|
||||
}
|
||||
|
||||
return aEntry;
|
||||
return aEntry
|
||||
}, async () => {
|
||||
// Always add a verification_token value
|
||||
const verification_token = uuidv4();
|
||||
const res = await knex("subscribers")
|
||||
const verification_token = uuidv4()
|
||||
const res = await knex('subscribers')
|
||||
.insert({ primary_sha1: getSha1(email.toLowerCase()), primary_email: email, signup_language, primary_verification_token: verification_token, primary_verified: verified })
|
||||
.returning("*");
|
||||
return res[0];
|
||||
});
|
||||
.returning('*')
|
||||
return res[0]
|
||||
})
|
||||
} catch (e) {
|
||||
throw new FluentError("error-could-not-add-email");
|
||||
throw new FluentError('error-could-not-add-email')
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -204,14 +200,14 @@ const DB = {
|
|||
* @param {string} fxaProfileData from Firefox Account
|
||||
* @returns {object} subscriber knex object added to DB
|
||||
*/
|
||||
async addSubscriber(email, signupLanguage, fxaAccessToken=null, fxaRefreshToken=null, fxaProfileData=null) {
|
||||
const emailHash = await this._addEmailHash(getSha1(email), email, signupLanguage, true);
|
||||
const verified = await this._verifySubscriber(emailHash);
|
||||
const verifiedSubscriber = Array.isArray(verified) ? verified[0] : null;
|
||||
async addSubscriber (email, signupLanguage, fxaAccessToken = null, fxaRefreshToken = null, fxaProfileData = null) {
|
||||
const emailHash = await this._addEmailHash(getSha1(email), email, signupLanguage, true)
|
||||
const verified = await this._verifySubscriber(emailHash)
|
||||
const verifiedSubscriber = Array.isArray(verified) ? verified[0] : null
|
||||
if (fxaRefreshToken || fxaProfileData) {
|
||||
return this._updateFxAData(verifiedSubscriber, fxaAccessToken, fxaRefreshToken, fxaProfileData);
|
||||
return this._updateFxAData(verifiedSubscriber, fxaAccessToken, fxaRefreshToken, fxaProfileData)
|
||||
}
|
||||
return verifiedSubscriber;
|
||||
return verifiedSubscriber
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -223,42 +219,41 @@ const DB = {
|
|||
* @param {object} emailHash knex object in DB
|
||||
* @returns {object} verified subscriber knex object in DB
|
||||
*/
|
||||
async _verifySubscriber(emailHash) {
|
||||
async _verifySubscriber (emailHash) {
|
||||
// TODO: move this "up" into controllers/users ?
|
||||
await HIBP.subscribeHash(emailHash.primary_sha1);
|
||||
await HIBP.subscribeHash(emailHash.primary_sha1)
|
||||
|
||||
const verifiedSubscriber = await knex("subscribers")
|
||||
.where("primary_email", "=", emailHash.primary_email)
|
||||
const verifiedSubscriber = await knex('subscribers')
|
||||
.where('primary_email', '=', emailHash.primary_email)
|
||||
.update({
|
||||
primary_verified: true,
|
||||
updated_at: knex.fn.now(),
|
||||
updated_at: knex.fn.now()
|
||||
})
|
||||
.returning("*");
|
||||
.returning('*')
|
||||
|
||||
return verifiedSubscriber;
|
||||
return verifiedSubscriber
|
||||
},
|
||||
|
||||
// Verifies new emails added by existing users
|
||||
async _verifyNewEmail(emailHash) {
|
||||
await HIBP.subscribeHash(emailHash.sha1);
|
||||
async _verifyNewEmail (emailHash) {
|
||||
await HIBP.subscribeHash(emailHash.sha1)
|
||||
|
||||
const verifiedEmail = await knex("email_addresses")
|
||||
.where("id", "=", emailHash.id)
|
||||
const verifiedEmail = await knex('email_addresses')
|
||||
.where('id', '=', emailHash.id)
|
||||
.update({
|
||||
verified: true,
|
||||
verified: true
|
||||
})
|
||||
.returning("*");
|
||||
.returning('*')
|
||||
|
||||
return verifiedEmail;
|
||||
return verifiedEmail
|
||||
},
|
||||
|
||||
async getUserEmails(userId) {
|
||||
async getUserEmails (userId) {
|
||||
const userEmails = await knex('email_addresses')
|
||||
.where('subscriber_id', '=', userId)
|
||||
.returning('*')
|
||||
|
||||
const userEmails = await knex("email_addresses")
|
||||
.where("subscriber_id", "=", userId)
|
||||
.returning("*");
|
||||
|
||||
return userEmails;
|
||||
return userEmails
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -270,174 +265,172 @@ const DB = {
|
|||
* @param {string} fxaProfileData from Firefox Account
|
||||
* @returns {object} updated subscriber knex object in DB
|
||||
*/
|
||||
async _updateFxAData(subscriber, fxaAccessToken, fxaRefreshToken, fxaProfileData) {
|
||||
const fxaUID = JSON.parse(fxaProfileData).uid;
|
||||
const updated = await knex("subscribers")
|
||||
.where("id", "=", subscriber.id)
|
||||
.update({
|
||||
fxa_uid: fxaUID,
|
||||
fxa_access_token: fxaAccessToken,
|
||||
fxa_refresh_token: fxaRefreshToken,
|
||||
fxa_profile_json: fxaProfileData,
|
||||
})
|
||||
.returning("*");
|
||||
const updatedSubscriber = Array.isArray(updated) ? updated[0] : null;
|
||||
if (updatedSubscriber) {
|
||||
FXA.destroyOAuthToken({refresh_token: subscriber.fxa_refresh_token});
|
||||
}
|
||||
return updatedSubscriber;
|
||||
},
|
||||
|
||||
async updateFxAProfileData(subscriber, fxaProfileData) {
|
||||
await knex("subscribers").where("id", subscriber.id)
|
||||
.update({
|
||||
fxa_profile_json: fxaProfileData,
|
||||
});
|
||||
return this.getSubscriberById(subscriber.id);
|
||||
},
|
||||
|
||||
async setBreachesLastShownNow(subscriber) {
|
||||
// TODO: turn 2 db queries into a single query (also see #942)
|
||||
const nowDateTime = new Date();
|
||||
const nowTimeStamp = nowDateTime.toISOString();
|
||||
await knex("subscribers")
|
||||
.where("id", "=", subscriber.id)
|
||||
.update({
|
||||
breaches_last_shown: nowTimeStamp,
|
||||
});
|
||||
return this.getSubscriberByEmail(subscriber.primary_email);
|
||||
},
|
||||
|
||||
async setAllEmailsToPrimary(subscriber, allEmailsToPrimary) {
|
||||
const updated = await knex("subscribers")
|
||||
.where("id", subscriber.id)
|
||||
.update({
|
||||
all_emails_to_primary: allEmailsToPrimary,
|
||||
})
|
||||
.returning("*");
|
||||
const updatedSubscriber = Array.isArray(updated) ? updated[0] : null;
|
||||
return updatedSubscriber;
|
||||
},
|
||||
|
||||
async setBreachesResolved(options) {
|
||||
const { user, updatedResolvedBreaches } = options;
|
||||
await knex("subscribers")
|
||||
.where("id", user.id)
|
||||
.update({
|
||||
breaches_resolved: updatedResolvedBreaches,
|
||||
});
|
||||
return this.getSubscriberByEmail(user.primary_email);
|
||||
},
|
||||
|
||||
async setWaitlistsJoined(options) {
|
||||
const { user, updatedWaitlistsJoined } = options;
|
||||
await knex("subscribers")
|
||||
.where("id", user.id)
|
||||
async _updateFxAData (subscriber, fxaAccessToken, fxaRefreshToken, fxaProfileData) {
|
||||
const fxaUID = JSON.parse(fxaProfileData).uid
|
||||
const updated = await knex('subscribers')
|
||||
.where('id', '=', subscriber.id)
|
||||
.update({
|
||||
waitlists_joined: updatedWaitlistsJoined,
|
||||
});
|
||||
return this.getSubscriberByEmail(user.primary_email);
|
||||
fxa_uid: fxaUID,
|
||||
fxa_access_token: fxaAccessToken,
|
||||
fxa_refresh_token: fxaRefreshToken,
|
||||
fxa_profile_json: fxaProfileData
|
||||
})
|
||||
.returning('*')
|
||||
const updatedSubscriber = Array.isArray(updated) ? updated[0] : null
|
||||
if (updatedSubscriber) {
|
||||
FXA.destroyOAuthToken({ refresh_token: subscriber.fxa_refresh_token })
|
||||
}
|
||||
return updatedSubscriber
|
||||
},
|
||||
|
||||
async removeSubscriber(subscriber) {
|
||||
await knex("email_addresses").where({"subscriber_id": subscriber.id}).del();
|
||||
await knex("subscribers").where({"id": subscriber.id}).del();
|
||||
async updateFxAProfileData (subscriber, fxaProfileData) {
|
||||
await knex('subscribers').where('id', subscriber.id)
|
||||
.update({
|
||||
fxa_profile_json: fxaProfileData
|
||||
})
|
||||
return this.getSubscriberById(subscriber.id)
|
||||
},
|
||||
|
||||
async setBreachesLastShownNow (subscriber) {
|
||||
// TODO: turn 2 db queries into a single query (also see #942)
|
||||
const nowDateTime = new Date()
|
||||
const nowTimeStamp = nowDateTime.toISOString()
|
||||
await knex('subscribers')
|
||||
.where('id', '=', subscriber.id)
|
||||
.update({
|
||||
breaches_last_shown: nowTimeStamp
|
||||
})
|
||||
return this.getSubscriberByEmail(subscriber.primary_email)
|
||||
},
|
||||
|
||||
async setAllEmailsToPrimary (subscriber, allEmailsToPrimary) {
|
||||
const updated = await knex('subscribers')
|
||||
.where('id', subscriber.id)
|
||||
.update({
|
||||
all_emails_to_primary: allEmailsToPrimary
|
||||
})
|
||||
.returning('*')
|
||||
const updatedSubscriber = Array.isArray(updated) ? updated[0] : null
|
||||
return updatedSubscriber
|
||||
},
|
||||
|
||||
async setBreachesResolved (options) {
|
||||
const { user, updatedResolvedBreaches } = options
|
||||
await knex('subscribers')
|
||||
.where('id', user.id)
|
||||
.update({
|
||||
breaches_resolved: updatedResolvedBreaches
|
||||
})
|
||||
return this.getSubscriberByEmail(user.primary_email)
|
||||
},
|
||||
|
||||
async setWaitlistsJoined (options) {
|
||||
const { user, updatedWaitlistsJoined } = options
|
||||
await knex('subscribers')
|
||||
.where('id', user.id)
|
||||
.update({
|
||||
waitlists_joined: updatedWaitlistsJoined
|
||||
})
|
||||
return this.getSubscriberByEmail(user.primary_email)
|
||||
},
|
||||
|
||||
async removeSubscriber (subscriber) {
|
||||
await knex('email_addresses').where({ subscriber_id: subscriber.id }).del()
|
||||
await knex('subscribers').where({ id: subscriber.id }).del()
|
||||
},
|
||||
|
||||
// This is used by SES callbacks to remove email addresses when recipients
|
||||
// perma-bounce or mark our emails as spam
|
||||
// Removes from either subscribers or email_addresses as necessary
|
||||
async removeEmail(email) {
|
||||
const subscriber = await this.getSubscriberByEmail(email);
|
||||
async removeEmail (email) {
|
||||
const subscriber = await this.getSubscriberByEmail(email)
|
||||
if (!subscriber) {
|
||||
const emailAddress = await this.getEmailAddressRecordByEmail(email);
|
||||
const emailAddress = await this.getEmailAddressRecordByEmail(email)
|
||||
if (!emailAddress) {
|
||||
log.warn("removed-subscriber-not-found");
|
||||
return;
|
||||
log.warn('removed-subscriber-not-found')
|
||||
return
|
||||
}
|
||||
await knex("email_addresses")
|
||||
await knex('email_addresses')
|
||||
.where({
|
||||
"email": email,
|
||||
"verified": true,
|
||||
email,
|
||||
verified: true
|
||||
})
|
||||
.del();
|
||||
return;
|
||||
.del()
|
||||
return
|
||||
}
|
||||
// This can fail if a subscriber has more email_addresses and marks
|
||||
// a primary email as spam, but we should let it fail so we can see it
|
||||
// in the logs
|
||||
await knex("subscribers")
|
||||
await knex('subscribers')
|
||||
.where({
|
||||
"primary_verification_token": subscriber.primary_verification_token,
|
||||
"primary_sha1": subscriber.primary_sha1,
|
||||
primary_verification_token: subscriber.primary_verification_token,
|
||||
primary_sha1: subscriber.primary_sha1
|
||||
})
|
||||
.del();
|
||||
return;
|
||||
.del()
|
||||
},
|
||||
|
||||
async removeSubscriberByToken(token, emailSha1) {
|
||||
const subscriber = await this.getSubscriberByTokenAndHash(token, emailSha1);
|
||||
async removeSubscriberByToken (token, emailSha1) {
|
||||
const subscriber = await this.getSubscriberByTokenAndHash(token, emailSha1)
|
||||
if (!subscriber) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
await knex("subscribers")
|
||||
await knex('subscribers')
|
||||
.where({
|
||||
"primary_verification_token": subscriber.primary_verification_token,
|
||||
"primary_sha1": subscriber.primary_sha1,
|
||||
primary_verification_token: subscriber.primary_verification_token,
|
||||
primary_sha1: subscriber.primary_sha1
|
||||
})
|
||||
.del();
|
||||
return subscriber;
|
||||
.del()
|
||||
return subscriber
|
||||
},
|
||||
|
||||
async removeOneSecondaryEmail(emailId) {
|
||||
await knex("email_addresses")
|
||||
async removeOneSecondaryEmail (emailId) {
|
||||
await knex('email_addresses')
|
||||
.where({
|
||||
"id": emailId,
|
||||
id: emailId
|
||||
})
|
||||
.del();
|
||||
return;
|
||||
.del()
|
||||
},
|
||||
|
||||
async getSubscribersByHashes(hashes) {
|
||||
return await knex("subscribers").whereIn("primary_sha1", hashes).andWhere("primary_verified", "=", true);
|
||||
async getSubscribersByHashes (hashes) {
|
||||
return await knex('subscribers').whereIn('primary_sha1', hashes).andWhere('primary_verified', '=', true)
|
||||
},
|
||||
|
||||
async getEmailAddressesByHashes(hashes) {
|
||||
return await knex("email_addresses")
|
||||
.join("subscribers", "email_addresses.subscriber_id", "=", "subscribers.id")
|
||||
.whereIn("email_addresses.sha1", hashes)
|
||||
.andWhere("email_addresses.verified", "=", true);
|
||||
async getEmailAddressesByHashes (hashes) {
|
||||
return await knex('email_addresses')
|
||||
.join('subscribers', 'email_addresses.subscriber_id', '=', 'subscribers.id')
|
||||
.whereIn('email_addresses.sha1', hashes)
|
||||
.andWhere('email_addresses.verified', '=', true)
|
||||
},
|
||||
|
||||
async deleteUnverifiedSubscribers() {
|
||||
const expiredDateTime = new Date(Date.now() - AppConstants.DELETE_UNVERIFIED_SUBSCRIBERS_TIMER * 1000);
|
||||
const expiredTimeStamp = expiredDateTime.toISOString();
|
||||
const numDeleted = await knex("subscribers")
|
||||
.where("primary_verified", false)
|
||||
.andWhere("created_at", "<", expiredTimeStamp)
|
||||
.del();
|
||||
log.info("deleteUnverifiedSubscribers", {msg:`Deleted ${numDeleted} rows.`});
|
||||
async deleteUnverifiedSubscribers () {
|
||||
const expiredDateTime = new Date(Date.now() - AppConstants.DELETE_UNVERIFIED_SUBSCRIBERS_TIMER * 1000)
|
||||
const expiredTimeStamp = expiredDateTime.toISOString()
|
||||
const numDeleted = await knex('subscribers')
|
||||
.where('primary_verified', false)
|
||||
.andWhere('created_at', '<', expiredTimeStamp)
|
||||
.del()
|
||||
log.info('deleteUnverifiedSubscribers', { msg: `Deleted ${numDeleted} rows.` })
|
||||
},
|
||||
|
||||
async deleteSubscriberByFxAUID(fxaUID) {
|
||||
await knex("subscribers").where("fxa_uid", fxaUID).del();
|
||||
async deleteSubscriberByFxAUID (fxaUID) {
|
||||
await knex('subscribers').where('fxa_uid', fxaUID).del()
|
||||
},
|
||||
|
||||
async deleteEmailAddressesByUid(uid) {
|
||||
await knex("email_addresses").where({"subscriber_id": uid}).del();
|
||||
async deleteEmailAddressesByUid (uid) {
|
||||
await knex('email_addresses').where({ subscriber_id: uid }).del()
|
||||
},
|
||||
|
||||
async createConnection() {
|
||||
async createConnection () {
|
||||
if (knex === null) {
|
||||
knex = Knex(knexConfig);
|
||||
knex = Knex(knexConfig)
|
||||
}
|
||||
},
|
||||
|
||||
async destroyConnection() {
|
||||
await knex.destroy();
|
||||
knex = null;
|
||||
},
|
||||
async destroyConnection () {
|
||||
await knex.destroy()
|
||||
knex = null
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = DB;
|
||||
module.exports = DB
|
||||
|
|
|
@ -1,31 +1,29 @@
|
|||
"use strict";
|
||||
|
||||
'use strict'
|
||||
|
||||
// eslint-disable-next-line node/no-extraneous-require
|
||||
const { parse } = require("pg-connection-string");
|
||||
const { parse } = require('pg-connection-string')
|
||||
|
||||
const AppConstants = require("../app-constants");
|
||||
const connectionObj = parse(AppConstants.DATABASE_URL);
|
||||
if (AppConstants.NODE_ENV === "heroku") {
|
||||
connectionObj.ssl = {rejectUnauthorized: false};
|
||||
const AppConstants = require('../app-constants')
|
||||
const connectionObj = parse(AppConstants.DATABASE_URL)
|
||||
if (AppConstants.NODE_ENV === 'heroku') {
|
||||
connectionObj.ssl = { rejectUnauthorized: false }
|
||||
}
|
||||
|
||||
|
||||
// For runtime, use DATABASE_URL
|
||||
const RUNTIME_CONFIG = {
|
||||
client: "postgresql",
|
||||
connection: connectionObj,
|
||||
};
|
||||
client: 'postgresql',
|
||||
connection: connectionObj
|
||||
}
|
||||
|
||||
// For tests, use test-DATABASE
|
||||
const testConnectionObj = parse(AppConstants.DATABASE_URL.replace(/\/(\w*)$/, "/test-$1"));
|
||||
const testConnectionObj = parse(AppConstants.DATABASE_URL.replace(/\/(\w*)$/, '/test-$1'))
|
||||
const TESTS_CONFIG = {
|
||||
client: "postgresql",
|
||||
connection: testConnectionObj,
|
||||
};
|
||||
|
||||
if (AppConstants.NODE_ENV === "tests" ) {
|
||||
module.exports = TESTS_CONFIG;
|
||||
} else {
|
||||
module.exports = RUNTIME_CONFIG;
|
||||
client: 'postgresql',
|
||||
connection: testConnectionObj
|
||||
}
|
||||
|
||||
if (AppConstants.NODE_ENV === 'tests') {
|
||||
module.exports = TESTS_CONFIG
|
||||
} else {
|
||||
module.exports = RUNTIME_CONFIG
|
||||
}
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
exports.up = knex => {
|
||||
return knex.schema
|
||||
.createTable("subscribers", table => {
|
||||
table.increments("id").primary();
|
||||
table.string("sha1");
|
||||
table.string("email");
|
||||
table.string("verification_token").unique();
|
||||
table.boolean("verified").defaultTo(false);
|
||||
});
|
||||
};
|
||||
.createTable('subscribers', table => {
|
||||
table.increments('id').primary()
|
||||
table.string('sha1')
|
||||
table.string('email')
|
||||
table.string('verification_token').unique()
|
||||
table.boolean('verified').defaultTo(false)
|
||||
})
|
||||
}
|
||||
|
||||
exports.down = knex => {
|
||||
return knex.schema
|
||||
.dropTableIfExists("subscribers");
|
||||
};
|
||||
.dropTableIfExists('subscribers')
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
exports.up = function(knex, Promise) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.timestamps(false, true);
|
||||
});
|
||||
};
|
||||
exports.up = function (knex, Promise) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.timestamps(false, true)
|
||||
})
|
||||
}
|
||||
|
||||
exports.down = function(knex, Promise) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.dropTimestamps();
|
||||
});
|
||||
};
|
||||
exports.down = function (knex, Promise) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.dropTimestamps()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
exports.up = function(knex, Promise) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.boolean("fx_newsletter").defaultTo(false);
|
||||
});
|
||||
};
|
||||
exports.up = function (knex, Promise) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.boolean('fx_newsletter').defaultTo(false)
|
||||
})
|
||||
}
|
||||
|
||||
exports.down = function(knex, Promise) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.dropColumn("fx_newsletter");
|
||||
});
|
||||
};
|
||||
exports.down = function (knex, Promise) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.dropColumn('fx_newsletter')
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
exports.up = function(knex, Promise) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.string("signup_language");
|
||||
});
|
||||
};
|
||||
exports.up = function (knex, Promise) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.string('signup_language')
|
||||
})
|
||||
}
|
||||
|
||||
exports.down = function(knex, Promise) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.dropColumn("signup_language");
|
||||
});
|
||||
};
|
||||
exports.down = function (knex, Promise) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.dropColumn('signup_language')
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
"use strict";
|
||||
|
||||
'use strict'
|
||||
|
||||
// Note: this index was created on heroku, stage, and prod by hand
|
||||
// Use this statement to "fake" the migration:
|
||||
// INSERT INTO knex_migrations (name, batch, migration_time) values ('20181007085241_add_sha1_index.js', 4, '2018-10-07 08:52:42.000-05');
|
||||
exports.up = function(knex, Promise) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.index("sha1", "subscribers_sha1_idx");
|
||||
});
|
||||
};
|
||||
exports.up = function (knex, Promise) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.index('sha1', 'subscribers_sha1_idx')
|
||||
})
|
||||
}
|
||||
|
||||
exports.down = function(knex, Promise) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.dropIndex("sha1", "subscribers_sha1_idx");
|
||||
});
|
||||
};
|
||||
exports.down = function (knex, Promise) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.dropIndex('sha1', 'subscribers_sha1_idx')
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
exports.up = function(knex, Promise) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.index("created_at");
|
||||
});
|
||||
};
|
||||
exports.up = function (knex, Promise) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.index('created_at')
|
||||
})
|
||||
}
|
||||
|
||||
exports.down = function(knex, Promise) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.dropIndex("created_at");
|
||||
});
|
||||
};
|
||||
exports.down = function (knex, Promise) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.dropIndex('created_at')
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
exports.up = function(knex, Promise) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.index("email", "subscribers_email_idx");
|
||||
});
|
||||
};
|
||||
exports.up = function (knex, Promise) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.index('email', 'subscribers_email_idx')
|
||||
})
|
||||
}
|
||||
|
||||
exports.down = function(knex, Promise) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.dropIndex("email", "subscribers_email_idx");
|
||||
});
|
||||
};
|
||||
exports.down = function (knex, Promise) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.dropIndex('email', 'subscribers_email_idx')
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
exports.up = function(knex, Promise) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.string("fxa_refresh_token");
|
||||
table.jsonb("fxa_profile_json");
|
||||
});
|
||||
};
|
||||
exports.up = function (knex, Promise) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.string('fxa_refresh_token')
|
||||
table.jsonb('fxa_profile_json')
|
||||
})
|
||||
}
|
||||
|
||||
exports.down = function(knex, Promise) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.dropColumn("fxa_refresh_token");
|
||||
table.dropColumn("fxa_profile_json");
|
||||
});
|
||||
};
|
||||
exports.down = function (knex, Promise) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.dropColumn('fxa_refresh_token')
|
||||
table.dropColumn('fxa_profile_json')
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
exports.up = function (knex, Promise) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.index('verified', 'subscribers_verified_idx')
|
||||
})
|
||||
}
|
||||
|
||||
exports.up = function(knex, Promise) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.index("verified", "subscribers_verified_idx");
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex, Promise) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.dropIndex("verified", "subscribers_verified_idx");
|
||||
});
|
||||
};
|
||||
exports.down = function (knex, Promise) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.dropIndex('verified', 'subscribers_verified_idx')
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
exports.up = function(knex, Promise) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.string("fxa_uid");
|
||||
});
|
||||
};
|
||||
exports.up = function (knex, Promise) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.string('fxa_uid')
|
||||
})
|
||||
}
|
||||
|
||||
exports.down = function(knex, Promise) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.dropColumn("fxa_uid");
|
||||
});
|
||||
};
|
||||
exports.down = function (knex, Promise) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.dropColumn('fxa_uid')
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,34 +1,34 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
exports.up = function(knex) {
|
||||
exports.up = function (knex) {
|
||||
return Promise.all([
|
||||
knex.schema.createTable("email_addresses", table => {
|
||||
table.increments("id").primary();
|
||||
table.integer("subscriber_id").references("subscribers.id").notNullable();
|
||||
table.string("sha1");
|
||||
table.string("email");
|
||||
table.string("verification_token").unique();
|
||||
table.boolean("verified").defaultTo(false);
|
||||
knex.schema.createTable('email_addresses', table => {
|
||||
table.increments('id').primary()
|
||||
table.integer('subscriber_id').references('subscribers.id').notNullable()
|
||||
table.string('sha1')
|
||||
table.string('email')
|
||||
table.string('verification_token').unique()
|
||||
table.boolean('verified').defaultTo(false)
|
||||
}),
|
||||
|
||||
knex.schema.alterTable("subscribers", table => {
|
||||
table.renameColumn("sha1", "primary_sha1");
|
||||
table.renameColumn("email", "primary_email");
|
||||
table.renameColumn("verification_token", "primary_verification_token");
|
||||
table.renameColumn("verified", "primary_verified");
|
||||
}),
|
||||
]);
|
||||
};
|
||||
knex.schema.alterTable('subscribers', table => {
|
||||
table.renameColumn('sha1', 'primary_sha1')
|
||||
table.renameColumn('email', 'primary_email')
|
||||
table.renameColumn('verification_token', 'primary_verification_token')
|
||||
table.renameColumn('verified', 'primary_verified')
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
exports.down = function(knex) {
|
||||
exports.down = function (knex) {
|
||||
return Promise.all([
|
||||
knex.schema.dropTableIfExists("email_addresses"),
|
||||
knex.schema.dropTableIfExists('email_addresses'),
|
||||
|
||||
knex.schema.alterTable("subscribers", table => {
|
||||
table.renameColumn("primary_sha1", "sha1");
|
||||
table.renameColumn("primary_email", "email");
|
||||
table.renameColumn("primary_verification_token", "verification_token");
|
||||
table.renameColumn("primary_verified", "verified");
|
||||
}),
|
||||
]);
|
||||
};
|
||||
knex.schema.alterTable('subscribers', table => {
|
||||
table.renameColumn('primary_sha1', 'sha1')
|
||||
table.renameColumn('primary_email', 'email')
|
||||
table.renameColumn('primary_verification_token', 'verification_token')
|
||||
table.renameColumn('primary_verified', 'verified')
|
||||
})
|
||||
])
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
exports.up = function(knex, Promise) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.timestamp("breaches_last_shown");
|
||||
});
|
||||
};
|
||||
exports.up = function (knex, Promise) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.timestamp('breaches_last_shown')
|
||||
})
|
||||
}
|
||||
|
||||
exports.down = function(knex, Promise) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.dropColumn("breaches_last_shown");
|
||||
});
|
||||
};
|
||||
exports.down = function (knex, Promise) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.dropColumn('breaches_last_shown')
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
exports.up = function(knex, Promise) {
|
||||
return knex.schema.table("email_addresses", table => {
|
||||
table.timestamps(false);
|
||||
});
|
||||
};
|
||||
exports.up = function (knex, Promise) {
|
||||
return knex.schema.table('email_addresses', table => {
|
||||
table.timestamps(false)
|
||||
})
|
||||
}
|
||||
|
||||
exports.down = function(knex, Promise) {
|
||||
return knex.schema.table("email_addresses", table => {
|
||||
table.dropTimestamps();
|
||||
});
|
||||
};
|
||||
exports.down = function (knex, Promise) {
|
||||
return knex.schema.table('email_addresses', table => {
|
||||
table.dropTimestamps()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
exports.up = function (knex, Promise) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.boolean('all_emails_to_primary').defaultTo(false)
|
||||
})
|
||||
}
|
||||
|
||||
exports.up = function(knex, Promise) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.boolean("all_emails_to_primary").defaultTo(false);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex, Promise) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.dropColumn("all_emails_to_primary");
|
||||
});
|
||||
};
|
||||
exports.down = function (knex, Promise) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.dropColumn('all_emails_to_primary')
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
exports.up = function(knex, Promise) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.string("fxa_access_token");
|
||||
});
|
||||
};
|
||||
exports.up = function (knex, Promise) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.string('fxa_access_token')
|
||||
})
|
||||
}
|
||||
|
||||
exports.down = function(knex, Promise) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.dropColumn("fxa_access_token");
|
||||
});
|
||||
};
|
||||
exports.down = function (knex, Promise) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.dropColumn('fxa_access_token')
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
"use strict";
|
||||
|
||||
'use strict'
|
||||
|
||||
// Note: this index was created on heroku, stage, and prod by hand
|
||||
// Use this statement to "fake" the migration:
|
||||
// INSERT INTO knex_migrations (name, batch, migration_time) values ('20190713193852_add_email_sha1_index.js', (SELECT max(batch) + 1 FROM knex_migrations), '2019-07-13 19:52:42.000-05');
|
||||
exports.up = function(knex, Promise) {
|
||||
return knex.schema.table("email_addresses", table => {
|
||||
table.index("sha1", "email_addresses_sha1_idx");
|
||||
});
|
||||
};
|
||||
exports.up = function (knex, Promise) {
|
||||
return knex.schema.table('email_addresses', table => {
|
||||
table.index('sha1', 'email_addresses_sha1_idx')
|
||||
})
|
||||
}
|
||||
|
||||
exports.down = function(knex, Promise) {
|
||||
return knex.schema.table("email_addresses", table => {
|
||||
table.dropIndex("sha1", "email_addresses_sha1_idx");
|
||||
});
|
||||
};
|
||||
exports.down = function (knex, Promise) {
|
||||
return knex.schema.table('email_addresses', table => {
|
||||
table.dropIndex('sha1', 'email_addresses_sha1_idx')
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
"use strict";
|
||||
|
||||
'use strict'
|
||||
|
||||
// Note: this index was created on stage and prod by hand
|
||||
// Use this statement to "fake" the migration:
|
||||
// INSERT INTO knex_migrations (name, batch, migration_time) values ('20191118100718_add-fxa-uid-index.js', (SELECT max(batch) + 1 FROM knex_migrations), '2019-11-18 11:00:00.000-05');
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.index("fxa_uid", "subscribers_fxa_uid_idx");
|
||||
});
|
||||
};
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.index('fxa_uid', 'subscribers_fxa_uid_idx')
|
||||
})
|
||||
}
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.dropIndex("fxa_uid", "subscribers_fxa_uid_idx");
|
||||
});
|
||||
};
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.dropIndex('fxa_uid', 'subscribers_fxa_uid_idx')
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.table('email_addresses', table => {
|
||||
table.index('email', 'email_addresses_email_idx')
|
||||
})
|
||||
}
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.table("email_addresses", table => {
|
||||
table.index("email", "email_addresses_email_idx");
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.table("email_addresses", table => {
|
||||
table.dropIndex("email", "email_addresses_email_idx");
|
||||
});
|
||||
};
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.table('email_addresses', table => {
|
||||
table.dropIndex('email', 'email_addresses_email_idx')
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.jsonb("breaches_resolved");
|
||||
});
|
||||
};
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.jsonb('breaches_resolved')
|
||||
})
|
||||
}
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.dropColumn("breaches_resolved");
|
||||
});
|
||||
};
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.dropColumn('breaches_resolved')
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.jsonb("waitlists_joined");
|
||||
});
|
||||
};
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.jsonb('waitlists_joined')
|
||||
})
|
||||
}
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.dropColumn("waitlists_joined");
|
||||
});
|
||||
};
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.dropColumn('waitlists_joined')
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.index('breaches_last_shown', 'subscribers_breaches_last_shown_idx')
|
||||
})
|
||||
}
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.index("breaches_last_shown", "subscribers_breaches_last_shown_idx");
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.table("email_addresses", table => {
|
||||
table.dropIndex("breaches_last_shown", "subscribers_breaches_last_shown_idx");
|
||||
});
|
||||
};
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.table('email_addresses', table => {
|
||||
table.dropIndex('breaches_last_shown', 'subscribers_breaches_last_shown_idx')
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.table('subscribers', table => {
|
||||
table.index('signup_language', 'subscribers_signup_language_idx')
|
||||
})
|
||||
}
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.table("subscribers", table => {
|
||||
table.index("signup_language", "subscribers_signup_language_idx");
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.table("email_addresses", table => {
|
||||
table.dropIndex("signup_language", "subscribers_signup_language_idx");
|
||||
});
|
||||
};
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.table('email_addresses', table => {
|
||||
table.dropIndex('signup_language', 'subscribers_signup_language_idx')
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,70 +1,69 @@
|
|||
"use strict";
|
||||
|
||||
const getSha1 = require("../../sha1-utils");
|
||||
'use strict'
|
||||
|
||||
const getSha1 = require('../../sha1-utils')
|
||||
|
||||
exports.TEST_SUBSCRIBERS = {
|
||||
firefox_account: {
|
||||
id: 12345,
|
||||
primary_sha1: getSha1("firefoxaccount@test.com"),
|
||||
primary_email: "firefoxaccount@test.com",
|
||||
primary_verification_token: "0e2cb147-2041-4e5b-8ca9-494e773b2cf1",
|
||||
primary_sha1: getSha1('firefoxaccount@test.com'),
|
||||
primary_email: 'firefoxaccount@test.com',
|
||||
primary_verification_token: '0e2cb147-2041-4e5b-8ca9-494e773b2cf1',
|
||||
primary_verified: true,
|
||||
fxa_access_token: "4a4792b89434153f1a6262fbd6a4510c00834ff842585fc4f4d972da158f0fc0",
|
||||
fxa_refresh_token: "4a4792b89434153f1a6262fbd6a4510c00834ff842585fc4f4d972da158f0fc1",
|
||||
fxa_access_token: '4a4792b89434153f1a6262fbd6a4510c00834ff842585fc4f4d972da158f0fc0',
|
||||
fxa_refresh_token: '4a4792b89434153f1a6262fbd6a4510c00834ff842585fc4f4d972da158f0fc1',
|
||||
fxa_uid: 12345,
|
||||
fxa_profile_json: {},
|
||||
breaches_last_shown: "2019-04-24 13:27:08.421-05",
|
||||
breaches_resolved: {"firefoxaccount@test.com": [0]},
|
||||
breaches_last_shown: '2019-04-24 13:27:08.421-05',
|
||||
breaches_resolved: { 'firefoxaccount@test.com': [0] }
|
||||
},
|
||||
all_emails_to_primary: {
|
||||
id: 67890,
|
||||
primary_sha1: getSha1("all_emails_to_primary@test.com"),
|
||||
primary_email: "all_emails_to_primary@test.com",
|
||||
primary_verification_token: "0e2cb147-2041-4e5b-8ca9-494e773b2ca7",
|
||||
primary_sha1: getSha1('all_emails_to_primary@test.com'),
|
||||
primary_email: 'all_emails_to_primary@test.com',
|
||||
primary_verification_token: '0e2cb147-2041-4e5b-8ca9-494e773b2ca7',
|
||||
primary_verified: true,
|
||||
fxa_refresh_token: "4a4792b89434153f1a6262fbd6a4510c00834ff842585fc4f4d972da158f0fc2",
|
||||
breaches_last_shown: "2019-04-24 13:27:08.421-05",
|
||||
all_emails_to_primary: true,
|
||||
fxa_refresh_token: '4a4792b89434153f1a6262fbd6a4510c00834ff842585fc4f4d972da158f0fc2',
|
||||
breaches_last_shown: '2019-04-24 13:27:08.421-05',
|
||||
all_emails_to_primary: true
|
||||
},
|
||||
unverified_email: {
|
||||
primary_sha1: getSha1("unverifiedemail@test.com"),
|
||||
primary_email: "unverifiedemail@test.com",
|
||||
primary_verification_token: "0e2cb147-2041-4e5b-8ca9-494e773b2cf0",
|
||||
primary_verified: false,
|
||||
primary_sha1: getSha1('unverifiedemail@test.com'),
|
||||
primary_email: 'unverifiedemail@test.com',
|
||||
primary_verification_token: '0e2cb147-2041-4e5b-8ca9-494e773b2cf0',
|
||||
primary_verified: false
|
||||
},
|
||||
verified_email: {
|
||||
primary_sha1: getSha1("verifiedemail@test.com"),
|
||||
primary_email: "verifiedemail@test.com",
|
||||
primary_verification_token: "54010800-6c3c-4186-971a-76dc92874941",
|
||||
primary_sha1: getSha1('verifiedemail@test.com'),
|
||||
primary_email: 'verifiedemail@test.com',
|
||||
primary_verification_token: '54010800-6c3c-4186-971a-76dc92874941',
|
||||
primary_verified: true,
|
||||
signup_language: "en-US;q=0.7,en;q=0.3",
|
||||
},
|
||||
};
|
||||
signup_language: 'en-US;q=0.7,en;q=0.3'
|
||||
}
|
||||
}
|
||||
|
||||
exports.TEST_EMAIL_ADDRESSES = {
|
||||
firefox_account: {
|
||||
id: 11111,
|
||||
subscriber_id: 12345,
|
||||
sha1: getSha1("firefoxaccount-secondary@test.com"),
|
||||
email: "firefoxaccount-secondary@test.com",
|
||||
verification_token: "0e2cb147-2041-4e5b-8ca9-494e773b2cf2",
|
||||
verified: true,
|
||||
sha1: getSha1('firefoxaccount-secondary@test.com'),
|
||||
email: 'firefoxaccount-secondary@test.com',
|
||||
verification_token: '0e2cb147-2041-4e5b-8ca9-494e773b2cf2',
|
||||
verified: true
|
||||
},
|
||||
unverified_email_on_firefox_account: {
|
||||
id: 98765,
|
||||
subscriber_id: 12345,
|
||||
sha1: getSha1("firefoxaccount-tertiary@test.com"),
|
||||
email: "firefoxaccount-tertiary@test.com",
|
||||
verification_token: "0e2cb147-2041-4e5b-8ca9-494e773b2cf3",
|
||||
verified: false,
|
||||
sha1: getSha1('firefoxaccount-tertiary@test.com'),
|
||||
email: 'firefoxaccount-tertiary@test.com',
|
||||
verification_token: '0e2cb147-2041-4e5b-8ca9-494e773b2cf3',
|
||||
verified: false
|
||||
},
|
||||
all_emails_to_primary: {
|
||||
id: 99999,
|
||||
subscriber_id: 67890,
|
||||
sha1: getSha1("secondary_sending_to_primary@test.com"),
|
||||
email: "secondary_sending_to_primary@test.com",
|
||||
verification_token: "0e2cb147-2041-4e5b-8ca9-494e773b2cf4",
|
||||
verified: true,
|
||||
},
|
||||
};
|
||||
sha1: getSha1('secondary_sending_to_primary@test.com'),
|
||||
email: 'secondary_sending_to_primary@test.com',
|
||||
verification_token: '0e2cb147-2041-4e5b-8ca9-494e773b2cf4',
|
||||
verified: true
|
||||
}
|
||||
}
|
||||
|
|
143
email-utils.js
143
email-utils.js
|
@ -1,60 +1,57 @@
|
|||
"use strict";
|
||||
const { URL } = require("url");
|
||||
'use strict'
|
||||
const { URL } = require('url')
|
||||
|
||||
const AppConstants = require("./app-constants");
|
||||
const AppConstants = require('./app-constants')
|
||||
|
||||
const nodemailer = require("nodemailer");
|
||||
const hbs = require("nodemailer-express-handlebars");
|
||||
const nodemailer = require('nodemailer')
|
||||
const hbs = require('nodemailer-express-handlebars')
|
||||
|
||||
const HBSHelpers = require("./template-helpers/");
|
||||
const mozlog = require("./log");
|
||||
const HBSHelpers = require('./template-helpers/')
|
||||
const mozlog = require('./log')
|
||||
|
||||
|
||||
const log = mozlog("email-utils");
|
||||
const log = mozlog('email-utils')
|
||||
|
||||
const hbsOptions = {
|
||||
viewEngine: {
|
||||
extname: ".hbs",
|
||||
layoutsDir: __dirname + "/views/layouts",
|
||||
defaultLayout: "default_email",
|
||||
partialsDir: __dirname + "/views/partials",
|
||||
helpers: HBSHelpers.helpers,
|
||||
extname: '.hbs',
|
||||
layoutsDir: __dirname + '/views/layouts',
|
||||
defaultLayout: 'default_email',
|
||||
partialsDir: __dirname + '/views/partials',
|
||||
helpers: HBSHelpers.helpers
|
||||
},
|
||||
viewPath: __dirname + "/views/layouts",
|
||||
extName: ".hbs",
|
||||
};
|
||||
viewPath: __dirname + '/views/layouts',
|
||||
extName: '.hbs'
|
||||
}
|
||||
|
||||
// The SMTP transport object. This is initialized to a nodemailer transport
|
||||
// object while reading SMTP credentials, or to a dummy function in debug mode.
|
||||
let gTransporter;
|
||||
let gTransporter
|
||||
|
||||
const EmailUtils = {
|
||||
async init(smtpUrl = AppConstants.SMTP_URL) {
|
||||
|
||||
async init (smtpUrl = AppConstants.SMTP_URL) {
|
||||
// Allow a debug mode that will log JSON instead of sending emails.
|
||||
if (!smtpUrl) {
|
||||
log.info("smtpUrl-empty", { message: "EmailUtils will log a JSON response instead of sending emails." });
|
||||
gTransporter = nodemailer.createTransport({jsonTransport: true});
|
||||
return Promise.resolve(true);
|
||||
log.info('smtpUrl-empty', { message: 'EmailUtils will log a JSON response instead of sending emails.' })
|
||||
gTransporter = nodemailer.createTransport({ jsonTransport: true })
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
|
||||
gTransporter = nodemailer.createTransport(smtpUrl);
|
||||
const gTransporterVerification = await gTransporter.verify();
|
||||
gTransporter.use("compile", hbs(hbsOptions));
|
||||
return Promise.resolve(gTransporterVerification);
|
||||
gTransporter = nodemailer.createTransport(smtpUrl)
|
||||
const gTransporterVerification = await gTransporter.verify()
|
||||
gTransporter.use('compile', hbs(hbsOptions))
|
||||
return Promise.resolve(gTransporterVerification)
|
||||
},
|
||||
|
||||
|
||||
sendEmail(aRecipient, aSubject, aTemplate, aContext) {
|
||||
sendEmail (aRecipient, aSubject, aTemplate, aContext) {
|
||||
if (!gTransporter) {
|
||||
return Promise.reject("SMTP transport not initialized");
|
||||
return Promise.reject('SMTP transport not initialized')
|
||||
}
|
||||
|
||||
const emailContext = Object.assign({
|
||||
SERVER_URL: AppConstants.SERVER_URL,
|
||||
}, aContext);
|
||||
SERVER_URL: AppConstants.SERVER_URL
|
||||
}, aContext)
|
||||
return new Promise((resolve, reject) => {
|
||||
const emailFrom = AppConstants.EMAIL_FROM;
|
||||
const emailFrom = AppConstants.EMAIL_FROM
|
||||
const mailOptions = {
|
||||
from: emailFrom,
|
||||
to: aRecipient,
|
||||
|
@ -62,65 +59,65 @@ const EmailUtils = {
|
|||
template: aTemplate,
|
||||
context: emailContext,
|
||||
headers: {
|
||||
"x-ses-configuration-set": AppConstants.SES_CONFIG_SET,
|
||||
},
|
||||
};
|
||||
'x-ses-configuration-set': AppConstants.SES_CONFIG_SET
|
||||
}
|
||||
}
|
||||
|
||||
gTransporter.sendMail(mailOptions, (error, info) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
if (gTransporter.transporter.name === "JSONTransport") {
|
||||
log.info("JSONTransport", { message: info.message.toString() });
|
||||
if (gTransporter.transporter.name === 'JSONTransport') {
|
||||
log.info('JSONTransport', { message: info.message.toString() })
|
||||
}
|
||||
resolve(info);
|
||||
});
|
||||
});
|
||||
resolve(info)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
appendUtmParams(url, campaign, content) {
|
||||
appendUtmParams (url, campaign, content) {
|
||||
const utmParameters = {
|
||||
utm_source: "fx-monitor",
|
||||
utm_medium: "email",
|
||||
utm_source: 'fx-monitor',
|
||||
utm_medium: 'email',
|
||||
utm_campaign: campaign,
|
||||
utm_content: content,
|
||||
};
|
||||
utm_content: content
|
||||
}
|
||||
for (const param in utmParameters) {
|
||||
url.searchParams.append(param, utmParameters[param]);
|
||||
url.searchParams.append(param, utmParameters[param])
|
||||
}
|
||||
return url;
|
||||
return url
|
||||
},
|
||||
|
||||
getReportSubject(breaches, req) {
|
||||
getReportSubject (breaches, req) {
|
||||
if (breaches.length === 0) {
|
||||
return req.fluentFormat("email-subject-no-breaches");
|
||||
return req.fluentFormat('email-subject-no-breaches')
|
||||
}
|
||||
return req.fluentFormat("email-subject-found-breaches");
|
||||
return req.fluentFormat('email-subject-found-breaches')
|
||||
},
|
||||
|
||||
getEmailCtaHref(emailType, campaign, subscriberId=null) {
|
||||
const subscriberParamPath = (subscriberId) ? `/?subscriber_id=${subscriberId}` : "/";
|
||||
const url = new URL(subscriberParamPath, AppConstants.SERVER_URL);
|
||||
return this.appendUtmParams(url, campaign, emailType);
|
||||
getEmailCtaHref (emailType, campaign, subscriberId = null) {
|
||||
const subscriberParamPath = (subscriberId) ? `/?subscriber_id=${subscriberId}` : '/'
|
||||
const url = new URL(subscriberParamPath, AppConstants.SERVER_URL)
|
||||
return this.appendUtmParams(url, campaign, emailType)
|
||||
},
|
||||
|
||||
getVerificationUrl(subscriber) {
|
||||
let url = new URL(`${AppConstants.SERVER_URL}/user/verify`);
|
||||
url = this.appendUtmParams(url, "verified-subscribers", "account-verification-email");
|
||||
url.searchParams.append("token", encodeURIComponent(subscriber.verification_token));
|
||||
return url;
|
||||
getVerificationUrl (subscriber) {
|
||||
let url = new URL(`${AppConstants.SERVER_URL}/user/verify`)
|
||||
url = this.appendUtmParams(url, 'verified-subscribers', 'account-verification-email')
|
||||
url.searchParams.append('token', encodeURIComponent(subscriber.verification_token))
|
||||
return url
|
||||
},
|
||||
|
||||
getUnsubscribeUrl(subscriber, emailType) {
|
||||
let url = new URL(`${AppConstants.SERVER_URL}/user/unsubscribe`);
|
||||
const token = (subscriber.hasOwnProperty("verification_token")) ? subscriber.verification_token : subscriber.primary_verification_token;
|
||||
const hash = (subscriber.hasOwnProperty("sha1")) ? subscriber.sha1 : subscriber.primary_sha1;
|
||||
url.searchParams.append("token", encodeURIComponent(token));
|
||||
url.searchParams.append("hash", encodeURIComponent(hash));
|
||||
url = this.appendUtmParams(url, "unsubscribe", emailType);
|
||||
return url;
|
||||
},
|
||||
};
|
||||
getUnsubscribeUrl (subscriber, emailType) {
|
||||
let url = new URL(`${AppConstants.SERVER_URL}/user/unsubscribe`)
|
||||
const token = (subscriber.hasOwnProperty('verification_token')) ? subscriber.verification_token : subscriber.primary_verification_token
|
||||
const hash = (subscriber.hasOwnProperty('sha1')) ? subscriber.sha1 : subscriber.primary_sha1
|
||||
url.searchParams.append('token', encodeURIComponent(token))
|
||||
url.searchParams.append('hash', encodeURIComponent(hash))
|
||||
url = this.appendUtmParams(url, 'unsubscribe', emailType)
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = EmailUtils;
|
||||
module.exports = EmailUtils
|
||||
|
|
60
gulpfile.js
60
gulpfile.js
|
@ -1,51 +1,51 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const { src, watch, series, dest } = require("gulp");
|
||||
const sass = require("gulp-sass")(require("sass"));
|
||||
const del = require("del");
|
||||
const sourcemaps = require("gulp-sourcemaps");
|
||||
const { src, watch, series, dest } = require('gulp')
|
||||
const sass = require('gulp-sass')(require('sass'))
|
||||
const del = require('del')
|
||||
const sourcemaps = require('gulp-sourcemaps')
|
||||
|
||||
// directory for building SCSS, and bundles
|
||||
const buildDir = "./public/scss/libs/protocol/";
|
||||
const finalDir = "./public/css/";
|
||||
const buildDir = './public/scss/libs/protocol/'
|
||||
const finalDir = './public/css/'
|
||||
|
||||
const compiledCssDirectories = [
|
||||
"./public/css/*",
|
||||
"!./public/css/legacy/**",
|
||||
];
|
||||
'./public/css/*',
|
||||
'!./public/css/legacy/**'
|
||||
]
|
||||
|
||||
function cleanCompiledCssDirectory() {
|
||||
return del(compiledCssDirectories);
|
||||
function cleanCompiledCssDirectory () {
|
||||
return del(compiledCssDirectories)
|
||||
}
|
||||
|
||||
function resetCssDirectories() {
|
||||
return del(compiledCssDirectories.concat(buildDir));
|
||||
function resetCssDirectories () {
|
||||
return del(compiledCssDirectories.concat(buildDir))
|
||||
}
|
||||
|
||||
function styles() {
|
||||
return src("./public/scss/app.scss")
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(sass().on("error", sass.logError))
|
||||
.pipe(sourcemaps.write("."))
|
||||
.pipe(dest(finalDir));
|
||||
function styles () {
|
||||
return src('./public/scss/app.scss')
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(sass().on('error', sass.logError))
|
||||
.pipe(sourcemaps.write('.'))
|
||||
.pipe(dest(finalDir))
|
||||
}
|
||||
|
||||
function assetsCopyLegacy() {
|
||||
return src(["./public/scss/partials/legacy/**/*"]).pipe(dest("./public/css/legacy/"));
|
||||
function assetsCopyLegacy () {
|
||||
return src(['./public/scss/partials/legacy/**/*']).pipe(dest('./public/css/legacy/'))
|
||||
}
|
||||
|
||||
function watchCss() {
|
||||
return watch("./public/scss/**/*.scss", { ignoreInitial: false }, series(cleanCompiledCssDirectory, styles));
|
||||
function watchCss () {
|
||||
return watch('./public/scss/**/*.scss', { ignoreInitial: false }, series(cleanCompiledCssDirectory, styles))
|
||||
}
|
||||
|
||||
function assetsCopy() {
|
||||
return src(["./node_modules/@mozilla-protocol/core/protocol/**/*"]).pipe(dest(buildDir));
|
||||
function assetsCopy () {
|
||||
return src(['./node_modules/@mozilla-protocol/core/protocol/**/*']).pipe(dest(buildDir))
|
||||
}
|
||||
|
||||
exports.watchCss = watchCss;
|
||||
exports.watchCss = watchCss
|
||||
|
||||
exports.build = series(resetCssDirectories, assetsCopy, assetsCopyLegacy, styles);
|
||||
exports.build = series(resetCssDirectories, assetsCopy, assetsCopyLegacy, styles)
|
||||
|
||||
exports.default = series(
|
||||
cleanCompiledCssDirectory, assetsCopy, styles, watchCss
|
||||
);
|
||||
cleanCompiledCssDirectory, assetsCopy, styles, watchCss
|
||||
)
|
||||
|
|
204
hibp.js
204
hibp.js
|
@ -1,119 +1,115 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const got = require("got");
|
||||
const got = require('got')
|
||||
|
||||
const AppConstants = require("./app-constants");
|
||||
const { FluentError } = require("./locale-utils");
|
||||
const mozlog = require("./log");
|
||||
const pkg = require("./package.json");
|
||||
const AppConstants = require('./app-constants')
|
||||
const { FluentError } = require('./locale-utils')
|
||||
const mozlog = require('./log')
|
||||
const pkg = require('./package.json')
|
||||
|
||||
|
||||
const HIBP_USER_AGENT = `${pkg.name}/${pkg.version}`;
|
||||
const HIBP_USER_AGENT = `${pkg.name}/${pkg.version}`
|
||||
// When HIBP "re-names" a breach, it keeps its old 'Name' value but gets a new 'Title'
|
||||
// We use 'Name' in Firefox (via Remote Settings), so we have to maintain our own mapping of re-named breaches.
|
||||
const RENAMED_BREACHES = ["covve"];
|
||||
const RENAMED_BREACHES = ['covve']
|
||||
const RENAMED_BREACHES_MAP = {
|
||||
"covve": "db8151dd",
|
||||
};
|
||||
const log = mozlog("hibp");
|
||||
|
||||
|
||||
covve: 'db8151dd'
|
||||
}
|
||||
const log = mozlog('hibp')
|
||||
|
||||
const HIBP = {
|
||||
_addStandardOptions (options = {}) {
|
||||
const hibpOptions = {
|
||||
headers: {
|
||||
"User-Agent": HIBP_USER_AGENT,
|
||||
'User-Agent': HIBP_USER_AGENT
|
||||
},
|
||||
responseType: "json",
|
||||
};
|
||||
return Object.assign(options, hibpOptions);
|
||||
responseType: 'json'
|
||||
}
|
||||
return Object.assign(options, hibpOptions)
|
||||
},
|
||||
|
||||
async _throttledGot (url, reqOptions, tryCount = 1) {
|
||||
let response;
|
||||
let response
|
||||
try {
|
||||
response = await got(url, reqOptions);
|
||||
return response;
|
||||
response = await got(url, reqOptions)
|
||||
return response
|
||||
} catch (err) {
|
||||
log.error("_throttledGot", {err: err});
|
||||
log.error('_throttledGot', { err })
|
||||
if (err.statusCode === 404) {
|
||||
// 404 can mean "no results", return undefined response; sorry calling code
|
||||
return response;
|
||||
return response
|
||||
} else if (err.statusCode === 429) {
|
||||
log.info("_throttledGot", {err: "got a 429, tryCount: " + tryCount});
|
||||
log.info('_throttledGot', { err: 'got a 429, tryCount: ' + tryCount })
|
||||
if (tryCount >= AppConstants.HIBP_THROTTLE_MAX_TRIES) {
|
||||
log.error("_throttledGot", {err: err});
|
||||
throw new FluentError("error-hibp-throttled");
|
||||
log.error('_throttledGot', { err })
|
||||
throw new FluentError('error-hibp-throttled')
|
||||
} else {
|
||||
tryCount++;
|
||||
await new Promise(resolve => setTimeout(resolve, AppConstants.HIBP_THROTTLE_DELAY * tryCount));
|
||||
return await this._throttledGot(url, reqOptions, tryCount);
|
||||
tryCount++
|
||||
await new Promise(resolve => setTimeout(resolve, AppConstants.HIBP_THROTTLE_DELAY * tryCount))
|
||||
return await this._throttledGot(url, reqOptions, tryCount)
|
||||
}
|
||||
} else {
|
||||
throw new FluentError("error-hibp-connect");
|
||||
throw new FluentError('error-hibp-connect')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async req(path, options = {}) {
|
||||
const url = `${AppConstants.HIBP_API_ROOT}${path}`;
|
||||
const reqOptions = this._addStandardOptions(options);
|
||||
return await this._throttledGot(url, reqOptions);
|
||||
async req (path, options = {}) {
|
||||
const url = `${AppConstants.HIBP_API_ROOT}${path}`
|
||||
const reqOptions = this._addStandardOptions(options)
|
||||
return await this._throttledGot(url, reqOptions)
|
||||
},
|
||||
|
||||
async kAnonReq(path, options = {}) {
|
||||
async kAnonReq (path, options = {}) {
|
||||
// Construct HIBP url and standard headers
|
||||
const url = `${AppConstants.HIBP_KANON_API_ROOT}${path}?code=${encodeURIComponent(AppConstants.HIBP_KANON_API_TOKEN)}`;
|
||||
const reqOptions = this._addStandardOptions(options);
|
||||
return await this._throttledGot(url, reqOptions);
|
||||
const url = `${AppConstants.HIBP_KANON_API_ROOT}${path}?code=${encodeURIComponent(AppConstants.HIBP_KANON_API_TOKEN)}`
|
||||
const reqOptions = this._addStandardOptions(options)
|
||||
return await this._throttledGot(url, reqOptions)
|
||||
},
|
||||
|
||||
matchFluentID(dataCategory) {
|
||||
matchFluentID (dataCategory) {
|
||||
return dataCategory.toLowerCase()
|
||||
.replace(/[^-a-z0-9]/g, "-")
|
||||
.replace(/-{2,}/g, "-")
|
||||
.replace(/(^-|-$)/g, "");
|
||||
.replace(/[^-a-z0-9]/g, '-')
|
||||
.replace(/-{2,}/g, '-')
|
||||
.replace(/(^-|-$)/g, '')
|
||||
},
|
||||
|
||||
formatDataClassesArray(dataCategories) {
|
||||
const formattedArray = [];
|
||||
dataCategories.forEach(category => {
|
||||
formattedArray.push(this.matchFluentID(category));
|
||||
});
|
||||
return formattedArray;
|
||||
formatDataClassesArray (dataCategories) {
|
||||
const formattedArray = []
|
||||
dataCategories.forEach(category => {
|
||||
formattedArray.push(this.matchFluentID(category))
|
||||
})
|
||||
return formattedArray
|
||||
},
|
||||
|
||||
async loadBreachesIntoApp(app) {
|
||||
log.info("loadBreachesIntoApp");
|
||||
async loadBreachesIntoApp (app) {
|
||||
log.info('loadBreachesIntoApp')
|
||||
try {
|
||||
const breachesResponse = await this.req("/breaches");
|
||||
const breaches = [];
|
||||
const breachesResponse = await this.req('/breaches')
|
||||
const breaches = []
|
||||
|
||||
for (const breach of breachesResponse.body) {
|
||||
breach.DataClasses = this.formatDataClassesArray(breach.DataClasses);
|
||||
breach.LogoPath = /[^/]*$/.exec(breach.LogoPath)[0];
|
||||
breaches.push(breach);
|
||||
breach.DataClasses = this.formatDataClassesArray(breach.DataClasses)
|
||||
breach.LogoPath = /[^/]*$/.exec(breach.LogoPath)[0]
|
||||
breaches.push(breach)
|
||||
}
|
||||
app.locals.breaches = breaches;
|
||||
app.locals.breachesLoadedDateTime = Date.now();
|
||||
app.locals.latestBreach = this.getLatestBreach(breaches);
|
||||
app.locals.mostRecentBreachDateTime = app.locals.latestBreach.AddedDate;
|
||||
app.locals.breaches = breaches
|
||||
app.locals.breachesLoadedDateTime = Date.now()
|
||||
app.locals.latestBreach = this.getLatestBreach(breaches)
|
||||
app.locals.mostRecentBreachDateTime = app.locals.latestBreach.AddedDate
|
||||
} catch (error) {
|
||||
throw new FluentError("error-hibp-load-breaches");
|
||||
throw new FluentError('error-hibp-load-breaches')
|
||||
}
|
||||
log.info("done-loading-breaches");
|
||||
log.info('done-loading-breaches')
|
||||
},
|
||||
|
||||
async getBreachesForEmail (sha1, allBreaches, includeSensitive = false, filterBreaches = true) {
|
||||
let foundBreaches = []
|
||||
const sha1Prefix = sha1.slice(0, 6).toUpperCase()
|
||||
const path = `/breachedaccount/range/${sha1Prefix}`
|
||||
|
||||
async getBreachesForEmail(sha1, allBreaches, includeSensitive = false, filterBreaches = true) {
|
||||
let foundBreaches = [];
|
||||
const sha1Prefix = sha1.slice(0, 6).toUpperCase();
|
||||
const path = `/breachedaccount/range/${sha1Prefix}`;
|
||||
|
||||
const response = await this.kAnonReq(path);
|
||||
const response = await this.kAnonReq(path)
|
||||
if (!response) {
|
||||
return [];
|
||||
return []
|
||||
}
|
||||
// Parse response body, format:
|
||||
// [
|
||||
|
@ -122,79 +118,75 @@ const HIBP = {
|
|||
// ]
|
||||
for (const breachedAccount of response.body) {
|
||||
if (sha1.toUpperCase() === sha1Prefix + breachedAccount.hashSuffix) {
|
||||
foundBreaches = allBreaches.filter(breach => breachedAccount.websites.includes(breach.Name));
|
||||
foundBreaches = allBreaches.filter(breach => breachedAccount.websites.includes(breach.Name))
|
||||
if (filterBreaches) {
|
||||
foundBreaches = this.filterBreaches(foundBreaches);
|
||||
foundBreaches = this.filterBreaches(foundBreaches)
|
||||
}
|
||||
|
||||
// NOTE: DO NOT CHANGE THIS SORT LOGIC
|
||||
// We store breach resolutions by recency indices,
|
||||
// so that our DB does not contain any part of any user's list of accounts
|
||||
foundBreaches.sort( (a,b) => {
|
||||
return new Date(b.AddedDate) - new Date(a.AddedDate);
|
||||
});
|
||||
foundBreaches.sort((a, b) => {
|
||||
return new Date(b.AddedDate) - new Date(a.AddedDate)
|
||||
})
|
||||
|
||||
break;
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (includeSensitive) {
|
||||
return foundBreaches;
|
||||
return foundBreaches
|
||||
}
|
||||
return foundBreaches.filter(
|
||||
breach => !breach.IsSensitive
|
||||
);
|
||||
)
|
||||
},
|
||||
|
||||
|
||||
getBreachByName(allBreaches, breachName) {
|
||||
breachName = breachName.toLowerCase();
|
||||
getBreachByName (allBreaches, breachName) {
|
||||
breachName = breachName.toLowerCase()
|
||||
if (RENAMED_BREACHES.includes(breachName)) {
|
||||
breachName = RENAMED_BREACHES_MAP[breachName];
|
||||
breachName = RENAMED_BREACHES_MAP[breachName]
|
||||
}
|
||||
const foundBreach = allBreaches.find(breach => breach.Name.toLowerCase() === breachName);
|
||||
return foundBreach;
|
||||
const foundBreach = allBreaches.find(breach => breach.Name.toLowerCase() === breachName)
|
||||
return foundBreach
|
||||
},
|
||||
|
||||
|
||||
filterBreaches(breaches) {
|
||||
filterBreaches (breaches) {
|
||||
return breaches.filter(
|
||||
breach => !breach.IsRetired &&
|
||||
!breach.IsSpamList &&
|
||||
!breach.IsFabricated &&
|
||||
breach.IsVerified &&
|
||||
breach.Domain !== ""
|
||||
);
|
||||
breach.Domain !== ''
|
||||
)
|
||||
},
|
||||
|
||||
|
||||
getLatestBreach(breaches) {
|
||||
let latestBreach = {};
|
||||
let latestBreachDateTime = new Date(0);
|
||||
getLatestBreach (breaches) {
|
||||
let latestBreach = {}
|
||||
let latestBreachDateTime = new Date(0)
|
||||
for (const breach of breaches) {
|
||||
if (breach.IsSensitive) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
const breachAddedDate = new Date(breach.AddedDate);
|
||||
const breachAddedDate = new Date(breach.AddedDate)
|
||||
if (breachAddedDate > latestBreachDateTime) {
|
||||
latestBreachDateTime = breachAddedDate;
|
||||
latestBreach = breach;
|
||||
latestBreachDateTime = breachAddedDate
|
||||
latestBreach = breach
|
||||
}
|
||||
}
|
||||
return latestBreach;
|
||||
return latestBreach
|
||||
},
|
||||
|
||||
|
||||
async subscribeHash(sha1) {
|
||||
const sha1Prefix = sha1.slice(0, 6).toUpperCase();
|
||||
const path = "/range/subscribe";
|
||||
async subscribeHash (sha1) {
|
||||
const sha1Prefix = sha1.slice(0, 6).toUpperCase()
|
||||
const path = '/range/subscribe'
|
||||
const options = {
|
||||
method: "POST",
|
||||
json: {hashPrefix: sha1Prefix},
|
||||
};
|
||||
method: 'POST',
|
||||
json: { hashPrefix: sha1Prefix }
|
||||
}
|
||||
|
||||
return await this.kAnonReq(path, options);
|
||||
},
|
||||
};
|
||||
return await this.kAnonReq(path, options)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HIBP;
|
||||
module.exports = HIBP
|
||||
|
|
|
@ -1,54 +1,54 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const path = require("path");
|
||||
const reader = require("@maxmind/geoip2-node").Reader;
|
||||
const AppConstants = require("./app-constants");
|
||||
const path = require('path')
|
||||
const reader = require('@maxmind/geoip2-node').Reader
|
||||
const AppConstants = require('./app-constants')
|
||||
|
||||
let locationDb, timestamp;
|
||||
let locationDb, timestamp
|
||||
|
||||
async function openLocationDb() {
|
||||
if (locationDb && isFresh()) return console.warn("Location database already open.");
|
||||
async function openLocationDb () {
|
||||
if (locationDb && isFresh()) return console.warn('Location database already open.')
|
||||
|
||||
try {
|
||||
const dbPath = path.join(AppConstants.GEOIP_GEOLITE2_PATH, AppConstants.GEOIP_GEOLITE2_CITY_FILENAME);
|
||||
locationDb = await reader.open(dbPath);
|
||||
const dbPath = path.join(AppConstants.GEOIP_GEOLITE2_PATH, AppConstants.GEOIP_GEOLITE2_CITY_FILENAME)
|
||||
locationDb = await reader.open(dbPath)
|
||||
} catch (e) {
|
||||
return console.warn("Could not open location database:", e.message);
|
||||
return console.warn('Could not open location database:', e.message)
|
||||
}
|
||||
|
||||
timestamp = Date.now();
|
||||
return true;
|
||||
timestamp = Date.now()
|
||||
return true
|
||||
}
|
||||
|
||||
async function readLocationData(ip, locales) {
|
||||
let locationArr;
|
||||
async function readLocationData (ip, locales) {
|
||||
let locationArr
|
||||
|
||||
if (!isFresh()) await openLocationDb();
|
||||
if (!isFresh()) await openLocationDb()
|
||||
|
||||
try {
|
||||
const data = locationDb.city(ip);
|
||||
const countryName = data.country?.names[locales.find(locale => data.country?.names[locale])]; // find valid locale key and return its value
|
||||
const cityName = data.city?.names[locales.find(locale => data.city?.names[locale])];
|
||||
const subdivisionName = data.subdivisions?.[0].isoCode;
|
||||
const subdivisionFiltered = /[A-z]{2,}/.test(subdivisionName) ? subdivisionName : null; // return strings that are 2 or more letters, or null (avoid unfamiliar subdivisions like `E` or `09`)
|
||||
const data = locationDb.city(ip)
|
||||
const countryName = data.country?.names[locales.find(locale => data.country?.names[locale])] // find valid locale key and return its value
|
||||
const cityName = data.city?.names[locales.find(locale => data.city?.names[locale])]
|
||||
const subdivisionName = data.subdivisions?.[0].isoCode
|
||||
const subdivisionFiltered = /[A-z]{2,}/.test(subdivisionName) ? subdivisionName : null // return strings that are 2 or more letters, or null (avoid unfamiliar subdivisions like `E` or `09`)
|
||||
|
||||
locationArr = [cityName, subdivisionFiltered, countryName].filter(str => str); // [city name, state code, country code] with non-null items.
|
||||
locationArr = [cityName, subdivisionFiltered, countryName].filter(str => str) // [city name, state code, country code] with non-null items.
|
||||
} catch (e) {
|
||||
return console.warn("Could not read location from database:", e.message);
|
||||
return console.warn('Could not read location from database:', e.message)
|
||||
}
|
||||
|
||||
return {
|
||||
shortLocation: locationArr.slice(0, 2).join(", "), // shows the first two location values from the ones available
|
||||
fullLocation: locationArr.join(", "), // shows up to three location values from the ones available
|
||||
};
|
||||
shortLocation: locationArr.slice(0, 2).join(', '), // shows the first two location values from the ones available
|
||||
fullLocation: locationArr.join(', ') // shows up to three location values from the ones available
|
||||
}
|
||||
}
|
||||
|
||||
function isFresh() {
|
||||
if (Date.now() - timestamp < 259200000) return true; // 1000ms * 60s * 60m * 24h * 3 elapsed time is less than 24hrs
|
||||
return false;
|
||||
function isFresh () {
|
||||
if (Date.now() - timestamp < 259200000) return true // 1000ms * 60s * 60m * 24h * 3 elapsed time is less than 24hrs
|
||||
return false
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
openLocationDb,
|
||||
readLocationData,
|
||||
};
|
||||
readLocationData
|
||||
}
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const changePWLinks = {
|
||||
"MySpace": "https://myspace.com/settings/profile/password",
|
||||
"LinkedIn": "https://www.linkedin.com/psettings/change-password",
|
||||
"Dubsmash": "https://dubsmash.com/reset-password",
|
||||
"Canva": "https://www.canva.com/account",
|
||||
"MyFitnessPal": "https://www.myfitnesspal.com/account/change_password",
|
||||
"Adobe": "https://account.adobe.com/security",
|
||||
"Dropbox": "https://www.dropbox.com/account/security",
|
||||
"Houzz": "https://www.houzz.com/changePassword",
|
||||
"Evite": "https://www.evite.com/settings/password",
|
||||
"CafePress": "https://members.cafepress.com/account/profile.aspx",
|
||||
};
|
||||
MySpace: 'https://myspace.com/settings/profile/password',
|
||||
LinkedIn: 'https://www.linkedin.com/psettings/change-password',
|
||||
Dubsmash: 'https://dubsmash.com/reset-password',
|
||||
Canva: 'https://www.canva.com/account',
|
||||
MyFitnessPal: 'https://www.myfitnesspal.com/account/change_password',
|
||||
Adobe: 'https://account.adobe.com/security',
|
||||
Dropbox: 'https://www.dropbox.com/account/security',
|
||||
Houzz: 'https://www.houzz.com/changePassword',
|
||||
Evite: 'https://www.evite.com/settings/password',
|
||||
CafePress: 'https://members.cafepress.com/account/profile.aspx'
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
changePWLinks,
|
||||
};
|
||||
changePWLinks
|
||||
}
|
||||
|
|
118
lib/fxa.js
118
lib/fxa.js
|
@ -1,110 +1,108 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const ClientOAuth2 = require("client-oauth2");
|
||||
const got = require("got");
|
||||
const { URL } = require("url");
|
||||
const ClientOAuth2 = require('client-oauth2')
|
||||
const got = require('got')
|
||||
const { URL } = require('url')
|
||||
|
||||
const AppConstants = require("../app-constants");
|
||||
const mozlog = require("../log");
|
||||
const AppConstants = require('../app-constants')
|
||||
const mozlog = require('../log')
|
||||
|
||||
|
||||
const log = mozlog("fxa");
|
||||
const log = mozlog('fxa')
|
||||
|
||||
// This object exists instead of inlining the env vars to make it easy
|
||||
// to abstract fetching API endpoints from the OAuth server (instead
|
||||
// of specifying them in the environment) in the future.
|
||||
const FxAOAuthUtils = {
|
||||
get authorizationUri() { return AppConstants.OAUTH_AUTHORIZATION_URI; },
|
||||
get tokenUri() { return AppConstants.OAUTH_TOKEN_URI; },
|
||||
get profileUri() { return AppConstants.OAUTH_PROFILE_URI; },
|
||||
};
|
||||
get authorizationUri () { return AppConstants.OAUTH_AUTHORIZATION_URI },
|
||||
get tokenUri () { return AppConstants.OAUTH_TOKEN_URI },
|
||||
get profileUri () { return AppConstants.OAUTH_PROFILE_URI }
|
||||
}
|
||||
|
||||
const FxAOAuthClient = new ClientOAuth2({
|
||||
clientId: AppConstants.OAUTH_CLIENT_ID,
|
||||
clientSecret: AppConstants.OAUTH_CLIENT_SECRET,
|
||||
accessTokenUri: FxAOAuthUtils.tokenUri,
|
||||
authorizationUri: FxAOAuthUtils.authorizationUri,
|
||||
redirectUri: AppConstants.SERVER_URL + "/oauth/confirmed",
|
||||
scopes: ["profile"],
|
||||
});
|
||||
redirectUri: AppConstants.SERVER_URL + '/oauth/confirmed',
|
||||
scopes: ['profile']
|
||||
})
|
||||
|
||||
const FXA = {
|
||||
|
||||
async _postTokenRequest(path, token) {
|
||||
const fxaTokenOrigin = new URL(AppConstants.OAUTH_TOKEN_URI).origin;
|
||||
const tokenUrl = `${fxaTokenOrigin}${path}`;
|
||||
const tokenBody = (typeof token === "object") ? token : {token};
|
||||
async _postTokenRequest (path, token) {
|
||||
const fxaTokenOrigin = new URL(AppConstants.OAUTH_TOKEN_URI).origin
|
||||
const tokenUrl = `${fxaTokenOrigin}${path}`
|
||||
const tokenBody = (typeof token === 'object') ? token : { token }
|
||||
const tokenOptions = {
|
||||
method: "POST",
|
||||
method: 'POST',
|
||||
json: tokenBody,
|
||||
responseType: "json",
|
||||
};
|
||||
responseType: 'json'
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await got(tokenUrl, tokenOptions);
|
||||
return response;
|
||||
const response = await got(tokenUrl, tokenOptions)
|
||||
return response
|
||||
} catch (e) {
|
||||
log.error("_postTokenRequest", {stack: e.stack});
|
||||
return e;
|
||||
log.error('_postTokenRequest', { stack: e.stack })
|
||||
return e
|
||||
}
|
||||
},
|
||||
|
||||
async verifyOAuthToken(token) {
|
||||
async verifyOAuthToken (token) {
|
||||
try {
|
||||
const response = await this._postTokenRequest("/v1/verify", token);
|
||||
return response;
|
||||
const response = await this._postTokenRequest('/v1/verify', token)
|
||||
return response
|
||||
} catch (e) {
|
||||
log.error("verifyOAuthToken", {stack: e.stack});
|
||||
log.error('verifyOAuthToken', { stack: e.stack })
|
||||
}
|
||||
},
|
||||
|
||||
async destroyOAuthToken(token) {
|
||||
async destroyOAuthToken (token) {
|
||||
try {
|
||||
const response = await this._postTokenRequest("/v1/destroy", token);
|
||||
return response;
|
||||
const response = await this._postTokenRequest('/v1/destroy', token)
|
||||
return response
|
||||
} catch (e) {
|
||||
log.error("destroyOAuthToken", {stack: e.stack});
|
||||
log.error('destroyOAuthToken', { stack: e.stack })
|
||||
}
|
||||
},
|
||||
|
||||
async revokeOAuthTokens(subscriber) {
|
||||
await this.destroyOAuthToken({ token: subscriber.fxa_access_token });
|
||||
await this.destroyOAuthToken({ refresh_token: subscriber.fxa_refresh_token });
|
||||
async revokeOAuthTokens (subscriber) {
|
||||
await this.destroyOAuthToken({ token: subscriber.fxa_access_token })
|
||||
await this.destroyOAuthToken({ refresh_token: subscriber.fxa_refresh_token })
|
||||
},
|
||||
|
||||
async getProfileData(accessToken) {
|
||||
async getProfileData (accessToken) {
|
||||
try {
|
||||
const data = await got(FxAOAuthUtils.profileUri,
|
||||
{ headers: { Authorization: `Bearer ${accessToken}` } }
|
||||
);
|
||||
return data.body;
|
||||
)
|
||||
return data.body
|
||||
} catch (e) {
|
||||
log.warn("getProfileData", {stack: e.stack});
|
||||
return e;
|
||||
log.warn('getProfileData', { stack: e.stack })
|
||||
return e
|
||||
}
|
||||
},
|
||||
|
||||
async sendMetricsFlowPing(path) {
|
||||
const fxaMetricsFlowUrl = new URL(path, AppConstants.FXA_SETTINGS_URL);
|
||||
async sendMetricsFlowPing (path) {
|
||||
const fxaMetricsFlowUrl = new URL(path, AppConstants.FXA_SETTINGS_URL)
|
||||
const fxaMetricsFlowOptions = {
|
||||
method: "GET",
|
||||
headers: { Origin: AppConstants.SERVER_URL },
|
||||
};
|
||||
try {
|
||||
log.info(`GETting ${fxaMetricsFlowUrl}, with options: ${JSON.stringify(fxaMetricsFlowOptions)}`);
|
||||
const fxaResp = await got(fxaMetricsFlowUrl, fxaMetricsFlowOptions);
|
||||
log.info("pinged FXA metrics flow.");
|
||||
return fxaResp;
|
||||
} catch (e) {
|
||||
log.error("sendMetricsFlowPing", {stack: e.stack});
|
||||
return false;
|
||||
method: 'GET',
|
||||
headers: { Origin: AppConstants.SERVER_URL }
|
||||
}
|
||||
},
|
||||
|
||||
};
|
||||
try {
|
||||
log.info(`GETting ${fxaMetricsFlowUrl}, with options: ${JSON.stringify(fxaMetricsFlowOptions)}`)
|
||||
const fxaResp = await got(fxaMetricsFlowUrl, fxaMetricsFlowOptions)
|
||||
log.info('pinged FXA metrics flow.')
|
||||
return fxaResp
|
||||
} catch (e) {
|
||||
log.error('sendMetricsFlowPing', { stack: e.stack })
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
FXA,
|
||||
FxAOAuthClient,
|
||||
};
|
||||
FxAOAuthClient
|
||||
}
|
||||
|
|
|
@ -1,49 +1,47 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const got = require("got");
|
||||
const AppConstants = require("../app-constants");
|
||||
const got = require('got')
|
||||
const AppConstants = require('../app-constants')
|
||||
|
||||
|
||||
const BREACHES_COLLECTION = "fxmonitor-breaches";
|
||||
const FX_RS_COLLECTION = `${AppConstants.FX_REMOTE_SETTINGS_WRITER_SERVER}/buckets/main-workspace/collections/${BREACHES_COLLECTION}`;
|
||||
const FX_RS_RECORDS = `${FX_RS_COLLECTION}/records`;
|
||||
const FX_RS_WRITER_USER = AppConstants.FX_REMOTE_SETTINGS_WRITER_USER;
|
||||
const FX_RS_WRITER_PASS = AppConstants.FX_REMOTE_SETTINGS_WRITER_PASS;
|
||||
const BREACHES_COLLECTION = 'fxmonitor-breaches'
|
||||
const FX_RS_COLLECTION = `${AppConstants.FX_REMOTE_SETTINGS_WRITER_SERVER}/buckets/main-workspace/collections/${BREACHES_COLLECTION}`
|
||||
const FX_RS_RECORDS = `${FX_RS_COLLECTION}/records`
|
||||
const FX_RS_WRITER_USER = AppConstants.FX_REMOTE_SETTINGS_WRITER_USER
|
||||
const FX_RS_WRITER_PASS = AppConstants.FX_REMOTE_SETTINGS_WRITER_PASS
|
||||
|
||||
const RemoteSettings = {
|
||||
|
||||
async whichBreachesAreNotInRemoteSettingsYet(breaches) {
|
||||
async whichBreachesAreNotInRemoteSettingsYet (breaches) {
|
||||
const fxRSRecords = await got(FX_RS_RECORDS, {
|
||||
responseType: "json",
|
||||
responseType: 'json',
|
||||
username: FX_RS_WRITER_USER,
|
||||
password: FX_RS_WRITER_PASS,
|
||||
});
|
||||
password: FX_RS_WRITER_PASS
|
||||
})
|
||||
const remoteSettingsBreachesSet = new Set(
|
||||
fxRSRecords.body.data.map(b => b.Name)
|
||||
);
|
||||
)
|
||||
|
||||
return breaches.filter( ({Name}) => !remoteSettingsBreachesSet.has(Name) );
|
||||
return breaches.filter(({ Name }) => !remoteSettingsBreachesSet.has(Name))
|
||||
},
|
||||
|
||||
async postNewBreachToBreachesCollection(data) {
|
||||
async postNewBreachToBreachesCollection (data) {
|
||||
// Create the record
|
||||
return await got.post(FX_RS_RECORDS, {
|
||||
username: FX_RS_WRITER_USER,
|
||||
password: FX_RS_WRITER_PASS,
|
||||
json: { data },
|
||||
responseType: "json",
|
||||
});
|
||||
responseType: 'json'
|
||||
})
|
||||
},
|
||||
|
||||
async requestReviewOnBreachesCollection() {
|
||||
async requestReviewOnBreachesCollection () {
|
||||
return await got.patch(FX_RS_COLLECTION, {
|
||||
username: FX_RS_WRITER_USER,
|
||||
password: FX_RS_WRITER_PASS,
|
||||
json: { data: {status: "to-review"} },
|
||||
responseType: "json",
|
||||
});
|
||||
},
|
||||
};
|
||||
json: { data: { status: 'to-review' } },
|
||||
responseType: 'json'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = RemoteSettings;
|
||||
module.exports = RemoteSettings
|
||||
|
|
109
locale-utils.js
109
locale-utils.js
|
@ -1,109 +1,106 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
// node.js needs Intl.PluralRules polyfill
|
||||
require("intl-pluralrules");
|
||||
require('intl-pluralrules')
|
||||
|
||||
const { FluentBundle } = require("fluent");
|
||||
const { FluentBundle } = require('fluent')
|
||||
|
||||
const mozlog = require("./log");
|
||||
const {supportedLocales} = require("./package.json");
|
||||
const mozlog = require('./log')
|
||||
const { supportedLocales } = require('./package.json')
|
||||
|
||||
const log = mozlog('locale-utils')
|
||||
|
||||
const log = mozlog("locale-utils");
|
||||
|
||||
const localesDir = "locales";
|
||||
|
||||
const availableLanguages = [];
|
||||
const fluentBundles = {};
|
||||
const localesDir = 'locales'
|
||||
|
||||
const availableLanguages = []
|
||||
const fluentBundles = {}
|
||||
|
||||
class FluentError extends Error {
|
||||
constructor(fluentID = null, ...params) {
|
||||
super(...params);
|
||||
constructor (fluentID = null, ...params) {
|
||||
super(...params)
|
||||
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, FluentError);
|
||||
Error.captureStackTrace(this, FluentError)
|
||||
}
|
||||
|
||||
this.fluentID = fluentID;
|
||||
this.message = fluentID;
|
||||
this.fluentID = fluentID
|
||||
this.message = fluentID
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const LocaleUtils = {
|
||||
init() {
|
||||
let languageDirectories = supportedLocales.split(",");
|
||||
if (supportedLocales === "*") {
|
||||
languageDirectories = fs.readdirSync( localesDir ).filter(item => {
|
||||
return (!item.startsWith(".") && fs.lstatSync(path.join(localesDir, item)).isDirectory());
|
||||
});
|
||||
init () {
|
||||
let languageDirectories = supportedLocales.split(',')
|
||||
if (supportedLocales === '*') {
|
||||
languageDirectories = fs.readdirSync(localesDir).filter(item => {
|
||||
return (!item.startsWith('.') && fs.lstatSync(path.join(localesDir, item)).isDirectory())
|
||||
})
|
||||
}
|
||||
for (const lang of languageDirectories) {
|
||||
try {
|
||||
const langBundle = new FluentBundle(lang, {useIsolating: false});
|
||||
const langBundle = new FluentBundle(lang, { useIsolating: false })
|
||||
const ftlFiles = fs.readdirSync(path.join(localesDir, lang)).filter(item => {
|
||||
return (item.endsWith(".ftl"));
|
||||
});
|
||||
return (item.endsWith('.ftl'))
|
||||
})
|
||||
for (const file of ftlFiles) {
|
||||
const langFTLSource = fs.readFileSync(path.join(localesDir, lang, file), "utf8");
|
||||
langBundle.addMessages(langFTLSource);
|
||||
const langFTLSource = fs.readFileSync(path.join(localesDir, lang, file), 'utf8')
|
||||
langBundle.addMessages(langFTLSource)
|
||||
}
|
||||
fluentBundles[lang] = langBundle;
|
||||
availableLanguages.push(lang);
|
||||
fluentBundles[lang] = langBundle
|
||||
availableLanguages.push(lang)
|
||||
} catch (e) {
|
||||
log.error("loadFluentBundle", {stack: e.stack});
|
||||
log.error('loadFluentBundle', { stack: e.stack })
|
||||
}
|
||||
}
|
||||
log.info("LocaleUtils.init", {availableLanguages});
|
||||
log.info("LocaleUtils.init", {fluentBundles});
|
||||
return {availableLanguages, fluentBundles};
|
||||
log.info('LocaleUtils.init', { availableLanguages })
|
||||
log.info('LocaleUtils.init', { fluentBundles })
|
||||
return { availableLanguages, fluentBundles }
|
||||
},
|
||||
|
||||
loadLanguagesIntoApp (app) {
|
||||
app.locals.AVAILABLE_LANGUAGES = availableLanguages;
|
||||
app.locals.FLUENT_BUNDLES = fluentBundles;
|
||||
app.locals.AVAILABLE_LANGUAGES = availableLanguages
|
||||
app.locals.FLUENT_BUNDLES = fluentBundles
|
||||
},
|
||||
|
||||
fluentFormat (supportedLocales, id, args=null, errors=null) {
|
||||
fluentFormat (supportedLocales, id, args = null, errors = null) {
|
||||
for (const locale of supportedLocales) {
|
||||
const bundle = fluentBundles[locale];
|
||||
const bundle = fluentBundles[locale]
|
||||
if (bundle.hasMessage(id)) {
|
||||
const message = bundle.getMessage(id);
|
||||
return bundle.format(message, args);
|
||||
const message = bundle.getMessage(id)
|
||||
return bundle.format(message, args)
|
||||
}
|
||||
}
|
||||
return id;
|
||||
return id
|
||||
},
|
||||
|
||||
fluentFormatWithFallback (supportedLocales, id, fallbackId, args=null, errors=null) {
|
||||
fluentFormatWithFallback (supportedLocales, id, fallbackId, args = null, errors = null) {
|
||||
if (!fallbackId) {
|
||||
log.error("fluentFormatWithFallback: No fallbackId");
|
||||
return false;
|
||||
log.error('fluentFormatWithFallback: No fallbackId')
|
||||
return false
|
||||
}
|
||||
|
||||
for (const locale of supportedLocales) {
|
||||
const bundle = fluentBundles[locale];
|
||||
const bundle = fluentBundles[locale]
|
||||
if (bundle.hasMessage(id)) {
|
||||
const message = bundle.getMessage(id);
|
||||
return bundle.format(message, args);
|
||||
const message = bundle.getMessage(id)
|
||||
return bundle.format(message, args)
|
||||
}
|
||||
|
||||
// If first message id doesn't have translation, use fallback id
|
||||
if (fallbackId && bundle.hasMessage(fallbackId)) {
|
||||
const message = bundle.getMessage(fallbackId);
|
||||
return bundle.format(message, args);
|
||||
const message = bundle.getMessage(fallbackId)
|
||||
return bundle.format(message, args)
|
||||
}
|
||||
}
|
||||
return id;
|
||||
},
|
||||
return id
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
FluentError,
|
||||
LocaleUtils,
|
||||
};
|
||||
LocaleUtils
|
||||
}
|
||||
|
|
15
log.js
15
log.js
|
@ -1,14 +1,13 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const mozlog = require("mozlog");
|
||||
const mozlog = require('mozlog')
|
||||
|
||||
const AppConstants = require("./app-constants");
|
||||
const AppConstants = require('./app-constants')
|
||||
|
||||
const log = mozlog({
|
||||
app: "fx-monitor",
|
||||
app: 'fx-monitor',
|
||||
level: AppConstants.MOZLOG_LEVEL,
|
||||
fmt: AppConstants.MOZLOG_FMT,
|
||||
});
|
||||
fmt: AppConstants.MOZLOG_FMT
|
||||
})
|
||||
|
||||
|
||||
module.exports = log;
|
||||
module.exports = log
|
||||
|
|
247
middleware.js
247
middleware.js
|
@ -1,247 +1,234 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const { URLSearchParams } = require("url");
|
||||
const { URLSearchParams } = require('url')
|
||||
|
||||
const { negotiateLanguages, acceptedLanguages } = require("fluent-langneg");
|
||||
const Sentry = require("@sentry/node");
|
||||
const { negotiateLanguages, acceptedLanguages } = require('fluent-langneg')
|
||||
const Sentry = require('@sentry/node')
|
||||
|
||||
const AppConstants = require("./app-constants");
|
||||
const DB = require("./db/DB");
|
||||
const { FXA } = require("./lib/fxa");
|
||||
const { FluentError } = require("./locale-utils");
|
||||
const mozlog = require("./log");
|
||||
const AppConstants = require('./app-constants')
|
||||
const DB = require('./db/DB')
|
||||
const { FXA } = require('./lib/fxa')
|
||||
const { FluentError } = require('./locale-utils')
|
||||
const mozlog = require('./log')
|
||||
|
||||
const HIBP = require("./hibp");
|
||||
|
||||
const log = mozlog("middleware");
|
||||
const HIBP = require('./hibp')
|
||||
|
||||
const log = mozlog('middleware')
|
||||
|
||||
// adds the request object to a res.local var
|
||||
function addRequestToResponse (req, res, next) {
|
||||
res.locals.req = req;
|
||||
next();
|
||||
res.locals.req = req
|
||||
next()
|
||||
}
|
||||
|
||||
|
||||
// picks available language by Accept-Language and assigns to request
|
||||
function pickLanguage (req, res, next) {
|
||||
res.vary("Accept-Language");
|
||||
const requestedLanguage = acceptedLanguages(req.headers["accept-language"]);
|
||||
res.vary('Accept-Language')
|
||||
const requestedLanguage = acceptedLanguages(req.headers['accept-language'])
|
||||
const supportedLocales = negotiateLanguages(
|
||||
requestedLanguage,
|
||||
req.app.locals.AVAILABLE_LANGUAGES,
|
||||
{defaultLocale: "en"}
|
||||
);
|
||||
req.supportedLocales = supportedLocales;
|
||||
{ defaultLocale: 'en' }
|
||||
)
|
||||
req.supportedLocales = supportedLocales
|
||||
|
||||
req.fluentFormat = (id, args = null, errors = null) => {
|
||||
for (const locale of supportedLocales) {
|
||||
const bundle = req.app.locals.FLUENT_BUNDLES[locale];
|
||||
const bundle = req.app.locals.FLUENT_BUNDLES[locale]
|
||||
if (bundle.hasMessage(id)) {
|
||||
const message = bundle.getMessage(id);
|
||||
return bundle.format(message, args);
|
||||
const message = bundle.getMessage(id)
|
||||
return bundle.format(message, args)
|
||||
}
|
||||
}
|
||||
return id;
|
||||
};
|
||||
return id
|
||||
}
|
||||
|
||||
next();
|
||||
next()
|
||||
}
|
||||
|
||||
|
||||
async function recordVisitFromEmail (req, res, next) {
|
||||
if (req.query.utm_source && req.query.utm_source !== "fx-monitor") {
|
||||
next();
|
||||
return;
|
||||
if (req.query.utm_source && req.query.utm_source !== 'fx-monitor') {
|
||||
next()
|
||||
return
|
||||
}
|
||||
if (req.query.utm_medium && req.query.utm_medium !== "email") {
|
||||
next();
|
||||
return;
|
||||
if (req.query.utm_medium && req.query.utm_medium !== 'email') {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
const breachDetailsRE = /breach-details\/(\w*)$/;
|
||||
const capturedMatch = req.path.match(breachDetailsRE);
|
||||
const breachDetailsRE = /breach-details\/(\w*)$/
|
||||
const capturedMatch = req.path.match(breachDetailsRE)
|
||||
|
||||
// Send engagement ping to FXA if req.query contains a valid subscriber ID
|
||||
if (req.query.subscriber_id && Number.isInteger(Number(req.query.subscriber_id))) {
|
||||
const subscriber = await DB.getSubscriberById(req.query.subscriber_id);
|
||||
const subscriber = await DB.getSubscriberById(req.query.subscriber_id)
|
||||
|
||||
if (!subscriber.fxa_uid || subscriber.fxa_uid === "") {
|
||||
next();
|
||||
return;
|
||||
if (!subscriber.fxa_uid || subscriber.fxa_uid === '') {
|
||||
next()
|
||||
return
|
||||
}
|
||||
const utmContent = (capturedMatch) ? `&utm_content=${capturedMatch[1]}` : "";
|
||||
const fxaMetricsFlowPath = `metrics-flow?utm_source=${req.query.utm_source}&utm_medium=${req.query.utm_medium}${utmContent}&event_type=engage&uid=${subscriber.fxa_uid}&service=${AppConstants.OAUTH_CLIENT_ID}`;
|
||||
const fxaResult = await FXA.sendMetricsFlowPing(fxaMetricsFlowPath);
|
||||
log.info(`fxaResult: ${fxaResult}`);
|
||||
const utmContent = (capturedMatch) ? `&utm_content=${capturedMatch[1]}` : ''
|
||||
const fxaMetricsFlowPath = `metrics-flow?utm_source=${req.query.utm_source}&utm_medium=${req.query.utm_medium}${utmContent}&event_type=engage&uid=${subscriber.fxa_uid}&service=${AppConstants.OAUTH_CLIENT_ID}`
|
||||
const fxaResult = await FXA.sendMetricsFlowPing(fxaMetricsFlowPath)
|
||||
log.info(`fxaResult: ${fxaResult}`)
|
||||
}
|
||||
|
||||
// If user is already signed in, proceed
|
||||
if (req.session.user) {
|
||||
next();
|
||||
return;
|
||||
next()
|
||||
return
|
||||
}
|
||||
// If user is returning from FXA sign-in, proceed
|
||||
if (req.path === "/oauth/confirmed") {
|
||||
next();
|
||||
return;
|
||||
if (req.path === '/oauth/confirmed') {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// Redirect users who have clicked "Go to Dashboard" from an email and aren't signed in to the /oauth flow.
|
||||
if (
|
||||
req.query.utm_campaign && req.query.utm_campaign === "go-to-dashboard-link"
|
||||
) {
|
||||
const oauthUrl = new URL("/oauth/init", AppConstants.SERVER_URL);
|
||||
["utm_source", "utm_campaign", "utm_medium"].forEach(param => {
|
||||
req.query.utm_campaign && req.query.utm_campaign === 'go-to-dashboard-link'
|
||||
) {
|
||||
const oauthUrl = new URL('/oauth/init', AppConstants.SERVER_URL);
|
||||
['utm_source', 'utm_campaign', 'utm_medium'].forEach(param => {
|
||||
if (req.query[param]) {
|
||||
oauthUrl.searchParams.append(param, req.query[param]);
|
||||
oauthUrl.searchParams.append(param, req.query[param])
|
||||
}
|
||||
});
|
||||
req.url = `${oauthUrl.pathname}/${oauthUrl.search}`;
|
||||
next();
|
||||
return;
|
||||
})
|
||||
req.url = `${oauthUrl.pathname}/${oauthUrl.search}`
|
||||
next()
|
||||
return
|
||||
}
|
||||
next();
|
||||
next()
|
||||
}
|
||||
|
||||
|
||||
// Helps handle errors for all async route controllers
|
||||
// See https://medium.com/@Abazhenov/using-async-await-in-express-with-node-8-b8af872c0016
|
||||
function asyncMiddleware (fn) {
|
||||
return (req, res, next) => {
|
||||
Promise.resolve(fn(req, res, next)).catch(next);
|
||||
};
|
||||
Promise.resolve(fn(req, res, next)).catch(next)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function logErrors (err, req, res, next) {
|
||||
log.error("error", {stack: err.stack});
|
||||
Sentry.captureException(err);
|
||||
next(err);
|
||||
log.error('error', { stack: err.stack })
|
||||
Sentry.captureException(err)
|
||||
next(err)
|
||||
}
|
||||
|
||||
|
||||
function localizeErrorMessages (err, req, res, next) {
|
||||
if (err instanceof FluentError) {
|
||||
err.message = req.fluentFormat(err.fluentID);
|
||||
err.locales = req.supportedLocales;
|
||||
err.message = req.fluentFormat(err.fluentID)
|
||||
err.locales = req.supportedLocales
|
||||
}
|
||||
next(err);
|
||||
next(err)
|
||||
}
|
||||
|
||||
|
||||
function clientErrorHandler (err, req, res, next) {
|
||||
if (req.xhr || req.headers["content-type"] === "application/json") {
|
||||
res.status(500).send({ message: err.message });
|
||||
if (req.xhr || req.headers['content-type'] === 'application/json') {
|
||||
res.status(500).send({ message: err.message })
|
||||
} else {
|
||||
next(err);
|
||||
next(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function errorHandler (err, req, res, next) {
|
||||
res.status(500);
|
||||
res.render("subpage", {
|
||||
analyticsID: "error",
|
||||
headline: req.fluentFormat("error-headline"),
|
||||
subhead: err.message,
|
||||
});
|
||||
res.status(500)
|
||||
res.render('subpage', {
|
||||
analyticsID: 'error',
|
||||
headline: req.fluentFormat('error-headline'),
|
||||
subhead: err.message
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
async function _getRequestSessionUser(req, res, next) {
|
||||
async function _getRequestSessionUser (req, res, next) {
|
||||
if (req.session && req.session.user) {
|
||||
// make sure the user object has all subscribers and email_addresses properties
|
||||
return DB.getSubscriberById(req.session.user.id);
|
||||
return DB.getSubscriberById(req.session.user.id)
|
||||
}
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
async function requireSessionUser(req, res, next) {
|
||||
const user = await _getRequestSessionUser(req);
|
||||
async function requireSessionUser (req, res, next) {
|
||||
const user = await _getRequestSessionUser(req)
|
||||
if (!user) {
|
||||
const queryParams = new URLSearchParams(req.query).toString();
|
||||
return res.redirect(`/oauth/init?${queryParams}`);
|
||||
const queryParams = new URLSearchParams(req.query).toString()
|
||||
return res.redirect(`/oauth/init?${queryParams}`)
|
||||
}
|
||||
const fxaProfileData = await FXA.getProfileData(user.fxa_access_token);
|
||||
if (fxaProfileData.hasOwnProperty("name") && fxaProfileData.name === "HTTPError") {
|
||||
delete req.session.user;
|
||||
return res.redirect("/");
|
||||
const fxaProfileData = await FXA.getProfileData(user.fxa_access_token)
|
||||
if (fxaProfileData.hasOwnProperty('name') && fxaProfileData.name === 'HTTPError') {
|
||||
delete req.session.user
|
||||
return res.redirect('/')
|
||||
}
|
||||
await DB.updateFxAProfileData(user, fxaProfileData);
|
||||
req.session.user = user;
|
||||
req.user = user;
|
||||
next();
|
||||
await DB.updateFxAProfileData(user, fxaProfileData)
|
||||
req.session.user = user
|
||||
req.user = user
|
||||
next()
|
||||
}
|
||||
|
||||
function getShareUTMs(req, res, next) {
|
||||
function getShareUTMs (req, res, next) {
|
||||
// Step 1: See if the user needs to be redirected to the homepage or to a breach-detail page.
|
||||
const generalShareUrls = [
|
||||
"/share/orange", //Header
|
||||
"/share/purple", // Footer
|
||||
"/share/blue", // user/dashboard
|
||||
"/share/",
|
||||
];
|
||||
'/share/orange', // Header
|
||||
'/share/purple', // Footer
|
||||
'/share/blue', // user/dashboard
|
||||
'/share/'
|
||||
]
|
||||
|
||||
if (generalShareUrls.includes(req.url)) {
|
||||
// If not breach specific, redirect to "/"
|
||||
req.session.redirectHome = true;
|
||||
req.session.redirectHome = true
|
||||
}
|
||||
|
||||
// If user has no reference to experiment (default), add skip override
|
||||
if (typeof(req.session.experimentFlags) === "undefined") {
|
||||
if (typeof (req.session.experimentFlags) === 'undefined') {
|
||||
req.session.experimentFlags = {
|
||||
excludeFromExperiment: true,
|
||||
};
|
||||
excludeFromExperiment: true
|
||||
}
|
||||
}
|
||||
|
||||
const excludedFromExperiment = (req.session.experimentFlags.excludeFromExperiment);
|
||||
const excludedFromExperiment = (req.session.experimentFlags.excludeFromExperiment)
|
||||
|
||||
// Excluse user from experiment if they don't have any experimentFlags set already.
|
||||
if (excludedFromExperiment) {
|
||||
|
||||
// Step 2: Determine if user needs to have share-link UTMs set
|
||||
const colors = [
|
||||
"orange", //Header
|
||||
"purple", // Footer
|
||||
"blue", // user/dashboard
|
||||
];
|
||||
|
||||
const urlArray = req.url.split("/");
|
||||
const color = urlArray.slice(-1)[0];
|
||||
'orange', // Header
|
||||
'purple', // Footer
|
||||
'blue' // user/dashboard
|
||||
]
|
||||
|
||||
const urlArray = req.url.split('/')
|
||||
const color = urlArray.slice(-1)[0]
|
||||
|
||||
req.session.utmOverrides = {
|
||||
campaignName: "shareLinkTraffic",
|
||||
campaignTerm: "default",
|
||||
};
|
||||
campaignName: 'shareLinkTraffic',
|
||||
campaignTerm: 'default'
|
||||
}
|
||||
|
||||
// Set Color Var in UTM
|
||||
if (color.length && colors.includes(color)) {
|
||||
req.session.utmOverrides.campaignTerm = color;
|
||||
req.session.utmOverrides.campaignTerm = color
|
||||
}
|
||||
|
||||
if (color.length && !colors.includes(color)) {
|
||||
const allBreaches = req.app.locals.breaches;
|
||||
const breachName = color;
|
||||
const featuredBreach = HIBP.getBreachByName(allBreaches, breachName);
|
||||
const allBreaches = req.app.locals.breaches
|
||||
const breachName = color
|
||||
const featuredBreach = HIBP.getBreachByName(allBreaches, breachName)
|
||||
|
||||
if (featuredBreach) {
|
||||
req.session.utmOverrides.campaignTerm = featuredBreach.Name;
|
||||
req.session.utmOverrides.campaignTerm = featuredBreach.Name
|
||||
}
|
||||
}
|
||||
|
||||
// Exclude share users
|
||||
req.session.experimentFlags = {
|
||||
excludeFromExperiment: true,
|
||||
};
|
||||
excludeFromExperiment: true
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
next()
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
addRequestToResponse,
|
||||
pickLanguage,
|
||||
|
@ -252,5 +239,5 @@ module.exports = {
|
|||
clientErrorHandler,
|
||||
errorHandler,
|
||||
requireSessionUser,
|
||||
getShareUTMs,
|
||||
};
|
||||
getShareUTMs
|
||||
}
|
||||
|
|
|
@ -1,205 +1,205 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
/* global sendPing */
|
||||
|
||||
function replaceLogo(e) {
|
||||
e.target.src = "/img/logos/missing-logo-icon.png";
|
||||
e.target.removeEventListener("error", replaceLogo);
|
||||
return true;
|
||||
function replaceLogo (e) {
|
||||
e.target.src = '/img/logos/missing-logo-icon.png'
|
||||
e.target.removeEventListener('error', replaceLogo)
|
||||
return true
|
||||
}
|
||||
|
||||
function breachImages() {
|
||||
this.active = false;
|
||||
function breachImages () {
|
||||
this.active = false
|
||||
this.lazyLoad = () => {
|
||||
const lazyImages = [].slice.call(document.querySelectorAll(".lazy-img"));
|
||||
const lazyImages = [].slice.call(document.querySelectorAll('.lazy-img'))
|
||||
if (!this.active) {
|
||||
this.active = true;
|
||||
const winHeight = window.innerHeight;
|
||||
this.active = true
|
||||
const winHeight = window.innerHeight
|
||||
lazyImages.forEach(lazyImage => {
|
||||
if ((lazyImage.getBoundingClientRect().top <= winHeight && lazyImage.getBoundingClientRect().bottom >= 0)) {
|
||||
lazyImage.classList.add("lazy-loaded");
|
||||
lazyImage.classList.remove("lazy-img");
|
||||
lazyImage.src = lazyImage.dataset.src;
|
||||
lazyImage.addEventListener("error", replaceLogo);
|
||||
lazyImage.classList.add('lazy-loaded')
|
||||
lazyImage.classList.remove('lazy-img')
|
||||
lazyImage.src = lazyImage.dataset.src
|
||||
lazyImage.addEventListener('error', replaceLogo)
|
||||
|
||||
if (lazyImages.length === 0) {
|
||||
document.removeEventListener("scroll", this.lazyLoad);
|
||||
window.removeEventListener("resize", this.lazyLoad);
|
||||
window.removeEventListener("orientationchange", this.lazyLoad);
|
||||
document.removeEventListener('scroll', this.lazyLoad)
|
||||
window.removeEventListener('resize', this.lazyLoad)
|
||||
window.removeEventListener('orientationchange', this.lazyLoad)
|
||||
}
|
||||
}
|
||||
}),
|
||||
this.active = false;
|
||||
this.active = false
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const makeBreachInfoSpans = (className, spanContent, wrapper) => {
|
||||
const span = document.createElement("span");
|
||||
span["classList"] = className;
|
||||
span.textContent = spanContent;
|
||||
wrapper.appendChild(span);
|
||||
return span;
|
||||
};
|
||||
|
||||
function makeDiv(className, wrapper) {
|
||||
const div = document.createElement("div");
|
||||
div["classList"] = className;
|
||||
wrapper.appendChild(div);
|
||||
return div;
|
||||
const span = document.createElement('span')
|
||||
span.classList = className
|
||||
span.textContent = spanContent
|
||||
wrapper.appendChild(span)
|
||||
return span
|
||||
}
|
||||
|
||||
function clearBreaches(wrapper) {
|
||||
function makeDiv (className, wrapper) {
|
||||
const div = document.createElement('div')
|
||||
div.classList = className
|
||||
wrapper.appendChild(div)
|
||||
return div
|
||||
}
|
||||
|
||||
function clearBreaches (wrapper) {
|
||||
while (wrapper.firstChild) {
|
||||
wrapper.removeChild(wrapper.firstChild);
|
||||
wrapper.removeChild(wrapper.firstChild)
|
||||
}
|
||||
}
|
||||
|
||||
function makeBreaches(breaches, LocalizedBreachCardStrings, breachCardWrapper, breachLogos) {
|
||||
breachCardWrapper.classList.toggle("hide-breaches");
|
||||
clearBreaches(breachCardWrapper);
|
||||
function makeBreaches (breaches, LocalizedBreachCardStrings, breachCardWrapper, breachLogos) {
|
||||
breachCardWrapper.classList.toggle('hide-breaches')
|
||||
clearBreaches(breachCardWrapper)
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
fragment["id"] = "all-breaches";
|
||||
const fragment = document.createDocumentFragment()
|
||||
fragment.id = 'all-breaches'
|
||||
|
||||
const logosOrigin = document.body.dataset.logosOrigin;
|
||||
const logosOrigin = document.body.dataset.logosOrigin
|
||||
|
||||
for (const breach of breaches) {
|
||||
const card = document.createElement("a");
|
||||
const card = document.createElement('a')
|
||||
|
||||
card["classList"] = "breach-card three-up ab drop-shadow send-ga-ping";
|
||||
card["href"] = `/breach-details/${breach.Name}`;
|
||||
card.dataset.eventCategory = "All Breaches: More about this breach";
|
||||
card.dataset.eventAction = "Click";
|
||||
card.dataset.eventLabel = breach.Title;
|
||||
fragment.appendChild(card);
|
||||
card.classList = 'breach-card three-up ab drop-shadow send-ga-ping'
|
||||
card.href = `/breach-details/${breach.Name}`
|
||||
card.dataset.eventCategory = 'All Breaches: More about this breach'
|
||||
card.dataset.eventAction = 'Click'
|
||||
card.dataset.eventLabel = breach.Title
|
||||
fragment.appendChild(card)
|
||||
|
||||
const logoWrapper = makeDiv("breach-logo-wrapper", card);
|
||||
const logoWrapper = makeDiv('breach-logo-wrapper', card)
|
||||
|
||||
const breachLogo = document.createElement("img");
|
||||
breachLogo["alt"] = "";
|
||||
breachLogo["classList"] = "breach-logo lazy-img";
|
||||
breachLogo.dataset.src = `${logosOrigin}/img/logos/${breach.LogoPath}`;
|
||||
breachLogo.src = "/img/logos/lazyPlaceHolder.png";
|
||||
logoWrapper.appendChild(breachLogo);
|
||||
const breachLogo = document.createElement('img')
|
||||
breachLogo.alt = ''
|
||||
breachLogo.classList = 'breach-logo lazy-img'
|
||||
breachLogo.dataset.src = `${logosOrigin}/img/logos/${breach.LogoPath}`
|
||||
breachLogo.src = '/img/logos/lazyPlaceHolder.png'
|
||||
logoWrapper.appendChild(breachLogo)
|
||||
|
||||
// make wrapper for the breach-info and link
|
||||
const breachInfoWrapper = makeDiv("breach-info-wrapper flx flx-col", card);
|
||||
const breachInfoWrapper = makeDiv('breach-info-wrapper flx flx-col', card)
|
||||
|
||||
// make wrapper for the Added Date, Compromised Accounts etc info...
|
||||
let wrapper = makeDiv("flx flx-col", breachInfoWrapper);
|
||||
makeBreachInfoSpans("breach-title", breach.Title, wrapper);
|
||||
// make wrapper for the Added Date, Compromised Accounts etc info...
|
||||
let wrapper = makeDiv('flx flx-col', breachInfoWrapper)
|
||||
makeBreachInfoSpans('breach-title', breach.Title, wrapper)
|
||||
|
||||
// added date
|
||||
makeBreachInfoSpans("breach-key", LocalizedBreachCardStrings.BreachAdded, wrapper);
|
||||
makeBreachInfoSpans("breach-value", breach.AddedDate, wrapper);
|
||||
makeBreachInfoSpans('breach-key', LocalizedBreachCardStrings.BreachAdded, wrapper)
|
||||
makeBreachInfoSpans('breach-value', breach.AddedDate, wrapper)
|
||||
|
||||
// compromised data
|
||||
makeBreachInfoSpans("breach-key", LocalizedBreachCardStrings.CompromisedData, wrapper);
|
||||
makeBreachInfoSpans("breach-value", breach.DataClasses, wrapper);
|
||||
makeBreachInfoSpans('breach-key', LocalizedBreachCardStrings.CompromisedData, wrapper)
|
||||
makeBreachInfoSpans('breach-value', breach.DataClasses, wrapper)
|
||||
|
||||
// add link at bottom of card
|
||||
wrapper = makeDiv("breach-card-link-wrap", breachInfoWrapper);
|
||||
makeBreachInfoSpans("blue-link more-about-this-breach", LocalizedBreachCardStrings.MoreInfoLink, wrapper);
|
||||
wrapper = makeDiv('breach-card-link-wrap', breachInfoWrapper)
|
||||
makeBreachInfoSpans('blue-link more-about-this-breach', LocalizedBreachCardStrings.MoreInfoLink, wrapper)
|
||||
}
|
||||
|
||||
breachCardWrapper.appendChild(fragment);
|
||||
breachCardWrapper.classList.toggle("hide-breaches");
|
||||
breachLogos.lazyLoad();
|
||||
const loader = document.getElementById("breaches-loader");
|
||||
breachCardWrapper.appendChild(fragment)
|
||||
breachCardWrapper.classList.toggle('hide-breaches')
|
||||
breachLogos.lazyLoad()
|
||||
const loader = document.getElementById('breaches-loader')
|
||||
|
||||
loader.classList = ["hide"];
|
||||
return breaches;
|
||||
loader.classList = ['hide']
|
||||
return breaches
|
||||
}
|
||||
|
||||
function initBreaches() {
|
||||
const breachCardWrapper = document.querySelector("#all-breaches");
|
||||
function initBreaches () {
|
||||
const breachCardWrapper = document.querySelector('#all-breaches')
|
||||
if (breachCardWrapper) {
|
||||
const breachWrapper = document.getElementById("breach-array-json");
|
||||
const {LocalizedBreachCardStrings, breaches} = JSON.parse(breachWrapper.dataset.breachArray);
|
||||
const breachWrapper = document.getElementById('breach-array-json')
|
||||
const { LocalizedBreachCardStrings, breaches } = JSON.parse(breachWrapper.dataset.breachArray)
|
||||
|
||||
const breachLogos = new breachImages();
|
||||
document.addEventListener("scroll", breachLogos.lazyLoad);
|
||||
window.addEventListener("resize", breachLogos.lazyLoad);
|
||||
window.addEventListener("orientationchange", breachLogos.lazyLoad);
|
||||
const breachLogos = new breachImages()
|
||||
document.addEventListener('scroll', breachLogos.lazyLoad)
|
||||
window.addEventListener('resize', breachLogos.lazyLoad)
|
||||
window.addEventListener('orientationchange', breachLogos.lazyLoad)
|
||||
|
||||
const doBreaches = (arr) => {
|
||||
makeBreaches(arr, LocalizedBreachCardStrings, breachCardWrapper, breachLogos);
|
||||
};
|
||||
makeBreaches(arr, LocalizedBreachCardStrings, breachCardWrapper, breachLogos)
|
||||
}
|
||||
|
||||
const firstFifteen = breaches.slice(0,15);
|
||||
doBreaches(firstFifteen);
|
||||
const firstFifteen = breaches.slice(0, 15)
|
||||
doBreaches(firstFifteen)
|
||||
|
||||
const noResultsBlurb = document.getElementById("no-results-blurb");
|
||||
const noResultsBlurb = document.getElementById('no-results-blurb')
|
||||
|
||||
const fuzzyFindInput = document.getElementById("fuzzy-find-input");
|
||||
const fuzzyFinder = document.getElementById("fuzzy-form");
|
||||
const fuzzyFindInput = document.getElementById('fuzzy-find-input')
|
||||
const fuzzyFinder = document.getElementById('fuzzy-form')
|
||||
|
||||
const [fuzzyShowAll, showHiddenBreaches] = document.querySelectorAll(".show-all-breaches");
|
||||
const [fuzzyShowAll, showHiddenBreaches] = document.querySelectorAll('.show-all-breaches')
|
||||
|
||||
showHiddenBreaches.addEventListener("click", (e) => {
|
||||
sendPing(e.target, "Click", "All Breaches Page");
|
||||
doBreaches(breaches);
|
||||
showHiddenBreaches.classList.add("hide");
|
||||
});
|
||||
showHiddenBreaches.addEventListener('click', (e) => {
|
||||
sendPing(e.target, 'Click', 'All Breaches Page')
|
||||
doBreaches(breaches)
|
||||
showHiddenBreaches.classList.add('hide')
|
||||
})
|
||||
|
||||
fuzzyShowAll.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
fuzzyFindInput.value = "";
|
||||
doBreaches(breaches);
|
||||
noResultsBlurb.classList = [""];
|
||||
fuzzyShowAll.classList = ["fuzzy-find-show-breaches"];
|
||||
});
|
||||
fuzzyShowAll.addEventListener('click', (e) => {
|
||||
e.preventDefault()
|
||||
fuzzyFindInput.value = ''
|
||||
doBreaches(breaches)
|
||||
noResultsBlurb.classList = ['']
|
||||
fuzzyShowAll.classList = ['fuzzy-find-show-breaches']
|
||||
})
|
||||
|
||||
const searchBreaches = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault()
|
||||
e.stopImmediatePropagation()
|
||||
|
||||
// hide purple "Show All" button
|
||||
// show button to clear fuzzy input
|
||||
showHiddenBreaches.classList = ["hide"];
|
||||
fuzzyShowAll.classList = ["fuzzy-find-show-breaches show"];
|
||||
showHiddenBreaches.classList = ['hide']
|
||||
fuzzyShowAll.classList = ['fuzzy-find-show-breaches show']
|
||||
|
||||
const breachSearchTerm = fuzzyFindInput.value.toLowerCase();
|
||||
const breachSearchTerm = fuzzyFindInput.value.toLowerCase()
|
||||
|
||||
// filter breach array by search term
|
||||
const filteredBreachArray = breaches.filter(breach => {
|
||||
return breach.Title.toLowerCase().startsWith(breachSearchTerm);
|
||||
});
|
||||
return breach.Title.toLowerCase().startsWith(breachSearchTerm)
|
||||
})
|
||||
|
||||
// if hitting enter off a zero results search, restore breaches
|
||||
// and clear out input
|
||||
if (e.keyCode === 13 && noResultsBlurb.classList.contains("show")) {
|
||||
doBreaches(breaches);
|
||||
noResultsBlurb.classList.remove("show");
|
||||
fuzzyShowAll.classList.remove("show");
|
||||
fuzzyFindInput.value = "";
|
||||
return false;
|
||||
if (e.keyCode === 13 && noResultsBlurb.classList.contains('show')) {
|
||||
doBreaches(breaches)
|
||||
noResultsBlurb.classList.remove('show')
|
||||
fuzzyShowAll.classList.remove('show')
|
||||
fuzzyFindInput.value = ''
|
||||
return false
|
||||
}
|
||||
|
||||
// if no results, show "no results message"
|
||||
// otherwise make sure it isn't showing
|
||||
if (filteredBreachArray.length === 0) {
|
||||
noResultsBlurb.classList.add("show");
|
||||
noResultsBlurb.classList.add('show')
|
||||
} else {
|
||||
noResultsBlurb.classList = [""];
|
||||
noResultsBlurb.classList = ['']
|
||||
}
|
||||
doBreaches(filteredBreachArray);
|
||||
return false;
|
||||
};
|
||||
fuzzyFinder.addEventListener("keydown", () => {
|
||||
const finderInput = fuzzyFinder.querySelector("input[type=text]");
|
||||
if (finderInput.value === "") {
|
||||
sendPing(fuzzyFinder, "Engage", "All Breaches Page");
|
||||
doBreaches(filteredBreachArray)
|
||||
return false
|
||||
}
|
||||
fuzzyFinder.addEventListener('keydown', () => {
|
||||
const finderInput = fuzzyFinder.querySelector('input[type=text]')
|
||||
if (finderInput.value === '') {
|
||||
sendPing(fuzzyFinder, 'Engage', 'All Breaches Page')
|
||||
}
|
||||
});
|
||||
fuzzyFinder.addEventListener("keyup", searchBreaches);
|
||||
fuzzyFinder.addEventListener("submit", searchBreaches);
|
||||
})
|
||||
fuzzyFinder.addEventListener('keyup', searchBreaches)
|
||||
fuzzyFinder.addEventListener('submit', searchBreaches)
|
||||
}
|
||||
}
|
||||
|
||||
document.onreadystatechange = () => {
|
||||
if (document.readyState === "interactive") {
|
||||
initBreaches();
|
||||
if (document.readyState === 'interactive') {
|
||||
initBreaches()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
/*eslint-disable no-var */
|
||||
/*eslint-disable no-unused-vars */
|
||||
/* eslint-disable no-var */
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
/**
|
||||
* Returns true or false based on whether doNotTack is enabled. It also takes into account the
|
||||
|
@ -13,41 +13,40 @@
|
|||
* @returns {boolean} true if enabled else false
|
||||
*/
|
||||
|
||||
function _dntEnabled(dnt, userAgent) {
|
||||
function _dntEnabled (dnt, userAgent) {
|
||||
// for old version of IE we need to use the msDoNotTrack property of navigator
|
||||
// on newer versions, and newer platforms, this is doNotTrack but, on the window object
|
||||
// Safari also exposes the property on the window object.
|
||||
var dntStatus = dnt || navigator.doNotTrack || window.doNotTrack || navigator.msDoNotTrack
|
||||
var ua = userAgent || navigator.userAgent
|
||||
|
||||
// for old version of IE we need to use the msDoNotTrack property of navigator
|
||||
// on newer versions, and newer platforms, this is doNotTrack but, on the window object
|
||||
// Safari also exposes the property on the window object.
|
||||
var dntStatus = dnt || navigator.doNotTrack || window.doNotTrack || navigator.msDoNotTrack;
|
||||
var ua = userAgent || navigator.userAgent;
|
||||
// List of Windows versions known to not implement DNT according to the standard.
|
||||
var anomalousWinVersions = ['Windows NT 6.1', 'Windows NT 6.2', 'Windows NT 6.3']
|
||||
|
||||
// List of Windows versions known to not implement DNT according to the standard.
|
||||
var anomalousWinVersions = ["Windows NT 6.1", "Windows NT 6.2", "Windows NT 6.3"];
|
||||
var fxMatch = ua.match(/Firefox\/(\d+)/)
|
||||
var ieRegEx = /MSIE|Trident/i
|
||||
var isIE = ieRegEx.test(ua)
|
||||
// Matches from Windows up to the first occurance of ; un-greedily
|
||||
// http://www.regexr.com/3c2el
|
||||
var platform = ua.match(/Windows.+?(?=;)/g)
|
||||
|
||||
var fxMatch = ua.match(/Firefox\/(\d+)/);
|
||||
var ieRegEx = /MSIE|Trident/i;
|
||||
var isIE = ieRegEx.test(ua);
|
||||
// Matches from Windows up to the first occurance of ; un-greedily
|
||||
// http://www.regexr.com/3c2el
|
||||
var platform = ua.match(/Windows.+?(?=;)/g);
|
||||
// With old versions of IE, DNT did not exist so we simply return false;
|
||||
if (isIE && typeof Array.prototype.indexOf !== 'function') {
|
||||
return false
|
||||
} else if (fxMatch && parseInt(fxMatch[1], 10) < 32) {
|
||||
// Can"t say for sure if it is 1 or 0, due to Fx bug 887703
|
||||
dntStatus = 'Unspecified'
|
||||
} else if (isIE && platform && anomalousWinVersions.indexOf(platform.toString()) !== -1) {
|
||||
// default is on, which does not honor the specification
|
||||
dntStatus = 'Unspecified'
|
||||
} else {
|
||||
// sets dntStatus to Disabled or Enabled based on the value returned by the browser.
|
||||
// If dntStatus is undefined, it will be set to Unspecified
|
||||
dntStatus = { 0: 'Disabled', 1: 'Enabled' }[dntStatus] || 'Unspecified'
|
||||
}
|
||||
|
||||
// With old versions of IE, DNT did not exist so we simply return false;
|
||||
if (isIE && typeof Array.prototype.indexOf !== "function") {
|
||||
return false;
|
||||
} else if (fxMatch && parseInt(fxMatch[1], 10) < 32) {
|
||||
// Can"t say for sure if it is 1 or 0, due to Fx bug 887703
|
||||
dntStatus = "Unspecified";
|
||||
} else if (isIE && platform && anomalousWinVersions.indexOf(platform.toString()) !== -1) {
|
||||
// default is on, which does not honor the specification
|
||||
dntStatus = "Unspecified";
|
||||
} else {
|
||||
// sets dntStatus to Disabled or Enabled based on the value returned by the browser.
|
||||
// If dntStatus is undefined, it will be set to Unspecified
|
||||
dntStatus = { "0": "Disabled", "1": "Enabled" }[dntStatus] || "Unspecified";
|
||||
}
|
||||
|
||||
return dntStatus === "Enabled" ? true : false;
|
||||
return dntStatus === 'Enabled'
|
||||
}
|
||||
|
||||
/*eslint-enable no-var */
|
||||
/*eslint-enable no-unused-vars */
|
||||
/* eslint-enable no-var */
|
||||
/* eslint-enable no-unused-vars */
|
||||
|
|
|
@ -1,109 +1,106 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
/* global findAncestor */
|
||||
|
||||
|
||||
async function sendForm(action, formBody={}) {
|
||||
async function sendForm (action, formBody = {}) {
|
||||
const response = await fetch(`/user/${action}`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
},
|
||||
mode: "same-origin",
|
||||
method: "POST",
|
||||
body: JSON.stringify(formBody),
|
||||
});
|
||||
mode: 'same-origin',
|
||||
method: 'POST',
|
||||
body: JSON.stringify(formBody)
|
||||
})
|
||||
|
||||
if (response.redirected) {
|
||||
window.location = response.url;
|
||||
return;
|
||||
window.location = response.url
|
||||
return
|
||||
}
|
||||
return await response.json();
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
async function sendCommunicationOption(e) {
|
||||
const { formAction, commOption, csrfToken } = e.target.dataset;
|
||||
async function sendCommunicationOption (e) {
|
||||
const { formAction, commOption, csrfToken } = e.target.dataset
|
||||
sendForm(formAction, { communicationOption: commOption, _csrf: csrfToken })
|
||||
.then(data => {}) /*decide what to do with data */
|
||||
.catch(e => {})/* decide how to handle errors */;
|
||||
.then(data => {}) /* decide what to do with data */
|
||||
.catch(e => {})/* decide how to handle errors */
|
||||
}
|
||||
|
||||
async function resendEmail(e) {
|
||||
const resendEmailBtn = e.target;
|
||||
const { formAction, csrfToken, emailId } = resendEmailBtn.dataset;
|
||||
resendEmailBtn.classList.add("email-sent");
|
||||
async function resendEmail (e) {
|
||||
const resendEmailBtn = e.target
|
||||
const { formAction, csrfToken, emailId } = resendEmailBtn.dataset
|
||||
resendEmailBtn.classList.add('email-sent')
|
||||
|
||||
await sendForm(formAction, { _csrf: csrfToken, emailId })
|
||||
.then(data => {
|
||||
setTimeout( ()=> {
|
||||
const span = resendEmailBtn.nextElementSibling;
|
||||
span.classList.remove("hide");
|
||||
}, 1000);
|
||||
}) /*decide what to do with data */
|
||||
.catch(e => {})/* decide how to handle errors */;
|
||||
.then(data => {
|
||||
setTimeout(() => {
|
||||
const span = resendEmailBtn.nextElementSibling
|
||||
span.classList.remove('hide')
|
||||
}, 1000)
|
||||
}) /* decide what to do with data */
|
||||
.catch(e => {})/* decide how to handle errors */
|
||||
}
|
||||
|
||||
function hideShowOverflowBreaches(showBreachesButton, overflowBreaches) {
|
||||
function hideShowOverflowBreaches (showBreachesButton, overflowBreaches) {
|
||||
[showBreachesButton, overflowBreaches].forEach(el => {
|
||||
["show", "hide"].forEach(className => {
|
||||
el.classList.toggle(className);
|
||||
});
|
||||
});
|
||||
['show', 'hide'].forEach(className => {
|
||||
el.classList.toggle(className)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function showRemainingBreaches(e) {
|
||||
const showBreachesButton = e.target;
|
||||
const emailCard = findAncestor(e.target, "email-card");
|
||||
const additionalBreaches = emailCard.querySelector(".show-additional-breaches");
|
||||
hideShowOverflowBreaches(showBreachesButton, additionalBreaches);
|
||||
function showRemainingBreaches (e) {
|
||||
const showBreachesButton = e.target
|
||||
const emailCard = findAncestor(e.target, 'email-card')
|
||||
const additionalBreaches = emailCard.querySelector('.show-additional-breaches')
|
||||
hideShowOverflowBreaches(showBreachesButton, additionalBreaches)
|
||||
}
|
||||
|
||||
|
||||
if (document.querySelector(".email-card")) {
|
||||
|
||||
document.querySelectorAll(".show-remaining-breaches").forEach(btn => {
|
||||
btn.addEventListener("click", showRemainingBreaches);
|
||||
});
|
||||
if (document.querySelector('.email-card')) {
|
||||
document.querySelectorAll('.show-remaining-breaches').forEach(btn => {
|
||||
btn.addEventListener('click', showRemainingBreaches)
|
||||
})
|
||||
|
||||
// add listeners to "Hide / Show Resolved" buttons
|
||||
document.querySelectorAll(".toggle-resolved-breaches").forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
const emailCard = findAncestor(btn, "email-card");
|
||||
emailCard.classList.toggle("show-resolved-breach-cards");
|
||||
const showBreachesButton = emailCard.querySelector(".show-remaining-breaches");
|
||||
if (showBreachesButton && !showBreachesButton.classList.contains("hide")) {
|
||||
const additionalBreaches = emailCard.querySelector(".show-additional-breaches");
|
||||
hideShowOverflowBreaches(showBreachesButton, additionalBreaches);
|
||||
document.querySelectorAll('.toggle-resolved-breaches').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const emailCard = findAncestor(btn, 'email-card')
|
||||
emailCard.classList.toggle('show-resolved-breach-cards')
|
||||
const showBreachesButton = emailCard.querySelector('.show-remaining-breaches')
|
||||
if (showBreachesButton && !showBreachesButton.classList.contains('hide')) {
|
||||
const additionalBreaches = emailCard.querySelector('.show-additional-breaches')
|
||||
hideShowOverflowBreaches(showBreachesButton, additionalBreaches)
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
|
||||
const removeEmailButtons = document.querySelectorAll(".resend-email");
|
||||
const removeEmailButtons = document.querySelectorAll('.resend-email')
|
||||
removeEmailButtons.forEach(btn => {
|
||||
btn.addEventListener("click", resendEmail);
|
||||
});
|
||||
btn.addEventListener('click', resendEmail)
|
||||
})
|
||||
|
||||
const communicationRadioButtons = document.querySelectorAll(".radio-comm-option");
|
||||
const communicationRadioButtons = document.querySelectorAll('.radio-comm-option')
|
||||
communicationRadioButtons.forEach(option => {
|
||||
option.addEventListener("click", sendCommunicationOption);
|
||||
});
|
||||
option.addEventListener('click', sendCommunicationOption)
|
||||
})
|
||||
}
|
||||
|
||||
const removeMonitorButton = document.querySelector(".remove-fxm");
|
||||
const removeMonitorButton = document.querySelector('.remove-fxm')
|
||||
if (removeMonitorButton) {
|
||||
removeMonitorButton.addEventListener("click", async (e) => {
|
||||
const {formAction, csrfToken, primaryToken, primaryHash} = e.target.dataset;
|
||||
await sendForm(formAction, {_csrf: csrfToken, primaryToken, primaryHash});
|
||||
});
|
||||
removeMonitorButton.addEventListener('click', async (e) => {
|
||||
const { formAction, csrfToken, primaryToken, primaryHash } = e.target.dataset
|
||||
await sendForm(formAction, { _csrf: csrfToken, primaryToken, primaryHash })
|
||||
})
|
||||
}
|
||||
|
||||
const relayLink = document.querySelector("[data-event-label='Try Firefox Relay']");
|
||||
const userEmailElement = document.querySelector(".nav-user-email");
|
||||
const relayLink = document.querySelector("[data-event-label='Try Firefox Relay']")
|
||||
const userEmailElement = document.querySelector('.nav-user-email')
|
||||
if (userEmailElement && relayLink) {
|
||||
const user_email = userEmailElement.textContent;
|
||||
const user_email = userEmailElement.textContent
|
||||
if (user_email) {
|
||||
const relayUrl = new URL(relayLink.href);
|
||||
relayUrl.pathname += "accounts/fxa/login/";
|
||||
relayUrl.searchParams.append("process", "login");
|
||||
relayUrl.searchParams.append("auth_params", "prompt=none&login_hint=" + user_email);
|
||||
relayLink.href = relayUrl.href;
|
||||
const relayUrl = new URL(relayLink.href)
|
||||
relayUrl.pathname += 'accounts/fxa/login/'
|
||||
relayUrl.searchParams.append('process', 'login')
|
||||
relayUrl.searchParams.append('auth_params', 'prompt=none&login_hint=' + user_email)
|
||||
relayLink.href = relayUrl.href
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,312 +1,306 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
/* global ga */
|
||||
|
||||
function getFxAppLinkInfo(localizedBentoStrings, referringSiteURL) {
|
||||
function getFxAppLinkInfo (localizedBentoStrings, referringSiteURL) {
|
||||
return [
|
||||
[localizedBentoStrings.fxMonitor, "https://monitor.firefox.com/", "fx-monitor"],
|
||||
[localizedBentoStrings.pocket, "https://app.adjust.com/hr2n0yz?engagement_type=fallback_click&fallback=https%3A%2F%2Fgetpocket.com%2Ffirefox_learnmore%3Fsrc%3Dff_bento&fallback_lp=https%3A%2F%2Fapps.apple.com%2Fapp%2Fpocket-save-read-grow%2Fid309601447", "pocket"],
|
||||
[localizedBentoStrings.fxDesktop, `https://www.mozilla.org/firefox/new/?utm_source=${referringSiteURL}&utm_medium=referral&utm_campaign=bento&utm_content=desktop`, "fx-desktop"],
|
||||
[localizedBentoStrings.fxMobile, `http://mozilla.org/firefox/mobile?utm_source=${referringSiteURL}&utm_medium=referral&utm_campaign=bento&utm_content=desktop`, "fx-mobile"],
|
||||
[localizedBentoStrings.mozVPN, `https://vpn.mozilla.org/?utm_source=${referringSiteURL}&utm_medium=referral&utm_campaign=bento&utm_content=desktop`, "moz-vpn"],
|
||||
];
|
||||
[localizedBentoStrings.fxMonitor, 'https://monitor.firefox.com/', 'fx-monitor'],
|
||||
[localizedBentoStrings.pocket, 'https://app.adjust.com/hr2n0yz?engagement_type=fallback_click&fallback=https%3A%2F%2Fgetpocket.com%2Ffirefox_learnmore%3Fsrc%3Dff_bento&fallback_lp=https%3A%2F%2Fapps.apple.com%2Fapp%2Fpocket-save-read-grow%2Fid309601447', 'pocket'],
|
||||
[localizedBentoStrings.fxDesktop, `https://www.mozilla.org/firefox/new/?utm_source=${referringSiteURL}&utm_medium=referral&utm_campaign=bento&utm_content=desktop`, 'fx-desktop'],
|
||||
[localizedBentoStrings.fxMobile, `http://mozilla.org/firefox/mobile?utm_source=${referringSiteURL}&utm_medium=referral&utm_campaign=bento&utm_content=desktop`, 'fx-mobile'],
|
||||
[localizedBentoStrings.mozVPN, `https://vpn.mozilla.org/?utm_source=${referringSiteURL}&utm_medium=referral&utm_campaign=bento&utm_content=desktop`, 'moz-vpn']
|
||||
]
|
||||
}
|
||||
|
||||
function createAndAppendEl(wrapper, tagName, className = null) {
|
||||
const newEl = document.createElement(tagName);
|
||||
function createAndAppendEl (wrapper, tagName, className = null) {
|
||||
const newEl = document.createElement(tagName)
|
||||
if (className) {
|
||||
newEl.setAttribute("class", className);
|
||||
newEl.setAttribute('class', className)
|
||||
}
|
||||
wrapper.appendChild(newEl);
|
||||
return newEl;
|
||||
wrapper.appendChild(newEl)
|
||||
return newEl
|
||||
}
|
||||
|
||||
async function getlocalizedBentoStrings() {
|
||||
let localizedBentoStrings;
|
||||
async function getlocalizedBentoStrings () {
|
||||
let localizedBentoStrings
|
||||
try {
|
||||
const serverUrl = document.body.dataset.serverUrl;
|
||||
const serverUrl = document.body.dataset.serverUrl
|
||||
const res = await fetch(
|
||||
`${serverUrl}/getBentoStrings`,
|
||||
{
|
||||
mode: "cors",
|
||||
mode: 'cors'
|
||||
}
|
||||
);
|
||||
localizedBentoStrings = await res.json();
|
||||
} catch(e) {
|
||||
)
|
||||
localizedBentoStrings = await res.json()
|
||||
} catch (e) {
|
||||
// Error fetching the localized strings. Defaulting to English.
|
||||
localizedBentoStrings = {
|
||||
bentoButtonTitle: "Firefox apps and services",
|
||||
bentoHeadline: "Firefox is tech that fights for your online privacy.",
|
||||
bentoBottomLink: "Made by Mozilla",
|
||||
mobileCloseBentoButtonTitle: "Close menu",
|
||||
fxDesktop: "Firefox Browser for Desktop",
|
||||
fxMobile: "Firefox Browser for Mobile",
|
||||
fxMonitor: "Firefox Monitor",
|
||||
pocket: "Pocket",
|
||||
mozVPN: "Mozilla VPN",
|
||||
};
|
||||
bentoButtonTitle: 'Firefox apps and services',
|
||||
bentoHeadline: 'Firefox is tech that fights for your online privacy.',
|
||||
bentoBottomLink: 'Made by Mozilla',
|
||||
mobileCloseBentoButtonTitle: 'Close menu',
|
||||
fxDesktop: 'Firefox Browser for Desktop',
|
||||
fxMobile: 'Firefox Browser for Mobile',
|
||||
fxMonitor: 'Firefox Monitor',
|
||||
pocket: 'Pocket',
|
||||
mozVPN: 'Mozilla VPN'
|
||||
}
|
||||
}
|
||||
return localizedBentoStrings;
|
||||
return localizedBentoStrings
|
||||
}
|
||||
|
||||
class FirefoxApps extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
constructor () {
|
||||
super()
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
this._currentSite = document.body.dataset.bentoAppId;
|
||||
this._localizedBentoStrings = await getlocalizedBentoStrings();
|
||||
async connectedCallback () {
|
||||
this._currentSite = document.body.dataset.bentoAppId
|
||||
this._localizedBentoStrings = await getlocalizedBentoStrings()
|
||||
|
||||
this._active = false; // Becomes true when the bento is opened.
|
||||
this._active = false // Becomes true when the bento is opened.
|
||||
|
||||
this._frag = document.createDocumentFragment(); // Wrapping fragment for bento button and bento content.
|
||||
this._frag = document.createDocumentFragment() // Wrapping fragment for bento button and bento content.
|
||||
|
||||
this._bentoButton = createAndAppendEl(this._frag, "button", "fx-bento-button toggle-bento"); // Button toggles dropdown.
|
||||
this.addTitleAndAriaLabel(this._bentoButton, this._localizedBentoStrings.bentoButtonTitle);
|
||||
this._bentoButton = createAndAppendEl(this._frag, 'button', 'fx-bento-button toggle-bento') // Button toggles dropdown.
|
||||
this.addTitleAndAriaLabel(this._bentoButton, this._localizedBentoStrings.bentoButtonTitle)
|
||||
|
||||
this._bentoWrapper = document.createElement("div");
|
||||
this._bentoWrapper.classList = "fx-bento-content-wrapper";
|
||||
this._bentoWrapper = document.createElement('div')
|
||||
this._bentoWrapper.classList = 'fx-bento-content-wrapper'
|
||||
|
||||
const browserLanguage = window.navigator.language;
|
||||
if (!browserLanguage.includes("en")) {
|
||||
this._bentoWrapper.classList.add("fx-bento-hide-vpn");
|
||||
const browserLanguage = window.navigator.language
|
||||
if (!browserLanguage.includes('en')) {
|
||||
this._bentoWrapper.classList.add('fx-bento-hide-vpn')
|
||||
}
|
||||
|
||||
this._bentoHideOverflow = createAndAppendEl(this._bentoWrapper, 'div', 'fx-bento-hide-overflow')
|
||||
this._bentoContent = createAndAppendEl(this._bentoHideOverflow, 'div', 'fx-bento-content')
|
||||
|
||||
this._bentoHideOverflow = createAndAppendEl(this._bentoWrapper, "div", "fx-bento-hide-overflow");
|
||||
this._bentoContent = createAndAppendEl(this._bentoHideOverflow, "div", "fx-bento-content");
|
||||
|
||||
this._mobileCloseBentoButton = createAndAppendEl(this._bentoContent, "button", "fx-bento-mobile-close toggle-bento");
|
||||
this._mobileCloseBentoButton = createAndAppendEl(this._bentoContent, 'button', 'fx-bento-mobile-close toggle-bento')
|
||||
this.addTitleAndAriaLabel(this._mobileCloseBentoButton, this._localizedBentoStrings.mobileCloseBentoButtonTitle);
|
||||
|
||||
[this._bentoButton, this._mobileCloseBentoButton].forEach(btn => {
|
||||
btn.addEventListener("click", this);
|
||||
});
|
||||
btn.addEventListener('click', this)
|
||||
})
|
||||
|
||||
this._logoHeadlineWrapper = createAndAppendEl(this._bentoContent, "div", "fx-bento-headline-logo-wrapper");
|
||||
this._firefoxLogo = createAndAppendEl( this._logoHeadlineWrapper, "div", "fx-bento-logo");
|
||||
this._messageTop = createAndAppendEl( this._logoHeadlineWrapper, "span", "fx-bento-headline");
|
||||
this._messageTop.textContent = this._localizedBentoStrings.bentoHeadline;
|
||||
this._logoHeadlineWrapper = createAndAppendEl(this._bentoContent, 'div', 'fx-bento-headline-logo-wrapper')
|
||||
this._firefoxLogo = createAndAppendEl(this._logoHeadlineWrapper, 'div', 'fx-bento-logo')
|
||||
this._messageTop = createAndAppendEl(this._logoHeadlineWrapper, 'span', 'fx-bento-headline')
|
||||
this._messageTop.textContent = this._localizedBentoStrings.bentoHeadline
|
||||
|
||||
this._appList = this.makeAppList();
|
||||
this._appList = this.makeAppList()
|
||||
|
||||
this._messageBottomLink = createAndAppendEl(this._bentoContent, "a", "fx-bento-bottom-link fx-bento-link");
|
||||
this._messageBottomLink.textContent = this._localizedBentoStrings.bentoBottomLink;
|
||||
this._messageBottomLink.href = "https://www.mozilla.org/";
|
||||
this._messageBottomLink = createAndAppendEl(this._bentoContent, 'a', 'fx-bento-bottom-link fx-bento-link')
|
||||
this._messageBottomLink.textContent = this._localizedBentoStrings.bentoBottomLink
|
||||
this._messageBottomLink.href = 'https://www.mozilla.org/'
|
||||
|
||||
this._bentoContent.querySelectorAll("a").forEach( (anchorEl, idx) => {
|
||||
anchorEl.dataset.bentoLinkOrder = idx;
|
||||
anchorEl.addEventListener("click", this);
|
||||
anchorEl.tabIndex = "-1";
|
||||
});
|
||||
this._bentoContent.querySelectorAll('a').forEach((anchorEl, idx) => {
|
||||
anchorEl.dataset.bentoLinkOrder = idx
|
||||
anchorEl.addEventListener('click', this)
|
||||
anchorEl.tabIndex = '-1'
|
||||
})
|
||||
|
||||
this._frag.appendChild(this._bentoWrapper);
|
||||
this.appendChild(this._frag);
|
||||
this.addEventListener("close-bento-menu", this);
|
||||
this._frag.appendChild(this._bentoWrapper)
|
||||
this.appendChild(this._frag)
|
||||
this.addEventListener('close-bento-menu', this)
|
||||
}
|
||||
|
||||
addTitleAndAriaLabel(el, localizedCopy) {
|
||||
["title", "aria-label"].forEach(attrName => {
|
||||
el.setAttribute(attrName, localizedCopy);
|
||||
});
|
||||
addTitleAndAriaLabel (el, localizedCopy) {
|
||||
['title', 'aria-label'].forEach(attrName => {
|
||||
el.setAttribute(attrName, localizedCopy)
|
||||
})
|
||||
}
|
||||
|
||||
metricsSendEvent(eventAction, eventLabel) {
|
||||
if (typeof(ga) !== "undefined") {
|
||||
return ga("send", "event", "bento", eventAction, eventLabel);
|
||||
metricsSendEvent (eventAction, eventLabel) {
|
||||
if (typeof (ga) !== 'undefined') {
|
||||
return ga('send', 'event', 'bento', eventAction, eventLabel)
|
||||
}
|
||||
}
|
||||
|
||||
handleEvent(event) {
|
||||
handleEvent (event) {
|
||||
const closeBento = () => {
|
||||
this.handleBentoFocusTrap();
|
||||
window.removeEventListener("resize", this.handleBentoHeight);
|
||||
window.removeEventListener("click", this);
|
||||
document.removeEventListener("keydown", this);
|
||||
this.metricsSendEvent("bento-closed", this._currentSite);
|
||||
this.classList.remove("fx-bento-open");
|
||||
this._bentoWrapper.classList.add("fx-bento-fade-out");
|
||||
this.handleBentoFocusTrap()
|
||||
window.removeEventListener('resize', this.handleBentoHeight)
|
||||
window.removeEventListener('click', this)
|
||||
document.removeEventListener('keydown', this)
|
||||
this.metricsSendEvent('bento-closed', this._currentSite)
|
||||
this.classList.remove('fx-bento-open')
|
||||
this._bentoWrapper.classList.add('fx-bento-fade-out')
|
||||
setTimeout(() => {
|
||||
this._bentoWrapper.classList.remove("fx-bento-fade-out");
|
||||
this._bentoButton.blur();
|
||||
this.classList = [];
|
||||
}, 500);
|
||||
return;
|
||||
};
|
||||
this._bentoWrapper.classList.remove('fx-bento-fade-out')
|
||||
this._bentoButton.blur()
|
||||
this.classList = []
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const keydownEvent = (event.type === "keydown");
|
||||
const eventTarget = event.target;
|
||||
const keydownEvent = (event.type === 'keydown')
|
||||
const eventTarget = event.target
|
||||
|
||||
if (
|
||||
// ignore mouse clicks inside the bento
|
||||
(!keydownEvent && ["fx-bento-content active", "fx-bento-headline", "fx-bento-logo", "fx-bento-headline-logo-wrapper"].includes(eventTarget.className)) ||
|
||||
(!keydownEvent && ['fx-bento-content active', 'fx-bento-headline', 'fx-bento-logo', 'fx-bento-headline-logo-wrapper'].includes(eventTarget.className)) ||
|
||||
// ignore and don't prevent default behavior on key clicks other than Escape, Down Arrow, and Up Arrow
|
||||
(keydownEvent && ![27, 40, 38].includes(event.keyCode))
|
||||
) {
|
||||
return;
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const hasParent = (el, selector) => {
|
||||
while (el.parentElement) {
|
||||
el = el.parentElement;
|
||||
if (el.tagName === selector)
|
||||
return el;
|
||||
el = el.parentElement
|
||||
if (el.tagName === selector) { return el }
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// close Bento on mouse clicks outside the Bento menu
|
||||
if (hasParent(event.target, "FIREFOX-APPS") === null) {
|
||||
this._active = !this._active;
|
||||
return closeBento();
|
||||
return null
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
// close Bento on mouse clicks outside the Bento menu
|
||||
if (hasParent(event.target, 'FIREFOX-APPS') === null) {
|
||||
this._active = !this._active
|
||||
return closeBento()
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
if (keydownEvent) {
|
||||
const moveFocusWithArrows = (whichDirection) => {
|
||||
const activeEl = document.activeElement;
|
||||
const bentoLinks = this._bentoContent.querySelectorAll("a");
|
||||
const activeEl = document.activeElement
|
||||
const bentoLinks = this._bentoContent.querySelectorAll('a')
|
||||
if (!activeEl.dataset.bentoLinkOrder) { // check if link in Bento has focus
|
||||
bentoLinks[0].focus(); // focus first link in bento
|
||||
return;
|
||||
bentoLinks[0].focus() // focus first link in bento
|
||||
return
|
||||
}
|
||||
const activeLinkNum = parseInt(activeEl.dataset.bentoLinkOrder);
|
||||
const newActiveLink = parseInt(activeLinkNum + whichDirection);
|
||||
const activeLinkNum = parseInt(activeEl.dataset.bentoLinkOrder)
|
||||
const newActiveLink = parseInt(activeLinkNum + whichDirection)
|
||||
if (bentoLinks[newActiveLink]) {
|
||||
bentoLinks[newActiveLink].focus();
|
||||
bentoLinks[newActiveLink].focus()
|
||||
}
|
||||
return;
|
||||
};
|
||||
switch(event.keyCode) {
|
||||
case 27: // escape
|
||||
this._active = !this._active;
|
||||
closeBento();
|
||||
return;
|
||||
case 40 : // down arrow || up arrow
|
||||
moveFocusWithArrows(1);
|
||||
break;
|
||||
case 38: // arrow up
|
||||
moveFocusWithArrows(-1);
|
||||
break;
|
||||
}
|
||||
return;
|
||||
switch (event.keyCode) {
|
||||
case 27: // escape
|
||||
this._active = !this._active
|
||||
closeBento()
|
||||
return
|
||||
case 40 : // down arrow || up arrow
|
||||
moveFocusWithArrows(1)
|
||||
break
|
||||
case 38: // arrow up
|
||||
moveFocusWithArrows(-1)
|
||||
break
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
this._active = !this._active;
|
||||
const eventTargetClassList = event.target.classList;
|
||||
const MozLinkClick = (eventTargetClassList.contains("fx-bento-bottom-link"));
|
||||
this._active = !this._active
|
||||
const eventTargetClassList = event.target.classList
|
||||
const MozLinkClick = (eventTargetClassList.contains('fx-bento-bottom-link'))
|
||||
|
||||
if (eventTargetClassList.contains("fx-bento-app-link") || MozLinkClick) {
|
||||
const url = new URL(eventTarget.href); // add any additional UTM params - or whatever
|
||||
url.searchParams.append("utm_source", this._currentSite);
|
||||
url.searchParams.append("utm_medium", "referral");
|
||||
url.searchParams.append("utm_campaign", "bento");
|
||||
if (eventTargetClassList.contains('fx-bento-app-link') || MozLinkClick) {
|
||||
const url = new URL(eventTarget.href) // add any additional UTM params - or whatever
|
||||
url.searchParams.append('utm_source', this._currentSite)
|
||||
url.searchParams.append('utm_medium', 'referral')
|
||||
url.searchParams.append('utm_campaign', 'bento')
|
||||
if (MozLinkClick) {
|
||||
this.metricsSendEvent("bento-app-link-click", "Mozilla");
|
||||
window.open(url, "_blank", "noopener");
|
||||
return closeBento();
|
||||
this.metricsSendEvent('bento-app-link-click', 'Mozilla')
|
||||
window.open(url, '_blank', 'noopener')
|
||||
return closeBento()
|
||||
}
|
||||
const appToOpenId = eventTarget.dataset.bentoAppLinkId;
|
||||
this.metricsSendEvent("bento-app-link-click", appToOpenId);
|
||||
if (eventTargetClassList.contains("fx-bento-current-site")) { // open index page in existing window
|
||||
window.location = url;
|
||||
return closeBento();
|
||||
const appToOpenId = eventTarget.dataset.bentoAppLinkId
|
||||
this.metricsSendEvent('bento-app-link-click', appToOpenId)
|
||||
if (eventTargetClassList.contains('fx-bento-current-site')) { // open index page in existing window
|
||||
window.location = url
|
||||
return closeBento()
|
||||
}
|
||||
window.open(url, "_blank", "noopener");
|
||||
return closeBento();
|
||||
window.open(url, '_blank', 'noopener')
|
||||
return closeBento()
|
||||
}
|
||||
|
||||
if (!this._active) {
|
||||
return closeBento();
|
||||
return closeBento()
|
||||
}
|
||||
|
||||
const sendEventOnBentoOpen = new Event("bento-was-opened");
|
||||
document.dispatchEvent(sendEventOnBentoOpen);
|
||||
const sendEventOnBentoOpen = new Event('bento-was-opened')
|
||||
document.dispatchEvent(sendEventOnBentoOpen)
|
||||
|
||||
this.metricsSendEvent("bento-opened", this._currentSite);
|
||||
this.handleBentoHeight();
|
||||
document.addEventListener("keydown", this);
|
||||
window.addEventListener("resize", this.handleBentoHeight);
|
||||
window.addEventListener("click", this);
|
||||
this.metricsSendEvent('bento-opened', this._currentSite)
|
||||
this.handleBentoHeight()
|
||||
document.addEventListener('keydown', this)
|
||||
window.addEventListener('resize', this.handleBentoHeight)
|
||||
window.addEventListener('click', this)
|
||||
|
||||
this.classList = ["active fx-bento-open"];
|
||||
this._bentoButton.focus();
|
||||
return this.handleBentoFocusTrap();
|
||||
this.classList = ['active fx-bento-open']
|
||||
this._bentoButton.focus()
|
||||
return this.handleBentoFocusTrap()
|
||||
}
|
||||
|
||||
handleBentoHeight() { // resize bento max-height if necessary
|
||||
const bento = document.querySelector(".fx-bento-content");
|
||||
const winHeight = window.innerHeight;
|
||||
const newBentoHeight = winHeight - bento.offsetTop - 100;
|
||||
const setMaxHeight = (winHeight < 500 && window.innerWidth > 500);
|
||||
handleBentoHeight () { // resize bento max-height if necessary
|
||||
const bento = document.querySelector('.fx-bento-content')
|
||||
const winHeight = window.innerHeight
|
||||
const newBentoHeight = winHeight - bento.offsetTop - 100
|
||||
const setMaxHeight = (winHeight < 500 && window.innerWidth > 500)
|
||||
if (setMaxHeight) {
|
||||
bento.style.maxHeight = `${newBentoHeight}px`;
|
||||
bento.style.maxHeight = `${newBentoHeight}px`
|
||||
} else {
|
||||
bento.style.maxHeight = "1000px";
|
||||
bento.style.maxHeight = '1000px'
|
||||
}
|
||||
bento.classList.toggle("fx-bento-enable-scrolling", setMaxHeight);
|
||||
}
|
||||
bento.classList.toggle('fx-bento-enable-scrolling', setMaxHeight)
|
||||
}
|
||||
|
||||
handleBentoFocusTrap() {
|
||||
handleBentoFocusTrap () {
|
||||
const nonBentoPageElements = document.querySelectorAll(
|
||||
"a:not(.fx-bento-app-link):not(.fx-bento-bottom-link), button:not(.toggle-bento ), input, select, option, [tabindex]"
|
||||
);
|
||||
const bentoLinks = this._bentoContent.querySelectorAll(".fx-bento-app-link, .fx-bento-bottom-link");
|
||||
'a:not(.fx-bento-app-link):not(.fx-bento-bottom-link), button:not(.toggle-bento ), input, select, option, [tabindex]'
|
||||
)
|
||||
const bentoLinks = this._bentoContent.querySelectorAll('.fx-bento-app-link, .fx-bento-bottom-link')
|
||||
if (this._active) {
|
||||
nonBentoPageElements.forEach(el => {
|
||||
if (el.tabIndex > -1) {
|
||||
el.dataset.oldTabIndex = el.tabIndex;
|
||||
el.dataset.oldTabIndex = el.tabIndex
|
||||
}
|
||||
el.tabIndex = -1;
|
||||
});
|
||||
el.tabIndex = -1
|
||||
})
|
||||
bentoLinks.forEach(el => {
|
||||
el.tabIndex = 0;
|
||||
});
|
||||
return;
|
||||
el.tabIndex = 0
|
||||
})
|
||||
return
|
||||
}
|
||||
nonBentoPageElements.forEach(el => {
|
||||
if (el.dataset.oldTabIndex) {
|
||||
el.tabIndex = el.dataset.oldTabIndex;
|
||||
delete el.dataset.oldTabIndex;
|
||||
return;
|
||||
el.tabIndex = el.dataset.oldTabIndex
|
||||
delete el.dataset.oldTabIndex
|
||||
return
|
||||
}
|
||||
el.tabIndex = 0;
|
||||
});
|
||||
el.tabIndex = 0
|
||||
})
|
||||
bentoLinks.forEach(el => {
|
||||
el.tabIndex = -1;
|
||||
});
|
||||
el.tabIndex = -1
|
||||
})
|
||||
}
|
||||
|
||||
makeAppList() {
|
||||
|
||||
makeAppList () {
|
||||
// const browserLanguage = window.navigator.language
|
||||
const appLinks = getFxAppLinkInfo(this._localizedBentoStrings, this._currentSite);
|
||||
const appLinks = getFxAppLinkInfo(this._localizedBentoStrings, this._currentSite)
|
||||
|
||||
// appLinks.push([localizedBentoStrings.mozVPN, `https://vpn.mozilla.org/?utm_source=${referringSiteURL}&utm_medium=referral&utm_campaign=bento&utm_content=desktop`, "moz-vpn"])
|
||||
appLinks.forEach(app => {
|
||||
const newLink = document.createElement("a");
|
||||
const newLinkSpan = createAndAppendEl(newLink, "span", `fx-bento-app-link-span ${app[2]}`);
|
||||
newLink.setAttribute("class", `fx-bento-app-link fx-bento-link ${app[2]}`);
|
||||
newLinkSpan["textContent"] = app[0];
|
||||
["href", "data-bento-app-link-id"].forEach((attributeName, index) => {
|
||||
newLink.setAttribute(attributeName, app[index + 1]);
|
||||
});
|
||||
const newLink = document.createElement('a')
|
||||
const newLinkSpan = createAndAppendEl(newLink, 'span', `fx-bento-app-link-span ${app[2]}`)
|
||||
newLink.setAttribute('class', `fx-bento-app-link fx-bento-link ${app[2]}`)
|
||||
newLinkSpan.textContent = app[0];
|
||||
['href', 'data-bento-app-link-id'].forEach((attributeName, index) => {
|
||||
newLink.setAttribute(attributeName, app[index + 1])
|
||||
})
|
||||
if (newLink.dataset.bentoAppLinkId === this._currentSite) {
|
||||
newLink.classList.add("fx-bento-current-site");
|
||||
newLink.classList.add('fx-bento-current-site')
|
||||
}
|
||||
this._bentoContent.appendChild(newLink);
|
||||
});
|
||||
this._bentoContent.appendChild(newLink)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof(customElements) !== "undefined") {
|
||||
customElements.define("firefox-apps", FirefoxApps);
|
||||
if (typeof (customElements) !== 'undefined') {
|
||||
customElements.define('firefox-apps', FirefoxApps)
|
||||
} else { // Hide on unsupportive browsers
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document.body.classList.add("hide-bento");
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.body.classList.add('hide-bento')
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -1,239 +1,231 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* global _dntEnabled */
|
||||
/* global ga */
|
||||
|
||||
|
||||
const hasParent = (el, selector) => {
|
||||
while (el.parentNode) {
|
||||
el = el.parentNode;
|
||||
if (el.dataset && el.dataset.analyticsId === selector)
|
||||
return el;
|
||||
el = el.parentNode
|
||||
if (el.dataset && el.dataset.analyticsId === selector) { return el }
|
||||
}
|
||||
return null;
|
||||
};
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
function getLocation() {
|
||||
const eventLocation = document.querySelectorAll("[data-page-label]");
|
||||
function getLocation () {
|
||||
const eventLocation = document.querySelectorAll('[data-page-label]')
|
||||
if (eventLocation.length > 0) {
|
||||
return `Page ID: ${eventLocation[0].dataset.pageLabel}`;
|
||||
return `Page ID: ${eventLocation[0].dataset.pageLabel}`
|
||||
} else {
|
||||
return "Page ID: Undefined Page";
|
||||
return 'Page ID: Undefined Page'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function sendPing(el, eventAction, eventLabel = null, options = null) {
|
||||
if (typeof(ga) !== "undefined" && !el.classList.contains("hide")) {
|
||||
async function sendPing (el, eventAction, eventLabel = null, options = null) {
|
||||
if (typeof (ga) !== 'undefined' && !el.classList.contains('hide')) {
|
||||
if (!eventLabel) {
|
||||
eventLabel = `${getLocation()}`;
|
||||
eventLabel = `${getLocation()}`
|
||||
}
|
||||
const eventCategory = `[v2] ${el.dataset.eventCategory}`;
|
||||
return ga("send", "event", eventCategory, eventAction, eventLabel, options);
|
||||
const eventCategory = `[v2] ${el.dataset.eventCategory}`
|
||||
return ga('send', 'event', eventCategory, eventAction, eventLabel, options)
|
||||
}
|
||||
}
|
||||
|
||||
function appendFxaParams(url, storageObject) {
|
||||
function appendFxaParams (url, storageObject) {
|
||||
getUTMNames().forEach(param => {
|
||||
if (storageObject[param] && !url.searchParams.get(param)) {
|
||||
// Bug #2011 - This logic only allows params to be set/passed
|
||||
// on to FxA if that param isn't already set.
|
||||
// (Example: Overwriting a utm_source)
|
||||
url.searchParams.append(param, encodeURIComponent(storageObject[param]));
|
||||
url.searchParams.append(param, encodeURIComponent(storageObject[param]))
|
||||
}
|
||||
});
|
||||
return url;
|
||||
})
|
||||
return url
|
||||
}
|
||||
|
||||
function getFxaUtms(url) {
|
||||
function getFxaUtms (url) {
|
||||
if (sessionStorage) {
|
||||
url = appendFxaParams(url, sessionStorage);
|
||||
url = appendFxaParams(url, sessionStorage)
|
||||
}
|
||||
|
||||
return appendFxaParams(url, document.body.dataset);
|
||||
return appendFxaParams(url, document.body.dataset)
|
||||
}
|
||||
|
||||
function saveReferringPageData(utmParams) {
|
||||
function saveReferringPageData (utmParams) {
|
||||
if (sessionStorage) {
|
||||
getUTMNames().forEach(param => {
|
||||
if(utmParams.get(param)) {
|
||||
sessionStorage[param] = utmParams.get(param);
|
||||
if (utmParams.get(param)) {
|
||||
sessionStorage[param] = utmParams.get(param)
|
||||
}
|
||||
});
|
||||
return;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function getUTMNames() {
|
||||
return ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
|
||||
function getUTMNames () {
|
||||
return ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']
|
||||
}
|
||||
|
||||
function sendRecommendationPings(ctaSelector) {
|
||||
function sendRecommendationPings (ctaSelector) {
|
||||
document.querySelectorAll(ctaSelector).forEach(cta => {
|
||||
const eventLabel = cta.dataset.eventLabel;
|
||||
ga("send", "event", "Breach Detail: Recommendation CTA", "View", eventLabel, {nonInteraction: true});
|
||||
cta.addEventListener("click", () => {
|
||||
ga("send", "event", "Breach Detail: Recommendation CTA", "Engage", eventLabel, {transport: "beacon"});
|
||||
});
|
||||
});
|
||||
const eventLabel = cta.dataset.eventLabel
|
||||
ga('send', 'event', 'Breach Detail: Recommendation CTA', 'View', eventLabel, { nonInteraction: true })
|
||||
cta.addEventListener('click', () => {
|
||||
ga('send', 'event', 'Breach Detail: Recommendation CTA', 'Engage', eventLabel, { transport: 'beacon' })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function setMetricsIds(el) {
|
||||
if (el.dataset.entrypoint && hasParent(el, "sign-up-banner")) {
|
||||
el.dataset.eventCategory = `${el.dataset.eventCategory} - Banner`;
|
||||
el.dataset.entrypoint = `${el.dataset.entrypoint}-banner`;
|
||||
function setMetricsIds (el) {
|
||||
if (el.dataset.entrypoint && hasParent(el, 'sign-up-banner')) {
|
||||
el.dataset.eventCategory = `${el.dataset.eventCategory} - Banner`
|
||||
el.dataset.entrypoint = `${el.dataset.entrypoint}-banner`
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
function setGAListeners(){
|
||||
function setGAListeners () {
|
||||
// Send "View" pings for any visible recommendation CTAs.
|
||||
sendRecommendationPings(".first-four-recs");
|
||||
sendRecommendationPings('.first-four-recs')
|
||||
|
||||
document.querySelectorAll(".send-ga-ping, [data-send-ga-ping]").forEach((el) => {
|
||||
el.addEventListener("click", (e) => {
|
||||
const eventCategory = e.target.dataset.eventCategory;
|
||||
const eventAction = e.target.dataset.eventAction;
|
||||
const eventLabel = e.target.dataset.eventLabel;
|
||||
ga("send", "event", eventCategory, eventAction, eventLabel, {transport: "beacon"});
|
||||
});
|
||||
});
|
||||
document.querySelectorAll('.send-ga-ping, [data-send-ga-ping]').forEach((el) => {
|
||||
el.addEventListener('click', (e) => {
|
||||
const eventCategory = e.target.dataset.eventCategory
|
||||
const eventAction = e.target.dataset.eventAction
|
||||
const eventLabel = e.target.dataset.eventLabel
|
||||
ga('send', 'event', eventCategory, eventAction, eventLabel, { transport: 'beacon' })
|
||||
})
|
||||
})
|
||||
|
||||
// Update data-event-category and data-fxa-entrypoint if the element
|
||||
// is nested inside a sign up banner.
|
||||
document.querySelectorAll("#scan-user-email, .open-oauth").forEach(el => {
|
||||
setMetricsIds(el);
|
||||
});
|
||||
document.querySelectorAll('#scan-user-email, .open-oauth').forEach(el => {
|
||||
setMetricsIds(el)
|
||||
})
|
||||
|
||||
|
||||
document.querySelectorAll(".open-oauth").forEach( async(el) => {
|
||||
const fxaUrl = new URL("/metrics-flow?", document.body.dataset.fxaAddress);
|
||||
document.querySelectorAll('.open-oauth').forEach(async (el) => {
|
||||
const fxaUrl = new URL('/metrics-flow?', document.body.dataset.fxaAddress)
|
||||
|
||||
try {
|
||||
const response = await fetch(fxaUrl, {credentials: "omit"});
|
||||
fxaUrl.searchParams.append("entrypoint", encodeURIComponent(el.dataset.entrypoint));
|
||||
const response = await fetch(fxaUrl, { credentials: 'omit' })
|
||||
fxaUrl.searchParams.append('entrypoint', encodeURIComponent(el.dataset.entrypoint))
|
||||
if (response && response.status === 200) {
|
||||
const {flowId, flowBeginTime} = await response.json();
|
||||
el.dataset.flowId = flowId;
|
||||
el.dataset.flowBeginTime = flowBeginTime;
|
||||
const { flowId, flowBeginTime } = await response.json()
|
||||
el.dataset.flowId = flowId
|
||||
el.dataset.flowBeginTime = flowBeginTime
|
||||
}
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
// should we do anything with this?
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
if (typeof(ga) !== "undefined") {
|
||||
const pageLocation = getLocation();
|
||||
if (typeof (ga) !== 'undefined') {
|
||||
const pageLocation = getLocation()
|
||||
|
||||
// Elements for which we send Google Analytics "View" pings...
|
||||
const eventTriggers = [
|
||||
"#scan-user-email",
|
||||
"#add-another-email-form",
|
||||
".open-oauth:not(.product-promo-wrapper)", // The promo entrypoint events are handled elsewhere.
|
||||
"#vpnPromoCloseButton",
|
||||
];
|
||||
'#scan-user-email',
|
||||
'#add-another-email-form',
|
||||
'.open-oauth:not(.product-promo-wrapper)', // The promo entrypoint events are handled elsewhere.
|
||||
'#vpnPromoCloseButton'
|
||||
]
|
||||
// Send number of foundBreaches on Scan, Full Report, and User Dashboard pageviews
|
||||
if (pageLocation === ("Scan Results")) {
|
||||
const breaches = document.querySelectorAll(".breach-card");
|
||||
ga("send", "event", "[v2] Breach Count", "Returned Breaches", `${pageLocation}`, breaches.length);
|
||||
if (pageLocation === ('Scan Results')) {
|
||||
const breaches = document.querySelectorAll('.breach-card')
|
||||
ga('send', 'event', '[v2] Breach Count', 'Returned Breaches', `${pageLocation}`, breaches.length)
|
||||
}
|
||||
|
||||
// Send "View" pings and add event listeners.
|
||||
document.querySelectorAll(eventTriggers).forEach(el => {
|
||||
sendPing(el, "View", pageLocation, {nonInteraction: true});
|
||||
if (["BUTTON", "A"].includes(el.tagName)) {
|
||||
el.addEventListener("click", async(e) => {
|
||||
await sendPing(el, "Engage", pageLocation, {transport: "beacon"});
|
||||
});
|
||||
sendPing(el, 'View', pageLocation, { nonInteraction: true })
|
||||
if (['BUTTON', 'A'].includes(el.tagName)) {
|
||||
el.addEventListener('click', async (e) => {
|
||||
await sendPing(el, 'Engage', pageLocation, { transport: 'beacon' })
|
||||
})
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
// Add event listeners to event triggering elements
|
||||
// for which we do not send "View" pings.
|
||||
document.querySelectorAll("[data-ga-link]").forEach((el) => {
|
||||
el.addEventListener("click", async(e) => {
|
||||
const linkId = `Link ID: ${e.target.dataset.eventLabel}`;
|
||||
await sendPing(el, "Click", `${linkId}`);
|
||||
});
|
||||
});
|
||||
document.querySelectorAll('[data-ga-link]').forEach((el) => {
|
||||
el.addEventListener('click', async (e) => {
|
||||
const linkId = `Link ID: ${e.target.dataset.eventLabel}`
|
||||
await sendPing(el, 'Click', `${linkId}`)
|
||||
})
|
||||
})
|
||||
|
||||
document.querySelectorAll("video").forEach((el) => {
|
||||
el.addEventListener("play", async (e) => {
|
||||
if (e.target.currentTime > 0) return; // only track initial play event
|
||||
e.target.dataset.eventCategory = "video play";
|
||||
await sendPing(e.target, "Click", e.target.src);
|
||||
});
|
||||
});
|
||||
document.querySelectorAll('video').forEach((el) => {
|
||||
el.addEventListener('play', async (e) => {
|
||||
if (e.target.currentTime > 0) return // only track initial play event
|
||||
e.target.dataset.eventCategory = 'video play'
|
||||
await sendPing(e.target, 'Click', e.target.src)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
window.sessionStorage.setItem("gaInit", true);
|
||||
window.sessionStorage.setItem('gaInit', true)
|
||||
}
|
||||
|
||||
function isGoogleAnalyticsAvailable() {
|
||||
return (typeof(ga) !== "undefined");
|
||||
function isGoogleAnalyticsAvailable () {
|
||||
return (typeof (ga) !== 'undefined')
|
||||
}
|
||||
|
||||
(() => {
|
||||
const win = window;
|
||||
const winLocationSearch = win.location.search;
|
||||
const win = window
|
||||
const winLocationSearch = win.location.search
|
||||
|
||||
let winLocation = win.location;
|
||||
let winLocation = win.location
|
||||
|
||||
// Check for DoNotTrack header before running GA script
|
||||
if (!_dntEnabled()) {
|
||||
(function(i,s,o,g,r,a,m){i["GoogleAnalyticsObject"]=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments);},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m);
|
||||
})(window,document,"script","https://www.google-analytics.com/analytics.js","ga");
|
||||
(function (i, s, o, g, r, a, m) {
|
||||
i.GoogleAnalyticsObject = r; i[r] = i[r] || function () {
|
||||
(i[r].q = i[r].q || []).push(arguments)
|
||||
}, i[r].l = 1 * new Date(); a = s.createElement(o),
|
||||
m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m)
|
||||
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga')
|
||||
}
|
||||
|
||||
// Remove token and hash values from URL so that they aren't sent to GA
|
||||
if (winLocationSearch.includes("token=") || winLocationSearch.includes("hash=")) {
|
||||
winLocation = winLocation.toString().replace(/[?&]token=[A-Za-z0-9_-]+/, "").replace(/&hash=[A-Za-z0-9_-]+/, "");
|
||||
win.history.replaceState({}, "", winLocation);
|
||||
if (winLocationSearch.includes('token=') || winLocationSearch.includes('hash=')) {
|
||||
winLocation = winLocation.toString().replace(/[?&]token=[A-Za-z0-9_-]+/, '').replace(/&hash=[A-Za-z0-9_-]+/, '')
|
||||
win.history.replaceState({}, '', winLocation)
|
||||
}
|
||||
|
||||
const gaEnabled = (typeof(ga) !== "undefined");
|
||||
const utmParamsInUrl = (winLocationSearch.includes("utm_"));
|
||||
const gaEnabled = (typeof (ga) !== 'undefined')
|
||||
const utmParamsInUrl = (winLocationSearch.includes('utm_'))
|
||||
|
||||
const removeUtmsFromUrl = () => {
|
||||
if (utmParamsInUrl) {
|
||||
win.history.replaceState({}, "", winLocation.toString().replace(/[?&]utm_.*/g, ""));
|
||||
win.history.replaceState({}, '', winLocation.toString().replace(/[?&]utm_.*/g, ''))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Store UTM params in session
|
||||
if (utmParamsInUrl) {
|
||||
saveReferringPageData(new URL(winLocation).searchParams);
|
||||
saveReferringPageData(new URL(winLocation).searchParams)
|
||||
}
|
||||
|
||||
const gaInit = new Event("gaInit");
|
||||
const gaInit = new Event('gaInit')
|
||||
|
||||
if (gaEnabled) {
|
||||
ga("create", "UA-77033033-16");
|
||||
ga("set", "anonymizeIp", true);
|
||||
ga("set", "dimension6", `${document.body.dataset.signedInUser}`);
|
||||
ga('create', 'UA-77033033-16')
|
||||
ga('set', 'anonymizeIp', true)
|
||||
ga('set', 'dimension6', `${document.body.dataset.signedInUser}`)
|
||||
|
||||
ga("send", "pageview", {
|
||||
hitCallback: function() {
|
||||
removeUtmsFromUrl();
|
||||
sessionStorage.removeItem("gaInit");
|
||||
document.dispatchEvent(gaInit);
|
||||
},
|
||||
});
|
||||
|
||||
document.addEventListener("gaInit", (e) => {
|
||||
if (sessionStorage.getItem("gaInit")) {
|
||||
return;
|
||||
ga('send', 'pageview', {
|
||||
hitCallback: function () {
|
||||
removeUtmsFromUrl()
|
||||
sessionStorage.removeItem('gaInit')
|
||||
document.dispatchEvent(gaInit)
|
||||
}
|
||||
setGAListeners();
|
||||
}, false);
|
||||
})
|
||||
|
||||
document.addEventListener('gaInit', (e) => {
|
||||
if (sessionStorage.getItem('gaInit')) {
|
||||
return
|
||||
}
|
||||
setGAListeners()
|
||||
}, false)
|
||||
} else {
|
||||
removeUtmsFromUrl();
|
||||
removeUtmsFromUrl()
|
||||
}
|
||||
|
||||
|
||||
})();
|
||||
})()
|
||||
|
|
|
@ -1,71 +1,70 @@
|
|||
"use strict";
|
||||
|
||||
'use strict'
|
||||
|
||||
// open/close signed-in user fxa menu and set tabbing
|
||||
function toggleMenu (evt) {
|
||||
evt.stopPropagation();
|
||||
const otherFocusableEls = document.querySelectorAll("button, a:not(.fxa-menu-link), input");
|
||||
const fxaMenuLinks = document.querySelectorAll(".fxa-menu-link");
|
||||
evt.stopPropagation()
|
||||
const otherFocusableEls = document.querySelectorAll('button, a:not(.fxa-menu-link), input')
|
||||
const fxaMenuLinks = document.querySelectorAll('.fxa-menu-link')
|
||||
|
||||
const bodyClassList = document.body.classList;
|
||||
bodyClassList.toggle("menu-open");
|
||||
if (bodyClassList.contains("menu-open")) {
|
||||
const bento = document.querySelector("firefox-apps");
|
||||
const bodyClassList = document.body.classList
|
||||
bodyClassList.toggle('menu-open')
|
||||
if (bodyClassList.contains('menu-open')) {
|
||||
const bento = document.querySelector('firefox-apps')
|
||||
if (bento._active) {
|
||||
const closeBentoEvent = new Event("close-bento-menu");
|
||||
bento.dispatchEvent(closeBentoEvent);
|
||||
const closeBentoEvent = new Event('close-bento-menu')
|
||||
bento.dispatchEvent(closeBentoEvent)
|
||||
}
|
||||
document.addEventListener("bento-was-opened", toggleMenu);
|
||||
window.addEventListener("click", toggleMenu);
|
||||
document.addEventListener('bento-was-opened', toggleMenu)
|
||||
window.addEventListener('click', toggleMenu)
|
||||
|
||||
otherFocusableEls.forEach(el => {
|
||||
el.tabIndex = -1;
|
||||
});
|
||||
el.tabIndex = -1
|
||||
})
|
||||
fxaMenuLinks.forEach(link => {
|
||||
link.tabIndex = 0;
|
||||
});
|
||||
return;
|
||||
link.tabIndex = 0
|
||||
})
|
||||
return
|
||||
}
|
||||
otherFocusableEls.forEach(el => {
|
||||
el.tabIndex = 0;
|
||||
});
|
||||
el.tabIndex = 0
|
||||
})
|
||||
fxaMenuLinks.forEach(link => {
|
||||
link.tabIndex = -1;
|
||||
});
|
||||
link.tabIndex = -1
|
||||
})
|
||||
|
||||
document.removeEventListener("bento-was-opened", toggleMenu);
|
||||
window.removeEventListener("click", toggleMenu);
|
||||
document.removeEventListener('bento-was-opened', toggleMenu)
|
||||
window.removeEventListener('click', toggleMenu)
|
||||
}
|
||||
|
||||
const avatar = document.querySelector(".avatar-wrapper");
|
||||
const avatar = document.querySelector('.avatar-wrapper')
|
||||
if (avatar) {
|
||||
avatar.addEventListener("click", toggleMenu);
|
||||
const fxaMenuLinks = document.querySelectorAll(".fxa-menu-link");
|
||||
avatar.addEventListener('click', toggleMenu)
|
||||
const fxaMenuLinks = document.querySelectorAll('.fxa-menu-link')
|
||||
|
||||
avatar.addEventListener("focus", () => {
|
||||
avatar.addEventListener("keydown", (e) => {
|
||||
avatar.addEventListener('focus', () => {
|
||||
avatar.addEventListener('keydown', (e) => {
|
||||
// open menu on space bar (keyCode:32) or enter (keyCode:13) clicks
|
||||
if ([32, 13].includes(e.keyCode)) {
|
||||
e.preventDefault(); // prevents page from jumping or scrolling down
|
||||
e.stopImmediatePropagation();
|
||||
return toggleMenu(e);
|
||||
e.preventDefault() // prevents page from jumping or scrolling down
|
||||
e.stopImmediatePropagation()
|
||||
return toggleMenu(e)
|
||||
}
|
||||
// close menu on escape (keyCode:27) clicks
|
||||
if (e.keyCode === 27 && document.body.classList.contains("menu-open")) {
|
||||
return toggleMenu(e);
|
||||
if (e.keyCode === 27 && document.body.classList.contains('menu-open')) {
|
||||
return toggleMenu(e)
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
|
||||
fxaMenuLinks.forEach(link => {
|
||||
link.addEventListener("focus", (e) => {
|
||||
link.addEventListener("keyup", (e) => {
|
||||
link.addEventListener('focus', (e) => {
|
||||
link.addEventListener('keyup', (e) => {
|
||||
if (e.keyCode === 27) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
toggleMenu(e);
|
||||
e.preventDefault()
|
||||
e.stopImmediatePropagation()
|
||||
toggleMenu(e)
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
/* global sendPing */
|
||||
/* global getFxaUtms */
|
||||
|
@ -6,324 +6,320 @@
|
|||
/* global sendRecommendationPings */
|
||||
/* global ga */
|
||||
|
||||
if (typeof TextEncoder === "undefined") {
|
||||
const cryptoScript = document.createElement("script");
|
||||
const scripts = document.getElementsByTagName("script")[0];
|
||||
cryptoScript.src = "/dist/edge.min.js";
|
||||
scripts.parentNode.insertBefore(cryptoScript, scripts);
|
||||
if (typeof TextEncoder === 'undefined') {
|
||||
const cryptoScript = document.createElement('script')
|
||||
const scripts = document.getElementsByTagName('script')[0]
|
||||
cryptoScript.src = '/dist/edge.min.js'
|
||||
scripts.parentNode.insertBefore(cryptoScript, scripts)
|
||||
}
|
||||
|
||||
|
||||
function findAncestor(el, cls) {
|
||||
function findAncestor (el, cls) {
|
||||
while ((el = el.parentElement) && !el.classList.contains(cls));
|
||||
return el;
|
||||
return el
|
||||
}
|
||||
|
||||
|
||||
function toggleEl(e) {
|
||||
const toggleButton = e.target;
|
||||
const toggleParent = findAncestor(toggleButton, "toggle-parent");
|
||||
["inactive", "active"].forEach(className => {
|
||||
toggleParent.classList.toggle(className);
|
||||
});
|
||||
function toggleEl (e) {
|
||||
const toggleButton = e.target
|
||||
const toggleParent = findAncestor(toggleButton, 'toggle-parent');
|
||||
['inactive', 'active'].forEach(className => {
|
||||
toggleParent.classList.toggle(className)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function isValidEmail(val) {
|
||||
function isValidEmail (val) {
|
||||
// https://stackoverflow.com/a/46181
|
||||
const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
return re.test(String(val).toLowerCase());
|
||||
const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
return re.test(String(val).toLowerCase())
|
||||
}
|
||||
|
||||
function getSubmittedEmail() {
|
||||
const email = document.querySelector("#scan-user-email input[type=email]").value;
|
||||
function getSubmittedEmail () {
|
||||
const email = document.querySelector('#scan-user-email input[type=email]').value
|
||||
if (!isValidEmail(email)) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
return email;
|
||||
return email
|
||||
}
|
||||
|
||||
function overwriteLastScannedEmail(email, scannedEmailId) {
|
||||
function overwriteLastScannedEmail (email, scannedEmailId) {
|
||||
if (!sessionStorage) {
|
||||
throw new Error("Session storage not available");
|
||||
throw new Error('Session storage not available')
|
||||
}
|
||||
sessionStorage.removeItem("lastScannedEmail");
|
||||
sessionStorage.setItem("lastScannedEmail", email);
|
||||
scannedEmailId.value = sessionStorage.length;
|
||||
sessionStorage.removeItem('lastScannedEmail')
|
||||
sessionStorage.setItem('lastScannedEmail', email)
|
||||
scannedEmailId.value = sessionStorage.length
|
||||
}
|
||||
|
||||
function doOauth(el, { emailWatch = false } = {}) {
|
||||
let url = new URL("/oauth/init", document.body.dataset.serverUrl);
|
||||
function doOauth (el, { emailWatch = false } = {}) {
|
||||
let url = new URL('/oauth/init', document.body.dataset.serverUrl)
|
||||
url = getFxaUtms(url);
|
||||
|
||||
["flowId", "flowBeginTime", "entrypoint", "entrypoint_experiment", "entrypoint_variation", "form_type"].forEach(key => {
|
||||
['flowId', 'flowBeginTime', 'entrypoint', 'entrypoint_experiment', 'entrypoint_variation', 'form_type'].forEach(key => {
|
||||
if (el.dataset[key]) {
|
||||
url.searchParams.append(key, encodeURIComponent(el.dataset[key]));
|
||||
url.searchParams.append(key, encodeURIComponent(el.dataset[key]))
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
if (!sessionStorage) {
|
||||
window.location.assign(url);
|
||||
return;
|
||||
window.location.assign(url)
|
||||
return
|
||||
}
|
||||
|
||||
const lastScannedEmail = sessionStorage.getItem("lastScannedEmail");
|
||||
const lastScannedEmail = sessionStorage.getItem('lastScannedEmail')
|
||||
|
||||
if (typeof emailWatch !== "boolean") {
|
||||
throw new Error("invalid argument option in doOauth");
|
||||
if (typeof emailWatch !== 'boolean') {
|
||||
throw new Error('invalid argument option in doOauth')
|
||||
}
|
||||
|
||||
if (!emailWatch) {
|
||||
// Preserve entire control function
|
||||
if (lastScannedEmail) {
|
||||
url.searchParams.append("email", lastScannedEmail);
|
||||
url.searchParams.append('email', lastScannedEmail)
|
||||
}
|
||||
window.location.assign(url);
|
||||
return;
|
||||
window.location.assign(url)
|
||||
return
|
||||
}
|
||||
|
||||
const submittedEmail = getSubmittedEmail();
|
||||
const scannedEmailId = document.querySelector("#scan-user-email input[name=scannedEmailId]");
|
||||
const submittedEmail = getSubmittedEmail()
|
||||
const scannedEmailId = document.querySelector('#scan-user-email input[name=scannedEmailId]')
|
||||
|
||||
if (lastScannedEmail === submittedEmail) {
|
||||
url.searchParams.append("email", lastScannedEmail);
|
||||
window.location.assign(url);
|
||||
return;
|
||||
url.searchParams.append('email', lastScannedEmail)
|
||||
window.location.assign(url)
|
||||
return
|
||||
}
|
||||
|
||||
// Use the email address the user submitted in FxA Oauth flow
|
||||
overwriteLastScannedEmail(submittedEmail, scannedEmailId);
|
||||
url.searchParams.append("email", submittedEmail);
|
||||
window.location.assign(url);
|
||||
overwriteLastScannedEmail(submittedEmail, scannedEmailId)
|
||||
url.searchParams.append('email', submittedEmail)
|
||||
window.location.assign(url)
|
||||
}
|
||||
|
||||
|
||||
function addFormListeners() {
|
||||
function addFormListeners () {
|
||||
Array.from(document.forms).forEach(form => {
|
||||
if (form.querySelector("input[type=email]")) {
|
||||
const emailInput = form.querySelector("input[type=email]");
|
||||
emailInput.addEventListener("keydown", (e) => {
|
||||
form.classList.remove("invalid");
|
||||
});
|
||||
if (form.querySelector('input[type=email]')) {
|
||||
const emailInput = form.querySelector('input[type=email]')
|
||||
emailInput.addEventListener('keydown', (e) => {
|
||||
form.classList.remove('invalid')
|
||||
})
|
||||
|
||||
emailInput.addEventListener("invalid", (e) => {
|
||||
e.preventDefault();
|
||||
form.classList.add("invalid");
|
||||
});
|
||||
emailInput.addEventListener('invalid', (e) => {
|
||||
e.preventDefault()
|
||||
form.classList.add('invalid')
|
||||
})
|
||||
|
||||
emailInput.addEventListener("keydown", () => {
|
||||
if (emailInput.value === "") {
|
||||
sendPing(form, "Engage");
|
||||
emailInput.addEventListener('keydown', () => {
|
||||
if (emailInput.value === '') {
|
||||
sendPing(form, 'Engage')
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
emailInput.addEventListener("focus", () => {
|
||||
if (emailInput.value === "") {
|
||||
sendPing(form, "Engage");
|
||||
emailInput.addEventListener('focus', () => {
|
||||
if (emailInput.value === '') {
|
||||
sendPing(form, 'Engage')
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
form.addEventListener("submit", (e) => handleFormSubmits(e), true);
|
||||
});
|
||||
form.addEventListener('submit', (e) => handleFormSubmits(e), true)
|
||||
})
|
||||
}
|
||||
|
||||
function handleFormSubmits(formEvent) {
|
||||
formEvent.preventDefault();
|
||||
const thisForm = formEvent.target;
|
||||
let email = "";
|
||||
function handleFormSubmits (formEvent) {
|
||||
formEvent.preventDefault()
|
||||
const thisForm = formEvent.target
|
||||
let email = ''
|
||||
|
||||
sendPing(thisForm, "Submit", null, { transport: "beacon" });
|
||||
sendPing(thisForm, 'Submit', null, { transport: 'beacon' })
|
||||
|
||||
if (thisForm.email) {
|
||||
email = thisForm.email.value.trim();
|
||||
thisForm.email.value = email;
|
||||
email = thisForm.email.value.trim()
|
||||
thisForm.email.value = email
|
||||
}
|
||||
|
||||
const formClassList = thisForm.classList;
|
||||
const formClassList = thisForm.classList
|
||||
|
||||
if (thisForm.email && !isValidEmail(email)) {
|
||||
sendPing(thisForm, "Failure");
|
||||
formClassList.add("invalid");
|
||||
return;
|
||||
sendPing(thisForm, 'Failure')
|
||||
formClassList.add('invalid')
|
||||
return
|
||||
}
|
||||
if (formClassList.contains("email-scan")) {
|
||||
hashEmailAndSend(formEvent);
|
||||
return;
|
||||
if (formClassList.contains('email-scan')) {
|
||||
hashEmailAndSend(formEvent)
|
||||
return
|
||||
}
|
||||
// if the form contains the class "loading-data", it has
|
||||
// already been submitted, so return without re-submitting.
|
||||
if (formClassList.contains("loading-data")) {
|
||||
return;
|
||||
if (formClassList.contains('loading-data')) {
|
||||
return
|
||||
}
|
||||
formClassList.add("loading-data");
|
||||
return thisForm.submit();
|
||||
formClassList.add('loading-data')
|
||||
return thisForm.submit()
|
||||
}
|
||||
|
||||
//re-enables inputs and clears loader
|
||||
function restoreInputs() {
|
||||
// re-enables inputs and clears loader
|
||||
function restoreInputs () {
|
||||
Array.from(document.forms).forEach(form => {
|
||||
form.classList.remove("loading-data");
|
||||
form.classList.remove("invalid");
|
||||
});
|
||||
document.querySelectorAll("input").forEach(input => {
|
||||
form.classList.remove('loading-data')
|
||||
form.classList.remove('invalid')
|
||||
})
|
||||
document.querySelectorAll('input').forEach(input => {
|
||||
if (input.disabled) {
|
||||
input.disabled = false;
|
||||
input.disabled = false
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function toggleDropDownMenu(dropDownMenu) {
|
||||
if (dropDownMenu.classList.contains("mobile-menu-open")) {
|
||||
return dropDownMenu.classList.remove("mobile-menu-open");
|
||||
function toggleDropDownMenu (dropDownMenu) {
|
||||
if (dropDownMenu.classList.contains('mobile-menu-open')) {
|
||||
return dropDownMenu.classList.remove('mobile-menu-open')
|
||||
}
|
||||
return dropDownMenu.classList.add("mobile-menu-open");
|
||||
return dropDownMenu.classList.add('mobile-menu-open')
|
||||
}
|
||||
|
||||
function toggleArticles() {
|
||||
const windowWidth = window.innerWidth;
|
||||
const articleToggles = document.querySelectorAll(".st-toggle-wrapper, .relay-info.toggle-parent");
|
||||
function toggleArticles () {
|
||||
const windowWidth = window.innerWidth
|
||||
const articleToggles = document.querySelectorAll('.st-toggle-wrapper, .relay-info.toggle-parent')
|
||||
if (windowWidth > 600) {
|
||||
articleToggles.forEach(toggle => {
|
||||
toggle.classList.add("active");
|
||||
toggle.classList.remove("inactive");
|
||||
});
|
||||
return;
|
||||
toggle.classList.add('active')
|
||||
toggle.classList.remove('inactive')
|
||||
})
|
||||
return
|
||||
}
|
||||
articleToggles.forEach(toggle => {
|
||||
toggle.classList.remove("active");
|
||||
toggle.classList.add("inactive");
|
||||
});
|
||||
toggle.classList.remove('active')
|
||||
toggle.classList.add('inactive')
|
||||
})
|
||||
}
|
||||
|
||||
function hideShowNavBars(win, navBar, bentoButton) {
|
||||
function hideShowNavBars (win, navBar, bentoButton) {
|
||||
win.onscroll = function (e) {
|
||||
// catch a window that has resized from less than 600px
|
||||
// to greater than 600px and unhide navigation.
|
||||
if (win.innerWidth > 600) {
|
||||
navBar.classList = ["show-nav-bars"];
|
||||
return;
|
||||
navBar.classList = ['show-nav-bars']
|
||||
return
|
||||
}
|
||||
|
||||
if (win.pageYOffset < 100) {
|
||||
navBar.classList = ["show-nav-bars"];
|
||||
return;
|
||||
navBar.classList = ['show-nav-bars']
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
this.oldScroll < (this.scrollY - 50) &&
|
||||
navBar.classList.contains("show-nav-bars") &&
|
||||
navBar.classList.contains('show-nav-bars') &&
|
||||
!bentoButton._active
|
||||
) {
|
||||
navBar.classList = ["hide-nav-bars"];
|
||||
this.oldScroll = this.scrollY;
|
||||
return;
|
||||
navBar.classList = ['hide-nav-bars']
|
||||
this.oldScroll = this.scrollY
|
||||
return
|
||||
}
|
||||
|
||||
if (this.oldScroll > this.scrollY + 50) {
|
||||
navBar.classList = ["show-nav-bars"];
|
||||
this.oldScroll = this.scrollY;
|
||||
return;
|
||||
navBar.classList = ['show-nav-bars']
|
||||
this.oldScroll = this.scrollY
|
||||
return
|
||||
}
|
||||
this.oldScroll = this.scrollY;
|
||||
};
|
||||
this.oldScroll = this.scrollY
|
||||
}
|
||||
}
|
||||
|
||||
function toggleMobileFeatures(topNavBar) {
|
||||
const win = window;
|
||||
const windowWidth = win.innerWidth;
|
||||
function toggleMobileFeatures (topNavBar) {
|
||||
const win = window
|
||||
const windowWidth = win.innerWidth
|
||||
if (windowWidth > 800) {
|
||||
const emailCards = document.querySelectorAll(".breaches-dash.email-card:not(.zero-breaches)");
|
||||
const emailCards = document.querySelectorAll('.breaches-dash.email-card:not(.zero-breaches)')
|
||||
emailCards.forEach(card => {
|
||||
card.classList.add("active");
|
||||
});
|
||||
return;
|
||||
card.classList.add('active')
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const bentoButton = document.querySelector("firefox-apps");
|
||||
const closeActiveEmailCards = document.querySelectorAll(".breaches-dash.email-card.active");
|
||||
const bentoButton = document.querySelector('firefox-apps')
|
||||
const closeActiveEmailCards = document.querySelectorAll('.breaches-dash.email-card.active')
|
||||
closeActiveEmailCards.forEach(card => {
|
||||
card.classList.remove("active");
|
||||
});
|
||||
card.classList.remove('active')
|
||||
})
|
||||
|
||||
if (windowWidth < 600) {
|
||||
hideShowNavBars(win, topNavBar, bentoButton);
|
||||
addBentoObserver();
|
||||
hideShowNavBars(win, topNavBar, bentoButton)
|
||||
addBentoObserver()
|
||||
}
|
||||
}
|
||||
|
||||
function toggleHeaderStates(header, win) {
|
||||
function toggleHeaderStates (header, win) {
|
||||
if (win.outerWidth < 600) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (win.pageYOffset > 400) {
|
||||
header.classList.add("show-shadow");
|
||||
header.classList.add('show-shadow')
|
||||
} else {
|
||||
header.classList.remove("show-shadow");
|
||||
header.classList.remove('show-shadow')
|
||||
}
|
||||
}
|
||||
|
||||
function addMainNavListeners() {
|
||||
const inactiveNavLinks = document.querySelectorAll(".nav-link:not(.active-link)");
|
||||
function addMainNavListeners () {
|
||||
const inactiveNavLinks = document.querySelectorAll('.nav-link:not(.active-link)')
|
||||
inactiveNavLinks.forEach(link => {
|
||||
/* Remove the .active-link-underline class from any link
|
||||
that isn't the current ".active-link" which occasionally
|
||||
happens when the user navigates to a page using browser
|
||||
backwards/forwards buttons. */
|
||||
if (link.classList.contains("active-link-underline")) {
|
||||
link.classList.remove("active-link-underline");
|
||||
if (link.classList.contains('active-link-underline')) {
|
||||
link.classList.remove('active-link-underline')
|
||||
}
|
||||
link.addEventListener("mouseenter", () => {
|
||||
link.classList.add("active-link-underline");
|
||||
});
|
||||
link.addEventListener("mouseleave", () => {
|
||||
link.classList.remove("active-link-underline");
|
||||
});
|
||||
});
|
||||
link.addEventListener('mouseenter', () => {
|
||||
link.classList.add('active-link-underline')
|
||||
})
|
||||
link.addEventListener('mouseleave', () => {
|
||||
link.classList.remove('active-link-underline')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function addBentoObserver() {
|
||||
const bodyClasses = document.body.classList;
|
||||
const bentoButton = document.querySelector("firefox-apps");
|
||||
const observerConfig = { attributes: true };
|
||||
function addBentoObserver () {
|
||||
const bodyClasses = document.body.classList
|
||||
const bentoButton = document.querySelector('firefox-apps')
|
||||
const observerConfig = { attributes: true }
|
||||
const watchBentoChanges = function (bentoEl, observer) {
|
||||
for (const mutation of bentoEl) {
|
||||
if (mutation.type === "attributes") {
|
||||
bodyClasses.toggle("bento-open", bentoButton._active);
|
||||
if (mutation.type === 'attributes') {
|
||||
bodyClasses.toggle('bento-open', bentoButton._active)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
if (bentoButton) {
|
||||
const observer = new MutationObserver(watchBentoChanges);
|
||||
observer.observe(bentoButton, observerConfig);
|
||||
const observer = new MutationObserver(watchBentoChanges)
|
||||
observer.observe(bentoButton, observerConfig)
|
||||
}
|
||||
}
|
||||
|
||||
function setHeaderHeight() {
|
||||
const header = document.getElementById("header");
|
||||
const height = header?.offsetHeight || 0;
|
||||
function setHeaderHeight () {
|
||||
const header = document.getElementById('header')
|
||||
const height = header?.offsetHeight || 0
|
||||
|
||||
document.body.style.setProperty("--header-height", `${height}px`);
|
||||
document.body.style.setProperty('--header-height', `${height}px`)
|
||||
}
|
||||
|
||||
function recruitmentLogic() {
|
||||
const recruitmentBannerLink = document.querySelector("#recruitment-banner");
|
||||
function recruitmentLogic () {
|
||||
const recruitmentBannerLink = document.querySelector('#recruitment-banner')
|
||||
if (!recruitmentBannerLink) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const recruited = document.cookie.split("; ").some((item) => item.trim().startsWith("recruited="));
|
||||
const recruited = document.cookie.split('; ').some((item) => item.trim().startsWith('recruited='))
|
||||
if (recruited) {
|
||||
recruitmentBannerLink.parentElement.remove();
|
||||
return;
|
||||
recruitmentBannerLink.parentElement.remove()
|
||||
return
|
||||
} else {
|
||||
recruitmentBannerLink.removeAttribute("hidden");
|
||||
recruitmentBannerLink.removeAttribute('hidden')
|
||||
}
|
||||
|
||||
recruitmentBannerLink.addEventListener("click", () => {
|
||||
const date = new Date();
|
||||
date.setTime(date.getTime() + 30 * 24 * 60 * 60 * 1000);
|
||||
document.cookie = "recruited=true; expires=" + date.toUTCString();
|
||||
});
|
||||
recruitmentBannerLink.addEventListener('click', () => {
|
||||
const date = new Date()
|
||||
date.setTime(date.getTime() + 30 * 24 * 60 * 60 * 1000)
|
||||
document.cookie = 'recruited=true; expires=' + date.toUTCString()
|
||||
})
|
||||
}
|
||||
|
||||
// function addWaitlistSignupButtonListeners() {
|
||||
|
@ -366,7 +362,6 @@ function recruitmentLogic() {
|
|||
// const availableIntersectionObserver = ("IntersectionObserver" in window);
|
||||
// const gaAvailable = typeof (ga) !== undefined;
|
||||
|
||||
|
||||
// if (availableIntersectionObserver && gaAvailable) {
|
||||
// const sendWaitlistViewPing = elemData => {
|
||||
// if (elemData.userIsSignedUp === "true") {
|
||||
|
@ -403,251 +398,246 @@ function recruitmentLogic() {
|
|||
// }
|
||||
// }
|
||||
|
||||
async function initVpnBanner() {
|
||||
const vpnBanner = document.querySelector(".vpn-banner");
|
||||
async function initVpnBanner () {
|
||||
const vpnBanner = document.querySelector('.vpn-banner')
|
||||
|
||||
if (!vpnBanner) return;
|
||||
if (!vpnBanner) return
|
||||
|
||||
const resizeObserver = new ResizeObserver(entries => updateHeight(entries[0].contentRect.height));
|
||||
resizeObserver.observe(vpnBanner); // call before `await` for initial height render
|
||||
const resizeObserver = new ResizeObserver(entries => updateHeight(entries[0].contentRect.height))
|
||||
resizeObserver.observe(vpnBanner) // call before `await` for initial height render
|
||||
|
||||
const locationDataReq = new Request("/iplocation");
|
||||
const protectionDataReq = new Request("https://am.i.mullvad.net/json");
|
||||
const cache = await initCache();
|
||||
const locationDataReq = new Request('/iplocation')
|
||||
const protectionDataReq = new Request('https://am.i.mullvad.net/json')
|
||||
const cache = await initCache()
|
||||
const locationData = await fetch(locationDataReq)
|
||||
.then(res => res.json())
|
||||
.catch(e => console.warn("Error fetching location data.", e));
|
||||
.catch(e => console.warn('Error fetching location data.', e))
|
||||
|
||||
let protectionData = await getCacheData(protectionDataReq);
|
||||
let protectionData = await getCacheData(protectionDataReq)
|
||||
|
||||
if (!protectionData || protectionData.ip !== locationData?.clientIp) {
|
||||
// get fresh data if none cached or user IP changed since last cached response
|
||||
protectionData = await fetchData(protectionDataReq).then(data => {
|
||||
if (!data) return null;
|
||||
return { ip: data.ip, isProtected: data.mullvad_exit_ip };
|
||||
});
|
||||
if (!data) return null
|
||||
return { ip: data.ip, isProtected: data.mullvad_exit_ip }
|
||||
})
|
||||
}
|
||||
|
||||
if (locationData?.clientIp) {
|
||||
vpnBanner.querySelector(".client-ip output").textContent = locationData.clientIp;
|
||||
vpnBanner.querySelector('.client-ip output').textContent = locationData.clientIp
|
||||
} else {
|
||||
vpnBanner.querySelector(".client-ip").remove();
|
||||
vpnBanner.querySelector('.client-ip').remove()
|
||||
}
|
||||
|
||||
if (locationData?.shortLocation) {
|
||||
vpnBanner.querySelector(".short-location output").textContent = locationData.shortLocation;
|
||||
vpnBanner.querySelector('.short-location output').textContent = locationData.shortLocation
|
||||
} else {
|
||||
vpnBanner.querySelector(".short-location").remove();
|
||||
vpnBanner.querySelector('.short-location').remove()
|
||||
}
|
||||
|
||||
if (locationData?.fullLocation) {
|
||||
vpnBanner.querySelector(".full-location output").textContent = locationData.fullLocation;
|
||||
vpnBanner.querySelector('.full-location output').textContent = locationData.fullLocation
|
||||
} else {
|
||||
vpnBanner.querySelector(".full-location").remove();
|
||||
vpnBanner.querySelector('.full-location').remove()
|
||||
}
|
||||
|
||||
vpnBanner.cta = vpnBanner.querySelector("a.vpn-banner-cta");
|
||||
vpnBanner.cta.setAttribute("href", vpnBanner.cta.getAttribute("href") + getPageAttribution());
|
||||
vpnBanner.setAttribute("data-protected", Boolean(protectionData?.isProtected));
|
||||
vpnBanner.addEventListener("click", handleClick);
|
||||
vpnBanner.cta = vpnBanner.querySelector('a.vpn-banner-cta')
|
||||
vpnBanner.cta.setAttribute('href', vpnBanner.cta.getAttribute('href') + getPageAttribution())
|
||||
vpnBanner.setAttribute('data-protected', Boolean(protectionData?.isProtected))
|
||||
vpnBanner.addEventListener('click', handleClick)
|
||||
|
||||
if (cache && protectionData) cache.put(protectionDataReq, new Response(JSON.stringify(protectionData)));
|
||||
if (cache && protectionData) cache.put(protectionDataReq, new Response(JSON.stringify(protectionData)))
|
||||
|
||||
async function initCache() {
|
||||
const cacheAvailable = "caches" in self;
|
||||
let cache;
|
||||
async function initCache () {
|
||||
const cacheAvailable = 'caches' in self
|
||||
let cache
|
||||
|
||||
if (cacheAvailable) cache = await caches.open("vpn-banner").catch(e => null);
|
||||
if (cacheAvailable) cache = await caches.open('vpn-banner').catch(e => null)
|
||||
|
||||
return cache;
|
||||
return cache
|
||||
}
|
||||
|
||||
async function getCacheData(req) {
|
||||
if (!cache) return null;
|
||||
async function getCacheData (req) {
|
||||
if (!cache) return null
|
||||
|
||||
const json = await cache.match(req)
|
||||
.then(res => res.json())
|
||||
.catch(e => console.warn("Could not get cached response.", e.message));
|
||||
.catch(e => console.warn('Could not get cached response.', e.message))
|
||||
|
||||
return json;
|
||||
return json
|
||||
}
|
||||
|
||||
async function fetchData(req, reqTimeoutMs = 4000) {
|
||||
const abortController = new AbortController();
|
||||
const timer = setTimeout(() => abortController.abort(), reqTimeoutMs); // abort a delayed response
|
||||
async function fetchData (req, reqTimeoutMs = 4000) {
|
||||
const abortController = new AbortController()
|
||||
const timer = setTimeout(() => abortController.abort(), reqTimeoutMs) // abort a delayed response
|
||||
const json = await fetch(req, { signal: abortController.signal })
|
||||
.then(res => {
|
||||
clearTimeout(timer);
|
||||
if (!res.ok) throw new Error(`Bad response (${res.status})`);
|
||||
return res.json();
|
||||
clearTimeout(timer)
|
||||
if (!res.ok) throw new Error(`Bad response (${res.status})`)
|
||||
return res.json()
|
||||
})
|
||||
.catch(e => console.warn("Error fetching protection data.", e));
|
||||
.catch(e => console.warn('Error fetching protection data.', e))
|
||||
|
||||
return json;
|
||||
return json
|
||||
}
|
||||
|
||||
function getPageAttribution() {
|
||||
let page = location.pathname;
|
||||
function getPageAttribution () {
|
||||
let page = location.pathname
|
||||
|
||||
if (page.startsWith("/")) page = page.slice(1);
|
||||
if (page.startsWith('/')) page = page.slice(1)
|
||||
|
||||
if (page === "") page = "home";
|
||||
if (page === '') page = 'home'
|
||||
|
||||
return `&utm_content=${page}`;
|
||||
return `&utm_content=${page}`
|
||||
}
|
||||
|
||||
function handleClick(e) {
|
||||
function handleClick (e) {
|
||||
switch (e.target.className) {
|
||||
case "vpn-banner-top":
|
||||
case "vpn-banner-close":
|
||||
vpnBanner.toggleAttribute("data-expanded");
|
||||
break;
|
||||
case 'vpn-banner-top':
|
||||
case 'vpn-banner-close':
|
||||
vpnBanner.toggleAttribute('data-expanded')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function updateHeight(h) {
|
||||
document.body.style.setProperty("--vpn-banner-height", `${Math.floor(h)}px`);
|
||||
function updateHeight (h) {
|
||||
document.body.style.setProperty('--vpn-banner-height', `${Math.floor(h)}px`)
|
||||
}
|
||||
}
|
||||
|
||||
async function initCsatBanner() {
|
||||
const csatBanner = document.querySelector(".csat-banner");
|
||||
async function initCsatBanner () {
|
||||
const csatBanner = document.querySelector('.csat-banner')
|
||||
|
||||
if (!csatBanner) return;
|
||||
if (!csatBanner) return
|
||||
|
||||
csatBanner.addEventListener("click", handleEvent);
|
||||
csatBanner.addEventListener('click', handleEvent)
|
||||
|
||||
function handleEvent(e) {
|
||||
const ttl = new Date();
|
||||
ttl.setDate(ttl.getDate() + 90);
|
||||
document.cookie = "csatHidden=1; path=/; sameSite=Lax; expires=" + ttl.toUTCString();
|
||||
function handleEvent (e) {
|
||||
const ttl = new Date()
|
||||
ttl.setDate(ttl.getDate() + 90)
|
||||
document.cookie = 'csatHidden=1; path=/; sameSite=Lax; expires=' + ttl.toUTCString()
|
||||
|
||||
switch (e.target.name) {
|
||||
case "csat-close-btn":
|
||||
csatBanner.toggleAttribute("hidden", true);
|
||||
csatBanner.removeEventListener("click", handleEvent);
|
||||
break;
|
||||
case "csat-option":
|
||||
csatBanner.toggleAttribute("disabled", true);
|
||||
csatBanner.querySelector(".csat-question").textContent = "Thanks for your feedback!";
|
||||
e.target.parentElement.classList.add("selected");
|
||||
if (window.ga) ga("send", "event", "CSAT banner", "submit", e.target.nextSibling.textContent, e.target.value);
|
||||
break;
|
||||
case 'csat-close-btn':
|
||||
csatBanner.toggleAttribute('hidden', true)
|
||||
csatBanner.removeEventListener('click', handleEvent)
|
||||
break
|
||||
case 'csat-option':
|
||||
csatBanner.toggleAttribute('disabled', true)
|
||||
csatBanner.querySelector('.csat-question').textContent = 'Thanks for your feedback!'
|
||||
e.target.parentElement.classList.add('selected')
|
||||
if (window.ga) ga('send', 'event', 'CSAT banner', 'submit', e.target.nextSibling.textContent, e.target.value)
|
||||
break
|
||||
}
|
||||
|
||||
setHeaderHeight();
|
||||
setHeaderHeight()
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
document.addEventListener("touchstart", function () { }, true);
|
||||
const win = window;
|
||||
const header = document.getElementById("header");
|
||||
const topNavigation = document.querySelector("#navigation-wrapper");
|
||||
document.addEventListener('touchstart', function () { }, true)
|
||||
const win = window
|
||||
const header = document.getElementById('header')
|
||||
const topNavigation = document.querySelector('#navigation-wrapper')
|
||||
|
||||
win.addEventListener("pageshow", function () {
|
||||
addMainNavListeners();
|
||||
toggleMobileFeatures(topNavigation);
|
||||
toggleArticles();
|
||||
toggleHeaderStates(header, win);
|
||||
document.forms ? (restoreInputs(), addFormListeners()) : null;
|
||||
});
|
||||
win.addEventListener('pageshow', function () {
|
||||
addMainNavListeners()
|
||||
toggleMobileFeatures(topNavigation)
|
||||
toggleArticles()
|
||||
toggleHeaderStates(header, win)
|
||||
document.forms ? (restoreInputs(), addFormListeners()) : null
|
||||
})
|
||||
|
||||
document.forms ? (restoreInputs(), addFormListeners()) : null;
|
||||
document.forms ? (restoreInputs(), addFormListeners()) : null
|
||||
|
||||
let windowWidth = win.outerWidth;
|
||||
document.addEventListener("DOMContentLoaded", setHeaderHeight);
|
||||
win.addEventListener("resize", () => {
|
||||
const newWindowWidth = win.outerWidth;
|
||||
let windowWidth = win.outerWidth
|
||||
document.addEventListener('DOMContentLoaded', setHeaderHeight)
|
||||
win.addEventListener('resize', () => {
|
||||
const newWindowWidth = win.outerWidth
|
||||
if (newWindowWidth !== windowWidth) {
|
||||
toggleMobileFeatures(topNavigation);
|
||||
toggleArticles();
|
||||
windowWidth = newWindowWidth;
|
||||
setHeaderHeight();
|
||||
toggleMobileFeatures(topNavigation)
|
||||
toggleArticles()
|
||||
windowWidth = newWindowWidth
|
||||
setHeaderHeight()
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
document.addEventListener("scroll", () => toggleHeaderStates(header, win));
|
||||
document.addEventListener('scroll', () => toggleHeaderStates(header, win))
|
||||
|
||||
document.querySelectorAll(".breach-logo:not(.lazy-img)").forEach(logo => {
|
||||
logo.addEventListener("error", (missingLogo) => {
|
||||
missingLogo.target.src = "/img/svg/placeholder.svg";
|
||||
});
|
||||
});
|
||||
document.querySelectorAll('.breach-logo:not(.lazy-img)').forEach(logo => {
|
||||
logo.addEventListener('error', (missingLogo) => {
|
||||
missingLogo.target.src = '/img/svg/placeholder.svg'
|
||||
})
|
||||
})
|
||||
|
||||
document.querySelectorAll(".toggle").forEach(toggle => {
|
||||
toggle.addEventListener("click", toggleEl);
|
||||
});
|
||||
document.querySelectorAll('.toggle').forEach(toggle => {
|
||||
toggle.addEventListener('click', toggleEl)
|
||||
})
|
||||
|
||||
document.querySelectorAll(".open-oauth").forEach(button => {
|
||||
button.addEventListener("click", (e) => doOauth(e.target));
|
||||
});
|
||||
document.querySelectorAll('.open-oauth').forEach(button => {
|
||||
button.addEventListener('click', (e) => doOauth(e.target))
|
||||
})
|
||||
|
||||
document.querySelectorAll("#see-additional-recs").forEach(button => {
|
||||
button.addEventListener("click", () => {
|
||||
button.classList.add("fade-out");
|
||||
const overflowRecs = document.getElementById("overflow-recs");
|
||||
overflowRecs.classList.remove("hide");
|
||||
if (typeof (ga) !== "undefined") {
|
||||
document.querySelectorAll('#see-additional-recs').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
button.classList.add('fade-out')
|
||||
const overflowRecs = document.getElementById('overflow-recs')
|
||||
overflowRecs.classList.remove('hide')
|
||||
if (typeof (ga) !== 'undefined') {
|
||||
// Send "Click" ping for #see-additional-recs click
|
||||
ga("send", "event", "Breach Details: See Additional Recommendations", "Click", "See Additional Recommendations");
|
||||
ga('send', 'event', 'Breach Details: See Additional Recommendations', 'Click', 'See Additional Recommendations')
|
||||
// Send "View" pings for any CTAs that become visible on #see-additional-recs click
|
||||
sendRecommendationPings(".overflow-rec-cta");
|
||||
sendRecommendationPings('.overflow-rec-cta')
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
|
||||
setHeaderHeight();
|
||||
recruitmentLogic();
|
||||
setHeaderHeight()
|
||||
recruitmentLogic()
|
||||
// addWaitlistSignupButtonListeners();
|
||||
// addWaitlistObservers();
|
||||
initVpnBanner();
|
||||
initCsatBanner();
|
||||
initVpnBanner()
|
||||
initCsatBanner()
|
||||
|
||||
const dropDownMenu = document.querySelector(".mobile-nav.show-mobile");
|
||||
dropDownMenu.addEventListener("click", () => toggleDropDownMenu(dropDownMenu));
|
||||
const dropDownMenu = document.querySelector('.mobile-nav.show-mobile')
|
||||
dropDownMenu.addEventListener('click', () => toggleDropDownMenu(dropDownMenu))
|
||||
|
||||
if (document.getElementById("fxaCheckbox")) {
|
||||
document.getElementById("fxaCheckbox").style.display = "block";
|
||||
if (document.getElementById('fxaCheckbox')) {
|
||||
document.getElementById('fxaCheckbox').style.display = 'block'
|
||||
}
|
||||
|
||||
const createFxaCheckbox = document.getElementById("createFxaCheckbox");
|
||||
const submitBtn = document.querySelector(".breachesSubmitButton");
|
||||
const createFxaCheckbox = document.getElementById('createFxaCheckbox')
|
||||
const submitBtn = document.querySelector('.breachesSubmitButton')
|
||||
|
||||
if (submitBtn) {
|
||||
submitBtn.addEventListener("click", (e) => {
|
||||
submitBtn.addEventListener('click', (e) => {
|
||||
// Email Validation
|
||||
const scanForm = document.getElementById("scan-user-email");
|
||||
const scanFormEmailValue = document.querySelector("#scan-user-email input[type='email']").value;
|
||||
const scanForm = document.getElementById('scan-user-email')
|
||||
const scanFormEmailValue = document.querySelector("#scan-user-email input[type='email']").value
|
||||
|
||||
if (scanFormEmailValue.length < 1 || !isValidEmail(scanFormEmailValue)) {
|
||||
scanForm.classList.add("invalid");
|
||||
return;
|
||||
scanForm.classList.add('invalid')
|
||||
return
|
||||
}
|
||||
|
||||
if (createFxaCheckbox.checked) {
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
|
||||
// Send GA Ping
|
||||
if (typeof (ga) !== "undefined") {
|
||||
ga("send", {
|
||||
hitType: "event",
|
||||
eventCategory: "Sign Up Button",
|
||||
eventAction: "Engage",
|
||||
eventLabel: "fx-monitor-homepage-fxa-checkbox",
|
||||
if (typeof (ga) !== 'undefined') {
|
||||
ga('send', {
|
||||
hitType: 'event',
|
||||
eventCategory: 'Sign Up Button',
|
||||
eventAction: 'Engage',
|
||||
eventLabel: 'fx-monitor-homepage-fxa-checkbox',
|
||||
options: {
|
||||
transport: "beacon",
|
||||
},
|
||||
});
|
||||
transport: 'beacon'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
doOauth(e.target, { emailWatch: true });
|
||||
return;
|
||||
doOauth(e.target, { emailWatch: true })
|
||||
}
|
||||
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
})();
|
||||
})()
|
||||
|
|
|
@ -1,81 +1,78 @@
|
|||
"use strict";
|
||||
'use strict';
|
||||
|
||||
/* global ga */
|
||||
/* global doOauth */
|
||||
|
||||
(()=> {
|
||||
function trapFocusInModal(modal, trapFocusInModal=true) {
|
||||
(() => {
|
||||
function trapFocusInModal (modal, trapFocusInModal = true) {
|
||||
const focusableEls = [
|
||||
"button",
|
||||
"input",
|
||||
"[href]",
|
||||
"[tabindex]",
|
||||
];
|
||||
const allFocusableEls = document.querySelectorAll(focusableEls);
|
||||
const focusableModalEls = modal.querySelectorAll(focusableEls);
|
||||
let allFocusableElsTabIndexValue = 0;
|
||||
let focusableModalElsTabIndexValue = -1;
|
||||
'button',
|
||||
'input',
|
||||
'[href]',
|
||||
'[tabindex]'
|
||||
]
|
||||
const allFocusableEls = document.querySelectorAll(focusableEls)
|
||||
const focusableModalEls = modal.querySelectorAll(focusableEls)
|
||||
let allFocusableElsTabIndexValue = 0
|
||||
let focusableModalElsTabIndexValue = -1
|
||||
if (trapFocusInModal) {
|
||||
allFocusableElsTabIndexValue = -1;
|
||||
focusableModalElsTabIndexValue = 0;
|
||||
allFocusableElsTabIndexValue = -1
|
||||
focusableModalElsTabIndexValue = 0
|
||||
}
|
||||
const setTabIndex = (elemArray, tabIndexValue) => {
|
||||
elemArray.forEach(el => {
|
||||
el.tabIndex = tabIndexValue;
|
||||
});
|
||||
};
|
||||
el.tabIndex = tabIndexValue
|
||||
})
|
||||
}
|
||||
|
||||
setTabIndex(allFocusableEls, allFocusableElsTabIndexValue);
|
||||
setTabIndex(focusableModalEls, focusableModalElsTabIndexValue);
|
||||
return;
|
||||
setTabIndex(allFocusableEls, allFocusableElsTabIndexValue)
|
||||
setTabIndex(focusableModalEls, focusableModalElsTabIndexValue)
|
||||
}
|
||||
|
||||
function sendBreachDetailAnalyticsPing(eventCategory, eventAction, eventLabel) {
|
||||
if (typeof(ga) !== "undefined") {
|
||||
function sendBreachDetailAnalyticsPing (eventCategory, eventAction, eventLabel) {
|
||||
if (typeof (ga) !== 'undefined') {
|
||||
// Set view pings as nonInteraction:true to get accurate bounce rate
|
||||
let options = {};
|
||||
if (eventAction !== "Engage"){
|
||||
options = {nonInteraction: true};
|
||||
let options = {}
|
||||
if (eventAction !== 'Engage') {
|
||||
options = { nonInteraction: true }
|
||||
}
|
||||
ga("send", "event", eventCategory, eventAction, eventLabel, options);
|
||||
ga('send', 'event', eventCategory, eventAction, eventLabel, options)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// set up IntersectionObserver to watch for event triggers on breach-details pages
|
||||
// and send "View" ping when they become visible in the viewport
|
||||
const resolveBtns = document.querySelectorAll(".resolve-button, a.what-to-do-next");
|
||||
const productPromos = document.querySelectorAll(".product-promo-wrapper");
|
||||
const gaEventTriggers = [...productPromos, ...resolveBtns];
|
||||
const resolveBtns = document.querySelectorAll('.resolve-button, a.what-to-do-next')
|
||||
const productPromos = document.querySelectorAll('.product-promo-wrapper')
|
||||
const gaEventTriggers = [...productPromos, ...resolveBtns]
|
||||
|
||||
const availableIntersectionObserver = ("IntersectionObserver" in window);
|
||||
const gaAvailable = typeof(ga) !== undefined;
|
||||
const availableIntersectionObserver = ('IntersectionObserver' in window)
|
||||
const gaAvailable = typeof (ga) !== undefined
|
||||
|
||||
// TODO: Store this in the dataset of breach resolution event triggers
|
||||
const resolutionEventCategory = "Breach Resolution";
|
||||
const resolutionEventCategory = 'Breach Resolution'
|
||||
|
||||
if (availableIntersectionObserver && gaEventTriggers && gaAvailable) {
|
||||
const onEventTriggersComingIntoView = (entries, observer) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const entryData = entry.target.dataset;
|
||||
const analyticsLabel = entryData.analyticsLabel;
|
||||
if (entry.target.classList.contains("product-promo-wrapper")) {
|
||||
sendBreachDetailAnalyticsPing(entryData.eventCategory, "View", analyticsLabel);
|
||||
observer.unobserve(entry.target);
|
||||
return;
|
||||
const entryData = entry.target.dataset
|
||||
const analyticsLabel = entryData.analyticsLabel
|
||||
if (entry.target.classList.contains('product-promo-wrapper')) {
|
||||
sendBreachDetailAnalyticsPing(entryData.eventCategory, 'View', analyticsLabel)
|
||||
observer.unobserve(entry.target)
|
||||
return
|
||||
}
|
||||
sendBreachDetailAnalyticsPing(resolutionEventCategory, "View", analyticsLabel);
|
||||
observer.unobserve(entry.target);
|
||||
return;
|
||||
sendBreachDetailAnalyticsPing(resolutionEventCategory, 'View', analyticsLabel)
|
||||
observer.unobserve(entry.target)
|
||||
}
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(onEventTriggersComingIntoView, { rootMargin: "0px"});
|
||||
const observer = new IntersectionObserver(onEventTriggersComingIntoView, { rootMargin: '0px' })
|
||||
gaEventTriggers.forEach(el => {
|
||||
observer.observe(el);
|
||||
});
|
||||
observer.observe(el)
|
||||
})
|
||||
}
|
||||
|
||||
// Fallback for older browsers without IntersectionObserver:
|
||||
|
@ -83,85 +80,84 @@
|
|||
// of whether or not the triggers are actually visible.
|
||||
if (!availableIntersectionObserver && gaEventTriggers && gaAvailable) {
|
||||
gaEventTriggers.forEach(el => {
|
||||
const elemData = el.dataset;
|
||||
if (el.classList.contains("product-promo-wrappper")) {
|
||||
return sendBreachDetailAnalyticsPing(elemData.eventCategory, "View - No IntersectionObserver", elemData.analyticsLabel);
|
||||
const elemData = el.dataset
|
||||
if (el.classList.contains('product-promo-wrappper')) {
|
||||
return sendBreachDetailAnalyticsPing(elemData.eventCategory, 'View - No IntersectionObserver', elemData.analyticsLabel)
|
||||
}
|
||||
sendBreachDetailAnalyticsPing(resolutionEventCategory, "View - No IntersectionObserver", elemData.analyticsLabel);
|
||||
});
|
||||
sendBreachDetailAnalyticsPing(resolutionEventCategory, 'View - No IntersectionObserver', elemData.analyticsLabel)
|
||||
})
|
||||
}
|
||||
|
||||
productPromos.forEach(promo => {
|
||||
promo.addEventListener("click", (e) => {
|
||||
const promoData = promo.dataset;
|
||||
sendBreachDetailAnalyticsPing(promoData.eventCategory, "Engage", promoData.analyticsLabel);
|
||||
if (promo.classList.contains("open-oauth")) {
|
||||
e.preventDefault();
|
||||
doOauth(promo);
|
||||
promo.addEventListener('click', (e) => {
|
||||
const promoData = promo.dataset
|
||||
sendBreachDetailAnalyticsPing(promoData.eventCategory, 'Engage', promoData.analyticsLabel)
|
||||
if (promo.classList.contains('open-oauth')) {
|
||||
e.preventDefault()
|
||||
doOauth(promo)
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
resolveBtns.forEach(btn => {
|
||||
btn.addEventListener("click", async(e) => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
if (gaAvailable) {
|
||||
sendBreachDetailAnalyticsPing(resolutionEventCategory, "Engage", btn.dataset.analyticsLabel);
|
||||
sendBreachDetailAnalyticsPing(resolutionEventCategory, 'Engage', btn.dataset.analyticsLabel)
|
||||
}
|
||||
|
||||
// If "What to do next" link is clicked, scroll to the list of recommendations.
|
||||
// The default behavior for this link results in the "What to do for this breach" headline
|
||||
// scrolling to the very top of the page and being covered up by the position:fixed header.
|
||||
if (btn.dataset.analyticsLabel === "what-to-do-next") {
|
||||
e.preventDefault();
|
||||
const recommendationsWrapper = document.getElementById("what-to-do-next");
|
||||
return window.scrollTo(0, recommendationsWrapper.offsetTop-100);
|
||||
if (btn.dataset.analyticsLabel === 'what-to-do-next') {
|
||||
e.preventDefault()
|
||||
const recommendationsWrapper = document.getElementById('what-to-do-next')
|
||||
return window.scrollTo(0, recommendationsWrapper.offsetTop - 100)
|
||||
}
|
||||
|
||||
btn.classList.add("loading");
|
||||
btn.classList.add('loading')
|
||||
|
||||
const confirmationModal = document.querySelector(".breach-resolution-modal");
|
||||
confirmationModal.classList.add("modal-loading");
|
||||
trapFocusInModal(confirmationModal, true);
|
||||
const confirmationModal = document.querySelector('.breach-resolution-modal')
|
||||
confirmationModal.classList.add('modal-loading')
|
||||
trapFocusInModal(confirmationModal, true)
|
||||
|
||||
const resolutionData = JSON.stringify(btn.dataset);
|
||||
const resolutionData = JSON.stringify(btn.dataset)
|
||||
try {
|
||||
const response = await fetch("/user/resolve-breach", {
|
||||
const response = await fetch('/user/resolve-breach', {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
mode: "same-origin",
|
||||
method: "POST",
|
||||
body: resolutionData,
|
||||
});
|
||||
mode: 'same-origin',
|
||||
method: 'POST',
|
||||
body: resolutionData
|
||||
})
|
||||
if (response && response.redirected === true) {
|
||||
return window.location.reload(true);
|
||||
return window.location.reload(true)
|
||||
}
|
||||
if (response) {
|
||||
const { headline, headlineClassName, progressMessage, progressStatus } = await response.json();
|
||||
confirmationModal.classList.add("modal-open");
|
||||
const goToDashboardBtn = confirmationModal.querySelector(".go-to-dash");
|
||||
goToDashboardBtn.focus();
|
||||
const closeModalBtns = confirmationModal.querySelectorAll(".close-modal");
|
||||
const { headline, headlineClassName, progressMessage, progressStatus } = await response.json()
|
||||
confirmationModal.classList.add('modal-open')
|
||||
const goToDashboardBtn = confirmationModal.querySelector('.go-to-dash')
|
||||
goToDashboardBtn.focus()
|
||||
const closeModalBtns = confirmationModal.querySelectorAll('.close-modal')
|
||||
closeModalBtns.forEach(closeModalBtn => {
|
||||
closeModalBtn.addEventListener("click", () => {
|
||||
return window.location.reload();
|
||||
});
|
||||
});
|
||||
closeModalBtn.addEventListener('click', () => {
|
||||
return window.location.reload()
|
||||
})
|
||||
})
|
||||
|
||||
const modalHeadline = document.getElementById("confirmation-headline");
|
||||
modalHeadline.textContent = headline;
|
||||
modalHeadline.classList.add(headlineClassName);
|
||||
const modalHeadline = document.getElementById('confirmation-headline')
|
||||
modalHeadline.textContent = headline
|
||||
modalHeadline.classList.add(headlineClassName)
|
||||
|
||||
const modalProgressMessage = document.getElementById("confirmation-progress-message");
|
||||
modalProgressMessage.textContent = progressMessage;
|
||||
const modalProgressMessage = document.getElementById('confirmation-progress-message')
|
||||
modalProgressMessage.textContent = progressMessage
|
||||
|
||||
const modalProgressStatus = document.getElementById("modal-progress-status");
|
||||
modalProgressStatus.textContent = progressStatus;
|
||||
const modalProgressStatus = document.getElementById('modal-progress-status')
|
||||
modalProgressStatus.textContent = progressStatus
|
||||
}
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
// What do we want to do here?
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
})
|
||||
})
|
||||
})()
|
||||
|
|
|
@ -1,46 +1,46 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
/* global libpolycrypt */
|
||||
/* global sendPing */
|
||||
|
||||
async function sha1(message) {
|
||||
message = message.toLowerCase();
|
||||
const msgBuffer = new TextEncoder("utf-8").encode(message);
|
||||
let hashBuffer;
|
||||
async function sha1 (message) {
|
||||
message = message.toLowerCase()
|
||||
const msgBuffer = new TextEncoder('utf-8').encode(message)
|
||||
let hashBuffer
|
||||
if (/edge/i.test(navigator.userAgent)) {
|
||||
hashBuffer = libpolycrypt.sha1(msgBuffer);
|
||||
hashBuffer = libpolycrypt.sha1(msgBuffer)
|
||||
} else {
|
||||
hashBuffer = await crypto.subtle.digest("SHA-1", msgBuffer);
|
||||
hashBuffer = await crypto.subtle.digest('SHA-1', msgBuffer)
|
||||
}
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
const hashHex = hashArray.map(b => ("00" + b.toString(16)).slice(-2)).join("");
|
||||
return hashHex.toUpperCase();
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
||||
const hashHex = hashArray.map(b => ('00' + b.toString(16)).slice(-2)).join('')
|
||||
return hashHex.toUpperCase()
|
||||
}
|
||||
|
||||
async function hashEmailAndSend(emailFormSubmitEvent) {
|
||||
emailFormSubmitEvent.preventDefault();
|
||||
const emailForm = emailFormSubmitEvent.target;
|
||||
const emailInput = document.getElementById("scan-email");
|
||||
const userEmail = emailInput.value;
|
||||
async function hashEmailAndSend (emailFormSubmitEvent) {
|
||||
emailFormSubmitEvent.preventDefault()
|
||||
const emailForm = emailFormSubmitEvent.target
|
||||
const emailInput = document.getElementById('scan-email')
|
||||
const userEmail = emailInput.value
|
||||
|
||||
// show loader and hash email
|
||||
emailForm.classList.add("loading-data");
|
||||
emailForm.querySelector("input[name=emailHash]").value = await sha1(userEmail);
|
||||
emailForm.classList.add('loading-data')
|
||||
emailForm.querySelector('input[name=emailHash]').value = await sha1(userEmail)
|
||||
|
||||
// set unhashed email in client's sessionStorage and send key to server
|
||||
// so we can pluck these out later in scan-results and not lose them on back clicks
|
||||
if (sessionStorage) {
|
||||
const lastScannedEmail = sessionStorage.getItem("lastScannedEmail");
|
||||
const lastScannedEmail = sessionStorage.getItem('lastScannedEmail')
|
||||
if (!lastScannedEmail || lastScannedEmail !== userEmail) {
|
||||
sessionStorage.removeItem("lastScannedEmail");
|
||||
sessionStorage.setItem("lastScannedEmail", userEmail);
|
||||
emailForm.querySelector("input[name=scannedEmailId]").value = sessionStorage.length;
|
||||
sessionStorage.removeItem('lastScannedEmail')
|
||||
sessionStorage.setItem('lastScannedEmail', userEmail)
|
||||
emailForm.querySelector('input[name=scannedEmailId]').value = sessionStorage.length
|
||||
}
|
||||
}
|
||||
|
||||
// clear input, send ping, and submit
|
||||
emailInput.value = "";
|
||||
sendPing(emailForm, "Success", null, {transport: "beacon"});
|
||||
emailForm.submit();
|
||||
emailInput.value = ''
|
||||
sendPing(emailForm, 'Success', null, { transport: 'beacon' })
|
||||
emailForm.submit()
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
"use strict";
|
||||
'use strict';
|
||||
|
||||
(() => {
|
||||
if (document.getElementById("scannedEmail")) {
|
||||
const scannedEmail = document.getElementById("scannedEmail");
|
||||
scannedEmail.textContent = sessionStorage.getItem("lastScannedEmail");
|
||||
if (document.getElementById('scannedEmail')) {
|
||||
const scannedEmail = document.getElementById('scannedEmail')
|
||||
scannedEmail.textContent = sessionStorage.getItem('lastScannedEmail')
|
||||
}
|
||||
})();
|
||||
})()
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const express = require("express");
|
||||
const { getBreachDetail } = require("../controllers/breach-details");
|
||||
const { asyncMiddleware } = require("../middleware");
|
||||
const express = require('express')
|
||||
const { getBreachDetail } = require('../controllers/breach-details')
|
||||
const { asyncMiddleware } = require('../middleware')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
const router = express.Router();
|
||||
router.get('/:breachName', asyncMiddleware(getBreachDetail))
|
||||
|
||||
|
||||
router.get("/:breachName", asyncMiddleware(getBreachDetail));
|
||||
|
||||
module.exports = router;
|
||||
module.exports = router
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const express = require("express");
|
||||
const {vers, heartbeat} = require("../controllers/dockerflow");
|
||||
const express = require('express')
|
||||
const { vers, heartbeat } = require('../controllers/dockerflow')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
|
||||
router.get("/__version__", vers);
|
||||
router.get("/__heartbeat__", heartbeat);
|
||||
router.get("/__lbheartbeat__", heartbeat);
|
||||
|
||||
|
||||
module.exports = router;
|
||||
router.get('/__version__', vers)
|
||||
router.get('/__heartbeat__', heartbeat)
|
||||
router.get('/__lbheartbeat__', heartbeat)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
"use strict";
|
||||
const express = require("express");
|
||||
const { getEmailMockUps, notFound } = require("../controllers/email-l10n");
|
||||
const router = express.Router();
|
||||
'use strict'
|
||||
const express = require('express')
|
||||
const { getEmailMockUps, notFound } = require('../controllers/email-l10n')
|
||||
const router = express.Router()
|
||||
|
||||
router.get('/', getEmailMockUps)
|
||||
router.use(notFound)
|
||||
|
||||
router.get("/", getEmailMockUps);
|
||||
router.use(notFound);
|
||||
|
||||
module.exports = router;
|
||||
module.exports = router
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const express = require("express");
|
||||
const bodyParser = require("body-parser");
|
||||
const express = require('express')
|
||||
const bodyParser = require('body-parser')
|
||||
|
||||
const bearerToken = require("express-bearer-token");
|
||||
const bearerToken = require('express-bearer-token')
|
||||
|
||||
const {asyncMiddleware} = require("../middleware");
|
||||
const {notify, breaches} = require("../controllers/hibp");
|
||||
const { asyncMiddleware } = require('../middleware')
|
||||
const { notify, breaches } = require('../controllers/hibp')
|
||||
|
||||
const router = express.Router()
|
||||
const jsonParser = bodyParser.json()
|
||||
|
||||
const router = express.Router();
|
||||
const jsonParser = bodyParser.json();
|
||||
router.use('/notify', bearerToken())
|
||||
router.post('/notify', jsonParser, asyncMiddleware(notify))
|
||||
router.get('/breaches', jsonParser, asyncMiddleware(breaches))
|
||||
|
||||
router.use("/notify", bearerToken());
|
||||
router.post("/notify", jsonParser, asyncMiddleware(notify));
|
||||
router.get("/breaches", jsonParser, asyncMiddleware(breaches));
|
||||
|
||||
module.exports = router;
|
||||
module.exports = router
|
||||
|
|
|
@ -1,34 +1,34 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const express = require("express");
|
||||
const bodyParser = require("body-parser");
|
||||
const csrf = require("csurf");
|
||||
const express = require('express')
|
||||
const bodyParser = require('body-parser')
|
||||
const csrf = require('csurf')
|
||||
|
||||
const {
|
||||
home, getAboutPage, getAllBreaches, getBentoStrings,
|
||||
getSecurityTips, notFound, removeMyData, addEmailToWaitlist,
|
||||
} = require("../controllers/home");
|
||||
const { getIpLocation } = require("../controllers/ip-location");
|
||||
getSecurityTips, notFound, removeMyData, addEmailToWaitlist
|
||||
} = require('../controllers/home')
|
||||
const { getIpLocation } = require('../controllers/ip-location')
|
||||
|
||||
const { getShareUTMs, requireSessionUser } = require("../middleware");
|
||||
const { getShareUTMs, requireSessionUser } = require('../middleware')
|
||||
|
||||
const csrfProtection = csrf();
|
||||
const jsonParser = bodyParser.json();
|
||||
const router = express.Router();
|
||||
const csrfProtection = csrf()
|
||||
const jsonParser = bodyParser.json()
|
||||
const router = express.Router()
|
||||
|
||||
router.get("/", csrfProtection, home);
|
||||
router.get("/share/orange", csrfProtection, getShareUTMs, home);
|
||||
router.get("/share/purple", csrfProtection, getShareUTMs, home);
|
||||
router.get("/share/blue", csrfProtection, getShareUTMs, home);
|
||||
router.get("/share/:breach", csrfProtection, getShareUTMs, home);
|
||||
router.get("/share/", csrfProtection, getShareUTMs, home);
|
||||
router.get("/about", getAboutPage);
|
||||
router.get("/breaches", getAllBreaches);
|
||||
router.get("/security-tips", getSecurityTips);
|
||||
router.get("/getBentoStrings", getBentoStrings);
|
||||
router.get("/remove-my-data", requireSessionUser, removeMyData);
|
||||
router.post("/join-waitlist", jsonParser, requireSessionUser, addEmailToWaitlist);
|
||||
router.get("/iplocation", getIpLocation);
|
||||
router.use(notFound);
|
||||
router.get('/', csrfProtection, home)
|
||||
router.get('/share/orange', csrfProtection, getShareUTMs, home)
|
||||
router.get('/share/purple', csrfProtection, getShareUTMs, home)
|
||||
router.get('/share/blue', csrfProtection, getShareUTMs, home)
|
||||
router.get('/share/:breach', csrfProtection, getShareUTMs, home)
|
||||
router.get('/share/', csrfProtection, getShareUTMs, home)
|
||||
router.get('/about', getAboutPage)
|
||||
router.get('/breaches', getAllBreaches)
|
||||
router.get('/security-tips', getSecurityTips)
|
||||
router.get('/getBentoStrings', getBentoStrings)
|
||||
router.get('/remove-my-data', requireSessionUser, removeMyData)
|
||||
router.post('/join-waitlist', jsonParser, requireSessionUser, addEmailToWaitlist)
|
||||
router.get('/iplocation', getIpLocation)
|
||||
router.use(notFound)
|
||||
|
||||
module.exports = router;
|
||||
module.exports = router
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const express = require("express");
|
||||
const bodyParser = require("body-parser");
|
||||
const express = require('express')
|
||||
const bodyParser = require('body-parser')
|
||||
|
||||
const {asyncMiddleware} = require("../middleware");
|
||||
const {init, confirmed} = require("../controllers/oauth");
|
||||
const { asyncMiddleware } = require('../middleware')
|
||||
const { init, confirmed } = require('../controllers/oauth')
|
||||
|
||||
const router = express.Router()
|
||||
const jsonParser = bodyParser.json()
|
||||
|
||||
const router = express.Router();
|
||||
const jsonParser = bodyParser.json();
|
||||
router.get('/init', jsonParser, init)
|
||||
router.get('/confirmed', jsonParser, asyncMiddleware(confirmed))
|
||||
|
||||
router.get("/init", jsonParser, init);
|
||||
router.get("/confirmed", jsonParser, asyncMiddleware(confirmed));
|
||||
|
||||
module.exports = router;
|
||||
module.exports = router
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const express = require("express");
|
||||
const bodyParser = require("body-parser");
|
||||
const csrf = require("csurf");
|
||||
const express = require('express')
|
||||
const bodyParser = require('body-parser')
|
||||
const csrf = require('csurf')
|
||||
|
||||
const { asyncMiddleware } = require("../middleware");
|
||||
const { post, get } = require("../controllers/scan");
|
||||
const { asyncMiddleware } = require('../middleware')
|
||||
const { post, get } = require('../controllers/scan')
|
||||
|
||||
const router = express.Router();
|
||||
const urlEncodedParser = bodyParser.urlencoded({ extended: false });
|
||||
const csrfProtection = csrf();
|
||||
const router = express.Router()
|
||||
const urlEncodedParser = bodyParser.urlencoded({ extended: false })
|
||||
const csrfProtection = csrf()
|
||||
|
||||
router.post('/', urlEncodedParser, csrfProtection, asyncMiddleware(post))
|
||||
router.get('/', get)
|
||||
|
||||
router.post("/", urlEncodedParser, csrfProtection, asyncMiddleware(post));
|
||||
router.get("/", get);
|
||||
|
||||
module.exports = router;
|
||||
module.exports = router
|
||||
|
|
|
@ -1,23 +1,22 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const bodyParser = require("body-parser");
|
||||
const express = require("express");
|
||||
const bodyParser = require('body-parser')
|
||||
const express = require('express')
|
||||
|
||||
const AppConstants = require("../app-constants");
|
||||
const mozlog = require("../log");
|
||||
const {notification} = require("../controllers/ses");
|
||||
const AppConstants = require('../app-constants')
|
||||
const mozlog = require('../log')
|
||||
const { notification } = require('../controllers/ses')
|
||||
|
||||
|
||||
const log = mozlog("routes.ses");
|
||||
const router = express.Router();
|
||||
const textParser = bodyParser.text();
|
||||
const log = mozlog('routes.ses')
|
||||
const router = express.Router()
|
||||
const textParser = bodyParser.text()
|
||||
|
||||
if (AppConstants.SES_NOTIFICATION_LOG_ONLY) {
|
||||
router.post("/notification", textParser, (req, res, next) => {
|
||||
log.info("ses-notification-body", { body: req.body });
|
||||
});
|
||||
router.post('/notification', textParser, (req, res, next) => {
|
||||
log.info('ses-notification-body', { body: req.body })
|
||||
})
|
||||
} else {
|
||||
router.post("/notification", textParser, notification);
|
||||
router.post('/notification', textParser, notification)
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
module.exports = router
|
||||
|
|
|
@ -1,38 +1,37 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const express = require("express");
|
||||
const bearerToken = require("express-bearer-token");
|
||||
const bodyParser = require("body-parser");
|
||||
const csrf = require("csurf");
|
||||
const express = require('express')
|
||||
const bearerToken = require('express-bearer-token')
|
||||
const bodyParser = require('body-parser')
|
||||
const csrf = require('csurf')
|
||||
|
||||
const { asyncMiddleware, requireSessionUser } = require("../middleware");
|
||||
const { asyncMiddleware, requireSessionUser } = require('../middleware')
|
||||
const {
|
||||
add, verify, logout,
|
||||
getDashboard, getPreferences, getBreachStats,
|
||||
removeEmail, resendEmail, updateCommunicationOptions,
|
||||
getUnsubscribe, postUnsubscribe, getRemoveFxm, postRemoveFxm, postResolveBreach,
|
||||
} = require("../controllers/user");
|
||||
getUnsubscribe, postUnsubscribe, getRemoveFxm, postRemoveFxm, postResolveBreach
|
||||
} = require('../controllers/user')
|
||||
|
||||
const router = express.Router();
|
||||
const jsonParser = bodyParser.json();
|
||||
const urlEncodedParser = bodyParser.urlencoded({ extended: false });
|
||||
const csrfProtection = csrf();
|
||||
const router = express.Router()
|
||||
const jsonParser = bodyParser.json()
|
||||
const urlEncodedParser = bodyParser.urlencoded({ extended: false })
|
||||
const csrfProtection = csrf()
|
||||
|
||||
|
||||
router.get("/dashboard", csrfProtection, requireSessionUser, asyncMiddleware(getDashboard));
|
||||
router.get("/preferences", csrfProtection, requireSessionUser, asyncMiddleware(getPreferences));
|
||||
router.use("/breach-stats", bearerToken());
|
||||
router.get("/breach-stats", urlEncodedParser, asyncMiddleware(getBreachStats));
|
||||
router.get("/logout", logout);
|
||||
router.post("/email", urlEncodedParser, csrfProtection, requireSessionUser, asyncMiddleware(add));
|
||||
router.post("/remove-email", urlEncodedParser, csrfProtection, requireSessionUser, asyncMiddleware(removeEmail));
|
||||
router.post("/resend-email", jsonParser, csrfProtection, requireSessionUser, asyncMiddleware(resendEmail));
|
||||
router.post("/update-comm-option", jsonParser, csrfProtection, requireSessionUser, asyncMiddleware(updateCommunicationOptions));
|
||||
router.get("/verify", jsonParser, asyncMiddleware(verify));
|
||||
router.use("/unsubscribe", urlEncodedParser);
|
||||
router.get("/unsubscribe", urlEncodedParser, asyncMiddleware(getUnsubscribe));
|
||||
router.post("/unsubscribe", csrfProtection, asyncMiddleware(postUnsubscribe));
|
||||
router.get("/remove-fxm", urlEncodedParser, csrfProtection, requireSessionUser, asyncMiddleware(getRemoveFxm));
|
||||
router.post("/remove-fxm", jsonParser, csrfProtection, requireSessionUser, asyncMiddleware(postRemoveFxm));
|
||||
router.post("/resolve-breach", jsonParser, urlEncodedParser, requireSessionUser, asyncMiddleware(postResolveBreach));
|
||||
module.exports = router;
|
||||
router.get('/dashboard', csrfProtection, requireSessionUser, asyncMiddleware(getDashboard))
|
||||
router.get('/preferences', csrfProtection, requireSessionUser, asyncMiddleware(getPreferences))
|
||||
router.use('/breach-stats', bearerToken())
|
||||
router.get('/breach-stats', urlEncodedParser, asyncMiddleware(getBreachStats))
|
||||
router.get('/logout', logout)
|
||||
router.post('/email', urlEncodedParser, csrfProtection, requireSessionUser, asyncMiddleware(add))
|
||||
router.post('/remove-email', urlEncodedParser, csrfProtection, requireSessionUser, asyncMiddleware(removeEmail))
|
||||
router.post('/resend-email', jsonParser, csrfProtection, requireSessionUser, asyncMiddleware(resendEmail))
|
||||
router.post('/update-comm-option', jsonParser, csrfProtection, requireSessionUser, asyncMiddleware(updateCommunicationOptions))
|
||||
router.get('/verify', jsonParser, asyncMiddleware(verify))
|
||||
router.use('/unsubscribe', urlEncodedParser)
|
||||
router.get('/unsubscribe', urlEncodedParser, asyncMiddleware(getUnsubscribe))
|
||||
router.post('/unsubscribe', csrfProtection, asyncMiddleware(postUnsubscribe))
|
||||
router.get('/remove-fxm', urlEncodedParser, csrfProtection, requireSessionUser, asyncMiddleware(getRemoveFxm))
|
||||
router.post('/remove-fxm', jsonParser, csrfProtection, requireSessionUser, asyncMiddleware(postRemoveFxm))
|
||||
router.post('/resolve-breach', jsonParser, urlEncodedParser, requireSessionUser, asyncMiddleware(postResolveBreach))
|
||||
module.exports = router
|
||||
|
|
132
scan-results.js
132
scan-results.js
|
@ -1,91 +1,90 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const { URL } = require("url");
|
||||
const { URL } = require('url')
|
||||
|
||||
const HIBP = require("./hibp");
|
||||
const sha1 = require("./sha1-utils");
|
||||
const HIBP = require('./hibp')
|
||||
const sha1 = require('./sha1-utils')
|
||||
|
||||
const AppConstants = require("./app-constants");
|
||||
const EXPERIMENTS_ENABLED = (AppConstants.EXPERIMENT_ACTIVE === "1");
|
||||
const { getExperimentFlags } = require("./controllers/utils");
|
||||
const AppConstants = require('./app-constants')
|
||||
const EXPERIMENTS_ENABLED = (AppConstants.EXPERIMENT_ACTIVE === '1')
|
||||
const { getExperimentFlags } = require('./controllers/utils')
|
||||
|
||||
const scanResult = async(req, selfScan=false) => {
|
||||
const scanResult = async (req, selfScan = false) => {
|
||||
const allBreaches = req.app.locals.breaches
|
||||
let scannedEmail = null
|
||||
|
||||
const allBreaches = req.app.locals.breaches;
|
||||
let scannedEmail = null;
|
||||
const experimentFlags = getExperimentFlags(req, EXPERIMENTS_ENABLED)
|
||||
|
||||
const experimentFlags = getExperimentFlags(req, EXPERIMENTS_ENABLED);
|
||||
|
||||
const title = req.fluentFormat("scan-title");
|
||||
let foundBreaches = [];
|
||||
let specificBreach = null;
|
||||
let doorhangerScan = false;
|
||||
let userCompromised = false;
|
||||
let signedInUser = null;
|
||||
let fullReport = false;
|
||||
let userDash = false;
|
||||
let scannedEmailId = null;
|
||||
const title = req.fluentFormat('scan-title')
|
||||
let foundBreaches = []
|
||||
let specificBreach = null
|
||||
let doorhangerScan = false
|
||||
let userCompromised = false
|
||||
let signedInUser = null
|
||||
let fullReport = false
|
||||
let userDash = false
|
||||
let scannedEmailId = null
|
||||
|
||||
if (req.session.user) {
|
||||
signedInUser = req.session.user;
|
||||
signedInUser = req.session.user
|
||||
}
|
||||
|
||||
// Checks if the user scanning their own verified email.
|
||||
if (req.body && req.body.emailHash) {
|
||||
scannedEmail = req.body.emailHash;
|
||||
scannedEmail = req.body.emailHash
|
||||
|
||||
if (req.body.scannedEmailId) {
|
||||
scannedEmailId = req.body.scannedEmailId;
|
||||
scannedEmailId = req.body.scannedEmailId
|
||||
}
|
||||
|
||||
if (signedInUser) {
|
||||
for (const emailAddress of signedInUser.email_addresses) {
|
||||
if (!selfScan && sha1(emailAddress.email) === req.body.emailHash) {
|
||||
selfScan = true;
|
||||
break;
|
||||
selfScan = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const url = new URL(req.url, req.app.locals.SERVER_URL);
|
||||
const url = new URL(req.url, req.app.locals.SERVER_URL)
|
||||
const thisBreach = (breach) => {
|
||||
return (element) => element.Name.toLowerCase() === breach.toLowerCase();
|
||||
};
|
||||
|
||||
// Checks for a signedInUser arriving from doorhanger.
|
||||
if (signedInUser && url.searchParams.has("utm_source") && url.searchParams.get("utm_source") === "firefox") {
|
||||
doorhangerScan = true, selfScan = true;
|
||||
specificBreach = allBreaches.find(thisBreach(req.query.breach));
|
||||
return (element) => element.Name.toLowerCase() === breach.toLowerCase()
|
||||
}
|
||||
|
||||
fullReport = url.pathname === "/full_report";
|
||||
// Checks for a signedInUser arriving from doorhanger.
|
||||
if (signedInUser && url.searchParams.has('utm_source') && url.searchParams.get('utm_source') === 'firefox') {
|
||||
doorhangerScan = true, selfScan = true
|
||||
specificBreach = allBreaches.find(thisBreach(req.query.breach))
|
||||
}
|
||||
|
||||
userDash = url.pathname === "/user_dashboard";
|
||||
fullReport = url.pathname === '/full_report'
|
||||
|
||||
userDash = url.pathname === '/user_dashboard'
|
||||
|
||||
if (selfScan) {
|
||||
scannedEmail = sha1(signedInUser.primary_email);
|
||||
scannedEmail = sha1(signedInUser.primary_email)
|
||||
}
|
||||
|
||||
if (scannedEmail) {
|
||||
// Gets sensitive breaches only if selfScan === true
|
||||
foundBreaches = await HIBP.getBreachesForEmail(scannedEmail, allBreaches, selfScan);
|
||||
foundBreaches = await HIBP.getBreachesForEmail(scannedEmail, allBreaches, selfScan)
|
||||
}
|
||||
|
||||
// Checks if scan originated from a breach detail/"featured breach" page.
|
||||
if (req.body && req.body.featuredBreach) {
|
||||
specificBreach = allBreaches.find(thisBreach(req.body.featuredBreach));
|
||||
specificBreach = allBreaches.find(thisBreach(req.body.featuredBreach))
|
||||
}
|
||||
|
||||
if (doorhangerScan || specificBreach) {
|
||||
const specificBreachIndex = foundBreaches.findIndex(breach => breach.Name === specificBreach.Name);
|
||||
const specificBreachIndex = foundBreaches.findIndex(breach => breach.Name === specificBreach.Name)
|
||||
|
||||
// Checks foundBreaches for specificBreach and if found,
|
||||
// brings specificBreach to front of foundBreaches list.
|
||||
if (specificBreachIndex !== -1) {
|
||||
userCompromised = true;
|
||||
foundBreaches.splice(specificBreachIndex, 1);
|
||||
foundBreaches.unshift(specificBreach);
|
||||
userCompromised = true
|
||||
foundBreaches.splice(specificBreachIndex, 1)
|
||||
foundBreaches.unshift(specificBreach)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,55 +99,54 @@ const scanResult = async(req, selfScan=false) => {
|
|||
fullReport,
|
||||
userDash,
|
||||
scannedEmailId,
|
||||
experimentFlags,
|
||||
};
|
||||
};
|
||||
experimentFlags
|
||||
}
|
||||
}
|
||||
|
||||
function resultsSummary(verifiedEmails) {
|
||||
function resultsSummary (verifiedEmails) {
|
||||
const breachStats = {
|
||||
monitoredEmails: {
|
||||
count: 0,
|
||||
count: 0
|
||||
},
|
||||
numBreaches: {
|
||||
count: 0,
|
||||
numResolved: 0,
|
||||
numResolved: 0
|
||||
},
|
||||
passwords: {
|
||||
count: 0,
|
||||
numResolved: 0,
|
||||
},
|
||||
};
|
||||
let foundBreaches = [];
|
||||
numResolved: 0
|
||||
}
|
||||
}
|
||||
let foundBreaches = []
|
||||
|
||||
// combine the breaches for each account, breach duplicates are ok
|
||||
// since the user may have multiple accounts with different emails
|
||||
verifiedEmails.forEach(email => {
|
||||
|
||||
email.breaches.forEach(breach => {
|
||||
if (breach.IsResolved) {
|
||||
breachStats.numBreaches.numResolved++;
|
||||
breachStats.numBreaches.numResolved++
|
||||
}
|
||||
|
||||
const dataClasses = breach.DataClasses;
|
||||
if (dataClasses.includes("passwords")) {
|
||||
breachStats.passwords.count++;
|
||||
const dataClasses = breach.DataClasses
|
||||
if (dataClasses.includes('passwords')) {
|
||||
breachStats.passwords.count++
|
||||
if (breach.IsResolved) {
|
||||
breachStats.passwords.numResolved++;
|
||||
breachStats.passwords.numResolved++
|
||||
}
|
||||
}
|
||||
});
|
||||
foundBreaches = [...foundBreaches, ...email.breaches];
|
||||
});
|
||||
})
|
||||
foundBreaches = [...foundBreaches, ...email.breaches]
|
||||
})
|
||||
|
||||
// total number of verified emails being monitored
|
||||
breachStats.monitoredEmails.count = verifiedEmails.length;
|
||||
breachStats.monitoredEmails.count = verifiedEmails.length
|
||||
|
||||
// total number of breaches across all emails
|
||||
breachStats.numBreaches.count = foundBreaches.length;
|
||||
return breachStats;
|
||||
breachStats.numBreaches.count = foundBreaches.length
|
||||
return breachStats
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
scanResult,
|
||||
resultsSummary,
|
||||
};
|
||||
resultsSummary
|
||||
}
|
||||
|
|
|
@ -1,51 +1,49 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const HIBP = require("../hibp");
|
||||
const HIBP = require('../hibp')
|
||||
|
||||
// https://stackoverflow.com/a/8528531
|
||||
function dhm(t){
|
||||
const cd = 24 * 60 * 60 * 1000,
|
||||
ch = 60 * 60 * 1000,
|
||||
pad = (n) => { return n < 10 ? "0" + n : n; };
|
||||
let d = Math.floor(t / cd),
|
||||
h = Math.floor( (t - d * cd) / ch),
|
||||
m = Math.round( (t - d * cd - h * ch) / 60000);
|
||||
if( m === 60 ){
|
||||
h++;
|
||||
m = 0;
|
||||
function dhm (t) {
|
||||
const cd = 24 * 60 * 60 * 1000
|
||||
const ch = 60 * 60 * 1000
|
||||
const pad = (n) => { return n < 10 ? '0' + n : n }
|
||||
let d = Math.floor(t / cd)
|
||||
let h = Math.floor((t - d * cd) / ch)
|
||||
let m = Math.round((t - d * cd - h * ch) / 60000)
|
||||
if (m === 60) {
|
||||
h++
|
||||
m = 0
|
||||
}
|
||||
if( h === 24 ){
|
||||
d++;
|
||||
h = 0;
|
||||
if (h === 24) {
|
||||
d++
|
||||
h = 0
|
||||
}
|
||||
return [d, pad(h), pad(m)].join(":");
|
||||
return [d, pad(h), pad(m)].join(':')
|
||||
}
|
||||
|
||||
|
||||
(async () => {
|
||||
const breaches = await HIBP.req("/breaches");
|
||||
const breaches = await HIBP.req('/breaches')
|
||||
|
||||
let oldestBreachDate = new Date();
|
||||
let oldestBreach = "";
|
||||
let fastestResponseTime = Math.abs(new Date() - new Date(0));
|
||||
let fastestResponseBreach = "";
|
||||
let oldestBreachDate = new Date()
|
||||
let oldestBreach = ''
|
||||
let fastestResponseTime = Math.abs(new Date() - new Date(0))
|
||||
let fastestResponseBreach = ''
|
||||
|
||||
for (const breach of breaches.body) {
|
||||
console.log("checking breach: ", breach.Name);
|
||||
const breachDate = new Date(breach.BreachDate);
|
||||
if (breachDate < oldestBreachDate){
|
||||
oldestBreachDate = breachDate;
|
||||
oldestBreach = breach.Name;
|
||||
console.log('checking breach: ', breach.Name)
|
||||
const breachDate = new Date(breach.BreachDate)
|
||||
if (breachDate < oldestBreachDate) {
|
||||
oldestBreachDate = breachDate
|
||||
oldestBreach = breach.Name
|
||||
}
|
||||
const responseTime = Math.abs(breachDate - new Date(breach.AddedDate));
|
||||
const responseTime = Math.abs(breachDate - new Date(breach.AddedDate))
|
||||
if (responseTime < fastestResponseTime) {
|
||||
fastestResponseTime = responseTime;
|
||||
fastestResponseBreach = breach.Name;
|
||||
fastestResponseTime = responseTime
|
||||
fastestResponseBreach = breach.Name
|
||||
}
|
||||
}
|
||||
|
||||
console.log("===========================");
|
||||
console.log("oldest breach: ", oldestBreach, " on date: ", oldestBreachDate);
|
||||
console.log("fastest breach response time (dd:hh:mm): ", dhm(Math.abs(fastestResponseTime)), " for breach: ", fastestResponseBreach);
|
||||
})();
|
||||
|
||||
console.log('===========================')
|
||||
console.log('oldest breach: ', oldestBreach, ' on date: ', oldestBreachDate)
|
||||
console.log('fastest breach response time (dd:hh:mm): ', dhm(Math.abs(fastestResponseTime)), ' for breach: ', fastestResponseBreach)
|
||||
})()
|
||||
|
|
|
@ -1,79 +1,77 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
// TODO: Confirm db row has index
|
||||
|
||||
const Knex = require("knex");
|
||||
const knexConfig = require("../db/knexfile");
|
||||
const knex = Knex(knexConfig);
|
||||
const Knex = require('knex')
|
||||
const knexConfig = require('../db/knexfile')
|
||||
const knex = Knex(knexConfig)
|
||||
|
||||
const HIBP = require("../hibp");
|
||||
const HIBP = require('../hibp')
|
||||
|
||||
async function checkIfBreachesExist(sha1, breaches) {
|
||||
const breachResults = await HIBP.getBreachesForEmail(sha1, breaches, true);
|
||||
async function checkIfBreachesExist (sha1, breaches) {
|
||||
const breachResults = await HIBP.getBreachesForEmail(sha1, breaches, true)
|
||||
|
||||
if (breachResults.length >= 1) {
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
function getArgsValue(argument) {
|
||||
const cliArguments = process.argv;
|
||||
function getArgsValue (argument) {
|
||||
const cliArguments = process.argv
|
||||
|
||||
if (cliArguments.indexOf(argument) < 0) {
|
||||
throw new Error(`You are missing the argument: ${argument}`);
|
||||
throw new Error(`You are missing the argument: ${argument}`)
|
||||
}
|
||||
|
||||
const arguemntIndex = cliArguments.indexOf(argument);
|
||||
const value = cliArguments[(arguemntIndex + 1)];
|
||||
const arguemntIndex = cliArguments.indexOf(argument)
|
||||
const value = cliArguments[(arguemntIndex + 1)]
|
||||
|
||||
if (!value ) {
|
||||
throw new Error(`No value set for ${argument}.`);
|
||||
if (!value) {
|
||||
throw new Error(`No value set for ${argument}.`)
|
||||
}
|
||||
|
||||
const valueNumber = parseInt(value);
|
||||
const valueNumber = parseInt(value)
|
||||
|
||||
if (Number.isNaN(valueNumber)) {
|
||||
throw new Error(`The value for ${argument} is not an interger.`);
|
||||
throw new Error(`The value for ${argument} is not an interger.`)
|
||||
}
|
||||
|
||||
return valueNumber;
|
||||
|
||||
return valueNumber
|
||||
}
|
||||
|
||||
(async () => {
|
||||
console.log("Script starting");
|
||||
console.log('Script starting')
|
||||
|
||||
const allHibpBreachesResp = await HIBP.req("/breaches");
|
||||
const allHibpBreaches = allHibpBreachesResp.body;
|
||||
const allHibpBreachesResp = await HIBP.req('/breaches')
|
||||
const allHibpBreaches = allHibpBreachesResp.body
|
||||
|
||||
const limitQuery = getArgsValue("--limit");
|
||||
const cohortSize = getArgsValue("--cohort-size");
|
||||
const limitQuery = getArgsValue('--limit')
|
||||
const cohortSize = getArgsValue('--cohort-size')
|
||||
|
||||
console.log(`The limit of this query is ${limitQuery}`);
|
||||
console.log(`The target cohort size of this query is ${cohortSize}`);
|
||||
console.log(`The limit of this query is ${limitQuery}`)
|
||||
console.log(`The target cohort size of this query is ${cohortSize}`)
|
||||
|
||||
// "SELECT primary_email, primary_sha1 FROM subscribers WHERE signup_language LIKE 'en%' AND breaches_resolved IS NULL ORDER BY random();"
|
||||
|
||||
const results = await knex("subscribers").where("signup_language", "like", "en%").andWhere({breaches_resolved: null}).orderByRaw("RANDOM()").limit(limitQuery).select("primary_email", "primary_sha1");
|
||||
const results = await knex('subscribers').where('signup_language', 'like', 'en%').andWhere({ breaches_resolved: null }).orderByRaw('RANDOM()').limit(limitQuery).select('primary_email', 'primary_sha1')
|
||||
|
||||
const cohort = [];
|
||||
const cohort = []
|
||||
|
||||
for (const record of results) {
|
||||
if (cohort.length > cohortSize) {
|
||||
// Cohort size reached
|
||||
break;
|
||||
break
|
||||
}
|
||||
|
||||
const sha1 = record.primary_sha1;
|
||||
const isValidCohortMember = await checkIfBreachesExist(sha1, allHibpBreaches);
|
||||
if (isValidCohortMember) { cohort.push(record.primary_email); }
|
||||
const sha1 = record.primary_sha1
|
||||
const isValidCohortMember = await checkIfBreachesExist(sha1, allHibpBreaches)
|
||||
if (isValidCohortMember) { cohort.push(record.primary_email) }
|
||||
}
|
||||
|
||||
console.log("Script completed! See final output:");
|
||||
console.log(cohort.toString());
|
||||
console.log('Script completed! See final output:')
|
||||
console.log(cohort.toString())
|
||||
|
||||
process.exit();
|
||||
|
||||
})();
|
||||
process.exit()
|
||||
})()
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
"use strict";
|
||||
|
||||
const DB = require("../db/DB");
|
||||
'use strict'
|
||||
|
||||
const DB = require('../db/DB');
|
||||
|
||||
(async () => {
|
||||
await DB.deleteUnverifiedSubscribers();
|
||||
process.exit();
|
||||
})();
|
||||
await DB.deleteUnverifiedSubscribers()
|
||||
process.exit()
|
||||
})()
|
||||
|
|
|
@ -1,32 +1,32 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const readline = require("readline");
|
||||
const readline = require('readline')
|
||||
|
||||
const DB = require("../db/DB");
|
||||
const DB = require('../db/DB')
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
(async () => {
|
||||
rl.question("What FXA primary email address? ", first_answer => {
|
||||
rl.question("Please re-type the email address to confirm. ", async (second_answer) => {
|
||||
rl.question('What FXA primary email address? ', first_answer => {
|
||||
rl.question('Please re-type the email address to confirm. ', async (second_answer) => {
|
||||
if (first_answer !== second_answer) {
|
||||
console.error("Email addresses do not match.");
|
||||
process.exit(1);
|
||||
console.error('Email addresses do not match.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const subscriber = await DB.getSubscriberByEmail(second_answer);
|
||||
const subscriber = await DB.getSubscriberByEmail(second_answer)
|
||||
if (!subscriber) {
|
||||
console.error("Could not find subscriber.");
|
||||
process.exit(1);
|
||||
console.error('Could not find subscriber.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await DB.deleteEmailAddressesByUid(subscriber.id);
|
||||
await DB.deleteSubscriberByFxAUID(subscriber.fxa_uid);
|
||||
console.log("Deleted email_addresses and subscribers records.");
|
||||
process.exit();
|
||||
});
|
||||
});
|
||||
})();
|
||||
await DB.deleteEmailAddressesByUid(subscriber.id)
|
||||
await DB.deleteSubscriberByFxAUID(subscriber.fxa_uid)
|
||||
console.log('Deleted email_addresses and subscribers records.')
|
||||
process.exit()
|
||||
})
|
||||
})
|
||||
})()
|
||||
|
|
|
@ -1,50 +1,47 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const Knex = require('knex')
|
||||
const knexConfig = require('../db/knexfile')
|
||||
const knex = Knex(knexConfig)
|
||||
|
||||
const Knex = require("knex");
|
||||
const knexConfig = require("../db/knexfile");
|
||||
const knex = Knex(knexConfig);
|
||||
const HIBP = require('../hibp')
|
||||
const getSha1 = require('../sha1-utils')
|
||||
|
||||
const HIBP = require("../hibp");
|
||||
const getSha1 = require("../sha1-utils");
|
||||
|
||||
|
||||
async function subscribeLowercaseHashToHIBP(emailAddress) {
|
||||
const lowerCasedEmail = emailAddress.toLowerCase();
|
||||
const lowerCasedSha1 = getSha1(lowerCasedEmail);
|
||||
await HIBP.subscribeHash(lowerCasedSha1);
|
||||
return lowerCasedSha1;
|
||||
async function subscribeLowercaseHashToHIBP (emailAddress) {
|
||||
const lowerCasedEmail = emailAddress.toLowerCase()
|
||||
const lowerCasedSha1 = getSha1(lowerCasedEmail)
|
||||
await HIBP.subscribeHash(lowerCasedSha1)
|
||||
return lowerCasedSha1
|
||||
}
|
||||
|
||||
|
||||
(async () => {
|
||||
const subRecordsWithUpperChars = await knex.select("id", "primary_email").from("subscribers")
|
||||
.whereRaw("primary_email != lower(primary_email)");
|
||||
const subsWithUpperCount = subRecordsWithUpperChars.length;
|
||||
console.log(`found ${subsWithUpperCount} subscribers records with primary_email != lower(primary_email). fixing ...`);
|
||||
const subRecordsWithUpperChars = await knex.select('id', 'primary_email').from('subscribers')
|
||||
.whereRaw('primary_email != lower(primary_email)')
|
||||
const subsWithUpperCount = subRecordsWithUpperChars.length
|
||||
console.log(`found ${subsWithUpperCount} subscribers records with primary_email != lower(primary_email). fixing ...`)
|
||||
for (const subRecord of subRecordsWithUpperChars) {
|
||||
const lowerCasedSha1 = await subscribeLowercaseHashToHIBP(subRecord.primary_email);
|
||||
await knex("subscribers")
|
||||
const lowerCasedSha1 = await subscribeLowercaseHashToHIBP(subRecord.primary_email)
|
||||
await knex('subscribers')
|
||||
.update({
|
||||
primary_sha1: lowerCasedSha1,
|
||||
primary_sha1: lowerCasedSha1
|
||||
})
|
||||
.where("id", subRecord.id);
|
||||
console.log(`fixed subscribers record ID: ${subRecord.id}`);
|
||||
.where('id', subRecord.id)
|
||||
console.log(`fixed subscribers record ID: ${subRecord.id}`)
|
||||
}
|
||||
|
||||
const emailRecordsWithUpperChars = await knex.select("id", "email").from("email_addresses")
|
||||
.whereRaw("email != lower(email)");
|
||||
const emailsWithUpperCount = emailRecordsWithUpperChars.length;
|
||||
console.log(`found ${emailsWithUpperCount} email_addresses records with email != lower(email)`);
|
||||
const emailRecordsWithUpperChars = await knex.select('id', 'email').from('email_addresses')
|
||||
.whereRaw('email != lower(email)')
|
||||
const emailsWithUpperCount = emailRecordsWithUpperChars.length
|
||||
console.log(`found ${emailsWithUpperCount} email_addresses records with email != lower(email)`)
|
||||
for (const emailRecord of emailRecordsWithUpperChars) {
|
||||
const lowerCasedSha1 = await subscribeLowercaseHashToHIBP(emailRecord.email);
|
||||
await knex("email_addresses")
|
||||
const lowerCasedSha1 = await subscribeLowercaseHashToHIBP(emailRecord.email)
|
||||
await knex('email_addresses')
|
||||
.update({
|
||||
sha1: lowerCasedSha1,
|
||||
sha1: lowerCasedSha1
|
||||
})
|
||||
.where("id", emailRecord.id);
|
||||
console.log(`fixed email_addresses record ID: ${emailRecord.id}`);
|
||||
.where('id', emailRecord.id)
|
||||
console.log(`fixed email_addresses record ID: ${emailRecord.id}`)
|
||||
}
|
||||
console.log("done.");
|
||||
process.exit();
|
||||
})();
|
||||
console.log('done.')
|
||||
process.exit()
|
||||
})()
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const getSha1 = require("../sha1-utils");
|
||||
const stdin = process.openStdin();
|
||||
const getSha1 = require('../sha1-utils')
|
||||
const stdin = process.openStdin()
|
||||
|
||||
const PROMPT = "\nEnter an email address to get the SHA1 hash as it would appear in a HIBP hashset file:";
|
||||
const PROMPT = '\nEnter an email address to get the SHA1 hash as it would appear in a HIBP hashset file:'
|
||||
|
||||
console.log(PROMPT);
|
||||
console.log(PROMPT)
|
||||
|
||||
stdin.addListener("data", data => {
|
||||
const trimmedString = data.toString().trim();
|
||||
const sha1 = getSha1(trimmedString);
|
||||
console.log(`You entered: [${trimmedString}], sha1 hash of lowercase: ${sha1}`);
|
||||
console.log(PROMPT);
|
||||
});
|
||||
stdin.addListener('data', data => {
|
||||
const trimmedString = data.toString().trim()
|
||||
const sha1 = getSha1(trimmedString)
|
||||
console.log(`You entered: [${trimmedString}], sha1 hash of lowercase: ${sha1}`)
|
||||
console.log(PROMPT)
|
||||
})
|
||||
|
|
|
@ -1,52 +1,51 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const readline = require("readline");
|
||||
const readline = require('readline')
|
||||
|
||||
const AppConstants = require("../app-constants");
|
||||
const EmailUtils = require("../email-utils");
|
||||
const hibp = require("../controllers/hibp");
|
||||
const { LocaleUtils } = require("../locale-utils");
|
||||
const sha1 = require("../sha1-utils");
|
||||
const AppConstants = require('../app-constants')
|
||||
const EmailUtils = require('../email-utils')
|
||||
const hibp = require('../controllers/hibp')
|
||||
const { LocaleUtils } = require('../locale-utils')
|
||||
const sha1 = require('../sha1-utils')
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
const app = { locals: { breaches: [], AVAILABLE_LANGUAGES: ["en"] } };
|
||||
output: process.stdout
|
||||
})
|
||||
const app = { locals: { breaches: [], AVAILABLE_LANGUAGES: ['en'] } }
|
||||
|
||||
|
||||
LocaleUtils.init();
|
||||
LocaleUtils.init()
|
||||
LocaleUtils.loadLanguagesIntoApp(app);
|
||||
|
||||
(async () => {
|
||||
await EmailUtils.init();
|
||||
let emailAddress, breachName;
|
||||
await EmailUtils.init()
|
||||
let emailAddress, breachName
|
||||
const resp = {
|
||||
status: () => {},
|
||||
json: (arg) => {
|
||||
console.log(JSON.stringify(arg));
|
||||
},
|
||||
};
|
||||
rl.question("What email address? ", (answer) => {
|
||||
emailAddress = answer;
|
||||
const hash = sha1(emailAddress);
|
||||
const hashPrefix = hash.slice(0, 6).toUpperCase();
|
||||
const hashSuffix = hash.slice(6).toUpperCase();
|
||||
rl.question("What breach name? ", async (answer) => {
|
||||
breachName = answer;
|
||||
console.log(JSON.stringify(arg))
|
||||
}
|
||||
}
|
||||
rl.question('What email address? ', (answer) => {
|
||||
emailAddress = answer
|
||||
const hash = sha1(emailAddress)
|
||||
const hashPrefix = hash.slice(0, 6).toUpperCase()
|
||||
const hashSuffix = hash.slice(6).toUpperCase()
|
||||
rl.question('What breach name? ', async (answer) => {
|
||||
breachName = answer
|
||||
const req = {
|
||||
token: AppConstants.HIBP_NOTIFY_TOKEN,
|
||||
app: app,
|
||||
body: { hashPrefix, hashSuffixes: [hashSuffix], breachName },
|
||||
};
|
||||
app,
|
||||
body: { hashPrefix, hashSuffixes: [hashSuffix], breachName }
|
||||
}
|
||||
|
||||
await hibp.notify(req, resp);
|
||||
await hibp.notify(req, resp)
|
||||
|
||||
console.log(JSON.stringify(resp));
|
||||
console.log(JSON.stringify(resp))
|
||||
|
||||
rl.close();
|
||||
rl.close()
|
||||
|
||||
process.exit();
|
||||
});
|
||||
});
|
||||
})();
|
||||
process.exit()
|
||||
})
|
||||
})
|
||||
})()
|
||||
|
|
|
@ -1,70 +1,68 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
/* eslint-disable no-process-env */
|
||||
|
||||
const { negotiateLanguages, acceptedLanguages } = require("fluent-langneg");
|
||||
const { negotiateLanguages, acceptedLanguages } = require('fluent-langneg')
|
||||
|
||||
const AppConstants = require("../app-constants");
|
||||
const DB = require("../db/DB");
|
||||
const EmailHelpers = require("../template-helpers/emails.js");
|
||||
const EmailUtils = require("../email-utils");
|
||||
const { LocaleUtils } = require ("../locale-utils");
|
||||
const AppConstants = require('../app-constants')
|
||||
const DB = require('../db/DB')
|
||||
const EmailHelpers = require('../template-helpers/emails.js')
|
||||
const EmailUtils = require('../email-utils')
|
||||
const { LocaleUtils } = require('../locale-utils')
|
||||
|
||||
const PAGE_SIZE = process.env.PAGE_SIZE;
|
||||
const START_PAGE = process.env.START_PAGE;
|
||||
const PAGE_SIZE = process.env.PAGE_SIZE
|
||||
const START_PAGE = process.env.START_PAGE
|
||||
|
||||
if (!START_PAGE) {
|
||||
console.error("You must provide a START_PAGE environment variable.");
|
||||
process.exit();
|
||||
console.error('You must provide a START_PAGE environment variable.')
|
||||
process.exit()
|
||||
}
|
||||
|
||||
|
||||
(async (req) => {
|
||||
const localeUtils = LocaleUtils.init();
|
||||
EmailUtils.init();
|
||||
const notifiedSubscribers = [];
|
||||
const utmID = "pre-fxa";
|
||||
|
||||
const subscribersResult = await DB.getPreFxaSubscribersPage({ perPage: PAGE_SIZE, currentPage: START_PAGE, isLengthAware: true });
|
||||
const numPagesToProcess = subscribersResult.pagination.lastPage - START_PAGE;
|
||||
console.log(`Found ${subscribersResult.pagination.total} subscriber records with empty fxa_uid.`);
|
||||
console.log(`Will process ${numPagesToProcess} pages of size ${PAGE_SIZE}, starting with page ${START_PAGE} and ending with page ${subscribersResult.pagination.lastPage}.`);
|
||||
const lastPage = subscribersResult.pagination.lastPage;
|
||||
const localeUtils = LocaleUtils.init()
|
||||
EmailUtils.init()
|
||||
const notifiedSubscribers = []
|
||||
const utmID = 'pre-fxa'
|
||||
|
||||
const subscribersResult = await DB.getPreFxaSubscribersPage({ perPage: PAGE_SIZE, currentPage: START_PAGE, isLengthAware: true })
|
||||
const numPagesToProcess = subscribersResult.pagination.lastPage - START_PAGE
|
||||
console.log(`Found ${subscribersResult.pagination.total} subscriber records with empty fxa_uid.`)
|
||||
console.log(`Will process ${numPagesToProcess} pages of size ${PAGE_SIZE}, starting with page ${START_PAGE} and ending with page ${subscribersResult.pagination.lastPage}.`)
|
||||
const lastPage = subscribersResult.pagination.lastPage
|
||||
|
||||
for (let currentPage = START_PAGE; currentPage <= lastPage; currentPage++) {
|
||||
console.log(`Processing page ${currentPage} of ${lastPage}.`);
|
||||
const subscribersPageResult = await DB.getPreFxaSubscribersPage({ perPage: PAGE_SIZE, currentPage });
|
||||
console.log(`Processing page ${currentPage} of ${lastPage}.`)
|
||||
const subscribersPageResult = await DB.getPreFxaSubscribersPage({ perPage: PAGE_SIZE, currentPage })
|
||||
for (const subscriber of subscribersPageResult.data) {
|
||||
const signupLanguage = subscriber.signup_language;
|
||||
const subscriberEmail = subscriber.primary_email;
|
||||
const requestedLanguage = signupLanguage ? acceptedLanguages(signupLanguage) : "";
|
||||
const signupLanguage = subscriber.signup_language
|
||||
const subscriberEmail = subscriber.primary_email
|
||||
const requestedLanguage = signupLanguage ? acceptedLanguages(signupLanguage) : ''
|
||||
const supportedLocales = negotiateLanguages(
|
||||
requestedLanguage,
|
||||
localeUtils.availableLanguages,
|
||||
{defaultLocale: "en"}
|
||||
);
|
||||
{ defaultLocale: 'en' }
|
||||
)
|
||||
|
||||
if (!notifiedSubscribers.includes(subscriberEmail)) {
|
||||
const sendInfo = await EmailUtils.sendEmail(
|
||||
subscriberEmail,
|
||||
LocaleUtils.fluentFormat(supportedLocales, "pre-fxa-subject"), // email subject
|
||||
"default_email", // email template
|
||||
LocaleUtils.fluentFormat(supportedLocales, 'pre-fxa-subject'), // email subject
|
||||
'default_email', // email template
|
||||
{
|
||||
supportedLocales,
|
||||
SERVER_URL: AppConstants.SERVER_URL,
|
||||
unsubscribeUrl: EmailUtils.getUnsubscribeUrl(subscriber, utmID), // need to test the flow for legacy users who want to unsubscribe
|
||||
ctaHref: EmailHelpers.getPreFxaUtmParams(AppConstants.SERVER_URL, "create-account-button", subscriberEmail),
|
||||
whichPartial: "email_partials/pre-fxa",
|
||||
ctaHref: EmailHelpers.getPreFxaUtmParams(AppConstants.SERVER_URL, 'create-account-button', subscriberEmail),
|
||||
whichPartial: 'email_partials/pre-fxa',
|
||||
preFxaEmail: true,
|
||||
email: subscriberEmail,
|
||||
email: subscriberEmail
|
||||
}
|
||||
);
|
||||
notifiedSubscribers.push(subscriberEmail);
|
||||
console.log(`Sent email to ${subscriberEmail}, info: ${JSON.stringify(sendInfo)}`);
|
||||
)
|
||||
notifiedSubscribers.push(subscriberEmail)
|
||||
console.log(`Sent email to ${subscriberEmail}, info: ${JSON.stringify(sendInfo)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(`Notified subscribers: ${JSON.stringify(notifiedSubscribers)}`);
|
||||
process.exit();
|
||||
})();
|
||||
console.log(`Notified subscribers: ${JSON.stringify(notifiedSubscribers)}`)
|
||||
process.exit()
|
||||
})()
|
||||
|
|
|
@ -1,68 +1,64 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const Knex = require('knex')
|
||||
const knexConfig = require('../db/knexfile')
|
||||
const knex = Knex(knexConfig)
|
||||
|
||||
const Knex = require("knex");
|
||||
const knexConfig = require("../db/knexfile");
|
||||
const knex = Knex(knexConfig);
|
||||
const HIBP = require('../hibp')
|
||||
const getSha1 = require('../sha1-utils')
|
||||
|
||||
|
||||
const HIBP = require("../hibp");
|
||||
const getSha1 = require("../sha1-utils");
|
||||
|
||||
|
||||
async function subscribeLowercaseHashToHIBP(emailAddress) {
|
||||
const lowerCasedEmail = emailAddress.toLowerCase();
|
||||
const lowerCasedSha1 = getSha1(lowerCasedEmail);
|
||||
await HIBP.subscribeHash(lowerCasedSha1);
|
||||
return lowerCasedSha1;
|
||||
async function subscribeLowercaseHashToHIBP (emailAddress) {
|
||||
const lowerCasedEmail = emailAddress.toLowerCase()
|
||||
const lowerCasedSha1 = getSha1(lowerCasedEmail)
|
||||
await HIBP.subscribeHash(lowerCasedSha1)
|
||||
return lowerCasedSha1
|
||||
}
|
||||
|
||||
|
||||
(async () => {
|
||||
const chunkSize = process.argv[2];
|
||||
console.log(`subscribing lower-cased hashes in ${chunkSize}-sized chunks`);
|
||||
const chunkSize = process.argv[2]
|
||||
console.log(`subscribing lower-cased hashes in ${chunkSize}-sized chunks`)
|
||||
|
||||
const subRecordsThatNeedFixing = await knex("subscribers").count().whereRaw("primary_email != lower(primary_email)");
|
||||
const subsWithUpperCount = subRecordsThatNeedFixing[0].count;
|
||||
console.log(`found ${subsWithUpperCount} subscribers records with primary_email != lower(primary_email). fixing ...`);
|
||||
const subRecordsThatNeedFixing = await knex('subscribers').count().whereRaw('primary_email != lower(primary_email)')
|
||||
const subsWithUpperCount = subRecordsThatNeedFixing[0].count
|
||||
console.log(`found ${subsWithUpperCount} subscribers records with primary_email != lower(primary_email). fixing ...`)
|
||||
|
||||
let subRecordsFixed = 0;
|
||||
let subPrevMaxId = 0;
|
||||
let subRecordsFixed = 0
|
||||
let subPrevMaxId = 0
|
||||
while (subRecordsFixed < subsWithUpperCount) {
|
||||
console.log(`working on chunk where id > ${subPrevMaxId} ...`);
|
||||
const subRecordsWithUpperCharsChunk = await knex.select("id", "primary_email").from("subscribers")
|
||||
.where("id", ">", subPrevMaxId)
|
||||
.whereRaw("primary_email != lower(primary_email)")
|
||||
.orderBy("id", "asc")
|
||||
.limit(chunkSize);
|
||||
console.log(`working on chunk where id > ${subPrevMaxId} ...`)
|
||||
const subRecordsWithUpperCharsChunk = await knex.select('id', 'primary_email').from('subscribers')
|
||||
.where('id', '>', subPrevMaxId)
|
||||
.whereRaw('primary_email != lower(primary_email)')
|
||||
.orderBy('id', 'asc')
|
||||
.limit(chunkSize)
|
||||
for (const subRecord of subRecordsWithUpperCharsChunk) {
|
||||
await subscribeLowercaseHashToHIBP(subRecord.primary_email);
|
||||
subPrevMaxId = subRecord.id;
|
||||
subRecordsFixed++;
|
||||
console.log(`subscribed lower-case address hash for subscribers record ID: ${subRecord.id}`);
|
||||
await subscribeLowercaseHashToHIBP(subRecord.primary_email)
|
||||
subPrevMaxId = subRecord.id
|
||||
subRecordsFixed++
|
||||
console.log(`subscribed lower-case address hash for subscribers record ID: ${subRecord.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
const emailRecordsThatNeedFixing = await knex("email_addresses").count().whereRaw("email != lower(email)");
|
||||
const emailWithUpperCount = emailRecordsThatNeedFixing[0].count;
|
||||
console.log(`found ${emailWithUpperCount} email_address records with email != lower(email). fixing ...`);
|
||||
const emailRecordsThatNeedFixing = await knex('email_addresses').count().whereRaw('email != lower(email)')
|
||||
const emailWithUpperCount = emailRecordsThatNeedFixing[0].count
|
||||
console.log(`found ${emailWithUpperCount} email_address records with email != lower(email). fixing ...`)
|
||||
|
||||
let emailRecordsFixed = 0;
|
||||
let emailPrevMaxId = 0;
|
||||
let emailRecordsFixed = 0
|
||||
let emailPrevMaxId = 0
|
||||
while (emailRecordsFixed < emailWithUpperCount) {
|
||||
console.log(`working on chunk where id > ${emailPrevMaxId} ...`);
|
||||
const emailRecordsWithUpperChars = await knex.select("id", "email").from("email_addresses")
|
||||
.where("id", ">", emailPrevMaxId)
|
||||
.whereRaw("email != lower(email)")
|
||||
.orderBy("id", "asc")
|
||||
.limit(chunkSize);
|
||||
console.log(`working on chunk where id > ${emailPrevMaxId} ...`)
|
||||
const emailRecordsWithUpperChars = await knex.select('id', 'email').from('email_addresses')
|
||||
.where('id', '>', emailPrevMaxId)
|
||||
.whereRaw('email != lower(email)')
|
||||
.orderBy('id', 'asc')
|
||||
.limit(chunkSize)
|
||||
for (const emailRecord of emailRecordsWithUpperChars) {
|
||||
await subscribeLowercaseHashToHIBP(emailRecord.email);
|
||||
emailPrevMaxId = emailRecord.id;
|
||||
emailRecordsFixed++;
|
||||
console.log(`fixed email_addresses record ID: ${emailRecord.id}`);
|
||||
await subscribeLowercaseHashToHIBP(emailRecord.email)
|
||||
emailPrevMaxId = emailRecord.id
|
||||
emailRecordsFixed++
|
||||
console.log(`fixed email_addresses record ID: ${emailRecord.id}`)
|
||||
}
|
||||
}
|
||||
console.log("done.");
|
||||
process.exit();
|
||||
})();
|
||||
console.log('done.')
|
||||
process.exit()
|
||||
})()
|
||||
|
|
|
@ -1,33 +1,31 @@
|
|||
"use strict";
|
||||
|
||||
const AppConstants = require("../app-constants");
|
||||
const HIBP = require("../hibp");
|
||||
const RemoteSettings = require("../lib/remote-settings");
|
||||
'use strict'
|
||||
|
||||
const AppConstants = require('../app-constants')
|
||||
const HIBP = require('../hibp')
|
||||
const RemoteSettings = require('../lib/remote-settings')
|
||||
|
||||
if (
|
||||
!AppConstants.FX_REMOTE_SETTINGS_WRITER_USER ||
|
||||
!AppConstants.FX_REMOTE_SETTINGS_WRITER_PASS ||
|
||||
!AppConstants.FX_REMOTE_SETTINGS_WRITER_SERVER
|
||||
) {
|
||||
console.error("updatebreaches requires FX_REMOTE_SETTINGS_WRITER_SERVER, FX_REMOTE_SETTINGS_WRITER_USER, FX_REMOTE_SETTINGS_WRITER_PASS.");
|
||||
process.exit(1);
|
||||
console.error('updatebreaches requires FX_REMOTE_SETTINGS_WRITER_SERVER, FX_REMOTE_SETTINGS_WRITER_USER, FX_REMOTE_SETTINGS_WRITER_PASS.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
|
||||
(async () => {
|
||||
const allHibpBreaches = await HIBP.req("/breaches");
|
||||
const verifiedSiteBreaches = HIBP.filterBreaches(allHibpBreaches.body);
|
||||
const verifiedSiteBreachesWithPWs = verifiedSiteBreaches.filter(breach => breach.DataClasses.includes("Passwords"));
|
||||
const allHibpBreaches = await HIBP.req('/breaches')
|
||||
const verifiedSiteBreaches = HIBP.filterBreaches(allHibpBreaches.body)
|
||||
const verifiedSiteBreachesWithPWs = verifiedSiteBreaches.filter(breach => breach.DataClasses.includes('Passwords'))
|
||||
|
||||
const newBreaches = await RemoteSettings.whichBreachesAreNotInRemoteSettingsYet(verifiedSiteBreachesWithPWs);
|
||||
const newBreaches = await RemoteSettings.whichBreachesAreNotInRemoteSettingsYet(verifiedSiteBreachesWithPWs)
|
||||
|
||||
if (newBreaches.length <= 0) {
|
||||
console.log("No new breaches detected.");
|
||||
process.exit(0);
|
||||
console.log('No new breaches detected.')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
console.log(`${newBreaches.length} new breach(es) found.`);
|
||||
console.log(`${newBreaches.length} new breach(es) found.`)
|
||||
|
||||
for (const breach of newBreaches) {
|
||||
const data = {
|
||||
|
@ -36,20 +34,19 @@ if (
|
|||
BreachDate: breach.BreachDate,
|
||||
PwnCount: breach.PwnCount,
|
||||
AddedDate: breach.AddedDate,
|
||||
DataClasses: breach.DataClasses,
|
||||
};
|
||||
DataClasses: breach.DataClasses
|
||||
}
|
||||
|
||||
console.log("New breach detected: \n", data);
|
||||
console.log('New breach detected: \n', data)
|
||||
|
||||
try {
|
||||
await RemoteSettings.postNewBreachToBreachesCollection(data);
|
||||
await RemoteSettings.postNewBreachToBreachesCollection(data)
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
console.error(e)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Requesting review on breaches collection");
|
||||
await RemoteSettings.requestReviewOnBreachesCollection();
|
||||
|
||||
})();
|
||||
console.log('Requesting review on breaches collection')
|
||||
await RemoteSettings.requestReviewOnBreachesCollection()
|
||||
})()
|
||||
|
|
285
server.js
285
server.js
|
@ -1,149 +1,148 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
// initialize Sentry ASAP to capture fatal startup errors
|
||||
const Sentry = require("@sentry/node");
|
||||
const AppConstants = require("./app-constants");
|
||||
const Sentry = require('@sentry/node')
|
||||
const AppConstants = require('./app-constants')
|
||||
Sentry.init({
|
||||
dsn: AppConstants.SENTRY_DSN,
|
||||
environment: AppConstants.NODE_ENV,
|
||||
beforeSend(event, hint) {
|
||||
if (!hint.originalException.locales || hint.originalException.locales[0] === "en") return event; // return if no localization or localization is in english
|
||||
beforeSend (event, hint) {
|
||||
if (!hint.originalException.locales || hint.originalException.locales[0] === 'en') return event // return if no localization or localization is in english
|
||||
|
||||
try {
|
||||
if (hint.originalException.fluentID) {
|
||||
event.exception.values[0].value = LocaleUtils.fluentFormat(["en"], hint.originalException.fluentID);
|
||||
event.exception.values[0].value = LocaleUtils.fluentFormat(['en'], hint.originalException.fluentID)
|
||||
}
|
||||
} catch (e) {
|
||||
return event;
|
||||
return event
|
||||
}
|
||||
|
||||
return event;
|
||||
},
|
||||
});
|
||||
return event
|
||||
}
|
||||
})
|
||||
|
||||
const connectRedis = require("connect-redis");
|
||||
const express = require("express");
|
||||
const exphbs = require("express-handlebars");
|
||||
const helmet = require("helmet");
|
||||
const session = require("express-session");
|
||||
const cookieParser = require("cookie-parser");
|
||||
const { URL } = require("url");
|
||||
const connectRedis = require('connect-redis')
|
||||
const express = require('express')
|
||||
const exphbs = require('express-handlebars')
|
||||
const helmet = require('helmet')
|
||||
const session = require('express-session')
|
||||
const cookieParser = require('cookie-parser')
|
||||
const { URL } = require('url')
|
||||
|
||||
const EmailUtils = require("./email-utils");
|
||||
const HBSHelpers = require("./template-helpers/");
|
||||
const HIBP = require("./hibp");
|
||||
const IpLocationService = require("./ip-location-service");
|
||||
const EmailUtils = require('./email-utils')
|
||||
const HBSHelpers = require('./template-helpers/')
|
||||
const HIBP = require('./hibp')
|
||||
const IpLocationService = require('./ip-location-service')
|
||||
|
||||
const {
|
||||
addRequestToResponse, pickLanguage, logErrors, localizeErrorMessages,
|
||||
clientErrorHandler, errorHandler, recordVisitFromEmail,
|
||||
} = require("./middleware");
|
||||
const { LocaleUtils } = require("./locale-utils");
|
||||
const mozlog = require("./log");
|
||||
clientErrorHandler, errorHandler, recordVisitFromEmail
|
||||
} = require('./middleware')
|
||||
const { LocaleUtils } = require('./locale-utils')
|
||||
const mozlog = require('./log')
|
||||
|
||||
const DockerflowRoutes = require("./routes/dockerflow");
|
||||
const HibpRoutes = require("./routes/hibp");
|
||||
const HomeRoutes = require("./routes/home");
|
||||
const ScanRoutes = require("./routes/scan");
|
||||
const SesRoutes = require("./routes/ses");
|
||||
const OAuthRoutes = require("./routes/oauth");
|
||||
const UserRoutes = require("./routes/user");
|
||||
const EmailL10nRoutes= require("./routes/email-l10n");
|
||||
const BreachRoutes= require("./routes/breach-details");
|
||||
const DockerflowRoutes = require('./routes/dockerflow')
|
||||
const HibpRoutes = require('./routes/hibp')
|
||||
const HomeRoutes = require('./routes/home')
|
||||
const ScanRoutes = require('./routes/scan')
|
||||
const SesRoutes = require('./routes/ses')
|
||||
const OAuthRoutes = require('./routes/oauth')
|
||||
const UserRoutes = require('./routes/user')
|
||||
const EmailL10nRoutes = require('./routes/email-l10n')
|
||||
const BreachRoutes = require('./routes/breach-details')
|
||||
|
||||
const log = mozlog("server");
|
||||
const log = mozlog('server')
|
||||
|
||||
function getRedisStore() {
|
||||
const redisStoreConstructor = connectRedis(session);
|
||||
if (["", "redis-mock"].includes(AppConstants.REDIS_URL)) {
|
||||
function getRedisStore () {
|
||||
const redisStoreConstructor = connectRedis(session)
|
||||
if (['', 'redis-mock'].includes(AppConstants.REDIS_URL)) {
|
||||
// eslint-disable-next-line node/no-unpublished-require
|
||||
const redis = require("redis-mock");
|
||||
return new redisStoreConstructor({ client: redis.createClient() });
|
||||
const redis = require('redis-mock')
|
||||
return new redisStoreConstructor({ client: redis.createClient() })
|
||||
}
|
||||
const redis = require("redis");
|
||||
return new redisStoreConstructor({ client: redis.createClient({url: AppConstants.REDIS_URL }) });
|
||||
const redis = require('redis')
|
||||
return new redisStoreConstructor({ client: redis.createClient({ url: AppConstants.REDIS_URL }) })
|
||||
}
|
||||
|
||||
const app = express();
|
||||
const app = express()
|
||||
|
||||
|
||||
function devOrHeroku() {
|
||||
return ["dev", "heroku"].includes(AppConstants.NODE_ENV);
|
||||
function devOrHeroku () {
|
||||
return ['dev', 'heroku'].includes(AppConstants.NODE_ENV)
|
||||
}
|
||||
|
||||
if (app.get("env") !== "dev") {
|
||||
app.enable("trust proxy");
|
||||
app.use( (req, res, next) => {
|
||||
if (app.get('env') !== 'dev') {
|
||||
app.enable('trust proxy')
|
||||
app.use((req, res, next) => {
|
||||
if (req.secure) {
|
||||
next();
|
||||
next()
|
||||
} else {
|
||||
res.redirect("https://" + req.headers.host + req.url);
|
||||
res.redirect('https://' + req.headers.host + req.url)
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
LocaleUtils.init();
|
||||
LocaleUtils.loadLanguagesIntoApp(app);
|
||||
LocaleUtils.init()
|
||||
LocaleUtils.loadLanguagesIntoApp(app)
|
||||
} catch (error) {
|
||||
log.error("try-load-languages-error", { error: error });
|
||||
log.error('try-load-languages-error', { error })
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await HIBP.loadBreachesIntoApp(app);
|
||||
await HIBP.loadBreachesIntoApp(app)
|
||||
} catch (error) {
|
||||
log.error("try-load-breaches-error", { error: error });
|
||||
log.error('try-load-breaches-error', { error })
|
||||
}
|
||||
})();
|
||||
|
||||
(async () => {
|
||||
// open location database once at server start. Service includes 24hr check to reload fresh database.
|
||||
await IpLocationService.openLocationDb().catch(e => console.warn(e));
|
||||
})();
|
||||
await IpLocationService.openLocationDb().catch(e => console.warn(e))
|
||||
})()
|
||||
|
||||
// Use helmet to set security headers
|
||||
// only enable HSTS on heroku; Ops handles it in stage & prod configs
|
||||
if (AppConstants.NODE_ENV === "heroku") {
|
||||
app.use(helmet.hsts({ maxAge: 60 * 60 * 24 * 365 * 2 })); // 2 years
|
||||
if (AppConstants.NODE_ENV === 'heroku') {
|
||||
app.use(helmet.hsts({ maxAge: 60 * 60 * 24 * 365 * 2 })) // 2 years
|
||||
}
|
||||
|
||||
const SCRIPT_SOURCES = ["'self'", "https://www.google-analytics.com/analytics.js"];
|
||||
const STYLE_SOURCES = ["'self'", "https://code.cdn.mozilla.net/fonts/"];
|
||||
const FRAME_ANCESTORS = ["'none'"];
|
||||
const SCRIPT_SOURCES = ["'self'", 'https://www.google-analytics.com/analytics.js']
|
||||
const STYLE_SOURCES = ["'self'", 'https://code.cdn.mozilla.net/fonts/']
|
||||
const FRAME_ANCESTORS = ["'none'"]
|
||||
|
||||
app.locals.ENABLE_PONTOON_JS = false;
|
||||
app.locals.ENABLE_PONTOON_JS = false
|
||||
// Allow pontoon.mozilla.org on heroku for in-page localization
|
||||
const PONTOON_DOMAIN = "https://pontoon.mozilla.org";
|
||||
if (AppConstants.NODE_ENV === "heroku") {
|
||||
app.locals.ENABLE_PONTOON_JS = true;
|
||||
SCRIPT_SOURCES.push(PONTOON_DOMAIN);
|
||||
STYLE_SOURCES.push(PONTOON_DOMAIN);
|
||||
FRAME_ANCESTORS[0] = PONTOON_DOMAIN; // other sources cannot be declared alongside 'none'
|
||||
const PONTOON_DOMAIN = 'https://pontoon.mozilla.org'
|
||||
if (AppConstants.NODE_ENV === 'heroku') {
|
||||
app.locals.ENABLE_PONTOON_JS = true
|
||||
SCRIPT_SOURCES.push(PONTOON_DOMAIN)
|
||||
STYLE_SOURCES.push(PONTOON_DOMAIN)
|
||||
FRAME_ANCESTORS[0] = PONTOON_DOMAIN // other sources cannot be declared alongside 'none'
|
||||
}
|
||||
|
||||
const imgSrc = [
|
||||
"'self'",
|
||||
"https://www.google-analytics.com",
|
||||
"https://firefoxusercontent.com",
|
||||
"https://mozillausercontent.com/",
|
||||
"https://monitor.cdn.mozilla.net/",
|
||||
];
|
||||
'https://www.google-analytics.com',
|
||||
'https://firefoxusercontent.com',
|
||||
'https://mozillausercontent.com/',
|
||||
'https://monitor.cdn.mozilla.net/'
|
||||
]
|
||||
|
||||
const connectSrc = [
|
||||
"'self'",
|
||||
"https://code.cdn.mozilla.net/fonts/",
|
||||
"https://www.google-analytics.com",
|
||||
"https://accounts.firefox.com",
|
||||
"https://accounts.stage.mozaws.net/metrics-flow",
|
||||
"https://am.i.mullvad.net/json",
|
||||
];
|
||||
'https://code.cdn.mozilla.net/fonts/',
|
||||
'https://www.google-analytics.com',
|
||||
'https://accounts.firefox.com',
|
||||
'https://accounts.stage.mozaws.net/metrics-flow',
|
||||
'https://am.i.mullvad.net/json'
|
||||
]
|
||||
|
||||
if (AppConstants.FXA_ENABLED) {
|
||||
const fxaSrc = new URL(AppConstants.OAUTH_PROFILE_URI).origin;
|
||||
[imgSrc, connectSrc].forEach(arr => {
|
||||
arr.push(fxaSrc);
|
||||
});
|
||||
arr.push(fxaSrc)
|
||||
})
|
||||
}
|
||||
|
||||
app.use(
|
||||
|
@ -151,103 +150,103 @@ app.use(
|
|||
directives: {
|
||||
baseUri: ["'none'"],
|
||||
defaultSrc: ["'self'"],
|
||||
connectSrc: connectSrc,
|
||||
connectSrc,
|
||||
fontSrc: [
|
||||
"'self'",
|
||||
"https://fonts.gstatic.com/",
|
||||
"https://code.cdn.mozilla.net/fonts/",
|
||||
'https://fonts.gstatic.com/',
|
||||
'https://code.cdn.mozilla.net/fonts/'
|
||||
],
|
||||
frameAncestors: FRAME_ANCESTORS,
|
||||
mediaSrc: [
|
||||
"'self'",
|
||||
"https://monitor.cdn.mozilla.net/",
|
||||
'https://monitor.cdn.mozilla.net/'
|
||||
],
|
||||
formAction: ["'self'"],
|
||||
imgSrc: imgSrc,
|
||||
imgSrc,
|
||||
objectSrc: ["'none'"],
|
||||
scriptSrc: SCRIPT_SOURCES,
|
||||
styleSrc: STYLE_SOURCES,
|
||||
reportUri: "/__cspreport__",
|
||||
},
|
||||
reportUri: '/__cspreport__'
|
||||
}
|
||||
})
|
||||
);
|
||||
app.use(helmet.referrerPolicy({ policy: "strict-origin-when-cross-origin" }));
|
||||
)
|
||||
app.use(helmet.referrerPolicy({ policy: 'strict-origin-when-cross-origin' }))
|
||||
|
||||
// helmet no longer sets X-Content-Type-Options, so set it manually
|
||||
app.use((req, res, next) => {
|
||||
res.setHeader("X-Content-Type-Options", "nosniff");
|
||||
next();
|
||||
});
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff')
|
||||
next()
|
||||
})
|
||||
|
||||
app.use(express.static("public", {
|
||||
setHeaders: res => res.set("Cache-Control",
|
||||
"public, maxage=" + 10 * 60 * 1000 + ", s-maxage=" + 24 * 60 * 60 * 1000),
|
||||
})); // 10-minute client-side caching; 24-hour server-side caching
|
||||
app.use(express.static('public', {
|
||||
setHeaders: res => res.set('Cache-Control',
|
||||
'public, maxage=' + 10 * 60 * 1000 + ', s-maxage=' + 24 * 60 * 60 * 1000)
|
||||
})) // 10-minute client-side caching; 24-hour server-side caching
|
||||
|
||||
app.use(cookieParser());
|
||||
app.use(cookieParser())
|
||||
|
||||
const hbs = exphbs.create({
|
||||
extname: ".hbs",
|
||||
layoutsDir: __dirname + "/views/layouts",
|
||||
defaultLayout: "default",
|
||||
partialsDir: __dirname + "/views/partials",
|
||||
helpers: HBSHelpers.helpers,
|
||||
});
|
||||
app.engine("hbs", hbs.engine);
|
||||
app.set("view engine", "hbs");
|
||||
extname: '.hbs',
|
||||
layoutsDir: __dirname + '/views/layouts',
|
||||
defaultLayout: 'default',
|
||||
partialsDir: __dirname + '/views/partials',
|
||||
helpers: HBSHelpers.helpers
|
||||
})
|
||||
app.engine('hbs', hbs.engine)
|
||||
app.set('view engine', 'hbs')
|
||||
|
||||
// TODO: refactor all templates to use constants.VAR
|
||||
// instead of assigning these 1-by-1 to app.locales
|
||||
app.locals.constants = AppConstants;
|
||||
app.locals.FXA_ENABLED = AppConstants.FXA_ENABLED;
|
||||
app.locals.SERVER_URL = AppConstants.SERVER_URL;
|
||||
app.locals.MAX_NUM_ADDRESSES = AppConstants.MAX_NUM_ADDRESSES;
|
||||
app.locals.EXPERIMENT_ACTIVE = AppConstants.EXPERIMENT_ACTIVE;
|
||||
app.locals.RECRUITMENT_BANNER_LINK = AppConstants.RECRUITMENT_BANNER_LINK;
|
||||
app.locals.RECRUITMENT_BANNER_TEXT = AppConstants.RECRUITMENT_BANNER_TEXT;
|
||||
app.locals.LOGOS_ORIGIN = AppConstants.LOGOS_ORIGIN;
|
||||
app.locals.UTM_SOURCE = new URL(AppConstants.SERVER_URL).hostname;
|
||||
app.locals.constants = AppConstants
|
||||
app.locals.FXA_ENABLED = AppConstants.FXA_ENABLED
|
||||
app.locals.SERVER_URL = AppConstants.SERVER_URL
|
||||
app.locals.MAX_NUM_ADDRESSES = AppConstants.MAX_NUM_ADDRESSES
|
||||
app.locals.EXPERIMENT_ACTIVE = AppConstants.EXPERIMENT_ACTIVE
|
||||
app.locals.RECRUITMENT_BANNER_LINK = AppConstants.RECRUITMENT_BANNER_LINK
|
||||
app.locals.RECRUITMENT_BANNER_TEXT = AppConstants.RECRUITMENT_BANNER_TEXT
|
||||
app.locals.LOGOS_ORIGIN = AppConstants.LOGOS_ORIGIN
|
||||
app.locals.UTM_SOURCE = new URL(AppConstants.SERVER_URL).hostname
|
||||
|
||||
const SESSION_DURATION_HOURS = AppConstants.SESSION_DURATION_HOURS || 48;
|
||||
const SESSION_DURATION_HOURS = AppConstants.SESSION_DURATION_HOURS || 48
|
||||
app.use(session({
|
||||
cookie: {
|
||||
httpOnly: true,
|
||||
maxAge: SESSION_DURATION_HOURS * 60 * 60 * 1000, // 48 hours
|
||||
rolling: true,
|
||||
sameSite: "lax",
|
||||
secure: AppConstants.NODE_ENV !== "dev",
|
||||
sameSite: 'lax',
|
||||
secure: AppConstants.NODE_ENV !== 'dev'
|
||||
},
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
secret: AppConstants.COOKIE_SECRET,
|
||||
store: getRedisStore(),
|
||||
}));
|
||||
store: getRedisStore()
|
||||
}))
|
||||
|
||||
app.use(pickLanguage);
|
||||
app.use(addRequestToResponse);
|
||||
app.use(recordVisitFromEmail);
|
||||
app.use(pickLanguage)
|
||||
app.use(addRequestToResponse)
|
||||
app.use(recordVisitFromEmail)
|
||||
|
||||
app.use("/", DockerflowRoutes);
|
||||
app.use("/hibp", HibpRoutes);
|
||||
app.use('/', DockerflowRoutes)
|
||||
app.use('/hibp', HibpRoutes)
|
||||
if (AppConstants.FXA_ENABLED) {
|
||||
app.use("/oauth", OAuthRoutes);
|
||||
app.use('/oauth', OAuthRoutes)
|
||||
}
|
||||
app.use("/scan", ScanRoutes);
|
||||
app.use("/ses", SesRoutes);
|
||||
app.use("/user", UserRoutes);
|
||||
(devOrHeroku ? app.use("/email-l10n", EmailL10nRoutes) : null);
|
||||
app.use("/breach-details", BreachRoutes);
|
||||
app.use("/", HomeRoutes);
|
||||
app.use('/scan', ScanRoutes)
|
||||
app.use('/ses', SesRoutes)
|
||||
app.use('/user', UserRoutes);
|
||||
(devOrHeroku ? app.use('/email-l10n', EmailL10nRoutes) : null)
|
||||
app.use('/breach-details', BreachRoutes)
|
||||
app.use('/', HomeRoutes)
|
||||
|
||||
app.use(logErrors);
|
||||
app.use(localizeErrorMessages);
|
||||
app.use(clientErrorHandler);
|
||||
app.use(errorHandler);
|
||||
app.use(logErrors)
|
||||
app.use(localizeErrorMessages)
|
||||
app.use(clientErrorHandler)
|
||||
app.use(errorHandler)
|
||||
|
||||
EmailUtils.init().then(() => {
|
||||
const listener = app.listen(AppConstants.PORT, () => {
|
||||
log.info("Listening", { port: listener.address().port });
|
||||
});
|
||||
log.info('Listening', { port: listener.address().port })
|
||||
})
|
||||
}).catch(error => {
|
||||
log.error("try-initialize-email-error", { error: error });
|
||||
});
|
||||
log.error('try-initialize-email-error', { error })
|
||||
})
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const crypto = require("crypto");
|
||||
const crypto = require('crypto')
|
||||
|
||||
function getSha1(email) {
|
||||
return crypto.createHash("sha1").update(email).digest("hex");
|
||||
function getSha1 (email) {
|
||||
return crypto.createHash('sha1').update(email).digest('hex')
|
||||
}
|
||||
|
||||
module.exports = getSha1;
|
||||
module.exports = getSha1
|
||||
|
|
|
@ -1,513 +1,514 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const { LocaleUtils } = require("./../locale-utils");
|
||||
const { LocaleUtils } = require('./../locale-utils')
|
||||
|
||||
function getSecurityTipsIntro() {
|
||||
function getSecurityTipsIntro () {
|
||||
return [
|
||||
"Data breaches are becoming more common. Finding out you were part of one usually includes a long list of compromised information, such as your password, username, and email address. What does that mean for your internet safety? What should you do? Learn how you can take control after a data breach and better protect your devices, online accounts, and personal data from cyber criminals.",
|
||||
];
|
||||
'Data breaches are becoming more common. Finding out you were part of one usually includes a long list of compromised information, such as your password, username, and email address. What does that mean for your internet safety? What should you do? Learn how you can take control after a data breach and better protect your devices, online accounts, and personal data from cyber criminals.'
|
||||
]
|
||||
}
|
||||
|
||||
const articleCopy = {
|
||||
"how-hackers-work": {
|
||||
'how-hackers-work': {
|
||||
paragraphs: [
|
||||
{
|
||||
dropCap: "F",
|
||||
leads : [
|
||||
"orget about those hackers in movies trying to crack the code on someone’s computer to get their top-secret files. The hackers responsible for data breaches usually start by targeting companies, rather than specific individuals. They want to get data from as many people as possible so they can use, resell, or leverage it to make money. It all starts with getting your password.",
|
||||
],
|
||||
dropCap: 'F',
|
||||
leads: [
|
||||
'orget about those hackers in movies trying to crack the code on someone’s computer to get their top-secret files. The hackers responsible for data breaches usually start by targeting companies, rather than specific individuals. They want to get data from as many people as possible so they can use, resell, or leverage it to make money. It all starts with getting your password.'
|
||||
]
|
||||
},
|
||||
{
|
||||
subhead: "It's not personal. Not at first.",
|
||||
paragraphs: [
|
||||
"Hackers don’t really care whose personal information and credentials they can get, as long as they can get a lot of it. That’s why cyber criminals often target massive companies with millions of users. These hackers look for a security weakness — the digital equivalent of leaving a door unlocked or window open. They only need to find one door or window to get inside. Then they steal or copy as much personal information as possible.",
|
||||
"Once they get your data, cyber criminals can start their real work. We don’t always know what they intend to do with the data, but usually they will try to find a way to profit from it. The effects on you might not be immediate. But they can be very serious.",
|
||||
],
|
||||
'Hackers don’t really care whose personal information and credentials they can get, as long as they can get a lot of it. That’s why cyber criminals often target massive companies with millions of users. These hackers look for a security weakness — the digital equivalent of leaving a door unlocked or window open. They only need to find one door or window to get inside. Then they steal or copy as much personal information as possible.',
|
||||
'Once they get your data, cyber criminals can start their real work. We don’t always know what they intend to do with the data, but usually they will try to find a way to profit from it. The effects on you might not be immediate. But they can be very serious.'
|
||||
]
|
||||
},
|
||||
{
|
||||
subhead: "All types of data can be valuable.",
|
||||
subhead: 'All types of data can be valuable.',
|
||||
paragraphs: [
|
||||
"Some data — like banking information, bank card numbers, government-issued ID numbers, and PIN numbers — is valuable because it can be used to steal the victim’s identity or withdraw money. Email addresses and passwords are also valuable because hackers can try them on other accounts. All sorts of data can be valuable in some way because it can be sold on the dark web for a profit or kept for some future use.",
|
||||
],
|
||||
},
|
||||
'Some data — like banking information, bank card numbers, government-issued ID numbers, and PIN numbers — is valuable because it can be used to steal the victim’s identity or withdraw money. Email addresses and passwords are also valuable because hackers can try them on other accounts. All sorts of data can be valuable in some way because it can be sold on the dark web for a profit or kept for some future use.'
|
||||
]
|
||||
},
|
||||
{
|
||||
subhead: "Common passwords make a hacker’s work easy.",
|
||||
subhead: 'Common passwords make a hacker’s work easy.',
|
||||
paragraphs: [
|
||||
"Hackers aren’t actually guessing people’s passwords. To crack into accounts, they use automated programs that enter hundreds of popular passwords in just a few seconds. That’s why it’s important to avoid using the same passwords that everyone else does.",
|
||||
'Hackers aren’t actually guessing people’s passwords. To crack into accounts, they use automated programs that enter hundreds of popular passwords in just a few seconds. That’s why it’s important to avoid using the same passwords that everyone else does.'
|
||||
],
|
||||
list: [
|
||||
"123456 and password are the most commonly used passwords. Don’t use them.",
|
||||
"Switching a letter for a symbol (p@ssw0rd!) is an obvious trick hackers know well.",
|
||||
"Avoid favorite sports teams or pop culture references. Use something more obscure.",
|
||||
"Don’t use a single word like sunshine, monkey, or football. Using a phrase or sentence as your password is stronger.",
|
||||
"Don’t use common number patterns like 111111, abc123, or 654321.",
|
||||
"Adding a number or piece of punctuation at the end doesn’t make your password stronger.",
|
||||
],
|
||||
'123456 and password are the most commonly used passwords. Don’t use them.',
|
||||
'Switching a letter for a symbol (p@ssw0rd!) is an obvious trick hackers know well.',
|
||||
'Avoid favorite sports teams or pop culture references. Use something more obscure.',
|
||||
'Don’t use a single word like sunshine, monkey, or football. Using a phrase or sentence as your password is stronger.',
|
||||
'Don’t use common number patterns like 111111, abc123, or 654321.',
|
||||
'Adding a number or piece of punctuation at the end doesn’t make your password stronger.'
|
||||
]
|
||||
},
|
||||
{
|
||||
subhead: "One exposed password can unlock many accounts.",
|
||||
subhead: 'One exposed password can unlock many accounts.',
|
||||
paragraphs: [
|
||||
"Hackers know people reuse the same passwords. If your banking password is the same as your email password is the same as your Amazon password, a single vulnerability in one site can put the others at risk.",
|
||||
"It’s why you should use different passwords for every single account. The average person has 90 accounts, and that’s a lot of passwords to remember. Security experts recommend using a password manager to safely store unique passwords for every site.",
|
||||
],
|
||||
},
|
||||
{
|
||||
subhead: "Hackers don’t care how much money you have.",
|
||||
paragraphs: [
|
||||
"Think you don’t need to worry because you don’t have much money to steal? Hackers couldn’t care less. There are countless ways to leverage all types of personal data for profit.",
|
||||
"Through identity theft, cyber criminals can open new credit cards or apply for loans in your name. By getting your financial information, they can make purchases or withdrawals. These attackers can even find ways to target your friends and family once they gain access to your email.",
|
||||
],
|
||||
'Hackers know people reuse the same passwords. If your banking password is the same as your email password is the same as your Amazon password, a single vulnerability in one site can put the others at risk.',
|
||||
'It’s why you should use different passwords for every single account. The average person has 90 accounts, and that’s a lot of passwords to remember. Security experts recommend using a password manager to safely store unique passwords for every site.'
|
||||
]
|
||||
},
|
||||
],
|
||||
{
|
||||
subhead: 'Hackers don’t care how much money you have.',
|
||||
paragraphs: [
|
||||
'Think you don’t need to worry because you don’t have much money to steal? Hackers couldn’t care less. There are countless ways to leverage all types of personal data for profit.',
|
||||
'Through identity theft, cyber criminals can open new credit cards or apply for loans in your name. By getting your financial information, they can make purchases or withdrawals. These attackers can even find ways to target your friends and family once they gain access to your email.'
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"after-breach": {
|
||||
'after-breach': {
|
||||
paragraphs: [
|
||||
{
|
||||
leads: [
|
||||
"You get an email, either from Firefox Monitor or a company where you have an account. There’s been a security incident. Your account has been compromised.",
|
||||
'You get an email, either from Firefox Monitor or a company where you have an account. There’s been a security incident. Your account has been compromised.'
|
||||
],
|
||||
paragraphs: [
|
||||
"Getting notified that you’ve been a victim of a data breach can be alarming. You have valid cause for concern, but there are a <span class='bold'>few steps you can take immediately to protect your account and limit the damage.</span>",
|
||||
],
|
||||
},
|
||||
{
|
||||
toggles: [
|
||||
{
|
||||
subhead: "Read the details about the breach.",
|
||||
paragraphs: [
|
||||
"Read closely to learn what happened. What personal data of yours was included? Your next steps will depend on what information you need to protect. When did the breach happen? You may receive the notice months or even years after the data breach occurred. Sometimes it takes awhile for companies to discover a breach. Sometimes breaches are not immediately made public.",
|
||||
],
|
||||
},
|
||||
{
|
||||
subhead: "If you haven’t yet, change your password.",
|
||||
paragraphs: [
|
||||
"Lock down your account with a new password. If you can’t log in, contact the website to ask how you can recover or shut down the account. See an account you don’t recognize? The site may have changed names or someone may have created an account for you.",
|
||||
],
|
||||
},
|
||||
{
|
||||
subhead: "If you’ve used that password for other accounts, change those too.",
|
||||
paragraphs: [
|
||||
"Hackers may try to reuse your exposed password to get into other accounts. Create a different password for each website, especially for your financial accounts, email account, and other websites where you save personal information.",
|
||||
],
|
||||
},
|
||||
{
|
||||
subhead: "Take extra steps if your financial data was breached.",
|
||||
paragraphs: [
|
||||
"Many breaches expose emails and passwords, but some do include sensitive financial information. If your bank account or credit card numbers were included in a breach, alert your bank to possible fraud. Monitor statements for charges you don't recognize.",
|
||||
],
|
||||
},
|
||||
{
|
||||
subhead: "Review your credit reports to catch identity theft.",
|
||||
paragraphs: [
|
||||
"If you have credit history in the United States, check your credit reports for suspicious activity. Ensure that no new accounts, loans, or cards have been opened in your name. By law, you’re permitted to one free report from the three major credit reporting bureaus every year. Request them through annualcreditreport.com. And don’t worry, checking your own credit report never affects your score.",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]},
|
||||
"strong-passwords": {
|
||||
paragraphs: [
|
||||
{
|
||||
leads: [
|
||||
"Your password is your first line of defense against hackers and unauthorized access to your accounts. The strength of your passwords directly impacts your online security.",
|
||||
],
|
||||
"Getting notified that you’ve been a victim of a data breach can be alarming. You have valid cause for concern, but there are a <span class='bold'>few steps you can take immediately to protect your account and limit the damage.</span>"
|
||||
]
|
||||
},
|
||||
{
|
||||
toggles: [
|
||||
{
|
||||
subhead: "Combine unrelated words to make stronger passwords.",
|
||||
subhead: 'Read the details about the breach.',
|
||||
paragraphs: [
|
||||
"To create a strong password, try combining two or more unrelated words. It could even be an entire phrase. Then change some of the letters to special letters and numbers. The longer your password, the stronger it is.",
|
||||
"A single word with one letter changed to an @ or ! (such as p@ssword!) doesn’t make for a strong password. Password cracking programs contain every type of these combinations, in every single language.",
|
||||
],
|
||||
'Read closely to learn what happened. What personal data of yours was included? Your next steps will depend on what information you need to protect. When did the breach happen? You may receive the notice months or even years after the data breach occurred. Sometimes it takes awhile for companies to discover a breach. Sometimes breaches are not immediately made public.'
|
||||
]
|
||||
},
|
||||
{
|
||||
subhead: "Certain words should be avoided in all passwords.",
|
||||
subhead: 'If you haven’t yet, change your password.',
|
||||
paragraphs: [
|
||||
"Many people use familiar people, places, or things in passwords because it makes their passwords easy to remember. This also makes your passwords easy for hackers to guess.",
|
||||
"According to a study <a target='_blank' rel='noopener noreferrer' href='https://techland.time.com/2013/08/08/google-reveals-the-10-worst-password-ideas/'>conducted by Google</a>, passwords that contain the following information are considered insecure because they’re easy to figure out. You can find much of this info after reviewing someone’s social media profiles.",
|
||||
'Lock down your account with a new password. If you can’t log in, contact the website to ask how you can recover or shut down the account. See an account you don’t recognize? The site may have changed names or someone may have created an account for you.'
|
||||
]
|
||||
},
|
||||
{
|
||||
subhead: 'If you’ve used that password for other accounts, change those too.',
|
||||
paragraphs: [
|
||||
'Hackers may try to reuse your exposed password to get into other accounts. Create a different password for each website, especially for your financial accounts, email account, and other websites where you save personal information.'
|
||||
]
|
||||
},
|
||||
{
|
||||
subhead: 'Take extra steps if your financial data was breached.',
|
||||
paragraphs: [
|
||||
"Many breaches expose emails and passwords, but some do include sensitive financial information. If your bank account or credit card numbers were included in a breach, alert your bank to possible fraud. Monitor statements for charges you don't recognize."
|
||||
]
|
||||
},
|
||||
{
|
||||
subhead: 'Review your credit reports to catch identity theft.',
|
||||
paragraphs: [
|
||||
'If you have credit history in the United States, check your credit reports for suspicious activity. Ensure that no new accounts, loans, or cards have been opened in your name. By law, you’re permitted to one free report from the three major credit reporting bureaus every year. Request them through annualcreditreport.com. And don’t worry, checking your own credit report never affects your score.'
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
'strong-passwords': {
|
||||
paragraphs: [
|
||||
{
|
||||
leads: [
|
||||
'Your password is your first line of defense against hackers and unauthorized access to your accounts. The strength of your passwords directly impacts your online security.'
|
||||
]
|
||||
},
|
||||
{
|
||||
toggles: [
|
||||
{
|
||||
subhead: 'Combine unrelated words to make stronger passwords.',
|
||||
paragraphs: [
|
||||
'To create a strong password, try combining two or more unrelated words. It could even be an entire phrase. Then change some of the letters to special letters and numbers. The longer your password, the stronger it is.',
|
||||
'A single word with one letter changed to an @ or ! (such as p@ssword!) doesn’t make for a strong password. Password cracking programs contain every type of these combinations, in every single language.'
|
||||
]
|
||||
},
|
||||
{
|
||||
subhead: 'Certain words should be avoided in all passwords.',
|
||||
paragraphs: [
|
||||
'Many people use familiar people, places, or things in passwords because it makes their passwords easy to remember. This also makes your passwords easy for hackers to guess.',
|
||||
"According to a study <a target='_blank' rel='noopener noreferrer' href='https://techland.time.com/2013/08/08/google-reveals-the-10-worst-password-ideas/'>conducted by Google</a>, passwords that contain the following information are considered insecure because they’re easy to figure out. You can find much of this info after reviewing someone’s social media profiles."
|
||||
],
|
||||
list: [
|
||||
"Pet names",
|
||||
"A notable date, such as a wedding anniversary",
|
||||
"A family member’s birthday",
|
||||
"Your child’s name",
|
||||
"Another family member’s name",
|
||||
"Your birthplace",
|
||||
"A favorite holiday",
|
||||
"Something related to your favorite sports team",
|
||||
"The name of a significant other",
|
||||
"The word “Password,” or any variation of it. That includes P@assword!",
|
||||
'Pet names',
|
||||
'A notable date, such as a wedding anniversary',
|
||||
'A family member’s birthday',
|
||||
'Your child’s name',
|
||||
'Another family member’s name',
|
||||
'Your birthplace',
|
||||
'A favorite holiday',
|
||||
'Something related to your favorite sports team',
|
||||
'The name of a significant other',
|
||||
'The word “Password,” or any variation of it. That includes P@assword!'
|
||||
],
|
||||
securityTip: {
|
||||
tipHeadline: "<span class='bold'>SECURITY TIP</span> Steer clear of the 100 most-used passwords.",
|
||||
tipSubhead: "Every year, SplashData evaluates millions of leaked passwords and compiles the <a class='st-copy-link' target='_blank' rel='noopener noreferer' href='https://www.teamsid.com/splashdatas-top-100-worst-passwords-of-2018/'>100 most common ones.</a> The most recent list includes password, 123456, and other passwords you shouldn’t use. ",
|
||||
},
|
||||
tipSubhead: "Every year, SplashData evaluates millions of leaked passwords and compiles the <a class='st-copy-link' target='_blank' rel='noopener noreferer' href='https://www.teamsid.com/splashdatas-top-100-worst-passwords-of-2018/'>100 most common ones.</a> The most recent list includes password, 123456, and other passwords you shouldn’t use. "
|
||||
}
|
||||
},
|
||||
{
|
||||
subhead: "Use different passwords for every account.",
|
||||
subhead: 'Use different passwords for every account.',
|
||||
paragraphs: [
|
||||
"To keep your accounts as secure as possible, it’s best that every single one has a unique password. If one account gets breached, then hackers can’t use those login credentials to gain access to other accounts.",
|
||||
"While no one can stop hackers from hacking, you can stop reusing the same password everywhere. It makes it far too easy for cyber criminals to attack one site and get your password for others.",
|
||||
],
|
||||
'To keep your accounts as secure as possible, it’s best that every single one has a unique password. If one account gets breached, then hackers can’t use those login credentials to gain access to other accounts.',
|
||||
'While no one can stop hackers from hacking, you can stop reusing the same password everywhere. It makes it far too easy for cyber criminals to attack one site and get your password for others.'
|
||||
]
|
||||
},
|
||||
{
|
||||
subhead: "Use a password manager to remember all your passwords.",
|
||||
subhead: 'Use a password manager to remember all your passwords.',
|
||||
paragraphs: [
|
||||
"Do you really need to remember 100 passwords? Not at all. A password manager is a piece of software that keeps all your password safe, encrypted, and protected. It can even generate strong passwords for you and automatically enter them in to websites and apps.",
|
||||
"Password managers act like a digital safe-deposit box for all your online accounts. You just need one key to get into your accounts: A single, easy-to-remember but hard-to-guess password. That password unlocks the safe.",
|
||||
"But what if your password manager gets hacked? A good one keeps your passwords encrypted behind a password they don’t know (only you do). They don’t store any of your credentials on their servers. While no single tool can guarantee total online safety, security experts agree that using a password manager is far more secure than using the same password everywhere.",
|
||||
],
|
||||
},
|
||||
{
|
||||
subhead: "Add an extra layer of security with two-factor authentication.",
|
||||
paragraphs: [
|
||||
"Many websites offer two-factor authentication, also known as 2FA or multi-factor authentication. On top of your username and password, 2FA requires another piece of information to verify yourself. So, even if someone has your password, they can’t get in.",
|
||||
"Withdrawing money from an ATM is an example of 2FA. It requires your PIN code and your bank card. You need these two pieces to complete the transaction.",
|
||||
"<a class='st-copy-link' target='_blank' rel='noopener noreferer' href='https://2fa.directory/'>Websites that support 2FA</a> include Google and Amazon. When you have 2FA enabled, the site will text you a code to enter after your password. Other forms of 2FA include YubiKeys USB ports and security apps like DUO. ",
|
||||
"When you set up 2FA, many sites will give you a list of backup codes to verify your account. A password manager is a great place to store these codes.",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
passwordDosAndDonts: {
|
||||
listHeadline: "Password do’s and don’ts",
|
||||
doDontList : [
|
||||
{
|
||||
do: "Do use different passwords everywhere. Password managers and many browsers can generate secure and unique passwords.",
|
||||
dont: "Don’t use variations of the same password for different accounts.",
|
||||
},
|
||||
{
|
||||
do: "Do combine two or more unrelated words. Change letters to numbers or special characters.",
|
||||
dont: "Don’t use the word “password,” or any variation of it. “P@ssword!” is just as easy for hackers to guess.",
|
||||
},
|
||||
{
|
||||
do: "Do make your passwords at least 8 characters long. Aim for 12-15 characters.",
|
||||
dont: "Don't use short, one-word passwords, like sunshine, monkey, or football.",
|
||||
},
|
||||
{
|
||||
do: "Do intersperse numbers, symbols, and special characters throughout.",
|
||||
dont: "Don’t place special characters (@, !, 0, etc.) only at the beginning or the end.",
|
||||
},
|
||||
{
|
||||
do: "Do include unusual words only you would know. It should seem nonsensical to other people.",
|
||||
dont: "Don’t include personal information like your birthdate, address, or family members’ names.",
|
||||
},
|
||||
{
|
||||
do: "Do keep your passwords protected and safe, like encrypted in a password manager.",
|
||||
dont: "Don’t share your passwords. Don’t put them on a piece of paper stuck to your computer.",
|
||||
},
|
||||
{
|
||||
do: "Do spread various numbers and characters throughout your password.",
|
||||
dont: "Don’t use common patterns like 111111, abc123, or 654321.",
|
||||
},
|
||||
{
|
||||
do: "Do use an extra layer of security with two-factor authentication (2FA).",
|
||||
dont: "Don’t think a weaker password is safer because you have 2FA.",
|
||||
},
|
||||
],
|
||||
},
|
||||
'Do you really need to remember 100 passwords? Not at all. A password manager is a piece of software that keeps all your password safe, encrypted, and protected. It can even generate strong passwords for you and automatically enter them in to websites and apps.',
|
||||
'Password managers act like a digital safe-deposit box for all your online accounts. You just need one key to get into your accounts: A single, easy-to-remember but hard-to-guess password. That password unlocks the safe.',
|
||||
'But what if your password manager gets hacked? A good one keeps your passwords encrypted behind a password they don’t know (only you do). They don’t store any of your credentials on their servers. While no single tool can guarantee total online safety, security experts agree that using a password manager is far more secure than using the same password everywhere.'
|
||||
]
|
||||
},
|
||||
{
|
||||
subhead: 'Add an extra layer of security with two-factor authentication.',
|
||||
paragraphs: [
|
||||
'Many websites offer two-factor authentication, also known as 2FA or multi-factor authentication. On top of your username and password, 2FA requires another piece of information to verify yourself. So, even if someone has your password, they can’t get in.',
|
||||
'Withdrawing money from an ATM is an example of 2FA. It requires your PIN code and your bank card. You need these two pieces to complete the transaction.',
|
||||
"<a class='st-copy-link' target='_blank' rel='noopener noreferer' href='https://2fa.directory/'>Websites that support 2FA</a> include Google and Amazon. When you have 2FA enabled, the site will text you a code to enter after your password. Other forms of 2FA include YubiKeys USB ports and security apps like DUO. ",
|
||||
'When you set up 2FA, many sites will give you a list of backup codes to verify your account. A password manager is a great place to store these codes.'
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
{
|
||||
passwordDosAndDonts: {
|
||||
listHeadline: 'Password do’s and don’ts',
|
||||
doDontList: [
|
||||
{
|
||||
do: 'Do use different passwords everywhere. Password managers and many browsers can generate secure and unique passwords.',
|
||||
dont: 'Don’t use variations of the same password for different accounts.'
|
||||
},
|
||||
{
|
||||
do: 'Do combine two or more unrelated words. Change letters to numbers or special characters.',
|
||||
dont: 'Don’t use the word “password,” or any variation of it. “P@ssword!” is just as easy for hackers to guess.'
|
||||
},
|
||||
{
|
||||
do: 'Do make your passwords at least 8 characters long. Aim for 12-15 characters.',
|
||||
dont: "Don't use short, one-word passwords, like sunshine, monkey, or football."
|
||||
},
|
||||
{
|
||||
do: 'Do intersperse numbers, symbols, and special characters throughout.',
|
||||
dont: 'Don’t place special characters (@, !, 0, etc.) only at the beginning or the end.'
|
||||
},
|
||||
{
|
||||
do: 'Do include unusual words only you would know. It should seem nonsensical to other people.',
|
||||
dont: 'Don’t include personal information like your birthdate, address, or family members’ names.'
|
||||
},
|
||||
{
|
||||
do: 'Do keep your passwords protected and safe, like encrypted in a password manager.',
|
||||
dont: 'Don’t share your passwords. Don’t put them on a piece of paper stuck to your computer.'
|
||||
},
|
||||
{
|
||||
do: 'Do spread various numbers and characters throughout your password.',
|
||||
dont: 'Don’t use common patterns like 111111, abc123, or 654321.'
|
||||
},
|
||||
{
|
||||
do: 'Do use an extra layer of security with two-factor authentication (2FA).',
|
||||
dont: 'Don’t think a weaker password is safer because you have 2FA.'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"steps-to-protect": {
|
||||
'steps-to-protect': {
|
||||
paragraphs: [
|
||||
{
|
||||
leads: [
|
||||
"Data breaches are one of many online threats. Using secure internet connections, updating your software, avoiding scam emails, and employing better password hygiene will help you stay safer while you browse.",
|
||||
],
|
||||
'Data breaches are one of many online threats. Using secure internet connections, updating your software, avoiding scam emails, and employing better password hygiene will help you stay safer while you browse.'
|
||||
]
|
||||
},
|
||||
{
|
||||
toggles: [
|
||||
{
|
||||
subhead: "Be wary of public Wi-Fi networks.",
|
||||
subhead: 'Be wary of public Wi-Fi networks.',
|
||||
paragraphs: [
|
||||
"You can get Wi-Fi almost anywhere. But these open networks are the most vulnerable and tend to be the least secure. This includes the free Wi-Fi at restaurants, libraries, airports, and other public spaces. If you can avoid it, don’t use public Wi-Fi. Most importantly, don’t use these networks to log in to financial sites or shop online. It’s easy for anyone to see what you’re doing.",
|
||||
"Instead, we recommend using a Virtual Private Network (like <a target='_blank' href='https://vpn.mozilla.org/?utm_source=monitor.firefox.com&utm_medium=referral&utm_campaign=monitor-security-tips'>Mozilla VPN</a>), which lets you use public Wi-Fi more securely and keeps your online behavior private. A VPN routes your connection through a secure server that encrypts your data before you land on a web page. ",
|
||||
],
|
||||
'You can get Wi-Fi almost anywhere. But these open networks are the most vulnerable and tend to be the least secure. This includes the free Wi-Fi at restaurants, libraries, airports, and other public spaces. If you can avoid it, don’t use public Wi-Fi. Most importantly, don’t use these networks to log in to financial sites or shop online. It’s easy for anyone to see what you’re doing.',
|
||||
"Instead, we recommend using a Virtual Private Network (like <a target='_blank' href='https://vpn.mozilla.org/?utm_source=monitor.firefox.com&utm_medium=referral&utm_campaign=monitor-security-tips'>Mozilla VPN</a>), which lets you use public Wi-Fi more securely and keeps your online behavior private. A VPN routes your connection through a secure server that encrypts your data before you land on a web page. "
|
||||
]
|
||||
},
|
||||
{
|
||||
subhead: "Run software and app updates as soon as they’re available.",
|
||||
subhead: 'Run software and app updates as soon as they’re available.',
|
||||
paragraphs: [
|
||||
"Updating software on your computer or phone can seem like a pain, but it’s a crucial step to keeping devices safe. These updates fix bugs, software vulnerabilities, and security problems. Regularly updating your smartphone apps and operating systems makes your devices more secure.",
|
||||
'Updating software on your computer or phone can seem like a pain, but it’s a crucial step to keeping devices safe. These updates fix bugs, software vulnerabilities, and security problems. Regularly updating your smartphone apps and operating systems makes your devices more secure.'
|
||||
],
|
||||
listHeadline: "Tips for keeping all your online accounts secure",
|
||||
listHeadline: 'Tips for keeping all your online accounts secure',
|
||||
list: [
|
||||
"Use unique, strong passwords for every account",
|
||||
"Use a password manager to remember all your passwords for you",
|
||||
"Turn on two-factor authentication for an extra layer of security",
|
||||
'Use unique, strong passwords for every account',
|
||||
'Use a password manager to remember all your passwords for you',
|
||||
'Turn on two-factor authentication for an extra layer of security',
|
||||
"Use a VPN (like <a target='_blank' href='https://vpn.mozilla.org/?utm_source=monitor.firefox.com&utm_medium=referral&utm_campaign=monitor-security-tips'>Mozilla VPN</a>) when using public Wi-Fi",
|
||||
"Update to the latest version of all software and apps",
|
||||
],
|
||||
'Update to the latest version of all software and apps'
|
||||
]
|
||||
},
|
||||
{
|
||||
securityTip: {
|
||||
tipHeadline: "<span class='bold'>SECURITY TIP</span> Turn on automatic updates.",
|
||||
tipSubhead: "You can set your computer, browser, apps, and phone to update automatically as soon as new updates become available. Set it and forget it!",
|
||||
},
|
||||
tipSubhead: 'You can set your computer, browser, apps, and phone to update automatically as soon as new updates become available. Set it and forget it!'
|
||||
}
|
||||
},
|
||||
{
|
||||
subhead: "Be vigilant about emails that seem even a little bit strange.",
|
||||
subhead: 'Be vigilant about emails that seem even a little bit strange.',
|
||||
paragraphs: [
|
||||
"Phishing is a type of email scam that is becoming increasingly common. In these emails, hackers impersonate a service or company you trust. These emails can even come from one of your contacts. They look like the real thing because they mimic the design of authentic emails, like those from your bank or email provider.",
|
||||
"The goal of these hackers is to get you to unknowingly enter your password or download a document that can infect your computer. Most online services won’t ask you to enter your login info directly from an email. If they do, you should instead go directly to their website to log in.",
|
||||
"Think before you fill anything out. Does this email seem out of the blue? Does something seem off about it? Are you being asked to log in to an account to update something? Don’t click, and don’t enter your password anywhere. Open your browser, and type in the address of the company website instead.",
|
||||
'Phishing is a type of email scam that is becoming increasingly common. In these emails, hackers impersonate a service or company you trust. These emails can even come from one of your contacts. They look like the real thing because they mimic the design of authentic emails, like those from your bank or email provider.',
|
||||
'The goal of these hackers is to get you to unknowingly enter your password or download a document that can infect your computer. Most online services won’t ask you to enter your login info directly from an email. If they do, you should instead go directly to their website to log in.',
|
||||
'Think before you fill anything out. Does this email seem out of the blue? Does something seem off about it? Are you being asked to log in to an account to update something? Don’t click, and don’t enter your password anywhere. Open your browser, and type in the address of the company website instead.'
|
||||
],
|
||||
listHeadline: "Classic signs of a suspicious email",
|
||||
listHeadline: 'Classic signs of a suspicious email',
|
||||
list: [
|
||||
"Displays grammar or spelling mistakes",
|
||||
"Seems especially urgent or time-critical",
|
||||
"Send address looks unusual",
|
||||
"Promises something that seems too good to be true",
|
||||
"Asks you to log in from the email itself",
|
||||
"Asks you to open or download a file that you don’t recognize",
|
||||
'Displays grammar or spelling mistakes',
|
||||
'Seems especially urgent or time-critical',
|
||||
'Send address looks unusual',
|
||||
'Promises something that seems too good to be true',
|
||||
'Asks you to log in from the email itself',
|
||||
'Asks you to open or download a file that you don’t recognize'
|
||||
],
|
||||
warning: "Clicked on a phishing email? Contact your email administrator right away for next steps. Do this before sending or opening any other emails. The faster you report it, the faster the damage can be mitigated.",
|
||||
warning: 'Clicked on a phishing email? Contact your email administrator right away for next steps. Do this before sending or opening any other emails. The faster you report it, the faster the damage can be mitigated.'
|
||||
},
|
||||
{
|
||||
subhead: "Be selective about who you give your email address to.",
|
||||
subhead: 'Be selective about who you give your email address to.',
|
||||
paragraphs: [
|
||||
"The more online accounts you create, the greater the risk that you’ll be involved in a data breach. Many companies, services, apps, and websites ask for your email. But it’s not always required. Here are some ways to avoid giving out your email address:",
|
||||
'The more online accounts you create, the greater the risk that you’ll be involved in a data breach. Many companies, services, apps, and websites ask for your email. But it’s not always required. Here are some ways to avoid giving out your email address:'
|
||||
],
|
||||
list: [
|
||||
"Don’t create an account if it’s not required. For example, many online shopping portals allow you to check out as a guest.",
|
||||
"If a website requires an email address, use services like 10minutemail or Nada, which allow you to create a temporary one.",
|
||||
"Create a different email to sign up for promotions and newsletters. Don’t include any personal info that could be used to identify you in that email address, like your name or birthday.",
|
||||
],
|
||||
'Don’t create an account if it’s not required. For example, many online shopping portals allow you to check out as a guest.',
|
||||
'If a website requires an email address, use services like 10minutemail or Nada, which allow you to create a temporary one.',
|
||||
'Create a different email to sign up for promotions and newsletters. Don’t include any personal info that could be used to identify you in that email address, like your name or birthday.'
|
||||
]
|
||||
},
|
||||
{
|
||||
subhead: "Use unique, strong passwords for every single account.",
|
||||
subhead: 'Use unique, strong passwords for every single account.',
|
||||
paragraphs: [
|
||||
"One of the best ways to protect yourself online is to use different passwords across all your online accounts. This way, hackers won’t have the keys to your entire digital life if they get their hands on that one password you use everywhere.",
|
||||
"Your passwords also need to be strong. Single words (like sunshine, monkey, or football) make for weak passwords. So do these 100 most-commonly used passwords, which include password and 123456. Avoid pop-culture references, sports teams, and personal info. Do not use your address, birthday, names of family members, or pets’ names. The longer and more unique your passwords are, the harder they will be for hackers to crack.",
|
||||
'One of the best ways to protect yourself online is to use different passwords across all your online accounts. This way, hackers won’t have the keys to your entire digital life if they get their hands on that one password you use everywhere.',
|
||||
'Your passwords also need to be strong. Single words (like sunshine, monkey, or football) make for weak passwords. So do these 100 most-commonly used passwords, which include password and 123456. Avoid pop-culture references, sports teams, and personal info. Do not use your address, birthday, names of family members, or pets’ names. The longer and more unique your passwords are, the harder they will be for hackers to crack.'
|
||||
],
|
||||
securityTip: {
|
||||
tipHeadline: "<span class='bold'>SECURITY TIP</span> How to create strong passwords",
|
||||
tipSubhead: "Include a combination of upper and lowercase letters, numbers, and characters. Combining a few unrelated words and changing the letters is a good method. <a class='st-link' href='#strong-passwords'>Read the guide</a>",
|
||||
},
|
||||
tipSubhead: "Include a combination of upper and lowercase letters, numbers, and characters. Combining a few unrelated words and changing the letters is a good method. <a class='st-link' href='#strong-passwords'>Read the guide</a>"
|
||||
}
|
||||
},
|
||||
{
|
||||
subhead: "Remember all your passwords with a password manager.",
|
||||
subhead: 'Remember all your passwords with a password manager.',
|
||||
paragraphs: [
|
||||
"Ever forgotten your password? It happens all the time. The average person has 90 online accounts. And we’re being asked to create new ones all the time.",
|
||||
"The good news is you don’t have to recall all your passwords from memory. Password managers are secure, easy-to-use applications that do the remembering for you. They even fill your passwords into websites and apps when you need to log in. All you need to remember is a single password — the one you use to unlock your password manager. They can even generate hard-to-guess passwords to help make your accounts more secure. All your data is encrypted, making password managers pretty secure — even if they get hacked.",
|
||||
],
|
||||
'Ever forgotten your password? It happens all the time. The average person has 90 online accounts. And we’re being asked to create new ones all the time.',
|
||||
'The good news is you don’t have to recall all your passwords from memory. Password managers are secure, easy-to-use applications that do the remembering for you. They even fill your passwords into websites and apps when you need to log in. All you need to remember is a single password — the one you use to unlock your password manager. They can even generate hard-to-guess passwords to help make your accounts more secure. All your data is encrypted, making password managers pretty secure — even if they get hacked.'
|
||||
],
|
||||
securityTip: {
|
||||
tipHeadline: "<span class='bold'>SECURITY TIP</span>",
|
||||
tipSubhead: "Firefox recommends 1Password, LastPass, Dashlane, and Bitwarden for security and ease of use.",
|
||||
},
|
||||
tipSubhead: 'Firefox recommends 1Password, LastPass, Dashlane, and Bitwarden for security and ease of use.'
|
||||
}
|
||||
},
|
||||
{
|
||||
securityTip: {
|
||||
tipHeadline: "<span class='bold'>SECURITY TIP</span>",
|
||||
tipSubhead: "Still wary of password managers? What’s most important is that you use different passwords everywhere. To remember them, write down your passwords and store them in a safe place that only you have access to.",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
tipSubhead: 'Still wary of password managers? What’s most important is that you use different passwords everywhere. To remember them, write down your passwords and store them in a safe place that only you have access to.'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"five-myths": {
|
||||
'five-myths': {
|
||||
paragraphs: [
|
||||
{
|
||||
leads: [
|
||||
"Password managers are the most recommended tool by security experts to protect your online credentials from hackers. But many people are still hesitant to use them. Here’s why password managers are safe, secure, and your best defense against password-hungry cyber criminals.",
|
||||
],
|
||||
'Password managers are the most recommended tool by security experts to protect your online credentials from hackers. But many people are still hesitant to use them. Here’s why password managers are safe, secure, and your best defense against password-hungry cyber criminals.'
|
||||
]
|
||||
},
|
||||
{
|
||||
subhead: 'What is a password manager?',
|
||||
paragraphs: [
|
||||
'Think of it like a safe for your passwords. When you need something inside the safe, you unlock it. Password managers work the same for your online credentials.',
|
||||
'You create a single, super-strong password, which acts like a key. Install the password manager app on your phone, computer, browser, and other devices. Your passwords are securely stored inside. Anytime you need to log in to an account, unlock your password manager and retrieve your login info.'
|
||||
]
|
||||
},
|
||||
{
|
||||
subhead: "What is a password manager?",
|
||||
paragraphs: [
|
||||
"Think of it like a safe for your passwords. When you need something inside the safe, you unlock it. Password managers work the same for your online credentials.",
|
||||
"You create a single, super-strong password, which acts like a key. Install the password manager app on your phone, computer, browser, and other devices. Your passwords are securely stored inside. Anytime you need to log in to an account, unlock your password manager and retrieve your login info.",
|
||||
],
|
||||
},
|
||||
{
|
||||
toggles: [
|
||||
{
|
||||
subhead: "<span class='myth'>Myth 1:</span> Password managers aren’t safe or trustworthy.",
|
||||
paragraphs: [
|
||||
"With website vulnerabilities and security incidents on the rise, many people have grown to mistrust a tech tool to manage their passwords. What if the password manager gets hacked?",
|
||||
"Reputable password managers take extra steps to lock down your info and keep it safe from cyber criminals.",
|
||||
'With website vulnerabilities and security incidents on the rise, many people have grown to mistrust a tech tool to manage their passwords. What if the password manager gets hacked?',
|
||||
'Reputable password managers take extra steps to lock down your info and keep it safe from cyber criminals.'
|
||||
],
|
||||
listHeadline: "A good password manager:",
|
||||
listHeadline: 'A good password manager:',
|
||||
list: [
|
||||
"Doesn’t know your primary password (so hackers can never steal it)",
|
||||
"Only stores encrypted versions of your credentials and data on their servers ",
|
||||
"Does not store any of your data on their servers",
|
||||
"Can generate strong, secure password",
|
||||
],
|
||||
},
|
||||
],
|
||||
'Doesn’t know your primary password (so hackers can never steal it)',
|
||||
'Only stores encrypted versions of your credentials and data on their servers ',
|
||||
'Does not store any of your data on their servers',
|
||||
'Can generate strong, secure password'
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
toggles: [
|
||||
{
|
||||
subhead: "<span class='myth'>Myth 2:</span> Password managers aren’t 100% secure, so I shouldn’t use one.",
|
||||
paragraphs: [
|
||||
"No tool can completely guarantee your online safety. Even the most elaborate lock can be broken into. Yet we still lock our doors to our houses and cars. ",
|
||||
"The alternative to using a password manager is to rely on your own memory to remember all your credentials. This inevitably leads to recycling passwords or using variations — a bad habit that hackers love.",
|
||||
"Password managers can be such an effective security tool because they help us improve bad habits. With a password manager installed on your computer and phone, it’s a lot easier to take your logins everywhere so you can use unique, strong passwords on every account.",
|
||||
],
|
||||
'No tool can completely guarantee your online safety. Even the most elaborate lock can be broken into. Yet we still lock our doors to our houses and cars. ',
|
||||
'The alternative to using a password manager is to rely on your own memory to remember all your credentials. This inevitably leads to recycling passwords or using variations — a bad habit that hackers love.',
|
||||
'Password managers can be such an effective security tool because they help us improve bad habits. With a password manager installed on your computer and phone, it’s a lot easier to take your logins everywhere so you can use unique, strong passwords on every account.'
|
||||
]
|
||||
},
|
||||
{
|
||||
subhead: "<span class='myth'>Myth 3:</span> Storing all my passwords in one place makes them vulnerable to hackers.",
|
||||
paragraphs: [
|
||||
"Password managers don’t store all your credentials together in one place. Any data you store in a password manager — passwords, logins, security questions, and other sensitive info — is securely encrypted. Even if the password manager gets hacked, cyber criminals would not be able to see your logins.",
|
||||
"The only way to access your data is with a single primary password that only you know. You use this password to unlock the manager on your computer, phone, or other devices. Once it’s unlocked, a password manager can fill in your logins to websites and apps.",
|
||||
],
|
||||
'Password managers don’t store all your credentials together in one place. Any data you store in a password manager — passwords, logins, security questions, and other sensitive info — is securely encrypted. Even if the password manager gets hacked, cyber criminals would not be able to see your logins.',
|
||||
'The only way to access your data is with a single primary password that only you know. You use this password to unlock the manager on your computer, phone, or other devices. Once it’s unlocked, a password manager can fill in your logins to websites and apps.'
|
||||
]
|
||||
},
|
||||
{
|
||||
subhead: "<span class='myth'>Myth 4:</span> Remembering all my passwords is safer than trusting technology to do it for me.",
|
||||
paragraphs: [
|
||||
"Our memories sometimes fail us. Ever clicked a “forgot password?” link? It’s very common to use variations of the same password to make them easier to remember. With a password manager, you don’t need to remember any of your credentials. It can be installed on all your devices and will auto-fill your passwords for you. Once you get in the habit of using one, you’ll no longer have to worry about forgetting your credentials.",
|
||||
],
|
||||
'Our memories sometimes fail us. Ever clicked a “forgot password?” link? It’s very common to use variations of the same password to make them easier to remember. With a password manager, you don’t need to remember any of your credentials. It can be installed on all your devices and will auto-fill your passwords for you. Once you get in the habit of using one, you’ll no longer have to worry about forgetting your credentials.'
|
||||
]
|
||||
},
|
||||
{
|
||||
subhead: "<span class='myth'>Myth 5:</span> It’s a huge pain to set up a password manager.",
|
||||
paragraphs: [
|
||||
"Sure, it takes time to log all your credentials in a password manager. But you don’t need to do it all at once. You can always start small and change just a few passwords at a time. Try installing a password manager and creating new, unique passwords for the websites you visit most frequently. Over time, as you log in to other sites, you can add others.",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
'Sure, it takes time to log all your credentials in a password manager. But you don’t need to do it all at once. You can always start small and change just a few passwords at a time. Try installing a password manager and creating new, unique passwords for the websites you visit most frequently. Over time, as you log in to other sites, you can add others.'
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"next-steps": {
|
||||
'next-steps': {
|
||||
paragraphs: [
|
||||
{
|
||||
leads: [
|
||||
"When significant data breaches happen where high-risk data is at stake, there’s often talk about credit reports. Some companies may even be required to provide credit monitoring as part of its breach notification requirements. Security experts recommend you check your credit reports for suspicious activity. To protect your identity, they also recommend you freeze your credit. Here’s what that means and why it’s important.",
|
||||
'When significant data breaches happen where high-risk data is at stake, there’s often talk about credit reports. Some companies may even be required to provide credit monitoring as part of its breach notification requirements. Security experts recommend you check your credit reports for suspicious activity. To protect your identity, they also recommend you freeze your credit. Here’s what that means and why it’s important.'
|
||||
],
|
||||
subhead: "What’s a credit report? Do I have one?",
|
||||
subhead: 'What’s a credit report? Do I have one?',
|
||||
paragraphs: [
|
||||
"If you’ve ever rented an apartment, opened a bank account, or applied for a credit card or a loan, you likely have a credit report.",
|
||||
"In fact, you have three credit reports. There are three credit-reporting bureaus in the United States: Experian, TransUnion, and Equifax. Each one holds a report on you that contains personal information about your credit history. Your credit reports contain:",
|
||||
],
|
||||
'If you’ve ever rented an apartment, opened a bank account, or applied for a credit card or a loan, you likely have a credit report.',
|
||||
'In fact, you have three credit reports. There are three credit-reporting bureaus in the United States: Experian, TransUnion, and Equifax. Each one holds a report on you that contains personal information about your credit history. Your credit reports contain:'
|
||||
]
|
||||
},
|
||||
{
|
||||
list: [
|
||||
"Personal identifying information, such as your name, past and current addresses, Social Security number, and date of birth.",
|
||||
"Current and past credit accounts, such as credit cards, mortgages, student loans, and auto loans.",
|
||||
"Inquiry information, which are instances in which you’ve applied for new loans or credit cards.",
|
||||
"Bankruptcies and collection information.",
|
||||
"Your credit report does not include your credit score.",
|
||||
],
|
||||
'Personal identifying information, such as your name, past and current addresses, Social Security number, and date of birth.',
|
||||
'Current and past credit accounts, such as credit cards, mortgages, student loans, and auto loans.',
|
||||
'Inquiry information, which are instances in which you’ve applied for new loans or credit cards.',
|
||||
'Bankruptcies and collection information.',
|
||||
'Your credit report does not include your credit score.'
|
||||
]
|
||||
},
|
||||
{
|
||||
toggles: [
|
||||
{
|
||||
subhead: "Why you should check your credit reports once a year.",
|
||||
subhead: 'Why you should check your credit reports once a year.',
|
||||
paragraphs: [
|
||||
"Having your information exposed in a data breach puts you at risk of identity theft. If someone steals your identity and tries to open new cards or loans in your name, it will appear on your credit reports. Each bureau may have slightly different information, which is why it’s important to check all three regularly.",
|
||||
"By law, you are entitled to one free credit report a year from each of the three credit bureaus. You can request your credit reports at annualcreditreport.com. This is the only official and truly free website to obtain your reports. You can also call Experian, TransUnion, and Equifax directly or request your reports by mail.",
|
||||
],
|
||||
},
|
||||
],
|
||||
'Having your information exposed in a data breach puts you at risk of identity theft. If someone steals your identity and tries to open new cards or loans in your name, it will appear on your credit reports. Each bureau may have slightly different information, which is why it’s important to check all three regularly.',
|
||||
'By law, you are entitled to one free credit report a year from each of the three credit bureaus. You can request your credit reports at annualcreditreport.com. This is the only official and truly free website to obtain your reports. You can also call Experian, TransUnion, and Equifax directly or request your reports by mail.'
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
subhead: "Checking your own credit report will not affect your score.",
|
||||
subhead: 'Checking your own credit report will not affect your score.',
|
||||
paragraphs: [
|
||||
"You will never be penalized for checking your own report or your own credit score. Checking your report does not impact your score in any way. Experian, TransUnion, and Equifax may offer paid identity monitoring packages or charge for access to your credit score, but it’s always free to check your report once a year.",
|
||||
"Though the information on your credit report directly impacts your score, reports don’t actually contain your score. There are many websites, services, and credit cards where you can check your score for free. So it’s usually not necessary to pay the bureaus themselves to see your score.",
|
||||
],
|
||||
'You will never be penalized for checking your own report or your own credit score. Checking your report does not impact your score in any way. Experian, TransUnion, and Equifax may offer paid identity monitoring packages or charge for access to your credit score, but it’s always free to check your report once a year.',
|
||||
'Though the information on your credit report directly impacts your score, reports don’t actually contain your score. There are many websites, services, and credit cards where you can check your score for free. So it’s usually not necessary to pay the bureaus themselves to see your score.'
|
||||
]
|
||||
},
|
||||
{
|
||||
toggles: [
|
||||
{
|
||||
subhead: "What to look for to spot signs of identity theft.",
|
||||
subhead: 'What to look for to spot signs of identity theft.',
|
||||
paragraphs: [
|
||||
"When you receive your credit reports from Experian, TransUnion, and Equifax, review them carefully. These are long, dense documents that can be overwhelming, especially if you have a long credit history. You have a right to correct any inaccurate information on your record with the credit bureau. Make sure:",
|
||||
'When you receive your credit reports from Experian, TransUnion, and Equifax, review them carefully. These are long, dense documents that can be overwhelming, especially if you have a long credit history. You have a right to correct any inaccurate information on your record with the credit bureau. Make sure:'
|
||||
],
|
||||
list: [
|
||||
"All the accounts listed are ones you personally opened.",
|
||||
"All addresses listed and your employer are correct.",
|
||||
"Your balances and credit history are correct.",
|
||||
"All hard credit inquiries are from loans or credit cards you applied for. Soft inquiries may be listed, which are from pre-approved credit card offers. These do not affect your score.",
|
||||
"If anything looks strange or is incorrect, contact the credit bureau immediately to begin a dispute. Instructions for how to initiate corrections can be found on your report. It’s important not to let inaccuracies linger as they may lower your score or be difficult to clear later. If you are concerned that you might be a victim of identity theft, report it and get help from the Federal Trade Commision at identitytheft.gov.",
|
||||
'All the accounts listed are ones you personally opened.',
|
||||
'All addresses listed and your employer are correct.',
|
||||
'Your balances and credit history are correct.',
|
||||
'All hard credit inquiries are from loans or credit cards you applied for. Soft inquiries may be listed, which are from pre-approved credit card offers. These do not affect your score.',
|
||||
'If anything looks strange or is incorrect, contact the credit bureau immediately to begin a dispute. Instructions for how to initiate corrections can be found on your report. It’s important not to let inaccuracies linger as they may lower your score or be difficult to clear later. If you are concerned that you might be a victim of identity theft, report it and get help from the Federal Trade Commision at identitytheft.gov.'
|
||||
],
|
||||
warning: [
|
||||
"If anything looks strange or is incorrect, contact the credit bureau immediately to begin a dispute. All have processes by which you can dispute inaccuracies by mail or online and get information corrected. ",
|
||||
],
|
||||
},
|
||||
],
|
||||
'If anything looks strange or is incorrect, contact the credit bureau immediately to begin a dispute. All have processes by which you can dispute inaccuracies by mail or online and get information corrected. '
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
subhead: "Next step: Block unauthorized access to your credit report with a credit freeze. ",
|
||||
subhead: 'Next step: Block unauthorized access to your credit report with a credit freeze. ',
|
||||
paragraphs: [
|
||||
"Placing a freeze on your credit report is the most effective method to stop identity thieves in their tracks. It’s completely free with all three bureaus and will not affect your credit cards, credit report, or credit score. You can continue using your cards as you were before.",
|
||||
"Freezing your credit report means only you can apply for new cards or loans. No one else will be able to do this in your name. It’s like putting a lock on your credit report, and only you have the key. You can unlock (or unfreeze) your credit report at any time. For example, you may want to open a new credit card. You can temporarily lift the freeze to do so, then refreeze your credit report again after.",
|
||||
"Federal legislation requires credit-reporting agencies to offer free credit freezes and unfreezes. To freeze your credit report with Experian, TransUnion, and Equifax, call them directly or do it on their websites. You may be asked to create a PIN code or they may generate one for you. Keep this code safe, because it’s the one you’ll use if you need to unlock your credit. A password manager is a great place to save your PIN codes.",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
'Placing a freeze on your credit report is the most effective method to stop identity thieves in their tracks. It’s completely free with all three bureaus and will not affect your credit cards, credit report, or credit score. You can continue using your cards as you were before.',
|
||||
'Freezing your credit report means only you can apply for new cards or loans. No one else will be able to do this in your name. It’s like putting a lock on your credit report, and only you have the key. You can unlock (or unfreeze) your credit report at any time. For example, you may want to open a new credit card. You can temporarily lift the freeze to do so, then refreeze your credit report again after.',
|
||||
'Federal legislation requires credit-reporting agencies to offer free credit freezes and unfreezes. To freeze your credit report with Experian, TransUnion, and Equifax, call them directly or do it on their websites. You may be asked to create a PIN code or they may generate one for you. Keep this code safe, because it’s the one you’ll use if you need to unlock your credit. A password manager is a great place to save your PIN codes.'
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
function getArticleCopy(args) {
|
||||
const articles = articleLinks(args);
|
||||
articles.forEach(article => {
|
||||
const articleName = article["class"];
|
||||
article["copy"] = articleCopy[articleName].paragraphs;
|
||||
});
|
||||
|
||||
return articles;
|
||||
}
|
||||
|
||||
function articleLinks(args) {
|
||||
const locales = args.data.root.req.supportedLocales;
|
||||
function getArticleCopy (args) {
|
||||
const articles = articleLinks(args)
|
||||
articles.forEach(article => {
|
||||
const articleName = article.class
|
||||
article.copy = articleCopy[articleName].paragraphs
|
||||
})
|
||||
|
||||
return articles
|
||||
}
|
||||
|
||||
function articleLinks (args) {
|
||||
const locales = args.data.root.req.supportedLocales
|
||||
const articleLinks = [
|
||||
{
|
||||
title: "Understand how hackers work",
|
||||
stringId: LocaleUtils.fluentFormat(locales, "how-hackers-work"),
|
||||
class: "how-hackers-work",
|
||||
pathToPartial: "svg/icon-hackers",
|
||||
subhead: LocaleUtils.fluentFormat(locales, "how-hackers-work-desc"),
|
||||
title: 'Understand how hackers work',
|
||||
stringId: LocaleUtils.fluentFormat(locales, 'how-hackers-work'),
|
||||
class: 'how-hackers-work',
|
||||
pathToPartial: 'svg/icon-hackers',
|
||||
subhead: LocaleUtils.fluentFormat(locales, 'how-hackers-work-desc')
|
||||
|
||||
},
|
||||
{
|
||||
title: "What to do after a data breach",
|
||||
stringId: LocaleUtils.fluentFormat(locales,"what-to-do-after-breach"),
|
||||
class: "after-breach",
|
||||
pathToPartial: "svg/icon-at",
|
||||
subhead: LocaleUtils.fluentFormat(locales,"what-to-do-after-breach-desc"),
|
||||
title: 'What to do after a data breach',
|
||||
stringId: LocaleUtils.fluentFormat(locales, 'what-to-do-after-breach'),
|
||||
class: 'after-breach',
|
||||
pathToPartial: 'svg/icon-at',
|
||||
subhead: LocaleUtils.fluentFormat(locales, 'what-to-do-after-breach-desc')
|
||||
},
|
||||
{
|
||||
title: "How to create strong passwords",
|
||||
stringId: LocaleUtils.fluentFormat(locales,"create-strong-passwords"),
|
||||
class: "strong-passwords",
|
||||
pathToPartial: "svg/icon-password",
|
||||
subhead: LocaleUtils.fluentFormat(locales,"create-strong-passwords-desc"),
|
||||
title: 'How to create strong passwords',
|
||||
stringId: LocaleUtils.fluentFormat(locales, 'create-strong-passwords'),
|
||||
class: 'strong-passwords',
|
||||
pathToPartial: 'svg/icon-password',
|
||||
subhead: LocaleUtils.fluentFormat(locales, 'create-strong-passwords-desc')
|
||||
},
|
||||
{
|
||||
title: "Steps to take to protect your identity online",
|
||||
stringId: LocaleUtils.fluentFormat(locales,"steps-to-protect"),
|
||||
class: "steps-to-protect",
|
||||
pathToPartial: "svg/icon-fingerprinters",
|
||||
subhead: LocaleUtils.fluentFormat(locales,"steps-to-protect-desc"),
|
||||
title: 'Steps to take to protect your identity online',
|
||||
stringId: LocaleUtils.fluentFormat(locales, 'steps-to-protect'),
|
||||
class: 'steps-to-protect',
|
||||
pathToPartial: 'svg/icon-fingerprinters',
|
||||
subhead: LocaleUtils.fluentFormat(locales, 'steps-to-protect-desc')
|
||||
},
|
||||
{
|
||||
title: "5 myths about password managers",
|
||||
stringId: LocaleUtils.fluentFormat(locales,"five-myths"),
|
||||
class: "five-myths",
|
||||
pathToPartial: "svg/icon-myths",
|
||||
subhead: LocaleUtils.fluentFormat(locales,"five-myths-desc"),
|
||||
title: '5 myths about password managers',
|
||||
stringId: LocaleUtils.fluentFormat(locales, 'five-myths'),
|
||||
class: 'five-myths',
|
||||
pathToPartial: 'svg/icon-myths',
|
||||
subhead: LocaleUtils.fluentFormat(locales, 'five-myths-desc')
|
||||
},
|
||||
{
|
||||
title: "Take further steps to protect your identity",
|
||||
stringId: LocaleUtils.fluentFormat(locales,"take-further-steps"),
|
||||
class: "next-steps",
|
||||
pathToPartial: "svg/icon-trackers",
|
||||
subhead: LocaleUtils.fluentFormat(locales, "take-further-steps-desc"),
|
||||
},
|
||||
];
|
||||
title: 'Take further steps to protect your identity',
|
||||
stringId: LocaleUtils.fluentFormat(locales, 'take-further-steps'),
|
||||
class: 'next-steps',
|
||||
pathToPartial: 'svg/icon-trackers',
|
||||
subhead: LocaleUtils.fluentFormat(locales, 'take-further-steps-desc')
|
||||
}
|
||||
]
|
||||
|
||||
return articleLinks;
|
||||
return articleLinks
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
articleLinks,
|
||||
getArticleCopy,
|
||||
getSecurityTipsIntro,
|
||||
};
|
||||
getSecurityTipsIntro
|
||||
}
|
||||
|
|
|
@ -1,168 +1,162 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const AppConstants = require("./../app-constants");
|
||||
const AppConstants = require('./../app-constants')
|
||||
|
||||
const { prettyDate, localize } = require("./hbs-helpers");
|
||||
const { getAllPriorityDataClasses, getAllGenericRecommendations, getFourthPasswordRecommendation } = require("./recommendations");
|
||||
const { getPromoStrings } = require("./product-promos");
|
||||
const { prettyDate, localize } = require('./hbs-helpers')
|
||||
const { getAllPriorityDataClasses, getAllGenericRecommendations, getFourthPasswordRecommendation } = require('./recommendations')
|
||||
const { getPromoStrings } = require('./product-promos')
|
||||
|
||||
|
||||
function addRecommendationUtmParams(cta) {
|
||||
function addRecommendationUtmParams (cta) {
|
||||
try {
|
||||
const url = new URL(cta.ctaHref);
|
||||
const url = new URL(cta.ctaHref)
|
||||
if (url.host.match(/mozilla\.org|firefox\.com/) === null) {
|
||||
return cta.ctaHref;
|
||||
return cta.ctaHref
|
||||
}
|
||||
const utmParams = {
|
||||
utm_source: "monitor.firefox.com",
|
||||
utm_medium: "referral",
|
||||
utm_source: 'monitor.firefox.com',
|
||||
utm_medium: 'referral',
|
||||
utm_content: cta.ctaAnalyticsId,
|
||||
utm_campaign: "contextual-recommendations",
|
||||
};
|
||||
utm_campaign: 'contextual-recommendations'
|
||||
}
|
||||
|
||||
for (const param in utmParams) {
|
||||
url.searchParams.append(param, utmParams[param]);
|
||||
url.searchParams.append(param, utmParams[param])
|
||||
}
|
||||
return url.href;
|
||||
}
|
||||
catch (e) {
|
||||
return cta.ctaHref;
|
||||
return url.href
|
||||
} catch (e) {
|
||||
return cta.ctaHref
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getBreachTitle(args) {
|
||||
return args.data.root.featuredBreach.Title;
|
||||
function getBreachTitle (args) {
|
||||
return args.data.root.featuredBreach.Title
|
||||
}
|
||||
|
||||
|
||||
function getVars(args) {
|
||||
const locales = args.data.root.req.supportedLocales;
|
||||
const breach = args.data.root.featuredBreach;
|
||||
const changePWLink = args.data.root.changePWLink;
|
||||
const isUserBrowserFirefox = (/Firefox/i.test(args.data.root.req.headers["user-agent"]));
|
||||
return { locales, breach, changePWLink, isUserBrowserFirefox };
|
||||
function getVars (args) {
|
||||
const locales = args.data.root.req.supportedLocales
|
||||
const breach = args.data.root.featuredBreach
|
||||
const changePWLink = args.data.root.changePWLink
|
||||
const isUserBrowserFirefox = (/Firefox/i.test(args.data.root.req.headers['user-agent']))
|
||||
return { locales, breach, changePWLink, isUserBrowserFirefox }
|
||||
}
|
||||
|
||||
|
||||
function getBreachCategory(breach) {
|
||||
if (["Exactis", "Apollo", "YouveBeenScraped", "ElasticsearchSalesLeads", "Estonia", "MasterDeeds", "PDL"].includes(breach.Name)) {
|
||||
return "data-aggregator-breach";
|
||||
function getBreachCategory (breach) {
|
||||
if (['Exactis', 'Apollo', 'YouveBeenScraped', 'ElasticsearchSalesLeads', 'Estonia', 'MasterDeeds', 'PDL'].includes(breach.Name)) {
|
||||
return 'data-aggregator-breach'
|
||||
}
|
||||
if (breach.IsSensitive) {
|
||||
return "sensitive-breach";
|
||||
return 'sensitive-breach'
|
||||
}
|
||||
if (breach.Domain !== "") {
|
||||
return "website-breach";
|
||||
if (breach.Domain !== '') {
|
||||
return 'website-breach'
|
||||
}
|
||||
return "data-aggregator-breach";
|
||||
return 'data-aggregator-breach'
|
||||
}
|
||||
|
||||
|
||||
function getSortedDataClasses(locales, breach, isUserBrowserFirefox=false, isUserLocaleEnUs=false, isUserLocalEn=false, changePWLink=false) {
|
||||
const priorityDataClasses = getAllPriorityDataClasses(isUserBrowserFirefox, isUserLocaleEnUs, changePWLink);
|
||||
function getSortedDataClasses (locales, breach, isUserBrowserFirefox = false, isUserLocaleEnUs = false, isUserLocalEn = false, changePWLink = false) {
|
||||
const priorityDataClasses = getAllPriorityDataClasses(isUserBrowserFirefox, isUserLocaleEnUs, changePWLink)
|
||||
|
||||
const sortedDataClasses = {
|
||||
priority: [],
|
||||
lowerPriority: [],
|
||||
};
|
||||
|
||||
const dataClasses = breach.DataClasses;
|
||||
dataClasses.forEach(dataClass => {
|
||||
const dataType = localize(locales, dataClass);
|
||||
if (priorityDataClasses[dataClass]) {
|
||||
priorityDataClasses[dataClass]["dataType"] = dataType;
|
||||
sortedDataClasses.priority.push(priorityDataClasses[dataClass]);
|
||||
return;
|
||||
}
|
||||
sortedDataClasses.lowerPriority.push(dataType);
|
||||
});
|
||||
sortedDataClasses.priority.sort((a,b) => { return b.weight - a.weight; });
|
||||
sortedDataClasses.lowerPriority = sortedDataClasses.lowerPriority.join(", ");
|
||||
return sortedDataClasses;
|
||||
}
|
||||
|
||||
function compareBreachDates(breach) {
|
||||
const breachDate = new Date(breach.BreachDate);
|
||||
const addedDate = new Date(breach.AddedDate);
|
||||
const timeDiff = Math.abs(breachDate.getTime() - addedDate.getTime());
|
||||
const dayDiff = Math.ceil(timeDiff / (1000 * 3600 * 24));
|
||||
if (dayDiff > 90) {
|
||||
return true;
|
||||
lowerPriority: []
|
||||
}
|
||||
return false;
|
||||
|
||||
const dataClasses = breach.DataClasses
|
||||
dataClasses.forEach(dataClass => {
|
||||
const dataType = localize(locales, dataClass)
|
||||
if (priorityDataClasses[dataClass]) {
|
||||
priorityDataClasses[dataClass].dataType = dataType
|
||||
sortedDataClasses.priority.push(priorityDataClasses[dataClass])
|
||||
return
|
||||
}
|
||||
sortedDataClasses.lowerPriority.push(dataType)
|
||||
})
|
||||
sortedDataClasses.priority.sort((a, b) => { return b.weight - a.weight })
|
||||
sortedDataClasses.lowerPriority = sortedDataClasses.lowerPriority.join(', ')
|
||||
return sortedDataClasses
|
||||
}
|
||||
|
||||
function getGenericFillerRecs(locales, numberOfRecsNeeded) {
|
||||
let genericRecommendations = getAllGenericRecommendations();
|
||||
function compareBreachDates (breach) {
|
||||
const breachDate = new Date(breach.BreachDate)
|
||||
const addedDate = new Date(breach.AddedDate)
|
||||
const timeDiff = Math.abs(breachDate.getTime() - addedDate.getTime())
|
||||
const dayDiff = Math.ceil(timeDiff / (1000 * 3600 * 24))
|
||||
if (dayDiff > 90) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function getGenericFillerRecs (locales, numberOfRecsNeeded) {
|
||||
let genericRecommendations = getAllGenericRecommendations()
|
||||
|
||||
genericRecommendations = genericRecommendations
|
||||
.slice(0, numberOfRecsNeeded); // Slice array down to number of needed recommendations
|
||||
.slice(0, numberOfRecsNeeded) // Slice array down to number of needed recommendations
|
||||
|
||||
genericRecommendations.forEach(rec => {
|
||||
for (const pieceOfCopy in rec.recommendationCopy) {
|
||||
rec.recommendationCopy[pieceOfCopy] = localize(locales, rec.recommendationCopy[pieceOfCopy]);
|
||||
rec.recommendationCopy[pieceOfCopy] = localize(locales, rec.recommendationCopy[pieceOfCopy])
|
||||
}
|
||||
});
|
||||
return genericRecommendations;
|
||||
})
|
||||
return genericRecommendations
|
||||
}
|
||||
|
||||
function getBreachDetail(args) {
|
||||
const acceptsLanguages = args.data.root.req.acceptsLanguages();
|
||||
const { locales, breach, changePWLink, isUserBrowserFirefox } = getVars(args);
|
||||
const { sortedDataClasses, recommendations } = getSortedDataClassesAndRecs(acceptsLanguages, locales, breach, isUserBrowserFirefox, changePWLink);
|
||||
const breachCategory = getBreachCategory(breach);
|
||||
const breachExposedPasswords = breach.DataClasses.includes("passwords");
|
||||
function getBreachDetail (args) {
|
||||
const acceptsLanguages = args.data.root.req.acceptsLanguages()
|
||||
const { locales, breach, changePWLink, isUserBrowserFirefox } = getVars(args)
|
||||
const { sortedDataClasses, recommendations } = getSortedDataClassesAndRecs(acceptsLanguages, locales, breach, isUserBrowserFirefox, changePWLink)
|
||||
const breachCategory = getBreachCategory(breach)
|
||||
const breachExposedPasswords = breach.DataClasses.includes('passwords')
|
||||
|
||||
breach.LogoUrl = `${AppConstants.LOGOS_ORIGIN}/img/logos/${breach.LogoPath}`;
|
||||
breach.LogoUrl = `${AppConstants.LOGOS_ORIGIN}/img/logos/${breach.LogoPath}`
|
||||
|
||||
const breachDetail = {
|
||||
breach: breach,
|
||||
breachExposedPasswords: breachExposedPasswords,
|
||||
breach,
|
||||
breachExposedPasswords,
|
||||
overview: {
|
||||
headline: localize(locales, "breach-overview-title"),
|
||||
copy: localize(locales, "breach-overview-new", {
|
||||
headline: localize(locales, 'breach-overview-title'),
|
||||
copy: localize(locales, 'breach-overview-new', {
|
||||
addedDate: `<span class='bold'>${prettyDate(breach.AddedDate, locales)}</span>`,
|
||||
breachDate: `<span class='bold'>${prettyDate(breach.BreachDate, locales)}</span>`,
|
||||
breachTitle: breach.Title,
|
||||
}),
|
||||
breachTitle: breach.Title
|
||||
})
|
||||
},
|
||||
|
||||
categoryId: breachCategory,
|
||||
category: localize(locales, breachCategory),
|
||||
changePWLink: changePWLink,
|
||||
changePWLink,
|
||||
|
||||
dataClasses: {
|
||||
headline: localize(locales, "what-data"),
|
||||
dataTypes: sortedDataClasses,
|
||||
headline: localize(locales, 'what-data'),
|
||||
dataTypes: sortedDataClasses
|
||||
},
|
||||
|
||||
recommendations: {
|
||||
headline: breachExposedPasswords ? localize(locales, "rec-section-headline") : localize(locales, "rec-section-headline-no-pw"),
|
||||
copy: breachExposedPasswords ? localize(locales, "rec-section-subhead") : localize(locales, "rec-section-subhead-no-pw"),
|
||||
recommendationsList: recommendations,
|
||||
},
|
||||
};
|
||||
headline: breachExposedPasswords ? localize(locales, 'rec-section-headline') : localize(locales, 'rec-section-headline-no-pw'),
|
||||
copy: breachExposedPasswords ? localize(locales, 'rec-section-subhead') : localize(locales, 'rec-section-subhead-no-pw'),
|
||||
recommendationsList: recommendations
|
||||
}
|
||||
}
|
||||
|
||||
// Add correct "What is a ... breach" copy.
|
||||
switch (breachDetail.categoryId) {
|
||||
case "data-aggregator-breach":
|
||||
case 'data-aggregator-breach':
|
||||
breachDetail.whatIsThisBreach = {
|
||||
headline: localize(locales, "what-is-data-agg"),
|
||||
copy: localize(locales, "what-is-data-agg-blurb"),
|
||||
};
|
||||
break;
|
||||
case "sensitive-breach":
|
||||
headline: localize(locales, 'what-is-data-agg'),
|
||||
copy: localize(locales, 'what-is-data-agg-blurb')
|
||||
}
|
||||
break
|
||||
case 'sensitive-breach':
|
||||
breachDetail.whatIsThisBreach = {
|
||||
headline: localize(locales, "sensitive-sites"),
|
||||
copy: localize(locales, "sensitive-sites-copy"),
|
||||
};
|
||||
break;
|
||||
headline: localize(locales, 'sensitive-sites'),
|
||||
copy: localize(locales, 'sensitive-sites-copy')
|
||||
}
|
||||
break
|
||||
default:
|
||||
breachDetail.whatIsThisBreach = {
|
||||
headline: localize(locales, "what-is-a-website-breach"),
|
||||
copy: localize(locales, "website-breach-blurb"),
|
||||
};
|
||||
headline: localize(locales, 'what-is-a-website-breach'),
|
||||
copy: localize(locales, 'website-breach-blurb')
|
||||
}
|
||||
}
|
||||
|
||||
// Compare the breach date to the breach added date
|
||||
|
@ -170,94 +164,93 @@ function getBreachDetail(args) {
|
|||
// message if necessary.
|
||||
if (compareBreachDates(breach)) {
|
||||
breachDetail.delayedReporting = {
|
||||
headline: localize(locales, "delayed-reporting-headline"),
|
||||
copy: localize(locales, "delayed-reporting-copy"),
|
||||
};
|
||||
headline: localize(locales, 'delayed-reporting-headline'),
|
||||
copy: localize(locales, 'delayed-reporting-copy')
|
||||
}
|
||||
}
|
||||
|
||||
// Determine which product promo to show
|
||||
breachDetail.promo = getPromoStrings(args);
|
||||
breachDetail.promo = getPromoStrings(args)
|
||||
|
||||
const BREACH_RESOLUTION_ENABLED = (AppConstants.BREACH_RESOLUTION_ENABLED === "1");
|
||||
const BREACH_RESOLUTION_ENABLED = (AppConstants.BREACH_RESOLUTION_ENABLED === '1')
|
||||
if (BREACH_RESOLUTION_ENABLED && args.data.root.affectedEmails) {
|
||||
const affectedEmails = args.data.root.affectedEmails;
|
||||
const numAffectedEmails = affectedEmails.length;
|
||||
const unresolvedAffectedEmails = [];
|
||||
const affectedEmails = args.data.root.affectedEmails
|
||||
const numAffectedEmails = affectedEmails.length
|
||||
const unresolvedAffectedEmails = []
|
||||
|
||||
if (numAffectedEmails > 0) {
|
||||
affectedEmails.forEach(email => {
|
||||
if (!email.isResolved) {
|
||||
unresolvedAffectedEmails.push(email);
|
||||
unresolvedAffectedEmails.push(email)
|
||||
}
|
||||
});
|
||||
// show top of page alert for any emails involved in this breach where the breach
|
||||
// has not yet been marked as resolved.
|
||||
// if all breaches have been resolved, show nothing
|
||||
if (unresolvedAffectedEmails.length > 0) {
|
||||
const affectedEmailNotification = unresolvedAffectedEmails.length > 1 ?
|
||||
localize(locales, "resolve-top-notification-plural", { numAffectedEmails: numAffectedEmails }) :
|
||||
localize(locales, "resolve-top-notification", { affectedEmail: unresolvedAffectedEmails[0].emailAddress });
|
||||
})
|
||||
// show top of page alert for any emails involved in this breach where the breach
|
||||
// has not yet been marked as resolved.
|
||||
// if all breaches have been resolved, show nothing
|
||||
if (unresolvedAffectedEmails.length > 0) {
|
||||
const affectedEmailNotification = unresolvedAffectedEmails.length > 1
|
||||
? localize(locales, 'resolve-top-notification-plural', { numAffectedEmails })
|
||||
: localize(locales, 'resolve-top-notification', { affectedEmail: unresolvedAffectedEmails[0].emailAddress })
|
||||
|
||||
breachDetail.affectedEmailNotification = formatNotificationLink(affectedEmailNotification);
|
||||
}
|
||||
breachDetail.affectedEmails = affectedEmails;
|
||||
breachDetail.affectedEmailNotification = formatNotificationLink(affectedEmailNotification)
|
||||
}
|
||||
breachDetail.affectedEmails = affectedEmails
|
||||
breachDetail.resolutionStrings = {
|
||||
subhead: localize(locales, "marking-this-subhead"),
|
||||
message: formatResolutionMessage(localize(locales, "marking-this-body")),
|
||||
resolveButtonTitle: localize(locales, "mark-as-resolve-button"),
|
||||
resolvedLabel: localize(locales, "marked-as-resolved-label"),
|
||||
undoResolvedLabel: localize(locales, "undo-button"),
|
||||
};
|
||||
subhead: localize(locales, 'marking-this-subhead'),
|
||||
message: formatResolutionMessage(localize(locales, 'marking-this-body')),
|
||||
resolveButtonTitle: localize(locales, 'mark-as-resolve-button'),
|
||||
resolvedLabel: localize(locales, 'marked-as-resolved-label'),
|
||||
undoResolvedLabel: localize(locales, 'undo-button')
|
||||
}
|
||||
}
|
||||
}
|
||||
return args.fn(breachDetail);
|
||||
return args.fn(breachDetail)
|
||||
}
|
||||
|
||||
function formatResolutionMessage(message) {
|
||||
return message.replace("<span>", "<span class='demi'>");
|
||||
function formatResolutionMessage (message) {
|
||||
return message.replace('<span>', "<span class='demi'>")
|
||||
}
|
||||
|
||||
function formatNotificationLink(message) {
|
||||
return message.replace("<a>", "<a class='what-to-do-next blue-link' href='#what-to-do-next' data-analytics-label='what-to-do-next'>");
|
||||
function formatNotificationLink (message) {
|
||||
return message.replace('<a>', "<a class='what-to-do-next blue-link' href='#what-to-do-next' data-analytics-label='what-to-do-next'>")
|
||||
}
|
||||
|
||||
function getSortedDataClassesAndRecs (acceptsLanguages, locales, breach, isUserBrowserFirefox = false, changePWLink = false) {
|
||||
const isUserLocaleEn = (acceptsLanguages[0].toLowerCase().startsWith('en'))
|
||||
const isUserLocaleEnUs = (acceptsLanguages[0].toLowerCase() === 'en-us')
|
||||
const sortedDataClasses = getSortedDataClasses(locales, breach, isUserBrowserFirefox, isUserLocaleEnUs, isUserLocaleEn, changePWLink)
|
||||
|
||||
function getSortedDataClassesAndRecs(acceptsLanguages, locales, breach, isUserBrowserFirefox=false, changePWLink=false) {
|
||||
const isUserLocaleEn = (acceptsLanguages[0].toLowerCase().startsWith("en"));
|
||||
const isUserLocaleEnUs = (acceptsLanguages[0].toLowerCase() === "en-us");
|
||||
const sortedDataClasses = getSortedDataClasses(locales, breach, isUserBrowserFirefox, isUserLocaleEnUs, isUserLocaleEn, changePWLink);
|
||||
|
||||
let recommendations = [];
|
||||
let recommendations = []
|
||||
|
||||
// Check each priority data class for a recommendation
|
||||
// and push localized recommendations into new array.
|
||||
sortedDataClasses.priority.forEach(dataClass => {
|
||||
if (dataClass.recommendations) {
|
||||
const recs = dataClass.recommendations;
|
||||
const recs = dataClass.recommendations
|
||||
recs.forEach(rec => {
|
||||
for (const pieceOfCopy in rec.recommendationCopy) {
|
||||
rec.recommendationCopy[pieceOfCopy] = localize(locales, rec.recommendationCopy[pieceOfCopy]);
|
||||
rec.recommendationCopy[pieceOfCopy] = localize(locales, rec.recommendationCopy[pieceOfCopy])
|
||||
}
|
||||
recommendations.push(rec);
|
||||
});
|
||||
recommendations.push(rec)
|
||||
})
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
// If the breach exposed passwords, push the fourth password recommendation
|
||||
// to the end of the recommendations list regardless of list length.
|
||||
if (breach.DataClasses.includes("passwords")) {
|
||||
recommendations.push(getFourthPasswordRecommendation(locales));
|
||||
if (breach.DataClasses.includes('passwords')) {
|
||||
recommendations.push(getFourthPasswordRecommendation(locales))
|
||||
}
|
||||
|
||||
// If there are fewer than four recommendations,
|
||||
// backfill with generic recommendations.
|
||||
const minimumNumberOfRecs = 4;
|
||||
const minimumNumberOfRecs = 4
|
||||
if (recommendations.length < minimumNumberOfRecs) {
|
||||
const numberOfRecsNeeded = minimumNumberOfRecs - recommendations.length;
|
||||
const genericRecs = getGenericFillerRecs(locales, numberOfRecsNeeded);
|
||||
recommendations = recommendations.concat(genericRecs);
|
||||
const numberOfRecsNeeded = minimumNumberOfRecs - recommendations.length
|
||||
const genericRecs = getGenericFillerRecs(locales, numberOfRecsNeeded)
|
||||
recommendations = recommendations.concat(genericRecs)
|
||||
}
|
||||
return {sortedDataClasses, recommendations};
|
||||
return { sortedDataClasses, recommendations }
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
@ -265,5 +258,5 @@ module.exports = {
|
|||
getBreachDetail,
|
||||
getBreachCategory,
|
||||
getSortedDataClasses,
|
||||
getBreachTitle,
|
||||
};
|
||||
getBreachTitle
|
||||
}
|
||||
|
|
|
@ -1,115 +1,112 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const AppConstants = require("../app-constants");
|
||||
const { resultsSummary } = require("../scan-results");
|
||||
const { localize } = require("./hbs-helpers");
|
||||
const AppConstants = require('../app-constants')
|
||||
const { resultsSummary } = require('../scan-results')
|
||||
const { localize } = require('./hbs-helpers')
|
||||
|
||||
|
||||
function getBreachStats(args) {
|
||||
const verifiedEmails = args.data.root.verifiedEmails;
|
||||
const locales = args.data.root.req.supportedLocales;
|
||||
function getBreachStats (args) {
|
||||
const verifiedEmails = args.data.root.verifiedEmails
|
||||
const locales = args.data.root.req.supportedLocales
|
||||
|
||||
const userBreachStats = {
|
||||
breachStats: resultsSummary(verifiedEmails),
|
||||
progressBar: "",
|
||||
progressIntro: "",
|
||||
};
|
||||
progressBar: '',
|
||||
progressIntro: ''
|
||||
}
|
||||
|
||||
const breachStatBundle = userBreachStats.breachStats;
|
||||
const totalEmailsStat = breachStatBundle.monitoredEmails;
|
||||
const breachStatBundle = userBreachStats.breachStats
|
||||
const totalEmailsStat = breachStatBundle.monitoredEmails
|
||||
// Format "00 emails being monitored" callout
|
||||
totalEmailsStat.subhead = localize(locales, "email-addresses-being-monitored", { emails: verifiedEmails.length });
|
||||
totalEmailsStat.displayCount = breachStatBundle.monitoredEmails.count;
|
||||
totalEmailsStat.subhead = localize(locales, 'email-addresses-being-monitored', { emails: verifiedEmails.length })
|
||||
totalEmailsStat.displayCount = breachStatBundle.monitoredEmails.count
|
||||
|
||||
const breachesStat = breachStatBundle.numBreaches;
|
||||
const passwordStat = breachStatBundle.passwords;
|
||||
const breachesStat = breachStatBundle.numBreaches
|
||||
const passwordStat = breachStatBundle.passwords
|
||||
|
||||
if (breachesStat.numResolved > 0) {
|
||||
// If a user has resolved at least one breach:
|
||||
// Change the password stat to show the number of password-exposing unresolved breaches.
|
||||
const remainingExposedPasswords = passwordStat.count - passwordStat.numResolved;
|
||||
passwordStat.subhead = localize(locales, "unresolved-passwords-exposed", { numPasswords: remainingExposedPasswords });
|
||||
passwordStat.displayCount = remainingExposedPasswords;
|
||||
const remainingExposedPasswords = passwordStat.count - passwordStat.numResolved
|
||||
passwordStat.subhead = localize(locales, 'unresolved-passwords-exposed', { numPasswords: remainingExposedPasswords })
|
||||
passwordStat.displayCount = remainingExposedPasswords
|
||||
|
||||
// Change the total number of breaches callout to show the total number of resolved breaches
|
||||
breachesStat.subhead = localize(locales, "known-data-breaches-resolved", { numResolvedBreaches: breachesStat.numResolved });
|
||||
breachesStat.displayCount = breachesStat.numResolved;
|
||||
breachesStat.subhead = localize(locales, 'known-data-breaches-resolved', { numResolvedBreaches: breachesStat.numResolved })
|
||||
breachesStat.displayCount = breachesStat.numResolved
|
||||
} else {
|
||||
passwordStat.subhead = localize(locales, 'passwords-exposed', { passwords: passwordStat.count })
|
||||
passwordStat.displayCount = passwordStat.count
|
||||
|
||||
passwordStat.subhead = localize(locales, "passwords-exposed", { passwords: passwordStat.count });
|
||||
passwordStat.displayCount = passwordStat.count;
|
||||
|
||||
breachesStat.subhead = localize(locales, "known-data-breaches-exposed", { breaches: breachesStat.count });
|
||||
breachesStat.displayCount = breachesStat.count;
|
||||
breachesStat.subhead = localize(locales, 'known-data-breaches-exposed', { breaches: breachesStat.count })
|
||||
breachesStat.displayCount = breachesStat.count
|
||||
}
|
||||
|
||||
// add progress bar strings
|
||||
if (AppConstants.BREACH_RESOLUTION_ENABLED === "1") {
|
||||
userBreachStats.progressBar = makeProgressBar(breachesStat, locales);
|
||||
if (AppConstants.BREACH_RESOLUTION_ENABLED === '1') {
|
||||
userBreachStats.progressBar = makeProgressBar(breachesStat, locales)
|
||||
}
|
||||
return args.fn(userBreachStats);
|
||||
return args.fn(userBreachStats)
|
||||
}
|
||||
|
||||
function formatProgressMessage(message) {
|
||||
return message.replace("<span>", "<span class='demi'>");
|
||||
function formatProgressMessage (message) {
|
||||
return message.replace('<span>', "<span class='demi'>")
|
||||
}
|
||||
|
||||
function getProgressMessage(locales, percentBreachesResolved) {
|
||||
function getProgressMessage (locales, percentBreachesResolved) {
|
||||
if (percentBreachesResolved <= 25) {
|
||||
return formatProgressMessage(localize(locales, "progress-message-1"));
|
||||
return formatProgressMessage(localize(locales, 'progress-message-1'))
|
||||
}
|
||||
if (percentBreachesResolved <= 50) {
|
||||
return formatProgressMessage(localize(locales, "progress-message-2"));
|
||||
return formatProgressMessage(localize(locales, 'progress-message-2'))
|
||||
}
|
||||
if (percentBreachesResolved <= 75) {
|
||||
return formatProgressMessage(localize(locales, "progress-message-3"));
|
||||
return formatProgressMessage(localize(locales, 'progress-message-3'))
|
||||
}
|
||||
return formatProgressMessage(localize(locales, "progress-message-4"));
|
||||
return formatProgressMessage(localize(locales, 'progress-message-4'))
|
||||
}
|
||||
|
||||
|
||||
function makeProgressBar(userBreachTotals, locales) {
|
||||
const numTotalBreaches = userBreachTotals.count;
|
||||
const numResolvedBreaches = userBreachTotals.numResolved;
|
||||
function makeProgressBar (userBreachTotals, locales) {
|
||||
const numTotalBreaches = userBreachTotals.count
|
||||
const numResolvedBreaches = userBreachTotals.numResolved
|
||||
|
||||
// Show nothing if there are no found breaches for any email
|
||||
if (numTotalBreaches === 0) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
// Show an introductory message about resolving breaches if the user
|
||||
// has found breaches, but hasn't resolved any of them yet.
|
||||
if (numResolvedBreaches === 0) {
|
||||
return {
|
||||
subhead: localize(locales, "progress-intro-subhead"),
|
||||
progressMessage: localize(locales, "progress-intro-message"),
|
||||
imageClassName: "breach-resolution-intro",
|
||||
};
|
||||
subhead: localize(locales, 'progress-intro-subhead'),
|
||||
progressMessage: localize(locales, 'progress-intro-message'),
|
||||
imageClassName: 'breach-resolution-intro'
|
||||
}
|
||||
}
|
||||
|
||||
let percentBreachesResolved = Math.floor(numResolvedBreaches / numTotalBreaches * 100);
|
||||
percentBreachesResolved = percentBreachesResolved < 1 ? 1 : percentBreachesResolved;
|
||||
let percentBreachesResolved = Math.floor(numResolvedBreaches / numTotalBreaches * 100)
|
||||
percentBreachesResolved = percentBreachesResolved < 1 ? 1 : percentBreachesResolved
|
||||
if (percentBreachesResolved === 100) {
|
||||
return {
|
||||
subhead: localize(locales, "progress-complete"),
|
||||
progressMessage: formatProgressMessage(localize(locales, "progress-complete-message")),
|
||||
imageClassName: "breach-resolution-complete",
|
||||
};
|
||||
subhead: localize(locales, 'progress-complete'),
|
||||
progressMessage: formatProgressMessage(localize(locales, 'progress-complete-message')),
|
||||
imageClassName: 'breach-resolution-complete'
|
||||
}
|
||||
}
|
||||
|
||||
// Show the progress bar if a user has resolved at least one breach
|
||||
// and has others left to resolve.
|
||||
return {
|
||||
progressStatus: localize(locales, "progress-status", {
|
||||
"numResolvedBreaches": numResolvedBreaches,
|
||||
"numTotalBreaches": numTotalBreaches,
|
||||
progressStatus: localize(locales, 'progress-status', {
|
||||
numResolvedBreaches,
|
||||
numTotalBreaches
|
||||
}),
|
||||
percentComplete: localize(locales, "progress-percent-complete", { "percentComplete": percentBreachesResolved}),
|
||||
percentComplete: localize(locales, 'progress-percent-complete', { percentComplete: percentBreachesResolved }),
|
||||
progressMessage: getProgressMessage(locales, percentBreachesResolved),
|
||||
percentBreachesResolved: percentBreachesResolved,
|
||||
};
|
||||
percentBreachesResolved
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getBreachStats,
|
||||
};
|
||||
getBreachStats
|
||||
}
|
||||
|
|
|
@ -1,142 +1,141 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const AppConstants = require("./../app-constants");
|
||||
const { getSortedDataClasses } = require("./breach-detail");
|
||||
const { prettyDate, localeString, localizedBreachDataClasses } = require("./hbs-helpers");
|
||||
const { LocaleUtils } = require("./../locale-utils");
|
||||
const { filterBreaches } = require("./../hibp");
|
||||
const AppConstants = require('./../app-constants')
|
||||
const { getSortedDataClasses } = require('./breach-detail')
|
||||
const { prettyDate, localeString, localizedBreachDataClasses } = require('./hbs-helpers')
|
||||
const { LocaleUtils } = require('./../locale-utils')
|
||||
const { filterBreaches } = require('./../hibp')
|
||||
|
||||
function getLocalizedBreachCardStrings(locales) {
|
||||
function getLocalizedBreachCardStrings (locales) {
|
||||
return {
|
||||
BreachAdded : LocaleUtils.fluentFormat(locales, "breach-added-label"),
|
||||
CompromisedAccounts: LocaleUtils.fluentFormat(locales, "compromised-accounts"),
|
||||
CompromisedData: LocaleUtils.fluentFormat(locales, "compromised-data"),
|
||||
LatestBreachLink: LocaleUtils.fluentFormat(locales, "latest-breach-link"),
|
||||
MoreInfoLink: LocaleUtils.fluentFormat(locales, "more-about-this-breach"),
|
||||
ResolveThisBreachLink: LocaleUtils.fluentFormat(locales, "resolve-this-breach-link"),
|
||||
};
|
||||
BreachAdded: LocaleUtils.fluentFormat(locales, 'breach-added-label'),
|
||||
CompromisedAccounts: LocaleUtils.fluentFormat(locales, 'compromised-accounts'),
|
||||
CompromisedData: LocaleUtils.fluentFormat(locales, 'compromised-data'),
|
||||
LatestBreachLink: LocaleUtils.fluentFormat(locales, 'latest-breach-link'),
|
||||
MoreInfoLink: LocaleUtils.fluentFormat(locales, 'more-about-this-breach'),
|
||||
ResolveThisBreachLink: LocaleUtils.fluentFormat(locales, 'resolve-this-breach-link')
|
||||
}
|
||||
}
|
||||
|
||||
function dataClassesforCards(breach, locales) {
|
||||
const topTwoClasses = [];
|
||||
const dataClasses = getSortedDataClasses(locales, breach);
|
||||
function dataClassesforCards (breach, locales) {
|
||||
const topTwoClasses = []
|
||||
const dataClasses = getSortedDataClasses(locales, breach)
|
||||
|
||||
dataClasses.priority.forEach(dataType => {
|
||||
topTwoClasses.push(dataType.dataType);
|
||||
});
|
||||
topTwoClasses.push(dataType.dataType)
|
||||
})
|
||||
|
||||
if (topTwoClasses.length >= 2) {
|
||||
return localizedBreachDataClasses(topTwoClasses.slice(0, 2), locales);
|
||||
return localizedBreachDataClasses(topTwoClasses.slice(0, 2), locales)
|
||||
}
|
||||
|
||||
topTwoClasses.concat(dataClasses.lowerPriority);
|
||||
return localizedBreachDataClasses(topTwoClasses.slice(0, 2), locales);
|
||||
topTwoClasses.concat(dataClasses.lowerPriority)
|
||||
return localizedBreachDataClasses(topTwoClasses.slice(0, 2), locales)
|
||||
}
|
||||
|
||||
function sortBreaches(breaches) {
|
||||
breaches = breaches.sort((a,b) => {
|
||||
const oldestBreach = new Date(a.AddedDate);
|
||||
const newestBreach = new Date(b.AddedDate);
|
||||
return newestBreach-oldestBreach;
|
||||
});
|
||||
return breaches;
|
||||
function sortBreaches (breaches) {
|
||||
breaches = breaches.sort((a, b) => {
|
||||
const oldestBreach = new Date(a.AddedDate)
|
||||
const newestBreach = new Date(b.AddedDate)
|
||||
return newestBreach - oldestBreach
|
||||
})
|
||||
return breaches
|
||||
}
|
||||
|
||||
function makeBreachCards(breaches, locales) {
|
||||
const formattedBreaches = [];
|
||||
const breachCardStrings = getLocalizedBreachCardStrings(locales);
|
||||
breaches = JSON.parse(JSON.stringify(breaches));
|
||||
function makeBreachCards (breaches, locales) {
|
||||
const formattedBreaches = []
|
||||
const breachCardStrings = getLocalizedBreachCardStrings(locales)
|
||||
breaches = JSON.parse(JSON.stringify(breaches))
|
||||
|
||||
for (const breachCard of breaches) {
|
||||
getLocalizedBreachValues(locales, breachCard);
|
||||
breachCard.LocalizedBreachCardStrings = breachCardStrings; // "Compromised Data: , Compromised Accounts: ..."
|
||||
breachCard.LogoUrl = `${AppConstants.LOGOS_ORIGIN}/img/logos/${breachCard.LogoPath}`;
|
||||
formattedBreaches.push(breachCard);
|
||||
getLocalizedBreachValues(locales, breachCard)
|
||||
breachCard.LocalizedBreachCardStrings = breachCardStrings // "Compromised Data: , Compromised Accounts: ..."
|
||||
breachCard.LogoUrl = `${AppConstants.LOGOS_ORIGIN}/img/logos/${breachCard.LogoPath}`
|
||||
formattedBreaches.push(breachCard)
|
||||
}
|
||||
return formattedBreaches;
|
||||
return formattedBreaches
|
||||
}
|
||||
|
||||
function lastAddedBreach(options) {
|
||||
const locales = options.data.root.req.supportedLocales;
|
||||
let latestBreach = [options.data.root.latestBreach];
|
||||
latestBreach = makeBreachCards(latestBreach, locales);
|
||||
return latestBreach;
|
||||
function lastAddedBreach (options) {
|
||||
const locales = options.data.root.req.supportedLocales
|
||||
let latestBreach = [options.data.root.latestBreach]
|
||||
latestBreach = makeBreachCards(latestBreach, locales)
|
||||
return latestBreach
|
||||
}
|
||||
|
||||
function getFoundBreaches(args) {
|
||||
const locales = args.data.root.req.supportedLocales;
|
||||
let userBreaches = args.data.root.foundBreaches;
|
||||
userBreaches = makeBreachCards(userBreaches, locales);
|
||||
userBreaches.cardType = "two-up drop-shadow";
|
||||
return userBreaches;
|
||||
function getFoundBreaches (args) {
|
||||
const locales = args.data.root.req.supportedLocales
|
||||
let userBreaches = args.data.root.foundBreaches
|
||||
userBreaches = makeBreachCards(userBreaches, locales)
|
||||
userBreaches.cardType = 'two-up drop-shadow'
|
||||
return userBreaches
|
||||
}
|
||||
|
||||
function getLocalizedBreachValues(locales, breach) {
|
||||
breach.AddedDate = prettyDate(breach.AddedDate, locales);
|
||||
breach.PwnCount = localeString(breach.PwnCount,locales);
|
||||
breach.DataClasses = dataClassesforCards(breach, locales);
|
||||
return breach;
|
||||
function getLocalizedBreachValues (locales, breach) {
|
||||
breach.AddedDate = prettyDate(breach.AddedDate, locales)
|
||||
breach.PwnCount = localeString(breach.PwnCount, locales)
|
||||
breach.DataClasses = dataClassesforCards(breach, locales)
|
||||
return breach
|
||||
}
|
||||
|
||||
function getBreachArray(args) {
|
||||
const locales = args.data.root.req.supportedLocales;
|
||||
function getBreachArray (args) {
|
||||
const locales = args.data.root.req.supportedLocales
|
||||
|
||||
let breaches = args.data.root.req.app.locals.breaches;
|
||||
breaches = JSON.parse(JSON.stringify(breaches));
|
||||
let breaches = args.data.root.req.app.locals.breaches
|
||||
breaches = JSON.parse(JSON.stringify(breaches))
|
||||
// should we consider filtering the breaches when the app loads
|
||||
// since we aren't ever showing them now anyway?
|
||||
breaches = filterBreaches(breaches);
|
||||
breaches = sortBreaches(breaches);
|
||||
breaches = breaches.filter(breach => !breach.IsSensitive);
|
||||
breaches = filterBreaches(breaches)
|
||||
breaches = sortBreaches(breaches)
|
||||
breaches = breaches.filter(breach => !breach.IsSensitive)
|
||||
breaches.forEach(breach => {
|
||||
getLocalizedBreachValues(locales, breach);
|
||||
delete(breach.Description);
|
||||
delete(breach.IsVerified);
|
||||
delete(breach.ModifiedDate);
|
||||
delete(breach.IsFabricated);
|
||||
delete(breach.Domain);
|
||||
delete(breach.IsRetired);
|
||||
delete(breach.IsSensitive);
|
||||
delete(breach.IsSpamList);
|
||||
delete(breach.BreachDate);
|
||||
});
|
||||
getLocalizedBreachValues(locales, breach)
|
||||
delete (breach.Description)
|
||||
delete (breach.IsVerified)
|
||||
delete (breach.ModifiedDate)
|
||||
delete (breach.IsFabricated)
|
||||
delete (breach.Domain)
|
||||
delete (breach.IsRetired)
|
||||
delete (breach.IsSensitive)
|
||||
delete (breach.IsSpamList)
|
||||
delete (breach.BreachDate)
|
||||
})
|
||||
|
||||
const allBreaches = {
|
||||
LocalizedBreachCardStrings: getLocalizedBreachCardStrings(locales),
|
||||
breaches: breaches,
|
||||
};
|
||||
return JSON.stringify(allBreaches);
|
||||
breaches
|
||||
}
|
||||
return JSON.stringify(allBreaches)
|
||||
}
|
||||
|
||||
function getBreachCardCta(breach, args) {
|
||||
const BREACH_RESOLUTION_ENABLED = (AppConstants.BREACH_RESOLUTION_ENABLED === "1");
|
||||
const templateData = args.data.root;
|
||||
function getBreachCardCta (breach, args) {
|
||||
const BREACH_RESOLUTION_ENABLED = (AppConstants.BREACH_RESOLUTION_ENABLED === '1')
|
||||
const templateData = args.data.root
|
||||
|
||||
if (breach.latestBreach) {
|
||||
return args.fn({
|
||||
ctaTitle: breach.LocalizedBreachCardStrings.LatestBreachLink,
|
||||
ctaAnalyticsLabel: "Latest Breach: See if you were in this breach",
|
||||
});
|
||||
ctaAnalyticsLabel: 'Latest Breach: See if you were in this breach'
|
||||
})
|
||||
}
|
||||
|
||||
if (BREACH_RESOLUTION_ENABLED && templateData.whichPartial === "dashboards/breaches-dash" && !breach.IsResolved) {
|
||||
return args.fn({
|
||||
ctaTitle: breach.LocalizedBreachCardStrings.ResolveThisBreachLink,
|
||||
ctaAnalyticsLabel: "Breach Card: Resolve this breach",
|
||||
});
|
||||
if (BREACH_RESOLUTION_ENABLED && templateData.whichPartial === 'dashboards/breaches-dash' && !breach.IsResolved) {
|
||||
return args.fn({
|
||||
ctaTitle: breach.LocalizedBreachCardStrings.ResolveThisBreachLink,
|
||||
ctaAnalyticsLabel: 'Breach Card: Resolve this breach'
|
||||
})
|
||||
}
|
||||
|
||||
return args.fn({
|
||||
ctaTitle: breach.LocalizedBreachCardStrings.MoreInfoLink,
|
||||
ctaAnalyticsLabel: "Breach Card: More about this breach",
|
||||
});
|
||||
ctaAnalyticsLabel: 'Breach Card: More about this breach'
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
lastAddedBreach,
|
||||
getFoundBreaches,
|
||||
makeBreachCards,
|
||||
getBreachArray,
|
||||
getBreachCardCta,
|
||||
};
|
||||
getBreachCardCta
|
||||
}
|
||||
|
|
|
@ -1,183 +1,181 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const { LocaleUtils } = require("./../locale-utils");
|
||||
const { makeBreachCards } = require("./breaches");
|
||||
const { hasUserSignedUpForRelay } = require("./../controllers/utils");
|
||||
const { LocaleUtils } = require('./../locale-utils')
|
||||
const { makeBreachCards } = require('./breaches')
|
||||
const { hasUserSignedUpForRelay } = require('./../controllers/utils')
|
||||
|
||||
function enLocaleIsSupported(args) {
|
||||
return args.data.root.req.headers["accept-language"].includes("en");
|
||||
function enLocaleIsSupported (args) {
|
||||
return args.data.root.req.headers['accept-language'].includes('en')
|
||||
}
|
||||
|
||||
function userIsOnRelayWaitList(args) {
|
||||
return hasUserSignedUpForRelay(args.data.root.req.user);
|
||||
function userIsOnRelayWaitList (args) {
|
||||
return hasUserSignedUpForRelay(args.data.root.req.user)
|
||||
}
|
||||
|
||||
function getBreachesDashboard(args) {
|
||||
const verifiedEmails = args.data.root.verifiedEmails;
|
||||
const locales = args.data.root.req.supportedLocales;
|
||||
let breachesFound = false;
|
||||
function getBreachesDashboard (args) {
|
||||
const verifiedEmails = args.data.root.verifiedEmails
|
||||
const locales = args.data.root.req.supportedLocales
|
||||
let breachesFound = false
|
||||
|
||||
// move emails with 0 breaches to the bottom of the page
|
||||
verifiedEmails.sort((a, b) => {
|
||||
if (
|
||||
a.breaches.length === 0 && b.breaches.length > 0 ||
|
||||
a.breaches.length === 0 && b.breaches.length > 0 ||
|
||||
b.breaches.length === 0 && a.breaches.length > 0
|
||||
) {
|
||||
return b.breaches.length - a.breaches.length;
|
||||
) {
|
||||
return b.breaches.length - a.breaches.length
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
return 0
|
||||
})
|
||||
|
||||
verifiedEmails.forEach(email => {
|
||||
const breachCards = makeBreachCards(email.breaches, locales);
|
||||
const breachCards = makeBreachCards(email.breaches, locales)
|
||||
|
||||
if (!breachesFound && breachCards.length > 0) {
|
||||
breachesFound = true;
|
||||
breachesFound = true
|
||||
}
|
||||
|
||||
email.numBreaches = breachCards.length;
|
||||
email.numResolvedBreaches = 0;
|
||||
email.numUnresolvedBreaches = 0;
|
||||
email.numBreaches = breachCards.length
|
||||
email.numResolvedBreaches = 0
|
||||
email.numUnresolvedBreaches = 0
|
||||
|
||||
// Get the number of resolved breaches for email
|
||||
email.breaches.forEach(breach => {
|
||||
if (breach.IsResolved) {
|
||||
email.numResolvedBreaches++;
|
||||
email.numResolvedBreaches++
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
// Move resolved breaches to the end of breach list
|
||||
if (email.numResolvedBreaches > 0) {
|
||||
breachCards.sort((a,b) => {
|
||||
breachCards.sort((a, b) => {
|
||||
if (a.IsResolved && !b.IsResolved) {
|
||||
return 1;
|
||||
return 1
|
||||
}
|
||||
if (!a.IsResolved && b.IsResolved) {
|
||||
return -1;
|
||||
return -1
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
delete email.breaches;
|
||||
email.numUnresolvedBreaches = email.numBreaches - email.numResolvedBreaches;
|
||||
email.foundBreaches = {};
|
||||
delete email.breaches
|
||||
email.numUnresolvedBreaches = email.numBreaches - email.numResolvedBreaches
|
||||
email.foundBreaches = {}
|
||||
|
||||
// If there are more than four unresolved breaches, show only the first four by default.
|
||||
if (email.numUnresolvedBreaches > 4) {
|
||||
email.foundBreaches.breachesShownByDefault = breachCards.slice(0, 4);
|
||||
email.foundBreaches.remainingBreaches = breachCards.slice(4, breachCards.length);
|
||||
email.foundBreaches.breachesShownByDefault = breachCards.slice(0, 4)
|
||||
email.foundBreaches.remainingBreaches = breachCards.slice(4, breachCards.length)
|
||||
} else {
|
||||
email.foundBreaches.breachesShownByDefault = breachCards;
|
||||
email.foundBreaches.breachesShownByDefault = breachCards
|
||||
}
|
||||
});
|
||||
})
|
||||
const emailCards = {
|
||||
verifiedEmails: verifiedEmails,
|
||||
breachesFound: breachesFound,
|
||||
};
|
||||
|
||||
return args.fn(emailCards);
|
||||
}
|
||||
|
||||
function welcomeMessage(args) {
|
||||
const locales = args.data.root.req.supportedLocales;
|
||||
const userEmail = args.data.root.req.session.user.fxa_profile_json.email;
|
||||
const newUser = args.data.root.req.session.newUser;
|
||||
|
||||
if (newUser) {
|
||||
return LocaleUtils.fluentFormat(locales, "welcome-user", { userName: userEmail });
|
||||
verifiedEmails,
|
||||
breachesFound
|
||||
}
|
||||
|
||||
return LocaleUtils.fluentFormat(locales, "welcome-back", { userName: userEmail});
|
||||
return args.fn(emailCards)
|
||||
}
|
||||
|
||||
function makeEmailAddedToSubscriptionString(email, args) {
|
||||
const locales = args.data.root.req.supportedLocales;
|
||||
const nestedEmail = `<span class="bold">${email}</span>`;
|
||||
return LocaleUtils.fluentFormat(locales, "email-added-to-subscription", { email: nestedEmail });
|
||||
function welcomeMessage (args) {
|
||||
const locales = args.data.root.req.supportedLocales
|
||||
const userEmail = args.data.root.req.session.user.fxa_profile_json.email
|
||||
const newUser = args.data.root.req.session.newUser
|
||||
|
||||
if (newUser) {
|
||||
return LocaleUtils.fluentFormat(locales, 'welcome-user', { userName: userEmail })
|
||||
}
|
||||
|
||||
return LocaleUtils.fluentFormat(locales, 'welcome-back', { userName: userEmail })
|
||||
}
|
||||
|
||||
|
||||
function makeEmailVerifiedString(args) {
|
||||
const locales = args.data.root.req.supportedLocales;
|
||||
let nestedSignInLink = LocaleUtils.fluentFormat(locales, "sign-in-nested", {});
|
||||
nestedSignInLink = `<a class="text-link bold blue-link" href="/oauth/init">${nestedSignInLink}</a>`;
|
||||
|
||||
return LocaleUtils.fluentFormat(locales, "email-verified-view-dashboard", { nestedSignInLink: nestedSignInLink});
|
||||
function makeEmailAddedToSubscriptionString (email, args) {
|
||||
const locales = args.data.root.req.supportedLocales
|
||||
const nestedEmail = `<span class="bold">${email}</span>`
|
||||
return LocaleUtils.fluentFormat(locales, 'email-added-to-subscription', { email: nestedEmail })
|
||||
}
|
||||
|
||||
function makeEmailVerifiedString (args) {
|
||||
const locales = args.data.root.req.supportedLocales
|
||||
let nestedSignInLink = LocaleUtils.fluentFormat(locales, 'sign-in-nested', {})
|
||||
nestedSignInLink = `<a class="text-link bold blue-link" href="/oauth/init">${nestedSignInLink}</a>`
|
||||
|
||||
function getUserPreferences(args) {
|
||||
const csrfToken = args.data.root.csrfToken;
|
||||
const unverifiedEmails = args.data.root.unverifiedEmails;
|
||||
const verifiedEmails = args.data.root.verifiedEmails;
|
||||
const sessionUser = args.data.root.req.session.user;
|
||||
const communicationOption = (sessionUser.all_emails_to_primary) ? 1 : 0;
|
||||
return LocaleUtils.fluentFormat(locales, 'email-verified-view-dashboard', { nestedSignInLink })
|
||||
}
|
||||
|
||||
const locales = args.data.root.req.supportedLocales;
|
||||
args.data.root.preferences = true;
|
||||
function getUserPreferences (args) {
|
||||
const csrfToken = args.data.root.csrfToken
|
||||
const unverifiedEmails = args.data.root.unverifiedEmails
|
||||
const verifiedEmails = args.data.root.verifiedEmails
|
||||
const sessionUser = args.data.root.req.session.user
|
||||
const communicationOption = (sessionUser.all_emails_to_primary) ? 1 : 0
|
||||
|
||||
const locales = args.data.root.req.supportedLocales
|
||||
args.data.root.preferences = true
|
||||
|
||||
verifiedEmails.forEach(email => {
|
||||
email.numBreaches = email.breaches.length;
|
||||
delete email.breaches;
|
||||
});
|
||||
email.numBreaches = email.breaches.length
|
||||
delete email.breaches
|
||||
})
|
||||
|
||||
const primaryEmail = verifiedEmails.shift();
|
||||
const primaryEmail = verifiedEmails.shift()
|
||||
|
||||
const emailAddresses = {
|
||||
primary: {
|
||||
subhead: LocaleUtils.fluentFormat(locales, "fxa-primary-email"),
|
||||
className: "fxa-primary-email",
|
||||
email_addresses: [ primaryEmail ], // put in array for template looping
|
||||
subhead: LocaleUtils.fluentFormat(locales, 'fxa-primary-email'),
|
||||
className: 'fxa-primary-email',
|
||||
email_addresses: [primaryEmail] // put in array for template looping
|
||||
},
|
||||
secondary: {
|
||||
subhead: LocaleUtils.fluentFormat(locales, "other-monitored-emails"),
|
||||
className: "other-monitored-emails",
|
||||
email_addresses: verifiedEmails,
|
||||
subhead: LocaleUtils.fluentFormat(locales, 'other-monitored-emails'),
|
||||
className: 'other-monitored-emails',
|
||||
email_addresses: verifiedEmails
|
||||
},
|
||||
unverified: {
|
||||
subhead: LocaleUtils.fluentFormat(locales, "email-verification-required"),
|
||||
className: "email-verification-required",
|
||||
email_addresses: unverifiedEmails,
|
||||
subhead: LocaleUtils.fluentFormat(locales, 'email-verification-required'),
|
||||
className: 'email-verification-required',
|
||||
email_addresses: unverifiedEmails
|
||||
},
|
||||
total: [ primaryEmail ].length + verifiedEmails.length + unverifiedEmails.length,
|
||||
};
|
||||
total: [primaryEmail].length + verifiedEmails.length + unverifiedEmails.length
|
||||
}
|
||||
|
||||
const communicationOptions = [
|
||||
{
|
||||
optionDescription: "Send breach alerts to the affected email address.",
|
||||
labelString: LocaleUtils.fluentFormat(locales, "to-affected-email"),
|
||||
optionId: "0",
|
||||
optionChecked: (communicationOption === 0) ? "checked" : "",
|
||||
optionDescription: 'Send breach alerts to the affected email address.',
|
||||
labelString: LocaleUtils.fluentFormat(locales, 'to-affected-email'),
|
||||
optionId: '0',
|
||||
optionChecked: (communicationOption === 0) ? 'checked' : ''
|
||||
},
|
||||
{
|
||||
optionDescription: "Send all breach alerts to subscriber's primary email address.",
|
||||
labelString: LocaleUtils.fluentFormat(locales, "comm-opt-1", {primaryEmail: `<span class="bold">${primaryEmail.email}</span>`}),
|
||||
optionId: "1",
|
||||
optionChecked: (communicationOption === 1) ? "checked" : "",
|
||||
},
|
||||
];
|
||||
labelString: LocaleUtils.fluentFormat(locales, 'comm-opt-1', { primaryEmail: `<span class="bold">${primaryEmail.email}</span>` }),
|
||||
optionId: '1',
|
||||
optionChecked: (communicationOption === 1) ? 'checked' : ''
|
||||
}
|
||||
]
|
||||
|
||||
const user = {
|
||||
primaryEmail: primaryEmail.email,
|
||||
emails : emailAddresses,
|
||||
communicationOptions: communicationOptions,
|
||||
csrfToken: csrfToken,
|
||||
};
|
||||
return args.fn(user);
|
||||
emails: emailAddresses,
|
||||
communicationOptions,
|
||||
csrfToken
|
||||
}
|
||||
return args.fn(user)
|
||||
}
|
||||
|
||||
function getLastAddedEmailStrings(args) {
|
||||
const locales = args.data.root.req.supportedLocales;
|
||||
const lastAddedEmail = args.data.root.lastAddedEmail;
|
||||
const lastAddedEmailSpan = `<span class="bold">${lastAddedEmail}</span>`;
|
||||
function getLastAddedEmailStrings (args) {
|
||||
const locales = args.data.root.req.supportedLocales
|
||||
const lastAddedEmail = args.data.root.lastAddedEmail
|
||||
const lastAddedEmailSpan = `<span class="bold">${lastAddedEmail}</span>`
|
||||
|
||||
const preferencesLinkString = LocaleUtils.fluentFormat(locales, "preferences");
|
||||
const preferencesLink = `<a class="demi text-link" href="/user/preferences">${preferencesLinkString}</a>`;
|
||||
const preferencesLinkString = LocaleUtils.fluentFormat(locales, 'preferences')
|
||||
const preferencesLink = `<a class="demi text-link" href="/user/preferences">${preferencesLinkString}</a>`
|
||||
|
||||
const lastAddedEmailStrings = [
|
||||
LocaleUtils.fluentFormat(locales, "verify-the-link", { userEmail: lastAddedEmailSpan }),
|
||||
LocaleUtils.fluentFormat(locales, "manage-all-emails", { preferencesLink }),
|
||||
];
|
||||
return lastAddedEmailStrings;
|
||||
LocaleUtils.fluentFormat(locales, 'verify-the-link', { userEmail: lastAddedEmailSpan }),
|
||||
LocaleUtils.fluentFormat(locales, 'manage-all-emails', { preferencesLink })
|
||||
]
|
||||
return lastAddedEmailStrings
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
@ -188,5 +186,5 @@ module.exports = {
|
|||
makeEmailVerifiedString,
|
||||
makeEmailAddedToSubscriptionString,
|
||||
enLocaleIsSupported,
|
||||
userIsOnRelayWaitList,
|
||||
};
|
||||
userIsOnRelayWaitList
|
||||
}
|
||||
|
|
|
@ -1,311 +1,299 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const { URL } = require("url");
|
||||
const { URL } = require('url')
|
||||
|
||||
const { LocaleUtils } = require("./../locale-utils");
|
||||
const { LocaleUtils } = require('./../locale-utils')
|
||||
|
||||
const { makeBreachCards } = require("./breaches");
|
||||
const { prettyDate, vpnPromoBlocked } = require("./hbs-helpers");
|
||||
const { makeBreachCards } = require('./breaches')
|
||||
const { prettyDate, vpnPromoBlocked } = require('./hbs-helpers')
|
||||
|
||||
|
||||
function emailBreachStats(args) {
|
||||
const locales = args.data.root.supportedLocales;
|
||||
const userBreaches = args.data.root.unsafeBreachesForEmail;
|
||||
let numPasswordsExposed = 0;
|
||||
function emailBreachStats (args) {
|
||||
const locales = args.data.root.supportedLocales
|
||||
const userBreaches = args.data.root.unsafeBreachesForEmail
|
||||
let numPasswordsExposed = 0
|
||||
|
||||
userBreaches.forEach(breach => {
|
||||
if (breach.DataClasses.includes("passwords")) {
|
||||
numPasswordsExposed++;
|
||||
if (breach.DataClasses.includes('passwords')) {
|
||||
numPasswordsExposed++
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
const emailBreachStats = {
|
||||
numBreaches: {
|
||||
statNumber: userBreaches.length,
|
||||
statTitle: LocaleUtils.fluentFormat(locales, "known-data-breaches-exposed", { breaches: userBreaches.length }),
|
||||
statTitle: LocaleUtils.fluentFormat(locales, 'known-data-breaches-exposed', { breaches: userBreaches.length })
|
||||
},
|
||||
numPasswords: {
|
||||
statNumber: numPasswordsExposed,
|
||||
statTitle: LocaleUtils.fluentFormat(locales, "passwords-exposed", { passwords: numPasswordsExposed }),
|
||||
},
|
||||
};
|
||||
return emailBreachStats;
|
||||
}
|
||||
|
||||
function getPreFxaUtmParams(serverUrl, content, userEmail) {
|
||||
const url = new URL(`${serverUrl}/oauth/init`);
|
||||
const utmParams = {
|
||||
utm_source: "fx-monitor",
|
||||
utm_medium: "fx-monitor-email",
|
||||
utm_content: content,
|
||||
utm_campaign: "pre-fxa-subscribers",
|
||||
email: userEmail,
|
||||
};
|
||||
for (const param in utmParams) {
|
||||
url.searchParams.append(param, utmParams[param]);
|
||||
statTitle: LocaleUtils.fluentFormat(locales, 'passwords-exposed', { passwords: numPasswordsExposed })
|
||||
}
|
||||
}
|
||||
return url;
|
||||
return emailBreachStats
|
||||
}
|
||||
|
||||
function getPreFxaTouts(args) {
|
||||
const locales = args.data.root.supportedLocales;
|
||||
const serverUrl = args.data.root.SERVER_URL;
|
||||
const userEmail = args.data.root.email;
|
||||
function getPreFxaUtmParams (serverUrl, content, userEmail) {
|
||||
const url = new URL(`${serverUrl}/oauth/init`)
|
||||
const utmParams = {
|
||||
utm_source: 'fx-monitor',
|
||||
utm_medium: 'fx-monitor-email',
|
||||
utm_content: content,
|
||||
utm_campaign: 'pre-fxa-subscribers',
|
||||
email: userEmail
|
||||
}
|
||||
for (const param in utmParams) {
|
||||
url.searchParams.append(param, utmParams[param])
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
function getPreFxaTouts (args) {
|
||||
const locales = args.data.root.supportedLocales
|
||||
const serverUrl = args.data.root.SERVER_URL
|
||||
const userEmail = args.data.root.email
|
||||
|
||||
const fxaTouts = [
|
||||
{
|
||||
imgSrc: `${serverUrl}/img/email_images/pictogram-alert.png`,
|
||||
headline: LocaleUtils.fluentFormat(locales, "pre-fxa-tout-1"),
|
||||
paragraph: LocaleUtils.fluentFormat(locales, "pre-fxa-p-1"),
|
||||
headline: LocaleUtils.fluentFormat(locales, 'pre-fxa-tout-1'),
|
||||
paragraph: LocaleUtils.fluentFormat(locales, 'pre-fxa-p-1')
|
||||
},
|
||||
{
|
||||
imgSrc: `${serverUrl}/img/email_images/pictogram-advice.png`,
|
||||
headline: LocaleUtils.fluentFormat(locales, "pre-fxa-tout-2"),
|
||||
paragraph: LocaleUtils.fluentFormat(locales, "pre-fxa-p-2"),
|
||||
headline: LocaleUtils.fluentFormat(locales, 'pre-fxa-tout-2'),
|
||||
paragraph: LocaleUtils.fluentFormat(locales, 'pre-fxa-p-2')
|
||||
},
|
||||
{
|
||||
imgSrc: `${serverUrl}/img/email_images/pictogram-email.png`,
|
||||
headline: LocaleUtils.fluentFormat(locales, "pre-fxa-tout-3"),
|
||||
paragraph: LocaleUtils.fluentFormat(locales, "pre-fxa-p-3"),
|
||||
},
|
||||
];
|
||||
headline: LocaleUtils.fluentFormat(locales, 'pre-fxa-tout-3'),
|
||||
paragraph: LocaleUtils.fluentFormat(locales, 'pre-fxa-p-3')
|
||||
}
|
||||
]
|
||||
|
||||
// replace placeholder anchor tag markup in first tout to make link
|
||||
// add UTM params which are passed to FxA for account creation
|
||||
const fxaTout1 = fxaTouts[0].paragraph;
|
||||
const url = getPreFxaUtmParams(serverUrl, "create-account-link", userEmail);
|
||||
const fxaTout1 = fxaTouts[0].paragraph
|
||||
const url = getPreFxaUtmParams(serverUrl, 'create-account-link', userEmail)
|
||||
if ((/<a>/).test(fxaTout1) && (/<\/a>/).test(fxaTout1)) {
|
||||
const openingAnchorTag = `<a class="pre-fxa-nested-link" href="${url}" style="color: #0060df; font-family: sans-serif; font-weight: 300; font-size: 15px; text-decoration: none;">`;
|
||||
fxaTouts[0].paragraph = fxaTout1.replace("<a>", openingAnchorTag);
|
||||
const openingAnchorTag = `<a class="pre-fxa-nested-link" href="${url}" style="color: #0060df; font-family: sans-serif; font-weight: 300; font-size: 15px; text-decoration: none;">`
|
||||
fxaTouts[0].paragraph = fxaTout1.replace('<a>', openingAnchorTag)
|
||||
}
|
||||
|
||||
return fxaTouts;
|
||||
return fxaTouts
|
||||
}
|
||||
|
||||
function getUnsafeBreachesForEmailReport(args) {
|
||||
const locales = args.data.root.supportedLocales;
|
||||
const foundBreaches = JSON.parse(JSON.stringify(args.data.root.unsafeBreachesForEmail));
|
||||
function getUnsafeBreachesForEmailReport (args) {
|
||||
const locales = args.data.root.supportedLocales
|
||||
const foundBreaches = JSON.parse(JSON.stringify(args.data.root.unsafeBreachesForEmail))
|
||||
|
||||
if (foundBreaches.length > 4) {
|
||||
foundBreaches.length = 4;
|
||||
foundBreaches.length = 4
|
||||
}
|
||||
return makeBreachCards(foundBreaches, locales);
|
||||
return makeBreachCards(foundBreaches, locales)
|
||||
}
|
||||
|
||||
|
||||
function boldVioletText(breachedEmail, addBlockDisplayToEmail = false) {
|
||||
let optionalDisplayProperty = "";
|
||||
function boldVioletText (breachedEmail, addBlockDisplayToEmail = false) {
|
||||
let optionalDisplayProperty = ''
|
||||
|
||||
if (addBlockDisplayToEmail) {
|
||||
optionalDisplayProperty = "display: block;";
|
||||
optionalDisplayProperty = 'display: block;'
|
||||
}
|
||||
|
||||
// garble email address so that email clients won't turn it into a link
|
||||
breachedEmail = breachedEmail.replace(/([@.:])/g, "<span>$1</span>");
|
||||
return `<span class="rec-email text-bold" style=" ${optionalDisplayProperty} font-weight: 700; color: #9059ff; font-family: sans-serif; text-decoration: none;"> ${breachedEmail}</span>`;
|
||||
breachedEmail = breachedEmail.replace(/([@.:])/g, '<span>$1</span>')
|
||||
return `<span class="rec-email text-bold" style=" ${optionalDisplayProperty} font-weight: 700; color: #9059ff; font-family: sans-serif; text-decoration: none;"> ${breachedEmail}</span>`
|
||||
}
|
||||
|
||||
function getEmailHeader (args) {
|
||||
const locales = args.data.root.supportedLocales
|
||||
const emailType = args.data.root.whichPartial
|
||||
const breachedEmail = args.data.root.breachedEmail
|
||||
|
||||
function getEmailHeader(args) {
|
||||
const locales = args.data.root.supportedLocales;
|
||||
const emailType = args.data.root.whichPartial;
|
||||
const breachedEmail = args.data.root.breachedEmail;
|
||||
|
||||
if (emailType === "email_partials/email_verify") {
|
||||
return LocaleUtils.fluentFormat(locales, "email-link-expires");
|
||||
if (emailType === 'email_partials/email_verify') {
|
||||
return LocaleUtils.fluentFormat(locales, 'email-link-expires')
|
||||
}
|
||||
|
||||
if (emailType === "email_partials/pre-fxa") {
|
||||
return LocaleUtils.fluentFormat(locales, "pre-fxa-headline");
|
||||
if (emailType === 'email_partials/pre-fxa') {
|
||||
return LocaleUtils.fluentFormat(locales, 'pre-fxa-headline')
|
||||
}
|
||||
|
||||
if (args.data.root.breachAlert) {
|
||||
return LocaleUtils.fluentFormat(locales, "email-alert-hl", { userEmail: boldVioletText(breachedEmail, true) });
|
||||
return LocaleUtils.fluentFormat(locales, 'email-alert-hl', { userEmail: boldVioletText(breachedEmail, true) })
|
||||
}
|
||||
|
||||
const userBreaches = args.data.root.unsafeBreachesForEmail;
|
||||
const userBreaches = args.data.root.unsafeBreachesForEmail
|
||||
|
||||
if (userBreaches.length === 0) {
|
||||
return LocaleUtils.fluentFormat(locales, "email-no-breaches-hl", { userEmail: boldVioletText(breachedEmail, true) });
|
||||
return LocaleUtils.fluentFormat(locales, 'email-no-breaches-hl', { userEmail: boldVioletText(breachedEmail, true) })
|
||||
}
|
||||
|
||||
return LocaleUtils.fluentFormat(locales, "email-found-breaches-hl");
|
||||
return LocaleUtils.fluentFormat(locales, 'email-found-breaches-hl')
|
||||
}
|
||||
|
||||
|
||||
function makeFaqLink(target, campaign) {
|
||||
const url = new URL(`https://support.mozilla.org/kb/firefox-monitor-faq${target}`);
|
||||
function makeFaqLink (target, campaign) {
|
||||
const url = new URL(`https://support.mozilla.org/kb/firefox-monitor-faq${target}`)
|
||||
const utmParameters = {
|
||||
utm_source: "fx-monitor",
|
||||
utm_medium: "email",
|
||||
utm_campaign: campaign,
|
||||
};
|
||||
utm_source: 'fx-monitor',
|
||||
utm_medium: 'email',
|
||||
utm_campaign: campaign
|
||||
}
|
||||
|
||||
for (const param in utmParameters) {
|
||||
url.searchParams.append(param, utmParameters[param]);
|
||||
url.searchParams.append(param, utmParameters[param])
|
||||
}
|
||||
return url;
|
||||
return url
|
||||
}
|
||||
|
||||
function makePreFxaSubscriberMessage(args) {
|
||||
const serverUrl = args.data.root.SERVER_URL;
|
||||
const locales = args.data.root.supportedLocales;
|
||||
const url = new URL(`${serverUrl}/#fx-account-features`);
|
||||
function makePreFxaSubscriberMessage (args) {
|
||||
const serverUrl = args.data.root.SERVER_URL
|
||||
const locales = args.data.root.supportedLocales
|
||||
const url = new URL(`${serverUrl}/#fx-account-features`)
|
||||
|
||||
const utmParameters = {
|
||||
utm_source: "fx-monitor",
|
||||
utm_medium: "email",
|
||||
utm_content: "breach-alert",
|
||||
utm_campaign: "pre-fxa-subscribers",
|
||||
};
|
||||
for (const param in utmParameters) {
|
||||
url.searchParams.append(param, utmParameters[param]);
|
||||
utm_source: 'fx-monitor',
|
||||
utm_medium: 'email',
|
||||
utm_content: 'breach-alert',
|
||||
utm_campaign: 'pre-fxa-subscribers'
|
||||
}
|
||||
let preFxaMessage = LocaleUtils.fluentFormat(locales, "pre-fxa-message");
|
||||
for (const param in utmParameters) {
|
||||
url.searchParams.append(param, utmParameters[param])
|
||||
}
|
||||
let preFxaMessage = LocaleUtils.fluentFormat(locales, 'pre-fxa-message')
|
||||
if ((/<a>/).test(preFxaMessage) && (/<\/a>/).test(preFxaMessage)) {
|
||||
const openingAnchorTag = `<a class="pre-fxa-nested-link" href="${url}" style="color: #0060df; font-family: sans-serif; font-weight: 400; font-size: 16px; text-decoration: none;">`;
|
||||
preFxaMessage = preFxaMessage.replace("<a>", openingAnchorTag);
|
||||
const openingAnchorTag = `<a class="pre-fxa-nested-link" href="${url}" style="color: #0060df; font-family: sans-serif; font-weight: 400; font-size: 16px; text-decoration: none;">`
|
||||
preFxaMessage = preFxaMessage.replace('<a>', openingAnchorTag)
|
||||
}
|
||||
return preFxaMessage;
|
||||
return preFxaMessage
|
||||
}
|
||||
|
||||
|
||||
function getBreachAlertFaqs(args) {
|
||||
const supportedLocales = args.data.root.supportedLocales;
|
||||
function getBreachAlertFaqs (args) {
|
||||
const supportedLocales = args.data.root.supportedLocales
|
||||
const faqs = [
|
||||
{
|
||||
"linkTitle": LocaleUtils.fluentFormat(supportedLocales, "faq-v2-1", args),
|
||||
"stringDescription": "I don’t recognize one of these companies or websites. Why am I in this breach?",
|
||||
"href": makeFaqLink("#w_i-donaot-recognize-this-company-or-website-why-am-i-receiving-notifications-about-this-breach", "faq1"),
|
||||
linkTitle: LocaleUtils.fluentFormat(supportedLocales, 'faq-v2-1', args),
|
||||
stringDescription: 'I don’t recognize one of these companies or websites. Why am I in this breach?',
|
||||
href: makeFaqLink('#w_i-donaot-recognize-this-company-or-website-why-am-i-receiving-notifications-about-this-breach', 'faq1')
|
||||
},
|
||||
{
|
||||
"linkTitle": LocaleUtils.fluentFormat(supportedLocales, "faq-v2-2", args),
|
||||
"stringDescription": "Do I need to do anything if a breach happened years ago or this is an old account?",
|
||||
"href": makeFaqLink("#w_do-i-need-to-do-anything-if-a-breach-happened-years-ago-or-in-an-old-account", "faq2"),
|
||||
linkTitle: LocaleUtils.fluentFormat(supportedLocales, 'faq-v2-2', args),
|
||||
stringDescription: 'Do I need to do anything if a breach happened years ago or this is an old account?',
|
||||
href: makeFaqLink('#w_do-i-need-to-do-anything-if-a-breach-happened-years-ago-or-in-an-old-account', 'faq2')
|
||||
},
|
||||
{
|
||||
"linkTitle": LocaleUtils.fluentFormat(supportedLocales, "faq-v2-3", args),
|
||||
"stringDescription": "I just found out I’m in a data breach. What do I do next?",
|
||||
"href": makeFaqLink("#w_i-just-found-out-im-in-a-data-breach-what-do-i-do-next", "faq3"),
|
||||
},
|
||||
];
|
||||
linkTitle: LocaleUtils.fluentFormat(supportedLocales, 'faq-v2-3', args),
|
||||
stringDescription: 'I just found out I’m in a data breach. What do I do next?',
|
||||
href: makeFaqLink('#w_i-just-found-out-im-in-a-data-breach-what-do-i-do-next', 'faq3')
|
||||
}
|
||||
]
|
||||
|
||||
if (args.data.root.breachAlert && args.data.root.breachAlert.IsSensitive) {
|
||||
faqs.push({
|
||||
"linkTitle": LocaleUtils.fluentFormat(supportedLocales, "faq-v2-4", args),
|
||||
"stringDescription": "How does Firefox Monitor treat sensitive sites?",
|
||||
"href": makeFaqLink("#w_how-does-firefox-monitor-treat-sensitive-sites", "faq4"),
|
||||
});
|
||||
linkTitle: LocaleUtils.fluentFormat(supportedLocales, 'faq-v2-4', args),
|
||||
stringDescription: 'How does Firefox Monitor treat sensitive sites?',
|
||||
href: makeFaqLink('#w_how-does-firefox-monitor-treat-sensitive-sites', 'faq4')
|
||||
})
|
||||
}
|
||||
|
||||
const functionedFaqs = faqs.map(faq => args.fn(faq));
|
||||
return "".concat(...functionedFaqs);
|
||||
const functionedFaqs = faqs.map(faq => args.fn(faq))
|
||||
return ''.concat(...functionedFaqs)
|
||||
}
|
||||
|
||||
|
||||
function getReportHeader(args) {
|
||||
const locales = args.data.root.supportedLocales;
|
||||
function getReportHeader (args) {
|
||||
const locales = args.data.root.supportedLocales
|
||||
const reportHeader = {
|
||||
date: prettyDate(new Date(), locales),
|
||||
strings: {
|
||||
"email": "email-address",
|
||||
"reportDate": "report-date",
|
||||
"fxmReport": "firefox-monitor-report",
|
||||
},
|
||||
};
|
||||
email: 'email-address',
|
||||
reportDate: 'report-date',
|
||||
fxmReport: 'firefox-monitor-report'
|
||||
}
|
||||
}
|
||||
for (const stringId in reportHeader.strings) {
|
||||
const newString = LocaleUtils.fluentFormat(locales, reportHeader.strings[stringId]);
|
||||
reportHeader.strings[stringId] = newString;
|
||||
const newString = LocaleUtils.fluentFormat(locales, reportHeader.strings[stringId])
|
||||
reportHeader.strings[stringId] = newString
|
||||
}
|
||||
return args.fn(reportHeader);
|
||||
return args.fn(reportHeader)
|
||||
}
|
||||
|
||||
function getEmailFooterCopy (args) {
|
||||
const locales = args.data.root.supportedLocales
|
||||
|
||||
function getEmailFooterCopy(args) {
|
||||
const locales = args.data.root.supportedLocales;
|
||||
let faqLink = LocaleUtils.fluentFormat(locales, 'frequently-asked-questions')
|
||||
faqLink = `<a href="https://support.mozilla.org/kb/firefox-monitor-faq">${faqLink}</a>`
|
||||
|
||||
let faqLink = LocaleUtils.fluentFormat(locales, "frequently-asked-questions");
|
||||
faqLink = `<a href="https://support.mozilla.org/kb/firefox-monitor-faq">${faqLink}</a>`;
|
||||
|
||||
if (args.data.root.whichPartial === "email_partials/email_verify") {
|
||||
return LocaleUtils.fluentFormat(locales, "email-verify-footer-copy", { faqLink });
|
||||
if (args.data.root.whichPartial === 'email_partials/email_verify') {
|
||||
return LocaleUtils.fluentFormat(locales, 'email-verify-footer-copy', { faqLink })
|
||||
}
|
||||
|
||||
const unsubUrl = args.data.root.unsubscribeUrl;
|
||||
const unsubLinkText = LocaleUtils.fluentFormat(locales, "email-unsub-link");
|
||||
const unsubLink = `<a href="${unsubUrl}">${unsubLinkText}</a>`;
|
||||
const unsubUrl = args.data.root.unsubscribeUrl
|
||||
const unsubLinkText = LocaleUtils.fluentFormat(locales, 'email-unsub-link')
|
||||
const unsubLink = `<a href="${unsubUrl}">${unsubLinkText}</a>`
|
||||
|
||||
const localizedFooterCopy = LocaleUtils.fluentFormat(locales, "email-footer-blurb", {
|
||||
unsubLink: unsubLink,
|
||||
faqLink: faqLink,
|
||||
});
|
||||
const localizedFooterCopy = LocaleUtils.fluentFormat(locales, 'email-footer-blurb', {
|
||||
unsubLink,
|
||||
faqLink
|
||||
})
|
||||
|
||||
return localizedFooterCopy;
|
||||
return localizedFooterCopy
|
||||
}
|
||||
|
||||
function getEmailCTA (args) {
|
||||
const locales = args.data.root.supportedLocales
|
||||
const emailType = args.data.root.whichPartial
|
||||
|
||||
function getEmailCTA(args) {
|
||||
const locales = args.data.root.supportedLocales;
|
||||
const emailType = args.data.root.whichPartial;
|
||||
|
||||
if (emailType === "email_partials/email_verify") {
|
||||
return LocaleUtils.fluentFormat(locales, "verify-email-cta");
|
||||
if (emailType === 'email_partials/email_verify') {
|
||||
return LocaleUtils.fluentFormat(locales, 'verify-email-cta')
|
||||
}
|
||||
return LocaleUtils.fluentFormat(locales, "go-to-dashboard-link");
|
||||
return LocaleUtils.fluentFormat(locales, 'go-to-dashboard-link')
|
||||
}
|
||||
|
||||
|
||||
function getBreachSummaryHeadline(args) {
|
||||
const locales = args.data.root.supportedLocales;
|
||||
const breachedEmail = args.data.root.breachedEmail;
|
||||
return LocaleUtils.fluentFormat(locales, "email-breach-summary-for-email", { userEmail: boldVioletText(breachedEmail) });
|
||||
function getBreachSummaryHeadline (args) {
|
||||
const locales = args.data.root.supportedLocales
|
||||
const breachedEmail = args.data.root.breachedEmail
|
||||
return LocaleUtils.fluentFormat(locales, 'email-breach-summary-for-email', { userEmail: boldVioletText(breachedEmail) })
|
||||
}
|
||||
|
||||
|
||||
function getBreachAlert(args) {
|
||||
const locales = args.data.root.supportedLocales;
|
||||
const breachAlert = [args.data.root.breachAlert];
|
||||
const breachAlertCard = makeBreachCards(breachAlert, locales);
|
||||
return args.fn(breachAlertCard[0]);
|
||||
function getBreachAlert (args) {
|
||||
const locales = args.data.root.supportedLocales
|
||||
const breachAlert = [args.data.root.breachAlert]
|
||||
const breachAlertCard = makeBreachCards(breachAlert, locales)
|
||||
return args.fn(breachAlertCard[0])
|
||||
}
|
||||
|
||||
|
||||
// Show FAQs if the email type is a report with breaches, or a breach alert.
|
||||
function showFaqs(args) {
|
||||
if (args.data.root.whichPartial === "email_partials/email_verify") {
|
||||
return;
|
||||
function showFaqs (args) {
|
||||
if (args.data.root.whichPartial === 'email_partials/email_verify') {
|
||||
return
|
||||
}
|
||||
|
||||
if (args.data.root.breachAlert || (args.data.root.unsafeBreachesForEmail && args.data.root.unsafeBreachesForEmail.length > 0)) {
|
||||
return args.fn();
|
||||
return args.fn()
|
||||
}
|
||||
}
|
||||
|
||||
function ifPreFxaSubscriber(args) {
|
||||
function ifPreFxaSubscriber (args) {
|
||||
if (args.data.root.preFxaSubscriber) {
|
||||
return args.fn();
|
||||
return args.fn()
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
function getServerUrlForNestedEmailPartial(args) {
|
||||
return args.data.root.SERVER_URL;
|
||||
function getServerUrlForNestedEmailPartial (args) {
|
||||
return args.data.root.SERVER_URL
|
||||
}
|
||||
|
||||
function showProducts(args) {
|
||||
const { whichPartial, breachAlert } = args.data.root;
|
||||
function showProducts (args) {
|
||||
const { whichPartial, breachAlert } = args.data.root
|
||||
|
||||
switch (true) {
|
||||
case whichPartial === "email_partials/email_verify":
|
||||
case whichPartial === 'email_partials/email_verify':
|
||||
case vpnPromoBlocked(args):
|
||||
return; // don't show products partial for the cases above
|
||||
return // don't show products partial for the cases above
|
||||
}
|
||||
|
||||
return args.fn({
|
||||
strings: {
|
||||
campaign: breachAlert ? `monitor-alert-emails&utm_content=${breachAlert.Name}` : "report",
|
||||
},
|
||||
});
|
||||
campaign: breachAlert ? `monitor-alert-emails&utm_content=${breachAlert.Name}` : 'report'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
@ -324,5 +312,5 @@ module.exports = {
|
|||
ifPreFxaSubscriber,
|
||||
makePreFxaSubscriberMessage,
|
||||
showFaqs,
|
||||
showProducts,
|
||||
};
|
||||
showProducts
|
||||
}
|
||||
|
|
|
@ -1,71 +1,71 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const { getStrings } = require("./hbs-helpers");
|
||||
const { LocaleUtils } = require("./../locale-utils");
|
||||
const { getStrings } = require('./hbs-helpers')
|
||||
const { LocaleUtils } = require('./../locale-utils')
|
||||
|
||||
function getFooterLinks(args) {
|
||||
const locales = args.data.root.req.supportedLocales;
|
||||
function getFooterLinks (args) {
|
||||
const locales = args.data.root.req.supportedLocales
|
||||
const footerLinks = [
|
||||
{
|
||||
title: "About Firefox Monitor",
|
||||
stringId: "about-firefox-monitor",
|
||||
href: "/about",
|
||||
title: 'About Firefox Monitor',
|
||||
stringId: 'about-firefox-monitor',
|
||||
href: '/about'
|
||||
},
|
||||
{
|
||||
title: "Frequently Asked Questions",
|
||||
stringId: "frequently-asked-questions",
|
||||
href: "https://support.mozilla.org/kb/firefox-monitor-faq",
|
||||
title: 'Frequently Asked Questions',
|
||||
stringId: 'frequently-asked-questions',
|
||||
href: 'https://support.mozilla.org/kb/firefox-monitor-faq'
|
||||
},
|
||||
{
|
||||
title: "Terms & Privacy",
|
||||
stringId: "terms-and-privacy",
|
||||
href: "https://www.mozilla.org/privacy/firefox-monitor/?utm_campaign=fx_monitor_downloads&utm_content=site-footer-link&utm_medium=referral&utm_source=monitor.firefox.com",
|
||||
title: 'Terms & Privacy',
|
||||
stringId: 'terms-and-privacy',
|
||||
href: 'https://www.mozilla.org/privacy/firefox-monitor/?utm_campaign=fx_monitor_downloads&utm_content=site-footer-link&utm_medium=referral&utm_source=monitor.firefox.com'
|
||||
},
|
||||
{
|
||||
title: "GitHub",
|
||||
stringId: "GitHub-link-title",
|
||||
href: "https://github.com/mozilla/blurts-server",
|
||||
},
|
||||
];
|
||||
title: 'GitHub',
|
||||
stringId: 'GitHub-link-title',
|
||||
href: 'https://github.com/mozilla/blurts-server'
|
||||
}
|
||||
]
|
||||
|
||||
return getStrings(footerLinks, locales);
|
||||
return getStrings(footerLinks, locales)
|
||||
}
|
||||
|
||||
function getAboutPageStrings(args) {
|
||||
const locales = args.data.root.req.supportedLocales;
|
||||
function getAboutPageStrings (args) {
|
||||
const locales = args.data.root.req.supportedLocales
|
||||
const aboutPageStrings = [
|
||||
{
|
||||
headline:"how-fxm-1-headline",
|
||||
subhead: "how-fxm-1-blurb",
|
||||
localizedCta: LocaleUtils.fluentFormat(locales, "scan-submit"),
|
||||
href: "/",
|
||||
eventCategory: "About Page: Search Your Email",
|
||||
headline: 'how-fxm-1-headline',
|
||||
subhead: 'how-fxm-1-blurb',
|
||||
localizedCta: LocaleUtils.fluentFormat(locales, 'scan-submit'),
|
||||
href: '/',
|
||||
eventCategory: 'About Page: Search Your Email'
|
||||
},
|
||||
{
|
||||
headline:"how-fxm-2-headline",
|
||||
subhead: "how-fxm-2-blurb",
|
||||
ctaId: "signUp",
|
||||
localizedCta: LocaleUtils.fluentFormat(locales, "sign-up-for-alerts"),
|
||||
headline: 'how-fxm-2-headline',
|
||||
subhead: 'how-fxm-2-blurb',
|
||||
ctaId: 'signUp',
|
||||
localizedCta: LocaleUtils.fluentFormat(locales, 'sign-up-for-alerts')
|
||||
},
|
||||
{
|
||||
headline:"how-fxm-3-headline",
|
||||
subhead: "how-fxm-3-blurb",
|
||||
localizedCta: LocaleUtils.fluentFormat(locales, "download-firefox-banner-button"),
|
||||
href: "https://www.mozilla.org/firefox",
|
||||
eventCategory: "About Page: Download Firefox",
|
||||
download: "download",
|
||||
},
|
||||
];
|
||||
headline: 'how-fxm-3-headline',
|
||||
subhead: 'how-fxm-3-blurb',
|
||||
localizedCta: LocaleUtils.fluentFormat(locales, 'download-firefox-banner-button'),
|
||||
href: 'https://www.mozilla.org/firefox',
|
||||
eventCategory: 'About Page: Download Firefox',
|
||||
download: 'download'
|
||||
}
|
||||
]
|
||||
|
||||
aboutPageStrings.forEach(aboutBlock => {
|
||||
aboutBlock.headline = LocaleUtils.fluentFormat(locales, aboutBlock.headline);
|
||||
aboutBlock.subhead = LocaleUtils.fluentFormat(locales, aboutBlock.subhead);
|
||||
aboutBlock.localizedCta = LocaleUtils.fluentFormat(locales, aboutBlock.localizedCta);
|
||||
});
|
||||
return aboutPageStrings;
|
||||
aboutBlock.headline = LocaleUtils.fluentFormat(locales, aboutBlock.headline)
|
||||
aboutBlock.subhead = LocaleUtils.fluentFormat(locales, aboutBlock.subhead)
|
||||
aboutBlock.localizedCta = LocaleUtils.fluentFormat(locales, aboutBlock.localizedCta)
|
||||
})
|
||||
return aboutPageStrings
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAboutPageStrings,
|
||||
getFooterLinks,
|
||||
};
|
||||
getFooterLinks
|
||||
}
|
||||
|
|
|
@ -1,104 +1,99 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const AppConstants = require("./../app-constants");
|
||||
const { LocaleUtils } = require("./../locale-utils");
|
||||
const mozlog = require("./../log");
|
||||
const AppConstants = require('./../app-constants')
|
||||
const { LocaleUtils } = require('./../locale-utils')
|
||||
const mozlog = require('./../log')
|
||||
|
||||
const log = mozlog('template-helpers/hbs-helpers')
|
||||
|
||||
const log = mozlog("template-helpers/hbs-helpers");
|
||||
|
||||
function getSupportedLocales(args) {
|
||||
function getSupportedLocales (args) {
|
||||
if (args.data) {
|
||||
if (args.data.root.supportedLocales) {
|
||||
return args.data.root.supportedLocales;
|
||||
return args.data.root.supportedLocales
|
||||
}
|
||||
return args.data.root.req.supportedLocales;
|
||||
return args.data.root.req.supportedLocales
|
||||
}
|
||||
if (args.this) {
|
||||
return args.this.req.supportedLocales;
|
||||
return args.this.req.supportedLocales
|
||||
}
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
function getFirstItem(arr) {
|
||||
if (!arr) return;
|
||||
if (typeof arr === "string") return arr.split(",")[0];
|
||||
return arr[0];
|
||||
function getFirstItem (arr) {
|
||||
if (!arr) return
|
||||
if (typeof arr === 'string') return arr.split(',')[0]
|
||||
return arr[0]
|
||||
}
|
||||
|
||||
function vpnPromoBlocked(args) {
|
||||
const userLocales = getSupportedLocales(args);
|
||||
return AppConstants.VPN_PROMO_BLOCKED_LOCALES?.some(blockedLocale => userLocales[0].includes(blockedLocale));
|
||||
function vpnPromoBlocked (args) {
|
||||
const userLocales = getSupportedLocales(args)
|
||||
return AppConstants.VPN_PROMO_BLOCKED_LOCALES?.some(blockedLocale => userLocales[0].includes(blockedLocale))
|
||||
}
|
||||
|
||||
function englishInAcceptLanguages(args) {
|
||||
const acceptedLanguages = args.data.root.req.acceptsLanguages();
|
||||
return acceptedLanguages.some(locale => locale.startsWith("en"));
|
||||
function englishInAcceptLanguages (args) {
|
||||
const acceptedLanguages = args.data.root.req.acceptsLanguages()
|
||||
return acceptedLanguages.some(locale => locale.startsWith('en'))
|
||||
}
|
||||
|
||||
|
||||
function escapeHtmlAttributeChars(text) {
|
||||
return text.replace(/"/g, """).replace(/'/g, "'");
|
||||
function escapeHtmlAttributeChars (text) {
|
||||
return text.replace(/"/g, '"').replace(/'/g, ''')
|
||||
}
|
||||
|
||||
|
||||
function recruitmentBanner(args) {
|
||||
function recruitmentBanner (args) {
|
||||
if (!AppConstants.RECRUITMENT_BANNER_LINK || !AppConstants.RECRUITMENT_BANNER_TEXT) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (!englishInAcceptLanguages(args)) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
return `<div class="recruitment-banner"><a id="recruitment-banner" href="${AppConstants.RECRUITMENT_BANNER_LINK}" hidden target="_blank" rel="noopener noreferrer" data-ga-link="" data-event-category="Recruitment" data-event-label="${escapeHtmlAttributeChars(AppConstants.RECRUITMENT_BANNER_TEXT)}">${AppConstants.RECRUITMENT_BANNER_TEXT}</a></div>`;
|
||||
return `<div class="recruitment-banner"><a id="recruitment-banner" href="${AppConstants.RECRUITMENT_BANNER_LINK}" hidden target="_blank" rel="noopener noreferrer" data-ga-link="" data-event-category="Recruitment" data-event-label="${escapeHtmlAttributeChars(AppConstants.RECRUITMENT_BANNER_TEXT)}">${AppConstants.RECRUITMENT_BANNER_TEXT}</a></div>`
|
||||
}
|
||||
|
||||
function showCsatBanner(args) {
|
||||
const signupDate = args.data.root.req.session.user?.created_at;
|
||||
function showCsatBanner (args) {
|
||||
const signupDate = args.data.root.req.session.user?.created_at
|
||||
|
||||
if (!signupDate) return; // don't show if user is not logged in or not signed up
|
||||
if (!signupDate) return // don't show if user is not logged in or not signed up
|
||||
|
||||
if (args.data.root.req.cookies.csatHidden) return; // don't show if user closed banner
|
||||
if (args.data.root.req.cookies.csatHidden) return // don't show if user closed banner
|
||||
|
||||
if (Date.now() - Date.parse(signupDate) < 604800000) return; // don't show if sign-up is less than 7 days old
|
||||
if (Date.now() - Date.parse(signupDate) < 604800000) return // don't show if sign-up is less than 7 days old
|
||||
|
||||
if (AppConstants.RECRUITMENT_BANNER_LINK || AppConstants.RECRUITMENT_BANNER_TEXT) return; // don't show if recruitment banner is present
|
||||
if (AppConstants.RECRUITMENT_BANNER_LINK || AppConstants.RECRUITMENT_BANNER_TEXT) return // don't show if recruitment banner is present
|
||||
|
||||
if (!englishInAcceptLanguages(args)) return; // don't show if language is not english
|
||||
if (!englishInAcceptLanguages(args)) return // don't show if language is not english
|
||||
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
|
||||
function getString(id, args) {
|
||||
const supportedLocales = getSupportedLocales(args);
|
||||
return LocaleUtils.fluentFormat(supportedLocales, id, args.hash);
|
||||
function getString (id, args) {
|
||||
const supportedLocales = getSupportedLocales(args)
|
||||
return LocaleUtils.fluentFormat(supportedLocales, id, args.hash)
|
||||
}
|
||||
|
||||
function getStringWithFallback(id, fallbackId, args) {
|
||||
const supportedLocales = getSupportedLocales(args);
|
||||
return LocaleUtils.fluentFormatWithFallback(supportedLocales, id, fallbackId, args.hash);
|
||||
function getStringWithFallback (id, fallbackId, args) {
|
||||
const supportedLocales = getSupportedLocales(args)
|
||||
return LocaleUtils.fluentFormatWithFallback(supportedLocales, id, fallbackId, args.hash)
|
||||
}
|
||||
|
||||
function getStrings(stringArr, locales) {
|
||||
function getStrings (stringArr, locales) {
|
||||
stringArr.forEach(string => {
|
||||
const stringId = string.stringId;
|
||||
string.stringId = LocaleUtils.fluentFormat(locales, stringId);
|
||||
});
|
||||
return stringArr;
|
||||
const stringId = string.stringId
|
||||
string.stringId = LocaleUtils.fluentFormat(locales, stringId)
|
||||
})
|
||||
return stringArr
|
||||
}
|
||||
|
||||
|
||||
function fluentFxa(id, args) {
|
||||
const supportedLocales = args.data.root.req.supportedLocales;
|
||||
function fluentFxa (id, args) {
|
||||
const supportedLocales = args.data.root.req.supportedLocales
|
||||
if (AppConstants.FXA_ENABLED) {
|
||||
id = `fxa-${id}`;
|
||||
id = `fxa-${id}`
|
||||
}
|
||||
return LocaleUtils.fluentFormat(supportedLocales, id, args.hash);
|
||||
return LocaleUtils.fluentFormat(supportedLocales, id, args.hash)
|
||||
}
|
||||
|
||||
|
||||
function getStringID(id, number, args) {
|
||||
function getStringID (id, number, args) {
|
||||
// const supportedLocales = args.data.root.req.supportedLocales;
|
||||
// id = `${id}${number}`;
|
||||
// if (modifiedStringMap[id]) {
|
||||
|
@ -107,120 +102,108 @@ function getStringID(id, number, args) {
|
|||
// return LocaleUtils.fluentFormat(supportedLocales, id);
|
||||
}
|
||||
|
||||
|
||||
function localizedBreachDataClasses(dataClasses, locales) {
|
||||
const localizedDataClasses = [];
|
||||
function localizedBreachDataClasses (dataClasses, locales) {
|
||||
const localizedDataClasses = []
|
||||
for (const dataClass of dataClasses) {
|
||||
localizedDataClasses.push(LocaleUtils.fluentFormat(locales, dataClass));
|
||||
localizedDataClasses.push(LocaleUtils.fluentFormat(locales, dataClass))
|
||||
}
|
||||
return localizedDataClasses.join(", ");
|
||||
return localizedDataClasses.join(', ')
|
||||
}
|
||||
|
||||
|
||||
function fluentNestedBold(id, args) {
|
||||
const supportedLocales = args.data.root.req.supportedLocales;
|
||||
function fluentNestedBold (id, args) {
|
||||
const supportedLocales = args.data.root.req.supportedLocales
|
||||
|
||||
const addMarkup = (word) => {
|
||||
return ` <span class="bold">${word}</span> `;
|
||||
};
|
||||
|
||||
let localizedStrings = LocaleUtils.fluentFormat(supportedLocales, id, args.hash);
|
||||
if (args.hash.breachCount || args.hash.breachCount === 0) {
|
||||
localizedStrings = localizedStrings.replace(/(\s[\d]+\s)/, addMarkup(args.hash.breachCount));
|
||||
return ` <span class="bold">${word}</span> `
|
||||
}
|
||||
return localizedStrings;
|
||||
|
||||
let localizedStrings = LocaleUtils.fluentFormat(supportedLocales, id, args.hash)
|
||||
if (args.hash.breachCount || args.hash.breachCount === 0) {
|
||||
localizedStrings = localizedStrings.replace(/(\s[\d]+\s)/, addMarkup(args.hash.breachCount))
|
||||
}
|
||||
return localizedStrings
|
||||
}
|
||||
|
||||
|
||||
function prettyDate(date, locales) {
|
||||
const jsDate = new Date(date);
|
||||
const options = { year: "numeric", month: "long", day: "numeric" };
|
||||
const intlDateTimeFormatter = new Intl.DateTimeFormat(locales, options);
|
||||
return intlDateTimeFormatter.format(jsDate);
|
||||
function prettyDate (date, locales) {
|
||||
const jsDate = new Date(date)
|
||||
const options = { year: 'numeric', month: 'long', day: 'numeric' }
|
||||
const intlDateTimeFormatter = new Intl.DateTimeFormat(locales, options)
|
||||
return intlDateTimeFormatter.format(jsDate)
|
||||
}
|
||||
|
||||
|
||||
function localeString(numericInput, locales) {
|
||||
const intlNumberFormatter = new Intl.NumberFormat(locales);
|
||||
return intlNumberFormatter.format(numericInput);
|
||||
function localeString (numericInput, locales) {
|
||||
const intlNumberFormatter = new Intl.NumberFormat(locales)
|
||||
return intlNumberFormatter.format(numericInput)
|
||||
}
|
||||
|
||||
function getFxaUrl() {
|
||||
return AppConstants.FXA_SETTINGS_URL;
|
||||
function getFxaUrl () {
|
||||
return AppConstants.FXA_SETTINGS_URL
|
||||
}
|
||||
|
||||
function eachFromTo (ary, min, max, options) {
|
||||
if (!ary || ary.length === 0) { return options.inverse(this) }
|
||||
|
||||
function eachFromTo(ary, min, max, options) {
|
||||
if (!ary || ary.length === 0)
|
||||
return options.inverse(this);
|
||||
|
||||
let result = "";
|
||||
let result = ''
|
||||
|
||||
for (let i = min; i < max && i < ary.length; i++) {
|
||||
result = result + options.fn(ary[i]);
|
||||
result = result + options.fn(ary[i])
|
||||
}
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
function localize(locales, stringId, args) {
|
||||
return LocaleUtils.fluentFormat(locales, stringId, args);
|
||||
function localize (locales, stringId, args) {
|
||||
return LocaleUtils.fluentFormat(locales, stringId, args)
|
||||
}
|
||||
|
||||
|
||||
function loop(from, to, inc, block) {
|
||||
block = block || { fn: function () { return arguments[0]; } };
|
||||
const data = block.data || { index: null };
|
||||
let output = "";
|
||||
function loop (from, to, inc, block) {
|
||||
block = block || { fn: function () { return arguments[0] } }
|
||||
const data = block.data || { index: null }
|
||||
let output = ''
|
||||
for (let i = from; i <= to; i += inc) {
|
||||
data["index"] = i;
|
||||
output += block.fn(i, { data: data });
|
||||
data.index = i
|
||||
output += block.fn(i, { data })
|
||||
}
|
||||
return output;
|
||||
return output
|
||||
}
|
||||
|
||||
|
||||
function ifCompare(v1, operator, v2, options) {
|
||||
//https://stackoverflow.com/questions/28978759/length-check-in-a-handlebars-js-if-conditional
|
||||
function ifCompare (v1, operator, v2, options) {
|
||||
// https://stackoverflow.com/questions/28978759/length-check-in-a-handlebars-js-if-conditional
|
||||
const operators = {
|
||||
">": v1 > v2 ? true : false,
|
||||
">=": v1 >= v2 ? true : false,
|
||||
"<": v1 < v2 ? true : false,
|
||||
"<=": v1 <= v2 ? true : false,
|
||||
"===": v1 === v2 ? true : false,
|
||||
"&&": v1 && v2 ? true : false,
|
||||
"||": v1 || v2 ? true : false,
|
||||
"!|": !v1 || !v2 ? true : false,
|
||||
"!!": !v1 && !v2 ? true : false,
|
||||
};
|
||||
'>': v1 > v2,
|
||||
'>=': v1 >= v2,
|
||||
'<': v1 < v2,
|
||||
'<=': v1 <= v2,
|
||||
'===': v1 === v2,
|
||||
'&&': !!(v1 && v2),
|
||||
'||': !!(v1 || v2),
|
||||
'!|': !!(!v1 || !v2),
|
||||
'!!': !!(!v1 && !v2)
|
||||
}
|
||||
if (operators.hasOwnProperty(operator)) {
|
||||
if (operators[operator]) {
|
||||
return options.fn(this);
|
||||
return options.fn(this)
|
||||
}
|
||||
return options.inverse(this);
|
||||
return options.inverse(this)
|
||||
}
|
||||
log.error("ifCompare", { message: `${operator} not found` });
|
||||
return;
|
||||
log.error('ifCompare', { message: `${operator} not found` })
|
||||
}
|
||||
|
||||
|
||||
function breachMath(lValue, operator = null, rValue = null) {
|
||||
lValue = parseFloat(lValue);
|
||||
let returnValue = lValue;
|
||||
function breachMath (lValue, operator = null, rValue = null) {
|
||||
lValue = parseFloat(lValue)
|
||||
let returnValue = lValue
|
||||
if (operator) {
|
||||
rValue = parseFloat(rValue);
|
||||
rValue = parseFloat(rValue)
|
||||
returnValue = {
|
||||
"+": lValue + rValue,
|
||||
"-": lValue - rValue,
|
||||
"*": lValue * rValue,
|
||||
"/": lValue / rValue,
|
||||
"%": lValue % rValue,
|
||||
}[operator];
|
||||
'+': lValue + rValue,
|
||||
'-': lValue - rValue,
|
||||
'*': lValue * rValue,
|
||||
'/': lValue / rValue,
|
||||
'%': lValue % rValue
|
||||
}[operator]
|
||||
}
|
||||
return returnValue;
|
||||
return returnValue
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
recruitmentBanner,
|
||||
englishInAcceptLanguages,
|
||||
|
@ -242,5 +225,5 @@ module.exports = {
|
|||
breachMath,
|
||||
loop,
|
||||
showCsatBanner,
|
||||
vpnPromoBlocked,
|
||||
};
|
||||
vpnPromoBlocked
|
||||
}
|
||||
|
|
|
@ -1,69 +1,68 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const { getStrings, getFxaUrl } = require("./hbs-helpers");
|
||||
const { LocaleUtils } = require("./../locale-utils");
|
||||
const { getStrings, getFxaUrl } = require('./hbs-helpers')
|
||||
const { LocaleUtils } = require('./../locale-utils')
|
||||
|
||||
function getSignedInAs(args) {
|
||||
const locales = args.data.root.req.supportedLocales;
|
||||
const userEmail = args.data.root.req.session.user.primary_email;
|
||||
const signedInAs = LocaleUtils.fluentFormat(locales, "signed-in-as", {
|
||||
userEmail: `<span class="nav-user-email">${userEmail}</span>`});
|
||||
return signedInAs;
|
||||
function getSignedInAs (args) {
|
||||
const locales = args.data.root.req.supportedLocales
|
||||
const userEmail = args.data.root.req.session.user.primary_email
|
||||
const signedInAs = LocaleUtils.fluentFormat(locales, 'signed-in-as', { userEmail: `<span class="nav-user-email">${userEmail}</span>` })
|
||||
return signedInAs
|
||||
}
|
||||
|
||||
function navLinks(args) {
|
||||
const hostUrl = args.data.root.req.url;
|
||||
const serverUrl = args.data.root.constants.SERVER_URL;
|
||||
const locales = args.data.root.req.supportedLocales;
|
||||
function navLinks (args) {
|
||||
const hostUrl = args.data.root.req.url
|
||||
const serverUrl = args.data.root.constants.SERVER_URL
|
||||
const locales = args.data.root.req.supportedLocales
|
||||
const links = [
|
||||
{
|
||||
title: "Home",
|
||||
stringId: "home",
|
||||
title: 'Home',
|
||||
stringId: 'home',
|
||||
href: `${serverUrl}/`,
|
||||
activeLink: (hostUrl === "/" || hostUrl === "/dashboard"),
|
||||
activeLink: (hostUrl === '/' || hostUrl === '/dashboard')
|
||||
},
|
||||
{
|
||||
title: "Breaches",
|
||||
stringId: "breaches",
|
||||
title: 'Breaches',
|
||||
stringId: 'breaches',
|
||||
href: `${serverUrl}/breaches`,
|
||||
activeLink: (hostUrl === "/breaches"),
|
||||
activeLink: (hostUrl === '/breaches')
|
||||
},
|
||||
{
|
||||
title: "Security Tips",
|
||||
stringId: "security-tips",
|
||||
title: 'Security Tips',
|
||||
stringId: 'security-tips',
|
||||
href: `${serverUrl}/security-tips`,
|
||||
activeLink: (hostUrl === "/security-tips"),
|
||||
},
|
||||
];
|
||||
const headerLinks = getStrings(links, locales);
|
||||
return headerLinks;
|
||||
activeLink: (hostUrl === '/security-tips')
|
||||
}
|
||||
]
|
||||
const headerLinks = getStrings(links, locales)
|
||||
return headerLinks
|
||||
}
|
||||
|
||||
function fxaMenuLinks(args) {
|
||||
const locales = args.data.root.req.supportedLocales;
|
||||
function fxaMenuLinks (args) {
|
||||
const locales = args.data.root.req.supportedLocales
|
||||
const fxaLinks = [
|
||||
{
|
||||
title: "Preferences",
|
||||
stringId: "preferences",
|
||||
href: "/user/preferences",
|
||||
title: 'Preferences',
|
||||
stringId: 'preferences',
|
||||
href: '/user/preferences'
|
||||
},
|
||||
{
|
||||
title: "Firefox Account",
|
||||
stringId: "fxa-account",
|
||||
href: getFxaUrl(),
|
||||
title: 'Firefox Account',
|
||||
stringId: 'fxa-account',
|
||||
href: getFxaUrl()
|
||||
},
|
||||
{
|
||||
title: "Sign Out",
|
||||
stringId: "sign-out",
|
||||
href: "/user/logout",
|
||||
},
|
||||
];
|
||||
title: 'Sign Out',
|
||||
stringId: 'sign-out',
|
||||
href: '/user/logout'
|
||||
}
|
||||
]
|
||||
|
||||
return getStrings(fxaLinks, locales);
|
||||
return getStrings(fxaLinks, locales)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
navLinks,
|
||||
fxaMenuLinks,
|
||||
getSignedInAs,
|
||||
};
|
||||
getSignedInAs
|
||||
}
|
||||
|
|
|
@ -1,36 +1,35 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const { LocaleUtils } = require("./../locale-utils");
|
||||
const { LocaleUtils } = require('./../locale-utils')
|
||||
|
||||
function makeLanding(args) {
|
||||
const locales = args.data.root.req.supportedLocales;
|
||||
const featuredBreach = args.data.root.featuredBreach;
|
||||
function makeLanding (args) {
|
||||
const locales = args.data.root.req.supportedLocales
|
||||
const featuredBreach = args.data.root.featuredBreach
|
||||
|
||||
const landingCopy = {};
|
||||
const landingCopy = {}
|
||||
|
||||
if (featuredBreach) {
|
||||
landingCopy.headline = LocaleUtils.fluentFormat(locales, "was-your-info-exposed", { breachName : `<span class="bold">${featuredBreach.Title}</span>` });
|
||||
landingCopy.headline = LocaleUtils.fluentFormat(locales, 'was-your-info-exposed', { breachName: `<span class="bold">${featuredBreach.Title}</span>` })
|
||||
landingCopy.info = [
|
||||
{
|
||||
subhead: LocaleUtils.fluentFormat(locales, "about-fxm-headline"),
|
||||
body: LocaleUtils.fluentFormat(locales, "about-fxm-blurb"),
|
||||
},
|
||||
];
|
||||
subhead: LocaleUtils.fluentFormat(locales, 'about-fxm-headline'),
|
||||
body: LocaleUtils.fluentFormat(locales, 'about-fxm-blurb')
|
||||
}
|
||||
]
|
||||
} else {
|
||||
landingCopy.headline = LocaleUtils.fluentFormat(locales, "see-if-youve-been-part");
|
||||
landingCopy.subhead = LocaleUtils.fluentFormat(locales, "find-out-what-hackers-know");
|
||||
landingCopy.headline = LocaleUtils.fluentFormat(locales, 'see-if-youve-been-part')
|
||||
landingCopy.subhead = LocaleUtils.fluentFormat(locales, 'find-out-what-hackers-know')
|
||||
}
|
||||
if (featuredBreach && featuredBreach.IsSensitive) {
|
||||
landingCopy.breachIsSensitive = true;
|
||||
landingCopy.breachIsSensitive = true
|
||||
landingCopy.info.unshift({
|
||||
subhead: LocaleUtils.fluentFormat(locales, "sensitive-sites"),
|
||||
body: LocaleUtils.fluentFormat(locales, "sensitive-sites-copy"),
|
||||
});
|
||||
subhead: LocaleUtils.fluentFormat(locales, 'sensitive-sites'),
|
||||
body: LocaleUtils.fluentFormat(locales, 'sensitive-sites-copy')
|
||||
})
|
||||
}
|
||||
|
||||
return args.fn(landingCopy);
|
||||
|
||||
return args.fn(landingCopy)
|
||||
}
|
||||
module.exports = {
|
||||
makeLanding,
|
||||
};
|
||||
makeLanding
|
||||
}
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const articles = require("./articles");
|
||||
const breachDetail = require("./breach-detail");
|
||||
const breaches = require("./breaches");
|
||||
const breachStats = require("./breach-stats");
|
||||
const dashboard = require("./dashboard");
|
||||
const emails = require("./emails");
|
||||
const footer = require("./footer");
|
||||
const header = require("./header");
|
||||
const homepage = require("./homepage");
|
||||
const legacyHelpers = require("./hbs-helpers");
|
||||
const scanResults = require("./scan-results");
|
||||
const signUpBanners = require("./sign-up-banners");
|
||||
const productEducationVideo = require("./product-education-video");
|
||||
const articles = require('./articles')
|
||||
const breachDetail = require('./breach-detail')
|
||||
const breaches = require('./breaches')
|
||||
const breachStats = require('./breach-stats')
|
||||
const dashboard = require('./dashboard')
|
||||
const emails = require('./emails')
|
||||
const footer = require('./footer')
|
||||
const header = require('./header')
|
||||
const homepage = require('./homepage')
|
||||
const legacyHelpers = require('./hbs-helpers')
|
||||
const scanResults = require('./scan-results')
|
||||
const signUpBanners = require('./sign-up-banners')
|
||||
const productEducationVideo = require('./product-education-video')
|
||||
|
||||
module.exports = {
|
||||
helpers: Object.assign(
|
||||
|
@ -29,5 +29,5 @@ module.exports = {
|
|||
scanResults,
|
||||
signUpBanners,
|
||||
productEducationVideo
|
||||
),
|
||||
};
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const AppConstants = require("../app-constants");
|
||||
const { getString } = require("./hbs-helpers");
|
||||
const AppConstants = require('../app-constants')
|
||||
const { getString } = require('./hbs-helpers')
|
||||
|
||||
function productEducation(type, args) {
|
||||
const headingTxt = type.toLowerCase() === "relay" ? getString("ad-unit-1-how-do-you-keep", args) : getString("ad-unit-2-do-you-worry", args);
|
||||
function productEducation (type, args) {
|
||||
const headingTxt = type.toLowerCase() === 'relay' ? getString('ad-unit-1-how-do-you-keep', args) : getString('ad-unit-2-do-you-worry', args)
|
||||
|
||||
return {
|
||||
headingTxt,
|
||||
videoSrc: AppConstants[`EDUCATION_VIDEO_URL_${type.toUpperCase()}`],
|
||||
};
|
||||
videoSrc: AppConstants[`EDUCATION_VIDEO_URL_${type.toUpperCase()}`]
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { productEducation };
|
||||
module.exports = { productEducation }
|
||||
|
|
|
@ -1,75 +1,73 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const AppConstants = require("./../app-constants");
|
||||
const AppConstants = require('./../app-constants')
|
||||
|
||||
const { localize } = require("./hbs-helpers");
|
||||
const { localize } = require('./hbs-helpers')
|
||||
|
||||
function productPromos(locales, promoUtms, promoKey) {
|
||||
function productPromos (locales, promoUtms, promoKey) {
|
||||
const productPromos = {
|
||||
"monitor": {
|
||||
promoHeadline: localize(locales, "monitor-promo-headline"),
|
||||
promoBody: localize(locales, "monitor-promo-body"),
|
||||
promoCta: localize(locales, "sign-up-for-alerts"),
|
||||
promoId: "promo-monitor",
|
||||
monitor: {
|
||||
promoHeadline: localize(locales, 'monitor-promo-headline'),
|
||||
promoBody: localize(locales, 'monitor-promo-body'),
|
||||
promoCta: localize(locales, 'sign-up-for-alerts'),
|
||||
promoId: 'promo-monitor',
|
||||
promoUrl: `${AppConstants.SERVER_URL}/oauth/init` + promoUtms,
|
||||
fxaEntrypoint: true,
|
||||
fxaEntrypoint: true
|
||||
},
|
||||
"fx-mobile": {
|
||||
promoHeadline: localize(locales, "mobile-promo-headline"),
|
||||
promoBody: localize(locales, "mobile-promo-body"),
|
||||
promoCta: localize(locales, "mobile-promo-cta"),
|
||||
promoId: "promo-mobile",
|
||||
promoUrl: "http://mozilla.org/firefox/mobile" + promoUtms,
|
||||
'fx-mobile': {
|
||||
promoHeadline: localize(locales, 'mobile-promo-headline'),
|
||||
promoBody: localize(locales, 'mobile-promo-body'),
|
||||
promoCta: localize(locales, 'mobile-promo-cta'),
|
||||
promoId: 'promo-mobile',
|
||||
promoUrl: 'http://mozilla.org/firefox/mobile' + promoUtms
|
||||
},
|
||||
"fpn": {
|
||||
promoHeadline: localize(locales, "fpn-promo-headline"),
|
||||
promoBody: localize(locales, "promo-fpn-body"),
|
||||
promoCta: localize(locales, "promo-fpn-cta"),
|
||||
promoId: "promo-fpn",
|
||||
promoUrl: "https://fpn.firefox.com" + promoUtms,
|
||||
fpn: {
|
||||
promoHeadline: localize(locales, 'fpn-promo-headline'),
|
||||
promoBody: localize(locales, 'promo-fpn-body'),
|
||||
promoCta: localize(locales, 'promo-fpn-cta'),
|
||||
promoId: 'promo-fpn',
|
||||
promoUrl: 'https://fpn.firefox.com' + promoUtms
|
||||
},
|
||||
"fx-ecosystem": {
|
||||
promoHeadline: localize(locales, "ecosystem-promo-headline"),
|
||||
promoBody: localize(locales, "ecosystem-promo-body"),
|
||||
promoCta: localize(locales, "promo-ecosystem-cta"),
|
||||
promoId: "promo-ecosystem",
|
||||
promoUrl: "https://www.mozilla.org/firefox" + promoUtms,
|
||||
},
|
||||
};
|
||||
if (productPromos[promoKey]) {
|
||||
return productPromos[promoKey];
|
||||
'fx-ecosystem': {
|
||||
promoHeadline: localize(locales, 'ecosystem-promo-headline'),
|
||||
promoBody: localize(locales, 'ecosystem-promo-body'),
|
||||
promoCta: localize(locales, 'promo-ecosystem-cta'),
|
||||
promoId: 'promo-ecosystem',
|
||||
promoUrl: 'https://www.mozilla.org/firefox' + promoUtms
|
||||
}
|
||||
}
|
||||
productPromos["fx-ecosystem"];
|
||||
if (productPromos[promoKey]) {
|
||||
return productPromos[promoKey]
|
||||
}
|
||||
productPromos['fx-ecosystem']
|
||||
}
|
||||
|
||||
|
||||
function getPromoStrings(args) {
|
||||
const templateData = args.data.root;
|
||||
const locales = templateData.req.supportedLocales;
|
||||
const breach = templateData.featuredBreach;
|
||||
const promoUtms = "?utm_source=fx-monitor&utm_medium=referral&utm_campaign=promo-banner&utm_content=desktop";
|
||||
function getPromoStrings (args) {
|
||||
const templateData = args.data.root
|
||||
const locales = templateData.req.supportedLocales
|
||||
const breach = templateData.featuredBreach
|
||||
const promoUtms = '?utm_source=fx-monitor&utm_medium=referral&utm_campaign=promo-banner&utm_content=desktop'
|
||||
|
||||
// show Monitor sign up promo if there is no signed in user
|
||||
if (!templateData.req.session.user) {
|
||||
return productPromos(locales, promoUtms, "monitor");
|
||||
return productPromos(locales, promoUtms, 'monitor')
|
||||
}
|
||||
|
||||
const userAgent = templateData.req.headers["user-agent"];
|
||||
const userAgent = templateData.req.headers['user-agent']
|
||||
const isBrowserFirefoxMobile = (
|
||||
(/Mobile/i.test(userAgent) && /Firefox/i.test(userAgent))||
|
||||
(/Mobile/i.test(userAgent) && /Firefox/i.test(userAgent)) ||
|
||||
/FxiOS/i.test(userAgent)
|
||||
);
|
||||
const PRODUCT_PROMOS_ENABLED = (AppConstants.PRODUCT_PROMOS_ENABLED === "1");
|
||||
)
|
||||
const PRODUCT_PROMOS_ENABLED = (AppConstants.PRODUCT_PROMOS_ENABLED === '1')
|
||||
if (PRODUCT_PROMOS_ENABLED) {
|
||||
|
||||
// show promo for mobile unless the user is on Firefox Mobile
|
||||
if (!isBrowserFirefoxMobile) {
|
||||
return productPromos(locales, promoUtms, "fx-mobile");
|
||||
return productPromos(locales, promoUtms, 'fx-mobile')
|
||||
}
|
||||
|
||||
// show promo for FPN if IP addresses were exposed
|
||||
if (breach.DataClasses.includes("ip-addresses") && locales[0] === "en") {
|
||||
return productPromos(locales, promoUtms, "fpn");
|
||||
// show promo for FPN if IP addresses were exposed
|
||||
if (breach.DataClasses.includes('ip-addresses') && locales[0] === 'en') {
|
||||
return productPromos(locales, promoUtms, 'fpn')
|
||||
}
|
||||
|
||||
// Don't show Lockwise banner until Monitor is whitelisted and UITour is implemented
|
||||
|
@ -80,9 +78,9 @@ function getPromoStrings(args) {
|
|||
|
||||
// Return generic promo for Firefox's family of products
|
||||
// by default if PRODUCT_PROMOS_ENABLED !-- "1"
|
||||
return productPromos(locales, promoUtms, "fx-ecosystem");
|
||||
return productPromos(locales, promoUtms, 'fx-ecosystem')
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getPromoStrings,
|
||||
};
|
||||
getPromoStrings
|
||||
}
|
||||
|
|
|
@ -1,276 +1,277 @@
|
|||
"use strict";
|
||||
|
||||
const { LocaleUtils } = require("./../locale-utils");
|
||||
'use strict'
|
||||
|
||||
const { LocaleUtils } = require('./../locale-utils')
|
||||
|
||||
module.exports = {
|
||||
getAllGenericRecommendations() {
|
||||
getAllGenericRecommendations () {
|
||||
return [
|
||||
{
|
||||
recommendationCopy: {
|
||||
subhead: "rec-gen-1-subhead",
|
||||
body: "rec-gen-1",
|
||||
cta: "rec-gen-1-cta",
|
||||
subhead: 'rec-gen-1-subhead',
|
||||
body: 'rec-gen-1',
|
||||
cta: 'rec-gen-1-cta'
|
||||
},
|
||||
ctaHref: "https://monitor.firefox.com/security-tips#strong-passwords",
|
||||
ctaHref: 'https://monitor.firefox.com/security-tips#strong-passwords',
|
||||
ctaShouldOpenNewTab: false,
|
||||
ctaAnalyticsId: "How to create strong passwords",
|
||||
recIconClassName: "rec-gen-1",
|
||||
ctaAnalyticsId: 'How to create strong passwords',
|
||||
recIconClassName: 'rec-gen-1'
|
||||
},
|
||||
{
|
||||
recommendationCopy: {
|
||||
subhead: "rec-gen-2-subhead",
|
||||
body: "rec-gen-2",
|
||||
cta: "rec-gen-2-cta",
|
||||
subhead: 'rec-gen-2-subhead',
|
||||
body: 'rec-gen-2',
|
||||
cta: 'rec-gen-2-cta'
|
||||
},
|
||||
ctaHref: "https://monitor.firefox.com/security-tips#five-myths",
|
||||
ctaHref: 'https://monitor.firefox.com/security-tips#five-myths',
|
||||
ctaShouldOpenNewTab: false,
|
||||
ctaAnalyticsId: "Myths about password managers",
|
||||
recIconClassName: "rec-gen-2",
|
||||
ctaAnalyticsId: 'Myths about password managers',
|
||||
recIconClassName: 'rec-gen-2'
|
||||
},
|
||||
{
|
||||
recommendationCopy: {
|
||||
subhead: "rec-gen-3-subhead",
|
||||
body: "rec-gen-3",
|
||||
cta: "rec-gen-3-cta",
|
||||
subhead: 'rec-gen-3-subhead',
|
||||
body: 'rec-gen-3',
|
||||
cta: 'rec-gen-3-cta'
|
||||
},
|
||||
ctaHref: "https://monitor.firefox.com/security-tips#steps-to-protect",
|
||||
ctaHref: 'https://monitor.firefox.com/security-tips#steps-to-protect',
|
||||
ctaShouldOpenNewTab: false,
|
||||
ctaAnalyticsId: "Read more security tips",
|
||||
recIconClassName: "rec-gen-3",
|
||||
ctaAnalyticsId: 'Read more security tips',
|
||||
recIconClassName: 'rec-gen-3'
|
||||
},
|
||||
{
|
||||
recommendationCopy: {
|
||||
subhead: "rec-gen-4-subhead",
|
||||
body: "rec-gen-4",
|
||||
subhead: 'rec-gen-4-subhead',
|
||||
body: 'rec-gen-4'
|
||||
},
|
||||
recIconClassName: "rec-gen-4",
|
||||
},
|
||||
];
|
||||
recIconClassName: 'rec-gen-4'
|
||||
}
|
||||
]
|
||||
},
|
||||
getAllPriorityDataClasses(isUserBrowserFirefox=false, isUserLocaleEnUs=false, isUserLocaleEn=false, changePWLink=null) {
|
||||
return {
|
||||
"government-issued-ids" : {
|
||||
getAllPriorityDataClasses (isUserBrowserFirefox = false, isUserLocaleEnUs = false, isUserLocaleEn = false, changePWLink = null) {
|
||||
return {
|
||||
'government-issued-ids': {
|
||||
weight: 101,
|
||||
pathToGlyph: "svg/glyphs/social-security-numbers",
|
||||
pathToGlyph: 'svg/glyphs/social-security-numbers'
|
||||
},
|
||||
"social-security-numbers" : {
|
||||
'social-security-numbers': {
|
||||
weight: 101,
|
||||
pathToGlyph: "svg/glyphs/social-security-numbers",
|
||||
recommendations: isUserLocaleEnUs ? [
|
||||
{
|
||||
recommendationCopy: {
|
||||
subhead: "rec-ssn-cta",
|
||||
cta: "rec-ssn-cta",
|
||||
body: "rec-ssn",
|
||||
},
|
||||
ctaHref: "https://www.annualcreditreport.com/index.action",
|
||||
ctaShouldOpenNewTab: true,
|
||||
ctaAnalyticsId: "Request credit reports",
|
||||
recIconClassName: "rec-ssn",
|
||||
},
|
||||
] : null,
|
||||
pathToGlyph: 'svg/glyphs/social-security-numbers',
|
||||
recommendations: isUserLocaleEnUs
|
||||
? [
|
||||
{
|
||||
recommendationCopy: {
|
||||
subhead: 'rec-ssn-cta',
|
||||
cta: 'rec-ssn-cta',
|
||||
body: 'rec-ssn'
|
||||
},
|
||||
ctaHref: 'https://www.annualcreditreport.com/index.action',
|
||||
ctaShouldOpenNewTab: true,
|
||||
ctaAnalyticsId: 'Request credit reports',
|
||||
recIconClassName: 'rec-ssn'
|
||||
}
|
||||
]
|
||||
: null
|
||||
},
|
||||
"passwords": {
|
||||
passwords: {
|
||||
weight: 100,
|
||||
pathToGlyph: "svg/glyphs/passwords",
|
||||
pathToGlyph: 'svg/glyphs/passwords',
|
||||
recommendations: [
|
||||
{
|
||||
recommendationCopy : {
|
||||
subhead: "rec-pw-1-subhead",
|
||||
cta: changePWLink ? "rec-pw-1-cta" : "",
|
||||
body: "rec-pw-1",
|
||||
recommendationCopy: {
|
||||
subhead: 'rec-pw-1-subhead',
|
||||
cta: changePWLink ? 'rec-pw-1-cta' : '',
|
||||
body: 'rec-pw-1'
|
||||
},
|
||||
ctaHref: changePWLink,
|
||||
ctaShouldOpenNewTab: true,
|
||||
ctaAnalyticsId: "Change password for this site",
|
||||
recIconClassName: "rec-pw-1",
|
||||
ctaAnalyticsId: 'Change password for this site',
|
||||
recIconClassName: 'rec-pw-1'
|
||||
},
|
||||
{
|
||||
recommendationCopy : {
|
||||
subhead: "rec-pw-2-subhead",
|
||||
recommendationCopy: {
|
||||
subhead: 'rec-pw-2-subhead',
|
||||
// Comment this CTA back in once monitor.firefox.com
|
||||
// has been added to the whitelist and is able to open about:logins
|
||||
// https://searchfox.org/mozilla-central/source/browser/app/permissions
|
||||
// cta: isUserBrowserFirefox ? "rec-pw-2-cta-fx" : "",
|
||||
body: "rec-pw-2",
|
||||
body: 'rec-pw-2'
|
||||
},
|
||||
recIconClassName: "rec-pw-2",
|
||||
recIconClassName: 'rec-pw-2'
|
||||
// ctaHref: "", // Will open about:logins in the future or the lockwise website.
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
"bank-account-numbers": {
|
||||
'bank-account-numbers': {
|
||||
weight: 99,
|
||||
pathToGlyph: "svg/glyphs/bank-account-numbers",
|
||||
pathToGlyph: 'svg/glyphs/bank-account-numbers',
|
||||
recommendations: [
|
||||
{
|
||||
recommendationCopy : {
|
||||
subhead: "rec-bank-acc-subhead",
|
||||
body: "rec-bank-acc",
|
||||
recommendationCopy: {
|
||||
subhead: 'rec-bank-acc-subhead',
|
||||
body: 'rec-bank-acc'
|
||||
},
|
||||
recIconClassName: "rec-bank-acc",
|
||||
},
|
||||
],
|
||||
recIconClassName: 'rec-bank-acc'
|
||||
}
|
||||
]
|
||||
},
|
||||
"credit-cards": {
|
||||
'credit-cards': {
|
||||
weight: 98,
|
||||
pathToGlyph: "svg/glyphs/credit-cards",
|
||||
pathToGlyph: 'svg/glyphs/credit-cards',
|
||||
recommendations: [
|
||||
{
|
||||
recommendationCopy : {
|
||||
subhead: "rec-cc-subhead",
|
||||
body: "rec-cc",
|
||||
recommendationCopy: {
|
||||
subhead: 'rec-cc-subhead',
|
||||
body: 'rec-cc'
|
||||
},
|
||||
recIconClassName: "rec-cc",
|
||||
},
|
||||
],
|
||||
recIconClassName: 'rec-cc'
|
||||
}
|
||||
]
|
||||
},
|
||||
"credit-card-cvv": {
|
||||
'credit-card-cvv': {
|
||||
weight: 97,
|
||||
pathToGlyph: "svg/glyphs/credit-card-cvvs",
|
||||
pathToGlyph: 'svg/glyphs/credit-card-cvvs'
|
||||
},
|
||||
"partial-credit-card-data": {
|
||||
'partial-credit-card-data': {
|
||||
weight: 96,
|
||||
pathToGlyph: "svg/glyphs/partial-credit-card-data",
|
||||
pathToGlyph: 'svg/glyphs/partial-credit-card-data',
|
||||
recommendations: [
|
||||
{
|
||||
recommendationCopy : {
|
||||
subhead: "rec-cc-subhead",
|
||||
body: "rec-cc",
|
||||
recommendationCopy: {
|
||||
subhead: 'rec-cc-subhead',
|
||||
body: 'rec-cc'
|
||||
},
|
||||
recIconClassName: "rec-cc",
|
||||
},
|
||||
],
|
||||
recIconClassName: 'rec-cc'
|
||||
}
|
||||
]
|
||||
},
|
||||
"ip-addresses": {
|
||||
'ip-addresses': {
|
||||
weight: 95,
|
||||
pathToGlyph: "svg/glyphs/ip-addresses",
|
||||
pathToGlyph: 'svg/glyphs/ip-addresses',
|
||||
recommendations: [
|
||||
{
|
||||
recommendationCopy: {
|
||||
subhead: "rec-ip-subhead",
|
||||
cta: isUserLocaleEnUs ? "rec-moz-vpn-cta" : "",
|
||||
body: isUserLocaleEnUs ? "rec-moz-vpn-update" : "rec-ip-non-us",
|
||||
subhead: 'rec-ip-subhead',
|
||||
cta: isUserLocaleEnUs ? 'rec-moz-vpn-cta' : '',
|
||||
body: isUserLocaleEnUs ? 'rec-moz-vpn-update' : 'rec-ip-non-us'
|
||||
},
|
||||
ctaHref: "https://vpn.mozilla.org?utm_source=monitor.firefox.com&utm_medium=referral&utm_campaign=monitor-recommendations",
|
||||
ctaHref: 'https://vpn.mozilla.org?utm_source=monitor.firefox.com&utm_medium=referral&utm_campaign=monitor-recommendations',
|
||||
ctaShouldOpenNewTab: true,
|
||||
ctaAnalyticsId: "Try Mozilla VPN",
|
||||
recIconClassName: isUserLocaleEnUs ? "rec-ip-us" : "rec-ip-non-us",
|
||||
},
|
||||
],
|
||||
ctaAnalyticsId: 'Try Mozilla VPN',
|
||||
recIconClassName: isUserLocaleEnUs ? 'rec-ip-us' : 'rec-ip-non-us'
|
||||
}
|
||||
]
|
||||
},
|
||||
"historical-passwords": {
|
||||
'historical-passwords': {
|
||||
weight: 94,
|
||||
pathToGlyph: "svg/glyphs/historical-passwords",
|
||||
pathToGlyph: 'svg/glyphs/historical-passwords',
|
||||
recommendations: [
|
||||
{
|
||||
recommendationCopy: {
|
||||
subhead: "rec-hist-pw-subhead",
|
||||
subhead: 'rec-hist-pw-subhead'
|
||||
// Comment back in once Monitor is able to open about:logins
|
||||
// cta: isUserBrowserFirefox ? "rec-hist-pw-cta-fx" : "",
|
||||
},
|
||||
recIconClassName: "rec-hist-pw",
|
||||
recIconClassName: 'rec-hist-pw'
|
||||
// Comment back in once Monitor is able to open about:logins
|
||||
// ctaHref: "about:logins",
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
"security-questions-and-answers": {
|
||||
'security-questions-and-answers': {
|
||||
weight: 93,
|
||||
pathToGlyph: "svg/glyphs/security-questions-and-answers",
|
||||
pathToGlyph: 'svg/glyphs/security-questions-and-answers',
|
||||
recommendations: [
|
||||
{
|
||||
recommendationCopy: {
|
||||
subhead: "rec-sec-qa-subhead",
|
||||
body: "rec-sec-qa",
|
||||
subhead: 'rec-sec-qa-subhead',
|
||||
body: 'rec-sec-qa'
|
||||
},
|
||||
recIconClassName: "rec-sec-qa",
|
||||
},
|
||||
],
|
||||
recIconClassName: 'rec-sec-qa'
|
||||
}
|
||||
]
|
||||
},
|
||||
"phone-numbers": {
|
||||
'phone-numbers': {
|
||||
weight: 92,
|
||||
pathToGlyph: "svg/glyphs/phone-numbers",
|
||||
pathToGlyph: 'svg/glyphs/phone-numbers',
|
||||
recommendations: [
|
||||
{
|
||||
recommendationCopy: {
|
||||
subhead: "rec-phone-num-subhead",
|
||||
body: "rec-phone-num",
|
||||
subhead: 'rec-phone-num-subhead',
|
||||
body: 'rec-phone-num'
|
||||
},
|
||||
recIconClassName: "rec-phone-num",
|
||||
},
|
||||
],
|
||||
recIconClassName: 'rec-phone-num'
|
||||
}
|
||||
]
|
||||
},
|
||||
"email-addresses": {
|
||||
'email-addresses': {
|
||||
weight: 91,
|
||||
pathToGlyph: "svg/glyphs/email-addresses",
|
||||
pathToGlyph: 'svg/glyphs/email-addresses',
|
||||
recommendations: [
|
||||
{
|
||||
recommendationCopy: {
|
||||
subhead: "rec-email-subhead",
|
||||
body: "rec-email",
|
||||
cta: "rec-email-cta",
|
||||
subhead: 'rec-email-subhead',
|
||||
body: 'rec-email',
|
||||
cta: 'rec-email-cta'
|
||||
},
|
||||
ctaHref: "https://relay.firefox.com/",
|
||||
ctaHref: 'https://relay.firefox.com/',
|
||||
ctaShouldOpenNewTab: true,
|
||||
ctaAnalyticsId: "Try Firefox Relay",
|
||||
recIconClassName: "rec-email",
|
||||
},
|
||||
],
|
||||
ctaAnalyticsId: 'Try Firefox Relay',
|
||||
recIconClassName: 'rec-email'
|
||||
}
|
||||
]
|
||||
},
|
||||
"dates-of-birth": {
|
||||
'dates-of-birth': {
|
||||
weight: 90,
|
||||
pathToGlyph: "svg/glyphs/dates-of-birth",
|
||||
pathToGlyph: 'svg/glyphs/dates-of-birth',
|
||||
recommendations: [
|
||||
{
|
||||
recommendationCopy: {
|
||||
subhead: "rec-dob-subhead",
|
||||
body: "rec-dob",
|
||||
subhead: 'rec-dob-subhead',
|
||||
body: 'rec-dob'
|
||||
},
|
||||
recIconClassName: "rec-dob",
|
||||
},
|
||||
],
|
||||
recIconClassName: 'rec-dob'
|
||||
}
|
||||
]
|
||||
},
|
||||
"pins": {
|
||||
pins: {
|
||||
weight: 89,
|
||||
pathToGlyph: "svg/glyphs/pins",
|
||||
pathToGlyph: 'svg/glyphs/pins',
|
||||
recommendations: [
|
||||
{
|
||||
recommendationCopy: {
|
||||
subhead: "rec-pins-subhead",
|
||||
body: "rec-pins",
|
||||
subhead: 'rec-pins-subhead',
|
||||
body: 'rec-pins'
|
||||
},
|
||||
recIconClassName: "rec-pins",
|
||||
},
|
||||
],
|
||||
recIconClassName: 'rec-pins'
|
||||
}
|
||||
]
|
||||
},
|
||||
"physical-addresses": {
|
||||
'physical-addresses': {
|
||||
weight: 88,
|
||||
pathToGlyph: "svg/glyphs/physical-addresses",
|
||||
pathToGlyph: 'svg/glyphs/physical-addresses',
|
||||
recommendations: [
|
||||
{
|
||||
recommendationCopy: {
|
||||
subhead: "rec-address-subhead",
|
||||
body: "rec-address",
|
||||
subhead: 'rec-address-subhead',
|
||||
body: 'rec-address'
|
||||
},
|
||||
recIconClassName: "rec-address",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
recIconClassName: 'rec-address'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
getFourthPasswordRecommendation(locales) {
|
||||
getFourthPasswordRecommendation (locales) {
|
||||
return {
|
||||
recommendationCopy: {
|
||||
subhead: LocaleUtils.fluentFormat(locales, "rec-pw-4-subhead"),
|
||||
body: LocaleUtils.fluentFormat(locales, "rec-pw-4"),
|
||||
cta: LocaleUtils.fluentFormat(locales, "rec-pw-4-cta"),
|
||||
subhead: LocaleUtils.fluentFormat(locales, 'rec-pw-4-subhead'),
|
||||
body: LocaleUtils.fluentFormat(locales, 'rec-pw-4'),
|
||||
cta: LocaleUtils.fluentFormat(locales, 'rec-pw-4-cta')
|
||||
},
|
||||
ctaHref: "https://2fa.directory/",
|
||||
ctaHref: 'https://2fa.directory/',
|
||||
ctaShouldOpenNewTab: true,
|
||||
ctaAnalyticsId: "See sites that offer 2FA",
|
||||
recIconClassName: "rec-pw-4",
|
||||
};
|
||||
},
|
||||
};
|
||||
ctaAnalyticsId: 'See sites that offer 2FA',
|
||||
recIconClassName: 'rec-pw-4'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,64 +1,63 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const { LocaleUtils } = require("./../locale-utils");
|
||||
const { fluentNestedBold, getString } = require("./hbs-helpers");
|
||||
const { LocaleUtils } = require('./../locale-utils')
|
||||
const { fluentNestedBold, getString } = require('./hbs-helpers')
|
||||
|
||||
function getScanResultsHeadline(args) {
|
||||
const locales = args.data.root.req.supportedLocales;
|
||||
const featuredBreach = args.data.root.specificBreach;
|
||||
const userCompromised = args.data.root.userCompromised;
|
||||
const foundBreaches = args.data.root.foundBreaches;
|
||||
function getScanResultsHeadline (args) {
|
||||
const locales = args.data.root.req.supportedLocales
|
||||
const featuredBreach = args.data.root.specificBreach
|
||||
const userCompromised = args.data.root.userCompromised
|
||||
const foundBreaches = args.data.root.foundBreaches
|
||||
|
||||
const headlineStrings = {
|
||||
"headline": "",
|
||||
"subhead": "",
|
||||
};
|
||||
headline: '',
|
||||
subhead: ''
|
||||
}
|
||||
|
||||
args.hash = {
|
||||
breachName: `<span class="bold">${featuredBreach.Title}</span>`,
|
||||
breachCount: foundBreaches.length,
|
||||
};
|
||||
breachCount: foundBreaches.length
|
||||
}
|
||||
|
||||
if (userCompromised) {
|
||||
if (foundBreaches.length === 1) {
|
||||
headlineStrings.headline = fluentNestedBold("fb-comp-only", args);
|
||||
headlineStrings.subhead = LocaleUtils.fluentFormat(locales, "no-other-breaches-found");
|
||||
return args.fn(headlineStrings);
|
||||
headlineStrings.headline = fluentNestedBold('fb-comp-only', args)
|
||||
headlineStrings.subhead = LocaleUtils.fluentFormat(locales, 'no-other-breaches-found')
|
||||
return args.fn(headlineStrings)
|
||||
}
|
||||
headlineStrings.headline = fluentNestedBold("fb-comp-and-others", args);
|
||||
return args.fn(headlineStrings);
|
||||
headlineStrings.headline = fluentNestedBold('fb-comp-and-others', args)
|
||||
return args.fn(headlineStrings)
|
||||
}
|
||||
|
||||
if (foundBreaches.length === 0) {
|
||||
headlineStrings.headline = fluentNestedBold("fb-not-comp", args);
|
||||
headlineStrings.subhead = LocaleUtils.fluentFormat(locales, "no-other-breaches-found");
|
||||
return args.fn(headlineStrings);
|
||||
headlineStrings.headline = fluentNestedBold('fb-not-comp', args)
|
||||
headlineStrings.subhead = LocaleUtils.fluentFormat(locales, 'no-other-breaches-found')
|
||||
return args.fn(headlineStrings)
|
||||
}
|
||||
|
||||
|
||||
headlineStrings.headline = fluentNestedBold("fb-not-comp", args);
|
||||
headlineStrings.subhead = fluentNestedBold("other-known-breaches-found", args);
|
||||
return args.fn(headlineStrings);
|
||||
headlineStrings.headline = fluentNestedBold('fb-not-comp', args)
|
||||
headlineStrings.subhead = fluentNestedBold('other-known-breaches-found', args)
|
||||
return args.fn(headlineStrings)
|
||||
}
|
||||
|
||||
function getFacebookResultMessage(id, args) {
|
||||
let message = getString(id, args);
|
||||
message = message.replace("<span>", "<span class='bold'>");
|
||||
let ctaHref = "";
|
||||
function getFacebookResultMessage (id, args) {
|
||||
let message = getString(id, args)
|
||||
message = message.replace('<span>', "<span class='bold'>")
|
||||
let ctaHref = ''
|
||||
switch (id) {
|
||||
case "facebook-breach-what-to-do-1-headline":
|
||||
ctaHref = "https://blog.mozilla.org/firefox/facebook-data-leak-explained/";
|
||||
break;
|
||||
case "facebook-breach-what-to-do-2-headline":
|
||||
ctaHref = "https://blog.mozilla.org/firefox/mozilla-explains-sim-swapping/";
|
||||
break;
|
||||
case 'facebook-breach-what-to-do-1-headline':
|
||||
ctaHref = 'https://blog.mozilla.org/firefox/facebook-data-leak-explained/'
|
||||
break
|
||||
case 'facebook-breach-what-to-do-2-headline':
|
||||
ctaHref = 'https://blog.mozilla.org/firefox/mozilla-explains-sim-swapping/'
|
||||
break
|
||||
}
|
||||
message = message.replace("<a>", `<a target="_blank" href="${ctaHref}">`);
|
||||
message = message.replace('<a>', `<a target="_blank" href="${ctaHref}">`)
|
||||
|
||||
return message;
|
||||
return message
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getScanResultsHeadline,
|
||||
getFacebookResultMessage,
|
||||
};
|
||||
getFacebookResultMessage
|
||||
}
|
||||
|
|
|
@ -1,64 +1,63 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const { LocaleUtils } = require("./../locale-utils");
|
||||
const { LocaleUtils } = require('./../locale-utils')
|
||||
|
||||
|
||||
function signUpBannerBulletPoints(args) {
|
||||
const locales = args.data.root.req.supportedLocales;
|
||||
function signUpBannerBulletPoints (args) {
|
||||
const locales = args.data.root.req.supportedLocales
|
||||
const bulletPoints = [
|
||||
{
|
||||
"title": "Enroll multiple emails in breach monitoring",
|
||||
"stringId": "feat-enroll-multiple",
|
||||
title: 'Enroll multiple emails in breach monitoring',
|
||||
stringId: 'feat-enroll-multiple'
|
||||
},
|
||||
{
|
||||
"title": "Advanced search in sensitive breaches",
|
||||
"stringId": "feat-sensitive",
|
||||
title: 'Advanced search in sensitive breaches',
|
||||
stringId: 'feat-sensitive'
|
||||
},
|
||||
{
|
||||
"string": "Security tips to protect your accounts",
|
||||
"stringId": "feat-security-tips",
|
||||
},
|
||||
];
|
||||
string: 'Security tips to protect your accounts',
|
||||
stringId: 'feat-security-tips'
|
||||
}
|
||||
]
|
||||
bulletPoints.forEach(bulletPoint => {
|
||||
bulletPoint["translatedString"] = LocaleUtils.fluentFormat(locales, bulletPoint["stringId"]);
|
||||
});
|
||||
return bulletPoints;
|
||||
bulletPoint.translatedString = LocaleUtils.fluentFormat(locales, bulletPoint.stringId)
|
||||
})
|
||||
return bulletPoints
|
||||
}
|
||||
|
||||
function monitorFeaturesList(args) {
|
||||
const locales = args.data.root.req.supportedLocales;
|
||||
function monitorFeaturesList (args) {
|
||||
const locales = args.data.root.req.supportedLocales
|
||||
const features = [
|
||||
{
|
||||
title: "Stay alert to new breaches",
|
||||
titleStringId: "stay-alert",
|
||||
title: 'Stay alert to new breaches',
|
||||
titleStringId: 'stay-alert',
|
||||
subtitle: "If your information surfaces in a new data breach, we'll send you an alert.",
|
||||
subtitleStringId: "if-your-info",
|
||||
pictogramPath: "alert",
|
||||
subtitleStringId: 'if-your-info',
|
||||
pictogramPath: 'alert'
|
||||
},
|
||||
{
|
||||
title: "Monitor several emails",
|
||||
titleStringId: "monitor-several-emails",
|
||||
subtitle: "Get ongoing breach monitoring for multiple email addresses.",
|
||||
subtitleStringId: "get-ongoing-breach-monitoring",
|
||||
pictogramPath: "email",
|
||||
title: 'Monitor several emails',
|
||||
titleStringId: 'monitor-several-emails',
|
||||
subtitle: 'Get ongoing breach monitoring for multiple email addresses.',
|
||||
subtitleStringId: 'get-ongoing-breach-monitoring',
|
||||
pictogramPath: 'email'
|
||||
},
|
||||
{
|
||||
title: "Protect your privacy",
|
||||
titleStringId: "protect-your-privacy",
|
||||
subtitle: "Find out what you need to do to keep your data safe from cyber criminals.",
|
||||
subtitleStringId: "keep-your-data-safe",
|
||||
pictogramPath: "advice",
|
||||
},
|
||||
];
|
||||
title: 'Protect your privacy',
|
||||
titleStringId: 'protect-your-privacy',
|
||||
subtitle: 'Find out what you need to do to keep your data safe from cyber criminals.',
|
||||
subtitleStringId: 'keep-your-data-safe',
|
||||
pictogramPath: 'advice'
|
||||
}
|
||||
]
|
||||
|
||||
features.forEach(feature => {
|
||||
feature.title = LocaleUtils.fluentFormat(locales, feature.titleStringId);
|
||||
feature.subtitle = LocaleUtils.fluentFormat(locales, feature.subtitleStringId);
|
||||
});
|
||||
return features;
|
||||
feature.title = LocaleUtils.fluentFormat(locales, feature.titleStringId)
|
||||
feature.subtitle = LocaleUtils.fluentFormat(locales, feature.subtitleStringId)
|
||||
})
|
||||
return features
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
monitorFeaturesList,
|
||||
signUpBannerBulletPoints,
|
||||
};
|
||||
signUpBannerBulletPoints
|
||||
}
|
||||
|
|
|
@ -1,24 +1,23 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const {vers, heartbeat} = require("../../controllers/dockerflow");
|
||||
const { vers, heartbeat } = require('../../controllers/dockerflow')
|
||||
|
||||
test('GET __version__ calls sendFile', () => {
|
||||
const mockRequest = {}
|
||||
const mockResponse = { sendFile: jest.fn() }
|
||||
|
||||
test("GET __version__ calls sendFile", () => {
|
||||
const mockRequest = {};
|
||||
const mockResponse = { sendFile: jest.fn() };
|
||||
vers(mockRequest, mockResponse)
|
||||
|
||||
vers(mockRequest, mockResponse);
|
||||
const mockSendFileCallArgs = mockResponse.sendFile.mock.calls[0]
|
||||
expect(mockSendFileCallArgs[0]).toContain('version.json')
|
||||
})
|
||||
|
||||
const mockSendFileCallArgs = mockResponse.sendFile.mock.calls[0];
|
||||
expect(mockSendFileCallArgs[0]).toContain("version.json");
|
||||
});
|
||||
test('GET __heartbeat__ calls send OK', () => {
|
||||
const mockRequest = {}
|
||||
const mockResponse = { send: jest.fn() }
|
||||
|
||||
test("GET __heartbeat__ calls send OK", () => {
|
||||
const mockRequest = {};
|
||||
const mockResponse = { send: jest.fn() };
|
||||
heartbeat(mockRequest, mockResponse)
|
||||
|
||||
heartbeat(mockRequest, mockResponse);
|
||||
|
||||
const mockSendCallArgs = mockResponse.send.mock.calls[0];
|
||||
expect(mockSendCallArgs[0]).toBe("OK");
|
||||
});
|
||||
const mockSendCallArgs = mockResponse.send.mock.calls[0]
|
||||
expect(mockSendCallArgs[0]).toBe('OK')
|
||||
})
|
||||
|
|
|
@ -1,140 +1,132 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const AppConstants = require("../../app-constants");
|
||||
const DB = require("../../db/DB");
|
||||
const HIBPLib = require("../../hibp");
|
||||
const hibp = require("../../controllers/hibp");
|
||||
const EmailUtils = require("../../email-utils");
|
||||
const { LocaleUtils } = require("../../locale-utils");
|
||||
const sha1 = require("../../sha1-utils");
|
||||
const AppConstants = require('../../app-constants')
|
||||
const DB = require('../../db/DB')
|
||||
const HIBPLib = require('../../hibp')
|
||||
const hibp = require('../../controllers/hibp')
|
||||
const EmailUtils = require('../../email-utils')
|
||||
const { LocaleUtils } = require('../../locale-utils')
|
||||
const sha1 = require('../../sha1-utils')
|
||||
|
||||
const { testBreaches } = require("../test-breaches");
|
||||
require("../resetDB");
|
||||
const { testBreaches } = require('../test-breaches')
|
||||
require('../resetDB')
|
||||
|
||||
test('notify POST without token should throw error', async () => {
|
||||
const testEmail = 'victim@spoofattack.com'
|
||||
const testHash = sha1(testEmail)
|
||||
const testPrefix = testHash.slice(0, 6).toUpperCase()
|
||||
const testSuffix = testHash.slice(6).toUpperCase()
|
||||
|
||||
test("notify POST without token should throw error", async() => {
|
||||
const testEmail = "victim@spoofattack.com";
|
||||
const testHash = sha1(testEmail);
|
||||
const testPrefix = testHash.slice(0, 6).toUpperCase();
|
||||
const testSuffix = testHash.slice(6).toUpperCase();
|
||||
const mockRequest = { body: { hashPrefix: testPrefix, hashSuffixes: [testSuffix], breachName: 'SomeSensitiveBreach' } }
|
||||
const mockResponse = { status: jest.fn(), json: jest.fn() }
|
||||
|
||||
const mockRequest = { body: { hashPrefix: testPrefix, hashSuffixes: [testSuffix], breachName: "SomeSensitiveBreach" } };
|
||||
const mockResponse = { status: jest.fn(), json: jest.fn() };
|
||||
await expect(hibp.notify(mockRequest, mockResponse)).rejects.toThrow('HIBP notify endpoint requires valid authorization token.')
|
||||
})
|
||||
|
||||
await expect(hibp.notify(mockRequest, mockResponse)).rejects.toThrow("HIBP notify endpoint requires valid authorization token.");
|
||||
});
|
||||
test('notify POST with invalid token should throw error', async () => {
|
||||
const testEmail = 'victim@spoofattack.com'
|
||||
const testHash = sha1(testEmail)
|
||||
const testPrefix = testHash.slice(0, 6).toUpperCase()
|
||||
const testSuffix = testHash.slice(6).toUpperCase()
|
||||
|
||||
const mockRequest = { token: 'token-that-doesnt-match-AppConstants', body: { hashPrefix: testPrefix, hashSuffixes: [testSuffix], breachName: 'SomeSensitiveBreach' } }
|
||||
const mockResponse = { status: jest.fn(), json: jest.fn() }
|
||||
|
||||
test("notify POST with invalid token should throw error", async() => {
|
||||
const testEmail = "victim@spoofattack.com";
|
||||
const testHash = sha1(testEmail);
|
||||
const testPrefix = testHash.slice(0, 6).toUpperCase();
|
||||
const testSuffix = testHash.slice(6).toUpperCase();
|
||||
await expect(hibp.notify(mockRequest, mockResponse)).rejects.toThrow('HIBP notify endpoint requires valid authorization token.')
|
||||
})
|
||||
|
||||
const mockRequest = { token: "token-that-doesnt-match-AppConstants", body: { hashPrefix: testPrefix, hashSuffixes: [testSuffix], breachName: "SomeSensitiveBreach" } };
|
||||
const mockResponse = { status: jest.fn(), json: jest.fn() };
|
||||
|
||||
await expect(hibp.notify(mockRequest, mockResponse)).rejects.toThrow("HIBP notify endpoint requires valid authorization token.");
|
||||
});
|
||||
|
||||
|
||||
async function checkNotifyCallsEverythingItShould(breachedEmail, recipientEmail) {
|
||||
async function checkNotifyCallsEverythingItShould (breachedEmail, recipientEmail) {
|
||||
if (recipientEmail === undefined) {
|
||||
recipientEmail = breachedEmail;
|
||||
recipientEmail = breachedEmail
|
||||
}
|
||||
jest.mock("../../email-utils");
|
||||
EmailUtils.sendEmail = jest.fn();
|
||||
LocaleUtils.fluentFormat = jest.fn();
|
||||
HIBPLib.getBreachesForEmail = jest.fn();
|
||||
jest.mock('../../email-utils')
|
||||
EmailUtils.sendEmail = jest.fn()
|
||||
LocaleUtils.fluentFormat = jest.fn()
|
||||
HIBPLib.getBreachesForEmail = jest.fn()
|
||||
|
||||
const testHash = sha1(breachedEmail);
|
||||
const testPrefix = testHash.slice(0, 6).toUpperCase();
|
||||
const testSuffix = testHash.slice(6).toUpperCase();
|
||||
const mockRequest = { token: AppConstants.HIBP_NOTIFY_TOKEN, body: { hashPrefix: testPrefix, hashSuffixes: [testSuffix], breachName: "Test" }, app: { locals: { breaches: testBreaches, AVAILABLE_LANGUAGES: ["en"] } } };
|
||||
const mockResponse = { status: jest.fn(), json: jest.fn() };
|
||||
const testHash = sha1(breachedEmail)
|
||||
const testPrefix = testHash.slice(0, 6).toUpperCase()
|
||||
const testSuffix = testHash.slice(6).toUpperCase()
|
||||
const mockRequest = { token: AppConstants.HIBP_NOTIFY_TOKEN, body: { hashPrefix: testPrefix, hashSuffixes: [testSuffix], breachName: 'Test' }, app: { locals: { breaches: testBreaches, AVAILABLE_LANGUAGES: ['en'] } } }
|
||||
const mockResponse = { status: jest.fn(), json: jest.fn() }
|
||||
|
||||
await hibp.notify(mockRequest, mockResponse);
|
||||
await hibp.notify(mockRequest, mockResponse)
|
||||
|
||||
const mockFluentFormatCalls = LocaleUtils.fluentFormat.mock.calls;
|
||||
expect (mockFluentFormatCalls.length).toBe(1);
|
||||
const mockFluentFormatCallArgs = mockFluentFormatCalls[0];
|
||||
expect (mockFluentFormatCallArgs[0]).toEqual(["en"]);
|
||||
expect (mockFluentFormatCallArgs[1]).toBe("breach-alert-subject");
|
||||
const mockFluentFormatCalls = LocaleUtils.fluentFormat.mock.calls
|
||||
expect(mockFluentFormatCalls.length).toBe(1)
|
||||
const mockFluentFormatCallArgs = mockFluentFormatCalls[0]
|
||||
expect(mockFluentFormatCallArgs[0]).toEqual(['en'])
|
||||
expect(mockFluentFormatCallArgs[1]).toBe('breach-alert-subject')
|
||||
|
||||
const mockSendEmailCalls = EmailUtils.sendEmail.mock.calls;
|
||||
expect (mockSendEmailCalls.length).toBe(1);
|
||||
const mockSendEmailCallArgs = mockSendEmailCalls[0];
|
||||
expect (mockSendEmailCallArgs[0]).toBe(recipientEmail);
|
||||
expect (mockSendEmailCallArgs[2]).toBe("default_email");
|
||||
const mockStatusCallArgs = mockResponse.status.mock.calls[0];
|
||||
expect(mockStatusCallArgs[0]).toBe(200);
|
||||
const mockJsonCallArgs = mockResponse.json.mock.calls[0];
|
||||
expect(mockJsonCallArgs[0].info).toContain("Notified");
|
||||
const mockSendEmailCalls = EmailUtils.sendEmail.mock.calls
|
||||
expect(mockSendEmailCalls.length).toBe(1)
|
||||
const mockSendEmailCallArgs = mockSendEmailCalls[0]
|
||||
expect(mockSendEmailCallArgs[0]).toBe(recipientEmail)
|
||||
expect(mockSendEmailCallArgs[2]).toBe('default_email')
|
||||
const mockStatusCallArgs = mockResponse.status.mock.calls[0]
|
||||
expect(mockStatusCallArgs[0]).toBe(200)
|
||||
const mockJsonCallArgs = mockResponse.json.mock.calls[0]
|
||||
expect(mockJsonCallArgs[0].info).toContain('Notified')
|
||||
}
|
||||
|
||||
test("good notify POST with breach, subscriber hash prefix and suffixes should call sendEmail and respond with 200", async () => {
|
||||
const testEmail = "verifiedemail@test.com";
|
||||
await checkNotifyCallsEverythingItShould(testEmail);
|
||||
});
|
||||
test('good notify POST with breach, subscriber hash prefix and suffixes should call sendEmail and respond with 200', async () => {
|
||||
const testEmail = 'verifiedemail@test.com'
|
||||
await checkNotifyCallsEverythingItShould(testEmail)
|
||||
})
|
||||
|
||||
test('good notify POST with breach, secondary email hash prefix and suffixes should call sendEmail and respond with 200', async () => {
|
||||
const testSecondaryEmail = 'firefoxaccount-secondary@test.com'
|
||||
await checkNotifyCallsEverythingItShould(testSecondaryEmail)
|
||||
})
|
||||
|
||||
test("good notify POST with breach, secondary email hash prefix and suffixes should call sendEmail and respond with 200", async () => {
|
||||
const testSecondaryEmail = "firefoxaccount-secondary@test.com";
|
||||
await checkNotifyCallsEverythingItShould(testSecondaryEmail);
|
||||
});
|
||||
|
||||
|
||||
test("good notify POST with breach, secondary email hash prefix and suffixes, all_emails_to_primary should call sendEmail to primary_email and respond with 200", async () => {
|
||||
const testBreachedEmail = "secondary_sending_to_primary@test.com";
|
||||
const expectedRecipientEmail = "all_emails_to_primary@test.com";
|
||||
await checkNotifyCallsEverythingItShould(testBreachedEmail, expectedRecipientEmail);
|
||||
});
|
||||
|
||||
test('good notify POST with breach, secondary email hash prefix and suffixes, all_emails_to_primary should call sendEmail to primary_email and respond with 200', async () => {
|
||||
const testBreachedEmail = 'secondary_sending_to_primary@test.com'
|
||||
const expectedRecipientEmail = 'all_emails_to_primary@test.com'
|
||||
await checkNotifyCallsEverythingItShould(testBreachedEmail, expectedRecipientEmail)
|
||||
})
|
||||
|
||||
// TODO: test("notify POST with unknown breach should successfully reload breaches")
|
||||
|
||||
test('notify POST with unknown breach should throw error', async () => {
|
||||
jest.mock('../../hibp')
|
||||
HIBPLib.loadBreachesIntoApp = jest.fn()
|
||||
const testEmail = 'test@example.com'
|
||||
const testHash = sha1(testEmail)
|
||||
const testPrefix = testHash.slice(0, 6).toUpperCase()
|
||||
const testSuffix = testHash.slice(6).toUpperCase()
|
||||
|
||||
test("notify POST with unknown breach should throw error", async () => {
|
||||
jest.mock("../../hibp");
|
||||
HIBPLib.loadBreachesIntoApp = jest.fn();
|
||||
const testEmail = "test@example.com";
|
||||
const testHash = sha1(testEmail);
|
||||
const testPrefix = testHash.slice(0, 6).toUpperCase();
|
||||
const testSuffix = testHash.slice(6).toUpperCase();
|
||||
const mockRequest = { token: AppConstants.HIBP_NOTIFY_TOKEN, body: { hashPrefix: testPrefix, hashSuffixes: [testSuffix], breachName: 'Test' }, app: { locals: { breaches: [] } } }
|
||||
const mockResponse = { status: jest.fn(), json: jest.fn() }
|
||||
|
||||
const mockRequest = { token: AppConstants.HIBP_NOTIFY_TOKEN, body: { hashPrefix: testPrefix, hashSuffixes: [testSuffix], breachName: "Test" }, app: { locals: { breaches: [] } } };
|
||||
const mockResponse = { status: jest.fn(), json: jest.fn() };
|
||||
await expect(hibp.notify(mockRequest, mockResponse)).rejects.toThrow('Unrecognized breach: test')
|
||||
})
|
||||
|
||||
await expect(hibp.notify(mockRequest, mockResponse)).rejects.toThrow("Unrecognized breach: test");
|
||||
});
|
||||
test('notify POST for subscriber with no signup_language should default to en', async () => {
|
||||
jest.mock('../../email-utils')
|
||||
jest.mock('../../hibp')
|
||||
EmailUtils.sendEmail = jest.fn()
|
||||
LocaleUtils.fluentFormat = jest.fn()
|
||||
HIBPLib.subscribeHash = jest.fn()
|
||||
|
||||
const testEmail = 'subscriberwithoutlanguage@test.com'
|
||||
|
||||
test("notify POST for subscriber with no signup_language should default to en", async () => {
|
||||
jest.mock("../../email-utils");
|
||||
jest.mock("../../hibp");
|
||||
EmailUtils.sendEmail = jest.fn();
|
||||
LocaleUtils.fluentFormat = jest.fn();
|
||||
HIBPLib.subscribeHash = jest.fn();
|
||||
await DB.addSubscriber(testEmail)
|
||||
|
||||
const testEmail = "subscriberwithoutlanguage@test.com";
|
||||
const testHash = sha1(testEmail)
|
||||
const testPrefix = testHash.slice(0, 6).toUpperCase()
|
||||
const testSuffix = testHash.slice(6).toUpperCase()
|
||||
|
||||
await DB.addSubscriber(testEmail);
|
||||
const mockRequest = { token: AppConstants.HIBP_NOTIFY_TOKEN, body: { hashPrefix: testPrefix, hashSuffixes: [testSuffix], breachName: 'Test' }, app: { locals: { breaches: testBreaches } } }
|
||||
const mockResponse = { status: jest.fn(), json: jest.fn() }
|
||||
|
||||
const testHash = sha1(testEmail);
|
||||
const testPrefix = testHash.slice(0, 6).toUpperCase();
|
||||
const testSuffix = testHash.slice(6).toUpperCase();
|
||||
await hibp.notify(mockRequest, mockResponse)
|
||||
|
||||
const mockRequest = { token: AppConstants.HIBP_NOTIFY_TOKEN, body: { hashPrefix: testPrefix, hashSuffixes: [testSuffix], breachName: "Test" }, app: { locals: { breaches: testBreaches } } };
|
||||
const mockResponse = { status: jest.fn(), json: jest.fn() };
|
||||
|
||||
await hibp.notify(mockRequest, mockResponse);
|
||||
|
||||
const mockSendEmailCalls = EmailUtils.sendEmail.mock.calls;
|
||||
expect (mockSendEmailCalls.length).toBe(1);
|
||||
const mockSendEmailCallArgs = mockSendEmailCalls[0];
|
||||
expect (mockSendEmailCallArgs[0]).toBe(testEmail);
|
||||
expect (mockSendEmailCallArgs[2]).toBe("default_email");
|
||||
const mockFluentFormatCalls = LocaleUtils.fluentFormat.mock.calls;
|
||||
const mockFluentFormatCallArgs = mockFluentFormatCalls[0];
|
||||
expect (mockFluentFormatCallArgs[0]).toEqual(["en"]);
|
||||
});
|
||||
const mockSendEmailCalls = EmailUtils.sendEmail.mock.calls
|
||||
expect(mockSendEmailCalls.length).toBe(1)
|
||||
const mockSendEmailCallArgs = mockSendEmailCalls[0]
|
||||
expect(mockSendEmailCallArgs[0]).toBe(testEmail)
|
||||
expect(mockSendEmailCallArgs[2]).toBe('default_email')
|
||||
const mockFluentFormatCalls = LocaleUtils.fluentFormat.mock.calls
|
||||
const mockFluentFormatCallArgs = mockFluentFormatCalls[0]
|
||||
expect(mockFluentFormatCallArgs[0]).toEqual(['en'])
|
||||
})
|
||||
|
|
|
@ -1,135 +1,131 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const AppConstants = require("../../app-constants");
|
||||
const home = require("../../controllers/home");
|
||||
const { getExperimentBranch } = require("../../controllers/utils");
|
||||
const { scanResult } = require("../../scan-results");
|
||||
const AppConstants = require('../../app-constants')
|
||||
const home = require('../../controllers/home')
|
||||
const { getExperimentBranch } = require('../../controllers/utils')
|
||||
const { scanResult } = require('../../scan-results')
|
||||
|
||||
let mockRequest = { fluentFormat: jest.fn(), csrfToken: jest.fn() };
|
||||
let mockRequest = { fluentFormat: jest.fn(), csrfToken: jest.fn() }
|
||||
|
||||
function mockRequestSessionReset(mockRequest) {
|
||||
function mockRequestSessionReset (mockRequest) {
|
||||
mockRequest.session.experimentFlags = {
|
||||
excludeFromExperiment: false,
|
||||
experimentBranch: false,
|
||||
treatmentBranch: false,
|
||||
controlBranch: false,
|
||||
};
|
||||
controlBranch: false
|
||||
}
|
||||
|
||||
mockRequest.headers = {
|
||||
"accept-language": "en",
|
||||
};
|
||||
'accept-language': 'en'
|
||||
}
|
||||
|
||||
return mockRequest;
|
||||
return mockRequest
|
||||
}
|
||||
|
||||
function addBreachesToMockRequest(mockRequest) {
|
||||
function addBreachesToMockRequest (mockRequest) {
|
||||
const mockBreaches = [
|
||||
{Name: "Test"},
|
||||
{Name: "DontShow"},
|
||||
];
|
||||
mockRequest.app = { locals: { breaches: mockBreaches } };
|
||||
return mockRequest;
|
||||
{ Name: 'Test' },
|
||||
{ Name: 'DontShow' }
|
||||
]
|
||||
mockRequest.app = { locals: { breaches: mockBreaches } }
|
||||
return mockRequest
|
||||
}
|
||||
|
||||
test("home GET without breach renders monitor without breach", () => {
|
||||
mockRequest.query = { breach: null };
|
||||
mockRequest = addBreachesToMockRequest(mockRequest);
|
||||
mockRequest.session = { user: null} ;
|
||||
const mockResponse = { render: jest.fn() };
|
||||
test('home GET without breach renders monitor without breach', () => {
|
||||
mockRequest.query = { breach: null }
|
||||
mockRequest = addBreachesToMockRequest(mockRequest)
|
||||
mockRequest.session = { user: null }
|
||||
const mockResponse = { render: jest.fn() }
|
||||
|
||||
home.home(mockRequest, mockResponse);
|
||||
home.home(mockRequest, mockResponse)
|
||||
|
||||
const mockRenderCallArgs = mockResponse.render.mock.calls[0];
|
||||
expect(mockRenderCallArgs[0]).toBe("monitor");
|
||||
expect(mockRenderCallArgs[1].featuredBreach).toBe(null);
|
||||
});
|
||||
const mockRenderCallArgs = mockResponse.render.mock.calls[0]
|
||||
expect(mockRenderCallArgs[0]).toBe('monitor')
|
||||
expect(mockRenderCallArgs[1].featuredBreach).toBe(null)
|
||||
})
|
||||
|
||||
test('home GET with breach renders monitor with breach', async () => {
|
||||
const testBreach = { Name: 'Test' }
|
||||
mockRequest.query = { breach: testBreach.Name }
|
||||
mockRequest = addBreachesToMockRequest(mockRequest)
|
||||
mockRequest.session = { user: null }
|
||||
mockRequest.url = { url: 'https://www.mozilla.com' }
|
||||
mockRequest.app.locals.SERVER_URL = AppConstants.SERVER_URL
|
||||
|
||||
test("home GET with breach renders monitor with breach", async() => {
|
||||
const testBreach = {Name: "Test"};
|
||||
mockRequest.query = { breach: testBreach.Name };
|
||||
mockRequest = addBreachesToMockRequest(mockRequest);
|
||||
mockRequest.session = { user: null };
|
||||
mockRequest.url = { url: "https://www.mozilla.com" };
|
||||
mockRequest.app.locals.SERVER_URL = AppConstants.SERVER_URL;
|
||||
const mockResponse = { render: jest.fn(), redirect: jest.fn() }
|
||||
home.home(mockRequest, mockResponse)
|
||||
const scanRes = await scanResult(mockRequest)
|
||||
|
||||
expect(scanRes.doorhangerScan).toBe(false)
|
||||
expect(scanRes.selfScan).toBe(false)
|
||||
const mockRenderCallArgs = mockResponse.render.mock.calls[0]
|
||||
expect(mockRenderCallArgs[0]).toBe('monitor')
|
||||
expect(mockRenderCallArgs[1].featuredBreach).toEqual(testBreach)
|
||||
})
|
||||
|
||||
const mockResponse = { render: jest.fn(), redirect: jest.fn() };
|
||||
home.home(mockRequest, mockResponse);
|
||||
const scanRes = await scanResult(mockRequest);
|
||||
test('notFound set status 404 and renders 404', () => {
|
||||
const mockResponse = { status: jest.fn(), render: jest.fn() }
|
||||
|
||||
expect(scanRes.doorhangerScan).toBe(false);
|
||||
expect(scanRes.selfScan).toBe(false);
|
||||
const mockRenderCallArgs = mockResponse.render.mock.calls[0];
|
||||
expect(mockRenderCallArgs[0]).toBe("monitor");
|
||||
expect(mockRenderCallArgs[1].featuredBreach).toEqual(testBreach);
|
||||
});
|
||||
home.notFound(mockRequest, mockResponse)
|
||||
|
||||
const mockStatusCallArgs = mockResponse.status.mock.calls[0]
|
||||
const mockRenderCallArgs = mockResponse.render.mock.calls[0]
|
||||
expect(mockStatusCallArgs[0]).toBe(404)
|
||||
expect(mockRenderCallArgs[0]).toBe('subpage')
|
||||
})
|
||||
|
||||
test("notFound set status 404 and renders 404", () => {
|
||||
const mockResponse = { status: jest.fn(), render: jest.fn() };
|
||||
|
||||
home.notFound(mockRequest, mockResponse);
|
||||
|
||||
const mockStatusCallArgs = mockResponse.status.mock.calls[0];
|
||||
const mockRenderCallArgs = mockResponse.render.mock.calls[0];
|
||||
expect(mockStatusCallArgs[0]).toBe(404);
|
||||
expect(mockRenderCallArgs[0]).toBe("subpage");
|
||||
});
|
||||
|
||||
test("Experiment Cohort Assignment Unit Test", () => {
|
||||
|
||||
test('Experiment Cohort Assignment Unit Test', () => {
|
||||
// Resets session and language after each test
|
||||
mockRequestSessionReset(mockRequest);
|
||||
mockRequestSessionReset(mockRequest)
|
||||
|
||||
// Set accept-language headers to German for first test.
|
||||
mockRequest.headers = {
|
||||
"accept-language": "de",
|
||||
};
|
||||
'accept-language': 'de'
|
||||
}
|
||||
|
||||
// For this test, the split is 25/25/50 (Control, Treatment, Excluded)
|
||||
const cohortVariation = {
|
||||
"va": 25,
|
||||
"vb": 25,
|
||||
};
|
||||
va: 25,
|
||||
vb: 25
|
||||
}
|
||||
|
||||
// The session is excluded from the experiment because the set language is German.
|
||||
let experimentBranch = getExperimentBranch(mockRequest, false, ["en"], cohortVariation);
|
||||
expect(experimentBranch).toBeFalsy();
|
||||
// The session is excluded from the experiment because the set language is German.
|
||||
let experimentBranch = getExperimentBranch(mockRequest, false, ['en'], cohortVariation)
|
||||
expect(experimentBranch).toBeFalsy()
|
||||
|
||||
mockRequestSessionReset(mockRequest);
|
||||
mockRequestSessionReset(mockRequest)
|
||||
|
||||
// The session is assigned to the control group when the coin flip is 0;
|
||||
let experimentNumber = 0;
|
||||
let experimentNumber = 0
|
||||
|
||||
experimentBranch = getExperimentBranch(mockRequest, experimentNumber, ["en"], cohortVariation);
|
||||
expect(experimentBranch).toBe("va");
|
||||
experimentBranch = getExperimentBranch(mockRequest, experimentNumber, ['en'], cohortVariation)
|
||||
expect(experimentBranch).toBe('va')
|
||||
|
||||
mockRequestSessionReset(mockRequest);
|
||||
mockRequestSessionReset(mockRequest)
|
||||
|
||||
// The session is assigned to the control group when the coin flip is 25;
|
||||
experimentNumber = 25;
|
||||
experimentBranch = getExperimentBranch(mockRequest, experimentNumber, ["en"], cohortVariation);
|
||||
expect(experimentBranch).toBe("va");
|
||||
experimentNumber = 25
|
||||
experimentBranch = getExperimentBranch(mockRequest, experimentNumber, ['en'], cohortVariation)
|
||||
expect(experimentBranch).toBe('va')
|
||||
|
||||
mockRequestSessionReset(mockRequest);
|
||||
mockRequestSessionReset(mockRequest)
|
||||
|
||||
// The session is assigned to the treatment group when the coin flip is 29;
|
||||
experimentNumber = 26;
|
||||
experimentBranch = getExperimentBranch(mockRequest, experimentNumber, ["en"], cohortVariation);
|
||||
expect(experimentBranch).toBe("vb");
|
||||
experimentNumber = 26
|
||||
experimentBranch = getExperimentBranch(mockRequest, experimentNumber, ['en'], cohortVariation)
|
||||
expect(experimentBranch).toBe('vb')
|
||||
|
||||
mockRequestSessionReset(mockRequest);
|
||||
mockRequestSessionReset(mockRequest)
|
||||
|
||||
// The session is assigned to the treatment group when the coin flip is 50;
|
||||
experimentNumber = 50;
|
||||
experimentBranch = getExperimentBranch(mockRequest, experimentNumber, ["en"], cohortVariation);
|
||||
expect(experimentBranch).toBe("vb");
|
||||
experimentNumber = 50
|
||||
experimentBranch = getExperimentBranch(mockRequest, experimentNumber, ['en'], cohortVariation)
|
||||
expect(experimentBranch).toBe('vb')
|
||||
|
||||
mockRequestSessionReset(mockRequest);
|
||||
mockRequestSessionReset(mockRequest)
|
||||
|
||||
// The session is excluded from the experiment when the coin flip is 100
|
||||
experimentNumber = 100;
|
||||
experimentBranch = getExperimentBranch(mockRequest, experimentNumber, ["en"], cohortVariation);
|
||||
expect(experimentBranch).toBeFalsy();
|
||||
});
|
||||
experimentNumber = 100
|
||||
experimentBranch = getExperimentBranch(mockRequest, experimentNumber, ['en'], cohortVariation)
|
||||
expect(experimentBranch).toBeFalsy()
|
||||
})
|
||||
|
|
|
@ -1,132 +1,123 @@
|
|||
"use strict";
|
||||
'use strict'
|
||||
|
||||
const got = require("got");
|
||||
const got = require('got')
|
||||
|
||||
const AppConstants = require("../../app-constants");
|
||||
const DB = require("../../db/DB");
|
||||
const EmailUtils = require("../../email-utils");
|
||||
const getSha1 = require("../../sha1-utils");
|
||||
const {init, confirmed} = require("../../controllers/oauth");
|
||||
const AppConstants = require('../../app-constants')
|
||||
const DB = require('../../db/DB')
|
||||
const EmailUtils = require('../../email-utils')
|
||||
const getSha1 = require('../../sha1-utils')
|
||||
const { init, confirmed } = require('../../controllers/oauth')
|
||||
|
||||
require("../resetDB");
|
||||
const {testBreaches} = require("../test-breaches");
|
||||
require('../resetDB')
|
||||
const { testBreaches } = require('../test-breaches')
|
||||
|
||||
jest.mock('got')
|
||||
|
||||
jest.mock("got");
|
||||
const mockRequest = { fluentFormat: jest.fn() }
|
||||
|
||||
test('init request sets session cookie and redirects with access_type=offline', () => {
|
||||
mockRequest.session = { }
|
||||
mockRequest.query = { }
|
||||
const mockResponse = { redirect: jest.fn() }
|
||||
|
||||
const mockRequest = { fluentFormat: jest.fn() };
|
||||
init(mockRequest, mockResponse)
|
||||
|
||||
const mockRedirectCallArgs = mockResponse.redirect.mock.calls[0]
|
||||
expect(mockRedirectCallArgs[0].href).toMatch(AppConstants.OAUTH_AUTHORIZATION_URI)
|
||||
expect(mockRedirectCallArgs[0].href).toMatch('access_type=offline')
|
||||
})
|
||||
|
||||
test("init request sets session cookie and redirects with access_type=offline", () => {
|
||||
mockRequest.session = { };
|
||||
mockRequest.query = { };
|
||||
const mockResponse = { redirect: jest.fn() };
|
||||
|
||||
init(mockRequest, mockResponse);
|
||||
|
||||
const mockRedirectCallArgs = mockResponse.redirect.mock.calls[0];
|
||||
expect(mockRedirectCallArgs[0].href).toMatch(AppConstants.OAUTH_AUTHORIZATION_URI);
|
||||
expect(mockRedirectCallArgs[0].href).toMatch("access_type=offline");
|
||||
});
|
||||
|
||||
|
||||
function getMockRequest(userAddLanguages = "en-US,en;q=0.5", sessionState="test-state") {
|
||||
function getMockRequest (userAddLanguages = 'en-US,en;q=0.5', sessionState = 'test-state') {
|
||||
return {
|
||||
app: { locals: { breaches: testBreaches } },
|
||||
headers: { "accept-language": userAddLanguages },
|
||||
headers: { 'accept-language': userAddLanguages },
|
||||
fluentFormat: jest.fn(),
|
||||
session: { state: sessionState },
|
||||
originalUrl: "",
|
||||
};
|
||||
originalUrl: ''
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
test("confirmed request checks session cookie, calls FXA for token and email, adds new subscriber with signup language, and redirects", async () => {
|
||||
const testFxAEmail = "fxa-new-user@test.com";
|
||||
const userAddLanguages = "en-US,en;q=0.5";
|
||||
const mockState = "123456789";
|
||||
EmailUtils.sendEmail = jest.fn();
|
||||
test('confirmed request checks session cookie, calls FXA for token and email, adds new subscriber with signup language, and redirects', async () => {
|
||||
const testFxAEmail = 'fxa-new-user@test.com'
|
||||
const userAddLanguages = 'en-US,en;q=0.5'
|
||||
const mockState = '123456789'
|
||||
EmailUtils.sendEmail = jest.fn()
|
||||
// Mock the getToken, got, and render calls
|
||||
const mockRequest = getMockRequest(userAddLanguages, mockState);
|
||||
mockRequest.query = { state: mockState };
|
||||
const mockResponse = { redirect: jest.fn()};
|
||||
const mockFxAClient = { code : { getToken: jest.fn().mockReturnValueOnce({ accessToken: "testToken"}) } };
|
||||
got.mockResolvedValue({ body: `{"email": "${testFxAEmail}"}` });
|
||||
const mockRequest = getMockRequest(userAddLanguages, mockState)
|
||||
mockRequest.query = { state: mockState }
|
||||
const mockResponse = { redirect: jest.fn() }
|
||||
const mockFxAClient = { code: { getToken: jest.fn().mockReturnValueOnce({ accessToken: 'testToken' }) } }
|
||||
got.mockResolvedValue({ body: `{"email": "${testFxAEmail}"}` })
|
||||
|
||||
await confirmed(mockRequest, mockResponse, () => {}, mockFxAClient);
|
||||
await confirmed(mockRequest, mockResponse, () => {}, mockFxAClient)
|
||||
|
||||
const mockFxACallArgs = mockFxAClient.code.getToken.mock.calls[0];
|
||||
expect(mockFxACallArgs[0]).toBe(mockRequest.originalUrl);
|
||||
expect(mockFxACallArgs[1]).toEqual({state: mockState});
|
||||
expect(mockRequest.session.state).toBeNull();
|
||||
const mockGotCallArgs = got.mock.calls[0];
|
||||
expect(mockGotCallArgs[0]).toMatch(AppConstants.OAUTH_PROFILE_URI);
|
||||
expect(mockGotCallArgs[1].headers.Authorization).toMatch("testToken");
|
||||
const mockFxACallArgs = mockFxAClient.code.getToken.mock.calls[0]
|
||||
expect(mockFxACallArgs[0]).toBe(mockRequest.originalUrl)
|
||||
expect(mockFxACallArgs[1]).toEqual({ state: mockState })
|
||||
expect(mockRequest.session.state).toBeNull()
|
||||
const mockGotCallArgs = got.mock.calls[0]
|
||||
expect(mockGotCallArgs[0]).toMatch(AppConstants.OAUTH_PROFILE_URI)
|
||||
expect(mockGotCallArgs[1].headers.Authorization).toMatch('testToken')
|
||||
|
||||
const subscribers = await DB.getSubscribersByHashes([getSha1(testFxAEmail)]);
|
||||
expect(subscribers[0].primary_verified).toBeTruthy();
|
||||
expect(subscribers[0].primary_email).toBe(testFxAEmail);
|
||||
expect(subscribers[0].signup_language).toBe(userAddLanguages);
|
||||
const subscribers = await DB.getSubscribersByHashes([getSha1(testFxAEmail)])
|
||||
expect(subscribers[0].primary_verified).toBeTruthy()
|
||||
expect(subscribers[0].primary_email).toBe(testFxAEmail)
|
||||
expect(subscribers[0].signup_language).toBe(userAddLanguages)
|
||||
|
||||
const mockRedirectCallArgs = mockResponse.redirect.mock.calls[0];
|
||||
expect(mockRedirectCallArgs[0]).toBe("/user/dashboard");
|
||||
});
|
||||
const mockRedirectCallArgs = mockResponse.redirect.mock.calls[0]
|
||||
expect(mockRedirectCallArgs[0]).toBe('/user/dashboard')
|
||||
})
|
||||
|
||||
|
||||
test("confirmed request checks session cookie, calls FXA for token and email, recognizes existing subscriber and redirects", async () => {
|
||||
EmailUtils.sendEmail = jest.fn();
|
||||
const mockState = "123456789";
|
||||
const userAddLanguages = "en-US,en;q=0.5";
|
||||
const mockRequest = getMockRequest(userAddLanguages, mockState);
|
||||
mockRequest.query = { state: mockState };
|
||||
const mockResponse = { redirect: jest.fn() };
|
||||
const mockFxAClient = { code : { getToken: jest.fn().mockReturnValueOnce({ accessToken: "testToken"}) } };
|
||||
test('confirmed request checks session cookie, calls FXA for token and email, recognizes existing subscriber and redirects', async () => {
|
||||
EmailUtils.sendEmail = jest.fn()
|
||||
const mockState = '123456789'
|
||||
const userAddLanguages = 'en-US,en;q=0.5'
|
||||
const mockRequest = getMockRequest(userAddLanguages, mockState)
|
||||
mockRequest.query = { state: mockState }
|
||||
const mockResponse = { redirect: jest.fn() }
|
||||
const mockFxAClient = { code: { getToken: jest.fn().mockReturnValueOnce({ accessToken: 'testToken' }) } }
|
||||
|
||||
/* eslint-disable quotes */
|
||||
got.mockResolvedValue({ body: `{"email": "firefoxaccount@test.com"}` });
|
||||
/*eslint-enable quotes*/
|
||||
got.mockResolvedValue({ body: `{"email": "firefoxaccount@test.com"}` })
|
||||
/* eslint-enable quotes */
|
||||
|
||||
await confirmed(mockRequest, mockResponse, () => {}, mockFxAClient);
|
||||
await confirmed(mockRequest, mockResponse, () => {}, mockFxAClient)
|
||||
|
||||
const mockFxACallArgs = mockFxAClient.code.getToken.mock.calls[0];
|
||||
expect(mockFxACallArgs[0]).toBe(mockRequest.originalUrl);
|
||||
expect(mockFxACallArgs[1]).toEqual({state: mockState});
|
||||
expect(mockRequest.session.state).toBeNull();
|
||||
const mockFxACallArgs = mockFxAClient.code.getToken.mock.calls[0]
|
||||
expect(mockFxACallArgs[0]).toBe(mockRequest.originalUrl)
|
||||
expect(mockFxACallArgs[1]).toEqual({ state: mockState })
|
||||
expect(mockRequest.session.state).toBeNull()
|
||||
|
||||
const mockGotCallArgs = got.mock.calls[0];
|
||||
expect(mockGotCallArgs[0]).toMatch(AppConstants.OAUTH_PROFILE_URI);
|
||||
expect(mockGotCallArgs[1].headers.Authorization).toMatch("testToken");
|
||||
const mockGotCallArgs = got.mock.calls[0]
|
||||
expect(mockGotCallArgs[0]).toMatch(AppConstants.OAUTH_PROFILE_URI)
|
||||
expect(mockGotCallArgs[1].headers.Authorization).toMatch('testToken')
|
||||
|
||||
const mockRedirectCallArgs = mockResponse.redirect.mock.calls[0];
|
||||
expect(mockRedirectCallArgs[0]).toMatch("/");
|
||||
});
|
||||
const mockRedirectCallArgs = mockResponse.redirect.mock.calls[0]
|
||||
expect(mockRedirectCallArgs[0]).toMatch('/')
|
||||
})
|
||||
|
||||
test('confirmed request without session state cookie throws Error', async () => {
|
||||
mockRequest.session = {}
|
||||
const mockResponse = {}
|
||||
|
||||
test("confirmed request without session state cookie throws Error", async () => {
|
||||
mockRequest.session = {};
|
||||
const mockResponse = {};
|
||||
await expect(confirmed(mockRequest, mockResponse)).rejects.toThrowError('oauth-invalid-session')
|
||||
})
|
||||
|
||||
await expect(confirmed(mockRequest, mockResponse)).rejects.toThrowError("oauth-invalid-session");
|
||||
});
|
||||
|
||||
|
||||
test("confirmed request with no session state cookie throws Error", async () => {
|
||||
test('confirmed request with no session state cookie throws Error', async () => {
|
||||
// Mock request, but don't mock the getToken call to trigger the client-oauth2 error
|
||||
mockRequest.session = { state: { } };
|
||||
mockRequest.originalUrl = "";
|
||||
const mockResponse = {};
|
||||
mockRequest.session = { state: { } }
|
||||
mockRequest.originalUrl = ''
|
||||
const mockResponse = {}
|
||||
|
||||
await expect(confirmed(mockRequest, mockResponse)).rejects.toThrow("oauth-invalid-session");
|
||||
});
|
||||
await expect(confirmed(mockRequest, mockResponse)).rejects.toThrow('oauth-invalid-session')
|
||||
})
|
||||
|
||||
|
||||
test("confirmed request with bad session state cookie throws Error", async () => {
|
||||
test('confirmed request with bad session state cookie throws Error', async () => {
|
||||
// Mock request, but don't mock the getToken call to trigger the client-oauth2 error
|
||||
mockRequest.session = { state: 12345 };
|
||||
mockRequest.query = { state: 67890 };
|
||||
mockRequest.originalUrl = "";
|
||||
const mockResponse = {};
|
||||
mockRequest.session = { state: 12345 }
|
||||
mockRequest.query = { state: 67890 }
|
||||
mockRequest.originalUrl = ''
|
||||
const mockResponse = {}
|
||||
|
||||
await expect(confirmed(mockRequest, mockResponse)).rejects.toThrow("oauth-invalid-session");
|
||||
});
|
||||
await expect(confirmed(mockRequest, mockResponse)).rejects.toThrow('oauth-invalid-session')
|
||||
})
|
||||
|
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче