├── vite.config.js
├── src
├── Can.jsx
├── main.jsx
├── ability.js
├── App.css
├── App.jsx
├── TodoApp.jsx
├── PermissionController.jsx
└── index.css
├── .gitignore
├── index.html
├── .eslintrc.cjs
├── package.json
├── README.md
└── public
└── vite.svg
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------
/src/Can.jsx:
--------------------------------------------------------------------------------
1 | // create a context for permissionscreat and abilities
2 |
3 | import { createContext } from "react";
4 | import { createContextualCan } from "@casl/react";
5 |
6 | export const AbilityContext = createContext();
7 | export const Can = createContextualCan(AbilityContext.Consumer);
8 |
--------------------------------------------------------------------------------
/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App.jsx'
4 | import './index.css'
5 |
6 | ReactDOM.createRoot(document.getElementById('root')).render(
7 |
8 |
9 | ,
10 | )
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:react/recommended',
7 | 'plugin:react/jsx-runtime',
8 | 'plugin:react-hooks/recommended',
9 | ],
10 | ignorePatterns: ['dist', '.eslintrc.cjs'],
11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
12 | settings: { react: { version: '18.2' } },
13 | plugins: ['react-refresh'],
14 | rules: {
15 | 'react-refresh/only-export-components': [
16 | 'warn',
17 | { allowConstantExport: true },
18 | ],
19 | },
20 | }
21 |
--------------------------------------------------------------------------------
/src/ability.js:
--------------------------------------------------------------------------------
1 | import { defineAbility } from "@casl/ability";
2 |
3 | // "manage" and "all" are special keywords in CASL.
4 | // "manage" represents any action and "all" represents any subject.
5 |
6 | // const ability = defineAbility((can, cannot) => {
7 | // can("manage", "all");
8 | // cannot("delete", "user");
9 | // }); // better definer 👇
10 |
11 | // Dynamic function to define abilities
12 | export const defineAbilityFor = (permissions) => {
13 | return defineAbility((can) => {
14 | Object.keys(permissions).forEach((resource) => {
15 | permissions[resource].forEach((permission) => {
16 | can(permission, resource);
17 | });
18 | });
19 | });
20 | };
21 |
22 | export default defineAbilityFor;
23 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | #root {
2 | max-width: 1280px;
3 | margin: 0 auto;
4 | padding: 2rem;
5 | text-align: center;
6 | }
7 |
8 | .logo {
9 | height: 6em;
10 | padding: 1.5em;
11 | will-change: filter;
12 | transition: filter 300ms;
13 | }
14 | .logo:hover {
15 | filter: drop-shadow(0 0 2em #646cffaa);
16 | }
17 | .logo.react:hover {
18 | filter: drop-shadow(0 0 2em #61dafbaa);
19 | }
20 |
21 | @keyframes logo-spin {
22 | from {
23 | transform: rotate(0deg);
24 | }
25 | to {
26 | transform: rotate(360deg);
27 | }
28 | }
29 |
30 | @media (prefers-reduced-motion: no-preference) {
31 | a:nth-of-type(2) .logo {
32 | animation: logo-spin infinite 20s linear;
33 | }
34 | }
35 |
36 | .card {
37 | padding: 2em;
38 | }
39 |
40 | .read-the-docs {
41 | color: #888;
42 | }
43 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "casl",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@casl/ability": "^6.5.0",
14 | "@casl/react": "^3.1.0",
15 | "react": "^18.2.0",
16 | "react-dom": "^18.2.0"
17 | },
18 | "devDependencies": {
19 | "@types/react": "^18.2.43",
20 | "@types/react-dom": "^18.2.17",
21 | "@vitejs/plugin-react": "^4.2.1",
22 | "eslint": "^8.55.0",
23 | "eslint-plugin-react": "^7.33.2",
24 | "eslint-plugin-react-hooks": "^4.6.0",
25 | "eslint-plugin-react-refresh": "^0.4.5",
26 | "vite": "^5.0.8"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | import TodoApp from "./TodoApp";
4 | import { AbilityContext } from "./Can";
5 | import defineAbilityFor from "./ability";
6 |
7 | import "./App.css";
8 | import PermissionController from "./PermissionController";
9 |
10 | function App() {
11 | // list of permissions
12 | const [permissions, setPermissions] = useState({
13 | Todo: ["read", "update", "delete", "create"],
14 | });
15 |
16 | // ability instance
17 | const ability = defineAbilityFor(permissions);
18 |
19 | return (
20 | <>
21 |
25 |
26 |
27 |
28 | >
29 | );
30 | }
31 |
32 | export default App;
33 |
--------------------------------------------------------------------------------
/src/TodoApp.jsx:
--------------------------------------------------------------------------------
1 | import { Can } from "./Can";
2 |
3 | // read - update - delete - create
4 | function TodoApp() {
5 | const fakeTodos = [
6 | { id: 1, title: "Todo #1" },
7 | { id: 2, title: "Todo #2" },
8 | { id: 3, title: "Todo #3" },
9 | ];
10 |
11 | return (
12 |
13 |
Todo Form
14 |
20 |
21 |
22 | {fakeTodos.map((todo) => (
23 |
24 | {todo.title}
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | ))}
33 |
34 |
35 |
36 | );
37 | }
38 |
39 | export default TodoApp;
40 |
--------------------------------------------------------------------------------
/src/PermissionController.jsx:
--------------------------------------------------------------------------------
1 | // This component add/remove items of permissions state
2 | // to show or hide elements using component
3 |
4 | function PermissionController({ permissions: { Todo }, setPermissions }) {
5 | const changeHandler = (event) => {
6 | const name = event.target.name;
7 | const checked = event.target.checked;
8 |
9 | if (checked) {
10 | setPermissions({ Todo: [...Todo, name] });
11 | } else {
12 | setPermissions({ Todo: Todo.filter((p) => p !== name) });
13 | }
14 | };
15 |
16 | return (
17 |
18 |
Permission Editor
19 |
32 |
33 | );
34 | }
35 |
36 | export default PermissionController;
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CASL
2 |
3 | CASL operates on the abilities level, that is what a user can actually do in the application. An ability itself depends on the 4 parameters (last 3 are optional):
4 |
5 | ### User Action
6 |
7 | Describes what user can actually do in the app. User action is a word (usually a verb) which depends on the business logic (e.g., prolong, read). Very often it will be a list of words from CRUD - create, read, update and delete.
8 |
9 | ### Subject
10 |
11 | The subject or subject type which you want to check user action on. Usually this is a business (or domain) entity (e.g., Subscription, Article, User). The relation between subject and subject type is the same as relation between an object instance and its class.
12 |
13 | ### Fields
14 |
15 | Can be used to restrict user action only to matched subject's fields (e.g., to allow moderator to update status field of an Article and disallow to update description or title)
16 |
17 | ### Conditions
18 |
19 | Criteria which restricts user action only to matched subjects. This is useful when you need to give a permission on specific subjects (e.g., to allow user to manage own Article)
20 |
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
3 | line-height: 1.5;
4 | font-weight: 400;
5 |
6 | color-scheme: light dark;
7 | color: rgba(255, 255, 255, 0.87);
8 | background-color: #242424;
9 |
10 | font-synthesis: none;
11 | text-rendering: optimizeLegibility;
12 | -webkit-font-smoothing: antialiased;
13 | -moz-osx-font-smoothing: grayscale;
14 | }
15 |
16 | a {
17 | font-weight: 500;
18 | color: #646cff;
19 | text-decoration: inherit;
20 | }
21 | a:hover {
22 | color: #535bf2;
23 | }
24 |
25 | body {
26 | margin: 0;
27 | display: flex;
28 | place-items: center;
29 | min-width: 320px;
30 | min-height: 100vh;
31 | }
32 |
33 | h1 {
34 | font-size: 3.2em;
35 | line-height: 1.1;
36 | }
37 |
38 | button,
39 | input {
40 | margin: 5px;
41 | border-radius: 8px;
42 | border: 1px solid transparent;
43 | padding: 0.6em 1.2em;
44 | font-size: 1em;
45 | font-weight: 500;
46 | font-family: inherit;
47 | background-color: #1a1a1a;
48 | cursor: pointer;
49 | transition: border-color 0.25s;
50 | }
51 | button:hover {
52 | border-color: #646cff;
53 | }
54 | button:focus,
55 | button:focus-visible {
56 | outline: 4px auto -webkit-focus-ring-color;
57 | }
58 |
59 | section {
60 | display: flex;
61 | }
62 |
63 | h4 {
64 | margin: 50px 0 10px;
65 | }
66 |
67 | label {
68 | margin-left: 20px;
69 | }
70 |
71 | @media (prefers-color-scheme: light) {
72 | :root {
73 | color: #213547;
74 | background-color: #ffffff;
75 | }
76 | a:hover {
77 | color: #747bff;
78 | }
79 | button,
80 | input {
81 | background-color: #f3f3f3;
82 | }
83 | }
84 |
--------------------------------------------------------------------------------