fix #941: implement remove button
This commit is contained in:
Родитель
e970f7bede
Коммит
267d134eae
|
@ -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,
|
||||
|
|
5
db/DB.js
5
db/DB.js
|
@ -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>
|
Загрузка…
Ссылка в новой задаче