mirror of
https://github.com/wassname/talk.git
synced 2026-07-07 17:34:54 +08:00
Merge branch 'master' into live-reactions-no-comment
This commit is contained in:
+2
-1
@@ -1,9 +1,10 @@
|
||||
# excluded because we'll likely need to rebuild this.
|
||||
node_modules
|
||||
|
||||
# scripts are used during development and testing, not
|
||||
# most scripts are used during development and testing, not
|
||||
# production.
|
||||
scripts
|
||||
!scripts/generateIntrospectionResult.js
|
||||
|
||||
# documentation should not be visable in production.
|
||||
docs
|
||||
|
||||
@@ -5,6 +5,8 @@ dist
|
||||
npm-debug.log*
|
||||
dump.rdb
|
||||
|
||||
client/coral-framework/graphql/introspection.json
|
||||
|
||||
.env
|
||||
*.cfg
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const helmet = require('helmet');
|
||||
const authentication = require('./middleware/authentication');
|
||||
const {passport} = require('./services/passport');
|
||||
const plugins = require('./services/plugins');
|
||||
const pubsub = require('./services/pubsub');
|
||||
const i18n = require('./services/i18n');
|
||||
const enabled = require('debug').enabled;
|
||||
const errors = require('./errors');
|
||||
@@ -95,6 +96,17 @@ app.use(passport.initialize());
|
||||
// (if present) the JWT on the request.
|
||||
app.use('/api', authentication);
|
||||
|
||||
// To handle dependancy injection safer, we inject the pubsub handle onto the
|
||||
// request object.
|
||||
app.use('/api', (req, res, next) => {
|
||||
|
||||
// Attach the pubsub handle to the requests.
|
||||
req.pubsub = pubsub.createClient();
|
||||
|
||||
// Forward on the request.
|
||||
next();
|
||||
});
|
||||
|
||||
//==============================================================================
|
||||
// GraphQL Router
|
||||
//==============================================================================
|
||||
@@ -159,7 +171,7 @@ app.use('/', (err, req, res, next) => {
|
||||
}
|
||||
|
||||
i18n.init(req);
|
||||
|
||||
|
||||
if (err instanceof errors.APIError) {
|
||||
res.status(err.status);
|
||||
res.render('error', {
|
||||
|
||||
+11
-3
@@ -12,6 +12,7 @@ const SetupService = require('../services/setup');
|
||||
const kue = require('../services/kue');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const util = require('./util');
|
||||
const cache = require('../services/cache');
|
||||
const {createSubscriptionManager} = require('../graph/subscriptions');
|
||||
const {
|
||||
PORT
|
||||
@@ -80,7 +81,11 @@ function normalizePort(val) {
|
||||
* Event listener for HTTP server "listening" event.
|
||||
*/
|
||||
|
||||
function onListening() {
|
||||
async function onListening() {
|
||||
|
||||
// Start the cache instance.
|
||||
await cache.init();
|
||||
|
||||
let addr = server.address();
|
||||
let bind = typeof addr === 'string'
|
||||
? `pipe ${addr}`
|
||||
@@ -135,6 +140,11 @@ async function startApp(program) {
|
||||
/**
|
||||
* Listen on provided port, on all network interfaces.
|
||||
*/
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
server.on('listening', () => {
|
||||
|
||||
});
|
||||
server.listen(port, () => {
|
||||
|
||||
// Mount the websocket server if requested.
|
||||
@@ -145,8 +155,6 @@ async function startApp(program) {
|
||||
createSubscriptionManager(server);
|
||||
}
|
||||
});
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
||||
@@ -2,8 +2,10 @@ import React from 'react';
|
||||
import {render} from 'react-dom';
|
||||
import {ApolloProvider} from 'react-apollo';
|
||||
import smoothscroll from 'smoothscroll-polyfill';
|
||||
import EventEmitter from 'eventemitter2';
|
||||
import EventEmitterProvider from 'coral-framework/components/EventEmitterProvider';
|
||||
|
||||
import {getClient} from './services/client';
|
||||
import {getClient} from 'coral-framework/services/client';
|
||||
import store from './services/store';
|
||||
|
||||
import App from './components/App';
|
||||
@@ -12,13 +14,19 @@ import 'react-mdl/extra/material.js';
|
||||
import './graphql';
|
||||
import {loadPluginsTranslations, injectPluginsReducers} from 'coral-framework/helpers/plugins';
|
||||
|
||||
const eventEmitter = new EventEmitter();
|
||||
|
||||
// TODO: pass redux actions through the emitter.
|
||||
|
||||
loadPluginsTranslations();
|
||||
injectPluginsReducers();
|
||||
smoothscroll.polyfill();
|
||||
|
||||
render(
|
||||
<ApolloProvider client={getClient()} store={store}>
|
||||
<App />
|
||||
</ApolloProvider>
|
||||
<EventEmitterProvider eventEmitter={eventEmitter}>
|
||||
<ApolloProvider client={getClient()} store={store}>
|
||||
<App />
|
||||
</ApolloProvider>
|
||||
</EventEmitterProvider>
|
||||
, document.querySelector('#root')
|
||||
);
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import {getClient as getFrameworkClient} from 'coral-framework/services/client';
|
||||
import fragmentMatcher from './fragmentMatcher';
|
||||
|
||||
export function getClient() {
|
||||
return getFrameworkClient({fragmentMatcher});
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import {IntrospectionFragmentMatcher} from 'apollo-client';
|
||||
|
||||
// TODO this is a short-term fix
|
||||
// we need to set up something to query the server for the schema before ApolloClient initialization
|
||||
// https://github.com/apollographql/apollo-client/issues/1555#issuecomment-295834774
|
||||
const fm = new IntrospectionFragmentMatcher({
|
||||
introspectionQueryResultData: {
|
||||
__schema: {
|
||||
types: [
|
||||
{
|
||||
kind: 'INTERFACE',
|
||||
name: 'UserError',
|
||||
possibleTypes: [
|
||||
{name: 'GenericUserError'},
|
||||
{name: 'ValidationUserError'}
|
||||
]
|
||||
},
|
||||
{
|
||||
kind: 'INTERFACE',
|
||||
name: 'Response',
|
||||
possibleTypes: [
|
||||
{name: 'CreateCommentResponse'},
|
||||
{name: 'CreateFlagResponse'},
|
||||
{name: 'CreateDontAgreeResponse'},
|
||||
{name: 'DeleteActionResponse'},
|
||||
{name: 'SetUserStatusResponse'},
|
||||
{name: 'SuspendUserResponse'},
|
||||
{name: 'SetCommentStatusResponse'},
|
||||
{name: 'ModifyTagResponse'},
|
||||
{name: 'IgnoreUserResponse'},
|
||||
{name: 'StopIgnoringUserResponse'}
|
||||
]
|
||||
},
|
||||
{
|
||||
kind: 'INTERFACE',
|
||||
name: 'Action',
|
||||
possibleTypes: [
|
||||
{name: 'DefaultAction'},
|
||||
{name: 'FlagAction'},
|
||||
{name: 'DontAgreeAction'}
|
||||
]
|
||||
},
|
||||
{
|
||||
kind: 'INTERFACE',
|
||||
name: 'ActionSummary',
|
||||
possibleTypes: [
|
||||
{name: 'DefaultActionSummary'},
|
||||
{name: 'FlagActionSummary'},
|
||||
{name: 'DontAgreeActionSummary'}
|
||||
]
|
||||
},
|
||||
{
|
||||
kind: 'INTERFACE',
|
||||
name: 'AssetActionSummary',
|
||||
possibleTypes: [
|
||||
{name: 'DefaultAssetActionSummary'},
|
||||
{name: 'FlagAssetActionSummary'}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default fm;
|
||||
@@ -1,7 +1,7 @@
|
||||
import {createStore, combineReducers, applyMiddleware, compose} from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import mainReducer from '../reducers';
|
||||
import {getClient} from './client';
|
||||
import {getClient} from 'coral-framework/services/client';
|
||||
|
||||
const middlewares = [
|
||||
applyMiddleware(getClient().middleware()),
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
.filterWrapper {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
margin-top: 6px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.highlightedContainer {
|
||||
@@ -26,4 +26,4 @@
|
||||
margin-top: 28px;
|
||||
padding-bottom: 50px;
|
||||
min-height: 600px;
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,7 @@ class Stream extends React.Component {
|
||||
setActiveReplyBox,
|
||||
appendItemArray,
|
||||
commentClassNames,
|
||||
root: {asset, asset: {comments, totalCommentCount}, comment, me},
|
||||
root: {asset, asset: {comment, comments, totalCommentCount}, me},
|
||||
postComment,
|
||||
addNotification,
|
||||
editComment,
|
||||
|
||||
@@ -3,6 +3,7 @@ import {compose, gql} from 'react-apollo';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import get from 'lodash/get';
|
||||
import branch from 'recompose/branch';
|
||||
import renderComponent from 'recompose/renderComponent';
|
||||
|
||||
@@ -91,7 +92,7 @@ class EmbedContainer extends React.Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (!prevProps.root.comment && this.props.root.comment) {
|
||||
if (!get(prevProps, 'root.asset.comment') && get(this.props, 'root.asset.comment')) {
|
||||
|
||||
// Scroll to a permalinked comment if one is in the URL once the page is done rendering.
|
||||
setTimeout(() => pym.scrollParentToChildEl('talk-embed-stream-container'), 0);
|
||||
|
||||
@@ -237,16 +237,16 @@ const slots = [
|
||||
const fragments = {
|
||||
root: gql`
|
||||
fragment CoralEmbedStream_Stream_root on RootQuery {
|
||||
comment(id: $commentId) @include(if: $hasComment) {
|
||||
...CoralEmbedStream_Stream_comment
|
||||
${nest(`
|
||||
parent {
|
||||
...CoralEmbedStream_Stream_comment
|
||||
...nest
|
||||
}
|
||||
`, THREADING_LEVEL)}
|
||||
}
|
||||
asset(id: $assetId, url: $assetUrl) {
|
||||
comment(id: $commentId) @include(if: $hasComment) {
|
||||
...CoralEmbedStream_Stream_comment
|
||||
${nest(`
|
||||
parent {
|
||||
...CoralEmbedStream_Stream_comment
|
||||
...nest
|
||||
}
|
||||
`, THREADING_LEVEL)}
|
||||
}
|
||||
id
|
||||
title
|
||||
url
|
||||
|
||||
@@ -12,8 +12,8 @@ function determineCommentDepth(comment) {
|
||||
}
|
||||
|
||||
function applyToCommentsOrigin(root, callback) {
|
||||
if (root.comment) {
|
||||
let comment = root.comment;
|
||||
if (root.asset.comment) {
|
||||
let comment = root.asset.comment;
|
||||
for (let depth = 0; depth <= determineCommentDepth(comment); depth++) {
|
||||
let changes = {$apply: (node) => node ? callback(node) : node};
|
||||
for (let i = 0; i < depth; i++) {
|
||||
@@ -24,7 +24,10 @@ function applyToCommentsOrigin(root, callback) {
|
||||
|
||||
return {
|
||||
...root,
|
||||
comment,
|
||||
asset: {
|
||||
...root.asset,
|
||||
comment,
|
||||
},
|
||||
};
|
||||
}
|
||||
return update(root, {
|
||||
@@ -135,8 +138,8 @@ export function findCommentInEmbedQuery(root, callbackOrId) {
|
||||
if (typeof callbackOrId === 'string') {
|
||||
callback = (node) => node.id === callbackOrId;
|
||||
}
|
||||
if (root.comment) {
|
||||
return findComment([getTopLevelParent(root.comment)], callback);
|
||||
if (root.asset.comment) {
|
||||
return findComment([getTopLevelParent(root.asset.comment)], callback);
|
||||
}
|
||||
if (!root.asset.comments) {
|
||||
return false;
|
||||
|
||||
@@ -5,15 +5,19 @@ import {ApolloProvider} from 'react-apollo';
|
||||
import {checkLogin} from 'coral-framework/actions/auth';
|
||||
import './graphql';
|
||||
import {addExternalConfig} from 'coral-embed-stream/src/actions/config';
|
||||
import {getStore, injectReducers} from 'coral-framework/services/store';
|
||||
import {getStore, injectReducers, addListener} from 'coral-framework/services/store';
|
||||
import {getClient} from 'coral-framework/services/client';
|
||||
import {createReduxEmitter} from 'coral-framework/services/events';
|
||||
import pym from 'coral-framework/services/pym';
|
||||
import AppRouter from './AppRouter';
|
||||
import {loadPluginsTranslations, injectPluginsReducers} from 'coral-framework/helpers/plugins';
|
||||
import reducers from './reducers';
|
||||
import EventEmitter from 'eventemitter2';
|
||||
import EventEmitterProvider from 'coral-framework/components/EventEmitterProvider';
|
||||
|
||||
const store = getStore();
|
||||
const client = getClient();
|
||||
const eventEmitter = new EventEmitter({wildcard: true});
|
||||
|
||||
loadPluginsTranslations();
|
||||
injectPluginsReducers();
|
||||
@@ -42,11 +46,21 @@ if (!window.opener) {
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
// Pass any events through our parent.
|
||||
eventEmitter.onAny((eventName, value) => {
|
||||
pym.sendMessage('event', JSON.stringify({eventName, value}));
|
||||
});
|
||||
|
||||
// Add a redux listener to pass through all actions to our event emitter.
|
||||
addListener(createReduxEmitter(eventEmitter));
|
||||
}
|
||||
|
||||
render(
|
||||
<ApolloProvider client={client} store={store}>
|
||||
<AppRouter />
|
||||
</ApolloProvider>
|
||||
<EventEmitterProvider eventEmitter={eventEmitter}>
|
||||
<ApolloProvider client={client} store={store}>
|
||||
<AppRouter />
|
||||
</ApolloProvider>
|
||||
</EventEmitterProvider>
|
||||
, document.querySelector('#talk-embed-stream-container')
|
||||
);
|
||||
|
||||
@@ -3,6 +3,9 @@ import URLSearchParams from 'url-search-params';
|
||||
|
||||
import {buildUrl} from 'coral-framework/utils';
|
||||
import queryString from 'query-string';
|
||||
import EventEmitter from 'eventemitter2';
|
||||
|
||||
const eventEmitter = new EventEmitter({wildcard: true});
|
||||
|
||||
// TODO: Styles should live in a separate file
|
||||
const snackbarStyles = {
|
||||
@@ -57,6 +60,10 @@ function configurePymParent(pymParent, opts) {
|
||||
pymParent.sendMessage('config', JSON.stringify(config));
|
||||
}
|
||||
|
||||
if (opts.events) {
|
||||
opts.events(eventEmitter);
|
||||
}
|
||||
|
||||
pymParent.onMessage('coral-auth-changed', function(message) {
|
||||
if (opts.onAuthChanged) {
|
||||
opts.onAuthChanged(message ? JSON.parse(message) : null);
|
||||
@@ -167,6 +174,12 @@ function configurePymParent(pymParent, opts) {
|
||||
window.open(url, '_blank').focus();
|
||||
});
|
||||
|
||||
// Pass events from iframe to the event emitter
|
||||
pymParent.onMessage('event', (raw) => {
|
||||
const {eventName, value} = JSON.parse(raw);
|
||||
eventEmitter.emit(eventName, value);
|
||||
});
|
||||
|
||||
// get dimensions of viewport
|
||||
const viewport = () => {
|
||||
let e = window, a = 'inner';
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
const PropTypes = require('prop-types');
|
||||
|
||||
class EventEmitterProvider extends React.Component {
|
||||
getChildContext() {
|
||||
return {eventEmitter: this.props.eventEmitter};
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
EventEmitterProvider.childContextTypes = {
|
||||
eventEmitter: PropTypes.object,
|
||||
};
|
||||
|
||||
export default EventEmitterProvider;
|
||||
@@ -2,3 +2,6 @@ export {default as withFragments} from './withFragments';
|
||||
export {default as withMutation} from './withMutation';
|
||||
export {default as withQuery} from './withQuery';
|
||||
export {default as withCopyToClipboard} from './withCopyToClipboard';
|
||||
export {default as withEmit} from './withEmit';
|
||||
export {default as excludeIf} from './excludeIf';
|
||||
export {default as connect} from './connect';
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
const PropTypes = require('prop-types');
|
||||
|
||||
/**
|
||||
* WithEmit provides a property `emit: (eventName, value)`
|
||||
* to the wrapped component.
|
||||
*/
|
||||
export default (WrappedComponent) => {
|
||||
class WithEmit extends React.Component {
|
||||
static contextTypes = {
|
||||
eventEmitter: PropTypes.object,
|
||||
};
|
||||
|
||||
emit = (eventName, value) => {
|
||||
this.context.eventEmitter.emit(eventName, value);
|
||||
};
|
||||
|
||||
render() {
|
||||
return <WrappedComponent
|
||||
{...this.props}
|
||||
emit={this.emit}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
return WithEmit;
|
||||
};
|
||||
@@ -5,8 +5,8 @@ import uniq from 'lodash/uniq';
|
||||
import flatten from 'lodash/flatten';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import {getMutationOptions, resolveFragments} from 'coral-framework/services/graphqlRegistry';
|
||||
import {getStore} from 'coral-framework/services/store';
|
||||
import {getDefinitionName, getResponseErrors} from '../utils';
|
||||
import PropTypes from 'prop-types';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class ResponseErrors extends Error {
|
||||
@@ -36,98 +36,115 @@ export default (document, config = {}) => (WrappedComponent) => {
|
||||
options: config.options || {},
|
||||
props: config.props || ((data) => ({mutate: data.mutate()})),
|
||||
};
|
||||
const wrappedProps = (data) => {
|
||||
const name = getDefinitionName(document);
|
||||
const callbacks = getMutationOptions(name);
|
||||
const mutate = (base) => {
|
||||
const variables = base.variables || config.options.variables;
|
||||
const configs = callbacks.map((cb) => cb({variables, state: getStore().getState()}));
|
||||
|
||||
const optimisticResponse = merge(
|
||||
base.optimisticResponse || config.options.optimisticResponse,
|
||||
...configs.map((cfg) => cfg.optimisticResponse),
|
||||
);
|
||||
|
||||
const refetchQueries = flatten(uniq([
|
||||
base.refetchQueries || config.options.refetchQueries,
|
||||
...configs.map((cfg) => cfg.refetchQueries),
|
||||
].filter((i) => i)));
|
||||
|
||||
const updateCallbacks =
|
||||
[base.update || config.options.update]
|
||||
.concat(...configs.map((cfg) => cfg.update))
|
||||
.filter((i) => i);
|
||||
|
||||
const update = (proxy, result) => {
|
||||
if (getResponseErrors(result)) {
|
||||
|
||||
// Do not run updates when we have mutation errors.
|
||||
return;
|
||||
}
|
||||
updateCallbacks.forEach((cb) => cb(proxy, result));
|
||||
};
|
||||
|
||||
const updateQueries =
|
||||
[
|
||||
base.updateQueries || config.options.updateQueries,
|
||||
...configs.map((cfg) => cfg.updateQueries)
|
||||
]
|
||||
.filter((i) => i)
|
||||
.reduce((res, map) => {
|
||||
Object.keys(map).forEach((key) => {
|
||||
if (!(key in res)) {
|
||||
res[key] = (prev, result) => {
|
||||
if (getResponseErrors(result.mutationResult)) {
|
||||
|
||||
// Do not run updates when we have mutation errors.
|
||||
return prev;
|
||||
}
|
||||
return map[key](prev, result);
|
||||
};
|
||||
} else {
|
||||
const existing = res[key];
|
||||
res[key] = (prev, result) => {
|
||||
const next = existing(prev, result);
|
||||
return map[key](next, result);
|
||||
};
|
||||
}
|
||||
});
|
||||
return res;
|
||||
}, {});
|
||||
|
||||
const wrappedConfig = {
|
||||
variables,
|
||||
optimisticResponse,
|
||||
refetchQueries,
|
||||
updateQueries,
|
||||
update,
|
||||
};
|
||||
if (isEmpty(wrappedConfig.optimisticResponse)) {
|
||||
delete wrappedConfig.optimisticResponse;
|
||||
}
|
||||
return data.mutate(wrappedConfig)
|
||||
.then((res) => {
|
||||
const errors = getResponseErrors(res);
|
||||
if (errors) {
|
||||
throw new ResponseErrors(errors);
|
||||
}
|
||||
return Promise.resolve(res);
|
||||
});
|
||||
return class WithMutation extends React.Component {
|
||||
static contextTypes = {
|
||||
eventEmitter: PropTypes.object,
|
||||
store: PropTypes.object,
|
||||
};
|
||||
return config.props({...data, mutate});
|
||||
};
|
||||
|
||||
// Lazily resolve fragments from graphRegistry to support circular dependencies.
|
||||
let memoized = null;
|
||||
const getWrapped = () => {
|
||||
if (!memoized) {
|
||||
memoized = graphql(resolveFragments(document), {...config, props: wrappedProps})(WrappedComponent);
|
||||
// Lazily resolve fragments from graphRegistry to support circular dependencies.
|
||||
memoized = null;
|
||||
|
||||
wrappedProps = (data) => {
|
||||
const name = getDefinitionName(document);
|
||||
const callbacks = getMutationOptions(name);
|
||||
const mutate = (base) => {
|
||||
const variables = base.variables || config.options.variables;
|
||||
const configs = callbacks.map((cb) => cb({variables, state: this.context.store.getState()}));
|
||||
|
||||
const optimisticResponse = merge(
|
||||
base.optimisticResponse || config.options.optimisticResponse,
|
||||
...configs.map((cfg) => cfg.optimisticResponse),
|
||||
);
|
||||
|
||||
const refetchQueries = flatten(uniq([
|
||||
base.refetchQueries || config.options.refetchQueries,
|
||||
...configs.map((cfg) => cfg.refetchQueries),
|
||||
].filter((i) => i)));
|
||||
|
||||
const updateCallbacks =
|
||||
[base.update || config.options.update]
|
||||
.concat(...configs.map((cfg) => cfg.update))
|
||||
.filter((i) => i);
|
||||
|
||||
const update = (proxy, result) => {
|
||||
if (getResponseErrors(result)) {
|
||||
|
||||
// Do not run updates when we have mutation errors.
|
||||
return;
|
||||
}
|
||||
updateCallbacks.forEach((cb) => cb(proxy, result));
|
||||
};
|
||||
|
||||
const updateQueries =
|
||||
[
|
||||
base.updateQueries || config.options.updateQueries,
|
||||
...configs.map((cfg) => cfg.updateQueries)
|
||||
]
|
||||
.filter((i) => i)
|
||||
.reduce((res, map) => {
|
||||
Object.keys(map).forEach((key) => {
|
||||
if (!(key in res)) {
|
||||
res[key] = (prev, result) => {
|
||||
if (getResponseErrors(result.mutationResult)) {
|
||||
|
||||
// Do not run updates when we have mutation errors.
|
||||
return prev;
|
||||
}
|
||||
return map[key](prev, result);
|
||||
};
|
||||
} else {
|
||||
const existing = res[key];
|
||||
res[key] = (prev, result) => {
|
||||
const next = existing(prev, result);
|
||||
return map[key](next, result);
|
||||
};
|
||||
}
|
||||
});
|
||||
return res;
|
||||
}, {});
|
||||
|
||||
const wrappedConfig = {
|
||||
variables,
|
||||
optimisticResponse,
|
||||
refetchQueries,
|
||||
updateQueries,
|
||||
update,
|
||||
};
|
||||
if (isEmpty(wrappedConfig.optimisticResponse)) {
|
||||
delete wrappedConfig.optimisticResponse;
|
||||
}
|
||||
|
||||
this.context.eventEmitter.emit(`mutation.${name}.begin`, {variables});
|
||||
|
||||
return data.mutate(wrappedConfig)
|
||||
.then((res) => {
|
||||
const errors = getResponseErrors(res);
|
||||
if (errors) {
|
||||
throw new ResponseErrors(errors);
|
||||
}
|
||||
this.context.eventEmitter.emit(`mutation.${name}.success`, {variables, data: res.data});
|
||||
return Promise.resolve(res);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.context.eventEmitter.emit(`mutation.${name}.error`, {variables, error});
|
||||
throw new error;
|
||||
});
|
||||
};
|
||||
return config.props({...data, mutate});
|
||||
};
|
||||
|
||||
getWrapped = () => {
|
||||
if (!this.memoized) {
|
||||
this.memoized = graphql(resolveFragments(document), {...config, props: this.wrappedProps})(WrappedComponent);
|
||||
}
|
||||
return this.memoized;
|
||||
};
|
||||
|
||||
render() {
|
||||
const Wrapped = this.getWrapped();
|
||||
return <Wrapped {...this.props} />;
|
||||
}
|
||||
return memoized;
|
||||
};
|
||||
|
||||
return (props) => {
|
||||
const Wrapped = getWrapped();
|
||||
return <Wrapped {...props} />;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as React from 'react';
|
||||
import {graphql} from 'react-apollo';
|
||||
import {getQueryOptions, resolveFragments} from 'coral-framework/services/graphqlRegistry';
|
||||
import {getDefinitionName, separateDataAndRoot, getResponseErrors} from '../utils';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const withSkipOnErrors = (reducer) => (prev, action, ...rest) => {
|
||||
if (action.type === 'APOLLO_MUTATION_RESULT' && getResponseErrors(action.result)) {
|
||||
@@ -10,76 +11,146 @@ const withSkipOnErrors = (reducer) => (prev, action, ...rest) => {
|
||||
return reducer(prev, action, ...rest);
|
||||
};
|
||||
|
||||
function networkStatusToString(networkStatus) {
|
||||
switch(networkStatus) {
|
||||
case 1:
|
||||
return 'loading';
|
||||
case 2:
|
||||
return 'setVariables';
|
||||
case 3:
|
||||
return 'fetchMore';
|
||||
case 4:
|
||||
return 'refetch';
|
||||
case 6:
|
||||
return 'poll';
|
||||
case 7:
|
||||
return 'ready';
|
||||
case 8:
|
||||
return 'error';
|
||||
}
|
||||
throw new Error(`Unknown network status ${networkStatus}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports a HOC with the same signature as `graphql`, that will
|
||||
* apply query options registered in the graphRegistry.
|
||||
*/
|
||||
export default (document, config = {}) => (WrappedComponent) => {
|
||||
const wrappedConfig = {
|
||||
...config,
|
||||
options: config.options || {},
|
||||
props: (args) => {
|
||||
const wrappedArgs = {
|
||||
...args,
|
||||
data: {
|
||||
...args.data,
|
||||
subscribeToMore(stmArgs) {
|
||||
const name = getDefinitionName(document);
|
||||
|
||||
// Resolve document fragments before passing it to `apollo-client`.
|
||||
return args.data.subscribeToMore({
|
||||
...stmArgs,
|
||||
document: resolveFragments(stmArgs.document),
|
||||
});
|
||||
},
|
||||
fetchMore(lmArgs) {
|
||||
|
||||
// Resolve document fragments before passing it to `apollo-client`.
|
||||
return args.data.fetchMore({
|
||||
...lmArgs,
|
||||
query: resolveFragments(lmArgs.query),
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
return config.props
|
||||
? config.props(wrappedArgs)
|
||||
: separateDataAndRoot(wrappedArgs.data);
|
||||
},
|
||||
};
|
||||
|
||||
const wrappedOptions = (data) => {
|
||||
const base = (typeof wrappedConfig.options === 'function')
|
||||
? wrappedConfig.options(data)
|
||||
: wrappedConfig.options;
|
||||
const name = getDefinitionName(document);
|
||||
const configs = getQueryOptions(name);
|
||||
const reducerCallbacks =
|
||||
[base.reducer || ((i) => i)]
|
||||
.concat(...configs.map((cfg) => cfg.reducer))
|
||||
.filter((i) => i);
|
||||
|
||||
const reducer = withSkipOnErrors(
|
||||
reducerCallbacks.reduce(
|
||||
(a, b) => (prev, ...rest) =>
|
||||
b(a(prev, ...rest), ...rest),
|
||||
));
|
||||
|
||||
return {
|
||||
...base,
|
||||
reducer,
|
||||
return class WithQuery extends React.Component {
|
||||
static contextTypes = {
|
||||
eventEmitter: PropTypes.object,
|
||||
};
|
||||
};
|
||||
|
||||
let memoized = null;
|
||||
const getWrapped = () => {
|
||||
if (!memoized) {
|
||||
memoized = graphql(resolveFragments(document), {...wrappedConfig, options: wrappedOptions})(WrappedComponent);
|
||||
// Lazily resolve fragments from graphRegistry to support circular dependencies.
|
||||
memoized = null;
|
||||
lastNetworkStatus = null;
|
||||
|
||||
emitWhenNeeded(data) {
|
||||
const {variables, networkStatus} = data;
|
||||
if (this.lastNetworkStatus === networkStatus) {
|
||||
return;
|
||||
}
|
||||
this.lastNetworkStatus = networkStatus;
|
||||
|
||||
const status = networkStatusToString(networkStatus);
|
||||
|
||||
const {root} = separateDataAndRoot(data);
|
||||
this.context.eventEmitter.emit(`query.${name}.${status}`, {variables, data: root});
|
||||
}
|
||||
return memoized;
|
||||
};
|
||||
|
||||
return (props) => {
|
||||
const Wrapped = getWrapped();
|
||||
return <Wrapped {...props} />;
|
||||
wrappedConfig = {
|
||||
...config,
|
||||
options: config.options || {},
|
||||
props: (args) => {
|
||||
this.emitWhenNeeded(args.data);
|
||||
|
||||
const wrappedArgs = {
|
||||
...args,
|
||||
data: {
|
||||
...args.data,
|
||||
subscribeToMore: (stmArgs) => {
|
||||
|
||||
// Resolve document fragments before passing it to `apollo-client`.
|
||||
return args.data.subscribeToMore({
|
||||
...stmArgs,
|
||||
document: resolveFragments(stmArgs.document),
|
||||
onError: (err) => {
|
||||
if (stmArgs.onErr) {
|
||||
return stmArgs.onErr(err);
|
||||
}
|
||||
throw err;
|
||||
},
|
||||
});
|
||||
},
|
||||
fetchMore: (lmArgs) => {
|
||||
const fetchName = getDefinitionName(lmArgs.query);
|
||||
this.context.eventEmitter.emit(
|
||||
`query.${name}.fetchMore.${fetchName}.begin`,
|
||||
{variables: lmArgs.variables});
|
||||
|
||||
// Resolve document fragments before passing it to `apollo-client`.
|
||||
return args.data.fetchMore({
|
||||
...lmArgs,
|
||||
query: resolveFragments(lmArgs.query),
|
||||
})
|
||||
.then((res) => {
|
||||
this.context.eventEmitter.emit(
|
||||
`query.${name}.fetchMore.${fetchName}.success`,
|
||||
{variables: lmArgs.variables, data: res.data});
|
||||
return Promise.resolve(res);
|
||||
})
|
||||
.catch((err) => {
|
||||
this.context.eventEmitter.emit(
|
||||
`query.${name}.fetchMore.${fetchName}.error`,
|
||||
{variables: lmArgs.variables, error: err});
|
||||
throw err;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
return config.props
|
||||
? config.props(wrappedArgs)
|
||||
: separateDataAndRoot(wrappedArgs.data);
|
||||
},
|
||||
};
|
||||
|
||||
wrappedOptions = (data) => {
|
||||
const base = (typeof this.wrappedConfig.options === 'function')
|
||||
? this.wrappedConfig.options(data)
|
||||
: this.wrappedConfig.options;
|
||||
const configs = getQueryOptions(name);
|
||||
const reducerCallbacks =
|
||||
[base.reducer || ((i) => i)]
|
||||
.concat(...configs.map((cfg) => cfg.reducer))
|
||||
.filter((i) => i);
|
||||
|
||||
const reducer = withSkipOnErrors(
|
||||
reducerCallbacks.reduce(
|
||||
(a, b) => (prev, ...rest) =>
|
||||
b(a(prev, ...rest), ...rest),
|
||||
));
|
||||
|
||||
return {
|
||||
...base,
|
||||
reducer,
|
||||
};
|
||||
};
|
||||
|
||||
getWrapped = () => {
|
||||
if (!this.memoized) {
|
||||
this.memoized = graphql(
|
||||
resolveFragments(document),
|
||||
{...this.wrappedConfig, options: this.wrappedOptions},
|
||||
)(WrappedComponent);
|
||||
}
|
||||
return this.memoized;
|
||||
};
|
||||
|
||||
render() {
|
||||
const Wrapped = this.getWrapped();
|
||||
return <Wrapped {...this.props} />;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import ApolloClient, {addTypename} from 'apollo-client';
|
||||
import ApolloClient, {addTypename, IntrospectionFragmentMatcher} from 'apollo-client';
|
||||
import {networkInterface} from './transport';
|
||||
import {SubscriptionClient, addGraphQLSubscriptions} from 'subscriptions-transport-ws';
|
||||
import MessageTypes from 'subscriptions-transport-ws/dist/message-types';
|
||||
import {getAuthToken} from '../helpers/request';
|
||||
import introspectionQueryResultData from '../graphql/introspection.json';
|
||||
|
||||
let client, wsClient = null, wsClientToken = null;
|
||||
|
||||
@@ -59,6 +60,7 @@ export function getClient(options = {}) {
|
||||
...options,
|
||||
connectToDevTools: true,
|
||||
addTypename: true,
|
||||
fragmentMatcher: new IntrospectionFragmentMatcher({introspectionQueryResultData}),
|
||||
queryTransformer: addTypename,
|
||||
dataIdFromObject: (result) => {
|
||||
if (result.id && result.__typename) { // eslint-disable-line no-underscore-dangle
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export function createReduxEmitter(eventEmitter) {
|
||||
return (action) => {
|
||||
|
||||
// Handle apollo actions.
|
||||
if (action.type.startsWith('APOLLO_')) {
|
||||
if (action.type === 'APOLLO_SUBSCRIPTION_RESULT') {
|
||||
const {operationName, variables, result: {data}} = action;
|
||||
eventEmitter.emit(`subscription.${operationName}.data`, {variables, data});
|
||||
}
|
||||
return;
|
||||
}
|
||||
eventEmitter.emit(`action.${action.type}`, {action});
|
||||
};
|
||||
}
|
||||
@@ -3,12 +3,22 @@ import thunk from 'redux-thunk';
|
||||
import mainReducer from '../reducers';
|
||||
import {getClient} from './client';
|
||||
|
||||
let listeners = [];
|
||||
|
||||
export function injectReducers(reducers) {
|
||||
const store = getStore();
|
||||
store.coralReducers = {...store.coralReducers, ...reducers};
|
||||
store.replaceReducer(combineReducers(store.coralReducers));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a action listener to the redux store.
|
||||
* The action is passed as the first argument to the callback.
|
||||
*/
|
||||
export function addListener(cb) {
|
||||
listeners.push(cb);
|
||||
}
|
||||
|
||||
export function getStore() {
|
||||
if (window.coralStore) {
|
||||
return window.coralStore;
|
||||
@@ -21,11 +31,17 @@ export function getStore() {
|
||||
return next(action);
|
||||
};
|
||||
|
||||
const customListener = () => (next) => (action) => {
|
||||
listeners.forEach((cb) => {cb(action);});
|
||||
return next(action);
|
||||
};
|
||||
|
||||
const middlewares = [
|
||||
applyMiddleware(
|
||||
getClient().middleware(),
|
||||
thunk,
|
||||
apolloErrorReporter,
|
||||
customListener,
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
@@ -68,3 +68,13 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
@custom-media --small-viewport (max-width: 320px);
|
||||
|
||||
@media (--small-viewport) {
|
||||
.buttonSub, .buttonSubActive, .buttonSubActive:hover, .buttonSubActive:focus {
|
||||
font-size: 0.96em;
|
||||
padding-right: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ require('env-rewrite').rewrite();
|
||||
|
||||
const CONFIG = {
|
||||
|
||||
// WEBPACK indicates when webpack is currently building.
|
||||
WEBPACK: process.env.WEBPACK === 'true',
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// JWT based configuration
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -34,6 +37,9 @@ const CONFIG = {
|
||||
// JWT_EXPIRY is the time for which a given token is valid for.
|
||||
JWT_EXPIRY: process.env.TALK_JWT_EXPIRY || '1 day',
|
||||
|
||||
// JWT_ALG is the algorithm used for signing jwt tokens.
|
||||
JWT_ALG: process.env.TALK_JWT_ALG || 'HS256',
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Installation locks
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ class Context {
|
||||
this.plugins = decorateContextPlugins(this, contextPlugins);
|
||||
|
||||
// Bind the publish/subscribe to the context.
|
||||
this.pubsub = pubsub;
|
||||
this.pubsub = pubsub.createClient();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -445,12 +445,12 @@ const genRecentComments = (_, ids) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* genComments returns the comments by the id's. Only admins can see non-public comments.
|
||||
* getComments returns the comments by the id's. Only admins can see non-public comments.
|
||||
* @param {Object} context graph context
|
||||
* @param {Array<String>} ids the comment id's to fetch
|
||||
* @return {Promise} resolves to the comments
|
||||
*/
|
||||
const genComments = ({user}, ids) => {
|
||||
const getComments = ({user}, ids) => {
|
||||
let comments;
|
||||
if (user && user.can(SEARCH_OTHERS_COMMENTS)) {
|
||||
comments = CommentModel.find({
|
||||
@@ -478,7 +478,7 @@ const genComments = ({user}, ids) => {
|
||||
*/
|
||||
module.exports = (context) => ({
|
||||
Comments: {
|
||||
get: new DataLoader((ids) => genComments(context, ids)),
|
||||
get: new DataLoader((ids) => getComments(context, ids)),
|
||||
getByQuery: (query) => getCommentsByQuery(context, query),
|
||||
getCountByQuery: (query) => getCommentCountByQuery(context, query),
|
||||
countByAssetID: new SharedCounterDataLoader('Comments.totalCommentCount', 3600, (ids) => getCountsByAssetID(context, ids)),
|
||||
|
||||
@@ -4,6 +4,14 @@ const Asset = {
|
||||
recentComments({id}, _, {loaders: {Comments}}) {
|
||||
return Comments.genRecentComments.load(id);
|
||||
},
|
||||
async comment({id}, {id: commentId}, {loaders: {Comments}}) {
|
||||
const comments = await Comments.getByQuery({
|
||||
asset_id: id,
|
||||
ids: commentId
|
||||
});
|
||||
|
||||
return comments.nodes[0];
|
||||
},
|
||||
comments({id}, {sort, limit, deep, excludeIgnored, tags}, {loaders: {Comments}}) {
|
||||
return Comments.getByQuery({
|
||||
asset_id: id,
|
||||
|
||||
@@ -133,7 +133,7 @@ const setupFunctions = plugins.get('server', 'setupFunctions').reduce((acc, {plu
|
||||
const createSubscriptionManager = (server) => new SubscriptionServer({
|
||||
subscriptionManager: new SubscriptionManager({
|
||||
schema,
|
||||
pubsub,
|
||||
pubsub: pubsub.createClient(),
|
||||
setupFunctions,
|
||||
}),
|
||||
onConnect: ({token}, connection) => {
|
||||
|
||||
@@ -580,6 +580,9 @@ type Asset {
|
||||
deep: Boolean,
|
||||
): CommentConnection!
|
||||
|
||||
# A Comment from the Asset by comment's ID
|
||||
comment(id: ID!): Comment
|
||||
|
||||
# The count of top level comments on the asset.
|
||||
commentCount(excludeIgnored: Boolean, tags: [String!]): Int
|
||||
|
||||
|
||||
+9
-5
@@ -1,14 +1,16 @@
|
||||
{
|
||||
"name": "talk",
|
||||
"version": "2.5.0",
|
||||
"version": "3.0.0",
|
||||
"description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"postinstall": "./bin/cli plugins reconcile --skip-remote",
|
||||
"start": "./bin/cli serve -j -w",
|
||||
"dev-start": "nodemon -w . -w bin/cli -w bin/cli-serve --config .nodemon.json --exec \"./bin/cli -c .env serve -j -w\"",
|
||||
"build": "NODE_ENV=production webpack -p --config webpack.config.js --bail",
|
||||
"build-watch": "NODE_ENV=development webpack --progress --config webpack.config.js --watch",
|
||||
"dev-start": "nodemon -w . -w bin/cli -w bin/cli-serve --config .nodemon.json --exec \"yarn generate-introspection && ./bin/cli -c .env serve -j -w\"",
|
||||
"prebuild": "yarn generate-introspection",
|
||||
"build": "WEBPACK=true NODE_ENV=production webpack -p --config webpack.config.js --bail",
|
||||
"prebuild-watch": "yarn generate-introspection",
|
||||
"build-watch": "WEBPACK=true NODE_ENV=development webpack --progress --config webpack.config.js --watch",
|
||||
"lint": "eslint bin/* .",
|
||||
"lint-fix": "eslint bin/* . --fix",
|
||||
"test": "TEST_MODE=unit NODE_ENV=test mocha -R ${MOCHA_REPORTER:-spec}",
|
||||
@@ -17,7 +19,8 @@
|
||||
"e2e": "NODE_ENV=test nightwatch",
|
||||
"poste2e": "NODE_ENV=test scripts/poste2e.sh",
|
||||
"embed-start": "NODE_ENV=development yarn build && ./bin/cli serve --jobs",
|
||||
"heroku-postbuild": "./bin/cli plugins reconcile && yarn build"
|
||||
"heroku-postbuild": "./bin/cli plugins reconcile && yarn build",
|
||||
"generate-introspection": "WEBPACK=true NODE_ENV=test ./scripts/generateIntrospectionResult.js"
|
||||
},
|
||||
"talk": {
|
||||
"migration": {
|
||||
@@ -75,6 +78,7 @@
|
||||
"dotenv": "^4.0.0",
|
||||
"ejs": "^2.5.6",
|
||||
"env-rewrite": "^1.0.2",
|
||||
"eventemitter2": "^4.1.2",
|
||||
"express": "^4.15.2",
|
||||
"express-session": "^1.15.1",
|
||||
"file-loader": "^0.11.2",
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
composes: buttonReset from "coral-framework/styles/reset.css";
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.list {
|
||||
background: white;
|
||||
position: absolute;
|
||||
@@ -29,3 +33,27 @@
|
||||
font-size: 14px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
||||
@custom-media --small-viewport (max-width: 425px);
|
||||
|
||||
.filterText {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.filterIcon {
|
||||
vertical-align: middle;
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (--small-viewport) {
|
||||
.filterText {
|
||||
display: none;
|
||||
}
|
||||
.filterIcon {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import styles from './ViewingOptions.css';
|
||||
import {Slot, ClickOutside} from 'plugin-api/beta/client/components';
|
||||
import {t} from 'plugin-api/beta/client/services';
|
||||
import {Icon} from 'plugin-api/beta/client/components/ui';
|
||||
import {Slot, ClickOutside} from 'plugin-api/beta/client/components';
|
||||
|
||||
const ViewingOptions = (props) => {
|
||||
const toggleOpen = () => {
|
||||
@@ -23,7 +24,9 @@ const ViewingOptions = (props) => {
|
||||
<ClickOutside onClickOutside={handleClickOutside}>
|
||||
<div className={cn([styles.root, 'coral-plugin-viewing-options'])}>
|
||||
<div>
|
||||
<button className={styles.button} onClick={toggleOpen}>Viewing Options
|
||||
<button className={styles.button} onClick={toggleOpen}>
|
||||
<Icon className={styles.filterIcon} name="filter_list" />
|
||||
<span className={styles.filterText}>{t('viewing_options')}</span>
|
||||
{props.open ? <Icon name="arrow_drop_up" className={styles.icon}/> : <Icon name="arrow_drop_down" className={styles.icon}/>}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
en:
|
||||
viewing_options: "Viewing Options"
|
||||
es:
|
||||
viewing_options: "Opciones de visualización"
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
<div onMouseEnter={this.showTooltip} onMouseLeave={this.hideTooltip} >
|
||||
<div className={styles.noSelect} onMouseEnter={this.showTooltip}
|
||||
onMouseLeave={this.hideTooltip} onTouchStart={this.showTooltip}
|
||||
onTouchEnd={this.hideTooltip}>
|
||||
{
|
||||
isTagged(this.props.comment.tags, 'FEATURED') ? (
|
||||
<span className={cn(styles.tag, {[styles.on]: tooltip})}>
|
||||
<span
|
||||
className={cn(styles.tag, styles.noSelect, {[styles.on]: tooltip})}>
|
||||
{t('talk-plugin-featured-comments.featured')}
|
||||
</span>
|
||||
) : null
|
||||
|
||||
@@ -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
|
||||
|
||||
+48
-60
@@ -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;
|
||||
|
||||
+84
-98
@@ -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;
|
||||
|
||||
@@ -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});
|
||||
});
|
||||
|
||||
+10
-2
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
+19
-34
@@ -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;
|
||||
|
||||
+129
-140
@@ -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;
|
||||
|
||||
Executable
+24
@@ -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);
|
||||
});
|
||||
+23
-25
@@ -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) => {
|
||||
|
||||
+27
-5
@@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
+6
-3
@@ -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}) {
|
||||
|
||||
|
||||
+22
-8
@@ -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;
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
+12
-1
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
+25
-10
@@ -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;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
+7
-4
@@ -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.
|
||||
|
||||
+8
-6
@@ -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);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const redis = require('../helpers/redis');
|
||||
const cache = require('../../services/cache');
|
||||
|
||||
beforeEach(() => redis.clearDB());
|
||||
beforeEach(() => Promise.all([redis.clearDB(), cache.init()]));
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user