Skip to content

Commit da83bfd

Browse files
committed
docs: write readme
1 parent 2b5bc78 commit da83bfd

File tree

1 file changed

+223
-30
lines changed

1 file changed

+223
-30
lines changed

README.md

Lines changed: 223 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,239 @@
1-
<!--
2-
Get your module up and running quickly.
3-
4-
Find and replace all on all files (CMD+SHIFT+F):
5-
- Name: My Module
6-
- Package name: my-module
7-
- Description: My new Nuxt module
8-
-->
9-
101
# Nuxt Authorization
112

12-
Handle authorization with ease in both Nuxt and Nitro.
13-
14-
> [!NOTE]
15-
> In the future, this module could be available as a Nitro module and a Nuxt module.
16-
17-
<!--
18-
- define ability
19-
- ability return an authorization response => boolean or custom object
20-
- bouncer execute the ability to allow, deny or authorize the request -->
21-
22-
233
[![npm version][npm-version-src]][npm-version-href]
244
[![npm downloads][npm-downloads-src]][npm-downloads-href]
255
[![License][license-src]][license-href]
266
[![Nuxt][nuxt-src]][nuxt-href]
277

28-
My new Nuxt module for doing amazing things.
8+
Handle authorization with ease in both Nuxt and Nitro.
299

30-
- [&nbsp;Release Notes](/CHANGELOG.md)
31-
<!-- - [🏀 Online playground](https://stackblitz.com/github/your-org/my-module?file=playground%2Fapp.vue) -->
32-
<!-- - [📖 &nbsp;Documentation](https://example.com) -->
10+
_This module does not implement ACL or RBAC. It provides low-level primitives that you can use to implement your own authorization logic._
11+
12+
> [!NOTE]
13+
> In the future, this module could be available as a Nitro module and a Nuxt module, but Nitro module are not yet ready.
14+
15+
To learn more about this module and which problem it solves, checkout my blog post about [Authorization in Nuxt](https://soubiran.dev/posts/nuxt-going-full-stack-how-to-handle-authorization).
3316

3417
## Features
3518

36-
<!-- Highlight some of the features your module provide here -->
37-
-&nbsp;Foo
38-
- 🚠 &nbsp;Bar
39-
- 🌲 &nbsp;Baz
19+
-&nbsp;Works on both the client (Nuxt) and the server (Nitro)
20+
- 🌟 &nbsp;Write abilities once and use them everywhere
21+
- 👨‍👩‍👧‍👦 &nbsp;Agnostic of the authentication layer
22+
- 🫸 &nbsp;Use components to conditionally show part of the UI
23+
- 💧 &nbsp;Primitives are can be accessed for a full customization
4024

4125
## Quick Setup
4226

4327
Install the module to your Nuxt application with one command:
4428

4529
```bash
46-
npx nuxi module add my-module
30+
npx nuxi module add nuxt-authorization
31+
```
32+
33+
That's it! You can now use the module in your Nuxt app ✨
34+
35+
## Documentation
36+
37+
> [!NOTE]
38+
> You can take a look at the playground to see the module in action.
39+
40+
### Setup
41+
42+
Before using the module and defining your first ability, you need to provide 2 resolvers. These functions are used internally to retrieve the user but you must implement them. This allows the module to be agnostic of the authentication layer.
43+
44+
For the Nuxt app, create a new plugin in `plugins/authorization-resolver.ts`:
45+
46+
```ts
47+
export default defineNuxtPlugin({
48+
name: 'authorization-resolver',
49+
parallel: true,
50+
setup() {
51+
return {
52+
provide: {
53+
authorization: {
54+
resolveClientUser: () => {
55+
// Your logic to retrieve the user from the client
56+
},
57+
},
58+
},
59+
}
60+
},
61+
})
62+
```
63+
64+
This function is called every time you check for authorization on the client. It should return the user object or `null` if the user is not authenticated. It can by async.
65+
66+
For the Nitro server, create a new plugin in `server/plugins/authorization-resolver.ts`:
67+
68+
```ts
69+
export default defineNitroPlugin((nitroApp) => {
70+
nitroApp.$authorization = {
71+
resolveServerUser: (event) => {
72+
// Your logic to retrieve the user from the server
73+
},
74+
}
75+
})
76+
```
77+
78+
This resolver receive the event. You can use it to retrieve the user from the session or the request. It should return the user object or `null` if the user is not authenticated. It can by async.
79+
80+
Generally, you use a plugin to fetch the user when the app starts and then store it. Resolver functions should only return the stored user and not fetch it again (otherwise, you could have severe performance issues).
81+
82+
TypeScript should complain about a missing '$authorization' property on the `nitroApp` object. You can fix this by adding a declaration in `server/nitro.d.ts`:
83+
84+
```ts
85+
import type { H3Event } from 'h3'
86+
87+
declare module 'nitropack' {
88+
interface NitroApp {
89+
$authorization: {
90+
resolveServerUser: (event: H3Event) => object | null | Promise<object | null>
91+
}
92+
}
93+
}
94+
95+
export {}
96+
```
97+
98+
You can replace `object` with the type of your user object.
99+
100+
#### Example with `nuxt-auth-utils`
101+
102+
The module `nuxt-auth-utils` provides an authentication layer for Nuxt. If you use this module, you can use the following resolvers:
103+
104+
Nuxt plugin:
105+
106+
```ts
107+
export default defineNuxtPlugin({
108+
name: 'authorization-resolver',
109+
parallel: true,
110+
setup() {
111+
return {
112+
provide: {
113+
authorization: {
114+
resolveClientUser: () => useUserSession().user.value,
115+
},
116+
},
117+
}
118+
},
119+
})
120+
```
121+
122+
Nitro plugin:
123+
124+
125+
```ts
126+
export default defineNitroPlugin((nitroApp) => {
127+
nitroApp.$authorization = {
128+
resolveServerUser: async (event) => {
129+
const session = await getUserSession(event)
130+
return session.user ?? null
131+
},
132+
}
133+
})
134+
```
135+
136+
Easy!
137+
138+
### Define Abilities
139+
140+
Now the resolvers are set up, you can define your first ability. An ability is a function that takes at least the user, and returns a boolean to indicate if the user can perform the action. It can also take additional arguments.
141+
142+
I recommend to create a new file `utils/abilities.ts` to create your abilities:
143+
144+
```ts
145+
export const listPosts = defineAbility(() => true) // Only authenticated users can list posts
146+
147+
export const editPost = defineAbility((user: User, post: Post) => {
148+
return user.id === post.authorId
149+
})
150+
```
151+
152+
If you have many abilities, you could prefer to create a directory `utils/abilities/` and create a file for each ability. Having the abilities in the `utils` directory allows auto-import to work in the client while having a simple import in the server `~/utils/abilities`.
153+
154+
By default, guests are not allowed to perform any action and the ability is not called. This behavior can be changed per ability:
155+
156+
```ts
157+
export const listPosts = defineAbility({ allowGuests: true }, (user: User | null) => true)
158+
```
159+
160+
Now, unauthenticated users can list posts.
161+
162+
### Use Abilities
163+
164+
To use ability, you have access to 3 bouncer functions: `allows`, `denies`, and `authorize`. Both of them are available in the client and the server. _The implementation is different but the API is the same and it's entirely transparent the developer._
165+
166+
The `allows` function returns a boolean if the user can perform the action:
167+
168+
```ts
169+
if (await allows(listPosts)) {
170+
// User can list posts
171+
}
172+
```
173+
174+
The `denies` function returns a boolean if the user cannot perform the action:
175+
176+
```ts
177+
if (await denies(editPost, post)) {
178+
// User cannot edit the post
179+
}
47180
```
48181

49-
That's it! You can now use My Module in your Nuxt app ✨
182+
The `authorize` function throws an error if the user cannot perform the action:
50183

184+
```ts
185+
await authorize(editPost, post)
186+
187+
// User can edit the post
188+
```
189+
190+
You can customize the error message and the status code per return value of the ability. This can be useful to return a 404 instead of a 403 to keep the user unaware of the existence of the resource.
191+
192+
```ts
193+
export const editPost = defineAbility((user: User, post: Post) => {
194+
if(user.id === post.authorId) {
195+
return true // or allow()
196+
}
197+
198+
return deny('This post does not exist', 404)
199+
})
200+
```
201+
202+
`allow` and `deny` are similar to returning `true` and `false` but `deny` allows to return a custom message and status code for the error.
203+
204+
Most of the times, you API endpoints will use the `authorize`. This can be the first line of the endpoint if no parameters are needed or after a database query to check if the user can access the resource. You do not need to catch the error since it's a `H3Error` and will be caught by the Nitro server.
205+
206+
The `allows` and `denies` functions are useful in the client to perform conditional rendering or logic. You can also use them to have a fine-grained control on you authorization logic.
207+
208+
### Use Components
209+
210+
The module provides 2 components help you to conditionally show part of the UI. Imagine you have a button to edit a post, unauthorized users should not see the button.
211+
212+
```vue
213+
<template>
214+
<Can
215+
:ability="editPost"
216+
:args="[post]"
217+
>
218+
<button>Edit</button>
219+
</Can>
220+
</template>
221+
```
222+
223+
The `Can` component will render the button only if the user can edit the post. If the user cannot edit the post, the button will not be rendered.
224+
225+
As a counterpart, you can use the `Cannot` component to render the button only if the user cannot edit the post.
226+
227+
```vue
228+
<template>
229+
<Cannot
230+
:ability="editPost"
231+
:args="[post]"
232+
>
233+
<p>You're not allowed to edit the post.</p>
234+
</Cannot>
235+
</template>
236+
```
51237

52238
## Contribution
53239

@@ -80,6 +266,13 @@ That's it! You can now use My Module in your Nuxt app ✨
80266

81267
</details>
82268

269+
## Credits
270+
271+
This module, both code and design, is heavily inspired by the [Adonis Bouncer](https://docs.adonisjs.com/guides/security/authorization). It's a well written package and I do not think reinventing the wheel every time is unnecessary.
272+
273+
## License
274+
275+
[MIT License](./LICENSE)
83276

84277
<!-- Badges -->
85278
[npm-version-src]: https://img.shields.io/npm/v/my-module/latest.svg?style=flat&colorA=020420&colorB=00DC82

0 commit comments

Comments
 (0)