React components to build permission controlled ui's.
With inspiration from Pundit.
This lib returns arrays of elements in render so requires react >= 16. If you are using a lower version of react use version 1.x.x of this lib.
Install it via npm:
npm install --save react-pundit
Pundit based routes react-router Pundit.
import { PunditContainer, VisibleIf } from 'react-pundit';
import policies from './policies.js';
import './App.css';
class App extends Component {
render() {
const userOne = { id: 1, role: 'basic', activated: false };
const userTwo = { id: 2, role: 'basic', activated: true };
const userAdmin = { id: 3, role: 'admin', activated: true };
const userOneActivated = { id: 1, role: 'basic', activated: true };
const post = { user: { id: 1 }, body: 'test', editable: true };
return (
<div className="App">
<PunditContainer policies={policies} user={userOne}>
<PunditTypeSet type="Post">
<VisibleIf action="Create">
<button>create will not show</button>
</VisibleIf>
<VisibleIf action="Create" user={userTwo}>
<button>create will show</button>
</VisibleIf>
<VisibleIf action="Create" user={userAdmin}>
<button>create will show</button>
</VisibleIf>
<VisibleIf action="Edit" model={post}>
<button>edit will not show</button>
</VisibleIf>
<VisibleIf action="Edit" model={post} user={userOneActivated}>
<button>edit will show</button>
</VisibleIf>
<VisibleIf action="Edit" model={post} user={userAdmin}>
<button>edit will show</button>
</VisibleIf>
<VisibleIf type="Comment" action="Create" user={userOneActivated}>
<button>comment create will show</button>
</VisibleIf>
</PunditTypeSet>
</PunditContainer>
</div>
);
}
}
// policies.js
// Simple example
export default {
Post: (action, model, user) => {
if (user.activated === false) { return false; }
if (user.role === 'admin') { return true; }
switch (action) {
case 'Create':
return true;
case 'Edit':
return (model.editable && user.id === model.user.id);
default:
return false;
}
},
Comment: (action, model, user) => {
if (user.activated === false) { return false; }
if (user.role === 'admin') { return true; }
switch (action) {
case 'Create':
return true;
default:
return false;
}
}
};
// Function based example
import { createPolicy, toPolicyObject } from 'react-pundit';
const PostPolicy = createPolicy('Post');
PostPolicy.addAction('Edit', (model, user) => {
return user.activated && (user.role === 'admin' || (model.editable && user.id === model.user.id));
});
PostPolicy.addAction('Create', (model, user) => {
return user.activated;
});
const CommentPolicy = createPolicy('Comment');
CommentPolicy.addAction('Create', (model, user) => {
return user.activated;
});
export default toPolicyObject([PostPolicy, CommentPolicy]);
// OO example
import { PunditPolicy, toPolicyObject } from 'react-pundit';
class PostPolicy extends PunditPolicy {
constructor() {
super('Post');
}
Edit(model, user) {
return user.activated && (user.role === 'admin' || (model.editable && user.id === model.user.id));
}
Create(model, user) {
return user.activated;
}
}
class CommentPolicy extends PunditPolicy {
constructor() {
super('Comment');
}
Create(model, user) {
return user.activated;
}
}
export default toPolicyObject([new PostPolicy(), new CommentPolicy()]);
// Available components
import {
PunditContainer,
PunditTypeSet,
VisibleIf,
IfElseButton
} from 'react-pundit';
// Available helpers
import {
PunditPolicy,
createPolicy,
toPolicyObject,
PunditComponent
} from 'react-pundit';
PunditContainer
is the root of react-pundit and is where the policies are set.
You can pass a user into the container and have that act as the default user for
all children that use pundit. The container will only create DOM if there is
more then one child inside it. It creates a 'div'
by default in that case but you
can override with a element
prop ie: element="span"
or element={Wrapper}
.
<PunditContainer policies={policies} user={optionalDefaultUser}>
<div className="App">
</div>
</PunditContainer>
PunditTypeSet
is a convenience tool. It allows you not have to set the type
prop on any children in side of it as well as the model. Those children that do have type set will
override this type, the same is true for model. The type set will only create DOM if there is
more then one child inside it. It creates a 'span'
by default in that case but you
can override with a element
prop ie: element="div"
or element={Wrapper}
.
<PunditTypeSet type="DefaultType" model={optionalDefaultModel}>
</PunditTypeSet>
VisibleIf
is the base logic unit in react-pundit currently. It takes a
number of props.
type
: The policy classaction || method
: The method to check againstuser
: The user whose permission are being checkedmodel
: If needed the model the permissions are being checked against
It works so that if the permissions are met then the child will be rendered else it will not be
IfElseButton
is a button that has two click handlers one ifClick
that will trigger
if the user has permission and a elseClick
if they do not. The button will always have
the class IfElseButton
but you can add classes via the className
prop.
All Props:
type
: The policy classaction || method
: The method to check againstuser
: The user whose permission are being checkedmodel
: If needed the model the permissions are being checked againstifClick
: Function triggered if the user has permission and has clicked the buttonelseClick
: Function triggered if the user does not have permission and has clicked the buttonclassName
: Extra custom class to add to the button elementelement
: Optional component to use to override the default'button'
element
Any other props passed in will be passed to the rendering element.
Example:
In this case the user has to be logged in and activated to do the action but the button is
on a public facing page. We also use a custom Button
component to handle the render and a prop that will be passed to it.
<IfElseButton
type="Post"
action="ToggleLike"
model={post}
ifClick={() => this.toggleLike(post)}
elseClick={() => this.hasUser ? this.openModal('Please activate your account.') : this.openLogin)}
element={Button}
propSpecificToTheButton="Some Value"
>
{count} Likes
</IfElseButton>
class PostPolicy extends PunditPolicy {
constructor() {
super('Post');
}
ToggleLike(model, user) {
return user !== null && user.activated;
}
...
}
PunditComponent
is a base react component that can be extended to create
child components that use pundits checks. It does this by haveing all the default
params needed to run the checks and exposing passesPermissions
which return a
boolean true
of false
for if the user has the permissions required.
Look at the source for VisibleIf
for reference. This is a bit cleaner not
handling the case of more than one child.
class VisibleIf extends PunditComponent {
static displayName = 'VisibleIf';
render() {
if (this.passesPermissions()) {
return this.props.children;
}
return null;
}
}
If you need to extended the prop types or default props its is easy.
static propTypes = {
...PunditComponent.propTypes,
newProp: PropTypes.any,
};
static defaultProps = {
...PunditComponent.defaultProps,
newProp: 'some default',
};
Work in progress
See examples folder.
You can test changes by importing the library directly from a folder:
- Do changes to the library
- On your test project:
npm install /path/to/your/react-pundit/ --save
- For easy development, you can
npm link react-pundit
on your application - And finally
npm run compile
the react-pundit to have the changes in your application
MIT