-
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/actions.js b/plugins/talk-plugin-featured-comments/client/actions.js
new file mode 100644
index 000000000..653b8f565
--- /dev/null
+++ b/plugins/talk-plugin-featured-comments/client/actions.js
@@ -0,0 +1,9 @@
+import {SHOW_TOOLTIP, HIDE_TOOLTIP} from './constants';
+
+export const showTooltip = () => ({
+ type: SHOW_TOOLTIP
+});
+
+export const hideTooltip = () => ({
+ type: HIDE_TOOLTIP
+});
diff --git a/plugins/talk-plugin-featured-comments/client/components/Button.css b/plugins/talk-plugin-featured-comments/client/components/Button.css
index 11e35d40b..daaa329c9 100644
--- a/plugins/talk-plugin-featured-comments/client/components/Button.css
+++ b/plugins/talk-plugin-featured-comments/client/components/Button.css
@@ -3,12 +3,8 @@
/* TODO: figure out the best location to include the `reset.css` */
composes: buttonReset from "coral-framework/styles/reset.css";
- color: #2a2a2a;
- margin-right: 10px;
-}
-
-.button {
color: #767676;
+ margin-right: 10px;
}
.button.featured {
diff --git a/plugins/talk-plugin-featured-comments/client/components/InfoIcon.css b/plugins/talk-plugin-featured-comments/client/components/InfoIcon.css
new file mode 100644
index 000000000..4fe40905b
--- /dev/null
+++ b/plugins/talk-plugin-featured-comments/client/components/InfoIcon.css
@@ -0,0 +1,10 @@
+.infoIcon {
+ color: #616161;
+ vertical-align: middle;
+ font-size: 16px;
+ margin-left: 5px;
+}
+
+ .infoIcon.on {
+ color: #1d5294;
+}
\ No newline at end of file
diff --git a/plugins/talk-plugin-featured-comments/client/components/InfoIcon.js b/plugins/talk-plugin-featured-comments/client/components/InfoIcon.js
new file mode 100644
index 000000000..47560fd74
--- /dev/null
+++ b/plugins/talk-plugin-featured-comments/client/components/InfoIcon.js
@@ -0,0 +1,11 @@
+import React from 'react';
+import styles from './InfoIcon.css';
+import cn from 'classnames';
+import {Icon} from 'plugin-api/beta/client/components/ui';
+
+export default ({tooltip}) => (
+
+);
diff --git a/plugins/talk-plugin-featured-comments/client/components/Tab.js b/plugins/talk-plugin-featured-comments/client/components/Tab.js
index c780887c6..6ac5682d0 100644
--- a/plugins/talk-plugin-featured-comments/client/components/Tab.js
+++ b/plugins/talk-plugin-featured-comments/client/components/Tab.js
@@ -1,9 +1,18 @@
import React from 'react';
import {TabCount} from 'plugin-api/beta/client/components/ui';
+import InfoIcon from './InfoIcon';
import {t} from 'plugin-api/beta/client/services';
+import Tooltip from './Tooltip';
-export default ({active, asset: {featuredCommentsCount}}) => (
-
- {t('talk-plugin-featured-comments.featured')} {featuredCommentsCount}
-
-);
+export default ({active, asset: {featuredCommentsCount}, tooltip, ...props}) => {
+ return (
+
+ {t('talk-plugin-featured-comments.featured')}
+ {featuredCommentsCount}
+
+ {tooltip && }
+
+ );
+};
diff --git a/plugins/talk-plugin-featured-comments/client/components/Tag.css b/plugins/talk-plugin-featured-comments/client/components/Tag.css
index 8874bfa0c..7ade3d968 100644
--- a/plugins/talk-plugin-featured-comments/client/components/Tag.css
+++ b/plugins/talk-plugin-featured-comments/client/components/Tag.css
@@ -17,4 +17,35 @@
margin: 0px 5px;
padding: 5px 5px;
border-radius: 2px;
+}
+
+.tag.on {
+ background-color: #1862a7;
+ 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;
+ right: 10px;
+}
+
+.tooltip::before{
+ left: auto;
+ right: 16px;
+}
+
+.tooltip::after{
+ left: auto;
+ right: 16px;
+ top: -20px;
}
\ No newline at end of file
diff --git a/plugins/talk-plugin-featured-comments/client/components/Tag.js b/plugins/talk-plugin-featured-comments/client/components/Tag.js
index 4afa9629a..ee2d13b19 100644
--- a/plugins/talk-plugin-featured-comments/client/components/Tag.js
+++ b/plugins/talk-plugin-featured-comments/client/components/Tag.js
@@ -1,16 +1,50 @@
import React from 'react';
+import cn from 'classnames';
import styles from './Tag.css';
+import Tooltip from './Tooltip';
import {t} from 'plugin-api/beta/client/services';
import {isTagged} from 'plugin-api/beta/client/utils';
-export default (props) => (
-
- {
- isTagged(props.comment.tags, 'FEATURED') && props.depth === 0 ? (
-
- {t('talk-plugin-featured-comments.featured')}
-
- ) : null
- }
-
-);
+export default class Tag extends React.Component {
+ constructor() {
+ super();
+
+ this.state = {
+ tooltip: false
+ };
+
+ }
+
+ showTooltip = e => {
+ e.preventDefault();
+ this.setState({
+ tooltip: true
+ });
+ }
+
+ hideTooltip = (e) => {
+ e.preventDefault();
+ this.setState({
+ tooltip: false
+ });
+ }
+
+ render() {
+ const {tooltip} = this.state;
+ return(
+
+ {
+ isTagged(this.props.comment.tags, 'FEATURED') ? (
+
+ {t('talk-plugin-featured-comments.featured')}
+
+ ) : null
+ }
+ {tooltip && }
+
+ );
+ }
+}
diff --git a/plugins/talk-plugin-featured-comments/client/components/Tooltip.css b/plugins/talk-plugin-featured-comments/client/components/Tooltip.css
new file mode 100644
index 000000000..98eb50b46
--- /dev/null
+++ b/plugins/talk-plugin-featured-comments/client/components/Tooltip.css
@@ -0,0 +1,64 @@
+.tooltip {
+ background-color: white;
+ border: solid 1px #999;
+ border-radius: 3px;
+ padding: 10px;
+ position: absolute;
+ -webkit-box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2);
+ box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2);
+ z-index: 10;
+ top: 26px;
+ left: 50px;
+ width: 200px;
+ text-align: left;
+ color: #616161;
+}
+
+.tooltip::before{
+ content: '';
+ border: 10px solid transparent;
+ border-top-color: #999;
+ position: absolute;
+ left: 46px;
+ top: -21px;
+ transform: rotate(180deg);
+}
+
+.tooltip::after{
+ content: '';
+ border: 10px solid transparent;
+ border-top-color: white;
+ position: absolute;
+ left: 46px;
+ top: -20px;
+ transform: rotate(180deg);
+}
+
+.headline {
+ color: #484747;
+ display: inline-block;
+ margin: 0;
+ padding: 0;
+ font-size: 1.02em;
+ margin-left: 6px;
+ letter-spacing: 0.2px;
+ vertical-align: middle;
+ margin-bottom: 2px;
+ line-height: 22px;
+}
+
+.icon {
+ font-size: 1.4em;
+ vertical-align: middle;
+}
+
+.description {
+ width: 100%;
+ box-sizing: border-box;
+ white-space: pre-wrap;
+ padding: 0;
+ margin: 0;
+ padding-left: 25px;
+ font-weight: 400;
+ font-size: 1em;
+}
\ No newline at end of file
diff --git a/plugins/talk-plugin-featured-comments/client/components/Tooltip.js b/plugins/talk-plugin-featured-comments/client/components/Tooltip.js
new file mode 100644
index 000000000..f98d4c0d5
--- /dev/null
+++ b/plugins/talk-plugin-featured-comments/client/components/Tooltip.js
@@ -0,0 +1,17 @@
+import React from 'react';
+import cn from 'classnames';
+import styles from './Tooltip.css';
+import {t} from 'plugin-api/beta/client/services';
+import {Icon} from 'plugin-api/beta/client/components/ui';
+
+export default ({className = ''}) => (
+
+
+
+ {t('talk-plugin-featured-comments.featured_comments')}:
+
+
+ {t('talk-plugin-featured-comments.tooltip_description')}
+
+
+);
diff --git a/plugins/talk-plugin-featured-comments/client/constants.js b/plugins/talk-plugin-featured-comments/client/constants.js
new file mode 100644
index 000000000..7aad99014
--- /dev/null
+++ b/plugins/talk-plugin-featured-comments/client/constants.js
@@ -0,0 +1,2 @@
+export const SHOW_TOOLTIP = 'SHOW_TOOLTIP';
+export const HIDE_TOOLTIP = 'HIDE_TOOLTIP';
diff --git a/plugins/talk-plugin-featured-comments/client/containers/Comment.js b/plugins/talk-plugin-featured-comments/client/containers/Comment.js
index bba764a1d..7080627e6 100644
--- a/plugins/talk-plugin-featured-comments/client/containers/Comment.js
+++ b/plugins/talk-plugin-featured-comments/client/containers/Comment.js
@@ -31,6 +31,7 @@ export default withFragments({
name
}
}
+
user {
id
username
diff --git a/plugins/talk-plugin-featured-comments/client/containers/Tab.js b/plugins/talk-plugin-featured-comments/client/containers/Tab.js
index 5d92387d6..f28a63566 100644
--- a/plugins/talk-plugin-featured-comments/client/containers/Tab.js
+++ b/plugins/talk-plugin-featured-comments/client/containers/Tab.js
@@ -1,8 +1,19 @@
-import {compose, gql} from 'react-apollo';
-import {withFragments, excludeIf} from 'plugin-api/beta/client/hocs';
import Tab from '../components/Tab';
+import {bindActionCreators} from 'redux';
+import {showTooltip, hideTooltip} from '../actions';
+import {compose, gql} from 'react-apollo';
+import {withFragments, excludeIf, connect} from 'plugin-api/beta/client/hocs';
+
+const mapStateToProps = ({talkPluginFeaturedComments: state}) => state;
+
+const mapDispatchToProps = (dispatch) =>
+ bindActionCreators({
+ showTooltip,
+ hideTooltip,
+ }, dispatch);
const enhance = compose(
+ connect(mapStateToProps, mapDispatchToProps),
withFragments({
asset: gql`
fragment TalkFeaturedComments_Tab_asset on Asset {
diff --git a/plugins/talk-plugin-featured-comments/client/containers/TabPane.js b/plugins/talk-plugin-featured-comments/client/containers/TabPane.js
index e9d466061..3a082e732 100644
--- a/plugins/talk-plugin-featured-comments/client/containers/TabPane.js
+++ b/plugins/talk-plugin-featured-comments/client/containers/TabPane.js
@@ -47,7 +47,7 @@ class TabPaneContainer extends React.Component {
}
const LOAD_MORE_QUERY = gql`
- query CoralEmbedStream_LoadMoreComments($limit: Int = 5, $cursor: Date, $asset_id: ID, $sort: SORT_ORDER, $excludeIgnored: Boolean) {
+ query TalkFeaturedComments_LoadMoreComments($limit: Int = 5, $cursor: Date, $asset_id: ID, $sort: SORT_ORDER, $excludeIgnored: Boolean) {
comments(query: {limit: $limit, cursor: $cursor, tags: ["FEATURED"], asset_id: $asset_id, sort: $sort, excludeIgnored: $excludeIgnored}) {
nodes {
...${getDefinitionName(Comment.fragments.comment)}
diff --git a/plugins/talk-plugin-featured-comments/client/index.js b/plugins/talk-plugin-featured-comments/client/index.js
index d804e2b55..8e4d18058 100644
--- a/plugins/talk-plugin-featured-comments/client/index.js
+++ b/plugins/talk-plugin-featured-comments/client/index.js
@@ -1,14 +1,16 @@
import Tab from './containers/Tab';
-import TabPane from './containers/TabPane';
import Tag from './components/Tag';
import Button from './components/Button';
+import TabPane from './containers/TabPane';
import translations from './translations.yml';
import update from 'immutability-helper';
+import reducer from './reducer';
import {findCommentInEmbedQuery} from 'coral-embed-stream/src/graphql/utils';
import {insertCommentsSorted} from 'plugin-api/beta/client/utils';
export default {
+ reducer,
translations,
slots: {
streamTabs: [Tab],
diff --git a/plugins/talk-plugin-featured-comments/client/reducer.js b/plugins/talk-plugin-featured-comments/client/reducer.js
new file mode 100644
index 000000000..2fd9b82e0
--- /dev/null
+++ b/plugins/talk-plugin-featured-comments/client/reducer.js
@@ -0,0 +1,22 @@
+import {SHOW_TOOLTIP, HIDE_TOOLTIP} from './constants';
+
+const initialState = {
+ tooltip: false
+};
+
+export default function featured (state = initialState, action) {
+ switch (action.type) {
+ case SHOW_TOOLTIP:
+ return {
+ ...state,
+ tooltip: true
+ };
+ case HIDE_TOOLTIP:
+ return {
+ ...state,
+ tooltip: false
+ };
+ default :
+ return state;
+ }
+}
diff --git a/plugins/talk-plugin-featured-comments/client/translations.yml b/plugins/talk-plugin-featured-comments/client/translations.yml
index 7f6db9370..50e51d586 100644
--- a/plugins/talk-plugin-featured-comments/client/translations.yml
+++ b/plugins/talk-plugin-featured-comments/client/translations.yml
@@ -1,8 +1,12 @@
en:
talk-plugin-featured-comments:
featured: Featured
+ featured_comments: Featured Comments
go_to_conversation: Go to conversation
+ tooltip_description: Comments selected by our team as worth reading
es:
talk-plugin-featured-comments:
featured: Remarcado
+ featured_comments: Comentarios Remarcados
go_to_conversation: Ir al comentario
+ tooltip_description: Comentarios seleccionados por nuestro equipo que valen la pena ser leidos
\ No newline at end of file
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 13dddaf04..12da5ecce 100644
--- a/services/users.js
+++ b/services/users.js
@@ -1,6 +1,6 @@
const assert = require('assert');
const uuid = require('uuid');
-const bcrypt = require('bcrypt');
+const bcrypt = require('bcryptjs');
const url = require('url');
const jwt = require('jsonwebtoken');
const Wordlist = require('./wordlist');
@@ -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 710b89fa2..255dc7415 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1181,13 +1181,9 @@ bcrypt-pbkdf@^1.0.0:
dependencies:
tweetnacl "^0.14.3"
-bcrypt@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-1.0.2.tgz#d05fc5d223173e0e28ec381c0f00cc25ffaf2736"
- dependencies:
- bindings "1.2.1"
- nan "2.5.0"
- node-pre-gyp "0.6.32"
+bcryptjs@^2.4.3:
+ version "2.4.3"
+ resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb"
big.js@^3.1.3:
version "3.1.3"
@@ -1197,7 +1193,7 @@ binary-extensions@^1.0.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.8.0.tgz#48ec8d16df4377eae5fa5884682480af4d95c774"
-bindings@1.2.1, bindings@^1.2.1:
+bindings@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.2.1.tgz#14ad6113812d2d37d72e67b4cacb4bb726505f11"
@@ -2961,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"
@@ -5445,7 +5445,7 @@ mute-stream@0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
-nan@2.5.0, nan@^2.0.0, nan@^2.3.0, nan@^2.4.0:
+nan@^2.0.0, nan@^2.3.0, nan@^2.4.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.5.0.tgz#aa8f1e34531d807e9e27755b234b4a6ec0c152a8"
@@ -5562,7 +5562,7 @@ node-libs-browser@^2.0.0:
util "^0.10.3"
vm-browserify "0.0.4"
-node-pre-gyp@0.6.32, node-pre-gyp@^0.6.29, node-pre-gyp@^0.6.4:
+node-pre-gyp@^0.6.29, node-pre-gyp@^0.6.4:
version "0.6.32"
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.32.tgz#fc452b376e7319b3d255f5f34853ef6fd8fe1fd5"
dependencies: