Skip to content

Permissions ​

A permission is a fine-grained capability string — edit-posts, delete-users, publish-comments. Permissions can be attached to users directly, or indirectly through Roles and Permission Groups.

When permission.can() runs a check it looks through all three vectors in order:

  1. Direct permissions assigned to the user
  2. Permissions on all roles the user holds
  3. Permissions on all groups the user belongs to

Assigning permissions directly to users ​

ts
await permission.assign.permission(userId, 'edit-posts')
await permission.assign.permission(userId, 'view-drafts')

Assigning a permission the user already has is safe — it is a no-op.

Revoking permissions ​

ts
await permission.revoke.permission(userId, 'edit-posts')

WARNING

Revoking a permission that was never assigned (or does not exist) throws a PermissionError with code NOT_FOUND.

ts
import { PermissionError } from '@actinode/express-permission'

try {
  await permission.revoke.permission(userId, 'ghost-perm')
} catch (err) {
  if (err instanceof PermissionError) {
    console.log(err.code)    // 'NOT_FOUND'
    console.log(err.message) // 'Permission "ghost-perm" not found'
  }
}

Getting all direct permissions for a user ​

This returns only the permissions assigned directly to the user — not those inherited through roles or groups.

ts
const perms = await permission.get.permissions(userId)
// ['edit-posts', 'view-drafts']

Checking permissions in middleware ​

Use permission.can() to protect a route:

ts
app.get('/posts',
  permission.can('view-posts'),
  (req, res) => res.json({ posts: [] }),
)

app.post('/posts',
  permission.can('create-posts'),
  (req, res) => res.json({ created: true }),
)

app.delete('/posts/:id',
  permission.can('delete-posts'),
  (req, res) => res.json({ deleted: true }),
)

Unauthorized (no userId):

json
{ "error": "Unauthorized", "message": "User not authenticated" }

HTTP status: 401

Forbidden (lacks permission):

json
{ "error": "Forbidden", "message": "Missing permission: delete-posts" }

HTTP status: 403

Checking permissions via helpers ​

Use permission.check.can() in business logic:

ts
const canEdit = await permission.check.can(userId, 'edit-posts')

if (!canEdit) {
  throw new Error('Not allowed')
}

canAny — require at least one permission ​

canAny passes if the user holds at least one of the listed permissions. Use it when multiple roles or levels should all have access.

Middleware:

ts
app.put('/posts/:id',
  permission.canAny(['edit-posts', 'manage-posts']),
  async (req, res) => {
    await updatePost(req.params.id, req.body)
    res.json({ updated: true })
  },
)

Helper:

ts
const canProceed = await permission.check.canAny(userId, ['edit-posts', 'manage-posts'])

TIP

canAny is equivalent to an OR check. Great for situations like: "editors or admins can do this."

canAll — require every permission ​

canAll passes only if the user holds every listed permission. Use it when an action requires multiple capabilities together.

Middleware:

ts
app.post('/posts/:id/publish',
  permission.canAll(['edit-posts', 'publish-posts']),
  async (req, res) => {
    await publishPost(req.params.id)
    res.json({ published: true })
  },
)

Helper:

ts
const canPublish = await permission.check.canAll(userId, ['edit-posts', 'publish-posts'])

TIP

canAll is equivalent to an AND check. Great for two-factor capabilities: "you must be able to edit and publish."

canAny vs canAll — quick comparison ​

canAnycanAll
Passes whenuser has at least oneuser has all
Equivalent toORAND
Typical usemultiple roles allowedcompound capability required

Assigning permissions to roles ​

See Roles → Assigning permissions to a role.

Real-world example ​

ts
// Direct permissions for a service account
await permission.assign.permission(serviceAccountId, 'read-metrics')
await permission.assign.permission(serviceAccountId, 'read-logs')

// Route: only users who can both read AND export
app.get('/reports/export',
  permission.canAll(['read-metrics', 'export-data']),
  async (req, res) => {
    const report = await buildReport()
    res.json(report)
  },
)

// Route: editors or admins can update
app.patch('/content/:id',
  permission.canAny(['edit-content', 'admin-content']),
  async (req, res) => {
    const content = await updateContent(req.params.id, req.body)
    res.json(content)
  },
)

Need an admin panel?

Get a beautiful admin UI for all actinode packages. Contact us to learn more about our premium admin package.

No spam, ever. Unsubscribe at any time.

Released under the MIT License.