mirror of
https://github.com/wassname/talk.git
synced 2026-07-21 12:51:03 +08:00
implemented unsubscribe button
This commit is contained in:
+9
-1
@@ -1,10 +1,16 @@
|
||||
const debug = require('debug')('talk:graph:connectors');
|
||||
const merge = require('lodash/merge');
|
||||
|
||||
// Config.
|
||||
const config = require('../config');
|
||||
|
||||
// Secrets.
|
||||
const secrets = require('../secrets');
|
||||
|
||||
// Errors.
|
||||
const errors = require('../errors');
|
||||
|
||||
// Graph
|
||||
// Graph.
|
||||
const { getBroker } = require('./subscriptions/broker');
|
||||
const { getPubsub } = require('./subscriptions/pubsub');
|
||||
const resolvers = require('./resolvers');
|
||||
@@ -50,6 +56,8 @@ const Wordlist = require('../services/wordlist');
|
||||
// Connectors.
|
||||
const defaultConnectors = {
|
||||
errors,
|
||||
config,
|
||||
secrets,
|
||||
models: {
|
||||
Action,
|
||||
Asset,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
const { groupBy, forEach } = require('lodash');
|
||||
const debug = require('debug')('talk-plugin-notifications');
|
||||
const uuid = require('uuid/v4');
|
||||
const { UNSUBSCRIBE_SUBJECT } = require('./config');
|
||||
|
||||
class NotificationManager {
|
||||
constructor(context) {
|
||||
@@ -73,7 +75,11 @@ class NotificationManager {
|
||||
|
||||
async send(ctx, userID, date, handler, context) {
|
||||
const {
|
||||
connectors: { services: { Mailer, I18n: { t } } },
|
||||
connectors: {
|
||||
secrets: { jwt },
|
||||
config: { JWT_ISSUER, JWT_AUDIENCE },
|
||||
services: { Mailer, I18n: { t } },
|
||||
},
|
||||
loaders: { Settings },
|
||||
} = ctx;
|
||||
const { category } = handler;
|
||||
@@ -90,6 +96,16 @@ class NotificationManager {
|
||||
return;
|
||||
}
|
||||
|
||||
// unsubscribeToken is the token used to perform the one-click
|
||||
// unsubscribe.
|
||||
const unsubscribeToken = jwt.sign({
|
||||
jti: uuid(),
|
||||
iss: JWT_ISSUER,
|
||||
aud: JWT_AUDIENCE,
|
||||
sub: UNSUBSCRIBE_SUBJECT,
|
||||
user: userID,
|
||||
});
|
||||
|
||||
// Compose the subject for the email.
|
||||
const subject = t(
|
||||
`talk-plugin-notifications.categories.${category}.subject`,
|
||||
@@ -102,7 +118,7 @@ class NotificationManager {
|
||||
// Send the notification to the user.
|
||||
const task = await Mailer.send({
|
||||
template: 'notification',
|
||||
locals: { body, organizationName },
|
||||
locals: { body, organizationName, unsubscribeToken },
|
||||
subject,
|
||||
user: userID,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
UNSUBSCRIBE_SUBJECT: 'nunsub',
|
||||
};
|
||||
@@ -13,12 +13,12 @@ module.exports = connectors => {
|
||||
// notification template by passing the same name + format for the template
|
||||
// registration.
|
||||
Mailer.templates.register(
|
||||
path.join(__dirname, 'templates', 'notification.html.ejs'),
|
||||
path.join(__dirname, 'emails', 'notification.html.ejs'),
|
||||
'notification',
|
||||
'html'
|
||||
);
|
||||
Mailer.templates.register(
|
||||
path.join(__dirname, 'templates', 'notification.txt.ejs'),
|
||||
path.join(__dirname, 'emails', 'notification.txt.ejs'),
|
||||
'notification',
|
||||
'txt'
|
||||
);
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<p><%= linkify(body, {nl2br: true}) %></p>
|
||||
<p><%= t('talk-plugin-notifications.templates.footer', organizationName) %></p>
|
||||
<p><a href="<%= BASE_URL %>account/unsubscribe-notifications#<%= unsubscribeToken %>" target="_blank"><%= t('talk-plugin-notifications.templates.links.unsubscribe') %></a></p>
|
||||
+1
-1
@@ -4,4 +4,4 @@
|
||||
|
||||
<%= t('talk-plugin-notifications.templates.links.unsubscribe') %>
|
||||
|
||||
<%= BASE_URL %>account/unsubscribe-notifications
|
||||
<%= BASE_URL %>account/unsubscribe-notifications#<%= unsubscribeToken %>
|
||||
@@ -1,6 +1,95 @@
|
||||
const path = require('path');
|
||||
const { UNSUBSCRIBE_SUBJECT } = require('./config');
|
||||
const { get, isEmpty, reduce } = require('lodash');
|
||||
|
||||
module.exports = router => {
|
||||
router.get('/account/unsubscribe-notifications', async (req, res) => {
|
||||
// TODO: implement
|
||||
res.json({ ok: true });
|
||||
router.get('/account/unsubscribe-notifications', (req, res) => {
|
||||
res.render(path.join(__dirname, 'views/unsubscribe-notifications'));
|
||||
});
|
||||
|
||||
/**
|
||||
* Verifies that the token is valid.
|
||||
*/
|
||||
const verifyToken = (req, res, next) => {
|
||||
const {
|
||||
connectors: { secrets: { jwt }, config: { JWT_ISSUER, JWT_AUDIENCE } },
|
||||
} = req.context;
|
||||
const { token: tokenString = '' } = req.body;
|
||||
if (!tokenString) {
|
||||
return res.status(400).end();
|
||||
}
|
||||
|
||||
jwt.verify(
|
||||
tokenString,
|
||||
{
|
||||
issuer: JWT_ISSUER,
|
||||
subject: UNSUBSCRIBE_SUBJECT,
|
||||
audience: JWT_AUDIENCE,
|
||||
},
|
||||
(err, token) => {
|
||||
if (err) {
|
||||
return res.status(400).end();
|
||||
}
|
||||
|
||||
req.token = token;
|
||||
next();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Verifies that a token is valid.
|
||||
router.post(
|
||||
'/api/v1/account/unsubscribe-notifications/verify',
|
||||
verifyToken,
|
||||
(req, res) => {
|
||||
res.status(204).end();
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/api/v1/account/unsubscribe-notifications',
|
||||
verifyToken,
|
||||
async (req, res, next) => {
|
||||
const { connectors: { models: { User } } } = req.context;
|
||||
const { user: userID } = req.token;
|
||||
|
||||
try {
|
||||
const user = await User.findOne({ id: userID });
|
||||
if (!user) {
|
||||
return res.status(400).end();
|
||||
}
|
||||
|
||||
// Get the notification settings.
|
||||
const settings = get(user, 'metadata.notifications.settings', {});
|
||||
|
||||
// If they have no notification settings set to true, then we're done.
|
||||
if (isEmpty(settings)) {
|
||||
return res.status(204).end();
|
||||
}
|
||||
|
||||
const update = reduce(
|
||||
settings,
|
||||
(updates, value, key) => {
|
||||
if (value) {
|
||||
updates[`metadata.notifications.settings.${key}`] = false;
|
||||
}
|
||||
|
||||
return updates;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
if (isEmpty(update)) {
|
||||
return res.status(204).end();
|
||||
}
|
||||
|
||||
// Save the user.
|
||||
await User.update({ id: userID }, { $set: update });
|
||||
|
||||
res.status(204).end();
|
||||
} catch (err) {
|
||||
res.status(400).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<p><%= linkify(body, {nl2br: true}) %></p>
|
||||
<p><%= t('talk-plugin-notifications.templates.footer', organizationName) %></p>
|
||||
<p><a href="<%= BASE_URL %>account/unsubscribe-notifications" target="_blank"><%= t('talk-plugin-notifications.templates.links.unsubscribe') %></a></p>
|
||||
@@ -3,4 +3,10 @@ en:
|
||||
templates:
|
||||
footer: "You received this notification because you are a commenter on {0} and you opted in to receive notifications."
|
||||
links:
|
||||
unsubscribe: "Unsubscribe from comment notifications"
|
||||
unsubscribe: "Unsubscribe from comment notifications"
|
||||
unsubscribe_page:
|
||||
unsubscribe: "Unsubscribe from comment notifications"
|
||||
click_to_confirm: "Click below to confirm that you would like to unsubscribe from all notifications"
|
||||
confirm: "Confirm"
|
||||
are_unsubscribed: "You are now unsubscribed from all notifications."
|
||||
token_invalid: "Unsubscribe link is invalid, click the link from a more recent email or visit a comment stream and login to change your notification preferences"
|
||||
@@ -0,0 +1,64 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
|
||||
<title><%= t('talk-plugin-notifications.unsubscribe_page.unsubscribe') %></title>
|
||||
<link rel="stylesheet" href="https://code.getmdl.io/1.2.1/material.indigo-pink.min.css">
|
||||
<link rel="stylesheet" href="<%= BASE_PATH %>public/css/admin.css">
|
||||
<%- include(root + '/partials/head') %>
|
||||
</head>
|
||||
<body class="confirm-email-page">
|
||||
<div id="root">
|
||||
<div class="error-console container"><%= t('talk-plugin-notifications.unsubscribe_page.token_invalid') %></div>
|
||||
<div id="success" style="display:none;" class="legend container"><%= t('talk-plugin-notifications.unsubscribe_page.are_unsubscribed') %></div>
|
||||
<form id="unsubscribe-form" class="container">
|
||||
<legend class="legend"><%= t('talk-plugin-notifications.unsubscribe_page.click_to_confirm') %></legend>
|
||||
<button type="submit"><%= t('talk-plugin-notifications.unsubscribe_page.confirm') %></button>
|
||||
</form>
|
||||
</div>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
var submitting = false;
|
||||
var payload = JSON.stringify({token: location.hash.replace('#', '')});
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (submitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
submitting = true;
|
||||
$('.error-console').removeClass('active');
|
||||
|
||||
$.ajax({
|
||||
url: '<%= BASE_PATH %>api/v1/account/unsubscribe-notifications',
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
data: payload,
|
||||
}).then(function (success) {
|
||||
$('#unsubscribe-form').fadeOut(function () {
|
||||
$('#success').fadeIn();
|
||||
});
|
||||
}).catch(function () {
|
||||
submitting = false;
|
||||
$('.error-console').addClass('active');
|
||||
});
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '<%= BASE_PATH %>api/v1/account/unsubscribe-notifications/verify',
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
data: payload,
|
||||
})
|
||||
.then(function () {
|
||||
$('#unsubscribe-form').fadeIn().on('submit', handleSubmit);
|
||||
})
|
||||
.catch(function () {
|
||||
$('.error-console').addClass('active');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,9 +1,17 @@
|
||||
const express = require('express');
|
||||
const debug = require('debug')('talk:routes:plugins');
|
||||
const plugins = require('../services/plugins');
|
||||
const staticTemplate = require('../middleware/staticTemplate');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Routes mounted from plugins won't have access to our internal partials
|
||||
// directory, so we should make that available.
|
||||
router.use(staticTemplate, (req, res, next) => {
|
||||
res.locals.root = res.app.get('views');
|
||||
next();
|
||||
});
|
||||
|
||||
// Inject server route plugins.
|
||||
plugins.get('server', 'router').forEach(plugin => {
|
||||
debug(`added plugin '${plugin.plugin.name}'`);
|
||||
|
||||
+6
-3
@@ -101,9 +101,12 @@ class Secret {
|
||||
jwt.verify(
|
||||
token,
|
||||
this.verifiyingKey,
|
||||
Object.assign({}, options, {
|
||||
algorithms: [this.algorithm],
|
||||
}),
|
||||
omitBy(
|
||||
merge({}, options, {
|
||||
algorithms: [this.algorithm],
|
||||
}),
|
||||
isUndefined
|
||||
),
|
||||
callback
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user