This commit is contained in:
Luke Crouch 2019-05-22 13:35:22 -05:00
Родитель e970f7bede
Коммит 267d134eae
10 изменённых файлов: 99 добавлений и 87 удалений

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

@ -1,6 +1,5 @@
"use strict";
const crypto = require("crypto");
const isemail = require("isemail");
@ -226,22 +225,26 @@ async function verify(req, res) {
}
// legacy /user/unsubscribe controller for pre-FxA unsubscribe links
async function getUnsubscribe(req, res) {
if (!req.query.token) {
throw new FluentError("user-unsubscribe-token-error");
}
const subscriber = await DB.getSubscriberByToken(req.query.token);
// Token is for a primary email address,
// redirect to preferences to remove Firefox Monitor
if (subscriber) {
return res.redirect("/user/preferences");
}
//throws error if user backs into and refreshes unsubscribe page
if (!subscriber) {
const emailAddress = await DB.getEmailByToken(req.query.token);
if (!subscriber && !emailAddress) {
throw new FluentError("error-not-subscribed");
}
res.render("subpage", {
title: req.fluentFormat("user-unsubscribe-title"),
headline: req.fluentFormat("unsub-headline"),
subhead: req.fluentFormat("unsub-blurb"),
whichPartial: "subpages/unsubscribe",
token: req.query.token,
hash: req.query.hash,
@ -249,22 +252,46 @@ async function getUnsubscribe(req, res) {
}
async function getRemoveFxm(req, res) {
const sessionUser = _requireSessionUser(req);
res.render("subpage", {
title: req.fluentFormat("remove-fxm"),
subscriber: sessionUser,
whichPartial: "subpages/remove_fxm",
});
}
async function postRemoveFxm(req, res) {
const sessionUser = _requireSessionUser(req);
await DB.removeSubscriber(sessionUser);
req.session.reset();
res.redirect("/");
}
async function postUnsubscribe(req, res) {
if (!req.body.token || !req.body.emailHash) {
const { token, emailHash } = req.body;
if (!token || !emailHash) {
throw new FluentError("user-unsubscribe-token-email-error");
}
const unsubscribedUser = await DB.removeSubscriberByToken(req.body.token, req.body.emailHash);
await FXA.revokeOAuthToken(unsubscribedUser.fxa_refresh_token);
// if user backs into unsubscribe page and clicks "unsubscribe" again
// legacy unsubscribe link page uses removeSubscriberByToken
const unsubscribedUser = await DB.removeSubscriberByToken(token, emailHash);
if (!unsubscribedUser) {
throw new FluentError("error-not-subscribed");
const emailAddress = await DB.getEmailByToken(token);
if (!emailAddress) {
throw new FluentError("error-not-subscribed");
}
await DB.removeOneSecondaryEmail(emailAddress.id);
return res.redirect("/user/preferences");
}
const surveyTicket = crypto.randomBytes(40).toString("hex");
req.session.unsub = surveyTicket;
res.redirect("unsubscribe_survey");
await FXA.revokeOAuthToken(unsubscribedUser.fxa_refresh_token);
req.session.reset();
res.redirect("/");
}
@ -282,28 +309,6 @@ async function getPreferences(req, res) {
}
function getUnsubSurvey(req, res) {
//throws error if user refreshes unsubscribe survey page after they have submitted an answer
if(!req.session.unsub) {
throw new FluentError("error-not-subscribed");
}
res.render("subpage", {
title: req.fluentFormat("user-unsubscribe-survey-title"),
headline: req.fluentFormat("unsub-survey-headline-v2"),
subhead: req.fluentFormat("unsub-survey-blurb-v2"),
whichPartial: "subpages/unsubscribe_survey",
});
}
function postUnsubSurvey(req, res) {
//clear session in case a user subscribes / unsubscribes multiple times or with multiple email addresses.
req.session.reset();
res.send({
title: req.fluentFormat("user-unsubscribed-title"),
});
}
function logout(req, res) {
req.session.reset();
res.redirect("/");
@ -317,8 +322,8 @@ module.exports = {
verify,
getUnsubscribe,
postUnsubscribe,
getUnsubSurvey,
postUnsubSurvey,
getRemoveFxm,
postRemoveFxm,
logout,
removeEmail,
resendEmail,

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

@ -259,6 +259,11 @@ const DB = {
return updatedSubscriber;
},
async removeSubscriber(subscriber) {
await knex("email_addresses").where({"subscriber_id": subscriber.id}).del();
await knex("subscribers").where({"id": subscriber.id}).del();
},
async removeSubscriberByEmail(email) {
const sha1 = getSha1(email);
return await this._getSha1EntryAndDo(sha1, async aEntry => {

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

@ -6,28 +6,29 @@ async function sendForm(action, formBody={}) {
headers: {
"Content-Type": "application/json; charset=utf-8",
},
mode: "cors",
mode: "same-origin",
method: "POST",
body: JSON.stringify(formBody),
});
if (response.redirected) {
window.location = response.url;
return;
}
return await response.json();
}
async function sendCommunicationOption(e) {
const radioButton = e.target;
const formAction = radioButton.dataset.formAction;
const option = radioButton.dataset.commOption;
sendForm(formAction, { communicationOption: option })
const { formAction, commOption } = e.target.dataset;
sendForm(formAction, { communicationOption: commOption })
.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, emailId } = resendEmailBtn.dataset;
resendEmailBtn.classList.add("email-sent");
const emailId = resendEmailBtn.dataset.emailId;
const formAction = resendEmailBtn.dataset.formAction;
await sendForm(formAction, { emailId: emailId })
.then(data => {
@ -51,3 +52,11 @@ if (document.querySelector(".email-card")) {
option.addEventListener("click", sendCommunicationOption);
});
}
if (document.querySelector(".remove-fxm")) {
const removeMonitorButton = document.querySelector(".remove-fxm");
removeMonitorButton.addEventListener("click", async (e) => {
const {formAction, primaryToken, primaryHash} = e.target.dataset;
await sendForm(formAction, {primaryToken, primaryHash});
});
}

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

@ -4,7 +4,11 @@ const express = require("express");
const bodyParser = require("body-parser");
const { asyncMiddleware } = require("../middleware");
const { getDashboard,getPreferences, add, verify, getUnsubscribe, postUnsubscribe, getUnsubSurvey, postUnsubSurvey, removeEmail, resendEmail, updateCommunicationOptions, logout } = require("../controllers/user");
const {
add, verify, logout,
getDashboard, getPreferences, removeEmail, resendEmail, updateCommunicationOptions,
getUnsubscribe, postUnsubscribe, getRemoveFxm, postRemoveFxm,
} = require("../controllers/user");
const router = express.Router();
const jsonParser = bodyParser.json();
@ -19,10 +23,10 @@ router.post("/remove-email", urlEncodedParser, asyncMiddleware(removeEmail));
router.post("/resend-email", jsonParser, asyncMiddleware(resendEmail));
router.post("/update-comm-option", jsonParser, asyncMiddleware(updateCommunicationOptions));
router.get("/verify", jsonParser, asyncMiddleware(verify));
router.use("/email/unsubscribe", urlEncodedParser);
router.get("/email/unsubscribe", asyncMiddleware(getUnsubscribe));
router.post("/email/unsubscribe", asyncMiddleware(postUnsubscribe));
router.get("/email/unsubscribe_survey", getUnsubSurvey);
router.post("/email/unsubscribe_survey", jsonParser, postUnsubSurvey);
router.use("/unsubscribe", urlEncodedParser);
router.get("/unsubscribe", asyncMiddleware(getUnsubscribe));
router.post("/unsubscribe", asyncMiddleware(postUnsubscribe));
router.get("/remove-fxm", urlEncodedParser, asyncMiddleware(getRemoveFxm));
router.post("/remove-fxm", jsonParser, asyncMiddleware(postRemoveFxm));
module.exports = router;

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

@ -156,8 +156,8 @@ app.locals.UTM_SOURCE = new URL(AppConstants.SERVER_URL).hostname;
app.use(sessions({
cookieName: "session",
secret: AppConstants.COOKIE_SECRET,
duration: 15 * 60 * 1000, // 15 minutes
activeDuration: 5 * 60 * 1000, // 5 minutes
duration: 60 * 60 * 1000, // 60 minutes
activeDuration: 15 * 60 * 1000, // 15 minutes
cookie: cookie,
}));

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

@ -246,7 +246,7 @@ test("user verify request with invalid token returns error", async () => {
});
test("user unsubscribe GET request with valid token and hash returns 200 without error", async () => {
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);
@ -258,10 +258,16 @@ test("user unsubscribe GET request with valid token and hash returns 200 without
// Call code-under-test
await user.getUnsubscribe(req, resp);
expect(resp.statusCode).toEqual(200);
expect(resp.statusCode).toEqual(302);
expect(resp._getRedirectUrl()).toEqual("/user/preferences");
});
// TODO: test("remove-email POST");
// TODO: test("remove-fxm get");
// TODO: test("remove-fxm POST");
test("user unsubscribe GET request with invalid token returns error", async () => {
const invalidToken = "123456789";
@ -276,23 +282,21 @@ test("user unsubscribe GET request with invalid token returns error", async () =
});
test("user unsubscribe POST request with valid hash and token unsubscribes user and calls FXA.revokeOAuthToken", async () => {
const validToken = TEST_SUBSCRIBERS.unverified_email.primary_verification_token;
const validHash = getSha1(TEST_SUBSCRIBERS.unverified_email.primary_email);
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();
FXA.revokeOAuthToken = jest.fn();
// Call code-under-test
await user.postUnsubscribe(req, resp);
expect(resp.statusCode).toEqual(302);
const subscriber = await DB.getSubscriberByToken(validToken);
expect(subscriber).toBeUndefined();
const mockCalls = FXA.revokeOAuthToken.mock.calls;
expect(mockCalls.length).toEqual(1);
expect(resp._getRedirectUrl()).toEqual("/user/preferences");
const emailAddress = await DB.getEmailByToken(validToken);
expect(emailAddress).toBeUndefined();
});

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

@ -36,9 +36,7 @@
{{> forms/add-another-email-form }}
</div>
<div class="pref remove">
<h3 class="pref-section-headline remove">{{ getString "remove-fxm" }}</h3>
<p class="subhead">{{ getString "remove-fxm-blurb" }}</p>
<button class="remove-fxm subhead flx" href="/">{{ getString "remove-fxm" }}{{> svg/arrow-head-right }}</button>
<a href="/user/remove-fxm"><h3 class="remove-fxm subhead">{{ getString "remove-fxm" }}{{> svg/arrow-head-right }}</h3></a>
</div>
</div>
</section>

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

@ -0,0 +1,5 @@
<section id="unsubscribe" class="half">
<h3 class="pref-section-headline remove">{{ getString "remove-fxm" }}</h3>
<p class="subhead">{{ getString "remove-fxm-blurb" }}</p>
<button class="remove-fxm subhead flx" data-form-action="remove-fxm" data-primary-token="{{ primaryToken }}" data-primary-hash="{{ primaryHash }}" href="#">{{ getString "remove-fxm" }}{{> svg/arrow-head-right }}</button>
</section>

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

@ -1,4 +1,6 @@
<section id="unsubscribe" class="half">
<h3 class="pref-section-headline remove">{{{ getString "unsub-headline" }}}</h3>
<p class="subhead">{{{ getString "unsub-blurb" }}}</p>
<form action="/user/unsubscribe" class="form-group" method="post" id="unsubscribe-form">
<input type="hidden" name="token" value="{{ token }}">
<input type="hidden" name="emailHash" value="{{ hash }}">

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

@ -1,20 +0,0 @@
<section id="unsubscribe-survey" class="unsubscribe-survey section-wrapper drop-shadow">
<div class="whole">
<form action="" method="post" id="unsubscribe-survey-form" class="form-group">
<span class="secondary-title" tabindex="1">{{{getString "unsub-survey-form-label"}}}</span>
{{#loop 1 6 1}}
<div class="radio-button-group" tabindex="1">
<input id="reason{{ this }}" type="radio" name="unsubscribe-reason" data-analytics-label="unsub-reason-{{ this }}" value="{{getStringID "unsub-reason-" this}}" />
<span class="radio-dot"></span>
<label for="reason{{ this }}">{{{getStringID "unsub-reason-" this}}}</label>
</div>
{{/loop}}
<div class="input-group-button">
<span class="thank-you-message">{{getString "unsub-survey-thankyou"}}</span>
<span class="error-message">{{getString "unsub-survey-error"}}</span>
<input id="unsubscribe-survey-submit" type="submit" class="button submit transparent-button" value="{{getString "unsub-survey-button"}}" tabindex="1" />
<img class="loader" src="/img/loader.gif" alt="" />
</div>
</form>
</div>
</section>