Skip to content

Instantly share code, notes, and snippets.

@Dr-Nikson
Last active August 20, 2025 02:36
Show Gist options
  • Save Dr-Nikson/1eabfc6bcc132384368c to your computer and use it in GitHub Desktop.
Save Dr-Nikson/1eabfc6bcc132384368c to your computer and use it in GitHub Desktop.

Revisions

  1. Dr-Nikson revised this gist Jul 4, 2015. 2 changed files with 2 additions and 2 deletions.
    2 changes: 1 addition & 1 deletion bindCheckAuth.js
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,5 @@
    import { isAuthorized } from '../stores/auth';
    import { checkAccess } from './authAccessLevels';
    import { checkAccess } from './auth-helpers';

    /**
    * Creates requireAccess function and binds it to redux.
    2 changes: 1 addition & 1 deletion server.js
    Original file line number Diff line number Diff line change
    @@ -5,7 +5,7 @@ import { createRoutes } from './routes';
    import createRedux from './redux/createRedux';
    import fetchComponentsData from './core/fetchComponentsData';
    import renderTemplate from './core/renderTemplate';
    import { NotAuthorizedException, AccessDeniedException } from './core/authAccessLevels.js';
    import { NotAuthorizedException, AccessDeniedException } from './core/auth-helpers.js';
    import bindCheckAuth from './core/bindCheckAuth';


  2. Dr-Nikson revised this gist Jul 4, 2015. 6 changed files with 2 additions and 7 deletions.
    4 changes: 2 additions & 2 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,6 @@
    # This is an auth example
    # This is an auth example (WIP)
    *react + redux + RR*
    _WIP_

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

    **1) create redux**
    1 change: 0 additions & 1 deletion app.js
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,3 @@

    import 'babel/polyfill';
    import React from 'react';
    import { Router } from 'react-router';
    1 change: 0 additions & 1 deletion auth-helpers.js
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,3 @@

    import _ from 'lodash';
    import invariant from 'react/lib/invariant';

    1 change: 0 additions & 1 deletion auth.js
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,3 @@

    import { createStore, getActionIds } from '../redux/helpers.js';
    import { AuthActions } from '../actions/AuthActions';
    import { userRoles, accessLevels } from '../access';
    1 change: 0 additions & 1 deletion bindCheckAuth.js
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,3 @@

    import { isAuthorized } from '../stores/auth';
    import { checkAccess } from './authAccessLevels';

    1 change: 0 additions & 1 deletion routes.js
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,3 @@

    import React from 'react';
    import { Router, Route, DefaultRoute, Redirect } from 'react-router'; // eslint-disable-line no-unused-vars

  3. Dr-Nikson revised this gist Jul 4, 2015. 7 changed files with 70 additions and 38 deletions.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,6 @@
    # This is an auth example
    *react + redux + RR*

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

    **1) create redux**
    23 changes: 12 additions & 11 deletions app.js
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,4 @@

    import 'babel/polyfill';
    import React from 'react';
    import { Router } from 'react-router';
    @@ -7,27 +8,19 @@ import { Provider } from 'redux/react';
    import createRedux from './redux/createRedux';
    import { createRoutes } from './routes';
    import getInitialState from './core/getInitialState';
    import bindCheckAuth from './core/auth-helpers.js';
    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) => {
    /*
    Current version need this hack to avoid infinite /login redirect
    Need to split error states:
    1) "user not authorized" => [401] => redirect to login
    2) "access denied" => [403] => redirect to 403 page
    */
    if (nextState.location.pathname === '/login') {
    return;
    }

    transition.to('/login', {
    next: nextState.location.pathname
    });
    }, (nextState, transition) => {
    transition.to('/403');
    });
    const routes = createRoutes(requireAccess);
    const history = new BrowserHistory();
    @@ -37,6 +30,14 @@ function run() {
    {() => <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.');
    }
    }
    }


    9 changes: 8 additions & 1 deletion auth-helpers.js
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,4 @@

    import _ from 'lodash';
    import invariant from 'react/lib/invariant';

    @@ -10,12 +11,18 @@ export function accessEquals(requiredLevel, currentLevel) {
    return requiredLevel.bitMask === currentLevel.bitMask;
    }

    export class NeedRedirect {
    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
    8 changes: 7 additions & 1 deletion auth.js
    Original file line number Diff line number Diff line change
    @@ -1,11 +1,13 @@

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

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

    export const auth = createStore(initialState, {

    [actions.authenticate.success]: (state, action) => {
    @@ -16,3 +18,7 @@ export const auth = createStore(initialState, {
    },

    });

    export function isAuthorized(state) {
    return state.accessLvl.bitMask !== accessLevels.anon.bitMask;
    }
    33 changes: 26 additions & 7 deletions bindCheckAuth.js
    Original file line number Diff line number Diff line change
    @@ -1,12 +1,31 @@
    import { checkAccess } from './core/auth-helpers.js';

    export default function bindCheckAuth(redux, accessFailHandler) {
    return (accessLevel, customCheckFun) => (nextState, transition) => {
    const currentAccessLvl = redux.getState().auth.accessLvl;
    import { isAuthorized } from '../stores/auth';
    import { checkAccess } from './authAccessLevels';

    if (customCheckFun && !customCheckFun(accessLevel, currentAccessLvl)
    || !checkAccess(accessLevel, currentAccessLvl)) {
    accessFailHandler(nextState, transition);
    /**
    * 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);
    };
    }
    11 changes: 7 additions & 4 deletions routes.js
    Original file line number Diff line number Diff line change
    @@ -1,20 +1,23 @@

    import React from 'react';
    import { Router, Route, DefaultRoute, Redirect } from 'react-router'; // eslint-disable-line no-unused-vars

    import { accessEquals } from './core/auth-helpers.js';
    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="/" components={HomePage} onEnter={requireAccess(accessLevels.user)}/>
    <Route name="info" path="/info" components={InfoPage} onEnter={requireAccess(accessLevels.user)}/>
    <Route name="login" path="/login" components={LoginPage} onEnter={requireAccess(accessLevels.anon, accessEquals)}/>
    <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>
    );
    };
    22 changes: 9 additions & 13 deletions server.js
    Original file line number Diff line number Diff line change
    @@ -5,7 +5,7 @@ import { createRoutes } from './routes';
    import createRedux from './redux/createRedux';
    import fetchComponentsData from './core/fetchComponentsData';
    import renderTemplate from './core/renderTemplate';
    import { NeedRedirect } from './core/auth-helpers.js';
    import { NotAuthorizedException, AccessDeniedException } from './core/authAccessLevels.js';
    import bindCheckAuth from './core/bindCheckAuth';


    @@ -18,16 +18,9 @@ server.get('*', (req, res, next) => {

    const redux = createRedux();
    const requireAccess = bindCheckAuth(redux, (nextState) => {
    /*
    Current version need this hack to avoid infinite /login redirect
    Need to split error states:
    1) "user not authorized" => [401] => redirect to login
    2) "access denied" => [403] => redirect to 403 page
    */
    if (nextState.location.pathname === '/login') {
    return;
    }
    throw new NeedRedirect('/login?next=' + nextState.location.pathname);
    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);
    @@ -45,10 +38,13 @@ server.get('*', (req, res, next) => {
    });

    } catch (err) {
    if (err instanceof NeedRedirect) {
    // Redirect
    // 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);
  4. Dr-Nikson revised this gist Jul 2, 2015. 2 changed files with 2 additions and 2 deletions.
    2 changes: 1 addition & 1 deletion app.js
    Original file line number Diff line number Diff line change
    @@ -17,7 +17,7 @@ function run() {
    const requireAccess = bindCheckAuth(redux, (nextState, transition) => {
    /*
    Current version need this hack to avoid infinite /login redirect
    Need to fetch error states:
    Need to split error states:
    1) "user not authorized" => [401] => redirect to login
    2) "access denied" => [403] => redirect to 403 page
    */
    2 changes: 1 addition & 1 deletion server.js
    Original file line number Diff line number Diff line change
    @@ -20,7 +20,7 @@ server.get('*', (req, res, next) => {
    const requireAccess = bindCheckAuth(redux, (nextState) => {
    /*
    Current version need this hack to avoid infinite /login redirect
    Need to fetch error states:
    Need to split error states:
    1) "user not authorized" => [401] => redirect to login
    2) "access denied" => [403] => redirect to 403 page
    */
  5. Dr-Nikson revised this gist Jul 2, 2015. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -23,5 +23,7 @@ const createRoutes = (requireAccess) => {
    };
    ```
    **4) run router**

    **5) ...**

    **6) profit!**
  6. Dr-Nikson revised this gist Jul 2, 2015. 1 changed file with 5 additions and 5 deletions.
    10 changes: 5 additions & 5 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -7,11 +7,11 @@ It uses https://gist.github.com/iNikNik/3c1b870f63dc0de67c38 for stores and acti
    ```javascript
    const redux = createRedux(state);
    ```
    2. get requireAccess func => bindCheckAuth to redux
    **2) get requireAccess func => bindCheckAuth to redux**
    ```javascript
    const requireAccess = bindCheckAuth(redux, accessErrorHandler)
    ```
    3. pass onEnter callback to route
    **3) pass onEnter callback to route**
    ```javascript
    const createRoutes = (requireAccess) => {
    return (
    @@ -22,6 +22,6 @@ const createRoutes = (requireAccess) => {
    );
    };
    ```
    4. run router
    5. ...
    6. profit!
    **4) run router**
    **5) ...**
    **6) profit!**
  7. Dr-Nikson revised this gist Jul 2, 2015. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -3,7 +3,7 @@

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

    1. create redux
    **1) create redux**
    ```javascript
    const redux = createRedux(state);
    ```
  8. Dr-Nikson revised this gist Jul 2, 2015. 1 changed file with 6 additions and 6 deletions.
    12 changes: 6 additions & 6 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -3,15 +3,15 @@

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

    ** 1) create redux **
    1. create redux
    ```javascript
    const redux = createRedux(state);
    ```
    ** 2) get requireAccess func => bindCheckAuth to redux **
    2. get requireAccess func => bindCheckAuth to redux
    ```javascript
    const requireAccess = bindCheckAuth(redux, accessErrorHandler)
    ```
    ** 3) pass onEnter callback to route **
    3. pass onEnter callback to route
    ```javascript
    const createRoutes = (requireAccess) => {
    return (
    @@ -22,6 +22,6 @@ const createRoutes = (requireAccess) => {
    );
    };
    ```
    ** 4) run router **
    ** 5) ... **
    ** 6) profit! **
    4. run router
    5. ...
    6. profit!
  9. Dr-Nikson revised this gist Jul 2, 2015. 2 changed files with 2 additions and 2 deletions.
    2 changes: 1 addition & 1 deletion app.js
    Original file line number Diff line number Diff line change
    @@ -29,7 +29,7 @@ function run() {
    next: nextState.location.pathname
    });
    });
    const routes = createRoutes(redux, requireAccess);
    const routes = createRoutes(requireAccess);
    const history = new BrowserHistory();

    React.render((
    2 changes: 1 addition & 1 deletion server.js
    Original file line number Diff line number Diff line change
    @@ -29,7 +29,7 @@ server.get('*', (req, res, next) => {
    }
    throw new NeedRedirect('/login?next=' + nextState.location.pathname);
    });
    const routes = createRoutes(redux, requireAccess);
    const routes = createRoutes(requireAccess);
    const location = new Location(req.path, req.query);

    Router.run(routes, location, async (error, initialState) => {
  10. Dr-Nikson revised this gist Jul 2, 2015. 3 changed files with 31 additions and 2 deletions.
    25 changes: 24 additions & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,27 @@
    # This is an auth example
    *react + redux + RR*

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

    ** 1) create redux **
    ```javascript
    const redux = createRedux(state);
    ```
    ** 2) get requireAccess func => bindCheckAuth to redux **
    ```javascript
    const requireAccess = bindCheckAuth(redux, accessErrorHandler)
    ```
    ** 3) pass onEnter callback to route **
    ```javascript
    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! **
    2 changes: 1 addition & 1 deletion routes.js
    Original file line number Diff line number Diff line change
    @@ -9,7 +9,7 @@ import HomePage from './containers/HomePage';
    import InfoPage from './containers/InfoPage';
    import LoginPage from './containers/LoginPage';

    export const createRoutes = (redux, requireAccess) => {
    export const createRoutes = (requireAccess) => {
    return (
    <Route name="app" component={App}>
    <Route name="home" path="/" components={HomePage} onEnter={requireAccess(accessLevels.user)}/>
    6 changes: 6 additions & 0 deletions server.js
    Original file line number Diff line number Diff line change
    @@ -18,6 +18,12 @@ server.get('*', (req, res, next) => {

    const redux = createRedux();
    const requireAccess = bindCheckAuth(redux, (nextState) => {
    /*
    Current version need this hack to avoid infinite /login redirect
    Need to fetch error states:
    1) "user not authorized" => [401] => redirect to login
    2) "access denied" => [403] => redirect to 403 page
    */
    if (nextState.location.pathname === '/login') {
    return;
    }
  11. Dr-Nikson revised this gist Jul 2, 2015. 8 changed files with 0 additions and 8 deletions.
    1 change: 0 additions & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,3 @@

    # This is an auth example
    *react + redux + RR*

    1 change: 0 additions & 1 deletion app.js
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,3 @@

    import 'babel/polyfill';
    import React from 'react';
    import { Router } from 'react-router';
    1 change: 0 additions & 1 deletion auth-helpers.js
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,3 @@

    import _ from 'lodash';
    import invariant from 'react/lib/invariant';

    1 change: 0 additions & 1 deletion auth.js
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,3 @@

    import { createStore, getActionIds } from '../redux/helpers.js';
    import { AuthActions } from '../actions/AuthActions';
    import { userRoles } from '../access';
    1 change: 0 additions & 1 deletion AuthActions.js → authActions.js
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,3 @@

    import { createActions, asyncAction } from '../redux/helpers.js';
    import { userRoles } from '../access';

    1 change: 0 additions & 1 deletion bindCheckAuth.js
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,3 @@

    import { checkAccess } from './core/auth-helpers.js';

    export default function bindCheckAuth(redux, accessFailHandler) {
    1 change: 0 additions & 1 deletion routes.js
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,3 @@

    import React from 'react';
    import { Router, Route, DefaultRoute, Redirect } from 'react-router'; // eslint-disable-line no-unused-vars

    1 change: 0 additions & 1 deletion server.js
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,3 @@

    // ...

    import { createRoutes } from './routes';
  12. Dr-Nikson created this gist Jul 2, 2015.
    20 changes: 20 additions & 0 deletions AuthActions.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,20 @@

    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;
    },

    });
    5 changes: 5 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,5 @@

    # This is an auth example
    *react + redux + RR*

    It uses https://gist.github.com/iNikNik/3c1b870f63dc0de67c38 for stores and actions.
    31 changes: 31 additions & 0 deletions access.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,31 @@
    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);
    54 changes: 54 additions & 0 deletions app.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,54 @@

    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/auth-helpers.js';

    function run() {

    const reactRoot = window.document.getElementById('app');
    const state = getInitialState('#__INITIAL_STATE__');
    const redux = createRedux(state);
    const requireAccess = bindCheckAuth(redux, (nextState, transition) => {
    /*
    Current version need this hack to avoid infinite /login redirect
    Need to fetch error states:
    1) "user not authorized" => [401] => redirect to login
    2) "access denied" => [403] => redirect to 403 page
    */
    if (nextState.location.pathname === '/login') {
    return;
    }

    transition.to('/login', {
    next: nextState.location.pathname
    });
    });
    const routes = createRoutes(redux, requireAccess);
    const history = new BrowserHistory();

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


    // 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);
    114 changes: 114 additions & 0 deletions auth-helpers.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,114 @@

    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 NeedRedirect {
    constructor(to = '/login') {
    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;
    }
    19 changes: 19 additions & 0 deletions auth.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,19 @@

    import { createStore, getActionIds } from '../redux/helpers.js';
    import { AuthActions } from '../actions/AuthActions';
    import { userRoles } 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
    };
    },

    });
    13 changes: 13 additions & 0 deletions bindCheckAuth.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,13 @@

    import { checkAccess } from './core/auth-helpers.js';

    export default function bindCheckAuth(redux, accessFailHandler) {
    return (accessLevel, customCheckFun) => (nextState, transition) => {
    const currentAccessLvl = redux.getState().auth.accessLvl;

    if (customCheckFun && !customCheckFun(accessLevel, currentAccessLvl)
    || !checkAccess(accessLevel, currentAccessLvl)) {
    accessFailHandler(nextState, transition);
    }
    };
    }
    21 changes: 21 additions & 0 deletions routes.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,21 @@

    import React from 'react';
    import { Router, Route, DefaultRoute, Redirect } from 'react-router'; // eslint-disable-line no-unused-vars

    import { accessEquals } from './core/auth-helpers.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';

    export const createRoutes = (redux, requireAccess) => {
    return (
    <Route name="app" component={App}>
    <Route name="home" path="/" components={HomePage} onEnter={requireAccess(accessLevels.user)}/>
    <Route name="info" path="/info" components={InfoPage} onEnter={requireAccess(accessLevels.user)}/>
    <Route name="login" path="/login" components={LoginPage} onEnter={requireAccess(accessLevels.anon, accessEquals)}/>
    </Route>
    );
    };
    52 changes: 52 additions & 0 deletions server.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,52 @@

    // ...

    import { createRoutes } from './routes';

    import createRedux from './redux/createRedux';
    import fetchComponentsData from './core/fetchComponentsData';
    import renderTemplate from './core/renderTemplate';
    import { NeedRedirect } 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) => {
    if (nextState.location.pathname === '/login') {
    return;
    }
    throw new NeedRedirect('/login?next=' + nextState.location.pathname);
    });
    const routes = createRoutes(redux, 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) {
    if (err instanceof NeedRedirect) {
    // Redirect
    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 {
    res.status(500).send(err.stack);
    next(err);
    }
    }
    });