moro-programmer
1/14/2016 - 8:44 AM

Auth example (react + redux + react-router)

Auth example (react + redux + react-router)

// ...

import { createRoutes } from './routes';

import createRedux from './redux/createRedux';
import fetchComponentsData from './core/fetchComponentsData';
import renderTemplate from './core/renderTemplate';
import { NotAuthorizedException, AccessDeniedException } from './core/auth-helpers.js';
import bindCheckAuth from './core/bindCheckAuth';


// ...
// server configuration
// ...

server.get('*', (req, res, next) => {
  try {

    const redux = createRedux();
    const requireAccess = bindCheckAuth(redux, (nextState) => {
      throw new NotAuthorizedException('/login?next=' + nextState.location.pathname);
    }, (nextState) => {
      throw new AccessDeniedException('/403?next=' + nextState.location.pathname);
    });
    const routes = createRoutes(requireAccess);
    const location = new Location(req.path, req.query);

    Router.run(routes, location, async (error, initialState) => {
      try {
        const state = await fetchComponentsData(initialState.components, redux);
        const html = renderTemplate(redux, state, initialState, location);
        res.send(html).end();

      } catch (err) {
        res.status(500).send(err.stack);
        next(err);
      }
    });

  } catch (err) {
    // refactoring needed
    if (err instanceof NotAuthorizedException) {
      res.set('Content-Type', 'text/html');
      res.status(401).send('<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=' + err.redirectTo + '"></head></html>');
    } else if (err instanceof AccessDeniedException) {
      res.set('Content-Type', 'text/html');
      res.status(403).send('<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=' + err.redirectTo + '"></head></html>');
    } else {
      res.status(500).send(err.stack);
      next(err);
    }
  }
});
import React from 'react';
import { Router, Route, DefaultRoute, Redirect } from 'react-router'; // eslint-disable-line no-unused-vars

import { accessEquals } from './core/authAccessLevels.js';
import { accessLevels } from './access';

import App from './containers/App';
import HomePage from './containers/HomePage';
import InfoPage from './containers/InfoPage';
import LoginPage from './containers/LoginPage';
import AccessDeniedPage from './containers/AccessDeniedPage';

export const createRoutes = (requireAccess) => {
  return (
    <Route name="app" component={App}>
      <Route name="home" path="/" component={HomePage} onEnter={requireAccess(accessLevels.user)}/>
      <Route name="info" path="/info" component={InfoPage} onEnter={requireAccess(accessLevels.user)}/>
      <Route name="login" path="/login" component={LoginPage} onEnter={requireAccess(accessLevels.anon, accessEquals)}/>
      <Route name="access-denied" path="/403" component={AccessDeniedPage} onEnter={requireAccess(accessLevels.public)}/>
    </Route>
  );
};
import { isAuthorized } from '../stores/auth';
import { checkAccess } from './auth-helpers';

/**
 * Creates requireAccess function and binds it to redux.
 *
 * @param redux Redux instance
 * @param {Function} notAuthorizedHandler called when access is denied and user is not authorized (eq 401 code)
 * @param {Function} accessDeniedHandler called when access is denied for current user (eq 403 code)
 * @returns {Function} Return function with signature requireAuth(accessLevel, [checkAccessHandler]).
 *  checkAccessHandler is optional, by default checkAccessHandler = checkAccess (from access-helpers.js)
 */
export default function bindCheckAuth(redux, notAuthorizedHandler, accessDeniedHandler) {
  return (accessLevel, checkAccessHandler = checkAccess) => (nextState, transition) => {
    const state = redux.getState().auth;
    const currentAccessLvl = state.accessLvl;

    if (checkAccessHandler(accessLevel, currentAccessLvl)) {
      // Access granted
      return;
    }

    if (!isAuthorized(state)) {
      notAuthorizedHandler(nextState, transition);
      return;
    }

    accessDeniedHandler(nextState, transition);
  };
}
import { createActions, asyncAction } from '../redux/helpers.js';
import { userRoles } from '../access';

export const AuthActions = createActions({

  @asyncAction()
  authenticate(login, pass) {
    // success authentication mock - just for example :)
    const promise =
        new Promise((resolve) => {
          setTimeout(() => {
            resolve({accessLvl: userRoles.user});
          }, 1000);
        })
    ;
    return promise;
  },

});
import { createStore, getActionIds } from '../redux/helpers.js';
import { AuthActions } from '../actions/AuthActions';
import { userRoles, accessLevels } from '../access';

const actions = getActionIds(AuthActions);
const initialState = {
  accessLvl: userRoles.public
};

export const auth = createStore(initialState, {

  [actions.authenticate.success]: (state, action) => {
    return {
      ...state,
      accessLvl: action.result.accessLvl
    };
  },

});

export function isAuthorized(state) {
  return state.accessLvl.bitMask !== accessLevels.anon.bitMask;
}
import _ from 'lodash';
import invariant from 'react/lib/invariant';


export function checkAccess(requiredLevel, currentLevel) {
  return !!(requiredLevel.bitMask & currentLevel.bitMask);
}

export function accessEquals(requiredLevel, currentLevel) {
  return requiredLevel.bitMask === currentLevel.bitMask;
}

export class NotAuthorizedException {
  constructor(to = '/login') {
    this.redirectTo = to;
  }
}

export class AccessDeniedException {
  constructor(to = '/403') {
    this.redirectTo = to;
  }
}

/*
 Method to build a distinct bit mask for each role
 It starts off with "1" and shifts the bit to the left for each element in the
 roles array parameter
 */
export function buildRoles(roles) {
  let bitMask = '01';

  invariant(
    roles.length <= 31,
    'You have too many roles!' +
    'Max=31 before the bit shift pushes the accompanying integer out of the memory footprint for an integer'
  );

  // dbg
  const userRoles = _.reduce(roles, (result, role) => {
    const intCode = parseInt(bitMask, 2);
    result[role] = {
      bitMask: intCode,
      title: role
    };
    bitMask = (intCode << 1 ).toString(2);
    return result;
  }, {});

  return userRoles;
}

/*
 This method builds access level bit masks based on the accessLevelDeclaration parameter which must
 contain an array for each access level containing the allowed user roles.
 */
export function buildAccessLevels(accessLevelDeclarations, userRoles) {

  /*
    Zero step - transform
    { level1Name: level1, level2Name: level2 } object
     =>
    [ { name: level1Name, level: level1 }, { name: level2Name, level: level2 } ] array
   */
  const declarationsArr = _.map(accessLevelDeclarations, (level, name) => ({ name, level }));

  /*
    First step: filter access levels like:
     'public': '*',
    That means every user role enabled, so bitMask => sum of all bit masks
   */
  let accessLevels = _
      .filter(declarationsArr, ({ level }) => typeof level === 'string') // eslint-disable-line no-shadow
      .reduce((result, { level, name }) => { // eslint-disable-line no-shadow

        invariant(
          level === '*',
          'Access Control Error: Could not parse "' + level + '" as access definition for level "' + name + '"'
        );

        const resultBitMask = _.reduce(userRoles, (result) => result + '1', ''); // eslint-disable-line no-shadow

        result[name] = {
          bitMask: parseInt(resultBitMask, 2)
        };

        return result;
      }, {})
  ;

  /*
    Second step: filter access levels like:
     'user': ['user', 'admin'],
    That means we need to iterate on ['user', 'admin'] array and summ bit mask for 'user' and 'admin'
   */
  accessLevels = _
    .filter(declarationsArr, ({ level }) => typeof level !== 'string') // eslint-disable-line no-shadow
    .reduce((result, { level, name }) => { // eslint-disable-line no-shadow
      const levelName = name;
      const levelsArr = level;
      const resultBitMask = _.reduce(levelsArr, (resultBitMask, roleName) => { // eslint-disable-line no-shadow

        invariant(
          userRoles.hasOwnProperty(roleName) === true,
          'Access Control Error: Could not find role "' + roleName + '" in registered roles while building access for "' + levelName + '"'
        );

        return resultBitMask | userRoles[roleName].bitMask;
      }, 0);


      result[name] = {
        bitMask: resultBitMask
      };
      return result;
    }, accessLevels)
  ;
  return accessLevels;
}
import 'babel/polyfill';
import React from 'react';
import { Router } from 'react-router';
import BrowserHistory from 'react-router/lib/BrowserHistory';
import { Provider } from 'redux/react';

import createRedux from './redux/createRedux';
import { createRoutes } from './routes';
import getInitialState from './core/getInitialState';
import bindCheckAuth from './core/bindCheckAuth';

function run() {

  const reactRoot = window.document.getElementById('app');
  const state = getInitialState('#__INITIAL_STATE__');
  const redux = createRedux(state);
  const requireAccess = bindCheckAuth(redux, (nextState, transition) => {
    transition.to('/login', {
      next: nextState.location.pathname
    });
  }, (nextState, transition) => {
    transition.to('/403');
  });
  const routes = createRoutes(requireAccess);
  const history = new BrowserHistory();

  React.render((
    <Provider redux={redux}>
      {() => <Router history={history} children={routes}/> }
    </Provider>
  ), reactRoot);

  if (process.env.NODE_ENV !== 'production') {
    window.React = React; // enable debugger

    if (!reactRoot || !reactRoot.firstChild || !reactRoot.firstChild.attributes || !reactRoot.firstChild.attributes['data-react-checksum']) {
      console.error('Server-side React render was discarded. Make sure that your initial render does not contain any client-side code.');
    }
  }
}


// Run the application when both DOM is ready
// and page content is loaded
Promise.all([
  new Promise((resolve) => {
    if (window.addEventListener) {
      window.addEventListener('DOMContentLoaded', resolve);
    } else {
      window.attachEvent('onload', resolve);
    }
  })
]).then(run);
import { buildRoles, buildAccessLevels } from './core/auth-helpers.js';

/*
  List all the roles you wish to use in the app
  You have a max of 31 before the bit shift pushes the accompanying integer out of
  the memory footprint for an integer
 */
const roles = [
  'banned',
  'public',
  'user',
  'admin'
];

/*
  Build out all the access levels you want referencing the roles listed above
  You can use the "*" symbol to represent access to all roles.

  The left-hand side specifies the name of the access level, and the right-hand side
  specifies what user roles have access to that access level. E.g. users with user role
  'user' and 'admin' have access to the access level 'user'.
 */
const levels = {
  'public': '*',
  'anon': ['public'],
  'user': ['user', 'admin'],
  'admin': ['admin']
};

export const userRoles = buildRoles(roles);
export const accessLevels = buildAccessLevels(levels, userRoles);

This is an auth example (WIP)

react + redux + RR

It uses https://gist.github.com/iNikNik/3c1b870f63dc0de67c38 for stores and actions.

1) create redux

const redux = createRedux(state);

2) get requireAccess func => bindCheckAuth to redux

const requireAccess = bindCheckAuth(redux, accessErrorHandler)

3) pass onEnter callback to route

const createRoutes = (requireAccess) => {
  return (
    <Route name="app" component={App}>
      <Route name="home" path="/" components={HomePage} onEnter={requireAccess(accessLevels.user)}/>
      ...
    </Route>
  );
};

4) run router

5) ...

6) profit!