diff --git a/docusaurus/docs/cms/backend-customization/guides/customizing-users-permissions-plugin-routes.md b/docusaurus/docs/cms/backend-customization/guides/customizing-users-permissions-plugin-routes.md new file mode 100644 index 0000000000..402f8a8ef1 --- /dev/null +++ b/docusaurus/docs/cms/backend-customization/guides/customizing-users-permissions-plugin-routes.md @@ -0,0 +1,695 @@ +--- +title: Customizing Users & Permissions plugin routes +description: Extend or override routes, controllers, and policies for the Users & Permissions plugin to add custom access control logic. +displayed_sidebar: cmsSidebar +tags: + - users & permissions + - routes + - policies + - controllers + - backend customization + - guides +--- + +# Customizing Users & Permissions plugin routes + + +[Users & Permissions](/cms/features/users-permissions) feature exposes `/users` and `/auth` routes that can be extended or overridden using the plugin extension system. This guide shows how to add custom policies, override controllers, and add new routes to the User collection. + + +The Users & Permissions feature ships with built-in routes for authentication (`/auth`) and user management (`/users`). Because these routes belong to a plugin rather than a user-created content-type, they cannot be customized with `createCoreRouter`. Instead, extend them through the [plugin extension system](/cms/plugins-development/plugins-extension) using a `strapi-server` file in the `/src/extensions/users-permissions/` folder. + +:::prerequisites +- A Strapi 5 project. +- Familiarity with [routes](/cms/backend-customization/routes) and [policies](/cms/backend-customization/policies). +::: + +## How it works + +[Users & Permissions](/cms/features/users-permissions) uses a route array and controller objects that differ from standard content-types. Understanding their structure is essential before customizing them. + +### Route structure + + + + +Unlike content-types you create (e.g., `api::restaurant.restaurant`), the Users & Permissions plugin registers its routes inside the `plugin.routes['content-api'].routes` array. This array contains all `/users`, `/auth`, `/roles`, and `/permissions` route definitions. + +Each route is an object with the following shape: + + +```js +{ + method: 'GET', // HTTP method + path: '/users', // URL path (relative to /api) + handler: 'user.find', // controller.action + config: { + prefix: '', // path prefix (empty means /api) + }, +} +``` + +Route configurations can also include optional `policies` and `middlewares` arrays (see [Add a custom policy](#add-custom-policy)). + +### The `strapi-server` extension file {#extend-routes} + + + +All customizations to the Users & Permissions plugin go in a single file: + + + + + +```js title="/src/extensions/users-permissions/strapi-server.js" +module.exports = (plugin) => { + // Your customizations here + + return plugin; +}; +``` + + + + + +```ts title="/src/extensions/users-permissions/strapi-server.ts" +export default (plugin) => { + // Your customizations here + + return plugin; +}; +``` + + + + + +The function receives the full plugin object and must return the plugin. You can modify `plugin.routes`, `plugin.controllers`, `plugin.policies`, and `plugin.services` before returning. + +### Available actions {#available-actions} + + + +The `user` controller is a plain object that exposes the following actions: + +| Action | Method | Path | Description | +| ------ | ------ | ---- | ----------- | +| `user.count` | `GET` | `/users/count` | Count users | +| `user.find` | `GET` | `/users` | Find all users | +| `user.me` | `GET` | `/users/me` | Get authenticated user | +| `user.findOne` | `GET` | `/users/:id` | Find one user | +| `user.create` | `POST` | `/users` | Create a user | +| `user.update` | `PUT` | `/users/:id` | Update a user | +| `user.destroy` | `DELETE` | `/users/:id` | Delete a user | + + + +The `auth` controller is a factory function `({ strapi }) => ({...})` that exposes the following actions: + +| Action | Method | Path | Rate limited | +| ------ | ------ | ---- | ------------ | +| `auth.callback` | `POST` | `/auth/local` | Yes | +| `auth.callback` | `GET` | `/auth/:provider/callback` | No | +| `auth.register` | `POST` | `/auth/local/register` | Yes | +| `auth.connect` | `GET` | `/connect/(.*)` | Yes | +| `auth.forgotPassword` | `POST` | `/auth/forgot-password` | Yes | +| `auth.resetPassword` | `POST` | `/auth/reset-password` | Yes | +| `auth.changePassword` | `POST` | `/auth/change-password` | Yes | +| `auth.emailConfirmation` | `GET` | `/auth/email-confirmation` | No | +| `auth.sendEmailConfirmation` | `POST` | `/auth/send-email-confirmation` | No | +| `auth.refresh` | `POST` | `/auth/refresh` | No | +| `auth.logout` | `POST` | `/auth/logout` | No | + +:::note +Because the `user` and `auth` controllers have different types (plain object vs. factory function), they require different override patterns (see [Override a `user` controller action](#override-controller) and [Override an `auth` controller action](#override-auth-route)). +::: + +## Customize routes {#customize-routes} + +You can add policies, register new endpoints, or remove existing ones by modifying the `plugin.routes['content-api'].routes` array in the extension file. + +### Add a custom policy {#add-custom-policy} + +A common requirement is restricting who can update or delete user accounts: for example, ensuring users can only update their own profile. + +#### 1. Create the policy file + + + + +Create a global policy that checks whether the authenticated user matches the target user. The policy function receives the Koa context (with access to `state.user` and `params`), an optional configuration object, and `{ strapi }`: + + + + + +```js title="/src/policies/is-own-user.js" +"use strict"; + +module.exports = (policyContext, config, { strapi }) => { + const currentUser = policyContext.state.user; + + if (!currentUser) { + return false; + } + + const targetUserId = Number(policyContext.params.id); + + if (currentUser.id !== targetUserId) { + return false; + } + + return true; +}; +``` + + + + + +```ts title="/src/policies/is-own-user.ts" +export default (policyContext, config, { strapi }) => { + const currentUser = policyContext.state.user; + + if (!currentUser) { + return false; + } + + const targetUserId = Number(policyContext.params.id); + + if (currentUser.id !== targetUserId) { + return false; + } + + return true; +}; +``` + + + + + +:::tip +The `is-own-user` policy above applies specifically to Users & Permissions plugin routes. For a similar pattern on standard content-types (restricting access to the entry author), see the [is-owner middleware example](/cms/backend-customization/middlewares#restricting-content-access-with-an-is-owner-policy) and the [is-owner-review policy example](/cms/backend-customization/examples/policies#creating-a-custom-policy). +::: + +#### 2. Attach the policy to the user routes + + + +In the plugin extension file, find the `update` and `delete` routes and add the policy: + + + + + +```js title="/src/extensions/users-permissions/strapi-server.js" +module.exports = (plugin) => { + // Find the routes that need the policy + const routes = plugin.routes['content-api'].routes; + + // Add the 'is-own-user' policy to the update route + const updateRoute = routes.find( + (route) => route.handler === 'user.update' + ); + + if (updateRoute) { + updateRoute.config = updateRoute.config || {}; + updateRoute.config.policies = updateRoute.config.policies || []; + updateRoute.config.policies.push('global::is-own-user'); + } + + // Add the same policy to the delete route + const deleteRoute = routes.find( + (route) => route.handler === 'user.destroy' + ); + + if (deleteRoute) { + deleteRoute.config = deleteRoute.config || {}; + deleteRoute.config.policies = deleteRoute.config.policies || []; + deleteRoute.config.policies.push('global::is-own-user'); + } + + return plugin; +}; +``` + + + + + +```ts title="/src/extensions/users-permissions/strapi-server.ts" +export default (plugin) => { + // Find the routes that need the policy + const routes = plugin.routes['content-api'].routes; + + // Add the 'is-own-user' policy to the update route + const updateRoute = routes.find( + (route) => route.handler === 'user.update' + ); + + if (updateRoute) { + updateRoute.config = updateRoute.config || {}; + updateRoute.config.policies = updateRoute.config.policies || []; + updateRoute.config.policies.push('global::is-own-user'); + } + + // Add the same policy to the delete route + const deleteRoute = routes.find( + (route) => route.handler === 'user.destroy' + ); + + if (deleteRoute) { + deleteRoute.config = deleteRoute.config || {}; + deleteRoute.config.policies = deleteRoute.config.policies || []; + deleteRoute.config.policies.push('global::is-own-user'); + } + + return plugin; +}; +``` + + + + + +With this configuration, `PUT /api/users/:id` and `DELETE /api/users/:id` return a `403 Forbidden` error if the authenticated user does not match the `:id` in the URL. + +:::tip +For a more informative error message, throw a `PolicyError` instead of returning `false`: + +```js +const { errors } = require('@strapi/utils'); +const { PolicyError } = errors; + +// Inside the policy: +throw new PolicyError('You can only modify your own account'); +``` + +
+For more details on policy patterns and error handling, see the [policies documentation](/cms/backend-customization/policies). +::: + +### Add a new route {#add-new-route} + +You can add custom routes to the Users & Permissions plugin. For example, add an endpoint that deactivates a user account as follows: + + + + + + + +```js title="/src/extensions/users-permissions/strapi-server.js" +module.exports = (plugin) => { + // Add a new controller action + plugin.controllers.user.deactivate = async (ctx) => { + const { id } = ctx.params; + + const user = await strapi + .plugin('users-permissions') + .service('user') + .edit(id, { blocked: true }); + + ctx.body = { message: `User ${user.username} has been deactivated` }; + }; + + // Register the route + plugin.routes['content-api'].routes.push({ + method: 'POST', + path: '/users/:id/deactivate', + handler: 'user.deactivate', + config: { + prefix: '', + policies: ['global::is-own-user'], + }, + }); + + return plugin; +}; +``` + + + + + +```ts title="/src/extensions/users-permissions/strapi-server.ts" +export default (plugin) => { + // Add a new controller action + plugin.controllers.user.deactivate = async (ctx) => { + const { id } = ctx.params; + + const user = await strapi + .plugin('users-permissions') + .service('user') + .edit(id, { blocked: true }); + + ctx.body = { message: `User ${user.username} has been deactivated` }; + }; + + // Register the route + plugin.routes['content-api'].routes.push({ + method: 'POST', + path: '/users/:id/deactivate', + handler: 'user.deactivate', + config: { + prefix: '', + policies: ['global::is-own-user'], + }, + }); + + return plugin; +}; +``` + + + + + +After restarting Strapi, `POST /api/users/:id/deactivate` becomes available. Grant the corresponding permission in the admin panel under *Users & Permissions plugin > Roles* for the roles that should access this endpoint. + +### Remove a route {#remove-route} + +You can disable a route by filtering it out of the routes array. For example, disable the user count endpoint as follows: + + + + + +```js title="/src/extensions/users-permissions/strapi-server.js" +module.exports = (plugin) => { + plugin.routes['content-api'].routes = plugin.routes['content-api'].routes.filter( + (route) => route.handler !== 'user.count' + ); + + return plugin; +}; +``` + + + + + +```ts title="/src/extensions/users-permissions/strapi-server.ts" +export default (plugin) => { + plugin.routes['content-api'].routes = plugin.routes['content-api'].routes.filter( + (route) => route.handler !== 'user.count' + ); + + return plugin; +}; +``` + + + + + +## Override controllers {#override-controllers} + +Beyond route-level customizations, you can override the controller actions themselves to change how the plugin handles requests. The `user` and `auth` controllers use different patterns, so each requires a specific approach. + +### Override a `user` controller action {#override-controller} + + + +The `user` controller is a plain object, so you can directly read and replace its methods in the extension file. For instance, to add custom logic to the `me` endpoint: + + + + + +```js title="/src/extensions/users-permissions/strapi-server.js" +module.exports = (plugin) => { + const originalMe = plugin.controllers.user.me; + + plugin.controllers.user.me = async (ctx) => { + // Call the original controller + await originalMe(ctx); + + // Add extra data to the response + if (ctx.body) { + ctx.body.timestamp = new Date().toISOString(); + } + }; + + return plugin; +}; +``` + + + + + +```ts title="/src/extensions/users-permissions/strapi-server.ts" +export default (plugin) => { + const originalMe = plugin.controllers.user.me; + + plugin.controllers.user.me = async (ctx) => { + // Call the original controller + await originalMe(ctx); + + // Add extra data to the response + if (ctx.body) { + ctx.body.timestamp = new Date().toISOString(); + } + }; + + return plugin; +}; +``` + + + + + +:::caution +When wrapping a controller, always call the original function first to preserve the default behavior. Skipping the original function means you take over the full request handling, including sanitization and error handling. +::: + +### Override an `auth` controller action {#override-auth-route} + + + + +The `auth` controller uses a factory pattern: it exports a function `({ strapi }) => ({...})` instead of a plain object. When your extension code runs, Strapi has not yet resolved this factory. As a result, `plugin.controllers.auth` is a function, not an object with methods. + +To override an auth action, wrap the factory itself: + + + + + +```js title="/src/extensions/users-permissions/strapi-server.js" +module.exports = (plugin) => { + const originalAuthFactory = plugin.controllers.auth; + + plugin.controllers.auth = ({ strapi }) => { + // Resolve the original factory to get the controller methods + const originalAuth = originalAuthFactory({ strapi }); + + // Override the register method + const originalRegister = originalAuth.register; + + originalAuth.register = async (ctx) => { + // Call the original register logic + await originalRegister(ctx); + + // Custom post-registration logic + if (ctx.body && ctx.body.user) { + strapi.log.info(`New user registered: ${ctx.body.user.email}`); + } + }; + + return originalAuth; + }; + + return plugin; +}; +``` + + + + + +```ts title="/src/extensions/users-permissions/strapi-server.ts" +export default (plugin) => { + const originalAuthFactory = plugin.controllers.auth; + + plugin.controllers.auth = ({ strapi }) => { + // Resolve the original factory to get the controller methods + const originalAuth = originalAuthFactory({ strapi }); + + // Override the register method + const originalRegister = originalAuth.register; + + originalAuth.register = async (ctx) => { + // Call the original register logic + await originalRegister(ctx); + + // Custom post-registration logic + if (ctx.body && ctx.body.user) { + strapi.log.info(`New user registered: ${ctx.body.user.email}`); + } + }; + + return originalAuth; + }; + + return plugin; +}; +``` + + + + + +:::caution +Do not access `plugin.controllers.auth.register` directly. Because `auth` is a factory function at extension time, its methods are not accessible until Strapi calls the factory. Always wrap the factory as shown above. +::: + +## Full example {#combine-customizations} + +The following example combines several customizations in a single file: it adds a policy to `update` and `delete`, wraps the `me` controller, and adds a new `profile` route. + + + + + +```js title="/src/extensions/users-permissions/strapi-server.js" +module.exports = (plugin) => { + const routes = plugin.routes['content-api'].routes; + + // 1. Add 'is-own-user' policy to update and delete + for (const route of routes) { + if (route.handler === 'user.update' || route.handler === 'user.destroy') { + route.config = route.config || {}; + route.config.policies = route.config.policies || []; + route.config.policies.push('global::is-own-user'); + } + } + + // 2. Wrap the 'me' controller to include the user's role + const originalMe = plugin.controllers.user.me; + + plugin.controllers.user.me = async (ctx) => { + await originalMe(ctx); + + if (ctx.state.user && ctx.body) { + const user = await strapi + .plugin('users-permissions') + .service('user') + .fetch(ctx.state.user.id, { populate: ['role'] }); + + ctx.body.role = user.role; + } + }; + + // 3. Add a custom route + plugin.controllers.user.profile = async (ctx) => { + const user = await strapi + .plugin('users-permissions') + .service('user') + .fetch(ctx.state.user.id, { populate: ['role'] }); + + ctx.body = { + username: user.username, + email: user.email, + role: user.role?.name, + createdAt: user.createdAt, + }; + }; + + routes.push({ + method: 'GET', + path: '/users/profile', + handler: 'user.profile', + config: { prefix: '' }, + }); + + return plugin; +}; +``` + + + + + +```ts title="/src/extensions/users-permissions/strapi-server.ts" +export default (plugin) => { + const routes = plugin.routes['content-api'].routes; + + // 1. Add 'is-own-user' policy to update and delete + for (const route of routes) { + if (route.handler === 'user.update' || route.handler === 'user.destroy') { + route.config = route.config || {}; + route.config.policies = route.config.policies || []; + route.config.policies.push('global::is-own-user'); + } + } + + // 2. Wrap the 'me' controller to include the user's role + const originalMe = plugin.controllers.user.me; + + plugin.controllers.user.me = async (ctx) => { + await originalMe(ctx); + + if (ctx.state.user && ctx.body) { + const user = await strapi + .plugin('users-permissions') + .service('user') + .fetch(ctx.state.user.id, { populate: ['role'] }); + + ctx.body.role = user.role; + } + }; + + // 3. Add a custom route + plugin.controllers.user.profile = async (ctx) => { + const user = await strapi + .plugin('users-permissions') + .service('user') + .fetch(ctx.state.user.id, { populate: ['role'] }); + + ctx.body = { + username: user.username, + email: user.email, + role: user.role?.name, + createdAt: user.createdAt, + }; + }; + + routes.push({ + method: 'GET', + path: '/users/profile', + handler: 'user.profile', + config: { prefix: '' }, + }); + + return plugin; +}; +``` + + + + + +## Validation + +After making changes, restart Strapi and verify your customizations: + +1. Run `yarn strapi routes:list` to confirm your new or modified routes appear. +2. Test protected routes without authentication to verify policies return `403 Forbidden`. +3. Test with an authenticated user to confirm the expected behavior. +4. Check the Strapi server logs for errors during startup. + +## Troubleshooting + +| Symptom | Possible cause | +| ------- | -------------- | +| Route not found (404) | The new route was not pushed to `plugin.routes['content-api'].routes`, or its `prefix` property is missing. | +| Policy not applied | The policy name is incorrect. Global policies require the `global::` prefix (e.g., `global::is-own-user`). | +| Controller returns 500 | The controller action name does not match the `handler` value in the route definition. | +| Changes not reflected | Strapi was not restarted after modifying the extension file. Extensions are loaded at startup. | +| Permission denied (403) | The new action is not enabled for the role. Enable it in *Users & Permissions plugin > Roles*. | +| Cannot read property of `auth` controller | The `auth` controller is a factory function, not a plain object. Wrap the factory instead of accessing methods directly (see [Override an `auth` controller action](#override-auth-route)). | diff --git a/docusaurus/docs/cms/features/users-permissions.md b/docusaurus/docs/cms/features/users-permissions.md index 2190771e60..d1d69484c8 100644 --- a/docusaurus/docs/cms/features/users-permissions.md +++ b/docusaurus/docs/cms/features/users-permissions.md @@ -1,18 +1,19 @@ --- title: Users & Permissions -description: Learn to use the Users & Permissions and API tokens features to manage end-users. +description: Learn to use the Users & Permissions feature to manage end-user accounts, authentication, and role-based access. +displayed_sidebar: cmsSidebar toc_max_heading_level: 5 tags: - admin panel - users & permissions -- api tokens +- authentication - features --- # Users & Permissions -Users & Permissions manages end-user accounts, JWT-based authentication, and role-based access to APIs. This documentation explains how to create roles, configure permissions, and issue API tokens for secure access control. +Users & Permissions manages end-user accounts, JWT-based authentication, and role-based access to APIs. This documentation explains how to create roles, configure permissions, and secure access to your content API. The Users & Permissions feature allows the management of the end-users 💡 **What are end users?**
End-users are the users who consume the content that is created and managed with a Strapi application and displayed on front-end applications (e.g. websites, mobile applications, connected devices etc.). Unlike the administrators, they do not have access to the admin panel.
of a Strapi project. It provides a full authentication process based on JSON Web Tokens (JWT) to protect your API, and an access-control list (ACL) strategy that enables you to manage permissions between groups of users. @@ -30,13 +31,13 @@ The Users & Permissions feature is configured from both the admin panel settings ### Roles -The Users & Permissions feature allows to create and manage roles for end users, to configure what they can have access to. +The Users & Permissions feature allows creating and managing roles for end users, to configure what they can have access to. #### Creating a new role **Path:** *Users & Permissions plugin > Roles* -On the top right side of the *Roles* interface, an **Add new role** button is displayed. It allows to create a new role for end users of your Strapi application. +On the top right side of the *Roles* interface, an **Add new role** button is displayed. It allows creating a new role for end users of your Strapi application. Click on the **Add new role** button to be redirected to the roles edition interface, where you will be able to name your new role and define its details and permissions (see [Editing a role](#editing-a-role)). @@ -65,7 +66,7 @@ By default, 2 end-user roles are defined for any Strapi application: More roles can however be created (see [Creating a new role](#creating-a-new-role)), and all can be edited through the role edition interface. -1. Click on the edit button of the role to edit — except if you directly landed on the role edition interface from creating a new role. +1. Click on the edit button of the role to edit. Skip this step if you directly landed on the role edition interface from creating a new role. 2. Fill in the *Role details*, following the instructions from the table below: | Role details | Instructions | @@ -103,7 +104,7 @@ Although the 2 default end-user roles cannot be deleted, the other ones can, as **Path:** *Users & Permissions plugin > Providers* -The Users & Permissions feature allows enabling and configuring providers, for end users to login via a third-party provider to access the content of a front-end application through the Strapi application API. +The Users & Permissions feature allows enabling and configuring providers, for end users to log in via a third-party provider to access the content of a front-end application through the Strapi application API. By default, a list of providers is available including one, "Email", enabled by default for all Strapi applications with Users & Permissions enabled. @@ -164,7 +165,7 @@ Both email templates can be modified. }} /> -### Advanced Settings +### Advanced settings **Path:** *Users & Permissions plugin > Advanced settings* @@ -209,9 +210,9 @@ Defining which mode is used is done by setting the `jwtManagement` property of t | Mode | Description | Use case | |------|-------------|----------| | `legacy-support` | (default) Issues long-lived JWTs using traditional configuration | Existing applications, simple authentication | -| `refresh` | Uses session management with short-lived access tokens and refresh tokens for enhanced security | New applications, enhanced security requirements
(additional information can be found in [admin panel configuration](/cms/configurations/admin-panel#session-management) documentation) | +| `refresh` | Uses session management with short-lived access tokens and refresh tokens for enhanced security | New applications, enhanced security requirements
(see [admin panel configuration](/cms/configurations/admin-panel#session-management)) | -For backwards compatibility, the Users & Permission feature defaults to legacy mode: +For backwards compatibility, the Users & Permissions feature defaults to legacy mode: ```js title="/config/plugins.js" module.exports = ({ env }) => ({ @@ -229,14 +230,14 @@ module.exports = ({ env }) => ({ :::note Notes - `jwtSecret` is a random string used to create new JWTs, typically set using the `JWT_SECRET` [environment variable](/cms/configurations/environment#strapi). - `jwt.expiresIn` is (legacy-mode only) is expressed in seconds or a string describing a time span.
- Eg: 60, "45m", "10h", "2 days", "7d", "2y". A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (minutes, hours, days, years, etc), otherwise milliseconds unit is used by default ("120" is equal to "120ms"). + For example: 60, "45m", "10h", "2 days", "7d", "2y". A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (minutes, hours, days, years, etc), otherwise milliseconds unit is used by default ("120" is equal to "120ms"). ::: :::warning Setting JWT expiry for more than 30 days is not recommended due to security concerns. ::: -When the `refresh` mode is used, the configuration file could look like as follows: +When the `refresh` mode is used, the configuration file looks like the following: @@ -302,7 +303,7 @@ export default ({ env }) => ({ ### Registration configuration -If you have added any additional fields in your User **model** Models, also called content-types in Strapi, define a representation of the content structure.
Users are a special type of built-in content-type found in any new Strapi application. You can customize the Users model, adding more fields for instance, like any other models.
For more information, please refer to the [models](/cms/backend-customization/models) documentation.
that need to be accepted on registration, you need to added them to the list of allowed fields in the `config.register` object of [the `/config/plugins` file](/cms/configurations/plugins), otherwise they will not be accepted. +If you have added any additional fields in your User **model** Models, also called content-types in Strapi, define a representation of the content structure.
Users are a special type of built-in content-type found in any new Strapi application. You can customize the Users model, adding more fields for instance, like any other models.
For more information, please refer to the [models](/cms/backend-customization/models) documentation.
that need to be accepted on registration, you need to add them to the list of allowed fields in the `config.register` object of [the `/config/plugins` file](/cms/configurations/plugins), otherwise they will not be accepted. The following example shows how to ensure a field called "nickname" is accepted by the API on user registration: @@ -348,7 +349,7 @@ export default ({ env }) => ({ ### Rate limiting configuration -Rate limiting is applied to authentication and registration endpoints to prevent abuse. The following parameters can be configured to change its behavior. Additional configuration options are provided by the package: +Rate limiting is applied to authentication and registration endpoints to prevent abuse. The following parameters can be configured to change its behavior. Additional configuration options are provided by the package: The following options are available in [the `/config/plugins` file](/cms/configurations/plugins): @@ -408,7 +409,7 @@ export default ({ env }) => ({
-### Templating emails +### Email template configuration {#templating-emails} By default this plugin comes with two templates: reset password and email address confirmation. The templates use to populate the variables. @@ -425,7 +426,7 @@ The following variables can be used: - `email` - `TOKEN` corresponds to the token generated to be able to reset the password. - `URL` is the link where the user will be redirected after clicking on it in the email. -- `SERVER_URL` is the absolute server url (configured in server configuration). +- `SERVER_URL` is the absolute server URL (configured in server configuration). @@ -437,7 +438,7 @@ The following variables can be used: - `email` - `CODE` corresponds to the CODE generated to be able confirm the user email. - `URL` is the Strapi backend URL that confirms the code (by default `/auth/email-confirmation`). -- `SERVER_URL` is the absolute server url (configured in server configuration). +- `SERVER_URL` is the absolute server URL (configured in server configuration). @@ -445,7 +446,7 @@ The following variables can be used: ### Security configuration -JWTs can be verified and trusted because the information is digitally signed. To sign a token a _secret_ is required. By default Strapi generates and stores it in `/extensions/users-permissions/config/jwt.js`. +JWTs can be verified and trusted because the information is digitally signed. To sign a token a _secret_ is required. By default Strapi generates and stores it in `/src/extensions/users-permissions/config/jwt.js`. This is useful during development but for security reasons it is recommended to set a custom token via an environment variable `JWT_SECRET` when deploying to production. @@ -455,7 +456,7 @@ By default you can set a `JWT_SECRET` environment variable and it will be used a -```js title="/extensions/users-permissions/config/jwt.js" +```js title="/src/extensions/users-permissions/config/jwt.js" module.exports = { jwtSecret: process.env.SOME_ENV_VAR, @@ -466,7 +467,7 @@ module.exports = { -```ts title="/extensions/users-permissions/config/jwt.ts" +```ts title="/src/extensions/users-permissions/config/jwt.ts" export default { jwtSecret: process.env.SOME_ENV_VAR, @@ -489,7 +490,7 @@ By default, Strapi SSO only redirects to the redirect URL that is exactly equal }} /> -If you need to configure a custom handler to accept other URLs, you can create a callback `validate` function in your plugins.js for the `users-permissions` plugin. +If you need to configure a custom handler to accept other URLs, you can create a callback `validate` function in your `plugins.js` for the `users-permissions` plugin. ```tsx title="/config/plugins.js|ts" // ... other plugins configuration ... @@ -519,6 +520,14 @@ If you need to configure a custom handler to accept other URLs, you can create a }, ``` +### Route and policy customization {#customizing-routes-and-policies} + +The Users & Permissions plugin routes and controllers can be extended and overridden through the [plugin extension system](/cms/plugins-development/plugins-extension). This is useful for adding custom policies to user endpoints, overriding controller logic, or adding new routes. + + + + + ## Usage The Users & Permissions feature can be used both via the admin panel, to create new end-user accounts, and via the APIs. @@ -537,7 +546,7 @@ With the Users & Permissions feature, the end users and their account informatio }} /> -Registering new end users in a front-end application with the Users & Permissions plugin consists in adding a new entry to the User collection type. +Registering new end users in a front-end application with the Users & Permissions plugin consists of adding a new entry to the User collection type. 1. Go to the User collection type in the Content Manager. 2. Click on the **Create new entry** button in the top right corner. @@ -553,7 +562,7 @@ Registering new end users in a front-end application with the Users & Permission 4. Click on the **Save** button. :::note -If end users can register themselves on your front-end application (see "Enable signups" option in [advanced settings](#advanced-settings)), a new entry will automatically be created and the fields of that entry will be filled up with the information indicated by the end user. All fields can however be edited by an administrator of the Strapi application. +If end users can register themselves on your front-end application (see "Enable signups" option in [advanced settings](#advanced-settings)), a new entry will automatically be created and the fields of that entry will be populated with the information indicated by the end user. All fields can however be edited by an administrator of the Strapi application. ::: ### API usage @@ -565,7 +574,7 @@ Each time an API request is sent the server checks if an `Authorization` header When you create a user without a role, or if you use the `/api/auth/local/register` route, the `authenticated` role is given to the user. ::: -#### Basic authentication endpoints +#### Authentication endpoints {#authentication-endpoints} The Users & Permissions feature provides the following authentication endpoints for user management and [Content API](/cms/api/rest) access: @@ -576,8 +585,10 @@ The Users & Permissions feature provides the following authentication endpoints | `POST` | `/api/auth/forgot-password` | Request password reset | | `POST` | `/api/auth/reset-password` | Reset password using token | | `GET` | `/api/auth/email-confirmation` | Confirm user email address | +| `POST` | `/api/auth/send-email-confirmation` | Resend confirmation email | +| `POST` | `/api/auth/change-password` | Change password (requires authentication) | -#### Session management endpoints +##### Session management endpoints When [session management](#jwt-management-modes) is enabled (`jwtManagement: 'refresh'`), additional endpoints are available: @@ -586,7 +597,7 @@ When [session management](#jwt-management-modes) is enabled (`jwtManagement: 're | `POST` | `/api/auth/refresh` | Refresh access token using refresh token | | `POST` | `/api/auth/logout` | Revoke user sessions (supports device-specific logout) | -To refresh your authentication token you could for instance send the following request: +To refresh your authentication token, send the following request: @@ -621,6 +632,216 @@ curl -X POST http://localhost:1337/api/auth/logout \ +#### User CRUD endpoints + +The Users & Permissions feature also exposes a set of endpoints for managing user records directly. These endpoints are separate from the authentication endpoints and allow you to create, read, update, and delete user entries: + +| Method | URL | Description | +| ------ | --- | ----------- | +| `GET` | `/api/users` | Find all users | +| `GET` | `/api/users/me` | Get the currently authenticated user | +| `GET` | `/api/users/:id` | Find a specific user by ID | +| `GET` | `/api/users/count` | Get the total number of users | +| `POST` | `/api/users` | Create a new user | +| `PUT` | `/api/users/:id` | Update a user by ID | +| `DELETE` | `/api/users/:id` | Delete a user by ID | + +:::note +These endpoints are protected by the role-based permission system. To access them, enable the corresponding action (e.g., `find`, `findOne`, `create`, `update`, `destroy`, `me`, `count`) for the desired role in *Users & Permissions plugin > Roles*. +::: + +##### Get the authenticated user + +The `GET /api/users/me` endpoint returns the user associated with the current JWT. The endpoint is useful for front-end applications that need to display user profile information after login. + + + + +```bash +curl -X GET http://localhost:1337/api/users/me \ + -H "Authorization: Bearer your-access-token" +``` + + + + + +```json +{ + "id": 1, + "documentId": "abc123", + "username": "kai", + "email": "kai@strapi.io", + "provider": "local", + "confirmed": true, + "blocked": false, + "createdAt": "2024-01-15T09:00:00.000Z", + "updatedAt": "2024-01-15T09:00:00.000Z", + "publishedAt": "2024-01-15T09:00:00.000Z" +} +``` + + + + +##### Find all users + +The `GET /api/users` endpoint returns a list of all users. [`populate` and `filters` parameters](/cms/api/rest/parameters) can be passed as query strings. + + + + +```bash +curl -X GET "http://localhost:1337/api/users?populate=role" \ + -H "Authorization: Bearer your-access-token" +``` + + + + + +```json +[ + { + "id": 1, + "documentId": "abc123", + "username": "kai", + "email": "kai@strapi.io", + "provider": "local", + "confirmed": true, + "blocked": false, + "role": { + "id": 1, + "name": "Authenticated", + "description": "Default role given to authenticated user.", + "type": "authenticated", + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-01T00:00:00.000Z" + }, + "createdAt": "2024-01-15T09:00:00.000Z", + "updatedAt": "2024-01-15T09:00:00.000Z", + "publishedAt": "2024-01-15T09:00:00.000Z" + } +] +``` + + + + +##### Create a user + +The `POST /api/users` endpoint creates a new user. Unlike `/api/auth/local/register`, this endpoint requires `create` permission for the Users & Permissions plugin. + + + + +```bash +curl -X POST http://localhost:1337/api/users \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your-access-token" \ + -d '{ + "username": "newuser", + "email": "newuser@strapi.io", + "password": "Password123", + "role": 1, + "confirmed": true + }' +``` + + + + + +```json +{ + "id": 2, + "documentId": "def456", + "username": "newuser", + "email": "newuser@strapi.io", + "provider": "local", + "confirmed": true, + "blocked": false, + "createdAt": "2024-01-16T10:00:00.000Z", + "updatedAt": "2024-01-16T10:00:00.000Z", + "publishedAt": "2024-01-16T10:00:00.000Z" +} +``` + + + + +##### Update a user + +The `PUT /api/users/:id` endpoint updates an existing user by ID. + + + + +```bash +curl -X PUT http://localhost:1337/api/users/2 \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your-access-token" \ + -d '{ + "username": "updateduser" + }' +``` + + + + + +```json +{ + "id": 2, + "documentId": "def456", + "username": "updateduser", + "email": "newuser@strapi.io", + "provider": "local", + "confirmed": true, + "blocked": false, + "createdAt": "2024-01-16T10:00:00.000Z", + "updatedAt": "2024-01-17T11:00:00.000Z", + "publishedAt": "2024-01-16T10:00:00.000Z" +} +``` + + + + +##### Delete a user + +The `DELETE /api/users/:id` endpoint deletes a user by ID. + + + + +```bash +curl -X DELETE http://localhost:1337/api/users/2 \ + -H "Authorization: Bearer your-access-token" +``` + + + + + +```json +{ + "id": 2, + "documentId": "def456", + "username": "updateduser", + "email": "newuser@strapi.io", + "provider": "local", + "confirmed": true, + "blocked": false, + "createdAt": "2024-01-16T10:00:00.000Z", + "updatedAt": "2024-01-17T11:00:00.000Z", + "publishedAt": "2024-01-16T10:00:00.000Z" +} +``` + + + + #### Identifier The `identifier` parameter sent with requests can be an email or username, as in the following examples: @@ -697,7 +918,7 @@ If the request is successful you will receive the **user's JWT** in the `jwt` ke The `jwt` may then be used for making permission-restricted API requests. To make an API request as a user place the JWT into an `Authorization` header of the `GET` request. -Any request without a token will assume the `public` role permissions by default. Modify the permissions of each user's role in the admin dashboard. +Any request without a token will assume the `public` role permissions by default. Modify the permissions of each user's role in the admin panel. Authentication failures return a `401 (unauthorized)` error. @@ -727,7 +948,7 @@ axios #### User registration -Creating a new user in the database with a default role as 'registered' can be done like in the following example: +Creating a new user in the database with a default role as 'authenticated' can be done as in the following example: ```js import axios from 'axios'; @@ -773,3 +994,4 @@ create: async ctx => { ctx.created(data); }; ``` + diff --git a/docusaurus/sidebars.js b/docusaurus/sidebars.js index c2d123492d..9646764e08 100644 --- a/docusaurus/sidebars.js +++ b/docusaurus/sidebars.js @@ -364,6 +364,15 @@ const sidebars = { 'cms/backend-customization/services', 'cms/backend-customization/models', 'cms/backend-customization/webhooks', + { + type: 'category', + label: 'Guides', + collapsible: true, + collapsed: true, + items: [ + 'cms/backend-customization/guides/customizing-users-permissions-plugin-routes', + ], + }, ], }, { diff --git a/docusaurus/static/llms-code.txt b/docusaurus/static/llms-code.txt index cef74b2611..8cd63048b2 100644 --- a/docusaurus/static/llms-code.txt +++ b/docusaurus/static/llms-code.txt @@ -12228,6 +12228,532 @@ module.exports = createCoreController('api::review.review', ({ strapi }) => ({ +# Customizing Users & Permissions plugin routes +Source: https://docs.strapi.io/cms/backend-customization/guides/customizing-users-permissions-plugin-routes + +## How Users & Permissions routes work +Description: Code example from "How Users & Permissions routes work" +(Source: https://docs.strapi.io/cms/backend-customization/guides/customizing-users-permissions-plugin-routes#how-routes-work) + +Language: JSON +File path: /plugins/users-permissions/server/routes/content-api/user.js + +```json +{ + method: 'GET', // HTTP method + path: '/users', // URL path (relative to /api) + handler: 'user.find', // controller.action + config: { + prefix: '', // path prefix (empty means /api) + }, +} +``` + + +## Extend routes with strapi-server +Description: All customizations to the Users & Permissions plugin go in a single file: +(Source: https://docs.strapi.io/cms/backend-customization/guides/customizing-users-permissions-plugin-routes#extend-routes) + +Language: JavaScript +File path: /src/extensions/users-permissions/strapi-server.js + +```js +module.exports = (plugin) => { + // Your customizations here + + return plugin; +}; +``` + +--- +Language: TypeScript +File path: /src/extensions/users-permissions/strapi-server.ts + +```ts +export default (plugin) => { + // Your customizations here + + return plugin; +}; +``` + + +## 1. Create the policy file +Description: Create a global policy that checks whether the authenticated user matches the target user. +(Source: https://docs.strapi.io/cms/backend-customization/guides/customizing-users-permissions-plugin-routes#1-create-the-policy-file) + +Language: JavaScript +File path: /src/policies/is-own-user.js + +```js +"use strict"; + +module.exports = (policyContext, config, { strapi }) => { + const currentUser = policyContext.state.user; + + if (!currentUser) { + return false; + } + + const targetUserId = Number(policyContext.params.id); + + if (currentUser.id !== targetUserId) { + return false; + } + + return true; +}; +``` + +--- +Language: TypeScript +File path: /src/policies/is-own-user.ts + +```ts +export default (policyContext, config, { strapi }) => { + const currentUser = policyContext.state.user; + + if (!currentUser) { + return false; + } + + const targetUserId = Number(policyContext.params.id); + + if (currentUser.id !== targetUserId) { + return false; + } + + return true; +}; +``` + + +## 2. Attach the policy to the user routes +Description: In the plugin extension file, find the update and delete routes and add the policy: +(Source: https://docs.strapi.io/cms/backend-customization/guides/customizing-users-permissions-plugin-routes#2-attach-the-policy-to-the-user-routes) + +Language: JavaScript +File path: /src/extensions/users-permissions/strapi-server.js + +```js +module.exports = (plugin) => { + // Find the routes that need the policy + const routes = plugin.routes['content-api'].routes; + + // Add the 'is-own-user' policy to the update route + const updateRoute = routes.find( + (route) => route.handler === 'user.update' + ); + + if (updateRoute) { + updateRoute.config = updateRoute.config || {}; + updateRoute.config.policies = updateRoute.config.policies || []; + updateRoute.config.policies.push('global::is-own-user'); + } + + // Add the same policy to the delete route + const deleteRoute = routes.find( + (route) => route.handler === 'user.destroy' + ); + + if (deleteRoute) { + deleteRoute.config = deleteRoute.config || {}; + deleteRoute.config.policies = deleteRoute.config.policies || []; + deleteRoute.config.policies.push('global::is-own-user'); + } + + return plugin; +}; +``` + +--- +Language: TypeScript +File path: /src/extensions/users-permissions/strapi-server.ts + +```ts +export default (plugin) => { + // Find the routes that need the policy + const routes = plugin.routes['content-api'].routes; + + // Add the 'is-own-user' policy to the update route + const updateRoute = routes.find( + (route) => route.handler === 'user.update' + ); + + if (updateRoute) { + updateRoute.config = updateRoute.config || {}; + updateRoute.config.policies = updateRoute.config.policies || []; + updateRoute.config.policies.push('global::is-own-user'); + } + + // Add the same policy to the delete route + const deleteRoute = routes.find( + (route) => route.handler === 'user.destroy' + ); + + if (deleteRoute) { + deleteRoute.config = deleteRoute.config || {}; + deleteRoute.config.policies = deleteRoute.config.policies || []; + deleteRoute.config.policies.push('global::is-own-user'); + } + + return plugin; +}; +``` + +Language: TypeScript +File path: /api/core/strapi/plugin-routes-extension-bc.test.api.ts + +```ts +const { errors } = require('@strapi/utils'); +const { PolicyError } = errors; + +// Inside the policy: +throw new PolicyError('You can only modify your own account'); +``` + + +## Override a controller action +Description: The user controller is a plain object, so you can directly read and replace its methods in the extension file. +(Source: https://docs.strapi.io/cms/backend-customization/guides/customizing-users-permissions-plugin-routes#override-controller) + +Language: JavaScript +File path: /src/extensions/users-permissions/strapi-server.js + +```js +module.exports = (plugin) => { + const originalMe = plugin.controllers.user.me; + + plugin.controllers.user.me = async (ctx) => { + // Call the original controller + await originalMe(ctx); + + // Add extra data to the response + if (ctx.body) { + ctx.body.timestamp = new Date().toISOString(); + } + }; + + return plugin; +}; +``` + +--- +Language: TypeScript +File path: /src/extensions/users-permissions/strapi-server.ts + +```ts +export default (plugin) => { + const originalMe = plugin.controllers.user.me; + + plugin.controllers.user.me = async (ctx) => { + // Call the original controller + await originalMe(ctx); + + // Add extra data to the response + if (ctx.body) { + ctx.body.timestamp = new Date().toISOString(); + } + }; + + return plugin; +}; +``` + + +## Override an auth controller action +Description: To override an auth action, wrap the factory itself: +(Source: https://docs.strapi.io/cms/backend-customization/guides/customizing-users-permissions-plugin-routes#override-auth-route) + +Language: JavaScript +File path: /src/extensions/users-permissions/strapi-server.js + +```js +module.exports = (plugin) => { + const originalAuthFactory = plugin.controllers.auth; + + plugin.controllers.auth = ({ strapi }) => { + // Resolve the original factory to get the controller methods + const originalAuth = originalAuthFactory({ strapi }); + + // Override the register method + const originalRegister = originalAuth.register; + + originalAuth.register = async (ctx) => { + // Call the original register logic + await originalRegister(ctx); + + // Custom post-registration logic + if (ctx.body && ctx.body.user) { + strapi.log.info(`New user registered: ${ctx.body.user.email}`); + } + }; + + return originalAuth; + }; + + return plugin; +}; +``` + +--- +Language: TypeScript +File path: /src/extensions/users-permissions/strapi-server.ts + +```ts +export default (plugin) => { + const originalAuthFactory = plugin.controllers.auth; + + plugin.controllers.auth = ({ strapi }) => { + // Resolve the original factory to get the controller methods + const originalAuth = originalAuthFactory({ strapi }); + + // Override the register method + const originalRegister = originalAuth.register; + + originalAuth.register = async (ctx) => { + // Call the original register logic + await originalRegister(ctx); + + // Custom post-registration logic + if (ctx.body && ctx.body.user) { + strapi.log.info(`New user registered: ${ctx.body.user.email}`); + } + }; + + return originalAuth; + }; + + return plugin; +}; +``` + + +## Add a new route +Description: Code example from "Add a new route" +(Source: https://docs.strapi.io/cms/backend-customization/guides/customizing-users-permissions-plugin-routes#add-new-route) + +Language: JavaScript +File path: /src/extensions/users-permissions/strapi-server.js + +```js +module.exports = (plugin) => { + // Add a new controller action + plugin.controllers.user.deactivate = async (ctx) => { + const { id } = ctx.params; + + const user = await strapi + .plugin('users-permissions') + .service('user') + .edit(id, { blocked: true }); + + ctx.body = { message: `User ${user.username} has been deactivated` }; + }; + + // Register the route + plugin.routes['content-api'].routes.push({ + method: 'POST', + path: '/users/:id/deactivate', + handler: 'user.deactivate', + config: { + prefix: '', + policies: ['global::is-own-user'], + }, + }); + + return plugin; +}; +``` + +--- +Language: TypeScript +File path: /src/extensions/users-permissions/strapi-server.ts + +```ts +export default (plugin) => { + // Add a new controller action + plugin.controllers.user.deactivate = async (ctx) => { + const { id } = ctx.params; + + const user = await strapi + .plugin('users-permissions') + .service('user') + .edit(id, { blocked: true }); + + ctx.body = { message: `User ${user.username} has been deactivated` }; + }; + + // Register the route + plugin.routes['content-api'].routes.push({ + method: 'POST', + path: '/users/:id/deactivate', + handler: 'user.deactivate', + config: { + prefix: '', + policies: ['global::is-own-user'], + }, + }); + + return plugin; +}; +``` + + +## Remove a route +Description: Disable a route by filtering it out of the routes array. +(Source: https://docs.strapi.io/cms/backend-customization/guides/customizing-users-permissions-plugin-routes#remove-route) + +Language: JavaScript +File path: /src/extensions/users-permissions/strapi-server.js + +```js +module.exports = (plugin) => { + plugin.routes['content-api'].routes = plugin.routes['content-api'].routes.filter( + (route) => route.handler !== 'user.count' + ); + + return plugin; +}; +``` + +--- +Language: TypeScript +File path: /src/extensions/users-permissions/strapi-server.ts + +```ts +export default (plugin) => { + plugin.routes['content-api'].routes = plugin.routes['content-api'].routes.filter( + (route) => route.handler !== 'user.count' + ); + + return plugin; +}; +``` + + +## Combine multiple customizations +Description: In practice, you often combine several customizations in the same file. +(Source: https://docs.strapi.io/cms/backend-customization/guides/customizing-users-permissions-plugin-routes#combine-customizations) + +Language: JavaScript +File path: /src/extensions/users-permissions/strapi-server.js + +```js +module.exports = (plugin) => { + const routes = plugin.routes['content-api'].routes; + + // 1. Add 'is-own-user' policy to update and delete + for (const route of routes) { + if (route.handler === 'user.update' || route.handler === 'user.destroy') { + route.config = route.config || {}; + route.config.policies = route.config.policies || []; + route.config.policies.push('global::is-own-user'); + } + } + + // 2. Wrap the 'me' controller to include the user's role + const originalMe = plugin.controllers.user.me; + + plugin.controllers.user.me = async (ctx) => { + await originalMe(ctx); + + if (ctx.state.user && ctx.body) { + const user = await strapi + .plugin('users-permissions') + .service('user') + .fetch(ctx.state.user.id, { populate: ['role'] }); + + ctx.body.role = user.role; + } + }; + + // 3. Add a custom route + plugin.controllers.user.profile = async (ctx) => { + const user = await strapi + .plugin('users-permissions') + .service('user') + .fetch(ctx.state.user.id, { populate: ['role'] }); + + ctx.body = { + username: user.username, + email: user.email, + role: user.role?.name, + createdAt: user.createdAt, + }; + }; + + routes.push({ + method: 'GET', + path: '/users/profile', + handler: 'user.profile', + config: { prefix: '' }, + }); + + return plugin; +}; +``` + +--- +Language: TypeScript +File path: /src/extensions/users-permissions/strapi-server.ts + +```ts +export default (plugin) => { + const routes = plugin.routes['content-api'].routes; + + // 1. Add 'is-own-user' policy to update and delete + for (const route of routes) { + if (route.handler === 'user.update' || route.handler === 'user.destroy') { + route.config = route.config || {}; + route.config.policies = route.config.policies || []; + route.config.policies.push('global::is-own-user'); + } + } + + // 2. Wrap the 'me' controller to include the user's role + const originalMe = plugin.controllers.user.me; + + plugin.controllers.user.me = async (ctx) => { + await originalMe(ctx); + + if (ctx.state.user && ctx.body) { + const user = await strapi + .plugin('users-permissions') + .service('user') + .fetch(ctx.state.user.id, { populate: ['role'] }); + + ctx.body.role = user.role; + } + }; + + // 3. Add a custom route + plugin.controllers.user.profile = async (ctx) => { + const user = await strapi + .plugin('users-permissions') + .service('user') + .fetch(ctx.state.user.id, { populate: ['role'] }); + + ctx.body = { + username: user.username, + email: user.email, + role: user.role?.name, + createdAt: user.createdAt, + }; + }; + + routes.push({ + method: 'GET', + path: '/users/profile', + handler: 'user.profile', + config: { prefix: '' }, + }); + + return plugin; +}; +``` + + + # Middlewares Source: https://docs.strapi.io/cms/backend-customization/middlewares @@ -23424,10 +23950,188 @@ File path: /config/plugins.js|ts ``` -## Session management endpoints -Description: Code example from "Session management endpoints" -(Source: https://docs.strapi.io/cms/features/users-permissions#session-management-endpoints) +## Get the authenticated user +Description: Code example from "Get the authenticated user" +(Source: https://docs.strapi.io/cms/features/users-permissions#get-the-authenticated-user) + +Language: Bash +File path: N/A +```bash +curl -X GET http://localhost:1337/api/users/me \ + -H "Authorization: Bearer your-access-token" +``` + +--- +Language: JSON +File path: N/A + +```json +{ + "id": 1, + "documentId": "abc123", + "username": "kai", + "email": "kai@strapi.io", + "provider": "local", + "confirmed": true, + "blocked": false, + "createdAt": "2024-01-15T09:00:00.000Z", + "updatedAt": "2024-01-15T09:00:00.000Z", + "publishedAt": "2024-01-15T09:00:00.000Z" +} +``` + + +## Find all users +Description: Code example from "Find all users" +(Source: https://docs.strapi.io/cms/features/users-permissions#find-all-users) + +Language: Bash +File path: N/A + +```bash +curl -X GET "http://localhost:1337/api/users?populate=role" \ + -H "Authorization: Bearer your-access-token" +``` + +--- +Language: JSON +File path: N/A + +```json +[ + { + "id": 1, + "documentId": "abc123", + "username": "kai", + "email": "kai@strapi.io", + "provider": "local", + "confirmed": true, + "blocked": false, + "role": { + "id": 1, + "name": "Authenticated", + "description": "Default role given to authenticated user.", + "type": "authenticated", + "createdAt": "2024-01-01T00:00:00.000Z", + "updatedAt": "2024-01-01T00:00:00.000Z" + }, + "createdAt": "2024-01-15T09:00:00.000Z", + "updatedAt": "2024-01-15T09:00:00.000Z", + "publishedAt": "2024-01-15T09:00:00.000Z" + } +] +``` + + +## Create a user +Description: Code example from "Create a user" +(Source: https://docs.strapi.io/cms/features/users-permissions#create-a-user) + +Language: Bash +File path: N/A + +```bash +curl -X POST http://localhost:1337/api/users \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your-access-token" \ + -d '{ + "username": "newuser", + "email": "newuser@strapi.io", + "password": "Password123", + "role": 1, + "confirmed": true + }' +``` + +--- +Language: JSON +File path: N/A + +```json +{ + "id": 2, + "documentId": "def456", + "username": "newuser", + "email": "newuser@strapi.io", + "provider": "local", + "confirmed": true, + "blocked": false, + "createdAt": "2024-01-16T10:00:00.000Z", + "updatedAt": "2024-01-16T10:00:00.000Z", + "publishedAt": "2024-01-16T10:00:00.000Z" +} +``` + + +## Update a user +Description: Code example from "Update a user" +(Source: https://docs.strapi.io/cms/features/users-permissions#update-a-user) + +Language: Bash +File path: N/A + +```bash +curl -X PUT http://localhost:1337/api/users/2 \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your-access-token" \ + -d '{ + "username": "updateduser" + }' +``` + +--- +Language: JSON +File path: N/A + +```json +{ + "id": 2, + "documentId": "def456", + "username": "updateduser", + "email": "newuser@strapi.io", + "provider": "local", + "confirmed": true, + "blocked": false, + "createdAt": "2024-01-16T10:00:00.000Z", + "updatedAt": "2024-01-17T11:00:00.000Z", + "publishedAt": "2024-01-16T10:00:00.000Z" +} +``` + + +## Delete a user +Description: Code example from "Delete a user" +(Source: https://docs.strapi.io/cms/features/users-permissions#delete-a-user) + +Language: Bash +File path: N/A + +```bash +curl -X DELETE http://localhost:1337/api/users/2 \ + -H "Authorization: Bearer your-access-token" +``` + +--- +Language: JSON +File path: N/A + +```json +{ + "id": 2, + "documentId": "def456", + "username": "updateduser", + "email": "newuser@strapi.io", + "provider": "local", + "confirmed": true, + "blocked": false, + "createdAt": "2024-01-16T10:00:00.000Z", + "updatedAt": "2024-01-17T11:00:00.000Z", + "publishedAt": "2024-01-16T10:00:00.000Z" +} +``` + +--- Language: JavaScript File path: N/A diff --git a/docusaurus/static/llms-full.txt b/docusaurus/static/llms-full.txt index 7d96e06f36..af59959f05 100644 --- a/docusaurus/static/llms-full.txt +++ b/docusaurus/static/llms-full.txt @@ -4938,6 +4938,166 @@ To list all the available controllers, run `yarn strapi controllers:list`. +# Customizing Users & Permissions plugin routes +Source: https://docs.strapi.io/cms/backend-customization/guides/customizing-users-permissions-plugin-routes + +# Customizing Users & Permissions plugin routes + +The [Users & Permissions plugin](/cms/features/users-permissions) ships with built-in routes for authentication (`/auth`) and user management (`/users`). Because these routes belong to a plugin rather than a user-created content-type, they cannot be customized with `createCoreRouter`. Instead, extend them through the [plugin extension system](/cms/plugins-development/plugins-extension) using a `strapi-server.js` or `strapi-server.ts` file in the `/src/extensions/users-permissions/` folder. + +:::prerequisites +- A Strapi 5 project with the Users & Permissions plugin installed (included by default). +- Familiarity with [routes](/cms/backend-customization/routes) and [policies](/cms/backend-customization/policies). +::: + +## How Users & Permissions routes work {#how-routes-work} + + + + +Content-types you create (e.g., `api::restaurant.restaurant`) register routes differently. The Users & Permissions plugin registers its routes inside the `plugin.routes['content-api'].routes` array, which contains all `/users`, `/auth`, and `/roles` route definitions. + +Each route is an object with the following shape: + + +```js +{ + method: 'GET', // HTTP method + path: '/users', // URL path (relative to /api) + handler: 'user.find', // controller.action + config: { + prefix: '', // path prefix (empty means /api) + }, +} +``` + +Route configs can also include optional `policies` and `middlewares` arrays (see [Add a custom policy to a user route](#add-custom-policy)). + + +The available `user` controller actions are: `find`, `findOne`, `create`, `update`, `destroy`, `me`, and `count`. + + +The available `auth` controller actions are: `callback` (login), `register`, `forgotPassword`, `resetPassword`, `changePassword`, `emailConfirmation`, `sendEmailConfirmation`, `connect`, `refresh`, and `logout`. + +:::note Understanding the controller types +The `user` controller is a plain object with methods, while the `auth` controller is a factory function `({ strapi }) => ({...})` that Strapi resolves lazily. In the plugin extension file, both are accessible on `plugin.controllers`, but they behave differently when overridden. See [Override a controller action](#override-controller) and [Override an auth controller action](#override-auth-route) for the correct pattern for each. +::: + +## Extend routes with strapi-server {#extend-routes} + + + +All customizations to the Users & Permissions plugin go in a single file: + + + +The function receives the full plugin object and must return it. You can modify `plugin.routes`, `plugin.controllers`, `plugin.policies`, and `plugin.services` before returning. + +## Add a custom policy to a user route {#add-custom-policy} + +A common requirement is restricting who can update or delete user accounts: for example, ensuring users can only update their own profile. + +### 1. Create the policy file + + + + +Create a global policy that checks whether the authenticated user matches the target user. The policy function receives the Koa context (with access to `state.user` and `params`), an optional config object, and `{ strapi }`: + + + +### 2. Attach the policy to the user routes + + + + +In the plugin extension file, find the `update` and `delete` routes and add the policy: + + + +With this configuration, `PUT /api/users/:id` and `DELETE /api/users/:id` return a `403 Forbidden` error if the authenticated user does not match the `:id` in the URL. + +:::tip +For a more informative error message, throw a `PolicyError` instead of returning `false`: + +```js +const { errors } = require('@strapi/utils'); +const { PolicyError } = errors; + +// Inside the policy: +throw new PolicyError('You can only modify your own account'); +``` + +See the [policies documentation](/cms/backend-customization/policies) for more details. +::: + +## Override a controller action {#override-controller} + + + +The `user` controller is a plain object, so you can directly read and replace its methods in the extension file. For instance, to add custom logic to the `me` endpoint: + + + +:::caution +When wrapping a controller, always call the original function first to preserve the default behavior. Skipping the original function means you take over the full request handling, including sanitization and error handling. +::: + +## Override an auth controller action {#override-auth-route} + + + + +The `auth` controller uses a factory pattern: it exports a function `({ strapi }) => ({...})` instead of a plain object. When your extension code runs, Strapi has not yet resolved this factory. As a result, `plugin.controllers.auth` is a function, not an object with methods. + +To override an auth action, wrap the factory itself: + + + +:::caution +Do not access `plugin.controllers.auth.register` directly. Because `auth` is a factory function at extension time, its methods are not accessible until Strapi calls the factory. Always wrap the factory as shown above. +::: + +## Add a new route {#add-new-route} + +You can add custom routes to the Users & Permissions plugin. For example, to add an endpoint that deactivates a user account: + + + + + +After restarting Strapi, `POST /api/users/:id/deactivate` becomes available. Grant the corresponding permission in the admin panel under + + + +## Combine multiple customizations {#combine-customizations} + +In practice, you often combine several customizations in the same file. The following example adds a policy to `update` and `delete`, wraps the `me` controller, and adds a new route: + + + +## Validation + +After making changes, restart Strapi and verify your customizations: + +1. Run `yarn strapi routes:list` to confirm your new or modified routes appear. +2. Test protected routes without authentication to verify policies return `403 Forbidden`. +3. Test with an authenticated user to confirm the expected behavior. +4. Check the Strapi server logs for errors during startup. + +## Troubleshooting + +| Symptom | Possible cause | +| ------- | -------------- | +| Route not found (404) | The new route was not pushed to `plugin.routes['content-api'].routes`, or its `prefix` property is missing. | +| Policy not applied | The policy name is incorrect. Global policies require the `global::` prefix (e.g., `global::is-own-user`). | +| Controller returns 500 | The controller action name does not match the `handler` value in the route definition. | +| Changes not reflected | Strapi was not restarted after modifying the extension file. Extensions are loaded at startup. | +| Permission denied (403) | The new action is not enabled for the role. Enable it in *Users & Permissions plugin > Roles*. | +| Cannot read property of `auth` controller | The `auth` controller is a factory function, not a plain object. Wrap the factory instead of accessing methods directly (see [Override an auth controller action](#override-auth-route)). | + + + # Middlewares Source: https://docs.strapi.io/cms/backend-customization/middlewares @@ -9267,6 +9427,34 @@ By default, Strapi SSO only redirects to the redirect URL that is exactly equal +##### Find all users + +The `GET /api/users` endpoint returns a list of all users. [`populate` and `filters` parameters](/cms/api/rest/parameters) can be passed as query strings. + + + +##### Create a user + +The `POST /api/users` endpoint creates a new user. Unlike `/api/auth/local/register`, this endpoint requires `create` permission for the Users & Permissions plugin. + + + +##### Update a user + +The `PUT /api/users/:id` endpoint updates an existing user by ID. + + + +##### Delete a user + +The `DELETE /api/users/:id` endpoint deletes a user by ID. + + + +To refresh your authentication token, send the following request: + + + To log out of all sessions, send the following request: @@ -9356,6 +9544,12 @@ create: async ctx => { }; ``` +## Customizing routes and policies {#customizing-routes-and-policies} + +The Users & Permissions plugin routes and controllers can be extended and overridden through the [plugin extension system](/cms/plugins-development/plugins-extension). This is useful for adding custom policies to user endpoints, overriding controller logic, or adding new routes. + +For a complete guide with step-by-step instructions and code examples, see [Customizing Users & Permissions plugin routes](/cms/backend-customization/guides/customizing-users-permissions-plugin-routes). + # Installation diff --git a/docusaurus/static/llms.txt b/docusaurus/static/llms.txt index 45429e3d1d..b933b5f438 100644 --- a/docusaurus/static/llms.txt +++ b/docusaurus/static/llms.txt @@ -55,6 +55,7 @@ - [Upload files](https://docs.strapi.io/cms/api/rest/upload): Learn how to use the /api/upload endpoints to upload files to Strapi with the REST API. - [Back-end customization](https://docs.strapi.io/cms/backend-customization): Strapi’s back end is a Koa-based server where requests pass through global middlewares, routes, controllers, services, and models before the Document Service returns responses. - [Controllers](https://docs.strapi.io/cms/backend-customization/controllers): Controllers bundle actions that handle business logic for each route within Strapi’s MVC pattern. This documentation demonstrates generating controllers, extending core ones with createCoreController, and delegating heavy logic to services. +- [Customizing Users & Permissions plugin routes](https://docs.strapi.io/cms/backend-customization/guides/customizing-users-permissions-plugin-routes): The Users & Permissions plugin exposes /users and /auth routes that can be extended or overridden using the plugin extension system. This guide shows how to add custom policies, override controllers, and add new routes to the User collection. - [Middlewares](https://docs.strapi.io/cms/backend-customization/middlewares): Middlewares alter the request or response flow at application or API levels. This documentation distinguishes global versus route middlewares and illustrates custom implementations with generation patterns. - [Models](https://docs.strapi.io/cms/backend-customization/models): Models define Strapi’s content structure via content-types and reusable components. This documentation walks through creating these models in the Content-type Builder or CLI and managing schema files with optional lifecycle hooks. - [Policies](https://docs.strapi.io/cms/backend-customization/policies): Policies execute before controllers to enforce authorization or other checks on routes. Instructions in this documentation cover generating global or scoped policies and wiring them into router configs.