Camas
Camas is a OO design minimal React authorization library.
- TypeScript friendly
- Zero dependencies
- React Hooks
- Scalable
- Easy to test
- Only 800 Bytes after gzip
Installation
Use npm:
$ npm install camas --save
Use yarn:
$ yarn add camas
Policies
Camas is focused around the notion of policy classes. I suggest that you put these classes in src/policies
. This is a simple example that allows updating a post if the user is an admin, or if the post is unpublished:
interface Post {
title: string;
body: string;
isPublished: boolean;
}
interface User {
username: string;
isAdmin: boolean;
}
class PostPolicy {
protected user: User;
construtor(context: { user: User }) {
this.user = context.user;
}
update(post: Post) {
return this.user.isAdmin || !post.isPublished;
}
}
As you can see, this is just a plain JavaScript class.
Usually you can set up a base class to inherit from:
class BasePolicy {
protected user: User;
construtor(context: { user: User }) {
this.user = context.user;
}
}
class PostPolicy extends BasePolicy {
update(post: Post) {
return this.user.isAdmin || !post.isPublished;
}
}
Context provider
import { Provider } from 'camas';
const App = () => (
<Provider context={{ user: currentUser }}>
<Routes />
</Provider>
);
Camas will pass context
to the policy class when initializing it.
Consume policy
Using hook
import { usePolicy } from 'camas';
const PostList = ({ posts }) => {
const postPolicy = usePolicy(PostPolicy);
return (
<div>
<ul>
{posts.map(post => (
<li>
{post.title}
{postPolicy.update(post) && <span>Edit</span>}
</li>
))}
</ul>
</div>
);
};
Using component
import { Authorize } from 'camas';
const PostList = ({ posts }) => {
return (
<div>
<ul>
{posts.map(post => (
<li>
{post.title}
<Authorize with={PostPolicy} if={policy => policy.update(post)}>
<span>Edit</span>
</Authorize>
</li>
))}
</ul>
</div>
);
};
Testing
Since polices are just plain classes, testing your polices can be very easy.
Here is a simple example with jest:
import PostPolicy from './PostPolicy';
descript('PostPolicy', () => {
const admin = {
isAdmin: true;
};
const normalUser = {
isAdmin: false;
}
const publishedPost = {
isPublished: true;
}
const unpublishedPost = {
isPublished: false;
}
it("denies access if post is published", () => {
expect(new PostPolicy({ user: normalUser }).update(publishedPost)).toBe(false);
});
it("grants access if post is unpublished", () => {
expect(new PostPolicy({ user: normalUser }).update(unpublishedPost)).toBe(true);
});
it("grants access if post is published and user is an admin", () => {
expect(new PostPolicy({ user: admin }).update(publishedPost)).toBe(true);
});
});
API
Provider
Props
context
- Camas passesconetxt
to the policy class when initializing it.
<Provider conetxt={{ user: currentUser }}>
<App />
</Provider>
Authorize
Props
with
- Policy class.if
- Check function for the policy, it accepts the policy instance as it's first argument.fallback
- Fallback element when policy check now pass.
<Authorize with={PostPolicy} if={policy => policy.show()} fallback={<div>You are not allow to view these posts.</div>}>
<PostList />
</Authorize>
usePolicy(...policies)
The usePolicy
hook receive policies class as it's arguments and return instances of them.
const postPolicy = usePolicy(PostPolicy);
withPolicies(policies)
A HOC injects policy instance to class component.
@withPolicies({
postPolicy: PostPolicy,
})
class PostList extends React.Component {
render() {
const { posts, postPolicy } = this.props;
return (
<div>
<ul>
{posts.map(post => (
<li>
{post.title}
{postPolicy.update(post) && <span>Edit</span>}
</li>
))}
</ul>
</div>
);
}
}
authorize(policy, check, fallback)
A HOC to apply policy to the component.
@authorize(
PostPolicy,
({ policy }) => policy.show(),
<Unauthorized />
)
class PostList extends React.Component {
render() {
return ...
}
}