{"id":14863388,"url":"https://github.com/Barbapapazes/nuxt-authorization","last_synced_at":"2025-09-18T17:32:38.094Z","repository":{"id":244210014,"uuid":"814172286","full_name":"Barbapapazes/nuxt-authorization","owner":"Barbapapazes","description":"Authorization module for managing permissions inside a Nuxt app.","archived":false,"fork":false,"pushed_at":"2025-01-06T01:55:46.000Z","size":293,"stargazers_count":198,"open_issues_count":10,"forks_count":7,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-01-06T08:37:59.326Z","etag":null,"topics":["authorization","module","nuxt"],"latest_commit_sha":null,"homepage":"https://soubiran.dev/posts/nuxt-going-full-stack-how-to-handle-authorization","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Barbapapazes.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":["barbapapazes"]}},"created_at":"2024-06-12T13:30:12.000Z","updated_at":"2024-12-29T06:39:00.000Z","dependencies_parsed_at":"2024-09-19T22:01:49.794Z","dependency_job_id":"9bbc5d66-b32e-4c25-bdb6-85dfb972032e","html_url":"https://github.com/Barbapapazes/nuxt-authorization","commit_stats":null,"previous_names":["barbapapazes/nuxt-authorization"],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Barbapapazes%2Fnuxt-authorization","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Barbapapazes%2Fnuxt-authorization/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Barbapapazes%2Fnuxt-authorization/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Barbapapazes%2Fnuxt-authorization/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Barbapapazes","download_url":"https://codeload.github.com/Barbapapazes/nuxt-authorization/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":233505821,"owners_count":18686388,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["authorization","module","nuxt"],"created_at":"2024-09-19T22:00:23.473Z","updated_at":"2025-09-18T17:32:38.080Z","avatar_url":"https://github.com/Barbapapazes.png","language":"TypeScript","funding_links":["https://github.com/sponsors/barbapapazes"],"categories":["TypeScript"],"sub_categories":[],"readme":"# Nuxt Authorization\n\n[![npm version][npm-version-src]][npm-version-href]\n[![npm downloads][npm-downloads-src]][npm-downloads-href]\n[![License][license-src]][license-href]\n[![Nuxt][nuxt-src]][nuxt-href]\n[![pkg.pr.new](https://pkg.pr.new/badge/Barbapapazes/nuxt-authorization)](https://pkg.pr.new/~/Barbapapazes/nuxt-authorization)\n\nHandle authorization with ease in both Nuxt and Nitro.\n\n_This module does not implement ACL or RBAC. It provides low-level primitives that you can use to implement your own authorization logic._\n\n\u003e [!NOTE]\n\u003e In the future, this module could be available as a Nitro module and a Nuxt module, but Nitro module are not yet ready.\n\nTo 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).\n\n## Features\n\n- ⛰ \u0026nbsp;Works on both the client (Nuxt) and the server (Nitro)\n- 🌟 \u0026nbsp;Write abilities once and use them everywhere\n- 👨‍👩‍👧‍👦 \u0026nbsp;Agnostic of the authentication layer\n- 🫸 \u0026nbsp;Use components to conditionally show part of the UI\n- 💧 \u0026nbsp;Primitives are can be accessed for a full customization\n\n## Quick Setup\n\nInstall the module to your Nuxt application with one command:\n\n```bash\nnpx nuxi module add nuxt-authorization\n```\n\nThat's it! You can now use the module in your Nuxt app ✨\n\n## Documentation\n\n\u003e [!NOTE]\n\u003e You can take a look at the playground to see the module in action.\n\n### Setup\n\nBefore 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.\n\nFor the Nuxt app, create a new plugin in `plugins/authorization-resolver.ts`:\n\n```ts\nexport default defineNuxtPlugin({\n  name: 'authorization-resolver',\n  parallel: true,\n  setup() {\n    return {\n      provide: {\n        authorization: {\n          resolveClientUser: () =\u003e {\n            // Your logic to retrieve the user from the client\n          },\n        },\n      },\n    }\n  },\n})\n```\n\nThis 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.\n\nFor the Nitro server, create a new plugin in `server/plugins/authorization-resolver.ts`:\n\n```ts\nexport default defineNitroPlugin((nitroApp) =\u003e {\n  nitroApp.hooks.hook('request', async (event) =\u003e {\n    event.context.$authorization = {\n      resolveServerUser: () =\u003e {\n        // Your logic to retrieve the user from the server\n      },\n    }\n  })\n})\n```\n\n\u003e [!NOTE]\n\u003e Read more about the [`event.context`](https://h3.unjs.io/guide/event#eventcontext)\n\nThis resolver is setup within the hook `request` and 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.\n\nGenerally, 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).\n\n#### Example with `nuxt-auth-utils`\n\nThe module `nuxt-auth-utils` provides an authentication layer for Nuxt. If you use this module, you can use the following resolvers:\n\nNuxt plugin:\n\n```ts\nexport default defineNuxtPlugin({\n  name: 'authorization-resolver',\n  parallel: true,\n  setup() {\n    return {\n      provide: {\n        authorization: {\n          resolveClientUser: () =\u003e useUserSession().user.value,\n        },\n      },\n    }\n  },\n})\n```\n\nNitro plugin:\n\n```ts\nexport default defineNitroPlugin((nitroApp) =\u003e {\n  nitroApp.hooks.hook('request', async (event) =\u003e {\n    event.context.$authorization = {\n      resolveServerUser: async () =\u003e {\n        const session = await getUserSession(event)\n        return session.user ?? null\n      },\n    }\n  })\n})\n```\n\nEasy!\n\n### Define Abilities\n\n\u003e [!NOTE]\n\u003e With Nuxt 4, a new `shared` directory will be introduced to easily share code between the client and the server.\n\u003e See [the video from Alexander Lichter](https://youtu.be/_m5ct5e8nVo?si=2iNcPVMttlIq5fLR).\n\nNow 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.\n\nI recommend to create a new file `shared/utils/abilities.ts` to create your abilities:\n\n```ts\nexport const listPosts = defineAbility(() =\u003e true) // Only authenticated users can list posts\n\nexport const editPost = defineAbility((user: User, post: Post) =\u003e {\n  return user.id === post.authorId\n})\n```\n\nIf you have many abilities, you could prefer to create a directory `shared/utils/abilities/` and create a file for each ability. Having the abilities in the `shared/utils` directory allows auto-import to work in the client while having a simple import in the server `~~/shared/utils/abilities`. **Remember that the shared folder only exports the first level of the directory.** So you have to export the abilities in the `shared/utils/abilities/index.ts` file.\n\nBy default, guests are not allowed to perform any action and the ability is not called. This behavior can be changed per ability:\n\n```ts\nexport const listPosts = defineAbility({ allowGuest: true }, (user: User | null) =\u003e true)\n```\n\nNow, unauthenticated users can list posts.\n\n### Use Abilities\n\nTo 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 (nearly) the same and it's entirely transparent the developer. On the server, the first parameter is the `event` from the handler._\n\nThe `allows` function returns a boolean if the user can perform the action:\n\n```ts\nif (await allows(listPosts)) {\n  // User can list posts\n}\n```\n\n_For the server:_\n\n```ts\nif (await allows(event, listPosts)) {\n  // User can list posts\n}\n```\n\nThe `denies` function returns a boolean if the user cannot perform the action:\n\n```ts\nif (await denies(editPost, post)) {\n  // User cannot edit the post\n}\n```\n\n_For the server:_\n\n```ts\nif (await denies(event, editPost, post)) {\n  // User cannot edit the post\n}\n```\n\nThe `authorize` function throws an error if the user cannot perform the action:\n\n```ts\nawait authorize(editPost, post)\n\n// User can edit the post\n```\n\n_For the server:_\n\n```ts\nawait authorize(event, editPost, post)\n```\n\nYou 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.\n\n```ts\nexport const editPost = defineAbility((user: User, post: Post) =\u003e {\n  if(user.id === post.authorId) {\n    return true // or allow()\n  }\n\n  return deny('This post does not exist', 404)\n})\n```\n\n`allow` and `deny` are similar to returning `true` and `false` but `deny` allows to return a custom message and status code for the error.\n\nMost 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.\n\nThe `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.\n\n### Use Components\n\nThe 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.\n\n```vue\n\u003ctemplate\u003e\n  \u003cCan\n    :ability=\"editPost\"\n    :args=\"[post]\" // Optional if the ability does not take any arguments\n  \u003e\n    \u003cbutton\u003eEdit\u003c/button\u003e\n  \u003c/Can\u003e\n\u003c/template\u003e\n```\n\nThe `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.\n\nAs a counterpart, you can use the `Cannot` component to render the button only if the user cannot edit the post.\n\n```vue\n\u003ctemplate\u003e\n  \u003cCannot\n    :ability=\"editPost\"\n    :args=\"[post]\" // Optional if the ability does not take any arguments\n  \u003e\n    \u003cp\u003eYou're not allowed to edit the post.\u003c/p\u003e\n  \u003c/Cannot\u003e\n\u003c/template\u003e\n```\n\nThe `Bouncer` component offers a more flexible and centralized way to handle both can and cannot scenarios within a single component. Instead of using separate `Can` and `Cannot` components, you can leverage the Bouncer component and its [named slots](https://vuejs.org/guide/components/slots.html#named-slots) to handle both states in a unified block.\n\n```vue\n\u003cBouncer\n  :ability=\"editPost\"\n  :args=\"[post]\" // Optional if the ability does not take any arguments\n\u003e\n  \u003ctemplate #can\u003e\n    \u003cbutton\u003eEdit\u003c/button\u003e\n  \u003c/template\u003e\n\n  \u003ctemplate #cannot\u003e\n    \u003cp\u003eYou're not allowed to edit the post.\u003c/p\u003e\n  \u003c/template\u003e\n\u003c/Bouncer\u003e\n```\n\nAll of these components accept a prop named `as` to define the HTML tag to render. By default, it's a renderless component.\n\n```vue\n\u003cCan\n  :ability=\"editPost\"\n  :args=\"[post]\"\n  as=\"div\"\n\u003e\n  \u003cbutton\u003eEdit\u003c/button\u003e\n\u003c/Can\u003e\n```\n\nThis will render:\n\n```html\n\u003cdiv\u003e\n  \u003cbutton\u003eEdit\u003c/button\u003e\n\u003c/div\u003e\n```\n\nInstead of:\n\n```html\n\u003cbutton\u003eEdit\u003c/button\u003e\n```\n\n#### Multiple abilities\n\nIf you possess multiple abilities, you can provide an array of abilities to the components. The component will render only if **all** of the abilities **match** the specified requirements of the component.\n\n```vue\n\u003cCan :ability=\"[editPost, deletePost]\" :args=\"[[post], [post]]\" /\u003e\n\n\u003cCannot :ability=\"[editPost, deletePost]\" :args=\"[[post], [post]]\" /\u003e\n\n\u003cBouncer :ability=\"[editPost, deletePost]\" :args=\"[[post], [post]]\"\u003e\n  \u003ctemplate #can\u003e\n    \u003cbutton\u003eEdit\u003c/button\u003e\n    \u003cbutton\u003eDelete\u003c/button\u003e\n  \u003c/template\u003e\n\n  \u003ctemplate #cannot\u003e\n    \u003cp\u003eYou're not allowed to edit or delete the post.\u003c/p\u003e\n  \u003c/template\u003e\n\u003c/Bouncer\u003e\n```\n\n## Contribution\n\n\u003cdetails\u003e\n  \u003csummary\u003eLocal development\u003c/summary\u003e\n\n  ```bash\n  # Install dependencies\n  npm install\n\n  # Generate type stubs\n  npm run dev:prepare\n\n  # Develop with the playground\n  npm run dev\n\n  # Build the playground\n  npm run dev:build\n\n  # Run ESLint\n  npm run lint\n\n  # Run Vitest\n  npm run test\n  npm run test:watch\n\n  # Release new version\n  npm run release\n  ```\n\n\u003c/details\u003e\n\n## Credits\n\nThis 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 think reinventing the wheel every time is unnecessary.\n\n## License\n\n[MIT License](./LICENSE)\n\n\u003c!-- Badges --\u003e\n[npm-version-src]: https://img.shields.io/npm/v/nuxt-authorization/latest.svg?style=flat\u0026colorA=020420\u0026colorB=00DC82\n[npm-version-href]: https://npmjs.com/package/nuxt-authorization\n\n[npm-downloads-src]: https://img.shields.io/npm/dm/nuxt-authorization.svg?style=flat\u0026colorA=020420\u0026colorB=00DC82\n[npm-downloads-href]: https://npmjs.com/package/nuxt-authorization\n\n[license-src]: https://img.shields.io/npm/l/nuxt-authorization.svg?style=flat\u0026colorA=020420\u0026colorB=00DC82\n[license-href]: https://npmjs.com/package/nuxt-authorization\n\n[nuxt-src]: https://img.shields.io/badge/Nuxt-020420?logo=nuxt.js\n[nuxt-href]: https://nuxt.com\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FBarbapapazes%2Fnuxt-authorization","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FBarbapapazes%2Fnuxt-authorization","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FBarbapapazes%2Fnuxt-authorization/lists"}