Skip to main content

Permissions

How an endpoint declares what it needs

Every protected controller method is annotated with @RequirePermissions(Permission.X) (or several). A global PermissionsGuard reads that decorator and checks the requesting user's JWT for a matching permission string:

@RequirePermissions(Permission.MANAGE_USERS)
@Get()
async findAll() { ... }

If the permission is missing, the guard throws a ForbiddenException — a 403, not a 401. A logged-in user with the wrong role gets a clean "you're allowed in, but not to do this" response, distinct from "you're not authenticated at all."

A handful of endpoints are marked @Public() instead (auth's own login/refresh, and the health check) — those skip the permission check entirely, by design.

The 4 built-in system roles

Seeded once, in migration 1782134698173-CreateRolesAndAddRoleIdToUsers, and marked isSystem: true (can't be deleted through the API):

RolePermissions
storeKeeperREAD, WRITE, VIEW_REPORTS, VERIFY_BATCH
storeManagerREAD, WRITE, VIEW_REPORTS, APPROVE_ORDER, ISSUE_STOCK, ADJUST_INVENTORY, VERIFY_BATCH
CentralUserREAD, VIEW_REPORTS, EXPORT, ADJUST_INVENTORY
Administratoreverything — full access, including MANAGE_USERS, MANAGE_ROLES, MANAGE_CONFIG

New roles can be created through the /roles API with any combination of permission strings — the 4 above are just the starting set, not a closed list.

One real gotcha: MANAGE_USERS and organisation level

UsersService.create() refuses to assign a role that has MANAGE_USERS to a user whose organisation isn't the main store (orgUnit.isMainStore). In practice: admin-level roles are central-only, enforced in code, not just convention.

First-run bootstrap

On a completely empty database, the very first application boot auto-creates a root Location (name/code from DEFAULT_ORG_NAME/ DEFAULT_ORG_CODE env vars, or Location01/ROOT if unset) and one admin01 user with a random temporary password, assigned the Administrator role. The password is logged once, at boot, and nowhere else — if you miss it, tempPassword is still readable via GET /users by another admin, until it's changed.