KTPV5 Retail POS

Roles & Scopes

User permission scopes and how they are enforced on the server and in the app.

Each User has a scope: string[]. There are no role objects — authorization is a flat set of scope strings. admin is a superuser that bypasses every check.

Scope strings

The editable scope set (app components/user/UserForm.tsx) is:

ScopeGrantsServer-enforced?
adminEverything (bypasses all scope checks)yes (bypass)
saleCreate sales & queries; vouchers; customer vouchersyes
refundRefund & repay operationsyes
shiftOpen / close shiftyes
cashioCash in / outyes
storeEdit store settings (POST /api/store)yes
userManage usersyes
interfaceInterface / device settingsclient-gated (no server route gate found)
hotkeyHotkey managementclient-gated (hotkey routes have no scopeMiddleware)

interface and hotkey gate UI access in the app; the corresponding server routes are open or rely on terminal/user middleware only. Read endpoints like GET /api/store, GET /api/user/code, GET /api/terminal/me, GET /api/hotkey are intentionally open.

Enforcement

Server

scopeMiddleware(scope) guards routes (retail_pos_server/src/v1/user/user.middleware.ts):

export function scopeMiddleware(scope: string) {
  return (req, res, next) => {
    const user = res.locals.user;
    if (!user) throw new UnauthorizedException("Unauthorized");
    if (!user.scope.includes("admin") && !user.scope.includes(scope)) {
      throw new UnauthorizedException("Insufficient permissions");
    }
    next();
  };
}

The user is resolved earlier from the Authorization: Bearer <userId>%%%<lastSignedAt> token by userMiddleware.

App

The renderer mirrors the same rule with hasScope (retail_pos_app/src/renderer/src/libs/scope-utils.ts):

export default function hasScope(userScopes: string[], requiredScopes: string[]) {
  if (userScopes.includes("admin")) return true;
  return requiredScopes.every((scope) => userScopes.includes(scope));
}

The app uses this to gate management screens; the server enforces it authoritatively on each request.

On this page