diff --git a/plugins/coral-plugin-viewing-options/client/index.js b/plugins/coral-plugin-viewing-options/client/index.js
index 4e9001c1b..464a38d7d 100644
--- a/plugins/coral-plugin-viewing-options/client/index.js
+++ b/plugins/coral-plugin-viewing-options/client/index.js
@@ -1,9 +1,11 @@
import ViewingOptions from './containers/ViewingOptions';
import reducer from './reducer';
+import translations from './translations.yml';
export default {
reducer,
slots: {
streamFilter: [ViewingOptions]
- }
+ },
+ translations
};
diff --git a/plugins/coral-plugin-viewing-options/client/translations.yml b/plugins/coral-plugin-viewing-options/client/translations.yml
new file mode 100644
index 000000000..6ab31fa1b
--- /dev/null
+++ b/plugins/coral-plugin-viewing-options/client/translations.yml
@@ -0,0 +1,4 @@
+en:
+ viewing_options: "Viewing Options"
+es:
+ viewing_options: "Opciones de visualización"
\ No newline at end of file
diff --git a/plugins/talk-plugin-featured-comments/client/components/Tag.css b/plugins/talk-plugin-featured-comments/client/components/Tag.css
index 88a019b5d..7ade3d968 100644
--- a/plugins/talk-plugin-featured-comments/client/components/Tag.css
+++ b/plugins/talk-plugin-featured-comments/client/components/Tag.css
@@ -24,6 +24,15 @@
cursor: pointer;
}
+.noSelect {
+ -ms-user-select:none;
+ -moz-user-select: none;
+ -webkit-user-select: none;
+ -webkit-touch-callout:none;
+ user-select: none;
+ -webkit-tap-highlight-color:rgba(0,0,0,0);
+}
+
.tooltip {
top: 36px;
left: auto;
diff --git a/plugins/talk-plugin-featured-comments/client/components/Tag.js b/plugins/talk-plugin-featured-comments/client/components/Tag.js
index b7ee8cd6b..ee2d13b19 100644
--- a/plugins/talk-plugin-featured-comments/client/components/Tag.js
+++ b/plugins/talk-plugin-featured-comments/client/components/Tag.js
@@ -15,13 +15,15 @@ export default class Tag extends React.Component {
}
- showTooltip = () => {
+ showTooltip = e => {
+ e.preventDefault();
this.setState({
tooltip: true
});
}
- hideTooltip = () => {
+ hideTooltip = (e) => {
+ e.preventDefault();
this.setState({
tooltip: false
});
@@ -30,10 +32,13 @@ export default class Tag extends React.Component {
render() {
const {tooltip} = this.state;
return(
-
+
{
isTagged(this.props.comment.tags, 'FEATURED') ? (
-
+
{t('talk-plugin-featured-comments.featured')}
) : null
diff --git a/plugins/talk-plugin-featured-comments/client/containers/Comment.js b/plugins/talk-plugin-featured-comments/client/containers/Comment.js
index 2b3de19b5..7080627e6 100644
--- a/plugins/talk-plugin-featured-comments/client/containers/Comment.js
+++ b/plugins/talk-plugin-featured-comments/client/containers/Comment.js
@@ -32,19 +32,6 @@ export default withFragments({
}
}
- ##
- # TODO: Remove this when we have the IntrospectionFragmentMatcher.
- # Currently without this loading more featured comments
- # brings apollo into an inconsistent state.
- action_summaries {
- __typename
- count
- current_user {
- id
- }
- }
- ##
-
user {
id
username
diff --git a/routes/api/account/index.js b/routes/api/account/index.js
index 4c49c00a5..99cb9a468 100644
--- a/routes/api/account/index.js
+++ b/routes/api/account/index.js
@@ -19,7 +19,7 @@ router.get('/', authorization.needed(), (req, res, next) => {
// POST /email/confirm takes the password confirmation token available as a
// payload parameter and if it verifies, it updates the confirmed_at date on the
// local profile.
-router.post('/email/verify', (req, res, next) => {
+router.post('/email/verify', async (req, res, next) => {
const {
token
@@ -29,57 +29,47 @@ router.post('/email/verify', (req, res, next) => {
return next(errors.ErrMissingToken);
}
- UsersService
- .verifyEmailConfirmation(token)
- .then(({referer}) => {
- res.json({redirectUri: referer});
- })
- .catch((err) => {
- next(err);
- });
+ try {
+ let {referer} = await UsersService.verifyEmailConfirmation(token);
+ res.json({redirectUri: referer});
+ } catch (e) {
+ return next(e);
+ }
});
/**
* this endpoint takes an email (username) and checks if it belongs to a User account
* if it does, create a JWT and send an email
*/
-router.post('/password/reset', (req, res, next) => {
+router.post('/password/reset', async (req, res, next) => {
const {email, loc} = req.body;
if (!email) {
- return next('you must submit an email when requesting a password.');
+ return next(errors.ErrMissingEmail);
}
- UsersService
- .createPasswordResetToken(email, loc)
- .then((token) => {
-
- // Check to see if the token isn't defined.
- if (!token) {
-
- // As it isn't, don't send any emails!
- return;
- }
-
- return mailer.sendSimple({
- template: 'password-reset', // needed to know which template to render!
- locals: { // specifies the template locals.
- token,
- rootURL: ROOT_URL
- },
- subject: 'Password Reset',
- to: email
- });
- })
- .then(() => {
-
- // we want to send a 204 regardless of the user being found in the db
- // if we fail on missing emails, it would reveal if people are registered or not.
+ try {
+ let token = await UsersService.createPasswordResetToken(email, loc);
+ if (!token) {
res.status(204).end();
- })
- .catch((err) => {
- next(err);
+ return;
+ }
+
+ // Send the password reset email.
+ await mailer.sendSimple({
+ template: 'password-reset', // needed to know which template to render!
+ locals: { // specifies the template locals.
+ token,
+ rootURL: ROOT_URL
+ },
+ subject: 'Password Reset',
+ to: email
});
+
+ res.status(204).end();
+ } catch (e) {
+ return next(e);
+ }
});
/**
@@ -87,7 +77,7 @@ router.post('/password/reset', (req, res, next) => {
* 1) the token that was in the url of the email link {String}
* 2) the new password {String}
*/
-router.put('/password/reset', (req, res, next) => {
+router.put('/password/reset', async (req, res, next) => {
const {
token,
@@ -102,28 +92,26 @@ router.put('/password/reset', (req, res, next) => {
return next(errors.ErrPasswordTooShort);
}
- UsersService
- .verifyPasswordResetToken(token)
- .then(([user, loc]) => {
- return Promise.all([UsersService.changePassword(user.id, password), loc]);
- })
- .then(([ , loc]) => {
- res.json({redirect: loc});
- })
- .catch(() => {
- next(authorization.ErrNotAuthorized);
- });
+ try {
+ let [user, loc] = await UsersService.verifyPasswordResetToken(token);
+
+ // Change the users' password.
+ await UsersService.changePassword(user.id, password);
+
+ res.json({redirect: loc});
+ } catch (e) {
+ console.error(e);
+ return next(errors.ErrNotAuthorized);
+ }
});
-router.put('/username', authorization.needed(), (req, res, next) => {
- UsersService
- .editName(req.user.id, req.body.username)
- .then(() => {
- res.status(204).end();
- })
- .catch((err) => {
- next(err);
- });
+router.put('/username', authorization.needed(), async (req, res, next) => {
+ try {
+ await UsersService.editName(req.user.id, req.body.username);
+ res.status(204).end();
+ } catch (e) {
+ return next(e);
+ }
});
module.exports = router;
diff --git a/routes/api/assets/index.js b/routes/api/assets/index.js
index f02b49f7e..a306b36b4 100644
--- a/routes/api/assets/index.js
+++ b/routes/api/assets/index.js
@@ -2,12 +2,39 @@ const express = require('express');
const router = express.Router();
const scraper = require('../../../services/scraper');
+const errors = require('../../../errors');
const AssetsService = require('../../../services/assets');
const AssetModel = require('../../../models/asset');
+const FilterOpenAssets = (query, filter) => {
+ switch(filter) {
+ case 'open':
+ return query.merge({
+ $or: [
+ {
+ closedAt: null
+ },
+ {
+ closedAt: {
+ $gt: Date.now()
+ }
+ }
+ ]
+ });
+ case 'closed':
+ return query.merge({
+ closedAt: {
+ $lt: Date.now()
+ }
+ });
+ default:
+ return query;
+ }
+};
+
// List assets.
-router.get('/', (req, res, next) => {
+router.get('/', async (req, res, next) => {
const {
limit = 20,
@@ -18,138 +45,97 @@ router.get('/', (req, res, next) => {
search = ''
} = req.query;
- const FilterOpenAssets = (query, filter) => {
- switch(filter) {
- case 'open':
- return query.merge({
- $or: [
- {
- closedAt: null
- },
- {
- closedAt: {
- $gt: Date.now()
- }
- }
- ]
- });
- case 'closed':
- return query.merge({
- closedAt: {
- $lt: Date.now()
- }
- });
- default:
- return query;
- }
- };
+ try {
- // Find all the assets.
- Promise.all([
+ // Find all the assets.
+ let [result, count] = await Promise.all([
- // Find the actuall assets.
- FilterOpenAssets(AssetsService.search({value: search}), filter)
- .sort({[field]: (sort === 'asc') ? 1 : -1})
- .skip(parseInt(skip))
- .limit(parseInt(limit)),
+ // Find the actuall assets.
+ FilterOpenAssets(AssetsService.search({value: search}), filter)
+ .sort({[field]: (sort === 'asc') ? 1 : -1})
+ .skip(parseInt(skip))
+ .limit(parseInt(limit)),
- // Get the count of actual assets.
- FilterOpenAssets(AssetsService.search({value: search}), filter)
- .count()
- ])
- .then(([result, count]) => {
+ // Get the count of actual assets.
+ FilterOpenAssets(AssetsService.search({value: search}), filter)
+ .count()
+ ]);
// Send back the asset data.
res.json({
result,
count
});
- })
- .catch((err) => {
- next(err);
- });
+ } catch (e) {
+ return next(e);
+ }
});
// Get an asset by id.
-router.get('/:asset_id', (req, res, next) => {
+router.get('/:asset_id', async (req, res, next) => {
+ try {
- // Send back the asset.
- AssetsService
- .findById(req.params.asset_id)
- .then((asset) => {
- if (!asset) {
- res.status(404).end();
- return;
- }
+ // Send back the asset.
+ let asset = await AssetsService.findById(req.params.asset_id);
+ if (!asset) {
+ return next(errors.ErrNotFound);
+ }
- res.json(asset);
- })
- .catch((err) => {
- next(err);
- });
+ return res.json(asset);
+ } catch (e) {
+ return next(e);
+ }
});
// Adds the asset id to the queue to be scraped.
-router.post('/:asset_id/scrape', (req, res, next) => {
+router.post('/:asset_id/scrape', async (req, res, next) => {
+ try {
- // Create a new asset scrape job.
- AssetsService
- .findById(req.params.asset_id)
- .then((asset) => {
- if (!asset) {
- res.status(404).end();
- return;
- }
+ // Send back the asset.
+ let asset = await AssetsService.findById(req.params.asset_id);
+ if (!asset) {
+ return next(errors.ErrNotFound);
+ }
- return scraper
- .create(asset)
- .then((job) => {
+ let job = await scraper.create(asset);
- // Send the job back for monitoring.
- res.status(201).json(job);
- });
- })
- .catch((err) => {
- next(err);
- });
+ // Send the job back for monitoring.
+ res.status(201).json(job);
+ } catch (e) {
+ return next(e);
+ }
});
-router.put('/:asset_id/settings', (req, res, next) => {
-
- // Override the settings for the asset.
- AssetsService
- .overrideSettings(req.params.asset_id, req.body)
- .then(() => {
- res.status(204).end();
- })
- .catch((err) => {
- next(err);
- });
+router.put('/:asset_id/settings', async (req, res, next) => {
+ try {
+ await AssetsService.overrideSettings(req.params.asset_id, req.body);
+ res.status(204).end();
+ } catch (e) {
+ return next(e);
+ }
});
-router.put('/:asset_id/status', (req, res, next) => {
-
- const id = req.params.asset_id;
-
+router.put('/:asset_id/status', async (req, res, next) => {
const {
closedAt,
closedMessage
} = req.body;
- AssetModel
- .update({id}, {
+ try {
+ await AssetModel.update({
+ id: req.params.asset_id
+ }, {
$set: {
closedAt,
closedMessage
}
- })
- .then(() => {
- res.status(204).json();
- })
- .catch((err) => {
- next(err);
});
+
+ res.status(204).json();
+ } catch (e) {
+ return next(e);
+ }
});
module.exports = router;
diff --git a/routes/api/auth/index.js b/routes/api/auth/index.js
index 392b04e9b..791573898 100644
--- a/routes/api/auth/index.js
+++ b/routes/api/auth/index.js
@@ -7,14 +7,11 @@ const router = express.Router();
* This returns the user if they are logged in.
*/
router.get('/', (req, res, next) => {
-
- if (req.user) {
- return next();
+ if (!req.user) {
+ res.status(204).end();
+ return;
}
- res.status(204).end();
-}, (req, res) => {
-
// Send back the user object.
res.json({user: req.user});
});
diff --git a/routes/api/index.js b/routes/api/index.js
index 764612f78..a3bc552f6 100644
--- a/routes/api/index.js
+++ b/routes/api/index.js
@@ -1,6 +1,9 @@
const express = require('express');
const authorization = require('../../middleware/authorization');
const pkg = require('../../package.json');
+const {
+ WEBPACK
+} = require('../../config');
const router = express.Router();
@@ -15,7 +18,12 @@ router.use('/users', require('./users'));
router.use('/account', require('./account'));
router.use('/setup', require('./setup'));
-// Bind the kue handler to the /kue path.
-router.use('/kue', authorization.needed('ADMIN'), require('../../services/kue').kue.app);
+// Enable the kue app only if we aren't in webpack mode.
+if (!WEBPACK) {
+
+ // Bind the kue handler to the /kue path.
+ router.use('/kue', authorization.needed('ADMIN'), require('../../services/kue').kue.app);
+
+}
module.exports = router;
diff --git a/routes/api/settings/index.js b/routes/api/settings/index.js
index 5c4b03568..fb54494d1 100644
--- a/routes/api/settings/index.js
+++ b/routes/api/settings/index.js
@@ -3,25 +3,22 @@ const SettingsService = require('../../../services/settings');
const router = express.Router();
-router.get('/', (req, res, next) => {
- SettingsService
- .retrieve()
- .then((settings) => {
- res.json(settings);
- })
- .catch((err) => {
- next(err);
- });
+router.get('/', async (req, res, next) => {
+ try {
+ let settings = await SettingsService.retrieve();
+ res.json(settings);
+ } catch (e) {
+ return next(e);
+ }
});
-router.put('/', (req, res, next) => {
- SettingsService
- .update(req.body).then(() => {
- res.status(204).end();
- })
- .catch((err) => {
- next(err);
- });
+router.put('/', async (req, res, next) => {
+ try {
+ await SettingsService.update(req.body);
+ res.status(204).end();
+ } catch (e) {
+ return next(e);
+ }
});
module.exports = router;
diff --git a/routes/api/setup/index.js b/routes/api/setup/index.js
index dfddc64d9..efb3c5d45 100644
--- a/routes/api/setup/index.js
+++ b/routes/api/setup/index.js
@@ -4,48 +4,33 @@ const SetupService = require('../../../services/setup');
const router = express.Router();
-router.get('/', (req, res, next) => {
- SetupService
- .isAvailable()
- .then(() => {
- res.json({installed: false});
- })
- .catch(() => {
- res.json({installed: true});
- });
+router.get('/', async (req, res, next) => {
+ try {
+ await SetupService.isAvailable();
+ res.json({installed: false});
+ } catch (e) {
+ res.json({installed: true});
+ }
});
-router.post('/', (req, res, next) => {
-
- SetupService
- .isAvailable()
- .then(() => {
-
- // Allow the request to keep going here.
- next();
- })
- .catch((err) => {
- next(err);
- });
-
-}, (req, res, next) => {
+router.post('/', async (req, res, next) => {
+ try {
+ await SetupService.isAvailable();
+ } catch (e) {
+ return next(e);
+ }
const {
settings,
user: {email, password, username}
} = req.body;
- SetupService
- .setup({settings, user: {email, password, username}})
- .then(() => {
-
- // We're setup!
- res.status(204).end();
- })
- .catch((err) => {
- next(err);
- });
-
+ try {
+ await SetupService.setup({settings, user: {email, password, username}});
+ res.status(204).end();
+ } catch (err) {
+ return next(err);
+ }
});
module.exports = router;
diff --git a/routes/api/users/index.js b/routes/api/users/index.js
index 47ff46853..60c568253 100644
--- a/routes/api/users/index.js
+++ b/routes/api/users/index.js
@@ -2,7 +2,6 @@ const express = require('express');
const router = express.Router();
const UsersService = require('../../../services/users');
const mailer = require('../../../services/mailer');
-const pubsub = require('../../../services/pubsub');
const errors = require('../../../errors');
const authorization = require('../../../middleware/authorization');
const i18n = require('../../../services/i18n');
@@ -11,7 +10,7 @@ const {
} = require('../../../config');
// get a list of users.
-router.get('/', authorization.needed('ADMIN'), (req, res, next) => {
+router.get('/', authorization.needed('ADMIN'), async (req, res, next) => {
const {
value = '',
field = 'created_at',
@@ -20,15 +19,17 @@ router.get('/', authorization.needed('ADMIN'), (req, res, next) => {
limit = 50 // Total Per Page
} = req.query;
- Promise.all([
- UsersService
+ try {
+
+ let [result, count] = await Promise.all([
+ UsersService
.search(value)
.sort({[field]: (asc === 'true') ? 1 : -1})
.skip((page - 1) * limit)
.limit(limit),
- UsersService.count()
- ])
- .then(([result, count]) => {
+ UsersService.count()
+ ]);
+
res.json({
result,
limit: Number(limit),
@@ -36,72 +37,74 @@ router.get('/', authorization.needed('ADMIN'), (req, res, next) => {
page: Number(page),
totalPages: Math.ceil(count / (limit === 0 ? 1 : limit))
});
- })
- .catch(next);
+
+ } catch (e) {
+ next(e);
+ }
+
});
-router.post('/:user_id/role', authorization.needed('ADMIN'), (req, res, next) => {
- UsersService
- .addRoleToUser(req.params.user_id, req.body.role)
- .then(() => {
- res.status(204).end();
- })
- .catch(next);
+router.post('/:user_id/role', authorization.needed('ADMIN'), async (req, res, next) => {
+ try {
+ await UsersService.addRoleToUser(req.params.user_id, req.body.role);
+ res.status(204).end();
+ } catch (e) {
+ next(e);
+ }
});
// update the status of a user
-router.post('/:user_id/status', authorization.needed('ADMIN'), (req, res, next) => {
- UsersService
- .setStatus(req.params.user_id, req.body.status)
- .then((user) => {
+router.post('/:user_id/status', authorization.needed('ADMIN'), async (req, res, next) => {
+ let {status} = req.body;
- // TODO: current updating status behavior is weird.
- if (user) {
- if (user.status === 'BANNED') {
- pubsub.publish('userBanned', user);
- }
- res.status(201).json(user.status);
- } else {
- res.status(500).json();
- }
- })
- .catch(next);
+ try {
+ let user = await UsersService.setStatus(req.params.user_id, status);
+ if (!user) {
+ return next(errors.ErrNotFound);
+ }
+
+ if (user.status === 'BANNED') {
+ req.pubsub.publish('userBanned', user);
+ }
+
+ // TODO: investigate why this is returning a value? Also why is this a POST vs PUT?
+ res.status(201).json(user.status);
+ } catch (e) {
+ next(e);
+ }
});
-router.post('/:user_id/username-enable', authorization.needed('ADMIN'), (req, res, next) => {
- UsersService
- .toggleNameEdit(req.params.user_id, true)
- .then(() => {
- res.status(204).end();
+router.post('/:user_id/username-enable', authorization.needed('ADMIN'), async (req, res, next) => {
+ try {
+ await UsersService.toggleNameEdit(req.params.user_id, true);
+ res.status(204).end();
+ } catch (e) {
+ next(e);
+ }
+});
+
+router.post('/:user_id/email', authorization.needed('ADMIN'), async (req, res, next) => {
+ try {
+ let user = await UsersService.findById(req.params.user_id);
+
+ let localProfile = user.profiles.find((profile) => profile.provider === 'local');
+ if (!localProfile) {
+ return next(errors.ErrMissingEmail);
+ }
+
+ await mailer.sendSimple({
+ template: 'notification', // needed to know which template to render!
+ locals: { // specifies the template locals.
+ body: req.body.body
+ },
+ subject: req.body.subject,
+ to: localProfile.id // This only works if the user has registered via e-mail.
});
-});
-router.post('/:user_id/email', authorization.needed('ADMIN'), (req, res, next) => {
-
- UsersService.findById(req.params.user_id)
- .then((user) => {
- let localProfile = user.profiles.find((profile) => profile.provider === 'local');
-
- if (localProfile) {
- const options =
- {
- template: 'notification', // needed to know which template to render!
- locals: { // specifies the template locals.
- body: req.body.body
- },
- subject: req.body.subject,
- to: localProfile.id // This only works if the user has registered via e-mail.
- // We may want a standard way to access a user's e-mail address in the future
- };
- return mailer.sendSimple(options);
- } else {
- res.json({error: 'User does not have an e-mail address.'});
- }
- })
- .then(() => {
- res.status(204).end();
- })
- .catch(next);
+ res.status(204).end();
+ } catch (e) {
+ next(e);
+ }
});
/**
@@ -110,72 +113,61 @@ router.post('/:user_id/email', authorization.needed('ADMIN'), (req, res, next) =
* @param {String} userID the id for the user to send the email to
* @param {String} email the email for the user to send the email to
*/
-const SendEmailConfirmation = (app, userID, email, referer) => UsersService
- .createEmailConfirmToken(userID, email, referer)
- .then((token) => {
- return mailer.sendSimple({
- template: 'email-confirm', // needed to know which template to render!
- locals: { // specifies the template locals.
- token,
- rootURL: ROOT_URL,
- email
- },
- subject: i18n.t('email.confirm.subject'),
- to: email
- });
+const SendEmailConfirmation = async (app, userID, email, referer) => {
+ let token = await UsersService.createEmailConfirmToken(userID, email, referer);
+
+ return mailer.sendSimple({
+ template: 'email-confirm', // needed to know which template to render!
+ locals: { // specifies the template locals.
+ token,
+ rootURL: ROOT_URL,
+ email
+ },
+ subject: i18n.t('email.confirm.subject'),
+ to: email
});
+};
// create a local user.
-router.post('/', (req, res, next) => {
+router.post('/', async (req, res, next) => {
const {email, password, username} = req.body;
const redirectUri = req.header('X-Pym-Url') || req.header('Referer');
- UsersService
- .createLocalUser(email, password, username)
- .then((user) => {
+ try {
+ let user = await UsersService.createLocalUser(email, password, username);
// Send an email confirmation. The Front end will know about the
// requireEmailConfirmation as it's included in the settings get endpoint.
- return SendEmailConfirmation(req.app, user.id, email, redirectUri)
- .then(() => {
+ await SendEmailConfirmation(req.app, user.id, email, redirectUri);
- // Then send back the user.
- res.status(201).json(user);
- });
- })
- .catch((err) => {
- next(err);
- });
+ res.status(201).json(user);
+ } catch (e) {
+ return next(e);
+ }
});
-router.post('/:user_id/actions', authorization.needed(), (req, res, next) => {
+router.post('/:user_id/actions', authorization.needed(), async (req, res, next) => {
const {
action_type,
metadata
} = req.body;
- UsersService
- .addAction(req.params.user_id, req.user.id, action_type, metadata)
- .then((action) => {
+ try {
+ let action = await UsersService.addAction(req.params.user_id, req.user.id, action_type, metadata);
- // Set the user status to "pending" for review by moderators
- if (action_type === 'FLAG') {
- return UsersService.setStatus(req.params.user_id, 'PENDING')
- .then(() => action);
- } else {
- return action;
- }
- })
- .then((action) => {
- res.status(201).json(action);
- })
- .catch((err) => {
- next(err);
- });
+ // Set the user status to "pending" for review by moderators
+ if (action_type === 'FLAG') {
+ await UsersService.setStatus(req.params.user_id, 'PENDING');
+ }
+
+ res.status(201).json(action);
+ } catch (e) {
+ return next(e);
+ }
});
// trigger an email confirmation re-send by a new user
-router.post('/resend-verify', (req, res, next) => {
+router.post('/resend-verify', async (req, res, next) => {
const {email} = req.body;
const redirectUri = req.header('X-Pym-Url') || req.header('Referer');
@@ -183,48 +175,45 @@ router.post('/resend-verify', (req, res, next) => {
return next(errors.ErrMissingEmail);
}
- // find user by email.
- // if the local profile is verified, return an error code?
- // send a 204 after the email is re-sent
- SendEmailConfirmation(req.app, null, email, redirectUri)
- .then(() => {
- res.status(204).end();
- })
- .catch(next);
+ try {
+
+ // find user by email.
+ // if the local profile is verified, return an error code?
+ // send a 204 after the email is re-sent
+ await SendEmailConfirmation(req.app, null, email, redirectUri);
+
+ res.status(204).end();
+ } catch (e) {
+ return next(e);
+ }
});
// trigger an email confirmation re-send from the admin panel
-router.post('/:user_id/email/confirm', authorization.needed('ADMIN'), (req, res, next) => {
+router.post('/:user_id/email/confirm', authorization.needed('ADMIN'), async (req, res, next) => {
const {
user_id
} = req.params;
- UsersService
- .findById(user_id)
- .then((user) => {
- if (!user) {
- res.status(404).end();
- return;
- }
+ try {
- // Find the first local profile.
- let localProfile = user.profiles.find((profile) => profile.provider === 'local');
+ let user = await UsersService.findById(user_id);
+ if (!user) {
+ return next(errors.ErrNotFound);
+ }
- // If there was no local profile for the user, error out.
- if (!localProfile) {
- res.status(404).end();
- return;
- }
+ // Find the first local profile.
+ let localProfile = user.profiles.find((profile) => profile.provider === 'local');
+ if (!localProfile) {
+ return next(errors.ErrMissingEmail);
+ }
- // Send the email to the first local profile that was found.
- return SendEmailConfirmation(req.app, user.id, localProfile.id)
- .then(() => {
- res.status(204).end();
- });
- })
- .catch((err) => {
- next(err);
- });
+ // Send the email to the first local profile that was found.
+ await SendEmailConfirmation(req.app, user.id, localProfile.id);
+
+ res.status(204).end();
+ } catch (e) {
+ return next(e);
+ }
});
module.exports = router;
diff --git a/scripts/generateIntrospectionResult.js b/scripts/generateIntrospectionResult.js
new file mode 100755
index 000000000..a42dcd955
--- /dev/null
+++ b/scripts/generateIntrospectionResult.js
@@ -0,0 +1,24 @@
+#! /usr/bin/env node
+
+const path = require('path');
+const introspectionFilename = path.resolve(__dirname, '..', 'client', 'coral-framework', 'graphql', 'introspection.json');
+
+const fs = require('fs');
+const {graphql, introspectionQuery} = require('graphql');
+const schema = require('../graph/schema');
+
+graphql(schema, introspectionQuery)
+ .then(({data}) => {
+
+ // Serialize the introspection result as JSON.
+ const introspectionResult = JSON.stringify(data, null, 2);
+
+ // Write the introspection result to the filesystem.
+ fs.writeFileSync(introspectionFilename, introspectionResult, 'utf8');
+
+ console.log(`Outputted result of introspectionQuery to ${introspectionFilename}`);
+ })
+ .catch((err) => {
+ console.error(err);
+ process.exit(1);
+ });
diff --git a/services/cache.js b/services/cache.js
index 25642e1d0..526178a99 100644
--- a/services/cache.js
+++ b/services/cache.js
@@ -2,9 +2,7 @@ const redis = require('./redis');
const debug = require('debug')('talk:services:cache');
const crypto = require('crypto');
-const cache = module.exports = {
- client: redis.createClient()
-};
+const cache = module.exports = {};
/**
* This collects a key that may either be an array or a string and creates a
@@ -70,9 +68,6 @@ if redis.call('GET', KEYS[1]) ~= false then
end
`;
-// Stores the SHA1 hash of INCR_SCRIPT, used for executing via EVALSHA.
-let INCR_SCRIPT_HASH;
-
// This is designed to decrement a key and add an expiry iff the key already
// exists.
const DECR_SCRIPT = `
@@ -82,9 +77,6 @@ if redis.call('GET', KEYS[1]) ~= false then
end
`;
-// Stores the SHA1 hash of DECR_SCRIPT, used for executing via EVALSHA.
-let DECR_SCRIPT_HASH;
-
// Load the script into redis and track the script hash that we will use to exec
// increments on.
const loadScript = (name, script) => new Promise((resolve, reject) => {
@@ -121,18 +113,24 @@ const loadScript = (name, script) => new Promise((resolve, reject) => {
});
});
-// Load the INCR_SCRIPT and DECR_SCRIPT into Redis.
-Promise.all([
- loadScript('INCR_SCRIPT', INCR_SCRIPT),
- loadScript('DECR_SCRIPT', DECR_SCRIPT)
-])
-.then(([incrScriptHash, decrScriptHash]) => {
- INCR_SCRIPT_HASH = incrScriptHash;
- DECR_SCRIPT_HASH = decrScriptHash;
-})
-.catch((err) => {
- throw err;
-});
+/**
+ * Init sets up the scripts used in Redis with the incr/decr commands.
+ */
+cache.init = async () => {
+
+ // Create the redis instance.
+ cache.client = redis.createClient();
+
+ // Load the INCR_SCRIPT and DECR_SCRIPT into Redis.
+ let [incrScriptHash, decrScriptHash] = await Promise.all([
+ loadScript('INCR_SCRIPT', INCR_SCRIPT),
+ loadScript('DECR_SCRIPT', DECR_SCRIPT)
+ ]);
+
+ // Set the globally scoped cache hashes.
+ cache.INCR_SCRIPT_HASH = incrScriptHash;
+ cache.DECR_SCRIPT_HASH = decrScriptHash;
+};
/**
* This will increment a key in redis and update the expiry iff it already
@@ -140,7 +138,7 @@ Promise.all([
*/
cache.incr = (key, expiry, kf = keyfunc) => new Promise((resolve, reject) => {
cache.client
- .evalsha(INCR_SCRIPT_HASH, 1, kf(key), expiry, (err) => {
+ .evalsha(cache.INCR_SCRIPT_HASH, 1, kf(key), expiry, (err) => {
if (err) {
return reject(err);
}
@@ -155,7 +153,7 @@ cache.incr = (key, expiry, kf = keyfunc) => new Promise((resolve, reject) => {
*/
cache.decr = (key, expiry, kf = keyfunc) => new Promise((resolve, reject) => {
cache.client
- .evalsha(DECR_SCRIPT_HASH, 1, kf(key), expiry, (err) => {
+ .evalsha(cache.DECR_SCRIPT_HASH, 1, kf(key), expiry, (err) => {
if (err) {
return reject(err);
}
@@ -174,7 +172,7 @@ cache.incrMany = (keys, expiry, kf = keyfunc) => {
keys.forEach((key) => {
// Queue up the evalsha command.
- multi.evalsha(INCR_SCRIPT_HASH, 1, kf(key), expiry);
+ multi.evalsha(cache.INCR_SCRIPT_HASH, 1, kf(key), expiry);
});
return new Promise((resolve, reject) => {
@@ -198,7 +196,7 @@ cache.decrMany = (keys, expiry, kf = keyfunc) => {
keys.forEach((key) => {
// Queue up the evalsha command.
- multi.evalsha(DECR_SCRIPT_HASH, 1, kf(key), expiry);
+ multi.evalsha(cache.DECR_SCRIPT_HASH, 1, kf(key), expiry);
});
return new Promise((resolve, reject) => {
diff --git a/services/kue.js b/services/kue.js
index ac299a3d5..1b3fb605a 100644
--- a/services/kue.js
+++ b/services/kue.js
@@ -8,18 +8,24 @@ const kue = module.exports.kue = require('kue');
// Note that unlike what the name createQueue suggests, it currently returns a
// singleton Queue instance. So you can configure and use only a single Queue
// object within your node.js process.
-const Queue = module.exports.queue = kue.createQueue({
- redis: {
- createClientFactory: () => redis.createClient()
- }
-});
+let Queue = module.exports.queue = null;
class Task {
constructor({name, attempts = 3, delay = 1000}) {
+ debug(`Created new Task[${name}]`);
+
this.name = name;
this.attempts = attempts;
this.delay = delay;
+
+ if (!Queue) {
+ module.exports.queue = Queue = kue.createQueue({
+ redis: {
+ createClientFactory: redis.createClientFactory()
+ }
+ });
+ }
}
/**
@@ -132,3 +138,19 @@ if (process.env.NODE_ENV === 'test') {
} else {
module.exports.Task = Task;
}
+
+module.exports.createTaskFactory = () => {
+ let taskInstance = null;
+
+ return (options) => {
+ if (taskInstance) {
+ return taskInstance;
+ }
+
+ options = Object.assign({}, options);
+
+ taskInstance = new module.exports.Task(options);
+
+ return taskInstance;
+ };
+};
diff --git a/services/mailer.js b/services/mailer.js
index 27672a35c..7212979df 100644
--- a/services/mailer.js
+++ b/services/mailer.js
@@ -1,6 +1,7 @@
const debug = require('debug')('talk:services:mailer');
const nodemailer = require('nodemailer');
const kue = require('./kue');
+const taskFactory = kue.createTaskFactory();
const path = require('path');
const fs = require('fs');
const _ = require('lodash');
@@ -75,9 +76,11 @@ const mailer = module.exports = {
/**
* Create the new Task kue.
*/
- task: new kue.Task({
- name: 'mailer'
- }),
+ get task() {
+ return taskFactory({
+ name: 'mailer'
+ });
+ },
sendSimple({template, locals, to, subject}) {
diff --git a/services/mongoose.js b/services/mongoose.js
index c697e38ef..2b05aaeaa 100644
--- a/services/mongoose.js
+++ b/services/mongoose.js
@@ -4,7 +4,8 @@ const enabled = require('debug').enabled;
const queryDebuger = require('debug')('talk:db:query');
const {
- MONGO_URL
+ MONGO_URL,
+ WEBPACK
} = require('../config');
// Loading the formatter from Mongoose:
@@ -40,14 +41,27 @@ if (enabled('talk:db')) {
mongoose.set('debug', debugQuery);
}
-// Connect to the Mongo instance.
-mongoose.connect(MONGO_URL, (err) => {
- if (err) {
- throw err;
- }
+if (WEBPACK) {
- debug('connection established');
-});
+ console.warn('Not connecting to mongodb during webpack build');
+
+ // @wyattjoh: We didn't call connect, but because we include mongoose, it will hold the socket ready,
+ // preventing node from exiting. Calling disconnect here just ensures that the application
+ // can quit correctly.
+ mongoose.disconnect();
+
+} else {
+
+ // Connect to the Mongo instance.
+ mongoose.connect(MONGO_URL, (err) => {
+ if (err) {
+ throw err;
+ }
+
+ debug('connection established');
+ });
+
+}
module.exports = mongoose;
diff --git a/services/passport.js b/services/passport.js
index 8396cc581..4ff6198f0 100644
--- a/services/passport.js
+++ b/services/passport.js
@@ -9,18 +9,19 @@ const LocalStrategy = require('passport-local').Strategy;
const errors = require('../errors');
const uuid = require('uuid');
const debug = require('debug')('talk:services:passport');
-const {createClient} = require('./redis');
const bowser = require('bowser');
const ms = require('ms');
// Create a redis client to use for authentication.
-const client = createClient();
+const {createClientFactory} = require('./redis');
+const client = createClientFactory();
const {
JWT_SECRET,
JWT_ISSUER,
JWT_EXPIRY,
JWT_AUDIENCE,
+ JWT_ALG,
RECAPTCHA_SECRET,
RECAPTCHA_ENABLED
} = require('../config');
@@ -148,7 +149,7 @@ const HandleLogout = (req, res, next) => {
const now = new Date();
const expiry = (jwt.exp - now.getTime() / 1000).toFixed(0);
- client.set(`jtir[${jwt.jti}]`, now.toISOString(), 'EX', expiry, (err) => {
+ client().set(`jtir[${jwt.jti}]`, now.toISOString(), 'EX', expiry, (err) => {
if (err) {
return next(err);
}
@@ -159,7 +160,7 @@ const HandleLogout = (req, res, next) => {
};
const checkGeneralTokenBlacklist = (jwt) => new Promise((resolve, reject) => {
- client.get(`jtir[${jwt.jti}]`, (err, expiry) => {
+ client().get(`jtir[${jwt.jti}]`, (err, expiry) => {
if (err) {
return reject(err);
}
@@ -219,6 +220,7 @@ passport.use(new JwtStrategy({
// Prepare the extractor from the header.
jwtFromRequest: ExtractJwt.fromExtractors([
cookieExtractor,
+ ExtractJwt.fromUrlQueryParameter('access_token'),
ExtractJwt.fromAuthHeaderWithScheme('Bearer')
]),
@@ -233,7 +235,7 @@ passport.use(new JwtStrategy({
audience: JWT_AUDIENCE,
// Enable only the HS256 algorithm.
- algorithms: ['HS256'],
+ algorithms: [JWT_ALG],
// Pass the request object back to the callback so we can attach the JWT to
// it.
diff --git a/services/pubsub.js b/services/pubsub.js
index a42ffb314..56e0f3e4d 100644
--- a/services/pubsub.js
+++ b/services/pubsub.js
@@ -2,4 +2,15 @@ const {RedisPubSub} = require('graphql-redis-subscriptions');
const {connectionOptions} = require('./redis');
-module.exports = new RedisPubSub({connection: connectionOptions});
+let pubsubInstance = null;
+module.exports = {
+ createClient: () => {
+ if (pubsubInstance) {
+ return pubsubInstance;
+ }
+
+ pubsubInstance = new RedisPubSub({connection: connectionOptions});
+
+ return pubsubInstance;
+ }
+};
diff --git a/services/redis.js b/services/redis.js
index c6506eb3c..1983249f1 100644
--- a/services/redis.js
+++ b/services/redis.js
@@ -29,21 +29,36 @@ const connectionOptions = {
}
};
+const createClient = () => {
+ let client = redis.createClient(connectionOptions);
+
+ client.ping((err) => {
+ if (err) {
+ console.error('Can\'t ping the redis server!');
+
+ throw err;
+ }
+
+ debug('connection established');
+ });
+
+ return client;
+};
+
module.exports = {
connectionOptions,
- createClient() {
- let client = redis.createClient(connectionOptions);
+ createClient,
+ createClientFactory: () => {
+ let client = null;
- client.ping((err) => {
- if (err) {
- console.error('Can\'t ping the redis server!');
-
- throw err;
+ return () => {
+ if (client) {
+ return client;
}
- debug('connection established');
- });
+ client = createClient();
- return client;
+ return client;
+ };
}
};
diff --git a/services/scraper.js b/services/scraper.js
index aa56bf61b..40aac29ad 100644
--- a/services/scraper.js
+++ b/services/scraper.js
@@ -1,4 +1,5 @@
const kue = require('./kue');
+const taskFactory = kue.createTaskFactory();
const debug = require('debug')('talk:services:scraper');
const AssetModel = require('../models/asset');
const AssetsService = require('./assets');
@@ -12,11 +13,13 @@ const metascraper = require('metascraper');
const scraper = {
/**
- * Create the new Task kue.
+ * Create the new Task kue singleton.
*/
- task: new kue.Task({
- name: 'scraper'
- }),
+ get task() {
+ return taskFactory({
+ name: 'scraper'
+ });
+ },
/**
* Creates a new scraper job and scrapes the url when it gets processed.
diff --git a/services/users.js b/services/users.js
index 3bdc5148b..12da5ecce 100644
--- a/services/users.js
+++ b/services/users.js
@@ -11,9 +11,6 @@ const {
} = require('../config');
const debug = require('debug')('talk:services:users');
-const redis = require('./redis');
-const redisClient = redis.createClient();
-
const UserModel = require('../models/user');
const USER_STATUS = require('../models/enum/user_status');
const USER_ROLES = require('../models/enum/user_roles');
@@ -32,6 +29,11 @@ const PASSWORD_RESET_JWT_SUBJECT = 'password_reset';
// through during the salting process.
const SALT_ROUNDS = 10;
+// Create a redis client to use for authentication.
+const {createClientFactory} = require('./redis');
+
+const client = createClientFactory();
+
// UsersService is the interface for the application to interact with the
// UserModel through.
module.exports = class UsersService {
@@ -67,7 +69,7 @@ module.exports = class UsersService {
const rdskey = `la[${email.toLowerCase().trim()}]`;
return new Promise((resolve, reject) => {
- redisClient
+ client()
.multi()
.incr(rdskey)
.expire(rdskey, RECAPTCHA_WINDOW_SECONDS)
@@ -80,7 +82,7 @@ module.exports = class UsersService {
if (replies[0] === 1 || replies[1] === -1) {
// then expire it after the timeout
- redisClient.expire(rdskey, RECAPTCHA_WINDOW_SECONDS);
+ client().expire(rdskey, RECAPTCHA_WINDOW_SECONDS);
}
if (replies[0] >= RECAPTCHA_INCORRECT_TRIGGER) {
@@ -102,7 +104,7 @@ module.exports = class UsersService {
const rdskey = `la[${email.toLowerCase().trim()}]`;
return new Promise((resolve, reject) => {
- redisClient
+ client()
.get(rdskey, (err, reply) => {
if (err) {
return reject(err);
diff --git a/test/server/redis.js b/test/server/redis.js
index 529548918..454a2afd8 100644
--- a/test/server/redis.js
+++ b/test/server/redis.js
@@ -1,3 +1,4 @@
const redis = require('../helpers/redis');
+const cache = require('../../services/cache');
-beforeEach(() => redis.clearDB());
+beforeEach(() => Promise.all([redis.clearDB(), cache.init()]));
diff --git a/views/article.ejs b/views/article.ejs
index d803b6295..f1a7ba5d9 100644
--- a/views/article.ejs
+++ b/views/article.ejs
@@ -30,6 +30,17 @@
asset_url: '<%= asset_url ? asset_url : '' %>',
asset_id: '<%= asset_id ? asset_id : '' %>',
auth_token: '',
+ /**
+ * You can listen to events using the example below.
+ * The argument passed is the event emitter from
+ * https://github.com/asyncly/EventEmitter2
+ *
+ * events: function(events) {
+ * events.onAny(function(eventName, data) {
+ * console.log(eventName, data);
+ * });
+ * },
+ */
plugin_config: {
/**
* You can disable rendering slot components of a plugin by doing:
diff --git a/yarn.lock b/yarn.lock
index d2177f384..255dc7415 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2957,6 +2957,10 @@ event-stream@~3.3.0:
stream-combiner "~0.0.4"
through "~2.3.1"
+eventemitter2@^4.1.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-4.1.2.tgz#0e1a8477af821a6ef3995b311bf74c23a5247f15"
+
eventemitter3@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba"