Skip to content

Instantly share code, notes, and snippets.

@lpirola
Forked from Dr-Nikson/README.md
Created September 25, 2017 03:17
Show Gist options
  • Save lpirola/de0e1922e824ed437f84750c79782aa4 to your computer and use it in GitHub Desktop.
Save lpirola/de0e1922e824ed437f84750c79782aa4 to your computer and use it in GitHub Desktop.
Auth example (react + redux + react-router)

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!

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);
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 _ 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 { 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 { 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 { 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 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 { 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);
}
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment