diff --git a/app.js b/app.js index 2bf7e93fe..2d679b0f7 100644 --- a/app.js +++ b/app.js @@ -4,13 +4,11 @@ const morgan = require('morgan'); const path = require('path'); const helmet = require('helmet'); const passport = require('./services/passport'); -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(); @@ -42,34 +40,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 @@ -83,10 +54,8 @@ app.use(passport.session()); // GraphQL Router //============================================================================== -graph.createSubscriptionManager(app, '/api/v1/live'); - // 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') { diff --git a/bin/cli-serve b/bin/cli-serve index ae5da3d3b..eef6526bd 100755 --- a/bin/cli-serve +++ b/bin/cli-serve @@ -1,14 +1,14 @@ #!/usr/bin/env node -const app = require('../app'); const program = require('./commander'); +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'); +const subscriptions = require('../services/subscriptions'); /** * Get port from environment and store in Express. @@ -91,7 +91,9 @@ function startApp() { * Listen on provided port, on all network interfaces. */ server.listen(port, () => { - createSubscriptionManager(server, '/api/v1/live'); + + // Mount the subscriptions server on the application server. + subscriptions.mount(server); }); server.on('error', onError); diff --git a/client/coral-embed-live-stream/src/Embed.js b/client/coral-embed-live-stream/src/Embed.js index 9301e2a21..6be149007 100644 --- a/client/coral-embed-live-stream/src/Embed.js +++ b/client/coral-embed-live-stream/src/Embed.js @@ -3,6 +3,8 @@ import React from 'react'; import {graphql} from 'react-apollo'; import gql from 'graphql-tag'; +import PubDate from 'coral-plugin-pubdate/PubDate'; + class Embed extends React.Component { static propTypes = { @@ -20,18 +22,24 @@ class Embed extends React.Component { } render() { - console.log(this.props); const {asset, loading} = this.props.data; return (
-

Live Stream

- +
); } @@ -40,12 +48,13 @@ class Embed extends React.Component { const COMMENT_QUERY = gql` query Comment($assetID: ID!) { asset(id: $assetID) { - comments { + comments(limit: 50) { id body user { username } + created_at } } } @@ -59,6 +68,7 @@ const COMMENTS_SUBSCRIPTION = gql` user { username } + created_at } } `; @@ -95,8 +105,6 @@ const withData = graphql(COMMENT_QUERY, { } }; - console.log('updateQuery', before, after); - return after; } }); diff --git a/client/coral-embed-live-stream/src/index.js b/client/coral-embed-live-stream/src/index.js index c546ccd2e..564ea0ed6 100644 --- a/client/coral-embed-live-stream/src/index.js +++ b/client/coral-embed-live-stream/src/index.js @@ -10,7 +10,7 @@ import Embed from './Embed'; render( - + , document.querySelector('#coralStream') ); diff --git a/client/coral-framework/services/client.js b/client/coral-framework/services/client.js index d1974df96..7744ea9bd 100644 --- a/client/coral-framework/services/client.js +++ b/client/coral-framework/services/client.js @@ -2,7 +2,7 @@ import ApolloClient, {addTypename} from 'apollo-client'; import getNetworkInterface from './transport'; import {SubscriptionClient, addGraphQLSubscriptions} from 'subscriptions-transport-ws'; -const wsClient = new SubscriptionClient('ws://localhost:3001/api/v1/live', { +const wsClient = new SubscriptionClient('ws://localhost:3000/api/v1/live', { reconnect: true }); const networkInterface = getNetworkInterface(); diff --git a/graph/context.js b/graph/context.js index 3d2e13991..ce66c4bcd 100644 --- a/graph/context.js +++ b/graph/context.js @@ -1,5 +1,6 @@ const loaders = require('./loaders'); const mutators = require('./mutators'); +const uuid = require('uuid'); const plugins = require('../plugins'); const debug = require('debug')('talk:graph:context'); @@ -33,6 +34,9 @@ const decorateContextPlugins = (context, contextPlugins) => contextPlugins.reduc class Context { 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) { this.user = user; diff --git a/graph/index.js b/graph/index.js index b690ec22c..78d9edc9a 100644 --- a/graph/index.js +++ b/graph/index.js @@ -19,7 +19,7 @@ module.exports = { // the lifespan of this request. context: new Context(req, pubsub) }), - createSubscriptionManager: (server, path) => new SubscriptionServer({ + createSubscriptionManager: (server, path, sessionFactory) => new SubscriptionServer({ subscriptionManager: new SubscriptionManager({ schema, pubsub, @@ -31,9 +31,19 @@ module.exports = { }), } }), - // onConnect: (connectionParams, webSocket) => { - // console.log(webSocket.upgradeReq.headers); - // } + onSubscribe: (parsedMessage, baseParams, connection) => { + + // Attach the context per request. + baseParams.context = () => sessionFactory(connection.upgradeReq) + .then((req) => new Context(req, pubsub)) + .catch((err) => { + console.error(err); + + return new Context({}, pubsub); + }); + + return baseParams; + } }, { server, path diff --git a/graph/resolvers/date.js b/graph/resolvers/date.js index 2335b9713..af79e3eb4 100644 --- a/graph/resolvers/date.js +++ b/graph/resolvers/date.js @@ -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) { diff --git a/services/session.js b/services/session.js new file mode 100644 index 000000000..5697e026d --- /dev/null +++ b/services/session.js @@ -0,0 +1,36 @@ +const session = require('express-session'); +const RedisStore = require('connect-redis')(session); +const redis = require('./services/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); diff --git a/services/subscriptions.js b/services/subscriptions.js new file mode 100644 index 000000000..b24d6d69b --- /dev/null +++ b/services/subscriptions.js @@ -0,0 +1,35 @@ +const {createSubscriptionManager} = require('../graph'); +const session = require('./session'); +const passport = require('./passport'); + +module.exports = class Subscriptions { + + static 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); + }); + } else { + resolve(req); + } + + }); + }); + } + + static mount(server) { + + // Create the SubscriptionManager and mount it on the specified route with + // this deserializer. + createSubscriptionManager(server, '/api/v1/live', Subscriptions.deserializeUser); + } +}; diff --git a/yarn.lock b/yarn.lock index 8f7116ad3..9da6badee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4251,10 +4251,6 @@ jsbn@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.0.tgz#650987da0dd74f4ebf5a11377a2aa2d273e97dfd" -jsdom-global@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/jsdom-global/-/jsdom-global-2.1.1.tgz#47d46fe77f6167baf5d34431d3bb59fc41b0915a" - jsdom@^7.0.2: version "7.2.2" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-7.2.2.tgz#40b402770c2bda23469096bee91ab675e3b1fc6e" @@ -4275,7 +4271,7 @@ jsdom@^7.0.2: whatwg-url-compat "~0.6.5" xml-name-validator ">= 2.0.1 < 3.0.0" -jsdom@^9.12.0: +jsdom@^9.8.3: version "9.12.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-9.12.0.tgz#e8c546fffcb06c00d4833ca84410fed7f8a097d4" dependencies: