Skip to content
Back to skills

Nocobase App Plugin Users

ASecurity

Integrate the Users plugin into a NocoBase App, build a user management page on its API and configure its permissions, or add an application-owned role scope through the public Server contracts.

  • 24,837 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added November 5, 2026
ai-agentsgoreactapidatabasesecurity

Works with

  • cli
  • api

Security analysis

A100/100

Scanned November 5, 2026

npx -y skills add nocobase/nocobase --skill nocobase-app-plugin-users --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Nocobase App Plugin Users?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Nocobase App Plugin Users
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/nocobase-nocobase-app-plugin-users/badge)](https://www.skillsdirectory.com/skills/nocobase-nocobase-app-plugin-users)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

SKILL.md
---
name: nocobase-app-plugin-users
description: Integrate the Users plugin into a NocoBase App, build a user management page on its API and configure its permissions, or add an application-owned role scope through the public Server contracts.
metadata:
  short-description: Integrate reusable user administration
---

# User Management App Plugin

Use this Skill when an application needs user management, or when another
plugin needs to expose application-specific roles in user management. Do
not use it to modify the Users plugin source or to replace Authentication's
user, account, or Session storage.

## Public surfaces

- Client registration factory and `UsersClientOptions`:
  `@nocobase/app-plugin-users/client`. It contributes only the public
  invitation page at `/invite/:token`; there is no user management page.
- Typed API client: `UsersClient` and its types from
  `@nocobase/app-plugin-users/client/user-client`.
- Server contracts: `userManagementServiceToken`,
  `userRoleScopeRegistryToken`, `UserRoleScope`, and related types from
  `@nocobase/app-plugin-users/server/tokens`.
- HTTP API: `GET /api/users` (`page`, `pageSize`, `q`, `status`, `roleScope`, `role`, `sort=name`, `order=asc|desc`; answers `{ data, meta: { page, pageSize, total } }`), `GET /api/users/options`, `GET /api/users/:userId` (latest managed profile and role scopes, requires `user/read`, 404 for absent accounts), `POST /api/users`, `PATCH /api/users/:userId`, `DELETE /api/users/:userId?confirm=true`, `POST /api/users/:userId/disable|enable|resetPassword|revokeSessions`, and `PUT /api/users/:userId/roleScopes/:scope`. The running application documents each one, with request and response schemas, at `/api/swagger/docs` (JSON at `/api/swagger`) under the `Users` tag. Failures use the standard error body; branch on `error.reason`, such as `USER_NOT_FOUND`, `SELF_DELETE_NOT_ALLOWED`, `LAST_ASSIGNMENT` or `USER_EMAIL_CONFLICT`.

`UsersClient.update(userId, { name?, username?, email?, roleScopes? })` supports one atomic editor Save. Profile fields require `update`; submitted `roleScopes` additionally require `assign-role`. A roles-only payload requires `assign-role` without `update`. The service validates scope values, runs each applicable target-account guard, and commits profile changes and all submitted scope replacements in the caller-owned transaction, notifying permission clients only after commit. Omitted scopes are preserved. Keep the existing per-scope PUT for callers needing only one scope. Do not implement a combined Save as sequential profile and role requests: a later failure would leave a partially saved account.

Every HTTP route requires Authentication and Authorization. Routes check the `user` record type with the applicable action from `read`, `create`, `update`, `disable`, `enable`, `assign-role`, `reset-password`, `revoke-sessions` or `delete`. Account creation checks both `create` and `assign-role`. `delete` is permitted only while an application role scope can clean a deleted user up.

## Register the plugin

1. Register Authentication, Authorization, and then Users in the App's Client
   and Server plugin arrays. The Client factory takes no placement options:
   `users()`.
2. Grant only the `user` actions those roles need, on `{ type: 'user', id: '*' }` because `user` is a record type. The plugin creates no roles and grants no access by itself.
3. For a user management page, build it in the App on `UsersClient` and declare it among the App's own routes with the page access it needs; the plugin ships none.

The invitation page reports results through `useToaster()` from `@nocobase/app-client`, so the App needs the `@nocobase/app-client` that exports it and registers a toaster service, as the templates do: `client/lib/toaster.ts` from the template, and `this.app.container.instance(toasterToken, createToaster())` in the `register()` of `client/service-provider.ts`, with the `Toaster` component mounted in `client/react-providers.ts`. Without the registration nothing throws, but its toasts are only logged to the browser console.

The default `app` permission-set scope is supplied by Users whenever the Authorization plugin's `authorizationToken` is available. Do not copy a user-roles Provider into an application. Set `users.permissionSets: false` to replace the default with an application-owned scope.

## Add an application role scope

Resolve `userRoleScopeRegistryToken` in an application or business plugin
ServiceProvider and register one `UserRoleScope` during `boot()`. The scope owns
its available options, current assignment lookup, role filtering, atomic
replacement, and any disable guard. Unregister it during `shutdown()`.

Use the `DatabaseConnection` passed to each scope method. This is the
caller-owned transaction used to keep account creation or state changes atomic
with role assignments. Do not open an unrelated transaction and do not write
Authentication's internal tables.

Guard who may act on whom with `assertCanAdminister(userId, actorId, action, connection)`: the management API calls it for `update`, `disable`, `enable`, `reset-password`, `revoke-sessions` and `assign-role` with the authenticated actor, in the action's transaction and before anything changes, after authorization of the action itself has passed. Throw a `UserRoleScopeError` to refuse, for example when someone who is not an application owner resets an owner's password; the route answers `FAILED_PRECONDITION` with the scope's reason. Service callers that pass no `actorId` are the application's own server-side work and are not asked. Deletion has its own `assertCanDelete(userId, actorId, connection)`.

For a required single-role scope, set `selection: 'single'` and
`requiredOnCreate: true`. Reject invalid values in the scope and enforce
business invariants such as the last-administrator rule on the server.

Set `assignable: false` or `removable: false` on an option when user management
must show a protected assignment but must not add or revoke it. Enforce the
same rule in `replace()` because these flags only control the Client. Set
`hasAuthenticatedDefaultAccess: true` when the scope lists direct assignments
but all signed-in users also inherit separately configured default access, so a
user management page can explain that distinction instead of treating the
default as a role.
Use `labelI18nKey` with `labelI18nNs` on a scope or option when its owner has
registered Client locale resources; keep `label` as the readable fallback.
Implement optional `getMany()` when assignments can be read as a batch. Users
uses it for list pages and falls back to `get()` for existing scopes.

## Invite users

Holders of `invite` on `user` can create invitations through `POST /api/users/invitations` or `UsersClient`; choosing roles also requires `assign-role`. Results include a shareable link even when email succeeds. A queued invitation closed or rotated by another request returns `emailSent=false` without a link, while the other recipients still complete. Original inviters can renew with `sendEmail=false`, which invalidates the old link. Plugin-specific invitations must use their domain endpoint, which rechecks its own grants. Other managers may resend email but receive no link. Tokens last seven days; only hashes are stored.

New accounts register directly from the invitation link with a name and password; acceptance leaves `emailVerified=false`. Email delivery is optional: authorized original inviters can privately forward the link when the email channel is absent or fails. Links use `app.publicOrigin` when configured, otherwise the invitation request origin. Existing accounts must sign in with the invited email. Acceptance consumes only the supplied invitation and applies only its grants; handlers run in its transaction and roll back account creation on failure.

## Ownership

- Authentication owns user identity, credentials, account state, password
  hashing, and Sessions.
- Authorization owns Permission Sets, grants, and assignments.
- Users owns the management API, the invitation page, orchestration transaction,
  `user` authorization handler, and role-scope registry.
- The App or business plugin owns role definitions, role grants, assignments,
  the user management page, and role-specific invariants.
- The plugin's `skills/` source is authoritative. `.agents/skills/` is a
  synchronized copy and must not be edited.

## Permissions and constraints

- Browser route access is only navigation control. The Server independently
  authenticates and authorizes every request.
- A conditional grant is not accepted as an unrestricted user-management
  grant; use explicit static grants for this resource.
- Disabled users are rejected by Authentication and lose their existing HTTP
  Sessions and Realtime connections.
- Password hashes, Session tokens, reset tokens, and submitted passwords are
  never returned by the service or included in security events.

## Verification

- Anonymous API requests return `401`; authenticated requests without the
  requested `user` action return `403`.
- Creating a user with a required role scope creates both records, while role
  assignment failure rolls back the user.
- Duplicate emails or usernames return a stable `409` conflict. Password reset
  and database Session revocation commit or roll back together.
- Disabling a user invalidates HTTP Sessions and Realtime connections; enabling
  the user requires a new login.
- Role changes become visible after transaction commit and do not alter
  assignments outside the registered scope.
- The target App passes its relevant tests, typecheck, and build. Skill
  synchronization alone proves only that the copy matches this source.

Deletion uses `DELETE /api/users/:userId?confirm=true` and `user/delete` authorization. Obtain an explicit user deletion request before calling it. Application role scopes can guard deletion and clean dependent credentials transactionally. Historical user identities are retained but cannot sign in or appear in management lists.

Invitation emails use `notificationService.sendTransient()` so credentials never enter notification message snapshots or job storage. This performs one bounded attempt without durable retries or deduplication. Failures retain safe status/category diagnostics without raw provider messages. The inviter can generate and privately deliver a new link if delivery fails. Historical notification snapshots are not rewritten.

## Creation defaults and existing applications

Apply an option scope’s `defaultValue` only when creating a new account. An explicit empty array takes precedence; editing uses saved roles and never reapplies defaults. Member is a visible direct role that can be removed. A roleless person retains authenticated account basics but no role-granted business access. Upgrade Authentication, Authorization, and Users together, stop old writers before applying database tasks, and merge the template user pages and translations separately: upgrading plugins does not replace application-owned source. The new Authorization seed preserves existing users before removing Member’s global assignment; never recreate that assignment in application seeds or startup reconciliation.

## Invitation email

Prefer application `mailers`, optionally selected by `users.invitations.mailer`. Selection is explicit mailer, explicit legacy emailChannel, mailers.default, then implicit system-email. The new path needs no Notification installation; legacy sendTransient retains no-record/no-retry behavior. Users stops new invitation work, closes its own sender and waits for sends in parallel; it waits through invitation bookkeeping before the database closes. A timed-out invitation retains its real in-flight slot and is never automatically resent. The outer application shutdown budget remains `APP_SHUTDOWN_TIMEOUT_MS`. Declare mailers with `defineMailersConfig` and explicit `mailerEnvironment` mappings from `@nocobase/app-server/mailers/config`; restart after changing senders. Existing invitation permissions, tokens, share links and manual resend remain unchanged.

## User language preferences

The users Client plugin automatically integrates with `useAppLocale()` through the optional `AppLocalePreferenceContext` from `@nocobase/app-client`. Explicit selections store a JSON string at `i18n.locale` in `userPreferences`. Restore a validated server value before browser cache; if absent or unsupported, use the application default without saving it. Do not migrate unowned legacy browser locales into an account. Anonymous choices remain separate, and identity transitions invalidate old locale operations, including when Authorization remounts providers.

For recipient-language jobs, import `resolveUserLocale` and `userPreferencesServiceToken` from `@nocobase/app-plugin-users/server` and `i18nToken` from `@nocobase/app-server/i18n`. Call `resolveUserLocale(container.resolve(userPreferencesServiceToken), container.resolve(i18nToken), userId)`: a supported string preference wins, then a supported application default, then `en-US`. Load that locale with `ensureLocaleLoaded` before calling `getFixedT(namespace, locale)`. Do not derive durable language from a request session or `Accept-Language`; request negotiation remains session `locale`, header, then default. The package README contains the complete example and cache compatibility notes.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.