mirror of
https://github.com/wassname/talk.git
synced 2026-07-09 09:20:38 +08:00
Merge branch 'master' into approved-comments-queue
This commit is contained in:
+2
-1
@@ -13,7 +13,8 @@ EXPOSE 5000
|
||||
COPY . /usr/src/app
|
||||
|
||||
# Install app dependencies and build static assets.
|
||||
RUN yarn install --frozen-lockfile && \
|
||||
RUN yarn global add node-gyp && \
|
||||
yarn install --frozen-lockfile && \
|
||||
cli plugins reconcile && \
|
||||
yarn build && \
|
||||
yarn install --production && \
|
||||
|
||||
+32
@@ -145,6 +145,10 @@ type RootMutation {
|
||||
type RootQuery {
|
||||
people: [Person!]
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
leader: Person
|
||||
}
|
||||
```
|
||||
|
||||
Thanks to [gql-merge](https://www.npmjs.com/package/gql-merge) the contents of
|
||||
@@ -259,6 +263,23 @@ If your post function accepts four parameters, then it can modify the field
|
||||
result. It is *required* that the function resolves a promise (or returns) with
|
||||
the modified value or simply the original if you didn't modify it.
|
||||
|
||||
#### Field: `setupFunctions`
|
||||
|
||||
```js
|
||||
setupFunctions: {
|
||||
leader: (options, args) => ({
|
||||
leader: {
|
||||
filter: (person) => person.place === 1
|
||||
},
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
Setup functions allow you to create filters that control which pubsub.publish() events
|
||||
send data to the client. If the type in question contains args, clients may subscribe using those arguments to further filter their subscription.
|
||||
|
||||
For more information, see the [Apollo Docs](https://github.com/apollographql/graphql-subscriptions).
|
||||
|
||||
#### Field: `router`
|
||||
|
||||
```js
|
||||
@@ -375,6 +396,10 @@ module.exports = {
|
||||
type RootQuery {
|
||||
people: [Person!]
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
leader: Person
|
||||
}
|
||||
`,
|
||||
context: {
|
||||
Slack: () => ({
|
||||
@@ -430,6 +455,13 @@ module.exports = {
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
setupFunctions: {
|
||||
leader: (options, args) => ({
|
||||
leader: {
|
||||
filter: (person) => person.place === 1
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -5,13 +5,11 @@ const path = require('path');
|
||||
const helmet = require('helmet');
|
||||
const {passport} = require('./services/passport');
|
||||
const plugins = require('./services/plugins');
|
||||
const session = require('express-session');
|
||||
const enabled = require('debug').enabled;
|
||||
const RedisStore = require('connect-redis')(session);
|
||||
const redis = require('./services/redis');
|
||||
const csrf = require('csurf');
|
||||
const errors = require('./errors');
|
||||
const graph = require('./graph');
|
||||
const session = require('./services/session');
|
||||
const {createGraphOptions} = require('./graph');
|
||||
const apollo = require('graphql-server-express');
|
||||
|
||||
const app = express();
|
||||
@@ -43,34 +41,7 @@ app.set('view engine', 'ejs');
|
||||
// SESSION MIDDLEWARE
|
||||
//==============================================================================
|
||||
|
||||
const session_opts = {
|
||||
secret: process.env.TALK_SESSION_SECRET,
|
||||
httpOnly: true,
|
||||
rolling: true,
|
||||
saveUninitialized: true,
|
||||
resave: true,
|
||||
unset: 'destroy',
|
||||
name: 'talk.sid',
|
||||
cookie: {
|
||||
secure: false,
|
||||
maxAge: 8.64e+7, // 24 hours for session token expiry
|
||||
},
|
||||
store: new RedisStore({
|
||||
client: redis.createClient(),
|
||||
})
|
||||
};
|
||||
|
||||
if (app.get('env') === 'production') {
|
||||
|
||||
// Enable the secure cookie when we are in production mode.
|
||||
session_opts.cookie.secure = true;
|
||||
} else if (app.get('env') === 'test') {
|
||||
|
||||
// Add in the secret during tests.
|
||||
session_opts.secret = 'keyboard cat';
|
||||
}
|
||||
|
||||
app.use(session(session_opts));
|
||||
app.use(session);
|
||||
|
||||
//==============================================================================
|
||||
// PASSPORT MIDDLEWARE
|
||||
@@ -96,7 +67,7 @@ app.use(passport.session());
|
||||
//==============================================================================
|
||||
|
||||
// GraphQL endpoint.
|
||||
app.use('/api/v1/graph/ql', apollo.graphqlExpress(graph.createGraphOptions));
|
||||
app.use('/api/v1/graph/ql', apollo.graphqlExpress(createGraphOptions));
|
||||
|
||||
// Only include the graphiql tool if we aren't in production mode.
|
||||
if (app.get('env') !== 'production') {
|
||||
|
||||
+20
-9
@@ -1,13 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const app = require('../app');
|
||||
const program = require('./commander');
|
||||
const http = require('http');
|
||||
const app = require('../app');
|
||||
const {createServer} = require('http');
|
||||
const scraper = require('../services/scraper');
|
||||
const mailer = require('../services/mailer');
|
||||
const kue = require('../services/kue');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const util = require('./util');
|
||||
const {createSubscriptionManager} = require('../graph/subscriptions');
|
||||
|
||||
/**
|
||||
* Get port from environment and store in Express.
|
||||
@@ -20,7 +21,7 @@ app.set('port', port);
|
||||
/**
|
||||
* Create HTTP server.
|
||||
*/
|
||||
const server = http.createServer(app);
|
||||
const server = createServer(app);
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "error" event.
|
||||
@@ -76,20 +77,29 @@ function normalizePort(val) {
|
||||
function onListening() {
|
||||
let addr = server.address();
|
||||
let bind = typeof addr === 'string'
|
||||
? `pipe ${ addr}`
|
||||
: `port ${ addr.port}`;
|
||||
console.log(`Listening on ${ bind}`);
|
||||
? `pipe ${addr}`
|
||||
: `port ${addr.port}`;
|
||||
console.log(`API Server Listening on ${bind}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the app.
|
||||
*/
|
||||
function startApp() {
|
||||
function startApp(program) {
|
||||
|
||||
/**
|
||||
* Listen on provided port, on all network interfaces.
|
||||
*/
|
||||
server.listen(port);
|
||||
server.listen(port, () => {
|
||||
|
||||
// Mount the websocket server if requested.
|
||||
if (program.websockets) {
|
||||
console.log(`Websocket Server Listening on ${port}`);
|
||||
|
||||
// Mount the subscriptions server on the application server.
|
||||
createSubscriptionManager(server);
|
||||
}
|
||||
});
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
}
|
||||
@@ -100,10 +110,11 @@ function startApp() {
|
||||
|
||||
program
|
||||
.option('-j, --jobs', 'enable job processing on this thread')
|
||||
.option('-w, --websockets', 'enable the websocket (subscriptions) handler on this thread')
|
||||
.parse(process.argv);
|
||||
|
||||
// Start the application serving.
|
||||
startApp();
|
||||
startApp(program);
|
||||
|
||||
// Enable job processing on the thread if enabled.
|
||||
if (program.jobs) {
|
||||
|
||||
@@ -28,6 +28,8 @@ const shortcuts = [
|
||||
export default class ModerationKeysModal extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
open: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
hideShortcutsNote: PropTypes.func.isRequired,
|
||||
shortcutsNoteVisible: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import {Logo} from './Logo';
|
||||
|
||||
const CoralHeader = ({handleLogout, restricted = false}) => (
|
||||
const CoralHeader = ({handleLogout, showShortcuts = () => {}, restricted = false}) => (
|
||||
<Header className={styles.header}>
|
||||
<Logo className={styles.logo} />
|
||||
{
|
||||
@@ -55,7 +55,8 @@ const CoralHeader = ({handleLogout, restricted = false}) => (
|
||||
<div>
|
||||
<IconButton name="settings" id="menu-settings"/>
|
||||
<Menu target="menu-settings" align="right">
|
||||
<MenuItem onClick={handleLogout}>Sign Out</MenuItem>
|
||||
<MenuItem onClick={() => showShortcuts(true)}>{lang.t('configure.shortcuts')}</MenuItem>
|
||||
<MenuItem onClick={handleLogout}>{lang.t('configure.sign-out')}</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</li>
|
||||
@@ -72,6 +73,7 @@ const CoralHeader = ({handleLogout, restricted = false}) => (
|
||||
);
|
||||
|
||||
CoralHeader.propTypes = {
|
||||
showShortcuts: PropTypes.func,
|
||||
handleLogout: PropTypes.func.isRequired,
|
||||
restricted: PropTypes.bool // hide elemnts from a user that's logged out
|
||||
};
|
||||
|
||||
@@ -4,9 +4,13 @@ import Header from './Header';
|
||||
import Drawer from './Drawer';
|
||||
import styles from './Layout.css';
|
||||
|
||||
const Layout = ({children, handleLogout = () => {}, restricted = false, ...props}) => (
|
||||
const Layout = ({children, handleLogout = () => {}, toggleShortcutModal, restricted = false, ...props}) => (
|
||||
<LayoutMDL fixedDrawer>
|
||||
<Header handleLogout={handleLogout} restricted={restricted} {...props} />
|
||||
<Header
|
||||
handleLogout={handleLogout}
|
||||
showShortcuts={toggleShortcutModal}
|
||||
restricted={restricted}
|
||||
{...props} />
|
||||
<Drawer handleLogout={handleLogout} restricted={restricted} {...props} />
|
||||
<div className={styles.layout}>
|
||||
{children}
|
||||
@@ -16,6 +20,7 @@ const Layout = ({children, handleLogout = () => {}, restricted = false, ...props
|
||||
|
||||
Layout.propTypes = {
|
||||
handleLogout: PropTypes.func,
|
||||
toggleShortcutModal: PropTypes.func,
|
||||
restricted: PropTypes.bool // hide elements from a user that's logged out
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import Layout from '../components/ui/Layout';
|
||||
import {checkLogin, handleLogin, logout, requestPasswordReset} from '../actions/auth';
|
||||
import {toggleModal as toggleShortcutModal} from '../actions/moderation';
|
||||
import {fetchConfig} from '../actions/config';
|
||||
import {FullLoading} from '../components/FullLoading';
|
||||
import AdminLogin from '../components/AdminLogin';
|
||||
@@ -23,7 +24,7 @@ class LayoutContainer extends Component {
|
||||
passwordRequestSuccess
|
||||
} = this.props.auth;
|
||||
|
||||
const {handleLogout, TALK_RECAPTCHA_PUBLIC} = this.props;
|
||||
const {handleLogout, toggleShortcutModal, TALK_RECAPTCHA_PUBLIC} = this.props;
|
||||
if (loadingUser) { return <FullLoading />; }
|
||||
if (!isAdmin) {
|
||||
return <AdminLogin
|
||||
@@ -34,7 +35,9 @@ class LayoutContainer extends Component {
|
||||
recaptchaPublic={TALK_RECAPTCHA_PUBLIC}
|
||||
errorMessage={loginError} />;
|
||||
}
|
||||
if (isAdmin && loggedIn) { return <Layout handleLogout={handleLogout} {...this.props} />; }
|
||||
if (isAdmin && loggedIn) {
|
||||
return <Layout handleLogout={handleLogout} toggleShortcutModal={toggleShortcutModal} {...this.props} />;
|
||||
}
|
||||
return <FullLoading />;
|
||||
}
|
||||
}
|
||||
@@ -49,6 +52,7 @@ const mapDispatchToProps = dispatch => ({
|
||||
fetchConfig: () => dispatch(fetchConfig()),
|
||||
handleLogin: (username, password, recaptchaResponse) => dispatch(handleLogin(username, password, recaptchaResponse)),
|
||||
requestPasswordReset: email => dispatch(requestPasswordReset(email)),
|
||||
toggleShortcutModal: toggle => dispatch(toggleShortcutModal(toggle)),
|
||||
handleLogout: () => dispatch(logout())
|
||||
});
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ const Comment = ({actions = [], comment, ...props}) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<li tabIndex={props.index} className={`mdl-card ${props.selected ? 'mdl-shadow--8dp' : 'mdl-shadow--2dp'} ${styles.Comment} ${styles.listItem}`}>
|
||||
<li tabIndex={props.index} className={`mdl-card ${props.selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp'} ${styles.Comment} ${styles.listItem} ${props.selected ? styles.selected : ''}`}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.itemHeader}>
|
||||
<div className={styles.author}>
|
||||
|
||||
@@ -7,6 +7,32 @@ const fm = new IntrospectionFragmentMatcher({
|
||||
introspectionQueryResultData: {
|
||||
__schema: {
|
||||
types: [
|
||||
{
|
||||
kind: 'INTERFACE',
|
||||
name: 'UserError',
|
||||
possibleTypes: [
|
||||
{name: 'GenericUserError'},
|
||||
{name: 'ValidationUserError'}
|
||||
]
|
||||
},
|
||||
{
|
||||
kind: 'INTERFACE',
|
||||
name: 'Response',
|
||||
possibleTypes: [
|
||||
{name: 'CreateCommentResponse'},
|
||||
{name: 'CreateLikeResponse'},
|
||||
{name: 'CreateFlagResponse'},
|
||||
{name: 'CreateDontAgreeResponse'},
|
||||
{name: 'DeleteActionResponse'},
|
||||
{name: 'SetUserStatusResponse'},
|
||||
{name: 'SuspendUserResponse'},
|
||||
{name: 'SetCommentStatusResponse'},
|
||||
{name: 'AddCommentTagResponse'},
|
||||
{name: 'RemoveCommentTagResponse'},
|
||||
{name: 'IgnoreUserResponse'},
|
||||
{name: 'StopIgnoringUserResponse'}
|
||||
]
|
||||
},
|
||||
{
|
||||
kind: 'INTERFACE',
|
||||
name: 'Action',
|
||||
@@ -24,6 +50,15 @@ const fm = new IntrospectionFragmentMatcher({
|
||||
{name: 'LikeActionSummary'},
|
||||
{name: 'DontAgreeActionSummary'}
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: 'INTERFACE',
|
||||
name: 'AssetActionSummary',
|
||||
possibleTypes: [
|
||||
{name: 'DefaultAssetActionSummary'},
|
||||
{name: 'FlagAssetActionSummary'},
|
||||
{name: 'LikeAssetActionSummary'}
|
||||
]
|
||||
}
|
||||
],
|
||||
},
|
||||
|
||||
@@ -80,6 +80,8 @@
|
||||
"copy": "Copy to Clipboard"
|
||||
},
|
||||
"configure": {
|
||||
"sign-out": "Sign Out",
|
||||
"shortcuts": "Shortcuts",
|
||||
"closed-stream-settings": "Closed Stream Message",
|
||||
"open-stream-configuration": "This comment stream is currently open. By closing this comment stream, no new comments may be submitted and all previous comments will still be displayed.",
|
||||
"close-stream-configuration": "This comment stream is currently closed. By opening this comment stream, new comments may be submitted and displayed",
|
||||
@@ -260,6 +262,8 @@
|
||||
"username_flags": "marcas para este nombre de usuario"
|
||||
},
|
||||
"configure": {
|
||||
"sign-out": "Desconectar",
|
||||
"shortcuts": "Atajos",
|
||||
"closed-stream-settings": "Mensaje a enviar cuando los comentarios están cerrados en el artículo",
|
||||
"open-stream-configuration": "Este hilo de comentarios esta abierto. Al cerrarlo, ningún nuevo comentario será publicado y todos los comentarios anteriores serán mostrados.",
|
||||
"close-stream-configuration": "Este hilo de comentario está en este momento cerrado. Al abrirlo, nuevos comentarios serán publicaods y mostrados.",
|
||||
|
||||
@@ -39,10 +39,21 @@ export const showSignInDialog = () => dispatch => {
|
||||
'menubar=0,resizable=0,width=500,height=550,top=200,left=500'
|
||||
);
|
||||
|
||||
signInPopUp.onbeforeunload = () => {
|
||||
dispatch(checkLogin());
|
||||
fetchMe();
|
||||
// Workaround odd behavior in older WebKit versions, where
|
||||
// onunload is called twice. (Encountered in IOS 8.3)
|
||||
let loaded = false;
|
||||
signInPopUp.onload = () => {
|
||||
loaded = true;
|
||||
};
|
||||
|
||||
// Use `onunload` instead of `onbeforeunload` which is not supported in IOS Safari.
|
||||
signInPopUp.onunload = () => {
|
||||
if (loaded) {
|
||||
dispatch(checkLogin());
|
||||
fetchMe();
|
||||
}
|
||||
};
|
||||
|
||||
dispatch({type: actions.SHOW_SIGNIN_DIALOG});
|
||||
};
|
||||
export const hideSignInDialog = () => dispatch => {
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
import ApolloClient, {addTypename} from 'apollo-client';
|
||||
import getNetworkInterface from './transport';
|
||||
|
||||
// import {SubscriptionClient, addGraphQLSubscriptions} from 'subscriptions-transport-ws';
|
||||
|
||||
// TODO: replace absolute reference with something loaded from the store/page.
|
||||
// const wsClient = new SubscriptionClient('ws://localhost:3000/api/v1/live', {
|
||||
// reconnect: true
|
||||
// });
|
||||
// const networkInterface = addGraphQLSubscriptions(
|
||||
// getNetworkInterface(),
|
||||
// wsClient,
|
||||
// );
|
||||
const networkInterface = getNetworkInterface();
|
||||
|
||||
export const client = new ApolloClient({
|
||||
connectToDevTools: true,
|
||||
queryTransformer: addTypename,
|
||||
@@ -10,7 +22,7 @@ export const client = new ApolloClient({
|
||||
}
|
||||
return null;
|
||||
},
|
||||
networkInterface: getNetworkInterface()
|
||||
networkInterface
|
||||
});
|
||||
|
||||
export default client;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"post": "Post",
|
||||
"cancel": "Cancel",
|
||||
"reply": "Reply",
|
||||
"comment": "Post a Comment",
|
||||
"comment": "Post a comment",
|
||||
"name": "Name",
|
||||
"comment-post-notif": "Your comment has been posted.",
|
||||
"comment-post-notif-premod": "Thank you for posting. Our moderation team will review your comment shortly.",
|
||||
@@ -14,7 +14,7 @@
|
||||
"post": "Publicar",
|
||||
"cancel": "Cancelar",
|
||||
"reply": "Responder",
|
||||
"comment": "Publicar un Comentario",
|
||||
"comment": "Publicar un comentario",
|
||||
"name": "Nombre",
|
||||
"comment-post-notif": "Tu comentario ha sido publicado.",
|
||||
"comment-post-notif-premod": "Gracias por el comentario. Nuestro equipo de moderación va a revisarlo muy pronto.",
|
||||
|
||||
+8
-1
@@ -1,5 +1,6 @@
|
||||
const loaders = require('./loaders');
|
||||
const mutators = require('./mutators');
|
||||
const uuid = require('uuid');
|
||||
|
||||
const plugins = require('../services/plugins');
|
||||
const debug = require('debug')('talk:graph:context');
|
||||
@@ -31,7 +32,10 @@ const decorateContextPlugins = (context, contextPlugins) => contextPlugins.reduc
|
||||
* Stores the request context.
|
||||
*/
|
||||
class Context {
|
||||
constructor({user = null}) {
|
||||
constructor({user = null}, pubsub) {
|
||||
|
||||
// Generate a new context id for the request.
|
||||
this.id = uuid.v4();
|
||||
|
||||
// Load the current logged in user to `user`, otherwise this'll be null.
|
||||
if (user) {
|
||||
@@ -46,6 +50,9 @@ class Context {
|
||||
|
||||
// Decorate the plugin context.
|
||||
this.plugins = decorateContextPlugins(this, contextPlugins);
|
||||
|
||||
// Bind the publish/subscribe to the context.
|
||||
this.pubsub = pubsub;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-2
@@ -1,5 +1,7 @@
|
||||
const schema = require('./schema');
|
||||
const Context = require('./context');
|
||||
const pubsub = require('./pubsub');
|
||||
const {createSubscriptionManager} = require('./subscriptions');
|
||||
|
||||
module.exports = {
|
||||
createGraphOptions: (req) => ({
|
||||
@@ -9,6 +11,7 @@ module.exports = {
|
||||
|
||||
// Load in the new context here, this'll create the loaders + mutators for
|
||||
// the lifespan of this request.
|
||||
context: new Context(req)
|
||||
})
|
||||
context: new Context(req, pubsub)
|
||||
}),
|
||||
createSubscriptionManager
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ const Wordlist = require('../../services/wordlist');
|
||||
* @param {String} [status='NONE'] the status of the new comment
|
||||
* @return {Promise} resolves to the created comment
|
||||
*/
|
||||
const createComment = ({user, loaders: {Comments}}, {body, asset_id, parent_id = null, tags = []}, status = 'NONE') => {
|
||||
const createComment = ({user, loaders: {Comments}, pubsub}, {body, asset_id, parent_id = null, tags = []}, status = 'NONE') => {
|
||||
|
||||
// Building array of tags
|
||||
tags = tags.map(tag => ({name: tag}));
|
||||
@@ -47,6 +47,12 @@ const createComment = ({user, loaders: {Comments}}, {body, asset_id, parent_id =
|
||||
Comments.parentCountByAssetID.incr(asset_id);
|
||||
}
|
||||
Comments.countByAssetID.incr(asset_id);
|
||||
|
||||
if (pubsub) {
|
||||
|
||||
// Publish the newly added comment via the subscription.
|
||||
pubsub.publish('commentAdded', comment);
|
||||
}
|
||||
}
|
||||
|
||||
return comment;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
const {RedisPubSub} = require('graphql-redis-subscriptions');
|
||||
|
||||
const {connectionOptions} = require('../services/redis');
|
||||
|
||||
module.exports = new RedisPubSub(connectionOptions);
|
||||
@@ -5,6 +5,10 @@ module.exports = new GraphQLScalarType({
|
||||
name: 'Date',
|
||||
description: 'Date represented as an ISO8601 string',
|
||||
serialize(value) {
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
|
||||
return value.toISOString();
|
||||
},
|
||||
parseValue(value) {
|
||||
|
||||
@@ -16,6 +16,7 @@ const LikeAction = require('./like_action');
|
||||
const RootMutation = require('./root_mutation');
|
||||
const RootQuery = require('./root_query');
|
||||
const Settings = require('./settings');
|
||||
const Subscription = require('./subscription');
|
||||
const UserError = require('./user_error');
|
||||
const User = require('./user');
|
||||
const ValidationUserError = require('./validation_user_error');
|
||||
@@ -39,6 +40,7 @@ let resolvers = {
|
||||
RootMutation,
|
||||
RootQuery,
|
||||
Settings,
|
||||
Subscription,
|
||||
UserError,
|
||||
User,
|
||||
ValidationUserError,
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
const Subscription = {
|
||||
commentAdded(comment) {
|
||||
return comment;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Subscription;
|
||||
@@ -0,0 +1,60 @@
|
||||
const {SubscriptionManager} = require('graphql-subscriptions');
|
||||
const {SubscriptionServer} = require('subscriptions-transport-ws');
|
||||
const _ = require('lodash');
|
||||
|
||||
const pubsub = require('./pubsub');
|
||||
const schema = require('./schema');
|
||||
const Context = require('./context');
|
||||
const plugins = require('../services/plugins');
|
||||
|
||||
const {deserializeUser} = require('../services/subscriptions');
|
||||
|
||||
// Core setup functions
|
||||
let setupFunctions = {
|
||||
commentAdded: (options, args) => ({
|
||||
commentAdded: {
|
||||
filter: (comment) => comment.asset_id === args.asset_id
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
/**
|
||||
* Plugin support requires that we merge in existing setupFunctions with our new
|
||||
* plugin based ones. This allows plugins to extend existing setupFunctions as well
|
||||
* as provide new ones.
|
||||
*/
|
||||
setupFunctions = plugins.get('server', 'setupFunctions').reduce((acc, {setupFunctions}) => {
|
||||
|
||||
return _.merge(acc, setupFunctions);
|
||||
}, setupFunctions);
|
||||
|
||||
/**
|
||||
* This creates a new subscription manager.
|
||||
*/
|
||||
const createSubscriptionManager = (server) => new SubscriptionServer({
|
||||
subscriptionManager: new SubscriptionManager({
|
||||
schema,
|
||||
pubsub,
|
||||
setupFunctions,
|
||||
}),
|
||||
onSubscribe: (parsedMessage, baseParams, connection) => {
|
||||
|
||||
// Attach the context per request.
|
||||
baseParams.context = () => deserializeUser(connection.upgradeReq)
|
||||
.then((req) => new Context(req, pubsub))
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
|
||||
return new Context({}, pubsub);
|
||||
});
|
||||
|
||||
return baseParams;
|
||||
}
|
||||
}, {
|
||||
server,
|
||||
path: '/api/v1/live'
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
createSubscriptionManager
|
||||
};
|
||||
@@ -820,6 +820,14 @@ type RootMutation {
|
||||
stopIgnoringUser(id: ID!): StopIgnoringUserResponse
|
||||
}
|
||||
|
||||
################################################################################
|
||||
## Subscriptions
|
||||
################################################################################
|
||||
|
||||
type Subscription {
|
||||
commentAdded(asset_id: ID!): Comment
|
||||
}
|
||||
|
||||
################################################################################
|
||||
## Schema
|
||||
################################################################################
|
||||
@@ -827,4 +835,5 @@ type RootMutation {
|
||||
schema {
|
||||
query: RootQuery
|
||||
mutation: RootMutation
|
||||
subscription: Subscription
|
||||
}
|
||||
|
||||
+7
-1
@@ -123,7 +123,13 @@ const UserSchema = new mongoose.Schema({
|
||||
|
||||
// user id of another user
|
||||
type: String,
|
||||
}]
|
||||
}],
|
||||
|
||||
// Additional metadata stored on the field.
|
||||
metadata: {
|
||||
default: {},
|
||||
type: Object
|
||||
}
|
||||
}, {
|
||||
|
||||
// This will ensure that we have proper timestamps available on this model.
|
||||
|
||||
+12
-9
@@ -5,8 +5,8 @@
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"postinstall": "./bin/cli plugins reconcile --skip-remote",
|
||||
"start": "./bin/cli serve --jobs",
|
||||
"dev-start": "nodemon --config .nodemon.json --exec \"./bin/cli -c .env serve --jobs\"",
|
||||
"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",
|
||||
"lint": "eslint bin/* .",
|
||||
@@ -68,10 +68,12 @@
|
||||
"express-session": "^1.15.1",
|
||||
"form-data": "^2.1.2",
|
||||
"gql-merge": "^0.0.4",
|
||||
"graphql": "^0.8.2",
|
||||
"graphql": "^0.9.1",
|
||||
"graphql-errors": "^2.1.0",
|
||||
"graphql-server-express": "^0.5.0",
|
||||
"graphql-tools": "^0.9.0",
|
||||
"graphql-redis-subscriptions": "^1.1.5",
|
||||
"graphql-server-express": "^0.6.0",
|
||||
"graphql-subscriptions": "^0.3.1",
|
||||
"graphql-tools": "^0.10.1",
|
||||
"helmet": "^3.5.0",
|
||||
"inquirer": "^3.0.6",
|
||||
"joi": "^10.4.1",
|
||||
@@ -84,7 +86,7 @@
|
||||
"minimist": "^1.2.0",
|
||||
"mongoose": "^4.9.1",
|
||||
"morgan": "^1.8.1",
|
||||
"natural": "^0.4.0",
|
||||
"natural": "^0.5.0",
|
||||
"node-emoji": "^1.5.1",
|
||||
"node-fetch": "^1.6.3",
|
||||
"nodemailer": "^2.6.4",
|
||||
@@ -95,10 +97,10 @@
|
||||
"react-apollo": "^1.1.0",
|
||||
"react-recaptcha": "^2.2.6",
|
||||
"redis": "^2.7.1",
|
||||
"resolve": "^1.3.2",
|
||||
"semver": "^5.3.0",
|
||||
"uuid": "^3.0.1",
|
||||
"simplemde": "^1.11.2",
|
||||
"uuid": "^2.0.3"
|
||||
"resolve": "^1.3.2",
|
||||
"semver": "^5.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"apollo-client": "^1.0.4",
|
||||
@@ -177,6 +179,7 @@
|
||||
"redux-thunk": "^2.1.0",
|
||||
"regenerator": "^0.8.46",
|
||||
"selenium-standalone": "^5.11.2",
|
||||
"subscriptions-transport-ws": "^0.5.5-alpha.0",
|
||||
"style-loader": "^0.16.0",
|
||||
"supertest": "^2.0.1",
|
||||
"timeago.js": "^2.0.3",
|
||||
|
||||
+27
-23
@@ -2,31 +2,35 @@ const redis = require('redis');
|
||||
const debug = require('debug')('talk:redis');
|
||||
const url = process.env.TALK_REDIS_URL || 'redis://localhost';
|
||||
|
||||
const connectionOptions = {
|
||||
url,
|
||||
retry_strategy: function(options) {
|
||||
if (options.error && options.error.code === 'ECONNREFUSED') {
|
||||
|
||||
// End reconnecting on a specific error and flush all commands with a individual error
|
||||
return new Error('The server refused the connection');
|
||||
}
|
||||
if (options.total_retry_time > 1000 * 60 * 60) {
|
||||
|
||||
// End reconnecting after a specific timeout and flush all commands with a individual error
|
||||
return new Error('Retry time exhausted');
|
||||
}
|
||||
|
||||
if (options.times_connected > 10) {
|
||||
|
||||
// End reconnecting with built in error
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// reconnect after
|
||||
return Math.max(options.attempt * 100, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
connectionOptions,
|
||||
createClient() {
|
||||
let client = redis.createClient(url, {
|
||||
retry_strategy: function(options) {
|
||||
if (options.error && options.error.code === 'ECONNREFUSED') {
|
||||
|
||||
// End reconnecting on a specific error and flush all commands with a individual error
|
||||
return new Error('The server refused the connection');
|
||||
}
|
||||
if (options.total_retry_time > 1000 * 60 * 60) {
|
||||
|
||||
// End reconnecting after a specific timeout and flush all commands with a individual error
|
||||
return new Error('Retry time exhausted');
|
||||
}
|
||||
|
||||
if (options.times_connected > 10) {
|
||||
|
||||
// End reconnecting with built in error
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// reconnect after
|
||||
return Math.max(options.attempt * 100, 3000);
|
||||
}
|
||||
});
|
||||
let client = redis.createClient(connectionOptions);
|
||||
|
||||
client.ping((err) => {
|
||||
if (err) {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
const session = require('express-session');
|
||||
const RedisStore = require('connect-redis')(session);
|
||||
const redis = require('./redis');
|
||||
|
||||
//==============================================================================
|
||||
// SESSION MIDDLEWARE
|
||||
//==============================================================================
|
||||
|
||||
const session_opts = {
|
||||
secret: process.env.TALK_SESSION_SECRET,
|
||||
httpOnly: true,
|
||||
rolling: true,
|
||||
saveUninitialized: true,
|
||||
resave: true,
|
||||
unset: 'destroy',
|
||||
name: 'talk.sid',
|
||||
cookie: {
|
||||
secure: false,
|
||||
maxAge: 8.64e+7, // 24 hours for session token expiry
|
||||
},
|
||||
store: new RedisStore({
|
||||
client: redis.createClient(),
|
||||
})
|
||||
};
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
|
||||
// Enable the secure cookie when we are in production mode.
|
||||
session_opts.cookie.secure = true;
|
||||
} else if (process.env.NODE_ENV === 'test') {
|
||||
|
||||
// Add in the secret during tests.
|
||||
session_opts.secret = 'keyboard cat';
|
||||
}
|
||||
|
||||
module.exports = session(session_opts);
|
||||
@@ -0,0 +1,36 @@
|
||||
const session = require('./session');
|
||||
const passport = require('./passport');
|
||||
|
||||
// Session data does not automatically attach to websocket req objects.
|
||||
// This middleware code looks for a user in the session and, if it exists,
|
||||
// attaches it to the graph req.
|
||||
const deserializeUser = (req) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
session(req, {}, () => {
|
||||
|
||||
if ('session' in req && 'passport' in req.session && 'user' in req.session.passport) {
|
||||
passport.deserializeUser(req.session.passport.user, (err, user) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
req.user = user;
|
||||
|
||||
return resolve(req);
|
||||
});
|
||||
}
|
||||
|
||||
// Remove the user from the request (if there was one)
|
||||
if (req.user) {
|
||||
delete req.user;
|
||||
}
|
||||
|
||||
// Resolve with the request (user removed possibly).
|
||||
return resolve(req);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
deserializeUser
|
||||
};
|
||||
Reference in New Issue
Block a user