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.