Skip to content

Commit

Permalink
fix mozilla#1110: add /user/breach-stats endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
groovecoder committed Jul 10, 2019
1 parent fe1d754 commit 93a56c7
Show file tree
Hide file tree
Showing 9 changed files with 120 additions and 70 deletions.
2 changes: 1 addition & 1 deletion controllers/home.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use strict";

const AppConstants = require("../app-constants");
const scanResult = require("../scan-results");
const { scanResult } = require("../scan-results");
const { generatePageToken } = require("./utils");


Expand Down
2 changes: 1 addition & 1 deletion controllers/scan.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const AppConstants = require("../app-constants");
const { FluentError } = require("../locale-utils");
// const { generatePageToken } = require("./utils");
const mozlog = require("../log");
const scanResult = require("../scan-results");
const { scanResult } = require("../scan-results");
const sha1 = require("../sha1-utils");

const log = mozlog("controllers.scan");
Expand Down
31 changes: 31 additions & 0 deletions controllers/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ 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");


Expand Down Expand Up @@ -337,6 +338,35 @@ async function getPreferences(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.",
});
}
const fxaResponse = await FXA.verifyOAuthToken(req.token);
if (!fxaResponse) {
return res.status(404).json({
errorMessage: "Cannot find FXA for that OAuth token.",
});
}
const user = await DB.getSubscriberByFxaUid(fxaResponse.body.user);
if (!fxaResponse) {
return res.status(404).json({
errorMessage: "Cannot find Monitor subscriber for that FXA.",
});
}
const allBreaches = req.app.locals.breaches;
const { verifiedEmails } = await getAllEmailsAndBreaches(user, allBreaches);
const breachStats = resultsSummary(verifiedEmails);
return res.json({
monitoredEmails: breachStats.monitoredEmails.count,
numBreaches: breachStats.numBreaches.count,
passwords: breachStats.passwords.count,
});
}


function logout(req, res) {
req.session.reset();
res.redirect("/");
Expand All @@ -346,6 +376,7 @@ function logout(req, res) {
module.exports = {
getPreferences,
getDashboard,
getBreachStats,
add,
verify,
getUnsubscribe,
Expand Down
29 changes: 19 additions & 10 deletions db/DB.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,7 @@ const DB = {
return res;
},

async getSubscriberById(id) {
const [subscriber] = await knex("subscribers").where({
"id": id,
});
async joinEmailAddressesToSubscriber(subscriber) {
if (subscriber) {
subscriber.email_addresses = await knex("email_addresses").where({
"subscriber_id": subscriber.id,
Expand All @@ -62,17 +59,29 @@ const DB = {
return subscriber;
},

async getSubscriberById(id) {
const [subscriber] = await knex("subscribers").where({
"id": 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 getSubscriberByEmail(email) {
const [subscriber] = await knex("subscribers").where({
"primary_email": email,
"primary_verified": true,
});
if (subscriber) {
subscriber.email_addresses = await knex("email_addresses").where({
"subscriber_id": subscriber.id,
});
}
return subscriber;
const subscriberAndEmails = await this.joinEmailAddressesToSubscriber(subscriber);
return subscriberAndEmails;
},

async getEmailAddressRecordByEmail(email) {
Expand Down
32 changes: 25 additions & 7 deletions lib/fxa.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,37 @@ const log = mozlog("fxa");

const FXA = {

async destroyOAuthToken(token) {
async _postTokenRequest(path, token) {
const fxaTokenOrigin = new URL(AppConstants.OAUTH_TOKEN_URI).origin;
const tokenDestroyUrl = `${fxaTokenOrigin}/v1/destroy`;
const tokenDestroyParams = token;
const tokenDestroyOptions = {
const tokenUrl = `${fxaTokenOrigin}${path}`;
const tokenBody = (typeof token === "object") ? token : {token};
const tokenOptions = {
method: "POST",
headers: {"Authorization": `Bearer ${AppConstants.OAUTH_CLIENT_SECRET}`},
json: true,
body: tokenDestroyParams,
body: tokenBody,
};

try {
return await got(tokenDestroyUrl, tokenDestroyOptions);
const response = await got(tokenUrl, tokenOptions);
return response;
} catch (e) {
log.error("_postTokenRequest", {stack: e.stack});
}
},

async verifyOAuthToken(token) {
try {
const response = await this._postTokenRequest("/v1/verify", token);
return response;
} catch (e) {
log.error("verifyOAuthToken", {stack: e.stack});
}
},

async destroyOAuthToken(token) {
try {
const response = await this._postTokenRequest("/v1/destroy", token);
return response;
} catch (e) {
log.error("destroyOAuthToken", {stack: e.stack});
}
Expand Down
6 changes: 5 additions & 1 deletion routes/user.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"use strict";

const express = require("express");
const bearerToken = require("express-bearer-token");
const bodyParser = require("body-parser");
const csrf = require("csurf");

const { asyncMiddleware } = require("../middleware");
const {
add, verify, logout,
getDashboard, getPreferences, removeEmail, resendEmail, updateCommunicationOptions,
getDashboard, getPreferences, getBreachStats,
removeEmail, resendEmail, updateCommunicationOptions,
getUnsubscribe, postUnsubscribe, getRemoveFxm, postRemoveFxm,
} = require("../controllers/user");

Expand All @@ -19,6 +21,8 @@ const csrfProtection = csrf();

router.get("/dashboard", csrfProtection, asyncMiddleware(getDashboard));
router.get("/preferences", csrfProtection, asyncMiddleware(getPreferences));
router.use("/breach-stats", bearerToken());
router.get("/breach-stats", urlEncodedParser, asyncMiddleware(getBreachStats));
router.get("/logout", logout);
router.post("/email", urlEncodedParser, csrfProtection, asyncMiddleware(add));
router.post("/remove-email", urlEncodedParser, csrfProtection, asyncMiddleware(removeEmail));
Expand Down
31 changes: 30 additions & 1 deletion scan-results.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,33 @@ const scanResult = async(req, selfScan=false) => {
};
};

module.exports = scanResult;
function resultsSummary(verifiedEmails) {
const breachStats = {
monitoredEmails: { count: 0 },
numBreaches: { count: 0 },
passwords: { count: 0 },
};
let foundBreaches = [];

breachStats.monitoredEmails.count = verifiedEmails.length;

// 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 => {
const dataClasses = breach.DataClasses;
if (dataClasses.includes("passwords")) {
breachStats.passwords.count++;
}
});
foundBreaches = [...foundBreaches, ...email.breaches];
});
// tally up total number of breaches
breachStats.numBreaches.count = foundBreaches.length;
return breachStats;
}

module.exports = {
scanResult,
resultsSummary,
};
55 changes: 7 additions & 48 deletions template-helpers/breach-stats.js
Original file line number Diff line number Diff line change
@@ -1,60 +1,19 @@
"use strict";

const { LocaleUtils } = require("./../locale-utils");
const { resultsSummary } = require("../scan-results");


function getBreachStats(args) {
let foundBreaches = [];
const verifiedEmails = args.data.root.verifiedEmails;
const locales = args.data.root.req.supportedLocales;

const breachStats = {
monitoredEmails: {
subhead: "",
count: 0,
},
numBreaches: {
subhead: "",
count: 0,
},
passwords: {
subhead: "",
affectedAccounts: {},
count: 0,
},
};
const breachStats = resultsSummary(args.data.root.verifiedEmails);

const locales = args.data.root.req.supportedLocales;

breachStats.monitoredEmails.count = verifiedEmails.length;
breachStats.monitoredEmails["allVerifiedEmails"] = verifiedEmails;
breachStats.monitoredEmails.subhead = LocaleUtils.fluentFormat(locales, "email-addresses-being-monitored", { emails: verifiedEmails.length });

// combine the breaches for each account, breach duplicates are ok
// since the user may have multiple accounts with different emails
verifiedEmails.forEach(email => {
const emailAddress = email.email;
email.breaches.forEach(breach => {
const dataClasses = breach.DataClasses;
if (dataClasses.includes("passwords")) {
breachStats.passwords.count++;
if (!breachStats.passwords.affectedAccounts[emailAddress]) {
breachStats.passwords.affectedAccounts[emailAddress] = {
"affectedEmailAddress": emailAddress,
"breaches": [],
};
}
breachStats.passwords.affectedAccounts[emailAddress].breaches.push(breach.Name);
}
});
breachStats.passwords.subhead = LocaleUtils.fluentFormat(locales, "passwords-exposed", { passwords: breachStats.passwords.count });
foundBreaches = [...foundBreaches, ...email.breaches];
});

// tally up total number of breaches
breachStats.numBreaches.count = foundBreaches.length;

// get localized string, send foundBreaches.length to get correct singular/plural version
breachStats.numBreaches.subhead = LocaleUtils.fluentFormat(locales, "known-data-breaches-exposed", { breaches: foundBreaches.length });
breachStats.monitoredEmails.subhead = LocaleUtils.fluentFormat(locales, "email-addresses-being-monitored", { emails: verifiedEmails.length });
breachStats.passwords.subhead = LocaleUtils.fluentFormat(locales, "passwords-exposed", { passwords: breachStats.passwords.count });
// get localized string, send foundBreaches.length to get correct singular/plural version
breachStats.numBreaches.subhead = LocaleUtils.fluentFormat(locales, "known-data-breaches-exposed", { breaches: breachStats.numBreaches.count });

return breachStats;
}
Expand Down
2 changes: 1 addition & 1 deletion tests/controllers/home.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

const AppConstants = require("../../app-constants");
const home = require("../../controllers/home");
const scanResult = require("../../scan-results");
const { scanResult } = require("../../scan-results");

let mockRequest = { fluentFormat: jest.fn(), csrfToken: jest.fn() };

Expand Down

0 comments on commit 93a56c7

Please sign in to comment.