Skip to content

Latest commit

 

History

History
451 lines (252 loc) · 9.01 KB

ssr-auth.md

File metadata and controls

451 lines (252 loc) · 9.01 KB

Wildcard API

 

SSR & Authentication

SSR (Server-Side Rendering) denotes the practice of rendering a page twice: once to HTML in Node.js and then again to the DOM in the browser.

⚠️ This document is meant for people that want to use Wildcard with SSR. If you don't know what SSR is then you are most likely not doing it. If you don't do SSR, you can use Wildcard without reading this document.

SSR works out of the box. But with one exception: when calling an endpoint in Node.js, you need to manually bind the context object.

For example:

// Node.js

const { server } = require('@wildcard-api/server');

// Our endpoint function `whoAmI` needs the name of the logged-in user.
server.whoAmI = async function() {
  const {user} = this;
  return 'You are '+user.name;
};
// Browser & Node.js (this code runs in the browser as well as in Node.js)

const { server } = require('@wildcard-api/client');

// Whether the code runs in the browser or in Node.js
function isNodejs() {
  return typeof window === "undefined";
}

// `req` is the HTTP request object provided by your server
// framework (Express/Koa/Hapi/...).
async function(req) {
  let {whoAmI} = server;

  if( isNodejs() ){
    // When we call `whoAmI` in Node.js we need to manually
    // provide any HTTP request information that `whoAmI` needs.

    // We provide information about the logged-in user to our
    // endpoint function `whoAmI` by binding `req.user`:
    const {user} = req;
    whoAmI = whoAmI.bind({user});
  }

  const userName = await whoAmI();
  console.log("I am "+userName);
}

We now dissect an authentication example in order to showcase and explain:

  • Where req comes from.
  • Why Wildcard requires you to manually provide req.

Feel free to skip the example dissection if you already know where to get req from — just remember to manually bind the context when doing SSR.

Example Dissection

Let's consider the endpoint whoAmI from above:

// Node.js

const { server } = require('@wildcard-api/server');

// Our endpoint function `whoAmI` needs the name of the logged-in user.
server.whoAmI = async function() {
  const {user} = this;
  return 'You are '+user.name;
};

And let's now dissect what happens when we call whoAmI in the browser:

// Browser

// We use the Wildcard client in the browser
const { server } = require('@wildcard-api/client');

(async () => {
  // Because we are on the browser, the Wildcard client makes an HTTP request to our Node.js server
  const userName = await server.whoAmI();

  console.log('Welcome to Wildcard, '+userName);
})();

The HTTP request that the Wildcard client made is handled by the Wildcard middleware:

// Node.js

const express = require('express');
const {wildcard} = require('@wildcard-api/server/express');

const app = express();

// Add the Wildcard middleware
app.use(wildcard(async req => {
  // The context object is available to endpoint functions as `this`.
  const context = {
    // Express authentication middlewares usually make user information available at `req.user`
    user: req.user,
  };
  return context;
}));

What happens here is:

  • We call whoAmI in the browser.
  • Wildcard makes an HTTP request to our Node.js server.
  • The Wildcard middleware is called.
  • Our context function adds req.user to context.
  • Wildcard binds context to our endpoint function whoAmI (in other words this===context in the whoAmI function).
  • The endpoint function whoAmI can access information about the logged-in user at this.user.

The key take away here is that it is the Wildcard middleware that provides req.user to our endpoint functions.

But when we call the endpoint whoAmI in Node.js, our endpoint function whoAmI is directly called: no HTTP request is made. This means that the Wildcard middleware is never called.

There is no way for Wildcard to get req.user — we have to manually bind() the req.user object:

// Node.js

// We use the Wildcard client in Node.js
const { server } = require('@wildcard-api/client');

module.exports = getGreeting;

async function getGreeting(
  // We see later where `req` comes from.
  req
) {

  // Because we are in Node.js, the Wildcard client directly calls
  // the endpoint function `whoAmI`. Wildcard doesn't have access to
  // `req.user` and we need to bind it ourselves:
  let whoAmI = server.whoAmI.bind({user: req.user});

  const userName = await whoAmI();

  return 'Welcome to Wildcard, '+userName;
}

You may wonder where req comes from. The req object should be provided by your SSR tool or, if you implemented SSR yourself, by your server framework (Express/Koa/Hapi/...) which we now showcase.

In a custom SSR implementation, you'd call getGreeting like this:

// Node.js

const express = require('express');
const React = require('react');
const ReactDOMServer = require('react-dom/server');
const getGreeting = require('../common/getGreeting');

const app = express();

app.get('/hello' , async (req, res) => {
  // The `req` object is provided by Express

  // We pass `req` to `getGreeting`
  const message = await getGreeting({req});

  res.send(`
    <html>
      <body>
        <div id='react-container'>${
          ReactDOMServer.renderToStaticMarkup(
            <div>{message}</div>
          )
        }</div>
      </body>
    </html>
  `);
});
// Browser

import React from 'react';
import ReactDOM from 'react-dom';
import getGreeting from '../common/getGreeting';

(async () => {
  // We don't have to pass `req` when calling `getGreeting` in the
  // browser: Wildcard is about to make an HTTP request to our
  // Node.js server and the Wildcard middleware will be called.
  // The middleware passes `context.user` to our endpoint function
  // `whoAmI`.
  const message = await getGreeting();

  ReactDOM.hydrate(
    <div>{message}</div>,
    document.getElementsById('react-container')
  );
})();

That way, whoAmI has always access to the req.user: when the client runs in the browser, this.user originates from the Wildcard middleware, and when the client runs in Node.js, this.user originates from our bind call.

The isomorphic (aka universal) usage of the Wildcard client looks this:

// Browser + Node.js

// /common/getGreeting.js

const { server } = require('@wildcard-api/client');
const assert = require('assert');

module.exports = getGreeting;

// Whether the code runs in the browser or in Node.js
function isNodejs() {
  return typeof window === "undefined";
}

async function getGreeting ({req}) {
  let {whoAmI} = server;

  if( isNodejs() ) {
    // We need `req` when calling the endpoint in Node.js.
    assert(req);

    // We use `Function.prototype.bind()` to make `req.user`
    // available to the `whoAmI` endpoint function.
    whoAmI = whoAmI.bind({user: req.user});
  } else {
    // When run in the browser, there is no HTTP request yet.
    assert(!req);
  }

  const userName = await whoAmI();

  return 'Welcome to Wildcard, '+userName;
};

Open a GitHub ticket if you have questions or something's not clear — we enjoy talking with our users.
TOP