From 344e394bb0e47f86c3b6e1ab923a48aa427ba442 Mon Sep 17 00:00:00 2001 From: Amri Toufali Date: Tue, 10 May 2022 16:40:26 -0700 Subject: [PATCH] add ESLint auto-fixable changes (quotes, semi...) --- __mocks__/sns-validator.js | 16 +- app-constants.js | 120 +-- controllers/breach-details.js | 58 +- controllers/dockerflow.js | 47 +- controllers/email-l10n.js | 132 +-- controllers/hibp.js | 131 ++- controllers/home.js | 184 ++-- controllers/ip-location.js | 42 +- controllers/oauth.js | 106 +- controllers/scan.js | 94 +- controllers/ses.js | 111 +-- controllers/user.js | 613 ++++++------ controllers/utils.js | 245 +++-- db/DB.js | 509 +++++----- db/knexfile.js | 38 +- .../20180418090800_initial_schema.js | 22 +- ...826102013_add_timestamps_to_subscribers.js | 22 +- ...20180829161115_add_fx_newsletter_column.js | 22 +- .../20180930071926_add_signup_language.js | 22 +- .../20181007085241_add_sha1_index.js | 23 +- .../20181108151941_add_created_at_index.js | 22 +- .../20181129152508_add_email_index.js | 22 +- .../20181227100332_add_fxa_columns.js | 26 +- .../20190117150910_add_verified_index.js | 23 +- .../20190219154519_add_fxa_uid_column.js | 22 +- ...0190328111900_add_email_addresses_table.js | 54 +- ...422140308_add_subscriber_breaches_shown.js | 22 +- ...52733_add_timestamps_to_email_addresses.js | 22 +- ...170106_add_all_emails_to_primary_column.js | 23 +- ...919_add_fxa_access_token_to_subscribers.js | 22 +- .../20190713193852_add_email_sha1_index.js | 23 +- .../20191118100718_add-fxa-uid-index.js | 23 +- ...8170713_add-email_addresses-email-index.js | 23 +- ...1202161125_add_breaches_resolved_column.js | 22 +- .../20200220143251_add-waitlists-column.js | 22 +- ...d-subscribers-breaches_last_shown-index.js | 23 +- ...0810144851_add_signup_language_index.js.js | 23 +- db/seeds/test_subscribers.js | 79 +- email-utils.js | 143 ++- gulpfile.js | 60 +- hibp.js | 204 ++-- ip-location-service.js | 60 +- lib/changePWLinks.js | 28 +- lib/fxa.js | 118 ++- lib/remote-settings.js | 50 +- locale-utils.js | 109 +- log.js | 15 +- middleware.js | 247 +++-- public/js/all-breaches/all-breaches.js | 250 ++--- public/js/analytics_dnt-helper.js | 69 +- public/js/dashboard.js | 139 ++- public/js/fx-bento.js | 394 ++++---- public/js/fxa-analytics.js | 262 +++-- public/js/fxa-menu.js | 87 +- public/js/monitor.js | 644 ++++++------ public/js/resolve-breaches.js | 194 ++-- public/js/scan-email.js | 48 +- public/js/scan-results.js | 10 +- routes/breach-details.js | 16 +- routes/dockerflow.js | 20 +- routes/email-l10n.js | 15 +- routes/hibp.js | 25 +- routes/home.js | 52 +- routes/oauth.js | 21 +- routes/scan.js | 25 +- routes/ses.js | 29 +- routes/user.js | 59 +- scan-results.js | 132 ++- scripts/breach-stats.js | 68 +- .../collect-unresolved-breaches-addresses.js | 74 +- scripts/delete-unverified-subscribers.js | 11 +- scripts/delete-user.js | 36 +- scripts/lowercase-all-records.js | 67 +- scripts/make-sha1-hashes.js | 22 +- scripts/send-breach-notification.js | 67 +- scripts/send-email-to-pre-fxa-subscribers.js | 78 +- scripts/subscribe-lowercase-hashes.js | 96 +- scripts/updatebreaches.js | 47 +- server.js | 285 +++--- sha1-utils.js | 10 +- template-helpers/articles.js | 677 ++++++------- template-helpers/breach-detail.js | 305 +++--- template-helpers/breach-stats.js | 117 ++- template-helpers/breaches.js | 179 ++-- template-helpers/dashboard.js | 218 ++-- template-helpers/emails.js | 348 ++++--- template-helpers/footer.js | 92 +- template-helpers/hbs-helpers.js | 247 +++-- template-helpers/header.js | 83 +- template-helpers/homepage.js | 41 +- template-helpers/index.js | 32 +- template-helpers/product-education-video.js | 16 +- template-helpers/product-promos.js | 102 +- template-helpers/recommendations.js | 319 +++--- template-helpers/scan-results.js | 77 +- template-helpers/sign-up-banners.js | 79 +- tests/controllers/dockerflow.test.js | 33 +- tests/controllers/hibp.test.js | 210 ++-- tests/controllers/home.test.js | 170 ++-- tests/controllers/oauth.test.js | 185 ++-- tests/controllers/scan.test.js | 94 +- tests/controllers/ses.test.js | 201 ++-- tests/controllers/user.test.js | 940 +++++++++--------- tests/db.test.js | 310 +++--- tests/email.test.js | 85 +- tests/fxa.test.js | 40 +- tests/hbs-helpers.test.js | 61 +- tests/hibp.test.js | 105 +- .../pages/desktop/dashboard.page.js | 25 +- tests/integration/pages/desktop/home.page.js | 27 +- .../pages/desktop/scanResults.page.js | 20 +- .../pages/desktop/userPreferences.page.js | 11 +- tests/integration/regions/navbar.region.js | 8 +- tests/integration/tests/test-breaches-page.js | 26 +- tests/integration/tests/test-home-page.js | 51 +- .../integration/utils/test-create-baseline.js | 39 +- tests/integration/wdio.conf.js | 416 ++++---- tests/integration/wdio.docker.js | 48 +- tests/jest.setup.js | 4 +- tests/middleware.test.js | 114 +-- tests/resetDB.js | 35 +- tests/sha1.test.js | 18 +- tests/test-breaches.js | 46 +- 123 files changed, 6681 insertions(+), 7012 deletions(-) diff --git a/__mocks__/sns-validator.js b/__mocks__/sns-validator.js index c6a4cb693..c088cab7d 100644 --- a/__mocks__/sns-validator.js +++ b/__mocks__/sns-validator.js @@ -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 diff --git a/app-constants.js b/app-constants.js index a99c8776e..97d6474e6 100644 --- a/app-constants.js +++ b/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) diff --git a/controllers/breach-details.js b/controllers/breach-details.js index bf9e2fad9..82b1c82c3 100644 --- a/controllers/breach-details.js +++ b/controllers/breach-details.js @@ -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 +} diff --git a/controllers/dockerflow.js b/controllers/dockerflow.js index a5f5e4ef9..36f1baccd 100644 --- a/controllers/dockerflow.js +++ b/controllers/dockerflow.js @@ -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 +} diff --git a/controllers/email-l10n.js b/controllers/email-l10n.js index ad4304177..fdd9c9a92 100644 --- a/controllers/email-l10n.js +++ b/controllers/email-l10n.js @@ -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 +} diff --git a/controllers/hibp.js b/controllers/hibp.js index c28949b78..3d2dc112e 100644 --- a/controllers/hibp.js +++ b/controllers/hibp.js @@ -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 +} diff --git a/controllers/home.js b/controllers/home.js index 619bde2c1..58380863c 100644 --- a/controllers/home.js +++ b/controllers/home.js @@ -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 +} diff --git a/controllers/ip-location.js b/controllers/ip-location.js index b443c4520..4a8c831a7 100644 --- a/controllers/ip-location.js +++ b/controllers/ip-location.js @@ -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 +} diff --git a/controllers/oauth.js b/controllers/oauth.js index e687212c0..6d2acef9b 100644 --- a/controllers/oauth.js +++ b/controllers/oauth.js @@ -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 +} diff --git a/controllers/scan.js b/controllers/scan.js index 335d4d8b2..a43f50976 100644 --- a/controllers/scan.js +++ b/controllers/scan.js @@ -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 +} diff --git a/controllers/ses.js b/controllers/ses.js index 11e39dde7..378572926 100644 --- a/controllers/ses.js +++ b/controllers/ses.js @@ -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 +} diff --git a/controllers/user.js b/controllers/user.js index 2c7dad4d1..07a810952 100644 --- a/controllers/user.js +++ b/controllers/user.js @@ -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 +} diff --git a/controllers/utils.js b/controllers/utils.js index b97fb941b..ad4e8c6f2 100644 --- a/controllers/utils.js +++ b/controllers/utils.js @@ -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 +} diff --git a/db/DB.js b/db/DB.js index cbae63737..ce8a4a4ad 100644 --- a/db/DB.js +++ b/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 diff --git a/db/knexfile.js b/db/knexfile.js index d09962fe1..148e16e2b 100644 --- a/db/knexfile.js +++ b/db/knexfile.js @@ -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 } diff --git a/db/migrations/20180418090800_initial_schema.js b/db/migrations/20180418090800_initial_schema.js index b477bafe4..032603bd9 100644 --- a/db/migrations/20180418090800_initial_schema.js +++ b/db/migrations/20180418090800_initial_schema.js @@ -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') +} diff --git a/db/migrations/20180826102013_add_timestamps_to_subscribers.js b/db/migrations/20180826102013_add_timestamps_to_subscribers.js index ee3085bdd..88b7e7b45 100644 --- a/db/migrations/20180826102013_add_timestamps_to_subscribers.js +++ b/db/migrations/20180826102013_add_timestamps_to_subscribers.js @@ -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() + }) +} diff --git a/db/migrations/20180829161115_add_fx_newsletter_column.js b/db/migrations/20180829161115_add_fx_newsletter_column.js index 237414d6d..0ac569928 100644 --- a/db/migrations/20180829161115_add_fx_newsletter_column.js +++ b/db/migrations/20180829161115_add_fx_newsletter_column.js @@ -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') + }) +} diff --git a/db/migrations/20180930071926_add_signup_language.js b/db/migrations/20180930071926_add_signup_language.js index ab820cc56..ca64ed916 100644 --- a/db/migrations/20180930071926_add_signup_language.js +++ b/db/migrations/20180930071926_add_signup_language.js @@ -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') + }) +} diff --git a/db/migrations/20181007085241_add_sha1_index.js b/db/migrations/20181007085241_add_sha1_index.js index 5637dda9f..f06b5448e 100644 --- a/db/migrations/20181007085241_add_sha1_index.js +++ b/db/migrations/20181007085241_add_sha1_index.js @@ -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') + }) +} diff --git a/db/migrations/20181108151941_add_created_at_index.js b/db/migrations/20181108151941_add_created_at_index.js index 4c49b154e..f71f2d0ed 100644 --- a/db/migrations/20181108151941_add_created_at_index.js +++ b/db/migrations/20181108151941_add_created_at_index.js @@ -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') + }) +} diff --git a/db/migrations/20181129152508_add_email_index.js b/db/migrations/20181129152508_add_email_index.js index 7b515cc61..cbc7cd661 100644 --- a/db/migrations/20181129152508_add_email_index.js +++ b/db/migrations/20181129152508_add_email_index.js @@ -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') + }) +} diff --git a/db/migrations/20181227100332_add_fxa_columns.js b/db/migrations/20181227100332_add_fxa_columns.js index 7b3c2553b..21c312149 100644 --- a/db/migrations/20181227100332_add_fxa_columns.js +++ b/db/migrations/20181227100332_add_fxa_columns.js @@ -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') + }) +} diff --git a/db/migrations/20190117150910_add_verified_index.js b/db/migrations/20190117150910_add_verified_index.js index c7e38e5b2..7485554aa 100644 --- a/db/migrations/20190117150910_add_verified_index.js +++ b/db/migrations/20190117150910_add_verified_index.js @@ -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') + }) +} diff --git a/db/migrations/20190219154519_add_fxa_uid_column.js b/db/migrations/20190219154519_add_fxa_uid_column.js index 80b4d199e..ab166229c 100644 --- a/db/migrations/20190219154519_add_fxa_uid_column.js +++ b/db/migrations/20190219154519_add_fxa_uid_column.js @@ -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') + }) +} diff --git a/db/migrations/20190328111900_add_email_addresses_table.js b/db/migrations/20190328111900_add_email_addresses_table.js index 2ae63f829..d1ab68857 100644 --- a/db/migrations/20190328111900_add_email_addresses_table.js +++ b/db/migrations/20190328111900_add_email_addresses_table.js @@ -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') + }) + ]) +} diff --git a/db/migrations/20190422140308_add_subscriber_breaches_shown.js b/db/migrations/20190422140308_add_subscriber_breaches_shown.js index 9b0d5ad4d..d196a94b0 100644 --- a/db/migrations/20190422140308_add_subscriber_breaches_shown.js +++ b/db/migrations/20190422140308_add_subscriber_breaches_shown.js @@ -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') + }) +} diff --git a/db/migrations/20190510152733_add_timestamps_to_email_addresses.js b/db/migrations/20190510152733_add_timestamps_to_email_addresses.js index cc9028ad1..ee9995090 100644 --- a/db/migrations/20190510152733_add_timestamps_to_email_addresses.js +++ b/db/migrations/20190510152733_add_timestamps_to_email_addresses.js @@ -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() + }) +} diff --git a/db/migrations/20190512170106_add_all_emails_to_primary_column.js b/db/migrations/20190512170106_add_all_emails_to_primary_column.js index e13ba3fa5..41f5643bd 100644 --- a/db/migrations/20190512170106_add_all_emails_to_primary_column.js +++ b/db/migrations/20190512170106_add_all_emails_to_primary_column.js @@ -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') + }) +} diff --git a/db/migrations/20190523152919_add_fxa_access_token_to_subscribers.js b/db/migrations/20190523152919_add_fxa_access_token_to_subscribers.js index 43a0ae63d..77a11889d 100644 --- a/db/migrations/20190523152919_add_fxa_access_token_to_subscribers.js +++ b/db/migrations/20190523152919_add_fxa_access_token_to_subscribers.js @@ -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') + }) +} diff --git a/db/migrations/20190713193852_add_email_sha1_index.js b/db/migrations/20190713193852_add_email_sha1_index.js index 5a1671a29..d98904258 100644 --- a/db/migrations/20190713193852_add_email_sha1_index.js +++ b/db/migrations/20190713193852_add_email_sha1_index.js @@ -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') + }) +} diff --git a/db/migrations/20191118100718_add-fxa-uid-index.js b/db/migrations/20191118100718_add-fxa-uid-index.js index a30bbd4c5..cdffc8100 100644 --- a/db/migrations/20191118100718_add-fxa-uid-index.js +++ b/db/migrations/20191118100718_add-fxa-uid-index.js @@ -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') + }) +} diff --git a/db/migrations/20191118170713_add-email_addresses-email-index.js b/db/migrations/20191118170713_add-email_addresses-email-index.js index aa78b260d..069eda33e 100644 --- a/db/migrations/20191118170713_add-email_addresses-email-index.js +++ b/db/migrations/20191118170713_add-email_addresses-email-index.js @@ -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') + }) +} diff --git a/db/migrations/20191202161125_add_breaches_resolved_column.js b/db/migrations/20191202161125_add_breaches_resolved_column.js index e6ce1fc26..5e2795e6e 100644 --- a/db/migrations/20191202161125_add_breaches_resolved_column.js +++ b/db/migrations/20191202161125_add_breaches_resolved_column.js @@ -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') + }) +} diff --git a/db/migrations/20200220143251_add-waitlists-column.js b/db/migrations/20200220143251_add-waitlists-column.js index 0a2b35a1e..518de6788 100644 --- a/db/migrations/20200220143251_add-waitlists-column.js +++ b/db/migrations/20200220143251_add-waitlists-column.js @@ -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') + }) +} diff --git a/db/migrations/20200708123351_add-subscribers-breaches_last_shown-index.js b/db/migrations/20200708123351_add-subscribers-breaches_last_shown-index.js index 52c50db9e..1f9af9769 100644 --- a/db/migrations/20200708123351_add-subscribers-breaches_last_shown-index.js +++ b/db/migrations/20200708123351_add-subscribers-breaches_last_shown-index.js @@ -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') + }) +} diff --git a/db/migrations/20200810144851_add_signup_language_index.js.js b/db/migrations/20200810144851_add_signup_language_index.js.js index 3b72a6640..413dc168a 100644 --- a/db/migrations/20200810144851_add_signup_language_index.js.js +++ b/db/migrations/20200810144851_add_signup_language_index.js.js @@ -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') + }) +} diff --git a/db/seeds/test_subscribers.js b/db/seeds/test_subscribers.js index b2b3a1258..db057e1a2 100644 --- a/db/seeds/test_subscribers.js +++ b/db/seeds/test_subscribers.js @@ -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 + } +} diff --git a/email-utils.js b/email-utils.js index c628e6a78..a9de73a3b 100644 --- a/email-utils.js +++ b/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 diff --git a/gulpfile.js b/gulpfile.js index 0e005d0d0..0c63494ae 100644 --- a/gulpfile.js +++ b/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 +) diff --git a/hibp.js b/hibp.js index 8d3f7c40e..a15b817e1 100644 --- a/hibp.js +++ b/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 diff --git a/ip-location-service.js b/ip-location-service.js index d637ddb43..ce5695b6e 100644 --- a/ip-location-service.js +++ b/ip-location-service.js @@ -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 +} diff --git a/lib/changePWLinks.js b/lib/changePWLinks.js index ab462c5ad..955c28911 100644 --- a/lib/changePWLinks.js +++ b/lib/changePWLinks.js @@ -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 +} diff --git a/lib/fxa.js b/lib/fxa.js index 78b3fee44..e679296c0 100644 --- a/lib/fxa.js +++ b/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 +} diff --git a/lib/remote-settings.js b/lib/remote-settings.js index 189e8335c..5b02b05b5 100644 --- a/lib/remote-settings.js +++ b/lib/remote-settings.js @@ -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 diff --git a/locale-utils.js b/locale-utils.js index 1fcc797ac..21b0c332a 100644 --- a/locale-utils.js +++ b/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 +} diff --git a/log.js b/log.js index f22092aaa..bc6e6c0d2 100644 --- a/log.js +++ b/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 diff --git a/middleware.js b/middleware.js index 53a1dc3e7..125d2cbfa 100644 --- a/middleware.js +++ b/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 +} diff --git a/public/js/all-breaches/all-breaches.js b/public/js/all-breaches/all-breaches.js index fd863df7b..9ceaa49fc 100644 --- a/public/js/all-breaches/all-breaches.js +++ b/public/js/all-breaches/all-breaches.js @@ -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() } -}; +} diff --git a/public/js/analytics_dnt-helper.js b/public/js/analytics_dnt-helper.js index 7e3a9a9fb..edb6be9b0 100644 --- a/public/js/analytics_dnt-helper.js +++ b/public/js/analytics_dnt-helper.js @@ -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 */ diff --git a/public/js/dashboard.js b/public/js/dashboard.js index abe8ab58b..87a7387f5 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -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 } } diff --git a/public/js/fx-bento.js b/public/js/fx-bento.js index ff723cc7d..3cf8fa033 100644 --- a/public/js/fx-bento.js +++ b/public/js/fx-bento.js @@ -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') + }) } - diff --git a/public/js/fxa-analytics.js b/public/js/fxa-analytics.js index 69ec4a226..ca1d2f187 100644 --- a/public/js/fxa-analytics.js +++ b/public/js/fxa-analytics.js @@ -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() } - - -})(); +})() diff --git a/public/js/fxa-menu.js b/public/js/fxa-menu.js index 2932d7304..77658dfb5 100644 --- a/public/js/fxa-menu.js +++ b/public/js/fxa-menu.js @@ -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) } - }); - }); - }); + }) + }) + }) } diff --git a/public/js/monitor.js b/public/js/monitor.js index a3c4b9c2d..341692f98 100644 --- a/public/js/monitor.js +++ b/public/js/monitor.js @@ -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 }) } - - }); + }) } - - - -})(); +})() diff --git a/public/js/resolve-breaches.js b/public/js/resolve-breaches.js index 5c86a7328..974f2bae1 100644 --- a/public/js/resolve-breaches.js +++ b/public/js/resolve-breaches.js @@ -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? } - }); - }); -})(); + }) + }) +})() diff --git a/public/js/scan-email.js b/public/js/scan-email.js index 98b867faa..2779f99cb 100644 --- a/public/js/scan-email.js +++ b/public/js/scan-email.js @@ -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() } diff --git a/public/js/scan-results.js b/public/js/scan-results.js index 091ce66de..932e74aa3 100644 --- a/public/js/scan-results.js +++ b/public/js/scan-results.js @@ -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') } -})(); +})() diff --git a/routes/breach-details.js b/routes/breach-details.js index aa53762d2..4499860ba 100644 --- a/routes/breach-details.js +++ b/routes/breach-details.js @@ -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 diff --git a/routes/dockerflow.js b/routes/dockerflow.js index 0ed68d94b..d761fd63e 100644 --- a/routes/dockerflow.js +++ b/routes/dockerflow.js @@ -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 diff --git a/routes/email-l10n.js b/routes/email-l10n.js index 16761ea7e..398576d1c 100644 --- a/routes/email-l10n.js +++ b/routes/email-l10n.js @@ -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 diff --git a/routes/hibp.js b/routes/hibp.js index 7a7879d9c..67ae25c86 100644 --- a/routes/hibp.js +++ b/routes/hibp.js @@ -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 diff --git a/routes/home.js b/routes/home.js index 1846d14e5..d882d8fde 100644 --- a/routes/home.js +++ b/routes/home.js @@ -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 diff --git a/routes/oauth.js b/routes/oauth.js index 896f20265..4408cd567 100644 --- a/routes/oauth.js +++ b/routes/oauth.js @@ -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 diff --git a/routes/scan.js b/routes/scan.js index 58118ca40..eb5aab97f 100644 --- a/routes/scan.js +++ b/routes/scan.js @@ -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 diff --git a/routes/ses.js b/routes/ses.js index eadbd3764..305a1a5f3 100644 --- a/routes/ses.js +++ b/routes/ses.js @@ -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 diff --git a/routes/user.js b/routes/user.js index 08f59a28b..31b687d20 100644 --- a/routes/user.js +++ b/routes/user.js @@ -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 diff --git a/scan-results.js b/scan-results.js index c728aa14c..b449441a4 100644 --- a/scan-results.js +++ b/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 +} diff --git a/scripts/breach-stats.js b/scripts/breach-stats.js index e96489e32..97527c2bd 100644 --- a/scripts/breach-stats.js +++ b/scripts/breach-stats.js @@ -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) +})() diff --git a/scripts/collect-unresolved-breaches-addresses.js b/scripts/collect-unresolved-breaches-addresses.js index 7ac392886..feab735ee 100644 --- a/scripts/collect-unresolved-breaches-addresses.js +++ b/scripts/collect-unresolved-breaches-addresses.js @@ -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() +})() diff --git a/scripts/delete-unverified-subscribers.js b/scripts/delete-unverified-subscribers.js index cb7e60c75..3728e938a 100644 --- a/scripts/delete-unverified-subscribers.js +++ b/scripts/delete-unverified-subscribers.js @@ -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() +})() diff --git a/scripts/delete-user.js b/scripts/delete-user.js index ca81dfbbf..dc91789ec 100644 --- a/scripts/delete-user.js +++ b/scripts/delete-user.js @@ -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() + }) + }) +})() diff --git a/scripts/lowercase-all-records.js b/scripts/lowercase-all-records.js index 9321c335b..bd547ad09 100644 --- a/scripts/lowercase-all-records.js +++ b/scripts/lowercase-all-records.js @@ -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() +})() diff --git a/scripts/make-sha1-hashes.js b/scripts/make-sha1-hashes.js index 537c86c36..d430b4385 100644 --- a/scripts/make-sha1-hashes.js +++ b/scripts/make-sha1-hashes.js @@ -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) +}) diff --git a/scripts/send-breach-notification.js b/scripts/send-breach-notification.js index 43e2ed555..279df2c5b 100644 --- a/scripts/send-breach-notification.js +++ b/scripts/send-breach-notification.js @@ -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() + }) + }) +})() diff --git a/scripts/send-email-to-pre-fxa-subscribers.js b/scripts/send-email-to-pre-fxa-subscribers.js index dad044a54..1a978bd70 100644 --- a/scripts/send-email-to-pre-fxa-subscribers.js +++ b/scripts/send-email-to-pre-fxa-subscribers.js @@ -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() +})() diff --git a/scripts/subscribe-lowercase-hashes.js b/scripts/subscribe-lowercase-hashes.js index 8f2343fe5..2c03e2caa 100644 --- a/scripts/subscribe-lowercase-hashes.js +++ b/scripts/subscribe-lowercase-hashes.js @@ -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() +})() diff --git a/scripts/updatebreaches.js b/scripts/updatebreaches.js index ef29621c8..d52c808a1 100644 --- a/scripts/updatebreaches.js +++ b/scripts/updatebreaches.js @@ -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() +})() diff --git a/server.js b/server.js index 602a7b13b..bf8e04853 100644 --- a/server.js +++ b/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 }) +}) diff --git a/sha1-utils.js b/sha1-utils.js index 674826762..ee12564d0 100644 --- a/sha1-utils.js +++ b/sha1-utils.js @@ -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 diff --git a/template-helpers/articles.js b/template-helpers/articles.js index 56d7ac8a6..ccd1338c4 100644 --- a/template-helpers/articles.js +++ b/template-helpers/articles.js @@ -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 few steps you can take immediately to protect your account and limit the damage.", - ], - }, - { - 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 few steps you can take immediately to protect your account and limit the damage." + ] }, { 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 conducted by Google, 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 conducted by Google, 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: "SECURITY TIP Steer clear of the 100 most-used passwords.", - tipSubhead: "Every year, SplashData evaluates millions of leaked passwords and compiles the 100 most common ones. 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 100 most common ones. 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.", - "Websites that support 2FA 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.', + "Websites that support 2FA 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 Mozilla VPN), 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 Mozilla VPN), 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 Mozilla VPN) 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: "SECURITY TIP 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: "SECURITY TIP 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. Read the guide", - }, + 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. Read the guide" + } }, { - 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: "SECURITY TIP", - 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: "SECURITY TIP", - 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: "Myth 1: 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: "Myth 2: 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: "Myth 3: 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: "Myth 4: 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: "Myth 5: 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 +} diff --git a/template-helpers/breach-detail.js b/template-helpers/breach-detail.js index d8683b007..083429f1f 100644 --- a/template-helpers/breach-detail.js +++ b/template-helpers/breach-detail.js @@ -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: `${prettyDate(breach.AddedDate, locales)}`, breachDate: `${prettyDate(breach.BreachDate, locales)}`, - 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("", ""); +function formatResolutionMessage (message) { + return message.replace('', "") } -function formatNotificationLink(message) { - return message.replace("", ""); +function formatNotificationLink (message) { + return message.replace('', "") } +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 +} diff --git a/template-helpers/breach-stats.js b/template-helpers/breach-stats.js index 43ff4fd7b..dab47ce6f 100644 --- a/template-helpers/breach-stats.js +++ b/template-helpers/breach-stats.js @@ -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("", ""); +function formatProgressMessage (message) { + return message.replace('', "") } -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 +} diff --git a/template-helpers/breaches.js b/template-helpers/breaches.js index 3223b8dcb..b4412489a 100644 --- a/template-helpers/breaches.js +++ b/template-helpers/breaches.js @@ -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 +} diff --git a/template-helpers/dashboard.js b/template-helpers/dashboard.js index 0027ca2b0..28497cdc7 100644 --- a/template-helpers/dashboard.js +++ b/template-helpers/dashboard.js @@ -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 = `${email}`; - 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 = `${nestedSignInLink}`; - - return LocaleUtils.fluentFormat(locales, "email-verified-view-dashboard", { nestedSignInLink: nestedSignInLink}); +function makeEmailAddedToSubscriptionString (email, args) { + const locales = args.data.root.req.supportedLocales + const nestedEmail = `${email}` + 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 = `${nestedSignInLink}` -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: `${primaryEmail.email}`}), - optionId: "1", - optionChecked: (communicationOption === 1) ? "checked" : "", - }, - ]; + labelString: LocaleUtils.fluentFormat(locales, 'comm-opt-1', { primaryEmail: `${primaryEmail.email}` }), + 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 = `${lastAddedEmail}`; +function getLastAddedEmailStrings (args) { + const locales = args.data.root.req.supportedLocales + const lastAddedEmail = args.data.root.lastAddedEmail + const lastAddedEmailSpan = `${lastAddedEmail}` - const preferencesLinkString = LocaleUtils.fluentFormat(locales, "preferences"); - const preferencesLink = `${preferencesLinkString}`; + const preferencesLinkString = LocaleUtils.fluentFormat(locales, 'preferences') + const preferencesLink = `${preferencesLinkString}` 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 +} diff --git a/template-helpers/emails.js b/template-helpers/emails.js index a5461f5dc..90350b997 100644 --- a/template-helpers/emails.js +++ b/template-helpers/emails.js @@ -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 ((//).test(fxaTout1) && (/<\/a>/).test(fxaTout1)) { - const openingAnchorTag = ``; - fxaTouts[0].paragraph = fxaTout1.replace("", openingAnchorTag); + const openingAnchorTag = `` + fxaTouts[0].paragraph = fxaTout1.replace('', 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, "$1"); - return ``; + breachedEmail = breachedEmail.replace(/([@.:])/g, '$1') + return `` } +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 ((//).test(preFxaMessage) && (/<\/a>/).test(preFxaMessage)) { - const openingAnchorTag = ``; - preFxaMessage = preFxaMessage.replace("", openingAnchorTag); + const openingAnchorTag = `` + preFxaMessage = preFxaMessage.replace('', 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 = `${faqLink}` - let faqLink = LocaleUtils.fluentFormat(locales, "frequently-asked-questions"); - faqLink = `${faqLink}`; - - 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 = `${unsubLinkText}`; + const unsubUrl = args.data.root.unsubscribeUrl + const unsubLinkText = LocaleUtils.fluentFormat(locales, 'email-unsub-link') + const unsubLink = `${unsubLinkText}` - 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 +} diff --git a/template-helpers/footer.js b/template-helpers/footer.js index 183c38387..154fd637b 100644 --- a/template-helpers/footer.js +++ b/template-helpers/footer.js @@ -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 +} diff --git a/template-helpers/hbs-helpers.js b/template-helpers/hbs-helpers.js index 7c7af8112..e5c1b8715 100644 --- a/template-helpers/hbs-helpers.js +++ b/template-helpers/hbs-helpers.js @@ -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 ``; + return `` } -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 ` ${word} `; - }; - - 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 ` ${word} ` } - 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 +} diff --git a/template-helpers/header.js b/template-helpers/header.js index 072f4c75d..29ffd96e2 100644 --- a/template-helpers/header.js +++ b/template-helpers/header.js @@ -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: `${userEmail}`}); - 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: `${userEmail}` }) + 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 +} diff --git a/template-helpers/homepage.js b/template-helpers/homepage.js index 156a129e4..06fe9be34 100644 --- a/template-helpers/homepage.js +++ b/template-helpers/homepage.js @@ -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 : `${featuredBreach.Title}` }); + landingCopy.headline = LocaleUtils.fluentFormat(locales, 'was-your-info-exposed', { breachName: `${featuredBreach.Title}` }) 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 +} diff --git a/template-helpers/index.js b/template-helpers/index.js index 9cf6c95ea..712aff182 100644 --- a/template-helpers/index.js +++ b/template-helpers/index.js @@ -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 - ), -}; + ) +} diff --git a/template-helpers/product-education-video.js b/template-helpers/product-education-video.js index 5695d1ad4..26f68fbf4 100644 --- a/template-helpers/product-education-video.js +++ b/template-helpers/product-education-video.js @@ -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 } diff --git a/template-helpers/product-promos.js b/template-helpers/product-promos.js index 2ca8e5814..fdf3f30fe 100644 --- a/template-helpers/product-promos.js +++ b/template-helpers/product-promos.js @@ -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 +} diff --git a/template-helpers/recommendations.js b/template-helpers/recommendations.js index 19240ce5e..d4305ebf3 100644 --- a/template-helpers/recommendations.js +++ b/template-helpers/recommendations.js @@ -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' + } + } +} diff --git a/template-helpers/scan-results.js b/template-helpers/scan-results.js index bce1ed0d9..d294581f1 100644 --- a/template-helpers/scan-results.js +++ b/template-helpers/scan-results.js @@ -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: `${featuredBreach.Title}`, - 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("", ""); - let ctaHref = ""; +function getFacebookResultMessage (id, args) { + let message = getString(id, args) + message = message.replace('', "") + 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("", ``); + message = message.replace('', ``) - return message; + return message } module.exports = { getScanResultsHeadline, - getFacebookResultMessage, -}; + getFacebookResultMessage +} diff --git a/template-helpers/sign-up-banners.js b/template-helpers/sign-up-banners.js index 669464bc1..8e129c635 100644 --- a/template-helpers/sign-up-banners.js +++ b/template-helpers/sign-up-banners.js @@ -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 +} diff --git a/tests/controllers/dockerflow.test.js b/tests/controllers/dockerflow.test.js index d2f38dabf..a1b52924f 100644 --- a/tests/controllers/dockerflow.test.js +++ b/tests/controllers/dockerflow.test.js @@ -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') +}) diff --git a/tests/controllers/hibp.test.js b/tests/controllers/hibp.test.js index db1ffb4ae..745da73f0 100644 --- a/tests/controllers/hibp.test.js +++ b/tests/controllers/hibp.test.js @@ -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']) +}) diff --git a/tests/controllers/home.test.js b/tests/controllers/home.test.js index 92c9f0047..a01783fe6 100644 --- a/tests/controllers/home.test.js +++ b/tests/controllers/home.test.js @@ -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() +}) diff --git a/tests/controllers/oauth.test.js b/tests/controllers/oauth.test.js index 53277f274..9fdae50ab 100644 --- a/tests/controllers/oauth.test.js +++ b/tests/controllers/oauth.test.js @@ -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') +}) diff --git a/tests/controllers/scan.test.js b/tests/controllers/scan.test.js index 3485b1dfa..90e601c75 100644 --- a/tests/controllers/scan.test.js +++ b/tests/controllers/scan.test.js @@ -1,69 +1,63 @@ -"use strict"; -const AppConstants = require("../../app-constants"); -const sha1 = require("../../sha1-utils"); -const HIBP = require("../../hibp"); -const scan = require("../../controllers/scan"); +'use strict' +const AppConstants = require('../../app-constants') +const sha1 = require('../../sha1-utils') +const HIBP = require('../../hibp') +const scan = require('../../controllers/scan') -const { testBreaches } = require ("../test-breaches"); -require("../resetDB"); +const { testBreaches } = require('../test-breaches') +require('../resetDB') +jest.mock('../../hibp') -jest.mock("../../hibp"); +const mockRequest = { fluentFormat: jest.fn() } +test('scan GET redirects to home', () => { + shouldRedirectHome(scan.get, mockRequest) +}) -const mockRequest = { fluentFormat: jest.fn() }; +test('scan POST with empty email hash redirects to home', () => { + mockRequest.body = { emailHash: null } -test("scan GET redirects to home", () => { - shouldRedirectHome(scan.get, mockRequest); -}); + shouldRedirectHome(scan.post, mockRequest) +}) +test('scan POST with hash of empty string redirects to home', () => { + mockRequest.body = { emailHash: sha1('') } -test("scan POST with empty email hash redirects to home", () => { - mockRequest.body = { emailHash: null }; + shouldRedirectHome(scan.post, mockRequest) +}) - shouldRedirectHome(scan.post, mockRequest); -}); +test('scan POST with hash should render scan with foundBreaches', async () => { + const testEmail = 'test@example.com' + const testFoundBreaches = [] - -test("scan POST with hash of empty string redirects to home", () => { - mockRequest.body = { emailHash: sha1("") }; - - shouldRedirectHome(scan.post, mockRequest); -}); - - -test("scan POST with hash should render scan with foundBreaches", async () => { - const testEmail = "test@example.com"; - const testFoundBreaches = []; - - mockRequest.body = { emailHash: sha1(testEmail) }; - mockRequest.app = { locals: { breaches: testBreaches } }; - mockRequest.session = { user: null }; + mockRequest.body = { emailHash: sha1(testEmail) } + mockRequest.app = { locals: { breaches: testBreaches } } + mockRequest.session = { user: null } mockRequest.query = { - experimentBranch: false, - }; + experimentBranch: false + } - mockRequest.url = { url: AppConstants.SERVER_URL }; - mockRequest.app.locals.SERVER_URL = AppConstants.SERVER_URL; - mockRequest.csrfToken = jest.fn(); + mockRequest.url = { url: AppConstants.SERVER_URL } + mockRequest.app.locals.SERVER_URL = AppConstants.SERVER_URL + mockRequest.csrfToken = jest.fn() - const mockResponse = { render: jest.fn() }; - HIBP.getBreachesForEmail.mockResolvedValue(testFoundBreaches); + const mockResponse = { render: jest.fn() } + HIBP.getBreachesForEmail.mockResolvedValue(testFoundBreaches) - await scan.post(mockRequest, mockResponse); + await scan.post(mockRequest, mockResponse) - const mockRenderCallArgs = mockResponse.render.mock.calls[0]; - expect(mockRenderCallArgs[0]).toBe("scan"); - expect(mockRenderCallArgs[1].foundBreaches).toBe(testFoundBreaches); - expect(mockRenderCallArgs[1].hasOwnProperty("featuredBreach")).toBeFalsy(); -}); + const mockRenderCallArgs = mockResponse.render.mock.calls[0] + expect(mockRenderCallArgs[0]).toBe('scan') + expect(mockRenderCallArgs[1].foundBreaches).toBe(testFoundBreaches) + expect(mockRenderCallArgs[1].hasOwnProperty('featuredBreach')).toBeFalsy() +}) +function shouldRedirectHome (fn, req) { + const mockResponse = { redirect: jest.fn() } -function shouldRedirectHome(fn, req) { - const mockResponse = { redirect: jest.fn() }; + fn(req, mockResponse) - fn(req, mockResponse); - - const mockRedirectCallArgs = mockResponse.redirect.mock.calls[0]; - expect(mockRedirectCallArgs[0]).toBe("/"); + const mockRedirectCallArgs = mockResponse.redirect.mock.calls[0] + expect(mockRedirectCallArgs[0]).toBe('/') } diff --git a/tests/controllers/ses.test.js b/tests/controllers/ses.test.js index 92b956412..ab05ba42d 100644 --- a/tests/controllers/ses.test.js +++ b/tests/controllers/ses.test.js @@ -1,139 +1,132 @@ -"use strict"; +'use strict' -const httpMocks = require("node-mocks-http"); +const httpMocks = require('node-mocks-http') -const DB = require("../../db/DB"); -const getSha1 = require("../../sha1-utils"); -const ses = require("../../controllers/ses"); +const DB = require('../../db/DB') +const getSha1 = require('../../sha1-utils') +const ses = require('../../controllers/ses') -require("../resetDB"); -jest.mock("../../hibp"); +require('../resetDB') +jest.mock('../../hibp') +const testNotifications = new Map() +testNotifications.set('bounce', require('./ses-bounce-notification.json')) +testNotifications.set('complaint', require('./ses-complaint-notification.json')) +testNotifications.set('invalid', require('./invalid-signature-ses-complaint-notification.json')) +testNotifications.set('fxa-delete', require('./sns-fxa-delete.json')) -const testNotifications = new Map(); -testNotifications.set("bounce", require("./ses-bounce-notification.json")); -testNotifications.set("complaint", require("./ses-complaint-notification.json")); -testNotifications.set("invalid", require("./invalid-signature-ses-complaint-notification.json")); -testNotifications.set("fxa-delete", require("./sns-fxa-delete.json")); +const createRequestBody = function (notificationType) { + const notification = testNotifications.get(notificationType) + return JSON.stringify(notification) +} - -const createRequestBody = function(notificationType) { - const notification = testNotifications.get(notificationType); - return JSON.stringify(notification); -}; - - -test("ses notification with Permanent bounce unsubscribes recipient subscriber", async () => { +test('ses notification with Permanent bounce unsubscribes recipient subscriber', async () => { // TODO: restore tests for ["General", "NoEmail", "Suppressed"] sub types - const testEmail = "bounce@simulator.amazonses.com"; - const testHashes = [getSha1(testEmail)]; + const testEmail = 'bounce@simulator.amazonses.com' + const testHashes = [getSha1(testEmail)] - await DB.addSubscriber(testEmail); - let subscribers = await DB.getSubscribersByHashes(testHashes); - expect(subscribers.length).toEqual(1); + await DB.addSubscriber(testEmail) + let subscribers = await DB.getSubscribersByHashes(testHashes) + expect(subscribers.length).toEqual(1) const req = httpMocks.createRequest({ - method: "POST", - url: "/ses/notification", - body: createRequestBody("bounce"), - }); - const resp = httpMocks.createResponse(); + method: 'POST', + url: '/ses/notification', + body: createRequestBody('bounce') + }) + const resp = httpMocks.createResponse() - await ses.notification(req, resp); - expect(resp.statusCode).toEqual(200); + await ses.notification(req, resp) + expect(resp.statusCode).toEqual(200) - subscribers = await DB.getSubscribersByHashes(testHashes); - expect(subscribers.length).toEqual(0); -}); + subscribers = await DB.getSubscribersByHashes(testHashes) + expect(subscribers.length).toEqual(0) +}) +test('ses notification with Complaint unsubscribes recipient subscriber', async () => { + const testEmail = 'complaint@simulator.amazonses.com' -test("ses notification with Complaint unsubscribes recipient subscriber", async () => { - const testEmail = "complaint@simulator.amazonses.com"; - - await DB.addSubscriber(testEmail); - let subscribers = await DB.getSubscribersByHashes([getSha1(testEmail)]); - expect(subscribers.length).toEqual(1); + await DB.addSubscriber(testEmail) + let subscribers = await DB.getSubscribersByHashes([getSha1(testEmail)]) + expect(subscribers.length).toEqual(1) const req = httpMocks.createRequest({ - method: "POST", - url: "/ses/notification", - body: createRequestBody("complaint"), - }); - const resp = httpMocks.createResponse(); + method: 'POST', + url: '/ses/notification', + body: createRequestBody('complaint') + }) + const resp = httpMocks.createResponse() - await ses.notification(req, resp); - expect(resp.statusCode).toEqual(200); + await ses.notification(req, resp) + expect(resp.statusCode).toEqual(200) - subscribers = await DB.getSubscribersByHashes([getSha1(testEmail)]); - expect(subscribers.length).toEqual(0); -}); + subscribers = await DB.getSubscribersByHashes([getSha1(testEmail)]) + expect(subscribers.length).toEqual(0) +}) +test('ses notification with Complaint unsubscribes recipient from email_addresses', async () => { + const testPrimaryEmail = 'secondary-email-complainer@mailinator.com' + const testSignupLanguage = 'en' + const testUser = await DB.addSubscriber(testPrimaryEmail, testSignupLanguage) + const testEmail = 'complaint@simulator.amazonses.com' -test("ses notification with Complaint unsubscribes recipient from email_addresses", async () => { - const testPrimaryEmail = "secondary-email-complainer@mailinator.com"; - const testSignupLanguage = "en"; - const testUser = await DB.addSubscriber(testPrimaryEmail, testSignupLanguage); - const testEmail = "complaint@simulator.amazonses.com"; - - await DB.addSubscriberUnverifiedEmailHash(testUser, testEmail); + await DB.addSubscriberUnverifiedEmailHash(testUser, testEmail) const req = httpMocks.createRequest({ - method: "POST", - url: "/ses/notification", - body: createRequestBody("complaint"), - }); - const resp = httpMocks.createResponse(); - - await ses.notification(req, resp); - expect(resp.statusCode).toEqual(200); - const noMoreEmailAddressRecord = await DB.getEmailAddressRecordByEmail(testEmail); - expect(noMoreEmailAddressRecord).toBeUndefined(); -}); + method: 'POST', + url: '/ses/notification', + body: createRequestBody('complaint') + }) + const resp = httpMocks.createResponse() + await ses.notification(req, resp) + expect(resp.statusCode).toEqual(200) + const noMoreEmailAddressRecord = await DB.getEmailAddressRecordByEmail(testEmail) + expect(noMoreEmailAddressRecord).toBeUndefined() +}) test("ses notification with invalid signature responds with error and doesn't change subscribers", async () => { - const testEmail = "complaint@simulator.amazonses.com"; + const testEmail = 'complaint@simulator.amazonses.com' - await DB.addSubscriber(testEmail); - let subscribers = await DB.getSubscribersByHashes([getSha1(testEmail)]); - expect(subscribers.length).toEqual(1); + await DB.addSubscriber(testEmail) + let subscribers = await DB.getSubscribersByHashes([getSha1(testEmail)]) + expect(subscribers.length).toEqual(1) const req = httpMocks.createRequest({ - method: "POST", - url: "/ses/notification", - body: createRequestBody("invalid"), - }); - const resp = httpMocks.createResponse(); + method: 'POST', + url: '/ses/notification', + body: createRequestBody('invalid') + }) + const resp = httpMocks.createResponse() - await expect(ses.notification(req, resp)).rejects.toMatch("invalid"); - expect(resp.statusCode).toEqual(401); + await expect(ses.notification(req, resp)).rejects.toMatch('invalid') + expect(resp.statusCode).toEqual(401) - subscribers = await DB.getSubscribersByHashes([getSha1(testEmail)]); - expect(subscribers.length).toEqual(1); -}); + subscribers = await DB.getSubscribersByHashes([getSha1(testEmail)]) + expect(subscribers.length).toEqual(1) +}) +test('sns notification for FxA account deletes monitor subscriber record', async () => { + const testEmail = 'fxa-deleter@mailinator.com' + const testSignupLanguage = 'en' + const testFxaAccessToken = 'abcdef123456789' + const testFxaRefreshToken = 'abcdef123456789' + const testFxaUID = '3b1a9d27f85b4a4c977f3a84838f9116' + const testFxaProfileData = JSON.stringify({ uid: testFxaUID }) + await DB.addSubscriber(testEmail, testSignupLanguage, testFxaAccessToken, testFxaRefreshToken, testFxaProfileData) -test("sns notification for FxA account deletes monitor subscriber record", async () => { - const testEmail = "fxa-deleter@mailinator.com"; - const testSignupLanguage = "en"; - const testFxaAccessToken = "abcdef123456789"; - const testFxaRefreshToken = "abcdef123456789"; - const testFxaUID = "3b1a9d27f85b4a4c977f3a84838f9116"; - const testFxaProfileData = JSON.stringify({uid: testFxaUID}); - await DB.addSubscriber(testEmail, testSignupLanguage, testFxaAccessToken, testFxaRefreshToken, testFxaProfileData); - - let subscribers = await DB.getSubscribersByHashes([getSha1(testEmail)]); - expect(subscribers.length).toEqual(1); + let subscribers = await DB.getSubscribersByHashes([getSha1(testEmail)]) + expect(subscribers.length).toEqual(1) const req = httpMocks.createRequest({ - method: "POST", - url: "/ses/notification", - body: createRequestBody("fxa-delete"), - }); - const resp = httpMocks.createResponse(); - await ses.notification(req, resp); - expect(resp.statusCode).toEqual(200); + method: 'POST', + url: '/ses/notification', + body: createRequestBody('fxa-delete') + }) + const resp = httpMocks.createResponse() + await ses.notification(req, resp) + expect(resp.statusCode).toEqual(200) - subscribers = await DB.getSubscribersByHashes([getSha1(testEmail)]); - expect(subscribers.length).toEqual(0); -}); + subscribers = await DB.getSubscribersByHashes([getSha1(testEmail)]) + expect(subscribers.length).toEqual(0) +}) diff --git a/tests/controllers/user.test.js b/tests/controllers/user.test.js index 9cf377309..fdf0050ea 100644 --- a/tests/controllers/user.test.js +++ b/tests/controllers/user.test.js @@ -1,40 +1,37 @@ -"use strict"; +'use strict' -const httpMocks = require("node-mocks-http"); +const httpMocks = require('node-mocks-http') -const DB = require("../../db/DB"); -const EmailUtils = require("../../email-utils"); -const { FXA } = require("../../lib/fxa"); -const getSha1 = require("../../sha1-utils"); -const HIBP = require("../../hibp"); -const user = require("../../controllers/user"); +const DB = require('../../db/DB') +const EmailUtils = require('../../email-utils') +const { FXA } = require('../../lib/fxa') +const getSha1 = require('../../sha1-utils') +const HIBP = require('../../hibp') +const user = require('../../controllers/user') -const { testBreaches } = require ("../test-breaches"); -const { TEST_SUBSCRIBERS, TEST_EMAIL_ADDRESSES } = require("../../db/seeds/test_subscribers"); +const { testBreaches } = require('../test-breaches') +const { TEST_SUBSCRIBERS, TEST_EMAIL_ADDRESSES } = require('../../db/seeds/test_subscribers') -require("../resetDB"); +require('../resetDB') +jest.mock('../../email-utils') -jest.mock("../../email-utils"); +const mockRequest = { fluentFormat: jest.fn() } -const mockRequest = { fluentFormat: jest.fn() }; - - -function expectResponseRenderedSubpagePartial(resp, partial) { - expect(resp.statusCode).toEqual(200); - expect(resp.render).toHaveBeenCalledTimes(1); - const renderCallArgs = resp.render.mock.calls[0]; - expect(renderCallArgs[0]).toEqual("subpage"); - expect(renderCallArgs[1].whichPartial).toEqual(partial); +function expectResponseRenderedSubpagePartial (resp, partial) { + expect(resp.statusCode).toEqual(200) + expect(resp.render).toHaveBeenCalledTimes(1) + const renderCallArgs = resp.render.mock.calls[0] + expect(renderCallArgs[0]).toEqual('subpage') + expect(renderCallArgs[1].whichPartial).toEqual(partial) } - -test("user getDashboard with valid user renders dashboard with verified emails and their breaches", async () => { - const testSubscriberEmail = TEST_SUBSCRIBERS.firefox_account.primary_email; - const testSubscriber = await DB.getSubscriberByEmail(testSubscriberEmail); - const mockReturnedBreaches = testBreaches.slice(0,2); - HIBP.getBreachesForEmail = jest.fn(); - HIBP.getBreachesForEmail.mockReturnValue(mockReturnedBreaches); +test('user getDashboard with valid user renders dashboard with verified emails and their breaches', async () => { + const testSubscriberEmail = TEST_SUBSCRIBERS.firefox_account.primary_email + const testSubscriber = await DB.getSubscriberByEmail(testSubscriberEmail) + const mockReturnedBreaches = testBreaches.slice(0, 2) + HIBP.getBreachesForEmail = jest.fn() + HIBP.getBreachesForEmail.mockReturnValue(mockReturnedBreaches) const req = { app: { locals: { breaches: testBreaches } }, @@ -42,649 +39,620 @@ test("user getDashboard with valid user renders dashboard with verified emails a fluentFormat: jest.fn(), session: { user: testSubscriber }, supportedLocales: [], - user: testSubscriber, - }; - const resp = httpMocks.createResponse(); - resp.render = jest.fn(); + user: testSubscriber + } + const resp = httpMocks.createResponse() + resp.render = jest.fn() - await user.getDashboard(req, resp); + await user.getDashboard(req, resp) - expect(resp.statusCode).toEqual(200); - expect(resp.render).toHaveBeenCalledTimes(1); - const renderCallArgs = resp.render.mock.calls[0]; - expect(renderCallArgs[0]).toEqual("dashboards"); - const renderCallContext = renderCallArgs[1]; - expect(renderCallContext.whichPartial).toEqual("dashboards/breaches-dash"); + expect(resp.statusCode).toEqual(200) + expect(resp.render).toHaveBeenCalledTimes(1) + const renderCallArgs = resp.render.mock.calls[0] + expect(renderCallArgs[0]).toEqual('dashboards') + const renderCallContext = renderCallArgs[1] + expect(renderCallContext.whichPartial).toEqual('dashboards/breaches-dash') - expect(renderCallContext.verifiedEmails.length).toEqual(2); - expect(renderCallContext.unverifiedEmails.length).toEqual(1); - expect(renderCallContext.verifiedEmails[0].breaches.length).toEqual(mockReturnedBreaches.length); -}); + expect(renderCallContext.verifiedEmails.length).toEqual(2) + expect(renderCallContext.unverifiedEmails.length).toEqual(1) + expect(renderCallContext.verifiedEmails[0].breaches.length).toEqual(mockReturnedBreaches.length) +}) - -test("user getDashboard with valid user renders dashboard with verified emails and resolved breaches", async () => { - const testSubscriberEmail = TEST_SUBSCRIBERS.firefox_account.primary_email; - const testSubscriber = await DB.getSubscriberByEmail(testSubscriberEmail); - const mockReturnedBreaches = testBreaches.slice(0,2); - HIBP.getBreachesForEmail = jest.fn(); - HIBP.getBreachesForEmail.mockReturnValue(mockReturnedBreaches); +test('user getDashboard with valid user renders dashboard with verified emails and resolved breaches', async () => { + const testSubscriberEmail = TEST_SUBSCRIBERS.firefox_account.primary_email + const testSubscriber = await DB.getSubscriberByEmail(testSubscriberEmail) + const mockReturnedBreaches = testBreaches.slice(0, 2) + HIBP.getBreachesForEmail = jest.fn() + HIBP.getBreachesForEmail.mockReturnValue(mockReturnedBreaches) const req = { app: { locals: { breaches: testBreaches } }, csrfToken: jest.fn(), fluentFormat: jest.fn(), query: { - experimentBranch: false, + experimentBranch: false }, session: { user: testSubscriber }, supportedLocales: [], - user: testSubscriber, - }; - const resp = httpMocks.createResponse(); - resp.render = jest.fn(); + user: testSubscriber + } + const resp = httpMocks.createResponse() + resp.render = jest.fn() - await user.getDashboard(req, resp); + await user.getDashboard(req, resp) - expect(resp.statusCode).toEqual(200); - expect(resp.render).toHaveBeenCalledTimes(1); - const renderCallArgs = resp.render.mock.calls[0]; - expect(renderCallArgs[0]).toEqual("dashboards"); - const renderCallContext = renderCallArgs[1]; - expect(renderCallContext.whichPartial).toEqual("dashboards/breaches-dash"); + expect(resp.statusCode).toEqual(200) + expect(resp.render).toHaveBeenCalledTimes(1) + const renderCallArgs = resp.render.mock.calls[0] + expect(renderCallArgs[0]).toEqual('dashboards') + const renderCallContext = renderCallArgs[1] + expect(renderCallContext.whichPartial).toEqual('dashboards/breaches-dash') - expect(renderCallContext.verifiedEmails.length).toEqual(2); - expect(renderCallContext.unverifiedEmails.length).toEqual(1); - expect(renderCallContext.verifiedEmails[0].breaches.length).toEqual(mockReturnedBreaches.length); - const resolvedBreaches = renderCallContext.verifiedEmails[0].breaches.filter(b => b.IsResolved); - expect(resolvedBreaches.length).toEqual(1); -}); + expect(renderCallContext.verifiedEmails.length).toEqual(2) + expect(renderCallContext.unverifiedEmails.length).toEqual(1) + expect(renderCallContext.verifiedEmails[0].breaches.length).toEqual(mockReturnedBreaches.length) + const resolvedBreaches = renderCallContext.verifiedEmails[0].breaches.filter(b => b.IsResolved) + expect(resolvedBreaches.length).toEqual(1) +}) - -test("user add POST with email adds unverified subscriber and sends verification email", async () => { - const testUserAddEmail = "addingnewemail@test.com"; - const testSubscriberEmail = "firefoxaccount@test.com"; - const testSubscriber = await DB.getSubscriberByEmail(testSubscriberEmail); +test('user add POST with email adds unverified subscriber and sends verification email', async () => { + const testUserAddEmail = 'addingnewemail@test.com' + const testSubscriberEmail = 'firefoxaccount@test.com' + const testSubscriber = await DB.getSubscriberByEmail(testSubscriberEmail) // Set up mocks const req = httpMocks.createRequest({ - method: "POST", - url: "/user/add", + method: 'POST', + url: '/user/add', body: { email: testUserAddEmail }, session: { user: testSubscriber }, user: testSubscriber, fluentFormat: jest.fn(), headers: { - referer: "", - }, - }); - const resp = httpMocks.createResponse(); - EmailUtils.sendEmail.mockResolvedValue(true); + referer: '' + } + }) + const resp = httpMocks.createResponse() + EmailUtils.sendEmail.mockResolvedValue(true) // Call code-under-test - await user.add(req, resp); + await user.add(req, resp) // Check expectations - expect(resp.statusCode).toEqual(302); + expect(resp.statusCode).toEqual(302) - expect(testSubscriber.primary_email).toEqual(testSubscriberEmail); + expect(testSubscriber.primary_email).toEqual(testSubscriberEmail) - const testSubscriberEmailAddressRecords = await DB.getUserEmails(testSubscriber.id); - const testSubscriberEmailAddresses = testSubscriberEmailAddressRecords.map(record => record.email); - expect(testSubscriberEmailAddresses.includes(testUserAddEmail)).toBeTruthy(); + const testSubscriberEmailAddressRecords = await DB.getUserEmails(testSubscriber.id) + const testSubscriberEmailAddresses = testSubscriberEmailAddressRecords.map(record => record.email) + expect(testSubscriberEmailAddresses.includes(testUserAddEmail)).toBeTruthy() for (const testSubscriberEmailAddress of testSubscriberEmailAddresses) { if (testSubscriberEmailAddress.email === testUserAddEmail) { - expect(testSubscriberEmailAddress.verified).toBeFalsy(); + expect(testSubscriberEmailAddress.verified).toBeFalsy() } } - const mockCalls = EmailUtils.sendEmail.mock.calls; - expect(mockCalls.length).toEqual(1); - const mockCallArgs = mockCalls[0]; - expect(mockCallArgs).toContain(testUserAddEmail); - expect(mockCallArgs).toContain("default_email"); -}); + const mockCalls = EmailUtils.sendEmail.mock.calls + expect(mockCalls.length).toEqual(1) + const mockCallArgs = mockCalls[0] + expect(mockCallArgs).toContain(testUserAddEmail) + expect(mockCallArgs).toContain('default_email') +}) - -test("user add POST with upperCaseAddress adds email_address record with lowercaseaddress sha1", async () => { - const testUserAddEmail = "addingUpperCaseEmail@test.com"; - const testSubscriberEmail = "firefoxaccount@test.com"; - const testSubscriber = await DB.getSubscriberByEmail(testSubscriberEmail); +test('user add POST with upperCaseAddress adds email_address record with lowercaseaddress sha1', async () => { + const testUserAddEmail = 'addingUpperCaseEmail@test.com' + const testSubscriberEmail = 'firefoxaccount@test.com' + const testSubscriber = await DB.getSubscriberByEmail(testSubscriberEmail) // Set up mocks const req = httpMocks.createRequest({ - method: "POST", - url: "/user/add", + method: 'POST', + url: '/user/add', body: { email: testUserAddEmail }, session: { user: testSubscriber }, user: testSubscriber, fluentFormat: jest.fn(), headers: { - referer: "", - }, - }); - const resp = httpMocks.createResponse(); - EmailUtils.sendEmail.mockResolvedValue(true); + referer: '' + } + }) + const resp = httpMocks.createResponse() + EmailUtils.sendEmail.mockResolvedValue(true) // Call code-under-test - await user.add(req, resp); + await user.add(req, resp) // Check expectations - expect(resp.statusCode).toEqual(302); - expect(testSubscriber.primary_email).toEqual(testSubscriberEmail); + expect(resp.statusCode).toEqual(302) + expect(testSubscriber.primary_email).toEqual(testSubscriberEmail) - const testSubscriberEmailAddressRecords = await DB.getUserEmails(testSubscriber.id); - const testSubscriberEmailAddresses = testSubscriberEmailAddressRecords.map(record => record.email); - expect(testSubscriberEmailAddresses.includes(testUserAddEmail)).toBeTruthy(); - const testSubscriberEmailAddressHashes = testSubscriberEmailAddressRecords.map(record => record.sha1); - expect(testSubscriberEmailAddressHashes.includes(getSha1(testUserAddEmail))).toBeTruthy(); -}); + const testSubscriberEmailAddressRecords = await DB.getUserEmails(testSubscriber.id) + const testSubscriberEmailAddresses = testSubscriberEmailAddressRecords.map(record => record.email) + expect(testSubscriberEmailAddresses.includes(testUserAddEmail)).toBeTruthy() + const testSubscriberEmailAddressHashes = testSubscriberEmailAddressRecords.map(record => record.sha1) + expect(testSubscriberEmailAddressHashes.includes(getSha1(testUserAddEmail))).toBeTruthy() +}) - -test("user resendEmail with valid session and email id resets email_address record and sends new verification email", async () => { - const testSubscriberEmail = TEST_SUBSCRIBERS.firefox_account.primary_email; - const testSubscriber = await DB.getSubscriberByEmail(testSubscriberEmail); - const testEmailAddressId = TEST_EMAIL_ADDRESSES.unverified_email_on_firefox_account.id; - const startingTestEmailAddress = await DB.getEmailById(testEmailAddressId); +test('user resendEmail with valid session and email id resets email_address record and sends new verification email', async () => { + const testSubscriberEmail = TEST_SUBSCRIBERS.firefox_account.primary_email + const testSubscriber = await DB.getSubscriberByEmail(testSubscriberEmail) + const testEmailAddressId = TEST_EMAIL_ADDRESSES.unverified_email_on_firefox_account.id + const startingTestEmailAddress = await DB.getEmailById(testEmailAddressId) // Set up mocks const req = httpMocks.createRequest({ - method: "POST", - url: "/user/resend-email", + method: 'POST', + url: '/user/resend-email', body: { emailId: testEmailAddressId }, session: { user: testSubscriber }, fluentFormat: jest.fn(), - user: testSubscriber, - }); - const resp = httpMocks.createResponse(); - EmailUtils.sendEmail.mockResolvedValue(true); + user: testSubscriber + }) + const resp = httpMocks.createResponse() + EmailUtils.sendEmail.mockResolvedValue(true) // Call code-under-test - await user.resendEmail(req, resp); + await user.resendEmail(req, resp) // Check expectations - expect(resp.statusCode).toEqual(200); - const resetTestEmailAddress = await DB.getEmailById(testEmailAddressId); - expect(startingTestEmailAddress.verification_token).not.toEqual(resetTestEmailAddress.verification_token); -}); + expect(resp.statusCode).toEqual(200) + const resetTestEmailAddress = await DB.getEmailById(testEmailAddressId) + expect(startingTestEmailAddress.verification_token).not.toEqual(resetTestEmailAddress.verification_token) +}) - -test("user updateCommunicationOptions request with valid session updates DB", async () => { - const testSubscriberEmail = TEST_SUBSCRIBERS.firefox_account.primary_email; - const testSubscriber = await DB.getSubscriberByEmail(testSubscriberEmail); +test('user updateCommunicationOptions request with valid session updates DB', async () => { + const testSubscriberEmail = TEST_SUBSCRIBERS.firefox_account.primary_email + const testSubscriber = await DB.getSubscriberByEmail(testSubscriberEmail) const req = httpMocks.createRequest({ - method: "POST", - url: "/user/update-comm-option", + method: 'POST', + url: '/user/update-comm-option', body: { communicationOption: 0 }, session: { user: testSubscriber }, - user: testSubscriber, - }); - const resp = httpMocks.createResponse(); + user: testSubscriber + }) + const resp = httpMocks.createResponse() // Call code-under-test - await user.updateCommunicationOptions(req, resp); + await user.updateCommunicationOptions(req, resp) // Check expectations - expect(resp.statusCode).toEqual(200); - const updatedTestSubscriber = await DB.getSubscriberByEmail(testSubscriberEmail); - expect(updatedTestSubscriber.all_emails_to_primary).toBeFalsy(); + expect(resp.statusCode).toEqual(200) + const updatedTestSubscriber = await DB.getSubscriberByEmail(testSubscriberEmail) + expect(updatedTestSubscriber.all_emails_to_primary).toBeFalsy() - - req.body = { communicationOption: 1 }; + req.body = { communicationOption: 1 } // Call code-under-test - await user.updateCommunicationOptions(req, resp); - - expect(resp.statusCode).toEqual(200); - const againUpdatedTestSubscriber = await DB.getSubscriberByEmail(testSubscriberEmail); - expect(againUpdatedTestSubscriber.all_emails_to_primary).toBeTruthy(); -}); + await user.updateCommunicationOptions(req, resp) + expect(resp.statusCode).toEqual(200) + const againUpdatedTestSubscriber = await DB.getSubscriberByEmail(testSubscriberEmail) + expect(againUpdatedTestSubscriber.all_emails_to_primary).toBeTruthy() +}) // TODO: more tests of resendEmail failure scenarios - -test("user add request with invalid email throws error", async () => { - const testSubscriberEmail = "firefoxaccount@test.com"; - const testSubscriber = await DB.getSubscriberByEmail(testSubscriberEmail); - - // Set up mocks - const req = httpMocks.createRequest({ - method: "POST", - url: "/user/add", - body: { email: "a" }, - session: { user: testSubscriber }, - fluentFormat: jest.fn(), - }); - const resp = httpMocks.createResponse(); - - // Call code-under-test - await expect(user.add(req, resp)).rejects.toThrow("user-add-invalid-email"); -}); - - -test("user verify request with valid token but no session renders email verified page", async () => { - const validToken = TEST_EMAIL_ADDRESSES.unverified_email_on_firefox_account.verification_token; - const mockReturnedBreaches = testBreaches.slice(0,2); - HIBP.subscribeHash = jest.fn(); - HIBP.getBreachesForEmail = jest.fn(); - HIBP.getBreachesForEmail.mockReturnValue(mockReturnedBreaches); - - const req = httpMocks.createRequest({ - method: "GET", - url: `/user/verify?token=${validToken}`, - fluentFormat: jest.fn(), - app: { locals: { breaches: testBreaches } }, - }); - const resp = httpMocks.createResponse(); - - // Call code-under-test - await user.verify(req, resp); - - expect(resp.statusCode).toEqual(200); - const emailAddress = await DB.getEmailByToken(validToken); - expect(emailAddress.verified).toBeTruthy(); -}); - - -test("user verify request with valid token verifies user and redirects to dashboard", async () => { - const validToken = TEST_EMAIL_ADDRESSES.unverified_email_on_firefox_account.verification_token; - const testSubscriberEmail = "firefoxaccount@test.com"; - const testSubscriber = await DB.getSubscriberByEmail(testSubscriberEmail); - const mockReturnedBreaches = testBreaches.slice(0,2); - HIBP.subscribeHash = jest.fn(); - HIBP.getBreachesForEmail = jest.fn(); - HIBP.getBreachesForEmail.mockReturnValue(mockReturnedBreaches); - - const req = httpMocks.createRequest({ - method: "GET", - url: `/user/verify?token=${validToken}`, - session: { user: testSubscriber }, - fluentFormat: jest.fn(), - app: { locals: { breaches: testBreaches } }, - user: testSubscriber, - }); - const resp = httpMocks.createResponse(); - - // Call code-under-test - await user.verify(req, resp); - - expect(resp.statusCode).toEqual(302); - const emailAddress = await DB.getEmailByToken(validToken); - expect(emailAddress.verified).toBeTruthy(); -}); - - -test("user verify request with valid token but wrong user session does NOT verify email address", async () => { - const validToken = TEST_EMAIL_ADDRESSES.unverified_email_on_firefox_account.verification_token; - const testSubscriberEmail = "verifiedemail@test.com"; - const testSubscriber = await DB.getSubscriberByEmail(testSubscriberEmail); - - const req = httpMocks.createRequest({ - method: "GET", - url: `/user/verify?token=${validToken}`, - session: { user: testSubscriber }, - fluentFormat: jest.fn(), - app: { locals: { breaches: testBreaches } }, - user: testSubscriber, - }); - const resp = httpMocks.createResponse(); - - // Call code-under-test - await expect(user.verify(req, resp)).rejects.toThrow("user-verify-token-error"); - - const emailAddress = await DB.getEmailByToken(validToken); - expect(emailAddress.verified).toBeFalsy(); -}); - - -test("user verify request for already verified user doesn't send extra email", async () => { - const alreadyVerifiedToken = TEST_EMAIL_ADDRESSES.firefox_account.verification_token; - const testSubscriberEmail = "firefoxaccount@test.com"; - const testSubscriber = await DB.getSubscriberByEmail(testSubscriberEmail); - - // Set up mocks - EmailUtils.sendEmail = jest.fn(); - mockRequest.session = { user: testSubscriber }; - mockRequest.query = { token: alreadyVerifiedToken }; - mockRequest.app = { locals: { breaches: testBreaches } }; - mockRequest.user = testSubscriber; - const resp = httpMocks.createResponse(); - - // Call code-under-test - await user.verify(mockRequest, resp); - - expect(resp.statusCode).toEqual(302); - const emailAddress = await DB.getEmailByToken(alreadyVerifiedToken); - expect(emailAddress.verified).toBeTruthy(); - expect(EmailUtils.sendEmail).not.toHaveBeenCalled(); -}); - - -test("user verify request with invalid token returns error", async () => { - const invalidToken = "123456789"; - const testSubscriberEmail = "firefoxaccount@test.com"; - const testSubscriber = await DB.getSubscriberByEmail(testSubscriberEmail); +test('user add request with invalid email throws error', async () => { + const testSubscriberEmail = 'firefoxaccount@test.com' + const testSubscriber = await DB.getSubscriberByEmail(testSubscriberEmail) // Set up mocks const req = httpMocks.createRequest({ - method: "GET", - url: `/user/verify?token=${invalidToken}`, + method: 'POST', + url: '/user/add', + body: { email: 'a' }, + session: { user: testSubscriber }, + fluentFormat: jest.fn() + }) + const resp = httpMocks.createResponse() + + // Call code-under-test + await expect(user.add(req, resp)).rejects.toThrow('user-add-invalid-email') +}) + +test('user verify request with valid token but no session renders email verified page', async () => { + const validToken = TEST_EMAIL_ADDRESSES.unverified_email_on_firefox_account.verification_token + const mockReturnedBreaches = testBreaches.slice(0, 2) + HIBP.subscribeHash = jest.fn() + HIBP.getBreachesForEmail = jest.fn() + HIBP.getBreachesForEmail.mockReturnValue(mockReturnedBreaches) + + const req = httpMocks.createRequest({ + method: 'GET', + url: `/user/verify?token=${validToken}`, + fluentFormat: jest.fn(), + app: { locals: { breaches: testBreaches } } + }) + const resp = httpMocks.createResponse() + + // Call code-under-test + await user.verify(req, resp) + + expect(resp.statusCode).toEqual(200) + const emailAddress = await DB.getEmailByToken(validToken) + expect(emailAddress.verified).toBeTruthy() +}) + +test('user verify request with valid token verifies user and redirects to dashboard', async () => { + const validToken = TEST_EMAIL_ADDRESSES.unverified_email_on_firefox_account.verification_token + const testSubscriberEmail = 'firefoxaccount@test.com' + const testSubscriber = await DB.getSubscriberByEmail(testSubscriberEmail) + const mockReturnedBreaches = testBreaches.slice(0, 2) + HIBP.subscribeHash = jest.fn() + HIBP.getBreachesForEmail = jest.fn() + HIBP.getBreachesForEmail.mockReturnValue(mockReturnedBreaches) + + const req = httpMocks.createRequest({ + method: 'GET', + url: `/user/verify?token=${validToken}`, session: { user: testSubscriber }, fluentFormat: jest.fn(), - }); + app: { locals: { breaches: testBreaches } }, + user: testSubscriber + }) + const resp = httpMocks.createResponse() - const resp = httpMocks.createResponse(); + // Call code-under-test + await user.verify(req, resp) - await expect(user.verify(req, resp)).rejects.toThrow("error-not-subscribed"); -}); + expect(resp.statusCode).toEqual(302) + const emailAddress = await DB.getEmailByToken(validToken) + expect(emailAddress.verified).toBeTruthy() +}) +test('user verify request with valid token but wrong user session does NOT verify email address', async () => { + const validToken = TEST_EMAIL_ADDRESSES.unverified_email_on_firefox_account.verification_token + const testSubscriberEmail = 'verifiedemail@test.com' + const testSubscriber = await DB.getSubscriberByEmail(testSubscriberEmail) -test("user unsubscribe GET request with valid token and hash for primary/subscriber record returns 302 to preferences", async () => { + const req = httpMocks.createRequest({ + method: 'GET', + url: `/user/verify?token=${validToken}`, + session: { user: testSubscriber }, + fluentFormat: jest.fn(), + app: { locals: { breaches: testBreaches } }, + user: testSubscriber + }) + const resp = httpMocks.createResponse() + + // Call code-under-test + await expect(user.verify(req, resp)).rejects.toThrow('user-verify-token-error') + + const emailAddress = await DB.getEmailByToken(validToken) + expect(emailAddress.verified).toBeFalsy() +}) + +test("user verify request for already verified user doesn't send extra email", async () => { + const alreadyVerifiedToken = TEST_EMAIL_ADDRESSES.firefox_account.verification_token + const testSubscriberEmail = 'firefoxaccount@test.com' + const testSubscriber = await DB.getSubscriberByEmail(testSubscriberEmail) + + // Set up mocks + EmailUtils.sendEmail = jest.fn() + mockRequest.session = { user: testSubscriber } + mockRequest.query = { token: alreadyVerifiedToken } + mockRequest.app = { locals: { breaches: testBreaches } } + mockRequest.user = testSubscriber + const resp = httpMocks.createResponse() + + // Call code-under-test + await user.verify(mockRequest, resp) + + expect(resp.statusCode).toEqual(302) + const emailAddress = await DB.getEmailByToken(alreadyVerifiedToken) + expect(emailAddress.verified).toBeTruthy() + expect(EmailUtils.sendEmail).not.toHaveBeenCalled() +}) + +test('user verify request with invalid token returns error', async () => { + const invalidToken = '123456789' + const testSubscriberEmail = 'firefoxaccount@test.com' + const testSubscriber = await DB.getSubscriberByEmail(testSubscriberEmail) + + // Set up mocks + const req = httpMocks.createRequest({ + method: 'GET', + url: `/user/verify?token=${invalidToken}`, + session: { user: testSubscriber }, + fluentFormat: jest.fn() + }) + + const resp = httpMocks.createResponse() + + await expect(user.verify(req, resp)).rejects.toThrow('error-not-subscribed') +}) + +test('user unsubscribe GET request with valid token and hash for primary/subscriber record returns 302 to preferences', async () => { // from db/seeds/test_subscribers.js - const subscriberToken = TEST_SUBSCRIBERS.firefox_account.primary_verification_token; - const subscriberHash = getSha1(TEST_SUBSCRIBERS.firefox_account.primary_email); + const subscriberToken = TEST_SUBSCRIBERS.firefox_account.primary_verification_token + const subscriberHash = getSha1(TEST_SUBSCRIBERS.firefox_account.primary_email) // Set up mocks - const req = { fluentFormat: jest.fn(), query: { token: subscriberToken, hash: subscriberHash } }; - const resp = httpMocks.createResponse(); + const req = { fluentFormat: jest.fn(), query: { token: subscriberToken, hash: subscriberHash } } + const resp = httpMocks.createResponse() // Call code-under-test - await user.getUnsubscribe(req, resp); + await user.getUnsubscribe(req, resp) - expect(resp.statusCode).toEqual(302); - expect(resp._getRedirectUrl()).toEqual("/user/preferences"); -}); + expect(resp.statusCode).toEqual(302) + expect(resp._getRedirectUrl()).toEqual('/user/preferences') +}) - -test("user unsubscribe GET request with valid token and hash for a secondary email_addresses record renders unsubscribe", async () => { +test('user unsubscribe GET request with valid token and hash for a secondary email_addresses record renders unsubscribe', async () => { // from db/seeds/test_subscribers.js - const subscriberToken = TEST_EMAIL_ADDRESSES.firefox_account.verification_token; - const subscriberHash = getSha1(TEST_EMAIL_ADDRESSES.firefox_account.email); + const subscriberToken = TEST_EMAIL_ADDRESSES.firefox_account.verification_token + const subscriberHash = getSha1(TEST_EMAIL_ADDRESSES.firefox_account.email) // Set up mocks - const req = { fluentFormat: jest.fn(), query: { token: subscriberToken, hash: subscriberHash } }; - const resp = httpMocks.createResponse(); - resp.render = jest.fn(); + const req = { fluentFormat: jest.fn(), query: { token: subscriberToken, hash: subscriberHash } } + const resp = httpMocks.createResponse() + resp.render = jest.fn() // Call code-under-test - await user.getUnsubscribe(req, resp); + await user.getUnsubscribe(req, resp) - expectResponseRenderedSubpagePartial(resp, "subpages/unsubscribe"); -}); + expectResponseRenderedSubpagePartial(resp, 'subpages/unsubscribe') +}) - -test("user unsubscribe GET request with valid token and hash for an old pre-FxA subscriber record renders unsubscribe", async () => { +test('user unsubscribe GET request with valid token and hash for an old pre-FxA subscriber record renders unsubscribe', async () => { // from db/seeds/test_subscribers.js - const subscriberToken = TEST_SUBSCRIBERS.verified_email.primary_verification_token; - const subscriberHash = getSha1(TEST_SUBSCRIBERS.firefox_account.primary_email); + const subscriberToken = TEST_SUBSCRIBERS.verified_email.primary_verification_token + const subscriberHash = getSha1(TEST_SUBSCRIBERS.firefox_account.primary_email) // Set up mocks - const req = { fluentFormat: jest.fn(), query: { token: subscriberToken, hash: subscriberHash } }; - const resp = httpMocks.createResponse(); - resp.render = jest.fn(); + const req = { fluentFormat: jest.fn(), query: { token: subscriberToken, hash: subscriberHash } } + const resp = httpMocks.createResponse() + resp.render = jest.fn() // Call code-under-test - await user.getUnsubscribe(req, resp); + await user.getUnsubscribe(req, resp) - expectResponseRenderedSubpagePartial(resp, "subpages/unsubscribe"); -}); + expectResponseRenderedSubpagePartial(resp, 'subpages/unsubscribe') +}) - -test("user unsubscribe POST request with valid session and emailId for email_address removes from DB", async () => { - const validToken = TEST_EMAIL_ADDRESSES.firefox_account.verification_token; - const validHash = TEST_EMAIL_ADDRESSES.firefox_account.sha1; +test('user unsubscribe POST request with valid session and emailId for email_address removes from DB', async () => { + const validToken = TEST_EMAIL_ADDRESSES.firefox_account.verification_token + const validHash = TEST_EMAIL_ADDRESSES.firefox_account.sha1 // Set up mocks - const req = { fluentFormat: jest.fn(), body: { token: validToken, emailHash: validHash }, session: { user: TEST_SUBSCRIBERS.firefox_account }}; - const resp = httpMocks.createResponse(); + const req = { fluentFormat: jest.fn(), body: { token: validToken, emailHash: validHash }, session: { user: TEST_SUBSCRIBERS.firefox_account } } + const resp = httpMocks.createResponse() // Call code-under-test - await user.postUnsubscribe(req, resp); + await user.postUnsubscribe(req, resp) - expect(resp.statusCode).toEqual(302); - expect(resp._getRedirectUrl()).toEqual("/user/preferences"); - const emailAddress = await DB.getEmailByToken(validToken); - expect(emailAddress).toBeUndefined(); -}); + expect(resp.statusCode).toEqual(302) + expect(resp._getRedirectUrl()).toEqual('/user/preferences') + const emailAddress = await DB.getEmailByToken(validToken) + expect(emailAddress).toBeUndefined() +}) - -test("user removeEmail POST request with valid session but wrong emailId for email_address throws error and doesnt remove email", async () => { - const testEmailAddress = TEST_EMAIL_ADDRESSES.all_emails_to_primary; - const testEmailId = testEmailAddress.id; +test('user removeEmail POST request with valid session but wrong emailId for email_address throws error and doesnt remove email', async () => { + const testEmailAddress = TEST_EMAIL_ADDRESSES.all_emails_to_primary + const testEmailId = testEmailAddress.id const req = { fluentFormat: jest.fn(), body: { emailId: testEmailId }, session: { user: TEST_SUBSCRIBERS.firefox_account }, - user: TEST_SUBSCRIBERS.firefox_account, - }; - const resp = httpMocks.createResponse(); + user: TEST_SUBSCRIBERS.firefox_account + } + const resp = httpMocks.createResponse() - await expect(user.removeEmail(req, resp)).rejects.toThrow("error-not-subscribed"); + await expect(user.removeEmail(req, resp)).rejects.toThrow('error-not-subscribed') - const emailAddress = await DB.getEmailByToken(testEmailAddress.verification_token); - expect(emailAddress.id).toEqual(testEmailId); -}); + const emailAddress = await DB.getEmailByToken(testEmailAddress.verification_token) + expect(emailAddress.id).toEqual(testEmailId) +}) - -test("user/remove-fxm GET request with valid session returns 200 and renders remove_fxm", async () => { +test('user/remove-fxm GET request with valid session returns 200 and renders remove_fxm', async () => { // Set up mocks - const req = { fluentFormat: jest.fn(), csrfToken: jest.fn(), session: { user: TEST_SUBSCRIBERS.firefox_account }}; - const resp = httpMocks.createResponse(); - resp.render = jest.fn(); + const req = { fluentFormat: jest.fn(), csrfToken: jest.fn(), session: { user: TEST_SUBSCRIBERS.firefox_account } } + const resp = httpMocks.createResponse() + resp.render = jest.fn() // Call code-under-test - await user.getRemoveFxm(req, resp); + await user.getRemoveFxm(req, resp) - expectResponseRenderedSubpagePartial(resp, "subpages/remove_fxm"); -}); + expectResponseRenderedSubpagePartial(resp, 'subpages/remove_fxm') +}) - -test("user remove-fxm POST request with valid session removes from DB and revokes FXA OAuth token", async () => { +test('user remove-fxm POST request with valid session removes from DB and revokes FXA OAuth token', async () => { const req = { fluentFormat: jest.fn(), session: { user: TEST_SUBSCRIBERS.firefox_account, destroy: jest.fn() }, - user: TEST_SUBSCRIBERS.firefox_account, - }; - const resp = httpMocks.createResponse(); - FXA.revokeOAuthTokens = jest.fn(); + user: TEST_SUBSCRIBERS.firefox_account + } + const resp = httpMocks.createResponse() + FXA.revokeOAuthTokens = jest.fn() - await user.postRemoveFxm(req, resp); + await user.postRemoveFxm(req, resp) - expect(resp.statusCode).toEqual(302); - expect(resp._getRedirectUrl()).toEqual("/"); - const subscriber = await DB.getEmailByToken(TEST_SUBSCRIBERS.firefox_account.primary_verification_token); - expect(subscriber).toBeUndefined(); - expect(FXA.revokeOAuthTokens).toHaveBeenCalledTimes(1); - expect(req.session.destroy).toHaveBeenCalledTimes(1); -}); + expect(resp.statusCode).toEqual(302) + expect(resp._getRedirectUrl()).toEqual('/') + const subscriber = await DB.getEmailByToken(TEST_SUBSCRIBERS.firefox_account.primary_verification_token) + expect(subscriber).toBeUndefined() + expect(FXA.revokeOAuthTokens).toHaveBeenCalledTimes(1) + expect(req.session.destroy).toHaveBeenCalledTimes(1) +}) - -test("user unsubscribe GET request with invalid token returns error", async () => { - const invalidToken = "123456789"; +test('user unsubscribe GET request with invalid token returns error', async () => { + const invalidToken = '123456789' const req = httpMocks.createRequest({ - method: "GET", + method: 'GET', url: `/user/unsubscribe?token=${invalidToken}`, - fluentFormat: jest.fn(), - }); - const resp = httpMocks.createResponse(); + fluentFormat: jest.fn() + }) + const resp = httpMocks.createResponse() - await expect(user.getUnsubscribe(req, resp)).rejects.toThrow("error-not-subscribed"); -}); + await expect(user.getUnsubscribe(req, resp)).rejects.toThrow('error-not-subscribed') +}) - -test("user unsubscribe POST request with valid hash and token for email_address removes from DB", async () => { - const validToken = TEST_EMAIL_ADDRESSES.firefox_account.verification_token; - const validHash = TEST_EMAIL_ADDRESSES.firefox_account.sha1; +test('user unsubscribe POST request with valid hash and token for email_address removes from DB', async () => { + const validToken = TEST_EMAIL_ADDRESSES.firefox_account.verification_token + const validHash = TEST_EMAIL_ADDRESSES.firefox_account.sha1 // Set up mocks - const req = { fluentFormat: jest.fn(), body: { token: validToken, emailHash: validHash }, session: {}}; - const resp = httpMocks.createResponse(); + const req = { fluentFormat: jest.fn(), body: { token: validToken, emailHash: validHash }, session: {} } + const resp = httpMocks.createResponse() // Call code-under-test - await user.postUnsubscribe(req, resp); + await user.postUnsubscribe(req, resp) - expect(resp.statusCode).toEqual(302); - expect(resp._getRedirectUrl()).toEqual("/user/preferences"); - const emailAddress = await DB.getEmailByToken(validToken); - expect(emailAddress).toBeUndefined(); -}); + expect(resp.statusCode).toEqual(302) + expect(resp._getRedirectUrl()).toEqual('/user/preferences') + const emailAddress = await DB.getEmailByToken(validToken) + expect(emailAddress).toBeUndefined() +}) +test('user unsubscribe POST request with invalid token and throws error', async () => { + const invalidToken = '123456789' + const invalidHash = '0123456789abcdef' -test("user unsubscribe POST request with invalid token and throws error", async () => { - const invalidToken = "123456789"; - const invalidHash = "0123456789abcdef"; + const req = { fluentFormat: jest.fn(), body: { token: invalidToken, emailHash: invalidHash } } + const resp = { redirect: jest.fn() } - const req = { fluentFormat: jest.fn(), body: { token: invalidToken, emailHash: invalidHash } }; - const resp = { redirect: jest.fn() }; + await expect(user.postUnsubscribe(req, resp)).rejects.toThrow('error-not-subscribed') +}) - await expect(user.postUnsubscribe(req, resp)).rejects.toThrow("error-not-subscribed"); -}); +test('user breach-stats POST request with no token responds unauthorized', async () => { + const req = { } + const mockStatus = jest.fn() + const mockJson = { json: jest.fn() } + mockStatus.mockReturnValueOnce(mockJson) + const resp = { status: mockStatus } -test("user breach-stats POST request with no token responds unauthorized", async () => { - const req = { }; - const mockStatus = jest.fn(); - const mockJson = { json: jest.fn() }; - mockStatus.mockReturnValueOnce(mockJson); + await user.getBreachStats(req, resp) - const resp = { status: mockStatus }; + const statusCallArgs = mockStatus.mock.calls[0] + const jsonCallArgs = mockJson.json.mock.calls[0] - await user.getBreachStats(req, resp); + expect(statusCallArgs[0]).toEqual(401) + expect(jsonCallArgs[0].errorMessage).toMatch('Authorization') +}) - const statusCallArgs = mockStatus.mock.calls[0]; - const jsonCallArgs = mockJson.json.mock.calls[0]; +test('user breach-stats POST request with FXA http error responds with FXA error', async () => { + const mockFXAStatusCode = '1234' + const req = { token: 'test-token' } + const mockStatus = jest.fn() + const mockJson = { json: jest.fn() } + mockStatus.mockReturnValueOnce(mockJson) + FXA.verifyOAuthToken = jest.fn() + FXA.verifyOAuthToken.mockReturnValueOnce({ name: 'HTTPError', response: { statusCode: mockFXAStatusCode } }) - expect(statusCallArgs[0]).toEqual(401); - expect(jsonCallArgs[0].errorMessage).toMatch("Authorization"); -}); + const resp = { status: mockStatus } + await user.getBreachStats(req, resp) -test("user breach-stats POST request with FXA http error responds with FXA error", async () => { - const mockFXAStatusCode = "1234"; - const req = { token: "test-token" }; - const mockStatus = jest.fn(); - const mockJson = { json: jest.fn() }; - mockStatus.mockReturnValueOnce(mockJson); - FXA.verifyOAuthToken = jest.fn(); - FXA.verifyOAuthToken.mockReturnValueOnce({name: "HTTPError", response: {statusCode: mockFXAStatusCode }}); + const statusCallArgs = mockStatus.mock.calls[0] + const jsonCallArgs = mockJson.json.mock.calls[0] - const resp = { status: mockStatus }; + expect(statusCallArgs[0]).toEqual(mockFXAStatusCode) + expect(jsonCallArgs[0].errorMessage).toMatch('FXA returned message') +}) - await user.getBreachStats(req, resp); +test('user breach-stats POST request with FXA response that has no Monitor scope responds unauthorized', async () => { + const req = { token: 'test-token' } + const mockStatus = jest.fn() + const mockJson = { json: jest.fn() } + mockStatus.mockReturnValueOnce(mockJson) + FXA.verifyOAuthToken = jest.fn() + FXA.verifyOAuthToken.mockReturnValueOnce({ body: { scope: [] } }) - const statusCallArgs = mockStatus.mock.calls[0]; - const jsonCallArgs = mockJson.json.mock.calls[0]; + const resp = { status: mockStatus } - expect(statusCallArgs[0]).toEqual(mockFXAStatusCode); - expect(jsonCallArgs[0].errorMessage).toMatch("FXA returned message"); -}); + await user.getBreachStats(req, resp) + const statusCallArgs = mockStatus.mock.calls[0] + const jsonCallArgs = mockJson.json.mock.calls[0] -test("user breach-stats POST request with FXA response that has no Monitor scope responds unauthorized", async () => { - const req = { token: "test-token" }; - const mockStatus = jest.fn(); - const mockJson = { json: jest.fn() }; - mockStatus.mockReturnValueOnce(mockJson); - FXA.verifyOAuthToken = jest.fn(); - FXA.verifyOAuthToken.mockReturnValueOnce({body: { scope: [] } }); + expect(statusCallArgs[0]).toEqual(401) + expect(jsonCallArgs[0].errorMessage).toMatch('Monitor scope') +}) - const resp = { status: mockStatus }; - - await user.getBreachStats(req, resp); - - const statusCallArgs = mockStatus.mock.calls[0]; - const jsonCallArgs = mockJson.json.mock.calls[0]; - - expect(statusCallArgs[0]).toEqual(401); - expect(jsonCallArgs[0].errorMessage).toMatch("Monitor scope"); -}); - - -test("user breach-stats POST request with FXA response for a user unknown to Monitor returns 404", async () => { - const req = { token: "test-token" }; - const mockStatus = jest.fn(); - const mockJson = { json: jest.fn() }; - mockStatus.mockReturnValueOnce(mockJson); - FXA.verifyOAuthToken = jest.fn(); +test('user breach-stats POST request with FXA response for a user unknown to Monitor returns 404', async () => { + const req = { token: 'test-token' } + const mockStatus = jest.fn() + const mockJson = { json: jest.fn() } + mockStatus.mockReturnValueOnce(mockJson) + FXA.verifyOAuthToken = jest.fn() FXA.verifyOAuthToken.mockReturnValueOnce({ - body: { - scope: [user.FXA_MONITOR_SCOPE], - user: "unknown-fxa-uid", - }, - }); + body: { + scope: [user.FXA_MONITOR_SCOPE], + user: 'unknown-fxa-uid' + } + }) - const resp = { status: mockStatus }; + const resp = { status: mockStatus } - await user.getBreachStats(req, resp); + await user.getBreachStats(req, resp) - const statusCallArgs = mockStatus.mock.calls[0]; - const jsonCallArgs = mockJson.json.mock.calls[0]; + const statusCallArgs = mockStatus.mock.calls[0] + const jsonCallArgs = mockJson.json.mock.calls[0] - expect(statusCallArgs[0]).toEqual(404); - expect(jsonCallArgs[0].errorMessage).toMatch("Cannot find Monitor subscriber"); -}); + expect(statusCallArgs[0]).toEqual(404) + expect(jsonCallArgs[0].errorMessage).toMatch('Cannot find Monitor subscriber') +}) - -test("user breach-stats POST request with FXA response for Monitor user returns breach stats json", async () => { - const testSubscriberFxAUID = TEST_SUBSCRIBERS.firefox_account.fxa_uid; +test('user breach-stats POST request with FXA response for Monitor user returns breach stats json', async () => { + const testSubscriberFxAUID = TEST_SUBSCRIBERS.firefox_account.fxa_uid const req = { - token: "test-token", + token: 'test-token', app: { locals: { breaches: testBreaches } }, - query: {}, - }; - FXA.verifyOAuthToken = jest.fn(); + query: {} + } + FXA.verifyOAuthToken = jest.fn() FXA.verifyOAuthToken.mockReturnValueOnce({ - body: { - scope: [user.FXA_MONITOR_SCOPE], - user: testSubscriberFxAUID, - }, - }); - HIBP.getBreachesForEmail = jest.fn(); - HIBP.getBreachesForEmail.mockReturnValue([]); + body: { + scope: [user.FXA_MONITOR_SCOPE], + user: testSubscriberFxAUID + } + }) + HIBP.getBreachesForEmail = jest.fn() + HIBP.getBreachesForEmail.mockReturnValue([]) - const resp = { json: jest.fn() }; + const resp = { json: jest.fn() } - await user.getBreachStats(req, resp); + await user.getBreachStats(req, resp) - const jsonCallArgs = resp.json.mock.calls[0]; + const jsonCallArgs = resp.json.mock.calls[0] expect(jsonCallArgs[0]).toMatchObject({ monitoredEmails: expect.anything(), numBreaches: expect.anything(), - passwords: expect.anything(), - }); -}); + passwords: expect.anything() + }) +}) - -test("user breach-stats POST request with includeResolved returns breach stats json with resolved", async () => { - const testSubscriberFxAUID = TEST_SUBSCRIBERS.firefox_account.fxa_uid; +test('user breach-stats POST request with includeResolved returns breach stats json with resolved', async () => { + const testSubscriberFxAUID = TEST_SUBSCRIBERS.firefox_account.fxa_uid const req = { - token: "test-token", + token: 'test-token', app: { locals: { breaches: testBreaches } }, - query: {includeResolved: "true"}, - }; - FXA.verifyOAuthToken = jest.fn(); + query: { includeResolved: 'true' } + } + FXA.verifyOAuthToken = jest.fn() FXA.verifyOAuthToken.mockReturnValueOnce({ - body: { - scope: [user.FXA_MONITOR_SCOPE], - user: testSubscriberFxAUID, - }, - }); - HIBP.getBreachesForEmail = jest.fn(); - HIBP.getBreachesForEmail.mockReturnValue([]); + body: { + scope: [user.FXA_MONITOR_SCOPE], + user: testSubscriberFxAUID + } + }) + HIBP.getBreachesForEmail = jest.fn() + HIBP.getBreachesForEmail.mockReturnValue([]) - const resp = { json: jest.fn() }; + const resp = { json: jest.fn() } - await user.getBreachStats(req, resp); + await user.getBreachStats(req, resp) - const jsonCallArgs = resp.json.mock.calls[0]; + const jsonCallArgs = resp.json.mock.calls[0] expect(jsonCallArgs[0]).toMatchObject({ monitoredEmails: expect.anything(), numBreaches: expect.anything(), passwords: expect.anything(), numBreachesResolved: expect.anything(), - passwordsResolved: expect.anything(), - }); -}); + passwordsResolved: expect.anything() + }) +}) diff --git a/tests/db.test.js b/tests/db.test.js index a178c1302..197af212f 100644 --- a/tests/db.test.js +++ b/tests/db.test.js @@ -1,219 +1,201 @@ -"use strict"; +'use strict' -const HIBP = require("../hibp"); -const DB = require("../db/DB"); -const { TEST_SUBSCRIBERS, TEST_EMAIL_ADDRESSES } = require("../db/seeds/test_subscribers"); -const getSha1 = require("../sha1-utils"); +const HIBP = require('../hibp') +const DB = require('../db/DB') +const { TEST_SUBSCRIBERS, TEST_EMAIL_ADDRESSES } = require('../db/seeds/test_subscribers') +const getSha1 = require('../sha1-utils') -require("./resetDB"); +require('./resetDB') +jest.mock('../hibp') -jest.mock("../hibp"); - - -function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); +function sleep (ms) { + return new Promise(resolve => setTimeout(resolve, ms)) } +test('getSubscriberByToken accepts token and returns subscriber', async () => { + const testEmail = 'unverifiedemail@test.com' + const testToken = '0e2cb147-2041-4e5b-8ca9-494e773b2cf0' + const subscriber = await DB.getSubscriberByToken(testToken) -test("getSubscriberByToken accepts token and returns subscriber", async () => { - const testEmail = "unverifiedemail@test.com"; - const testToken = "0e2cb147-2041-4e5b-8ca9-494e773b2cf0"; - const subscriber = await DB.getSubscriberByToken(testToken); + expect(subscriber.primary_email).toBe(testEmail) + expect(subscriber.primary_verification_token).toBe(testToken) +}) - expect(subscriber.primary_email).toBe(testEmail); - expect(subscriber.primary_verification_token).toBe(testToken); -}); - -test("getSubscribersByHashes accepts hashes and only returns verified subscribers", async () => { +test('getSubscribersByHashes accepts hashes and only returns verified subscribers', async () => { const testHashes = [ - "firefoxaccount@test.com", - "unverifiedemail@test.com", - "verifiedemail@test.com", - ].map(email => getSha1(email)); - const subscribers = await DB.getSubscribersByHashes(testHashes); + 'firefoxaccount@test.com', + 'unverifiedemail@test.com', + 'verifiedemail@test.com' + ].map(email => getSha1(email)) + const subscribers = await DB.getSubscribersByHashes(testHashes) for (const subscriber of subscribers) { - expect(subscriber.primary_verified).toBeTruthy(); + expect(subscriber.primary_verified).toBeTruthy() } -}); +}) - -test("getEmailAddressesByHashes accepts hashes and only returns verified email_addresses", async () => { +test('getEmailAddressesByHashes accepts hashes and only returns verified email_addresses', async () => { const testHashes = [ - "firefoxaccount-secondary@test.com", - "firefoxaccount-tertiary@test.com", - ].map(email => getSha1(email)); - const emailAddresses = await DB.getEmailAddressesByHashes(testHashes); + 'firefoxaccount-secondary@test.com', + 'firefoxaccount-tertiary@test.com' + ].map(email => getSha1(email)) + const emailAddresses = await DB.getEmailAddressesByHashes(testHashes) for (const emailAddress of emailAddresses) { - expect(emailAddress.verified).toBeTruthy(); + expect(emailAddress.verified).toBeTruthy() } -}); +}) +test('getEmailByToken accepts token and returns email_addresses record', async () => { + const testEmailAddress = TEST_EMAIL_ADDRESSES.firefox_account + const emailAddress = await DB.getEmailByToken(testEmailAddress.verification_token) + expect(emailAddress.email).toEqual(testEmailAddress.email) +}) -test("getEmailByToken accepts token and returns email_addresses record", async () => { - const testEmailAddress = TEST_EMAIL_ADDRESSES.firefox_account; - const emailAddress = await DB.getEmailByToken(testEmailAddress.verification_token); - expect(emailAddress.email).toEqual(testEmailAddress.email); -}); +test('getEmailById accepts id and returns email_addresses record', async () => { + const testEmailAddress = TEST_EMAIL_ADDRESSES.unverified_email_on_firefox_account + const emailAddress = await DB.getEmailById(testEmailAddress.id) + expect(emailAddress.email).toEqual(testEmailAddress.email) +}) - -test("getEmailById accepts id and returns email_addresses record", async () => { - const testEmailAddress = TEST_EMAIL_ADDRESSES.unverified_email_on_firefox_account; - const emailAddress = await DB.getEmailById(testEmailAddress.id); - expect(emailAddress.email).toEqual(testEmailAddress.email); -}); - - -test("addSubscriberUnverifiedEmailHash accepts user and email and returns unverified email_address with sha1 hash and verification token", async () => { - const testEmail = "test@test.com"; +test('addSubscriberUnverifiedEmailHash accepts user and email and returns unverified email_address with sha1 hash and verification token', async () => { + const testEmail = 'test@test.com' // https://stackoverflow.com/a/13653180 - const uuidRE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; - const subscriber = await DB.getSubscriberByEmail("firefoxaccount@test.com"); + const uuidRE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i + const subscriber = await DB.getSubscriberByEmail('firefoxaccount@test.com') - const unverifiedEmailAddress = await DB.addSubscriberUnverifiedEmailHash(subscriber, testEmail); - expect(unverifiedEmailAddress.sha1).toBe(getSha1(testEmail)); - expect(uuidRE.test(unverifiedEmailAddress.verification_token)).toBeTruthy(); - expect(unverifiedEmailAddress.verified).toBeFalsy(); -}); + const unverifiedEmailAddress = await DB.addSubscriberUnverifiedEmailHash(subscriber, testEmail) + expect(unverifiedEmailAddress.sha1).toBe(getSha1(testEmail)) + expect(uuidRE.test(unverifiedEmailAddress.verification_token)).toBeTruthy() + expect(unverifiedEmailAddress.verified).toBeFalsy() +}) +test('verifyEmailHash accepts token and returns verified subscriber', async () => { + const testEmail = 'verifyEmailHash@test.com' + const subscriber = await DB.getSubscriberByEmail('firefoxaccount@test.com') -test("verifyEmailHash accepts token and returns verified subscriber", async () => { - const testEmail = "verifyEmailHash@test.com"; - const subscriber = await DB.getSubscriberByEmail("firefoxaccount@test.com"); + const unverifiedEmailAddress = await DB.addSubscriberUnverifiedEmailHash(subscriber, testEmail) + expect(unverifiedEmailAddress.verified).toBeFalsy() - const unverifiedEmailAddress = await DB.addSubscriberUnverifiedEmailHash(subscriber, testEmail); - expect(unverifiedEmailAddress.verified).toBeFalsy(); + HIBP.subscribeHash.mockResolvedValue(true) + const verifiedEmailAddress = await DB.verifyEmailHash(unverifiedEmailAddress.verification_token) + expect(verifiedEmailAddress.sha1).toBe(getSha1(testEmail)) + expect(verifiedEmailAddress.verified).toBeTruthy() +}) - HIBP.subscribeHash.mockResolvedValue(true); - const verifiedEmailAddress = await DB.verifyEmailHash(unverifiedEmailAddress.verification_token); - expect(verifiedEmailAddress.sha1).toBe(getSha1(testEmail)); - expect(verifiedEmailAddress.verified).toBeTruthy(); -}); +test('verifyEmailHash accepts token and returns single verified subscriber', async () => { + const testEmail = 'verifyEmailHash@test.com' + const subscriber = await DB.getSubscriberByEmail('firefoxaccount@test.com') + const subscriber2 = await DB.getSubscriberByEmail('all_emails_to_primary@test.com') + const unverifiedEmailAddress = await DB.addSubscriberUnverifiedEmailHash(subscriber, testEmail) + expect(unverifiedEmailAddress.verified).toBeFalsy() -test("verifyEmailHash accepts token and returns single verified subscriber", async () => { - const testEmail = "verifyEmailHash@test.com"; - const subscriber = await DB.getSubscriberByEmail("firefoxaccount@test.com"); - const subscriber2 = await DB.getSubscriberByEmail("all_emails_to_primary@test.com"); + const unverifiedEmailAddress2 = await DB.addSubscriberUnverifiedEmailHash(subscriber2, testEmail) + expect(unverifiedEmailAddress2.verified).toBeFalsy() - const unverifiedEmailAddress = await DB.addSubscriberUnverifiedEmailHash(subscriber, testEmail); - expect(unverifiedEmailAddress.verified).toBeFalsy(); + HIBP.subscribeHash.mockResolvedValue(true) + const verifiedEmailAddress = await DB.verifyEmailHash(unverifiedEmailAddress.verification_token) + expect(verifiedEmailAddress.sha1).toBe(getSha1(testEmail)) + expect(verifiedEmailAddress.verified).toBeTruthy() + expect(unverifiedEmailAddress2.verified).toBeFalsy() +}) - const unverifiedEmailAddress2 = await DB.addSubscriberUnverifiedEmailHash(subscriber2, testEmail); - expect(unverifiedEmailAddress2.verified).toBeFalsy(); +test('addSubscriber invalid argument', async () => { + const testEmail = 'test'.repeat(255) - HIBP.subscribeHash.mockResolvedValue(true); - const verifiedEmailAddress = await DB.verifyEmailHash(unverifiedEmailAddress.verification_token); - expect(verifiedEmailAddress.sha1).toBe(getSha1(testEmail)); - expect(verifiedEmailAddress.verified).toBeTruthy(); - expect(unverifiedEmailAddress2.verified).toBeFalsy(); -}); + await expect(DB.addSubscriber(testEmail)).rejects.toThrow('error-could-not-add-email') +}) +test('addSubscriber accepts email, language and returns verified subscriber', async () => { + const testEmail = 'newfirefoxaccount@test.com' -test("addSubscriber invalid argument", async () => { - const testEmail = "test".repeat(255); + const verifiedSubscriber = await DB.addSubscriber(testEmail) - await expect(DB.addSubscriber(testEmail)).rejects.toThrow("error-could-not-add-email"); -}); + expect(verifiedSubscriber.primary_email).toBe(testEmail) + expect(verifiedSubscriber.primary_verified).toBeTruthy() + expect(verifiedSubscriber.primary_sha1).toBe(getSha1(testEmail)) +}) +test('addSubscriber with existing email updates updated_at', async () => { + const testEmail = 'newfirefoxaccount@test.com' -test("addSubscriber accepts email, language and returns verified subscriber", async () => { - const testEmail = "newfirefoxaccount@test.com"; + let verifiedSubscriber = await DB.addSubscriber(testEmail) - const verifiedSubscriber = await DB.addSubscriber(testEmail); + expect(verifiedSubscriber.primary_email).toBe(testEmail) + expect(verifiedSubscriber.primary_verified).toBeTruthy() + expect(verifiedSubscriber.primary_sha1).toBe(getSha1(testEmail)) + const updatedAt = verifiedSubscriber.updated_at - expect(verifiedSubscriber.primary_email).toBe(testEmail); - expect(verifiedSubscriber.primary_verified).toBeTruthy(); - expect(verifiedSubscriber.primary_sha1).toBe(getSha1(testEmail)); -}); + await sleep(1000) + verifiedSubscriber = await DB.addSubscriber(testEmail) -test("addSubscriber with existing email updates updated_at", async () => { - const testEmail = "newfirefoxaccount@test.com"; + expect(verifiedSubscriber.primary_email).toBe(testEmail) + expect(verifiedSubscriber.primary_verified).toBeTruthy() + expect(verifiedSubscriber.primary_sha1).toBe(getSha1(testEmail)) + expect(verifiedSubscriber.updated_at).not.toBe(updatedAt) +}) - let verifiedSubscriber = await DB.addSubscriber(testEmail); +test('addSubscriber accepts upperCasedAddress, and returns verified subscriber with lowercase address hash', async () => { + const testEmail = 'upperCasedAddress@test.com' - expect(verifiedSubscriber.primary_email).toBe(testEmail); - expect(verifiedSubscriber.primary_verified).toBeTruthy(); - expect(verifiedSubscriber.primary_sha1).toBe(getSha1(testEmail)); - const updatedAt = verifiedSubscriber.updated_at; + const verifiedSubscriber = await DB.addSubscriber(testEmail) - await sleep(1000); + expect(verifiedSubscriber.primary_email).toBe(testEmail) + expect(verifiedSubscriber.primary_sha1).toBe(getSha1(testEmail.toLowerCase())) +}) - verifiedSubscriber = await DB.addSubscriber(testEmail); +test('setBreachesLastShown updates column and returns subscriber', async () => { + const startingSubscriber = await DB.getSubscriberByEmail('firefoxaccount@test.com') - expect(verifiedSubscriber.primary_email).toBe(testEmail); - expect(verifiedSubscriber.primary_verified).toBeTruthy(); - expect(verifiedSubscriber.primary_sha1).toBe(getSha1(testEmail)); - expect(verifiedSubscriber.updated_at).not.toBe(updatedAt); -}); + await sleep(1000) + await DB.setBreachesLastShownNow(startingSubscriber) + const updatedSubscriber = await DB.getSubscriberByEmail(startingSubscriber.primary_email) + expect(new Date(updatedSubscriber.breaches_last_shown).getTime()).toBeGreaterThan(new Date(startingSubscriber.breaches_last_shown).getTime()) +}) -test("addSubscriber accepts upperCasedAddress, and returns verified subscriber with lowercase address hash", async () => { - const testEmail = "upperCasedAddress@test.com"; +test('setAllEmailsToPrimary updates column and returns subscriber', async () => { + const startingSubscriber = await DB.getSubscriberByEmail('firefoxaccount@test.com') - const verifiedSubscriber = await DB.addSubscriber(testEmail); + await DB.setAllEmailsToPrimary(startingSubscriber, false) - expect(verifiedSubscriber.primary_email).toBe(testEmail); - expect(verifiedSubscriber.primary_sha1).toBe(getSha1(testEmail.toLowerCase())); -}); + const updatedSubscriber = await DB.getSubscriberByEmail(startingSubscriber.primary_email) + expect(updatedSubscriber.all_emails_to_primary).toBeFalsy() +}) +test('removeSubscriber accepts subscriber and removes everything from subscribers and email_addresses tables', async () => { + const startingSubscriber = await DB.getSubscriberByEmail(TEST_SUBSCRIBERS.firefox_account.primary_email) + expect(startingSubscriber.id).toEqual(TEST_SUBSCRIBERS.firefox_account.id) + const startingEmailAddressRecord = await DB.getEmailById(TEST_EMAIL_ADDRESSES.firefox_account.id) + expect(startingEmailAddressRecord.id).toEqual(TEST_EMAIL_ADDRESSES.firefox_account.id) -test("setBreachesLastShown updates column and returns subscriber", async() => { - const startingSubscriber = await DB.getSubscriberByEmail("firefoxaccount@test.com"); + await DB.removeSubscriber(startingSubscriber) - await sleep(1000); - await DB.setBreachesLastShownNow(startingSubscriber); + const noMoreSubscribers = await DB.getSubscriberByEmail(startingSubscriber.primary_email) + expect(noMoreSubscribers).toBeUndefined() + const noMoreEmailAddress = await DB.getEmailById(startingEmailAddressRecord.id) + expect(noMoreEmailAddress).toBeUndefined() +}) - const updatedSubscriber = await DB.getSubscriberByEmail(startingSubscriber.primary_email); - expect (new Date(updatedSubscriber.breaches_last_shown).getTime()).toBeGreaterThan(new Date(startingSubscriber.breaches_last_shown).getTime()); -}); +test('removeEmail accepts email and removes from subscribers table', async () => { + const testEmail = 'removingfirefoxaccount@test.com' + const verifiedSubscriber = await DB.addSubscriber(testEmail) + const subscribers = await DB.getSubscribersByHashes([getSha1(testEmail)]) + expect(subscribers.length).toEqual(1) -test("setAllEmailsToPrimary updates column and returns subscriber", async() => { - const startingSubscriber = await DB.getSubscriberByEmail("firefoxaccount@test.com"); + await DB.removeEmail(verifiedSubscriber.primary_email) + const noMoreSubscribers = await DB.getSubscribersByHashes([getSha1(testEmail)]) + expect(noMoreSubscribers.length).toEqual(0) +}) - await DB.setAllEmailsToPrimary(startingSubscriber, false); +test('removeEmail accepts email and removes from email_addresses table', async () => { + const testEmailAddress = TEST_EMAIL_ADDRESSES.all_emails_to_primary + const emailAddress = await DB.getEmailById(testEmailAddress.id) + expect(emailAddress.email).toEqual(testEmailAddress.email) - const updatedSubscriber = await DB.getSubscriberByEmail(startingSubscriber.primary_email); - expect (updatedSubscriber.all_emails_to_primary).toBeFalsy(); -}); - - -test("removeSubscriber accepts subscriber and removes everything from subscribers and email_addresses tables", async () => { - const startingSubscriber = await DB.getSubscriberByEmail(TEST_SUBSCRIBERS.firefox_account.primary_email); - expect(startingSubscriber.id).toEqual(TEST_SUBSCRIBERS.firefox_account.id); - const startingEmailAddressRecord = await DB.getEmailById(TEST_EMAIL_ADDRESSES.firefox_account.id); - expect(startingEmailAddressRecord.id).toEqual(TEST_EMAIL_ADDRESSES.firefox_account.id); - - await DB.removeSubscriber(startingSubscriber); - - const noMoreSubscribers = await DB.getSubscriberByEmail(startingSubscriber.primary_email); - expect(noMoreSubscribers).toBeUndefined(); - const noMoreEmailAddress = await DB.getEmailById(startingEmailAddressRecord.id); - expect(noMoreEmailAddress).toBeUndefined(); -}); - - -test("removeEmail accepts email and removes from subscribers table", async () => { - const testEmail = "removingfirefoxaccount@test.com"; - - const verifiedSubscriber = await DB.addSubscriber(testEmail); - const subscribers = await DB.getSubscribersByHashes([getSha1(testEmail)]); - expect(subscribers.length).toEqual(1); - - await DB.removeEmail(verifiedSubscriber.primary_email); - const noMoreSubscribers = await DB.getSubscribersByHashes([getSha1(testEmail)]); - expect(noMoreSubscribers.length).toEqual(0); -}); - - -test("removeEmail accepts email and removes from email_addresses table", async () => { - const testEmailAddress = TEST_EMAIL_ADDRESSES.all_emails_to_primary; - const emailAddress = await DB.getEmailById(testEmailAddress.id); - expect(emailAddress.email).toEqual(testEmailAddress.email); - - await DB.removeEmail(emailAddress.email); - const noMoreEmailAddress = await DB.getEmailById(testEmailAddress.id); - expect(noMoreEmailAddress).toBeUndefined(); -}); + await DB.removeEmail(emailAddress.email) + const noMoreEmailAddress = await DB.getEmailById(testEmailAddress.id) + expect(noMoreEmailAddress).toBeUndefined() +}) diff --git a/tests/email.test.js b/tests/email.test.js index e5476befc..4a5b6c9c6 100644 --- a/tests/email.test.js +++ b/tests/email.test.js @@ -1,65 +1,58 @@ -"use strict"; +'use strict' -const nodemailer = require("nodemailer"); +const nodemailer = require('nodemailer') -const EmailUtils = require("../email-utils"); -const { TEST_SUBSCRIBERS, TEST_EMAIL_ADDRESSES } = require("../db/seeds/test_subscribers"); +const EmailUtils = require('../email-utils') +const { TEST_SUBSCRIBERS, TEST_EMAIL_ADDRESSES } = require('../db/seeds/test_subscribers') +jest.mock('nodemailer') -jest.mock("nodemailer"); +test('EmailUtils.init with empty host doesnt invoke nodemailer', () => { + nodemailer.createTransport = jest.fn() + EmailUtils.init('') -test("EmailUtils.init with empty host doesnt invoke nodemailer", () => { - nodemailer.createTransport = jest.fn(); + const mockCreateTransport = nodemailer.createTransport.mock + expect(mockCreateTransport.calls.length).toBe(1) + expect(mockCreateTransport.calls[0][0]).toEqual({ jsonTransport: true }) +}) - EmailUtils.init(""); +test('EmailUtils.init with user, pass, host, port invokes nodemailer.createTransport', () => { + const testSmtpUrl = 'smtps://test:test@test:1' + nodemailer.createTransport = jest.fn() - const mockCreateTransport = nodemailer.createTransport.mock; - expect(mockCreateTransport.calls.length).toBe(1); - expect(mockCreateTransport.calls[0][0]).toEqual({jsonTransport: true}); -}); + EmailUtils.init(testSmtpUrl) + const mockCreateTransport = nodemailer.createTransport.mock + expect(mockCreateTransport.calls.length).toBe(1) + expect(mockCreateTransport.calls[0][0]).toBe(testSmtpUrl) +}) -test("EmailUtils.init with user, pass, host, port invokes nodemailer.createTransport", () => { - const testSmtpUrl = "smtps://test:test@test:1"; - nodemailer.createTransport = jest.fn(); +test('EmailUtils.sendEmail with recipient, subject, template, context calls gTransporter.sendMail', () => { + const testSmtpUrl = 'smtps://test:test@test:1' + const sendMailArgs = ['test@example.com', 'subject', 'template.hbs', { breach: 'Test' }] + nodemailer.createTransport = jest.fn() - EmailUtils.init(testSmtpUrl); - - const mockCreateTransport = nodemailer.createTransport.mock; - expect(mockCreateTransport.calls.length).toBe(1); - expect(mockCreateTransport.calls[0][0]).toBe(testSmtpUrl); -}); - - -test("EmailUtils.sendEmail with recipient, subject, template, context calls gTransporter.sendMail", () => { - const testSmtpUrl = "smtps://test:test@test:1"; - const sendMailArgs = ["test@example.com", "subject", "template.hbs", {breach: "Test"}]; - nodemailer.createTransport = jest.fn(); - - EmailUtils.init(testSmtpUrl); - EmailUtils.sendEmail(...sendMailArgs); + EmailUtils.init(testSmtpUrl) + EmailUtils.sendEmail(...sendMailArgs) // TODO: find a way to expect gTransporter.sendMail -}); +}) +test('EmailUtils.getUnsubscribeUrl works with subscriber record', () => { + const subscriberRecord = TEST_SUBSCRIBERS.firefox_account -test("EmailUtils.getUnsubscribeUrl works with subscriber record", () => { - const subscriberRecord = TEST_SUBSCRIBERS.firefox_account; + const unsubUrl = EmailUtils.getUnsubscribeUrl(subscriberRecord).toString() - const unsubUrl = EmailUtils.getUnsubscribeUrl(subscriberRecord).toString(); + expect(unsubUrl).toMatch(subscriberRecord.primary_sha1) + expect(unsubUrl).toMatch(subscriberRecord.primary_verification_token) +}) - expect(unsubUrl).toMatch(subscriberRecord.primary_sha1); - expect(unsubUrl).toMatch(subscriberRecord.primary_verification_token); +test('EmailUtils.getUnsubscribeUrl works with email_address record', () => { + const emailAddressRecord = TEST_EMAIL_ADDRESSES.firefox_account -}); + const unsubUrl = EmailUtils.getUnsubscribeUrl(emailAddressRecord).toString() - -test("EmailUtils.getUnsubscribeUrl works with email_address record", () => { - const emailAddressRecord = TEST_EMAIL_ADDRESSES.firefox_account; - - const unsubUrl = EmailUtils.getUnsubscribeUrl(emailAddressRecord).toString(); - - expect(unsubUrl).toMatch(emailAddressRecord.sha1); - expect(unsubUrl).toMatch(emailAddressRecord.verification_token); -}); + expect(unsubUrl).toMatch(emailAddressRecord.sha1) + expect(unsubUrl).toMatch(emailAddressRecord.verification_token) +}) diff --git a/tests/fxa.test.js b/tests/fxa.test.js index 8cdacdbd4..9caaa94e1 100644 --- a/tests/fxa.test.js +++ b/tests/fxa.test.js @@ -1,29 +1,27 @@ -"use strict"; +'use strict' -const got = require("got"); +const got = require('got') -const { TEST_SUBSCRIBERS } = require("../db/seeds/test_subscribers"); -const { FXA } = require("../lib/fxa"); +const { TEST_SUBSCRIBERS } = require('../db/seeds/test_subscribers') +const { FXA } = require('../lib/fxa') +jest.mock('got') -jest.mock("got"); +test('revokeOAuthToken calls oauth destroy with fxa_refresh_token', async () => { + const subscriber = TEST_SUBSCRIBERS.firefox_account + await FXA.revokeOAuthTokens(subscriber) -test("revokeOAuthToken calls oauth destroy with fxa_refresh_token", async () => { - const subscriber = TEST_SUBSCRIBERS.firefox_account; + const gotCalls = got.mock.calls + expect(gotCalls.length).toEqual(2) - await FXA.revokeOAuthTokens(subscriber); + const accessGotCallArgs = gotCalls[0] + expect(accessGotCallArgs[0]).toContain('/v1/destroy') + const accessGotCallOptions = accessGotCallArgs[1] + expect(accessGotCallOptions.json.token).toEqual(subscriber.fxa_access_token) - const gotCalls = got.mock.calls; - expect(gotCalls.length).toEqual(2); - - const accessGotCallArgs = gotCalls[0]; - expect(accessGotCallArgs[0]).toContain("/v1/destroy"); - const accessGotCallOptions = accessGotCallArgs[1]; - expect(accessGotCallOptions.json.token).toEqual(subscriber.fxa_access_token); - - const refreshGotCallArgs = gotCalls[1]; - expect(refreshGotCallArgs[0]).toContain("/v1/destroy"); - const refreshGotCallOptions = refreshGotCallArgs[1]; - expect(refreshGotCallOptions.json.refresh_token).toEqual(subscriber.fxa_refresh_token); -}); + const refreshGotCallArgs = gotCalls[1] + expect(refreshGotCallArgs[0]).toContain('/v1/destroy') + const refreshGotCallOptions = refreshGotCallArgs[1] + expect(refreshGotCallOptions.json.refresh_token).toEqual(subscriber.fxa_refresh_token) +}) diff --git a/tests/hbs-helpers.test.js b/tests/hbs-helpers.test.js index b0dea05c5..2d5dab423 100644 --- a/tests/hbs-helpers.test.js +++ b/tests/hbs-helpers.test.js @@ -1,40 +1,37 @@ -"use strict"; - -const HBSHelpers = require("../template-helpers/hbs-helpers"); -const { LocaleUtils } = require("../locale-utils"); +'use strict' +const HBSHelpers = require('../template-helpers/hbs-helpers') +const { LocaleUtils } = require('../locale-utils') test("localizedBreachDataClasses joins array by ',' and renders fluent translation or fluent ID", () => { - LocaleUtils.init(); - LocaleUtils.loadLanguagesIntoApp({locals: {}}); - const supportedLocales = ["en"]; + LocaleUtils.init() + LocaleUtils.loadLanguagesIntoApp({ locals: {} }) + const supportedLocales = ['en'] - const singleDataClassArray = new Array(); - singleDataClassArray.push("usernames"); - const singleDisplay = HBSHelpers.localizedBreachDataClasses(singleDataClassArray, supportedLocales); - expect(singleDisplay).toEqual("Usernames"); + const singleDataClassArray = new Array() + singleDataClassArray.push('usernames') + const singleDisplay = HBSHelpers.localizedBreachDataClasses(singleDataClassArray, supportedLocales) + expect(singleDisplay).toEqual('Usernames') - const realDataClassesArray = new Array(); - realDataClassesArray.push("usernames"); - realDataClassesArray.push("passwords"); - const realDisplay = HBSHelpers.localizedBreachDataClasses(realDataClassesArray, supportedLocales); - expect(realDisplay).toEqual("Usernames, Passwords"); + const realDataClassesArray = new Array() + realDataClassesArray.push('usernames') + realDataClassesArray.push('passwords') + const realDisplay = HBSHelpers.localizedBreachDataClasses(realDataClassesArray, supportedLocales) + expect(realDisplay).toEqual('Usernames, Passwords') - const notFoundDataClassesArray = new Array(); - notFoundDataClassesArray.push("fdhsaigp12"); - notFoundDataClassesArray.push("jfdiosapgys8"); - const notFoundDisplay = HBSHelpers.localizedBreachDataClasses(notFoundDataClassesArray, supportedLocales); - expect(notFoundDisplay).toEqual("fdhsaigp12, jfdiosapgys8"); -}); + const notFoundDataClassesArray = new Array() + notFoundDataClassesArray.push('fdhsaigp12') + notFoundDataClassesArray.push('jfdiosapgys8') + const notFoundDisplay = HBSHelpers.localizedBreachDataClasses(notFoundDataClassesArray, supportedLocales) + expect(notFoundDisplay).toEqual('fdhsaigp12, jfdiosapgys8') +}) +test('localeString adds commas to numbers', () => { + const supportedLocales = ['en'] + const display = HBSHelpers.localeString(1000000, supportedLocales) + expect(display).toEqual('1,000,000') +}) -test("localeString adds commas to numbers", () => { - const supportedLocales = ["en"]; - const display = HBSHelpers.localeString(1000000, supportedLocales); - expect(display).toEqual("1,000,000"); -}); - - -test("breachMath accepts operators", () => { - expect(HBSHelpers.breachMath(1, "+", 2)).toBe(3); -}); +test('breachMath accepts operators', () => { + expect(HBSHelpers.breachMath(1, '+', 2)).toBe(3) +}) diff --git a/tests/hibp.test.js b/tests/hibp.test.js index e2383ebcf..e54cd92d3 100644 --- a/tests/hibp.test.js +++ b/tests/hibp.test.js @@ -1,78 +1,73 @@ -"use strict"; +'use strict' -const got = require("got"); +const got = require('got') -const AppConstants = require("../app-constants"); -const getSha1 = require("../sha1-utils"); -const hibp = require("../hibp"); +const AppConstants = require('../app-constants') +const getSha1 = require('../sha1-utils') +const hibp = require('../hibp') -const { testBreaches } = require("./test-breaches"); +const { testBreaches } = require('./test-breaches') +jest.mock('got') -jest.mock("got"); +test('req adds hibp api root and standard options; NOT token', async () => { + hibp.req('/some-path') -test("req adds hibp api root and standard options; NOT token", async() => { - hibp.req("/some-path"); + const gotCalls = got.mock.calls + expect(gotCalls.length).toEqual(1) + const gotCallArgs = gotCalls[0] + expect(gotCallArgs[0]).toContain(`${AppConstants.HIBP_API_ROOT}/some-path`) + expect(gotCallArgs[0]).not.toContain('?code=') + expect(gotCallArgs[1].headers['User-Agent']).toContain('blurts-server') + expect(gotCallArgs[1].responseType).toBe('json') +}) - const gotCalls = got.mock.calls; - expect(gotCalls.length).toEqual(1); - const gotCallArgs = gotCalls[0]; - expect(gotCallArgs[0]).toContain(`${AppConstants.HIBP_API_ROOT}/some-path`); - expect(gotCallArgs[0]).not.toContain("?code="); - expect(gotCallArgs[1].headers["User-Agent"]).toContain("blurts-server"); - expect(gotCallArgs[1].responseType).toBe("json"); -}); +test('loadBreachesIntoApp adds app.locals.breaches|breachesLoadedDateTime|mostRecentBreachDateTime', async () => { + got.mockClear() + got.mockResolvedValue({ body: testBreaches }) + const app = { locals: {} } + await hibp.loadBreachesIntoApp(app) -test("loadBreachesIntoApp adds app.locals.breaches|breachesLoadedDateTime|mostRecentBreachDateTime", async() => { - got.mockClear(); - got.mockResolvedValue( { body: testBreaches }); - const app = { locals: {} }; + const gotCalls = got.mock.calls + expect(gotCalls.length).toEqual(1) + const gotCallArgs = gotCalls[0] + expect(gotCallArgs[0]).toContain(`${AppConstants.HIBP_API_ROOT}/breaches`) + expect(app.locals.breaches).toEqual(testBreaches) + const latestBreach = hibp.getLatestBreach(testBreaches) + expect(app.locals.mostRecentBreachDateTime).toEqual(latestBreach.AddedDate) +}) - await hibp.loadBreachesIntoApp(app); - - const gotCalls = got.mock.calls; - expect(gotCalls.length).toEqual(1); - const gotCallArgs = gotCalls[0]; - expect(gotCallArgs[0]).toContain(`${AppConstants.HIBP_API_ROOT}/breaches`); - expect(app.locals.breaches).toEqual(testBreaches); - const latestBreach = hibp.getLatestBreach(testBreaches); - expect(app.locals.mostRecentBreachDateTime).toEqual(latestBreach.AddedDate); -}); - - -test("filterBreaches removes retired, spam list, fabricated, unverified, and non-website breaches", async() => { - let foundSensitive = false; +test('filterBreaches removes retired, spam list, fabricated, unverified, and non-website breaches', async () => { + let foundSensitive = false for (const breach of testBreaches) { if (breach.IsSensitive) { - foundSensitive = true; - break; + foundSensitive = true + break } } - expect(foundSensitive).toBe(true); + expect(foundSensitive).toBe(true) - - const safeBreaches = hibp.filterBreaches(testBreaches); + const safeBreaches = hibp.filterBreaches(testBreaches) for (const breach of safeBreaches) { - expect(breach.IsFabricated).toBe(false); - expect(breach.IsSpamList).toBe(false); - expect(breach.IsRetired).toBe(false); - expect(breach.IsVerified).toBe(true); + expect(breach.IsFabricated).toBe(false) + expect(breach.IsSpamList).toBe(false) + expect(breach.IsRetired).toBe(false) + expect(breach.IsVerified).toBe(true) } -}); +}) - -test("getBreachesForEmail HIBP responses with status of 429 cause throttled retries up to HIBP_THROTTLE_MAX_TRIES", async() => { +test('getBreachesForEmail HIBP responses with status of 429 cause throttled retries up to HIBP_THROTTLE_MAX_TRIES', async () => { // Assumes running with max tries of 3 and delay of 1000 - jest.setTimeout(20000); - got.mockClear(); - got.mockRejectedValue( { statusCode: 429 }); + jest.setTimeout(20000) + got.mockClear() + got.mockRejectedValue({ statusCode: 429 }) - await expect(hibp.getBreachesForEmail(getSha1("unverifiedemail@test.com"), testBreaches)).rejects.toThrow("error-hibp-throttled"); + await expect(hibp.getBreachesForEmail(getSha1('unverifiedemail@test.com'), testBreaches)).rejects.toThrow('error-hibp-throttled') - const gotCalls = got.mock.calls; - expect(gotCalls.length).toEqual(Number(AppConstants.HIBP_THROTTLE_MAX_TRIES)); + const gotCalls = got.mock.calls + expect(gotCalls.length).toEqual(Number(AppConstants.HIBP_THROTTLE_MAX_TRIES)) - jest.setTimeout(5000); -}); + jest.setTimeout(5000) +}) diff --git a/tests/integration/pages/desktop/dashboard.page.js b/tests/integration/pages/desktop/dashboard.page.js index 9b797b2b2..146f3760e 100644 --- a/tests/integration/pages/desktop/dashboard.page.js +++ b/tests/integration/pages/desktop/dashboard.page.js @@ -1,21 +1,20 @@ -"use strict"; +'use strict' class UserDashboardPage { - - waitForPageToLoad() { - $("#dashboard").waitForExist(5000); - return this; + waitForPageToLoad () { + $('#dashboard').waitForExist(5000) + return this } - get addEmailBox() { return $("#email-add"); } - get verificationLink() { return $("#email-add-submit"); } - manageEmailAddresses() { - const UserPreferencesPage = require("./userPreferences.page"); + get addEmailBox () { return $('#email-add') } + get verificationLink () { return $('#email-add-submit') } + manageEmailAddresses () { + const UserPreferencesPage = require('./userPreferences.page') - $(".manage-emails").waitForExist(5000); - $(".manage-emails").click(); - return UserPreferencesPage.waitForPageToLoad(); + $('.manage-emails').waitForExist(5000) + $('.manage-emails').click() + return UserPreferencesPage.waitForPageToLoad() } } -module.exports = new UserDashboardPage(); +module.exports = new UserDashboardPage() diff --git a/tests/integration/pages/desktop/home.page.js b/tests/integration/pages/desktop/home.page.js index 4c66d5789..76bcc0474 100644 --- a/tests/integration/pages/desktop/home.page.js +++ b/tests/integration/pages/desktop/home.page.js @@ -1,29 +1,28 @@ -"use strict"; +'use strict' class HomePage { /* Represents the Home page */ - waitForPageToLoad() { - this.monitorLogo.waitForExist(5000); - return this; + waitForPageToLoad () { + this.monitorLogo.waitForExist(5000) + return this } - get monitorLogo() { return $(".fx-monitor-logotype"); } - get breachCard() { return new BreachCard(); } - get breachEmailAddress() { return $("#scan-email"); } - get checkBreachesButton() { - $(".input-group-button > input:nth-child(1)").click(); - const ScanResultsPage = require("./scanResults.page"); + get monitorLogo () { return $('.fx-monitor-logotype') } + get breachCard () { return new BreachCard() } + get breachEmailAddress () { return $('#scan-email') } + get checkBreachesButton () { + $('.input-group-button > input:nth-child(1)').click() + const ScanResultsPage = require('./scanResults.page') - return ScanResultsPage.waitForPageToLoad(); + return ScanResultsPage.waitForPageToLoad() } } class BreachCard { /* Represents the Breach card region */ - get latestBreachCard() { return $(".latest-breach"); } - + get latestBreachCard () { return $('.latest-breach') } } -module.exports = new HomePage(); +module.exports = new HomePage() diff --git a/tests/integration/pages/desktop/scanResults.page.js b/tests/integration/pages/desktop/scanResults.page.js index 583e79117..932a5697b 100644 --- a/tests/integration/pages/desktop/scanResults.page.js +++ b/tests/integration/pages/desktop/scanResults.page.js @@ -1,16 +1,16 @@ -"use strict"; +'use strict' class ScanResultsPage { - - waitForPageToLoad() { - $(".scan-results").waitForExist(5000); - return this; + waitForPageToLoad () { + $('.scan-results').waitForExist(5000) + return this } - get numberOfBreaches() { - return $(".headline > span:nth-child(1)").getText(); - } - get breachCards() { return $$(".breach-card"); } + get numberOfBreaches () { + return $('.headline > span:nth-child(1)').getText() + } + + get breachCards () { return $$('.breach-card') } } -module.exports = new ScanResultsPage(); +module.exports = new ScanResultsPage() diff --git a/tests/integration/pages/desktop/userPreferences.page.js b/tests/integration/pages/desktop/userPreferences.page.js index b6fc90ce2..9b3e628af 100644 --- a/tests/integration/pages/desktop/userPreferences.page.js +++ b/tests/integration/pages/desktop/userPreferences.page.js @@ -1,11 +1,10 @@ -"use strict"; +'use strict' class UserPreferencesPage { - - waitForPageToLoad() { - $(".preferences").waitForExist(5000); - return this; + waitForPageToLoad () { + $('.preferences').waitForExist(5000) + return this } } -module.exports = new UserPreferencesPage(); +module.exports = new UserPreferencesPage() diff --git a/tests/integration/regions/navbar.region.js b/tests/integration/regions/navbar.region.js index b3cd9b02a..a3ccc788e 100644 --- a/tests/integration/regions/navbar.region.js +++ b/tests/integration/regions/navbar.region.js @@ -1,9 +1,9 @@ -"use strict"; +'use strict' class NavBar { - /* Represents the navbar */ + /* Represents the navbar */ - get signIn() { return $("#sign-in-btn"); } + get signIn () { return $('#sign-in-btn') } } -module.exports = new NavBar(); +module.exports = new NavBar() diff --git a/tests/integration/tests/test-breaches-page.js b/tests/integration/tests/test-breaches-page.js index 651679fa5..91f036487 100644 --- a/tests/integration/tests/test-breaches-page.js +++ b/tests/integration/tests/test-breaches-page.js @@ -1,17 +1,17 @@ -"use strict"; +'use strict' -describe("Firefox Monitor Breaches Page", function() { - this.retries(2); +describe('Firefox Monitor Breaches Page', function () { + this.retries(2) - beforeEach(function() { - browser.url("/breaches"); - }); + beforeEach(function () { + browser.url('/breaches') + }) - it("should look like normal", function() { - expect(browser.checkFullPageScreen("Breaches_Page", { + it('should look like normal', function () { + expect(browser.checkFullPageScreen('Breaches_Page', { hideElements: [ - $$(".breach-info-wrapper"), - ], - })).to.be.within(0, 34.99); - }); -}); + $$('.breach-info-wrapper') + ] + })).to.be.within(0, 34.99) + }) +}) diff --git a/tests/integration/tests/test-home-page.js b/tests/integration/tests/test-home-page.js index 273b0005a..cdc8c122c 100644 --- a/tests/integration/tests/test-home-page.js +++ b/tests/integration/tests/test-home-page.js @@ -1,42 +1,41 @@ /* eslint-disable no-console */ -"use strict"; +'use strict' -const HomePage = require("../pages/desktop/home.page"); +const HomePage = require('../pages/desktop/home.page') // Don't need these until secondary email test is restored // const NavBar = require("../regions/navbar.region"); // const UserDashboardPage = require("../pages/desktop/dashboard.page"); -describe("Firefox Monitor homepage", function() { +describe('Firefox Monitor homepage', function () { + beforeEach(function () { + browser.url('/?experimentBranch=false') + }) - beforeEach(function() { - browser.url("/?experimentBranch=false"); - }); + it('should load the latest breach card', function () { + const homePage = HomePage - it("should load the latest breach card", function() { - const homePage = HomePage; + homePage.waitForPageToLoad() + expect(homePage.breachCard.latestBreachCard.isDisplayed()).to.be.true + }) - homePage.waitForPageToLoad(); - expect(homePage.breachCard.latestBreachCard.isDisplayed()).to.be.true; - }); - - it("should look like normal", function() { - expect(browser.checkFullPageScreen("Home_Page", { + it('should look like normal', function () { + expect(browser.checkFullPageScreen('Home_Page', { hideElements: [ - $$(".breach-info-wrapper"), - ], - })).to.be.within(0, 34.99); - }); + $$('.breach-info-wrapper') + ] + })).to.be.within(0, 34.99) + }) - it("should load correct number of breaches from an email input", function() { - const homePage = HomePage; + it('should load correct number of breaches from an email input', function () { + const homePage = HomePage - homePage.waitForPageToLoad(); - homePage.breachEmailAddress.setValue(global.primaryEmail); - const scanResults = homePage.checkBreachesButton; + homePage.waitForPageToLoad() + homePage.breachEmailAddress.setValue(global.primaryEmail) + const scanResults = homePage.checkBreachesButton expect(scanResults.breachCards.length) .to - .equal(Number(scanResults.numberOfBreaches)); - }); + .equal(Number(scanResults.numberOfBreaches)) + }) /* it("should allow secondary email to be added", function() { @@ -80,4 +79,4 @@ describe("Firefox Monitor homepage", function() { expect(email.getText()).to.equal(global.primaryEmail); }); */ -}); +}) diff --git a/tests/integration/utils/test-create-baseline.js b/tests/integration/utils/test-create-baseline.js index c996c9bdc..5e77f5393 100644 --- a/tests/integration/utils/test-create-baseline.js +++ b/tests/integration/utils/test-create-baseline.js @@ -2,26 +2,25 @@ This file is used to create a baseline image for a webpage. */ -"use strict"; +'use strict' +describe('Firefox Monitor homepage', function () { + this.retries(2) -describe("Firefox Monitor homepage", function() { - this.retries(2); + beforeEach(function () { + browser.url('/') + }) - beforeEach(function() { - browser.url("/"); - }); - - it("should look like normal", function() { - browser.saveFullPageScreen("Home_Page", { - hideElements: [ - $$(".breach-info-wrapper"), - ], - }); - expect(browser.checkFullPageScreen("Home_Page", { - hideElements: [ - $$(".breach-info-wrapper"), - ], - })).to.equal(0); - }); -}); + it('should look like normal', function () { + browser.saveFullPageScreen('Home_Page', { + hideElements: [ + $$('.breach-info-wrapper') + ] + }) + expect(browser.checkFullPageScreen('Home_Page', { + hideElements: [ + $$('.breach-info-wrapper') + ] + })).to.equal(0) + }) +}) diff --git a/tests/integration/wdio.conf.js b/tests/integration/wdio.conf.js index 346c83c1e..eb08af02f 100644 --- a/tests/integration/wdio.conf.js +++ b/tests/integration/wdio.conf.js @@ -2,268 +2,266 @@ /* eslint-disable no-undef */ /* eslint-disable strict */ -const { join } = require("path"); -const video = require("wdio-video-reporter"); -require("dotenv").config(); +const { join } = require('path') +const video = require('wdio-video-reporter') +require('dotenv').config() exports.config = { - // - // ==================== - // Runner Configuration - // ==================== - // - // WebdriverIO allows it to run your tests in arbitrary locations (e.g. locally or - // on a remote machine). - runner: "local", - // - // Override default path ('/wd/hub') for chromedriver service. - // path: '/wd/hub', - // - // ================== - // Specify Test Files - // ================== - // Define which test specs should run. The pattern is relative to the directory - // from which `wdio` was called. Notice that, if you are calling `wdio` from an - // NPM script (see https://docs.npmjs.com/cli/run-script) then the current working - // directory is where your package.json resides, so `wdio` will be called from there. - // - specs: [ - "./tests/integration/tests/**/test-*-page.js", - ], - // Patterns to exclude. - exclude: [ - // 'path/to/excluded/files' - ], - // - // ============ - // Capabilities - // ============ - // Define your capabilities here. WebdriverIO can run multiple capabilities at the same - // time. Depending on the number of capabilities, WebdriverIO launches several test - // sessions. Within your capabilities you can overwrite the spec and exclude options in - // order to group specific specs to a specific capability. - // - // First, you can define how many instances should be started at the same time. Let's - // say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have - // set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec - // files and you set maxInstances to 10, all spec files will get tested at the same time - // and 30 processes will get spawned. The property handles how many capabilities - // from the same test should run tests. - // - maxInstances: 1, - // - // If you have trouble getting all important capabilities together, check out the - // Sauce Labs platform configurator - a great tool to configure your capabilities: - // https://docs.saucelabs.com/reference/platforms-configurator - // - capabilities: [{ - browserName: "firefox", - "moz:firefoxOptions": { - log: { level: "trace" }, - prefs: {}, - }, - }], - // - // =================== - // Test Configurations - // =================== - // Define all options that are relevant for the WebdriverIO instance here - // - // Level of logging verbosity: trace | debug | info | warn | error | silent - logLevel: "error", - // - // Set specific log levels per logger - // loggers: - // - webdriver, webdriverio - // - @wdio/applitools-service, @wdio/browserstack-service, @wdio/devtools-service, @wdio/sauce-service - // - @wdio/mocha-framework, @wdio/jasmine-framework - // - @wdio/local-runner, @wdio/lambda-runner - // - @wdio/sumologic-reporter - // - @wdio/cli, @wdio/config, @wdio/sync, @wdio/utils - // Level of logging verbosity: trace | debug | info | warn | error | silent - // logLevels: { - // webdriver: 'info', - // '@wdio/applitools-service': 'info' - // }, - // - // If you only want to run your tests until a specific amount of tests have failed use - // bail (default is 0 - don't bail, run all tests). - bail: 0, - // - // Set a base URL in order to shorten url command calls. If your `url` parameter starts - // with `/`, the base url gets prepended, not including the path portion of your baseUrl. - // If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url - // gets prepended directly. - baseUrl: process.env.SERVER_URL, - // - // Default timeout for all waitFor* commands. - waitforTimeout: 10000, - // - // Default timeout in milliseconds for request - // if browser driver or grid doesn't send response - connectionRetryTimeout: 90000, - // - // Default request retries count - connectionRetryCount: 3, - // - // Test runner services - // Services take over a specific job you don't want to take care of. They enhance - // your test setup with almost no effort. Unlike plugins, they don't add new - // commands. Instead, they hook themselves up into the test process. - services: [ - "firefox-profile", "selenium-standalone", ["image-comparison", { - baselineFolder: join(process.cwd(), "./tests/integration/tests/Visual_Baseline/"), - formatImageName: process.env.MOZ_HEADLESS ? "{tag}-headless-{width}x{height}" : "{tag}-{width}x{height}", - screenshotPath: join(process.cwd(), ".tmp/"), - savePerInstance: true, - // autoSaveBaseline: true, - }], - ], + // + // ==================== + // Runner Configuration + // ==================== + // + // WebdriverIO allows it to run your tests in arbitrary locations (e.g. locally or + // on a remote machine). + runner: 'local', + // + // Override default path ('/wd/hub') for chromedriver service. + // path: '/wd/hub', + // + // ================== + // Specify Test Files + // ================== + // Define which test specs should run. The pattern is relative to the directory + // from which `wdio` was called. Notice that, if you are calling `wdio` from an + // NPM script (see https://docs.npmjs.com/cli/run-script) then the current working + // directory is where your package.json resides, so `wdio` will be called from there. + // + specs: [ + './tests/integration/tests/**/test-*-page.js' + ], + // Patterns to exclude. + exclude: [ + // 'path/to/excluded/files' + ], + // + // ============ + // Capabilities + // ============ + // Define your capabilities here. WebdriverIO can run multiple capabilities at the same + // time. Depending on the number of capabilities, WebdriverIO launches several test + // sessions. Within your capabilities you can overwrite the spec and exclude options in + // order to group specific specs to a specific capability. + // + // First, you can define how many instances should be started at the same time. Let's + // say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have + // set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec + // files and you set maxInstances to 10, all spec files will get tested at the same time + // and 30 processes will get spawned. The property handles how many capabilities + // from the same test should run tests. + // + maxInstances: 1, + // + // If you have trouble getting all important capabilities together, check out the + // Sauce Labs platform configurator - a great tool to configure your capabilities: + // https://docs.saucelabs.com/reference/platforms-configurator + // + capabilities: [{ + browserName: 'firefox', + 'moz:firefoxOptions': { + log: { level: 'trace' }, + prefs: {} + } + }], + // + // =================== + // Test Configurations + // =================== + // Define all options that are relevant for the WebdriverIO instance here + // + // Level of logging verbosity: trace | debug | info | warn | error | silent + logLevel: 'error', + // + // Set specific log levels per logger + // loggers: + // - webdriver, webdriverio + // - @wdio/applitools-service, @wdio/browserstack-service, @wdio/devtools-service, @wdio/sauce-service + // - @wdio/mocha-framework, @wdio/jasmine-framework + // - @wdio/local-runner, @wdio/lambda-runner + // - @wdio/sumologic-reporter + // - @wdio/cli, @wdio/config, @wdio/sync, @wdio/utils + // Level of logging verbosity: trace | debug | info | warn | error | silent + // logLevels: { + // webdriver: 'info', + // '@wdio/applitools-service': 'info' + // }, + // + // If you only want to run your tests until a specific amount of tests have failed use + // bail (default is 0 - don't bail, run all tests). + bail: 0, + // + // Set a base URL in order to shorten url command calls. If your `url` parameter starts + // with `/`, the base url gets prepended, not including the path portion of your baseUrl. + // If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url + // gets prepended directly. + baseUrl: process.env.SERVER_URL, + // + // Default timeout for all waitFor* commands. + waitforTimeout: 10000, + // + // Default timeout in milliseconds for request + // if browser driver or grid doesn't send response + connectionRetryTimeout: 90000, + // + // Default request retries count + connectionRetryCount: 3, + // + // Test runner services + // Services take over a specific job you don't want to take care of. They enhance + // your test setup with almost no effort. Unlike plugins, they don't add new + // commands. Instead, they hook themselves up into the test process. + services: [ + 'firefox-profile', 'selenium-standalone', ['image-comparison', { + baselineFolder: join(process.cwd(), './tests/integration/tests/Visual_Baseline/'), + formatImageName: process.env.MOZ_HEADLESS ? '{tag}-headless-{width}x{height}' : '{tag}-{width}x{height}', + screenshotPath: join(process.cwd(), '.tmp/'), + savePerInstance: true + // autoSaveBaseline: true, + }] + ], - // Framework you want to run your specs with. - // The following are supported: Mocha, Jasmine, and Cucumber - // see also: https://webdriver.io/docs/frameworks.html - // - // Make sure you have the wdio adapter package for the specific framework installed - // before running any tests. - framework: "mocha", - // - // The number of times to retry the entire specfile when it fails as a whole - // specFileRetries: 1, - // - // Test reporter for stdout. - // The only one supported by default is 'dot' - // see also: https://webdriver.io/docs/dot-reporter.html - reporters: ["dot", "spec", [video, { - saveAllVideos: false, - videoSlowdownMultiplier: 25, - outputDir: "tests/integration/errorShots/videos", - }]], + // Framework you want to run your specs with. + // The following are supported: Mocha, Jasmine, and Cucumber + // see also: https://webdriver.io/docs/frameworks.html + // + // Make sure you have the wdio adapter package for the specific framework installed + // before running any tests. + framework: 'mocha', + // + // The number of times to retry the entire specfile when it fails as a whole + // specFileRetries: 1, + // + // Test reporter for stdout. + // The only one supported by default is 'dot' + // see also: https://webdriver.io/docs/dot-reporter.html + reporters: ['dot', 'spec', [video, { + saveAllVideos: false, + videoSlowdownMultiplier: 25, + outputDir: 'tests/integration/errorShots/videos' + }]], - // - // Options to be passed to Mocha. - // See the full list at http://mochajs.org/ - mochaOpts: { - ui: "bdd", - timeout: 120000, - }, - // - // ===== - // Hooks - // ===== - // WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance - // it and to build services around it. You can either apply a single function or an array of - // methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got - // resolved to continue. - /** + // + // Options to be passed to Mocha. + // See the full list at http://mochajs.org/ + mochaOpts: { + ui: 'bdd', + timeout: 120000 + }, + // + // ===== + // Hooks + // ===== + // WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance + // it and to build services around it. You can either apply a single function or an array of + // methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got + // resolved to continue. + /** * Gets executed once before all workers get launched. * @param {Object} config wdio configuration object * @param {Array.} capabilities list of capabilities details */ - // onPrepare: function (config, capabilities) { - // }, - /** + // onPrepare: function (config, capabilities) { + // }, + /** * Gets executed just before initialising the webdriver session and test framework. It allows you * to manipulate configurations depending on the capability or spec. * @param {Object} config wdio configuration object * @param {Array.} capabilities list of capabilities details * @param {Array.} specs List of spec file paths that are to be run */ - //beforeSession: function (config, capabilities, specs) { - //}, - /** + // beforeSession: function (config, capabilities, specs) { + // }, + /** * Gets executed before test execution begins. At this point you can access to all global * variables like `browser`. It is the perfect place to define custom commands. * @param {Array.} capabilities list of capabilities details * @param {Array.} specs List of spec file paths that are to be run */ - before: function (capabilities, specs) { - const chai = require("chai"); + before: function (capabilities, specs) { + const chai = require('chai') - global.expect = chai.expect; - chai.Should(); + global.expect = chai.expect + chai.Should() - global.primaryEmail = "test@mailinator.com"; - global.secondaryEmail = "test" + Math.random() + "@mailinator.com"; - global.monitorFxaPassword = process.env.MONITOR_FXA_PASSWORD || "a_secure_password ;)"; - browser.setWindowSize(1280, 720); - - }, - /** + global.primaryEmail = 'test@mailinator.com' + global.secondaryEmail = 'test' + Math.random() + '@mailinator.com' + global.monitorFxaPassword = process.env.MONITOR_FXA_PASSWORD || 'a_secure_password ;)' + browser.setWindowSize(1280, 720) + }, + /** * Runs before a WebdriverIO command gets executed. * @param {String} commandName hook command name * @param {Array} args arguments that command would receive */ - // beforeCommand: function (commandName, args) { - // }, - /** + // beforeCommand: function (commandName, args) { + // }, + /** * Hook that gets executed before the suite starts * @param {Object} suite suite details */ - // beforeSuite: function (suite) { - // }, - /** + // beforeSuite: function (suite) { + // }, + /** * Function to be executed before a test (in Mocha/Jasmine) starts. */ - // beforeTest: function (test, context) { - // }, - /** + // beforeTest: function (test, context) { + // }, + /** * Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling * beforeEach in Mocha) */ - // beforeHook: function (test, context) { - // }, - /** + // beforeHook: function (test, context) { + // }, + /** * Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling * afterEach in Mocha) */ - // afterHook: function (test, context, { error, result, duration, passed, retries }) { - // }, - /** + // afterHook: function (test, context, { error, result, duration, passed, retries }) { + // }, + /** * Function to be executed after a test (in Mocha/Jasmine). */ - afterTest: function(test, context, { error, result, duration, passed, retries }) { - /* Take screenshots for debugging on circleci */ - if ("ERROR_SHOTS" in process.env) { - const path = join(process.cwd(), "./tests/integration/errorShots/error-"+Date.now()+".png"); - browser.saveScreenshot(path); - } - }, + afterTest: function (test, context, { error, result, duration, passed, retries }) { + /* Take screenshots for debugging on circleci */ + if ('ERROR_SHOTS' in process.env) { + const path = join(process.cwd(), './tests/integration/errorShots/error-' + Date.now() + '.png') + browser.saveScreenshot(path) + } + } - - /** + /** * Hook that gets executed after the suite has ended * @param {Object} suite suite details */ - // afterSuite: function (suite) { - // }, - /** + // afterSuite: function (suite) { + // }, + /** * Runs after a WebdriverIO command gets executed * @param {String} commandName hook command name * @param {Array} args arguments that command would receive * @param {Number} result 0 - command success, 1 - command error * @param {Object} error error object if any */ - // afterCommand: function (commandName, args, result, error) { - // }, - /** + // afterCommand: function (commandName, args, result, error) { + // }, + /** * Gets executed after all tests are done. You still have access to all global variables from * the test. * @param {Number} result 0 - test pass, 1 - test fail * @param {Array.} capabilities list of capabilities details * @param {Array.} specs List of spec file paths that ran */ - // after: function (result, capabilities, specs) { - // }, - /** + // after: function (result, capabilities, specs) { + // }, + /** * Gets executed right after terminating the webdriver session. * @param {Object} config wdio configuration object * @param {Array.} capabilities list of capabilities details * @param {Array.} specs List of spec file paths that ran */ - // afterSession: function (config, capabilities, specs) { - // }, - /** + // afterSession: function (config, capabilities, specs) { + // }, + /** * Gets executed after all workers got shut down and the process is about to exit. An error * thrown in the onComplete hook will result in the test run failing. * @param {Object} exitCode 0 - success, 1 - fail @@ -271,13 +269,13 @@ exports.config = { * @param {Array.} capabilities list of capabilities details * @param {} results object containing test results */ - // onComplete: function(exitCode, config, capabilities, results) { - // }, - /** + // onComplete: function(exitCode, config, capabilities, results) { + // }, + /** * Gets executed when a refresh happens. * @param {String} oldSessionId session ID of the old session * @param {String} newSessionId session ID of the new session */ - //onReload: function(oldSessionId, newSessionId) { - //} -}; + // onReload: function(oldSessionId, newSessionId) { + // } +} diff --git a/tests/integration/wdio.docker.js b/tests/integration/wdio.docker.js index 903ed9f38..f49df85fa 100644 --- a/tests/integration/wdio.docker.js +++ b/tests/integration/wdio.docker.js @@ -1,38 +1,38 @@ /* eslint-disable no-process-env */ -"use strict"; +'use strict' -const common = require("./wdio.conf.js"); -const { join } = require("path"); +const common = require('./wdio.conf.js') +const { join } = require('path') exports.config = Object.assign({}, common.config, { - baseUrl: "http://localhost:6060", + baseUrl: 'http://localhost:6060', maxInstances: 3, - services: ["docker", "firefox-profile", "selenium-standalone", ["image-comparison", { - baselineFolder: join(process.cwd(), "./tests/integration/tests/Visual_Baseline/"), - formatImageName: process.env.MOZ_HEADLESS ? "{tag}-headless-{width}x{height}" : "{tag}-{width}x{height}", - screenshotPath: join(process.cwd(), ".tmp/"), - savePerInstance: true, + services: ['docker', 'firefox-profile', 'selenium-standalone', ['image-comparison', { + baselineFolder: join(process.cwd(), './tests/integration/tests/Visual_Baseline/'), + formatImageName: process.env.MOZ_HEADLESS ? '{tag}-headless-{width}x{height}' : '{tag}-{width}x{height}', + screenshotPath: join(process.cwd(), '.tmp/'), + savePerInstance: true // autoSaveBaseline: true, }]], dockerOptions: { - image: "selenium/standalone-firefox", + image: 'selenium/standalone-firefox', healthCheck: { - url: "http://localhost:4444", + url: 'http://localhost:4444', maxRetries: 3, inspectInterval: 1000, - startDelay: 2000, + startDelay: 2000 }, options: { - e: ["MOZ_HEADLESS=1", - "MONITOR_FXA_PASSWORD=${MONITOR_FXA_PASSWORD}", - "HIBP_KANON_API_TOKEN=${HIBP_KANON_API_TOKEN}", - "HIBP_API_TOKEN=${HIBP_API_TOKEN}", - ], - p: ["4444:4444", "5900:5900"], - v: "/dev/shm:/dev/shm", - shmSize: "2g", - network: "host", + e: ['MOZ_HEADLESS=1', + 'MONITOR_FXA_PASSWORD=${MONITOR_FXA_PASSWORD}', + 'HIBP_KANON_API_TOKEN=${HIBP_KANON_API_TOKEN}', + 'HIBP_API_TOKEN=${HIBP_API_TOKEN}' + ], + p: ['4444:4444', '5900:5900'], + v: '/dev/shm:/dev/shm', + shmSize: '2g', + network: 'host' }, - dockerLogs: "docker-logs/", - }, -}); + dockerLogs: 'docker-logs/' + } +}) diff --git a/tests/jest.setup.js b/tests/jest.setup.js index c416f7ee9..62e013c75 100644 --- a/tests/jest.setup.js +++ b/tests/jest.setup.js @@ -1,3 +1,3 @@ -"use strict"; +'use strict' -jest.setTimeout(30000); +jest.setTimeout(30000) diff --git a/tests/middleware.test.js b/tests/middleware.test.js index 0dbd4c6ce..0e873a034 100644 --- a/tests/middleware.test.js +++ b/tests/middleware.test.js @@ -1,75 +1,69 @@ -"use strict"; +'use strict' +const DB = require('../db/DB') +const { FXA } = require('../lib/fxa') +const { requireSessionUser } = require('../middleware') +const { TEST_SUBSCRIBERS } = require('../db/seeds/test_subscribers') -const DB = require("../db/DB"); -const { FXA } = require("../lib/fxa"); -const { requireSessionUser } = require("../middleware"); -const { TEST_SUBSCRIBERS } = require("../db/seeds/test_subscribers"); +require('./resetDB') +test('requireSessionUser calls getProfileData, updateFxAProfileData, and sets req.user', async () => { + const req = { session: { user: TEST_SUBSCRIBERS.firefox_account } } + const res = jest.fn() + const next = jest.fn() + jest.mock('../db/DB') + jest.mock('../lib/fxa') + FXA.getProfileData = jest.fn() + FXA.getProfileData.mockReturnValueOnce({}) + DB.updateFxAProfileData = jest.fn() -require("./resetDB"); + await requireSessionUser(req, res, next) + expect(req.user.id).toEqual(TEST_SUBSCRIBERS.firefox_account.id) + expect(Number(req.user.fxa_uid)).toEqual(Number(TEST_SUBSCRIBERS.firefox_account.fxa_uid)) + const mockNextCalls = next.mock.calls + expect(mockNextCalls.length).toBe(1) -test("requireSessionUser calls getProfileData, updateFxAProfileData, and sets req.user", async () => { - const req = { session: { user: TEST_SUBSCRIBERS.firefox_account } }; - const res = jest.fn(); - const next = jest.fn(); - jest.mock("../db/DB"); - jest.mock("../lib/fxa"); - FXA.getProfileData = jest.fn(); - FXA.getProfileData.mockReturnValueOnce({}); - DB.updateFxAProfileData = jest.fn(); + const mockGetProfileDataCalls = FXA.getProfileData.mock.calls + expect(mockGetProfileDataCalls.length).toBe(1) + expect(mockGetProfileDataCalls[0][0]).toBe(TEST_SUBSCRIBERS.firefox_account.fxa_access_token) + const mockUpdateFxAProfileDataCalls = DB.updateFxAProfileData.mock.calls + expect(mockUpdateFxAProfileDataCalls.length).toBe(1) +}) - await requireSessionUser(req, res, next); +test('requireSessionUser clears session user and redirects to / if FXA error', async () => { + const req = { session: { user: TEST_SUBSCRIBERS.firefox_account } } + const res = { redirect: jest.fn() } + const next = jest.fn() + jest.mock('../lib/fxa') + FXA.getProfileData = jest.fn() + FXA.getProfileData.mockReturnValueOnce({ name: 'HTTPError' }) - expect(req.user.id).toEqual(TEST_SUBSCRIBERS.firefox_account.id); - expect(Number(req.user.fxa_uid)).toEqual(Number(TEST_SUBSCRIBERS.firefox_account.fxa_uid)); - const mockNextCalls = next.mock.calls; - expect(mockNextCalls.length).toBe(1); + await requireSessionUser(req, res, next) - const mockGetProfileDataCalls = FXA.getProfileData.mock.calls; - expect(mockGetProfileDataCalls.length).toBe(1); - expect(mockGetProfileDataCalls[0][0]).toBe(TEST_SUBSCRIBERS.firefox_account.fxa_access_token); - const mockUpdateFxAProfileDataCalls = DB.updateFxAProfileData.mock.calls; - expect(mockUpdateFxAProfileDataCalls.length).toBe(1); -}); + expect(req.session.hasOwnProperty('user')).toBeFalsy() + const mockRedirectCallArgs = res.redirect.mock.calls[0] + expect(mockRedirectCallArgs[0]).toBe('/') +}) +test('requireSessionUser redirects to /oauth/init if no user', async () => { + const req = { session: { } } + const res = { redirect: jest.fn() } + const next = jest.fn() -test("requireSessionUser clears session user and redirects to / if FXA error", async () => { - const req = { session: { user: TEST_SUBSCRIBERS.firefox_account } }; - const res = { redirect: jest.fn() }; - const next = jest.fn(); - jest.mock("../lib/fxa"); - FXA.getProfileData = jest.fn(); - FXA.getProfileData.mockReturnValueOnce({ name: "HTTPError" }); + await requireSessionUser(req, res, next) - await requireSessionUser(req, res, next); + const mockRedirectCallArgs = res.redirect.mock.calls[0] + expect(mockRedirectCallArgs[0]).toBe('/oauth/init?') +}) - expect(req.session.hasOwnProperty("user")).toBeFalsy(); - const mockRedirectCallArgs = res.redirect.mock.calls[0]; - expect(mockRedirectCallArgs[0]).toBe("/"); -}); +test('requireSessionUser redirect preserves utm params', async () => { + const req = { session: { }, query: { utm_campaign: 'direct-to-dashboard' } } + const res = { redirect: jest.fn() } + const next = jest.fn() + await requireSessionUser(req, res, next) -test("requireSessionUser redirects to /oauth/init if no user", async () => { - const req = { session: { } }; - const res = { redirect: jest.fn() }; - const next = jest.fn(); - - await requireSessionUser(req, res, next); - - const mockRedirectCallArgs = res.redirect.mock.calls[0]; - expect(mockRedirectCallArgs[0]).toBe("/oauth/init?"); -}); - - -test("requireSessionUser redirect preserves utm params", async () => { - const req = { session: { }, query: {"utm_campaign": "direct-to-dashboard"} }; - const res = { redirect: jest.fn() }; - const next = jest.fn(); - - await requireSessionUser(req, res, next); - - const mockRedirectCallArgs = res.redirect.mock.calls[0]; - expect(mockRedirectCallArgs[0]).toBe("/oauth/init?utm_campaign=direct-to-dashboard"); -}); + const mockRedirectCallArgs = res.redirect.mock.calls[0] + expect(mockRedirectCallArgs[0]).toBe('/oauth/init?utm_campaign=direct-to-dashboard') +}) diff --git a/tests/resetDB.js b/tests/resetDB.js index b2c06e3fc..da3d4ebcb 100644 --- a/tests/resetDB.js +++ b/tests/resetDB.js @@ -1,32 +1,29 @@ -"use strict"; +'use strict' -const Knex = require("knex"); - -const DB = require("../db/DB"); -const knexConfig = require("../db/knexfile"); -const test_data = require("../db/seeds/test_subscribers"); +const Knex = require('knex') +const DB = require('../db/DB') +const knexConfig = require('../db/knexfile') +const test_data = require('../db/seeds/test_subscribers') // (Re-)create DB connection at the beginning of each test suite beforeAll(() => { - DB.createConnection(); -}); - + DB.createConnection() +}) // Reset the subscribers records before each test beforeEach(async () => { - const knex = Knex(knexConfig); - await knex("email_addresses").del(); - await knex("subscribers").del(); - await knex("subscribers").insert(Object.values(test_data.TEST_SUBSCRIBERS)); - await knex("email_addresses").insert(Object.values(test_data.TEST_EMAIL_ADDRESSES)); - knex.destroy(); -}); - + const knex = Knex(knexConfig) + await knex('email_addresses').del() + await knex('subscribers').del() + await knex('subscribers').insert(Object.values(test_data.TEST_SUBSCRIBERS)) + await knex('email_addresses').insert(Object.values(test_data.TEST_EMAIL_ADDRESSES)) + knex.destroy() +}) // Destroy DB connection at the end of each test suite // Without this, some test failures leave the handle open // causing the test runner to hang. afterAll(() => { - DB.destroyConnection(); -}); + DB.destroyConnection() +}) diff --git a/tests/sha1.test.js b/tests/sha1.test.js index 9e66a839c..af9951c56 100644 --- a/tests/sha1.test.js +++ b/tests/sha1.test.js @@ -1,18 +1,16 @@ -"use strict"; +'use strict' -const getSha1 = require("../sha1-utils"); +const getSha1 = require('../sha1-utils') - -function isHexString(hashDigest) { +function isHexString (hashDigest) { for (const character of hashDigest) { if (parseInt(character, 16).toString(16) !== character.toLowerCase()) { - return false; + return false } } - return true; + return true } - -test("getSha1 returns hex digest", () => { - expect(isHexString(getSha1("test@test.com"))).toBeTruthy(); -}); +test('getSha1 returns hex digest', () => { + expect(isHexString(getSha1('test@test.com'))).toBeTruthy() +}) diff --git a/tests/test-breaches.js b/tests/test-breaches.js index 3614975af..0ad4e1712 100644 --- a/tests/test-breaches.js +++ b/tests/test-breaches.js @@ -1,45 +1,45 @@ -"use strict"; +'use strict' const testBreaches = [ { - Title: "Test", - Name: "Test", - Domain: "test.com", - BreachDate: "2012-12-21", - AddedDate: "2013-01-01", + Title: 'Test', + Name: 'Test', + Domain: 'test.com', + BreachDate: '2012-12-21', + AddedDate: '2013-01-01', DataClasses: [], IsSpamList: false, IsFabricated: false, IsVerified: true, - IsRetired: false, + IsRetired: false }, { - Title: "Test2", - Name: "Test2", - Domain: "test2.com", - BreachDate: "2016-11-08", - AddedDate: "2017-01-01", + Title: 'Test2', + Name: 'Test2', + Domain: 'test2.com', + BreachDate: '2016-11-08', + AddedDate: '2017-01-01', DataClasses: [], IsSpamList: false, IsFabricated: false, IsVerified: true, - IsRetired: false, + IsRetired: false }, { - Title: "Sensitive", - Name: "Sensitive", - Domain: "sensitive.com", - BreachDate: "2017-11-08", - AddedDate: "2018-11-08", + Title: 'Sensitive', + Name: 'Sensitive', + Domain: 'sensitive.com', + BreachDate: '2017-11-08', + AddedDate: '2018-11-08', IsSensitive: true, DataClasses: [], IsSpamList: false, IsFabricated: false, IsVerified: true, - IsRetired: false, - }, -]; + IsRetired: false + } +] module.exports = { - testBreaches, -}; + testBreaches +}