Skip to content

Instantly share code, notes, and snippets.

@voodooattack
Last active September 24, 2017 21:29
Show Gist options
  • Save voodooattack/7a02881b0c762630160424f742b6f780 to your computer and use it in GitHub Desktop.
Save voodooattack/7a02881b0c762630160424f742b6f780 to your computer and use it in GitHub Desktop.
A server-side session API for Vulcan.js.

webtoken-session

A server-side session API for Vulcan.js.

This meteor package provides a session object on the Vulcan.js render context.

The session is stored inside a jsonwebtoken cookie on the client and is automatically saved at the end of each request. (including GraphQL queries)

Particularly useful when used in conjunction with internal-graphql*, which makes GraphQL requests internal to the server process.

Settings:

To configure the package, store your configuration under the webtoken-session field in settings.json.

{
  "webtoken-session": {
    "name": "session", // The name of the cookie issued to the browser.
    "secret": "very_secret", // The secret used to sign the jsonwebtoken.
    "silentErrors": true, // Never throw verification errors, token expiry errors are never thrown in both cases.  
    "verifyOptions": {}, // Webtoken verification options. See: https://github.com/auth0/node-jsonwebtoken#jwtverifytoken-secretorpublickey-options-callback 
    "signOptions": {}, // Webtoken signing options. See: https://github.com/auth0/node-jsonwebtoken#jwtsignpayload-secretorprivatekey-options-callback
    "cookieOptions": {} // res.cookie() options. See: http://expressjs.com/en/api.html#res.cookie
  }
}

* - Note:

Access to the render context inside resolvers may require a small edit to Vulcan.js.

This is using version 1.7.0.

Simply add this line:

  options.context.getRenderContext = () => req.renderContext;

Inside the file packages/vulcan-lib/lib/server/apollo_server.js, so that it looks like this:

    // merge with custom context
    options.context = deepmerge(options.context, GraphQLSchema.context);
    
    //////////////////////////////////////////////////////////////
    options.context.getRenderContext = () => req.renderContext; // !! Provide access to the render context. !!
    //////////////////////////////////////////////////////////////
    
    // go over context and add Dataloader to each collection
    Collections.forEach(collection => { ...

Then you can use it like this:

Inside middleware:

withRenderContextEnvironment(async function (renderContext, req, res, next) {
  if (!renderContext.session.locale) {
    // A locale has not been negotiated yet, negotiate one.
    try {
      renderContext.session.locale = req.acceptsLanguages(...localeManager.availableLocales);
    } catch (e) {
      return next(e);
    }
  }
  next();
}, { order: 22, name: 'intl-negotiation-middleware' });

And here's a usage example inside resolvers:

GraphQLSchema.addResolvers({
  Query: {
    /**
    * Format a message based on the supplied locale.
    * If no locale is explicitly supplied, the current locale will be used.
    */
    formatMessage(root, { locale, message, variables }, context) {
      const rc = context.getRenderContext();
      locale = locale || rc.session.locale;
      return context.localeManager.format(locale, message, variables);
    }
  },
  Mutation: {
    /**
    * This mutation switches the active site locale. It will persist the new locale in the session object.
    * A new cookie will be returned to the client with the GraphQL server's response, and all subsequent 
    * requests will have their `session.locale` adjusted.
    */
    switchLocale(root, { localeCode }, context) {
      const rc = context.getRenderContext();
      rc.session.locale = localeCode;
      return context.locale = localeCode;
    }
  }
});
Package.describe({
name: 'webtoken-session',
version: '0.0.1',
// Brief, one-line summary of the package.
summary: 'Provides a session object on the Vulcan.js render context.',
// URL to the Git repository containing the source code for this package.
git: '',
// By default, Meteor will default to using README.md for documentation.
// To avoid submitting documentation, set this field to null.
documentation: 'README.md'
});
Npm.depends({
'jsonwebtoken': '8.0.1',
'deepmerge': '1.2.0'
});
Package.onUse(function(api) {
api.versionsFrom('1.5.1');
api.use([
'ecmascript',
'vulcan:lib'
]);
api.mainModule('webtoken-session.js', ['server']);
});
import { withRenderContextEnvironment, getSetting } from 'meteor/vulcan:lib';
import { verify, sign } from 'jsonwebtoken';
import deepmerge from 'deepmerge';
const SETTINGS_KEY = 'webtoken-session';
withRenderContextEnvironment(function (context, req, res, next) {
try {
context.req = req;
context.session = context.session || {};
let settings = {
name: 'session',
secret: 'very_secret',
silentErrors: true,
verifyOptions: {},
signOptions: {},
cookieOptions: {}
};
settings = deepmerge(settings, getSetting(SETTINGS_KEY, {}));
const token = req.cookies[ settings.name ];
if (token) {
try {
context.session = deepmerge(context.session, verify(token, settings.secret, settings.verifyOptions));
} catch (e) {
if (e.message !== "jwt expired" && !settings.silentErrors) {
return next(e);
}
}
}
let sent = false;
const sendCookie = function sendCookie(cb) {
if (!sent) {
sent = true;
try {
const outputToken = sign(context.session, settings.secret, settings.signOptions);
if (context.session && Object.keys(context.session).length) {
res.cookie(settings.name, outputToken, settings.cookieOptions);
} else {
res.cookie(settings.name, '', settings.cookieOptions);
}
// eslint-disable-next-line no-empty
} catch (e) {
}
}
return cb.apply(this, [...arguments].slice(1));
};
res.writeHead = sendCookie.bind(res, res.writeHead);
res.status = sendCookie.bind(res, res.status);
res.send = sendCookie.bind(res, res.send);
res.end = sendCookie.bind(res, res.end);
} catch(e) {
return next(e);
}
next();
}, { order: 21, name: 'webtoken-session-middleware' });
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment