Skip to content

Deep-Dive: User Management & RBAC System

Area: #4 — User Management & RBAC (Full-Stack) Generated: 2026-02-25 | Last Updated: 2026-02-25 | Scan Level: Exhaustive | Workflow: document-project v1.2.0 Files Analyzed: 85+ (38 backend + 47+ frontend) Lines of Code Scanned: ~16,100+ (backend ~4,500 + frontend ~11,600+) Known Issues Found: 76 (11 Critical, 16 High, 26 Medium, 23 Low) Cross-references: Auth Deep-Dive (23 overlapping issues), API Reference (23 undocumented endpoints found) Security Assessment: RBAC Attack Scenarios — Full privilege escalation chain documented (zero-permission user → super_admin in 1 API call) Post-Scan Update: Backend refactored Feb 18-22 — middleware.ts now active (was dead code), new permissionMiddleware.ts added (third permission system), contextMiddleware now attaches permissions to req.user, auth.ts now loads userRoles


Table of Contents

  1. Executive Summary
  2. File Inventory
  3. Database Schema
  4. Backend Modules
  5. Permission System Architecture
  6. Frontend Architecture
  7. API Endpoint Reference
  8. Data Flow & User Journeys
  9. Dependency Graph
  10. Known Issues
  11. Seed Data
  12. Risks & Contributor Guidance

1. Executive Summary

The User Management & RBAC system provides administrative CRUD operations for users, roles, permissions, user settings (2FA), and an invite-only registration flow. It implements a hybrid role model: each user has one primary role (User.roleIdRole) plus zero or more extra roles via a many-to-many join table (user_roles). Permissions are route-string based (dot notation, e.g., user.create) and assigned to roles, not directly to users. A super_admin role bypasses all permission checks.

Key characteristics: - 5 backend modules (User, Role, Permission, InviteUser, UserSetting) + 8 middleware/shared dependency files — 85+ files total - 6 TypeORM entities (User, Role, Permission, UserRole, UserSetting, InviteUser) - Full soft-delete lifecycle: trash → restore → permanent delete (all entities) - Bulk operations: bulk delete, bulk restore, bulk permanent delete - Three parallel permission systems: middleware v1 (broken, 1 route), helper (primary, 30+ services), middleware v2 (new, 1 route) - Frontend: 57 files — 4 API hook files, 9 page components, 3 permission wrappers, sidebars, auth pages, settings, providers - 87 seeded permissions across 11 display groups - 76 issues found (11 Critical, 16 High, 26 Medium, 23 Low) — all attack vectors re-verified STILL VULNERABLE — all claims verified against actual code — expert panel found v2 middleware blocks super_admin (#76)


2. File Inventory

2.1 Backend — Entities (6 files, ~310 LOC)

File LOC Purpose
src/entity/User.ts 134 Core user entity: auth, profile, role binding, soft-delete
src/entity/Role.ts 57 Role definition with M2M permissions and user bindings
src/entity/Permission.ts 37 Route-based permission string with readonly flag
src/entity/UserRoles.ts 37 Explicit join entity for user↔role M2M (partially orphaned)
src/entity/UserSetting.ts 64 2FA config: provider, secrets, backup codes, language
src/entity/InviteUser.ts 38 Invite-only registration with PENDING/ACCEPTED status

2.2 Backend — Modules (21 files, ~2,200 LOC)

Module Files LOC Purpose
User controller, service, routes, validation ~890 User CRUD, profile, analytics, trash lifecycle
Role controller, service, routes, validation, repository ~570 Role CRUD with permission assignment, caching
Permission controller, service, routes, validation ~670 Permission CRUD, role granting, grouped views
InviteUser controller, service, routes, validation ~470 Invite-only flow: create/check/resend, email dispatch
UserSetting controller, service, routes, validation ~450 2FA setup (email + Google Authenticator), backup codes

2.3 Backend — Supporting & Infrastructure Files (14 files, ~1,780 LOC)

File LOC Purpose
src/helper/hasPermission.ts 296 Core permission checking engine — cache + DB fallback (service-level)
src/helper/checkPermissionAndThrow.ts 23 Wrapper that throws ApiError on permission failure
src/app/middlewares/auth.ts 353 JWT auth + refresh token — loads user with userRoles via QueryBuilder
src/app/middlewares/contextMiddleware.ts 93 AsyncLocalStorage context + permission cache init + attaches Set to req.user
src/app/middlewares/permissionMiddleware.ts 39 NEW (Feb 19) Route-level permission gate using req.user.permissions Set
src/app/middlewares/hasPermission.ts 79 Legacy route-level permission gate — route format, broken (1 route uses it)
src/app/middlewares/middleware.ts 95 Now active (Feb 19) Primary middleware pipeline — security, CORS, parsers, context
src/shared/requestContext.ts 122 AsyncLocalStorage per-request state
src/shared/permissionCache.ts 183 Per-request permission cache (Set-based)
src/shared/permissionNormalizer.ts 215 Dead code — bidirectional format converter, all calls commented out
src/seed/seed.ts 100 Seeds roles, permissions, settings, super admin
src/seed/data/permission.ts 454 87 seeded permission definitions
src/seed/role.seed.ts 36 Seeds admin, user, super_admin roles
src/seed/permission.seed.ts 25 Bulk-inserts permissions (all-or-nothing)

2.4 Frontend — API Hooks (4 files, ~1,024 LOC)

File LOC Exports
src/api/users/index.js 273 11 hooks: CRUD + bulk ops + profile
src/api/roles/index.js 217 9 hooks: CRUD + bulk ops
src/api/permissions/index.js 279 10 hooks: CRUD + bulk ops + 2 list variants
src/api/invited-users/index.js 255 12 hooks: CRUD + bulk ops + resend + check

2.5 Frontend — Components (12 files, ~5,905 LOC)

File LOC Purpose
dashboard/users/users-page.jsx 1120 Users management table with analytics, filters, bulk ops
dashboard/users/user-form.jsx 512 User create/edit modal with role assignment
dashboard/roles-permissions/roles-page.jsx 787 Roles table with user/permission display
dashboard/roles-permissions/role-form.jsx 413 Role create/edit with grouped permission checkboxes
dashboard/roles-permissions/permissions-page.jsx 804 Flat permissions table with full CRUD
dashboard/roles-permissions/permissions-group-page.jsx 497 Grouped permissions view (partially disabled)
dashboard/roles-permissions/permission-form.jsx 217 Permission create/edit modal
dashboard/invited-users/invited-users-page.jsx 859 Invited users table with status tracking
dashboard/invited-users/invite-form.jsx 176 Invite email form
custom/permission-wrapper.jsx 67 Route/component-level permission gate
custom/nav-permission-wrapper.jsx 63 Navigation-aware permission wrapper
custom/permission-badge-group.jsx 33 Truncated permission badge list

2.6 Frontend — Pages (5 files, ~132 LOC)

Route File LOC
/dashboard/users page.js 9
/dashboard/roles-permissions/roles page.js 9
/dashboard/roles-permissions/permissions page.js 9
/dashboard/roles-permissions/permissions-group page.js
/dashboard/invited-users page.js 13
/dashboard/permissions page.jsx 92 (debug/test page)

3. Database Schema

3.1 Entity Relationship Diagram

┌─────────────┐      1:N       ┌──────────┐      M:M      ┌──────────────┐
│    User      │───────────────▶│   Role   │◀─────────────▶│  Permission  │
│              │  (roleId FK)   │          │ (role_permission│              │
│ id           │                │ id       │  join table)   │ id           │
│ name         │                │ name     │                │ route        │
│ email        │   M:M          │ displayName               │ displayName  │
│ password     │◀──────────────▶│ isDeleted│                │ readonly     │
│ roleId (FK)  │ (user_roles    │ deletedAt│                │ isDeleted    │
│ avatar       │  join table)   └──────────┘                │ deletedAt    │
│ phone        │                                            └──────────────┘
│ address      │
│ status       │   1:1          ┌──────────────┐
│ provider     │───────────────▶│ UserSetting  │
│ inviteOnly   │                │ id           │
│ isDeleted    │                │ userId (FK)  │
│ deletedAt    │                │ isTwoFactorEnabled
│ stripeCustomerId              │ twoFactorProvider
│ lemonSqueezyCustomerId        │ twoFactorSecret
└─────────────┘                 │ tempTwoFactorSecret
                                │ backupCodes  │
                                │ language     │
                                │ deletedAt    │
                                └──────────────┘

┌───────────────────┐           ┌──────────────┐
│  UserRole         │           │  InviteUser  │
│  (entity: mostly  │           │ (user_invites│
│   orphaned)       │           │  table)      │
│ id                │           │ id           │
│ userId (FK→User)  │           │ email        │
│ roleId (FK→Role)  │           │ status       │
│ createdAt         │           │ deletedAt    │
│ deletedAt         │           └──────────────┘
└───────────────────┘

3.2 Join Tables

Table Columns Created By
user_roles userId, roleId User.userRoles @JoinTable (M2M)
user_role id, userId, roleId, createdAt, updatedAt, deletedAt UserRole entity @Entity()
role_permission roleId, permissionId Role.permissions @JoinTable (M2M)

3.3 Indexes

User: 6 compound indexes for query optimization: - (status, deletedAt), (email, deletedAt), (roleId, deletedAt), (status, roleId, deletedAt), (provider, email), (inviteOnly, status) - Individual: status, provider

Role: Partial unique index IDX_ROLE_NAME_ACTIVE on name where deletedAt IS NULL

Permission: Index on route (also has unique: true constraint)

UserSetting: Index on userId, index on twoFactorSecret (unnecessary — secret shouldn't be indexed)

3.4 Enums

Enum Entity Values
Provider User credential, google, facebook, github, otp
Status User active, inactive, suspend
TwoFactorProvider UserSetting email, google_authenticator, sms
InviteStatus InviteUser PENDING, ACCEPTED

4. Backend Modules

4.1 User Module

Path: src/app/modules/v1/User/

Routes (registered at /api/v1/users):

Method Path Auth Context Validation Handler Permission
GET /profile auth() No getProfile — (own user)
PATCH /profile auth() No updateUserValidation updateProfile — (own user)
GET /my-permissions auth() No myPermissions — (own user)
GET / auth() Yes getUserList user.view (route-level via permission() middleware v2 + service-level)
POST / auth() Yes createUserValidation createUser user.create
PATCH /bulk-delete auth() Yes deleteUsersByIds deleteUsersByIds user.delete
PATCH /bulk-restore auth() Yes bulkUserRestore bulkUserRestore user.restore
PATCH /:id auth() Yes updateUserValidation updateUser user.update
PATCH /:id/restore auth() Yes restoreUser user.restore
GET /:id auth() Yes getUserById user.read
DELETE /:id auth() Yes deleteUser user.delete
DELETE /:id/permanent auth() Yes deletePermanentUser user.delete

Service exports: createUser, updateUser, deleteUser, getUserList, getProfile, updateProfile, getUserById, deleteUsersByIds, restoreUserFromTrash, deletePermanentUser, userAnalytics, saveLemonSqueezyCustomerId, bulkUserRestore

Key behaviors: - getUserList filters out super_admin users from results (line 187) - getUserList returns analytics (total, paid, active, pending) alongside paginated data - createUser checks for soft-deleted users with same email (but rejects them instead of restoring) - updateUser has duplicated userRoles validation logic (see Issue #14) - updateProfile handles avatar upload with local/cloud branching - deleteUser uses TypeORM softDelete (sets deletedAt) - deletePermanentUser uses repo.remove() (hard delete)

4.2 Role Module

Path: src/app/modules/v1/Role/

Routes (registered at /api/v1/roles):

Method Path Auth Context Validation Handler Permission
GET / auth() Yes getRoleList role.view
GET /:id auth() Yes getRoleById role.read
POST / auth() Yes createRoleValidation createRole NONE (commented out)
PATCH /bulk-delete auth() Yes deleteRoleByIds deleteRoleByIds role.delete
PATCH /bulk-restore auth() Yes bulkRoleRestore bulkRoleRestore role.restore
PATCH /:id auth() Yes updateRoleValidation updateRole role.update
PATCH /:id/restore auth() Yes restoreFromTrash role.restore
DELETE /:id auth() Yes deleteRole role.delete
DELETE /:id/permanent auth() Yes deleteRolePermanent role.delete

Key behaviors: - getRoleList uses in-memory cache (node-cache) with 24-hour TTL, keyed by stringified query - createRole auto-restores soft-deleted roles with matching name (un-deletes) - createRole calls PermissionService.updateRolePermissions() to assign initial permissions - createRole has no permission check (commented out at line 150) - updateRole validates name uniqueness (excluding self) and displayName uniqueness - restoreFromTrash checks for name conflicts before restoring - upsertRole is used by seed only - Validation forbids reserved names: user, admin, super-admin, super_admin - All mutations call deleteCacheByPrefix("roles:") to invalidate cache

4.3 Permission Module

Path: src/app/modules/v1/Permission/

Routes (registered at /api/v1/permissions):

Method Path Auth Context Validation Handler Permission
GET /all auth() Yes getAllPermissions — (no check)
GET /:permissionId auth() Yes getPermissionById permission.read
GET / auth() Yes getPermissions permission.view
GET /user-permissions auth() Yes getUserPermissions
POST /check auth() Yes update schema hasPermission
PATCH /bulk-delete auth() Yes deletePermissionByIds deletePermissionByIds permission.delete
PATCH /bulk-restore auth() Yes bulkPermissionRestore bulkPermissionRestore permission.restore
PATCH /:permissionId auth() Yes update schema updatePermissionRoute permission.update
PUT /grant auth() Yes grantPermissionToRole grantPermissionToRole — (no check)
POST / auth() Yes create schema createPermissions permission.create
DELETE /delete auth() Yes deletePermissionByIds deletePermissionByIds permission.delete
DELETE /:permissionId auth() Yes deletePermissionById permission.delete
PATCH /:permissionId/restore auth() Yes restorePermissionFromTrash permission.restore
DELETE /:permissionId/permanent auth() Yes deletePermanentPermissionById permission.delete

Key behaviors: - getPermissions groups by displayName and adds action suffix analysis - getAllPermissions returns flat paginated list (separate from grouped view) - createPermission auto-assigns new permission to super_admin role - createPermission auto-restores soft-deleted permissions with same route - updatePermissionRoute blocks modification of readonly permissions - deletePermissionById blocks deletion of readonly permissions - grantPermissionToRole has no permission check — any authenticated user can call it - Route ordering issue: GET /all must come before GET /:permissionId — currently correct - Route conflict: GET /user-permissions after GET /:permissionId — will never match (:permissionId captures "user-permissions")

4.4 InviteUser Module

Path: src/app/modules/v1/InviteUser/

Routes (registered at both /api/v1/invite-users AND /api/v1/user-invites):

Method Path Auth Context Validation Handler Permission
POST /check-invite No No checkInviteUserExists checkIviteUserExists — (public)
GET / auth() Yes getInviteUserList invite.view
GET /:id auth() Yes getInviteUserById invite.read
POST / auth() Yes createInviteUser createInviteUser invite.create
PATCH /:id/restore auth() Yes restoreFromTrash invite.restore
PATCH /bulk-delete auth() Yes deleteInviteUserByIds deleteInviteUserByIds invite.delete
PATCH /bulk-restore auth() Yes bulkInviteUserRestore bulkInviteUserRestore invite.restore
PATCH /:id auth() Yes updateInviteUser updateInviteUser invite.update
DELETE /:id auth() Yes deleteInviteUser invite.delete
DELETE /:id/permanent auth() Yes deletePermanentInviteUser invite.delete

Key behaviors: - createInviteUser emits EVENT_TYPES.INVITE_USER_CREATED event → sends invitation email - checkIviteUserExists is public (no auth) — validates invite link for registration page - Invite link format: {config.invite_user_link}?email={encodeURIComponent(email)} - createInviteUser does NOT check for soft-deleted invites (commented-out restore logic) - updateInviteStatus (called by auth during registration) marks invite as ACCEPTED - Duplicate route registration: both /invite-users and /user-invites → same handler

4.5 UserSetting Module

Path: src/app/modules/v1/UserSetting/

Routes (registered at /api/v1/user-settings):

Method Path Auth Context Validation Handler Permission
PUT /2fa/email auth() No enableEmail2fa
PUT /2fa/google/setup auth() No start2FASetup
POST /2fa/generate-backup-codes auth() No generateBackupCodes
PATCH /2fa/google/verify auth() No verify2faGoogleAuthToken verifyAnd2FAGoogleSetup
PATCH /2fa/google/disable auth() No verify2faGoogleAuthToken disableGoogle2fa
GET / auth() Yes getUserSetting — (no check)
GET /me auth() Yes getMySetting
GET /:id auth() Yes getSettingById — (no check)

Key behaviors: - Two 2FA providers: Email OTP and Google Authenticator (TOTP via speakeasy) - Google 2FA flow: start2FASetup → stores temp secret → returns QR code → verify2FAGoogleSetup promotes to permanent - Backup codes: 10 codes, 10-char alphanumeric with hyphen, SHA-512 hashed, concatenated as single string - verifyBackupCode does string .includes(hash) to check — vulnerable to substring collisions (see Issue #27) - Email 2FA setup: creates OTP in database, sends via email - getUserSetting and getSettingById have no permission checks — any authenticated user can list all settings


5. Permission System Architecture

5.1 Permission Format

Permissions use dot notation: {domain}.{action}

Domain Actions
user view, read, create, update, delete, trash, restore
role view, read, create, update, delete, trash, restore
permission view, read, create, update, delete, trash, restore
invite view, read, create, update, delete, trash, restore
users.assign roles
users.update role

5.2 Permission Resolution Flow (Updated Feb 25)

Request
  ├── middleware.ts pipeline (global):
  │     security → cookies → CORS → session → passport → body parsers → contextMiddleware
  │     (contextMiddleware: loads user + roles, inits permission cache, attaches Set to req.user)
  ├── Route-level middleware:
  │     auth() → [contextMiddleware AGAIN on some routes — double execution]
  │            → permission("user.view") [NEW v2 middleware — on GET /users only so far]
  │            → Handler
  └── Service method:
        checkPermissionAndThrow(path) → hasPermission(path) [helper version]
              ├── Is super_admin? → return true
              ├── Check permissionCache (Set<string>) → return cached
              └── Cache miss → loadUserRoles() from DB → checkPermissionInRoles()

THREE SYSTEMS (should be consolidated to two — see ADR-2):
  [DELETE] middlewares/hasPermission.ts — route format, primary role only (1 route: GET /payments)
  [KEEP]   middlewares/permissionMiddleware.ts — dot format, req.user.permissions Set (route-level)
  [KEEP]   helper/hasPermission.ts — dot format, cache + DB (service-level conditional checks)

5.3 Super Admin Bypass

The super_admin role has three bypass mechanisms: 1. Helper level: hasPermission() checks isCurrentUserSuperAdmin() → returns true immediately 2. Service level: currentUserPermission() returns ALL permission routes for super_admin 3. Frontend: Super admin gets ['*'] which useCheckPermission would need to handle (but it doesn't — it uses every() with .includes(), which would fail on wildcard)

5.4 Permission Caching

  • Role list cache: node-cache with key prefix roles:, 24-hour TTL
  • Permission cache: permissionCache.ts (in-memory, per-request via AsyncLocalStorage)
  • Cache invalidation: All Role mutations call deleteCacheByPrefix("roles:")
  • Gap: Permission mutations do NOT invalidate the role cache, so role permission changes may be stale

6. Frontend Architecture

6.1 Permission Checking

useCheckPermission(requiredPermissions: string[]): boolean - Reads from Redux state.auth.user.permissions (flat string array) - Uses every() — ALL required permissions must be present - Returns true if requiredPermissions is empty/null - Does NOT handle wildcard * (super admin gets all routes as individual strings from backend)

PermissionWrapper component: - Props: permission (string or string[]), children, showLoader, showError, showMessage, redirectTo - For arrays: uses every() (all required) - Renders UnauthorizedError component on failure (if showError)

NavPermissionWrapper component: - Supports recursive sub-item checking for navigation menus - Modes: requireAll (every) vs default (some)

6.2 State Management

User permissions are stored in Redux state.auth.user.permissions — populated on login/profile fetch. The getProfile backend endpoint aggregates permissions from primary role + all extra roles into a flat deduplicated array.

6.3 Frontend Permission Strings Used

Page Permission Strings
Users user.view, user.create, user.update, user.delete, user.restore, user.trash, role.view, permission.view
Roles role.view, role.create, role.update, role.delete, role.restore, role.trash
Permissions role.view (page gate), permission.create, permission.update, permission.delete, permission.restore, permission.trash
Invited Users invite.view, invite.create, invite.update, invite.delete, invite.restore, invite.trash

7. API Endpoint Reference

7.1 Frontend → Backend Endpoint Mapping Gaps

Frontend Hook Calls Backend Route Exists?
useBulkPermanentDeleteUsers DELETE /users/bulk-permanent-delete NO
useBulkPermanentDeleteRoles DELETE /roles/bulk-permanent-delete NO
useBulkPermanentDeletePermissions DELETE /permissions/bulk-permanent-delete NO
useBulkPermanentDeleteInvitedUsers DELETE /user-invites/bulk-permanent-delete NO
useResendInvitation POST /user-invites/:id/resend NO

All 5 hooks will produce 404 errors when called.

7.2 Payload Key Inconsistencies

Operation Users payload Roles payload Permissions payload Invite payload
Bulk delete { userIds } { ids } { ids } { ids }
Bulk restore { ids } { ids } { ids } { ids }

Users bulk delete uses userIds while all others use ids.


8. Data Flow & User Journeys

8.1 Admin Creates a User

Admin clicks "Add User" → user-form.jsx opens
  → Fills name, email, role, password (or inviteOnly checkbox)
  → Submits → users-page.jsx handleFormSubmit()
  → ⚠️ BUG: calls updateUser.mutate() even for create mode
  → Backend POST /users → user.service.createUser()
    → checkPermissionAndThrow("user.create")
    → Validate role exists
    → Check email uniqueness (including soft-deleted)
    → Hash password
    → Validate extra userRoles if user has "users.assign.roles" permission
    → Save user

8.2 Admin Creates a Role with Permissions

Admin clicks "Add Role" → role-form.jsx opens
  → Fills display name, role name
  → Selects permissions via grouped checkboxes
  → Submits { role, displayName, permissionIds }
  → Backend POST /roles → role.service.createRole()
    → ⚠️ No permission check (commented out)
    → Check name uniqueness, displayName uniqueness
    → Create role entity
    → Call PermissionService.updateRolePermissions() to assign permissions
    → Invalidate role cache

8.3 Invite-Only Registration Flow

Admin creates invite → POST /user-invites { email }
  → inviteUser.service.createInviteUser()
    → Check email not already registered as User
    → Check email not already invited (active)
    → Create InviteUser with PENDING status
    → Emit INVITE_USER_CREATED event → sends email with invite link

User clicks invite link → Frontend /register?email=xxx
  → Frontend calls POST /user-invites/check-invite { email }
    → Validates invite exists and is PENDING
  → User fills registration form
  → Auth registration flow completes
  → inviteUser.service.updateInviteStatus(email) → marks ACCEPTED

8.4 Soft-Delete Lifecycle

All entities follow the same pattern:

Active → softDelete() → sets deletedAt timestamp
  → "Trash" tab shows soft-deleted items
  → restore() → clears deletedAt (with conflict checks)
  → OR repo.remove() → permanent hard delete


9. Dependency Graph

9.1 Backend Module Dependencies

User Module
├── imports: Role entity, Permission entity, PermissionService
├── uses: hasPermission helper, checkPermissionAndThrow helper
├── uses: eventEmitter (for inviteOnly user creation)
├── depends on: QueryBuilder, getSortQuery, parseBoolean, normalizeImageSrc
└── used by: Auth module, Seed, Analytics

Role Module
├── imports: Permission entity, PermissionService
├── uses: checkPermissionAndThrow helper
├── uses: node-cache (addCache, deleteCache, deleteCacheByPrefix, getCache)
├── depends on: QueryBuilder, getSortQuery, parseBoolean
└── used by: User module (role validation), Seed (upsertRole)

Permission Module
├── imports: Role entity, User entity
├── uses: hasPermission helper, checkPermissionAndThrow helper
├── depends on: QueryBuilder, getSortQuery, parseBoolean
└── used by: Role module (updateRolePermissions), User module (currentUserPermission)

InviteUser Module
├── imports: User entity, InviteUser entity
├── uses: checkPermissionAndThrow helper
├── uses: eventEmitter (INVITE_USER_CREATED event)
├── depends on: inviteUserEmailTemplate
└── used by: Auth module (updateInviteStatus during registration)

UserSetting Module
├── imports: UserSetting entity, Otp entity, User entity
├── uses: speakeasy, qrcode, crypto
├── uses: AuthService.logininfoWithToken (for backup code login)
├── depends on: sendEmail helper, QueryBuilder
└── used by: Auth module (2FA verification during login)

9.2 Frontend Component Tree

/dashboard/users/page.js
└── UsersPage
    ├── useGetUsers (API)
    ├── useUpdateUser, useDeleteUser, usePermanentDeleteUser, etc.
    ├── useGetRoles (for role filter dropdown)
    ├── usePermissions (for permission filter dropdown)
    ├── PermissionWrapper (gates entire page + individual actions)
    └── UserForm (modal)
        ├── useGetRoles (for role selector)
        └── PermissionWrapper

/dashboard/roles-permissions/roles/page.js
└── RolesPage
    ├── useGetRoles (API)
    ├── useCreateRole, useUpdateRole, useDeleteRole, etc.
    ├── PermissionWrapper
    └── RoleForm (modal)
        ├── useGetPermissions (grouped, for checkbox grid)
        └── PermissionWrapper

/dashboard/roles-permissions/permissions/page.js
└── PermissionsPage
    ├── usePermissions (flat list API)
    ├── useCreatePermission, useUpdatePermission, etc.
    ├── PermissionWrapper (gates on "role.view" — wrong? should be "permission.view")
    └── PermissionForm (modal)

/dashboard/invited-users/page.js
└── InvitedUsersPage
    ├── useGetInvitedUsers (API)
    ├── useCreateInvitedUser, useDeleteInvitedUser, etc.
    ├── PermissionWrapper
    └── InviteForm (modal)

10. Known Issues

Note: The canonical issue registry is in Section 17 with all 74 issues. This section provides a quick severity summary.

Re-verification (Feb 25): All 6 critical attack vectors confirmed STILL VULNERABLE against the current codebase. The Feb 18-22 backend refactoring did not address any security issues. See Attack Scenarios for details.

Severity Count Key Issues
Critical 11 Privilege escalation (#5b → super_admin after JWT refresh), createRole no check (#4), grantPermissionToRole no check (#5), triple permission systems (#8), format mismatch (#9), v2 middleware blocks super_admin (#76)
High 16 2FA secrets exposed (#15-17), debug logs in production (#12-13), wrong HTTP 403→400 (#10), stale JWT role (#20), auth() doesn't check user status, hasAnyPermission leaks function source (#69)
Medium 26 Duplicate routes (#25), redundant soft-delete (#26), missing seed permissions (#28), backup code includes() (#11), permissions page wrong gate (#30), invite status case mismatch (#75), useGetUsers no gate (#58)
Low 23 Typos (#47-49, #73), dead code (#51-53, #74), empty files (#51), unreachable /user-permissions route (#3)

11. Seed Data

11.1 Seeded Roles (3)

Role displayName Notes
super_admin null (not set) Has all permissions, bypasses checks
admin null (not set) No permissions auto-assigned
user null (not set) No permissions auto-assigned

11.2 Seeded Permissions (87)

Display Group Count Permission Routes
User Management 7 user.view, user.read, user.create, user.update, user.delete, user.trash, user.restore
Role Management 7 role.view, role.read, role.create, role.update, role.delete, role.trash, role.restore
Permission Management 7 permission.view, permission.read, permission.create, permission.update, permission.delete, permission.trash, permission.restore
Blog Management 14 blog.*, blog-tag.*, blog-category.* (CRUD + trash/restore)
Page Management 7 page.* (CRUD + trash/restore)
Package Management 14 package.*, package-category.* (CRUD + trash/restore)
Contact Management 5 contact.view, contact.read, contact.delete, contact.trash, contact.restore
Setting Management 4 setting.view, setting.create, setting.update, setting.delete
Menu Builder Management 8 menu.*, menu-item.* (CRUD)
Dashboard Management 1 dashboard.view
Global Permission Management 7 global.blog-* variants
Customizer Management 1 customizer.access
Article Writer Management 1 article-writer.use

Notable gap: No invite.* permissions are seeded. The InviteUser module requires invite.view, invite.create, invite.update, invite.delete, invite.restore but these must be created manually.

11.3 Seed Execution Order

1. roleSeed()       → Insert admin, user, super_admin roles
2. permissionSeed() → Insert 87 permissions (skips if any exist)
3. seedSettings()   → Insert app settings
4. seedSuperAdmin() → Create super_admin user from env vars

Note: Seed does NOT auto-assign permissions to super_admin. The allPermissionAssignToAdmin() function exists in PermissionService but is never called during seeding. Super_admin bypasses checks anyway, but the /my-permissions endpoint would return empty for super_admin until permissions are explicitly assigned.


12. Risks & Contributor Guidance

12.1 Before Modifying These Files

  • Permission string changes propagate to both backend services AND frontend PermissionWrapper/useCheckPermission calls. Search both codebases.
  • Role cache (node-cache) is NOT invalidated by permission changes — only by role mutations. After modifying permissions, the cache may serve stale data for up to 24 hours.
  • isDeleted vs deletedAt confusion exists across all entities. When adding soft-delete queries, use deletedAt (TypeORM's official mechanism), not isDeleted (legacy boolean that is never automatically set).
  • UserRole entity should not be used — use the User.userRoles M2M relation instead.

12.2 Verification Steps

  1. After any permission route change: verify the permission string matches in both backend checkPermissionAndThrow() calls and frontend PermissionWrapper/useCheckPermission arrays
  2. After role CRUD changes: check that deleteCacheByPrefix("roles:") is called
  3. After adding new backend routes: ensure they are registered in routes/v1/index.ts and check for parameter route conflicts (:id before named paths)
  4. After modifying user creation: test both direct-create and invite-only flows
  5. After 2FA changes: test the complete flow — enable → verify → login with 2FA → disable → backup code login

12.3 Suggested Tests

  • Unit: hasPermission() with cache hit, cache miss, super_admin, no roles, multiple roles with overlapping permissions
  • Unit: Backup code generation, encoding, verification (especially hash boundary testing)
  • Integration: User CRUD lifecycle: create → update → soft-delete → restore → permanent-delete
  • Integration: Role-permission grant flow: create role → assign permissions → verify user gains access
  • Integration: Invite flow: create invite → check-invite → register → verify status changes to ACCEPTED
  • E2E: Permission propagation: assign permission to role → user with that role can access the gated route

13. Backend Dependency Deep Analysis

13.1 Request Context (src/shared/requestContext.ts — 122 LOC)

Per-request state via Node.js AsyncLocalStorage. Stores the authenticated user, their role name (as string from JWT), roleId, userRoles (as Role[]), and the permission cache.

  • getCurrentUser() — throws ContextNotFoundError / UserNotFoundError if missing
  • isCurrentUserSuperAdmin() — compares user.role === 'super_admin' (hard-coded string literal)
  • setPermissionCache() / getPermissionCache() — validates userId ownership before read/write

Issue: Role name is taken from the JWT, not from DB. If a user's role is changed in the database after their JWT was issued, the context has a stale role name until the token expires.

13.2 Permission Cache (src/shared/permissionCache.ts — 183 LOC)

Per-request cache (NOT global). Initialized by contextMiddleware with all permission route strings extracted from the user's roles.

  • initializePermissionCache(user, roles) — extracts permission.route into a Set<string>
  • hasPermissionInCache(permission) — raw string match against the Set (returns boolean | null)
  • Cache stores raw dot-notation strings from the database (e.g., user.create)

Issue: The hasPermission helper has normalization commented out, so cache lookups work only when the caller uses the exact same format as stored in DB. Route-format queries like /users/create will always miss.

13.3 Permission Normalizer (src/shared/permissionNormalizer.ts — 215 LOC)

Designed to convert between route format (/users/create) and dot notation (users.create). Has 13 exports including normalizePermissionString, getPermissionVariants, convertDotToRoute, etc.

Status: Effectively dead infrastructure. The only consumer (src/helper/hasPermission.ts) has the normalization call commented out at lines 57-63. The ACTION_MAPPING constant (update→edit, view→read, etc.) is defined but never used by active code. The middleware has its own inline normalizePath function.

13.4 Context Middleware (src/app/middlewares/contextMiddleware.ts — 93 LOC)

Runs after auth(), loads full user with relations (role, userRoles, userRoles.permissions, role.permissions), wraps remaining request in AsyncLocalStorage.run(), and initializes permission cache.

UPDATE (Feb 22): Now also attaches permissions to req.user.permissions as a Set<string>. Previously permissions were only available via AsyncLocalStorage. This enables the new permissionMiddleware.ts (see 13.5b).

Architecture (corrected after expert review): The global registration in middleware.ts runs AFTER passport.session() but BEFORE per-route auth(). This is intentional dual-path design: - OAuth (Google) flow: passport.session()deserializeUser sets req.user → global contextMiddleware runs with user → context available - JWT flow: passport.session() does nothing → global contextMiddleware short-circuits (req.user undefined) → per-route auth() decodes JWT → per-route contextMiddleware runs → context available

Remaining issues: - Role name set from JWT (req.user.role) not from DB-loaded fullUser.role.name — stale after role changes - Silently continues on DB errors — request proceeds without context, req.user.permissions never set → v2 middleware throws TypeError - For OAuth sessions only: runs twice (global + per-route if route also applies it). Not an issue for JWT flows.

13.5 middleware.ts — Now Active (Previously Dead Code)

UPDATE (Feb 18-19): middleware.ts was documented as dead code in both the Middleware DD and this deep-dive. It is now the primary middleware pipelineapp.ts calls registerMiddlewares(app) which registers all middleware in order: securityMiddleware (Helmet — re-enabled), cookieParser, request logger, compression, CORS, session, passport, body parsers, timing, contextMiddleware.

Key implications: - securityMiddleware (Helmet) is active again — invalidates Middleware DD finding - contextMiddleware runs globally as the last middleware in registerMiddlewares() — this is the first invocation. Many routes add it again per-route, causing double execution. - registerErrorHandlers() is exported but still NOT calledapp.ts still registers error handlers inline - Webhook routes are now AFTER registerMiddlewares() which includes express.json()potentially breaks Stripe webhook signature verification (Stripe needs raw body) - Dead imports in app.ts: cors, passport, cookieParser, session, config, httpLogger, contextMiddleware are all imported but unused (now handled by middleware.ts)

13.5b NEW: Permission Middleware v2 (src/app/middlewares/permissionMiddleware.ts — 39 LOC)

Created Feb 19. A third permission-checking system alongside the existing two.

Exports: - permission(permission: string) — checks req.user.permissions.has(permission) (Set) - hasAnyPermission(permissions: string[]) — checks if any permission matches

Currently used by: GET /users in user.routes.tspermission("user.view")

How it differs from existing systems: - Uses req.user.permissions (Set attached by contextMiddleware) — no DB query, no AsyncLocalStorage - Uses dot notation (matches DB format) — correct, unlike middleware v1 - Route-level middleware (like v1) but name-based (like the helper)

Bugs: - hasAnyPermission error message references ${permission} (function object) instead of ${permissions} (array) — leaks function source code to users (#69) - CRITICAL (#76): No super_admin bypass. After yarn seed, the role_permission table has ZERO entries for super_admin (seed never calls allPermissionAssignToAdmin()). req.user.permissions is an empty Set → has() always returns false → 403 for super_admin on all v2-protected routes. Currently blocks GET /users for super_admin. - No null-safety: if contextMiddleware fails silently, req.user.permissions is undefined → has() throws TypeError

13.5c Triple hasPermission Implementations (CRITICAL — Updated)

Three separate permission-checking systems now exist:

Aspect Middleware v1 (hasPermission.ts, 79 LOC) Helper (helper/hasPermission.ts, 293 LOC) Middleware v2 (permissionMiddleware.ts, 39 LOC)
Type Express middleware Async function Express middleware
Format Route (/users/create) Dot (user.create) Dot (user.view)
Data source DB query by roleId Cache → DB fallback req.user.permissions Set
Multi-role? No (primary only) Yes Yes (Set built from all roles)
Cache? No Yes (AsyncLocalStorage) No (reads Set directly)
Used by 1 route (GET /payments) ~30 service methods 1 route (GET /users)

Architectural direction: Middleware v2 is the intended replacement. It uses the correct format (dot notation), reads from the permission Set (already computed by contextMiddleware), and works at route level. The helper remains necessary for in-service conditional checks. Middleware v1 should be deleted (ADR-2 still applies).

13.5d Auth Middleware Changes (src/app/middlewares/auth.ts — 353 LOC)

UPDATE (Feb 22): The default auth() now uses createQueryBuilder instead of findOne to load the authenticated user. It joins userRoles as extraRole:

leftJoinAndSelect("user.userRoles", "extraRole")

Computes allRoleIds = [role.id, ...userRoles.map(r => r.id)] but never uses it (the permission cache code that would consume it is commented out at lines 333-340). The authGuard export (named export) now validates the DB user's role name matches the JWT role name.

Still present: - user.refreshToken !== refreshToken (line 277) — User entity has no refreshToken column - Debug message "You are not authorized!33" (line 63) - 153 lines of commented-out dead code (old auth implementations) - escapeRegexPath function defined but never called - secure: true hardcoded for cookies (should be process.env.NODE_ENV === 'production')

13.6 Query Builder (src/app/builder/QueryBuilder.ts — 222 LOC)

Generic paginated query builder used by all list endpoints. Supports search, filters, relations, sorting, soft-delete modes (trashOnly, includeDeleted). Redundantly adds deletedAt IS NULL when TypeORM already excludes soft-deleted entities by default.

13.7 Resolved: Routes Controller Removed

UPDATE (Feb 18): Commit d021804 removed routerList.routes.ts and routes.controller.ts. The /api/v1/routes endpoint that crashed due to missing routes.json no longer exists. This resolves the Middleware DD Critical issue about the routes endpoint crash.


14. Cross-Reference: Auth Deep-Dive Overlap

23 issues from the Auth Deep-Dive overlap with this area. Issues #51-64 were tagged "out-of-scope" in the auth doc and are owned by this deep-dive as their canonical home.

14.1 Issues Owned by This Deep-Dive (from Auth DD #51-64)

Auth DD # Severity This DD # Description
#51 Critical #4 createRole permission check commented out
#53 High #3 GET /user-permissions route unreachable
#54 High #6 updateProfile returns bare res on password change
#55 High #14 Duplicate userRoles validation bypasses permission check
#56 High getAllPermissions has no permission check (new: #40)
#57 Critical #30 console.log(code) logs backup codes in plaintext
#58 Medium #28 Missing seeded permissions (invite.*, users.assign.roles, etc.)
#59 Medium Email 2FA startEmail2FASetup defined but never exported (new: #41)
#61 Medium #9 Backup codes stored without delimiters (substring collision)
#62 Medium createRole cache not invalidated on restore path (new: #42)
#64 High upsertRole inherits missing permission check (new: #43)

14.2 Issues Cross-Referenced from Auth DD (in-scope there)

Auth DD # Description Consistency Note
#2 Missing refreshToken column on User entity Unchanged, still present
#16 Dual role-assignment tables (user_roles vs user_role) Confirmed as this DD #7
#21 Permission normalization commented out in helper Confirmed, still commented out
#25 checkPermissionAndThrow uses HTTP 400 not 403 Confirmed as this DD #8
#28 Hardcoded 'super_admin' string in requestContext Confirmed, no constant exists
#29 Debug console.log in hasPermission helper Confirmed as this DD #12
#31 Unhashed password on user restoration Relevant to createUser soft-delete handling
#38 verifyLoginOtp auto-registration bypasses invite-only Relevant to invite flow

15. API Documentation Gaps

Cross-referencing docs/architecture/api.md against actual routes revealed 23 undocumented endpoints:

15.1 /roles — 4 Undocumented Endpoints

Method Path Purpose
PATCH /roles/bulk-delete Bulk soft-delete roles
PATCH /roles/bulk-restore Bulk restore roles from trash
PATCH /roles/:id/restore Restore single role from trash
DELETE /roles/:id/permanent Permanently delete role

15.2 /permissions — 12 Undocumented Endpoints (only 2 of 14 documented)

Method Path Purpose
GET /permissions/all Get all permissions (flat list, paginated)
GET /permissions/:permissionId Get single permission
GET /permissions/user-permissions Get current user's permissions (dead route)
POST /permissions/check Check if user has a permission
PATCH /permissions/bulk-delete Bulk soft-delete
PATCH /permissions/bulk-restore Bulk restore
PATCH /permissions/:permissionId Update a permission
PUT /permissions/grant Grant permissions to a role
DELETE /permissions/delete Delete permissions by IDs (body)
DELETE /permissions/:permissionId Delete single permission
PATCH /permissions/:permissionId/restore Restore from trash
DELETE /permissions/:permissionId/permanent Permanently delete

15.3 /invite-users — 7 Undocumented Endpoints (only 3 of 10 documented)

Method Path Purpose
POST /invite-users/check-invite Check invite validity (PUBLIC, no auth)
GET /invite-users/:id Get single invitation
PATCH /invite-users/:id/restore Restore from trash
PATCH /invite-users/bulk-delete Bulk soft-delete
PATCH /invite-users/bulk-restore Bulk restore
PATCH /invite-users/:id Update invitation
DELETE /invite-users/:id/permanent Permanently delete

15.4 Database Documentation Discrepancies

Entity Issue
All entities createdAt, updatedAt columns missing from docs
Role Partial unique index IDX_ROLE_NAME_ACTIVE not documented
UserRoles Docs say UserRoles (plural), code says UserRole (singular); docs show simple columns, code has full entity with relations
UserSetting onDelete: CASCADE, index on userId, index on twoFactorSecret not documented

16. Expanded Frontend Coverage

The deep scan found 57 frontend files (~13,100 LOC) related to RBAC, organized in 9 categories:

Category Files LOC
Core RBAC infrastructure (hooks, wrappers) 7 389
Auth state & middleware 7 272
API hooks (users, roles, permissions, invites) 4 1,024
Dashboard page components 9 5,377
App route pages (thin wrappers) 11 ~230
Sidebar navigation (permission-gated) 4 ~2,200
Auth pages (invite flow) 3 1,192
Settings pages + 2FA 8 1,937
Settings API 2 274

16.1 Additional Files Not in Original Scan

  • components/custom/unauthorized-error.jsx (73 LOC) — Access denied page rendered by PermissionWrapper
  • components/custom/roles-badge-group.jsx (67 LOC) — Role badge display with color coding
  • components/custom/user-avatar-group.jsx (75 LOC) — Overlapping avatar display for role user lists
  • store/slices/authSlice.js (47 LOC) — Redux state for user.permissions
  • services/authService.js (54 LOC) — Raw API calls including getProfile
  • api/auth/index.js (153 LOC) — React Query hooks for auth, loads user.permissions
  • middleware.js (23 LOC) — Token-only route protection (no permission/role checks)
  • components/providers/protected-route.jsx (28 LOC) — Client-side auth check
  • components/providers/auth-initializer.jsx (20 LOC) — Redux auth initialization
  • interceptors/axiosInstance.js (48 LOC) — Bearer token injection, 401/403 logout
  • components/pages/auth/register.jsx (424 LOC) — Invite-only registration flow
  • 3 sidebar variants + top-nav (permission-gated navigation)
  • settings/profile/page.jsx (375 LOC) — Profile editing page
  • api/settings/account-setting.js (123 LOC) — 2FA API hooks
  • 4 2FA components (742 LOC total)
  • 16+ additional consumer files using PermissionWrapper across blogs, packages, products, contacts, etc.

16.2 Additional Frontend Findings

  • useGetUsers does NOT check permissions (unlike useGetRoles/usePermissions/useGetInvitedUsers which gate via enabled: hasPermission)
  • permission-form.jsx update is brokenhandleUpdatePermission is commented out (lines 110-119)
  • roles-badge-group.jsx has duplicate demo IDdemoAssignedRoles array has id: 2 for both "Moderator" and "User"
  • Register page (register.jsx) handles invite-only flow via websiteType == 2, checking invite via POST /user-invites/check-invite
  • Dashboard layout (layout.js) has no permission checking — only uses useUser() for loading state

17. Complete Issue Registry (75 Total)

Critical (10) — includes privilege escalation chain (Attack Scenarios)

# Location Description Auth DD Ref
1 users-page.jsx Create user calls UPDATE mutationhandleFormSubmit for create mode calls updateUser.mutate() instead of createUser.mutate(). PATCH /users/undefined → 404. Error shown via toast but creation completely fails.
2 Frontend hooks Bulk permanent-delete + resend endpoints missing on backend. Users button is commented out (dead code). Invited-users button is active → 404 with error toast. Resend button hidden by case mismatch ("pending" vs "PENDING") — dormant. Roles/permissions bulk-permanent-delete buttons → 404.
4 role.service.ts:150 ESCALATION: createRole has no permission checkcheckPermissionAndThrow commented out. Part of privilege escalation chain. Auth #51
5 permission.service.ts:105 ESCALATION: grantPermissionToRole has no permission check — any authenticated user can assign arbitrary permissions. Part of privilege escalation chain.
5b user.service.ts:533 ESCALATION: updateProfile allows self-role-change — accepts role in payload, writes to roleId with no authorization. Delayed escalation: roleId changes immediately in DB, but JWT still has old role string. isCurrentUserSuperAdmin() returns false until JWT expires and refreshes. After refresh, new JWT has role: "super_admin" → full bypass. Delay = config.jwt.expires_in.
5c #4 + #5 + #5b combined Complete privilege escalation: 0-permission user → super_admin. Immediate partial access (DB-level permissions of new role). Full isCurrentUserSuperAdmin() bypass after JWT refresh. See Attack Scenarios.
6 user.controller.ts:59-61 updateProfile returns bare res on password change — malformed/hanging HTTP response Auth #54
7 UserRoles.ts + User.ts Dual user-role table conflictuser_roles (JoinTable) vs user_role (Entity), UserRole entity's inverse mapping references wrong relation type Auth #16
8 Dual hasPermission Two incompatible permission systems — middleware (route-format, primary role only) vs helper (dot-format, multi-role with cache) produce different results Auth #21
9 Seed + middleware Permission format mismatch — middleware constructs /users/create (route, plural), seed stores user.create (dot, singular). Non-super_admin middleware checks always fail.
76 permissionMiddleware.ts + seed NEW (expert panel): v2 permission() middleware blocks super_admin. After yarn seed, role_permission table has ZERO entries for super_admin — allPermissionAssignToAdmin() exists but is never called during seed. req.user.permissions is an empty Set → has("user.view") returns false → 403. Currently affects GET /users. Will break ALL routes as v2 expands. Requires: (1) add allPermissionAssignToAdmin() to seed, AND (2) add super_admin bypass to v2 middleware.

High (16) — Severity-adjusted after code verification + devil's advocate challenge

# Location Description Auth DD Ref
10 checkPermissionAndThrow.ts:19 Wrong HTTP status: 400 (BAD_REQUEST) instead of 403 (FORBIDDEN) Auth #25
12 user.service.ts:490 Debug console.log({ payload }) logs profile updates in production Auth #29
13 hasPermission.ts:68 Debug console.log({cachedResult}) logs every permission check Auth #29
14 user.service.ts:337-363 Duplicate userRoles validation — second unconditional block overwrites permission-gated first block Auth #55
15 user_setting.service.ts getUserSetting and getSettingById expose all users' 2FA settings without permission checks
16 UserSetting.ts:46 twoFactorSecret has select: true (default) — secrets selected in every query
17 user_setting.service.ts:162 start2FASetup returns secret: secret.base32 in API response — TOTP secret should never leave server
18 permission.routes.ts Redundant auth(), contextMiddleware on individual routes when already applied globally via router.use()
19 user.service.ts Mixed soft-delete strategies: getUserById checks isDeleted: false (boolean), deleteUser uses softDelete() (sets deletedAt). These are NOT the same check.
20 contextMiddleware.ts:37 Role name from JWT (stale) vs DB (fresh) — role changes not reflected until token expires
21 permissionCache.ts Cache stores raw dot-notation; cache lookups miss route-format queries
22 permissionNormalizer.ts ACTION_MAPPING dead code, entire normalizer module unused (all calls commented out)
23 permission.service.ts getAllPermissions (GET /permissions/all) has no permission check — any authenticated user can list all permissions Auth #56
24 role.service.ts upsertRole calls createRole which has permission check commented out — upsert is also unprotected Auth #64
69 permissionMiddleware.ts:37 Upgraded from Medium: hasAnyPermission error message references ${permission} (function object) instead of ${permissions} (array) — leaks function source code to end users. Information disclosure.

Medium (26) — Severity-adjusted after code verification + devil's advocate challenge

# Location Description Auth DD Ref
11 user_setting.service.ts:74 Downgraded from High: Backup code includes() on concatenated hash — theoretically fragile (substring match on concatenated SHA-512) but collision probability is ~2^-512 in practice. The replace() removal at line 82 is the real concern. Auth #61
25 routes/v1/index.ts:147-151 Duplicate InviteUser route registration at both /invite-users and /user-invites
26 Role.ts, Permission.ts, User.ts Redundant isDeleted boolean alongside @DeleteDateColumn
27 role.validation.ts:23-30 Update validation missing displayName field — silently stripped
28 Seed data Missing invite.* permissions, users.assign.roles, users.update.role Auth #58
29 permissions-page.jsx Duplicate ConfirmationModal JSX block (lines 773-784 and 786-797 are byte-for-byte identical)
31 api/roles/index.js Debug console.log("hasPermission", hasPermission)
32 permissions-group-page.jsx Mostly non-functional (checkboxes, actions, update handler commented out)
33 Payload keys Inconsistent bulk delete: Users uses { userIds }, all others use { ids }
34 seed/role.seed.ts:29 Seeded roles missing displayName — all have null
35 user_setting.service.ts:72 Debug console.log(code) logs plain-text backup code Auth #57
36 contextMiddleware.ts Silent failure on DB errors — request proceeds without context
30 permissions-page.jsx:309 Downgraded from High after challenge: Page gates on role.view instead of permission.view. Edge case — both permissions typically assigned together. Only affects rare "Permission Auditor" role configuration.
37 contextMiddleware.ts CORRECTED: Global contextMiddleware is a no-op for JWT-auth requests (runs before auth(), req.user is undefined → short-circuits). Only runs twice for rare Passport-session users (Google OAuth). Performance impact limited to OAuth flow.
38 requestContext.ts Hard-coded 'super_admin' string — no single source of truth Auth #28
39 permission.routes.ts Redundant DELETE /permissions/delete duplicates PATCH /permissions/bulk-delete
40 user_setting.service.ts startEmail2FASetup defined but never exported/called — email 2FA has no verification step Auth #59
41 role.service.ts createRole cache not invalidated on soft-delete restore path Auth #62
42 Seed data readonly field semantics unclear and inconsistent across permissions
43 Seed data Redundant read/view permission pairs — view never checked by middleware
44 seed/permission.seed.ts All-or-nothing seed — if any permissions exist, new ones never get seeded
45 api.md 23 undocumented endpoints across /roles, /permissions, /invite-users
46 database.md UserRoles entity docs show wrong class name, missing relations/constraints
58 useGetUsers Upgraded from Low: Does NOT gate on useCheckPermission unlike useGetRoles/usePermissions/useGetInvitedUsers. Fires unnecessary API call for users without user.view — visible failed request in browser dev tools.
70 app.ts + middleware.ts NEW (Feb 19): Dead imports in app.tscors, passport, cookieParser, session, config, httpLogger, contextMiddleware are imported but no longer used (now handled by middleware.ts)
75 invited-users-page.jsx:632 NEW (challenge finding): Invite status case mismatch — frontend checks invite.status === "pending" (lowercase) but backend returns "PENDING" (uppercase enum). Resend button is never visible. Masks the missing /resend endpoint (dormant double-bug).

Low (23) — Severity-adjusted after code verification + devil's advocate challenge

# Location Description
47 user.service.ts:4 Typo: UserSerivce (should be "Service")
48 invite-user.controller.ts:7 Typo: checkIviteUserExists (should be "Invite")
49 role.service.ts:130 Typo: roleDiplayNameExists (should be "Display")
50 UserSetting.ts:46 Unnecessary index on twoFactorSecret column
51 Role.repository.ts Empty file (3 lines of whitespace)
52 seed.ts:4 Dead import: UserRole imported but never used
53 user_setting.service.ts:7 Dead import: { get } from "http"
54 user_setting.service.ts:162 App name hardcoded as "Sass Boilerplate" (typo for "SaaS")
55 user.validation.ts:9 required_error dead code on optional() field
56 roles-badge-group.jsx Duplicate id: 2 in demoAssignedRoles array
57 permission-form.jsx handleUpdatePermission commented out — update is no-op
3 permission.routes.ts:13 Downgraded from Critical after challenge: GET /user-permissions unreachable (shadowed by /:permissionId). But frontend NEVER calls this endpoint — permissions come from GET /users/profile. Dead backend code only.
59 permissions/page.jsx Dev sandbox/test page — not a real management page
60 requestContext.ts RequestUser.role is string not Role — implicit convention
61 permissionCache.ts Excessive redundant userId validation (3-4 times per call chain)
62 permissionCache.ts Misleading timestamp field with no TTL check
63 permissionNormalizer.ts Multi-part permission path segment loss
64 QueryBuilder.ts Double soft-delete filtering (redundant with TypeORM default)
65 eventTypes.ts EVENT_TYPES not typed as const
66 inviteUserCreate.handler.ts Error message says "registration email" not "invite email"
67 inviteUserCreate.handler.ts Failed invite emails silently lost (errors only console.error'd)
68 seed/setting.seed.ts No RBAC configuration settings seeded (all hard-coded)
71 auth.ts:63 Debug message "You are not authorized!33" — leftover debug suffix in error string
72 auth.ts:13-15 Dead code: escapeRegexPath function defined but never called
73 permission.service.ts:576 Typo: getPemissionsByRole (missing "r" in "Permissions")
74 auth.ts:81-234 153 lines of commented-out dead code (two old auth implementations)

Resolved Issues (confirmed fixed by recent commits)

Former # Location Resolution
Middleware DD routes.controller.ts / routerList.routes.ts REMOVED in commit d021804 (Feb 18). The /api/v1/routes crash no longer occurs.
Middleware DD middleware.ts dead code RESOLVED — now active via registerMiddlewares() in app.ts (commit e7117e7, Feb 19)
Middleware DD Helmet/security middleware disabled RESOLVED — re-enabled in middleware.ts (commit 2922ba8, Feb 18)

18. Production Risk Assessment

Pre-mortem analysis identified 7 production failure scenarios. Full details in RBAC Attack Scenarios — Part 2.

Priority Scenario Business Impact Root Issues Fix Effort
P0 Data leak via privilege escalation (user → super_admin → dump all data) Company-ending #5b, #15 3 lines
P0 Fired employee retains admin access (no status check in middleware) Legal liability No status check Medium
P1 All non-super_admin users blocked (permission format mismatch) Product unusable #8, #9 Medium
P1 Silent user create failure (wrong mutation called in frontend) Admin productivity destroyed #1 1 line
P1 Invite registration broken (missing backend endpoint + unseeded permissions) Onboarding blocked #2, #28 Small
P2 Intermittent permission failures (dual systems, double middleware) User confusion #8, #37 Medium
P2 Database overload (~9 queries per page load, middleware runs twice) 10x hosting cost #37, #18 Medium

Minimum Viable Production Readiness (6 fixes, ~30 min)

  1. Strip role from updateProfile service + validation (Issues #5b — blocks escalation)
  2. Uncomment checkPermissionAndThrow("role.create") (Issue #4 — blocks role creation)
  3. Add checkPermissionAndThrow("permission.grant") to grantPermissionToRole (Issue #5 — blocks permission assignment)
  4. Add user status check in contextMiddleware (blocks fired employee access)
  5. Fix create-user mutation in users-page.jsx (Issue #1 — updateUsercreateUser)
  6. Seed invite.* permissions in seed/data/permission.ts (Issue #28 — unblocks invite system)

19. Architecture Decision Records

Five ADRs were produced to resolve the systemic architectural issues. Full migration plan in RBAC Attack Scenarios — Part 3.

ADR Summary

ADR Decision Rationale
ADR-1: Permission Format Standardize on dot notation (user.create) Already used by DB, seed data, frontend, helper. Route format only used by 1 dead-code middleware. Zero migration cost.
ADR-2: hasPermission Implementation Keep helper + new middleware v2 (with 3 fixes), delete v1 v2 uses correct dot format but has 3 CRITICAL prerequisites before expanding: (PRE-1) add allPermissionAssignToAdmin() to seed so super_admin has permissions in role_permission table, (PRE-2) add super_admin bypass to v2 (if (req.user.role === 'super_admin') return next()), (PRE-3) add null-safety for req.user.permissions undefined. Without these, v2 blocks super_admin (#76). Delete v1 after migrating GET /payments to v2.
ADR-3: UserRole Entity Delete entity Never used by any service code. Only imported (dead) in seed.ts. The @ManyToMany + @JoinTable on User.ts handles the relationship correctly.
ADR-4: isDeleted vs deletedAt Remove isDeleted, use deletedAt only TypeORM's softDelete() sets deletedAt but never touches isDeleted. Mixed checks create false negatives.
ADR-5: Permission Normalizer Delete entirely 215 LOC, 13 exports, zero active callers. All normalization calls commented out. With single format, no normalization needed.

Migration Phases

Phase Scope Effort Impact
1. Security P0 privilege escalation + status check fixes 30 min Blocks all 6 attack vectors
2. Unification Delete middleware v1, keep v2; delete entity, normalizer; dedupe contextMiddleware; fix hasAnyPermission bug 2-3 hrs Two-system model (v2 for routes + helper for services)
3. Cleanup Remove isDeleted, fix HTTP codes, remove console.logs, seed fixes 2-3 hrs Consistent data model
4. Frontend Remove broken hooks, fix permission gates, fix mutations 1-2 hrs Match backend fixes

Target Architecture

AFTER (updated for Feb 22 changes):
  Route-level: permissionMiddleware.ts (dot notation, req.user.permissions Set)
       ├── permission("user.view") — single permission check
       └── hasAnyPermission(["a","b"]) — any-of check (fix bug in error msg first)

  Service-level: helper/hasPermission.ts (dot notation, multi-role, cached)
       ├── checkPermissionAndThrow() — all services
       └── hasPermission() — conditional checks

  Both backed by: contextMiddleware → initializePermissionCache → req.user.permissions Set

  DELETE: middlewares/hasPermission.ts (v1, route format, broken)
  ONE format: dot notation
  ONE soft-delete: @DeleteDateColumn
  ONE user↔role table: user_roles (@ManyToMany JoinTable)

20. Root Cause Analysis

5 Whys analysis traced all 74 issues back to 5 systemic root causes. Full reasoning chains in RBAC Attack Scenarios — Part 4.

The 5 Root Causes

# Root Cause Issues It Explains
RC-1 Zero automated tests + super_admin bypass — RBAC was never tested with the users it's designed to protect. The super_admin bypass makes permission checks invisible during development. Format mismatch (#8, #9), unreachable route (#3), silent failures, all permission gaps
RC-2 No separation between internal and external service calls — Seed scripts call service methods that have permission checks, creating a conflict resolved by commenting out security. Commented-out role.create (#4), upsertRole unprotected (#24)
RC-3 Shared validation schemas between admin and self-service endpointsupdateProfile and updateUser share the same Zod schema, so self-service endpoints accept admin-only fields. Self-role-change escalation (#5b), the most severe vulnerability
RC-4 Single developer, no code review — Security-sensitive patterns (field-level access, permission checks) were missed with no second pair of eyes. Copy-paste bugs (#14, #30), dead code accumulation, debug logs (#12, #13, #31, #35)
RC-5 Incremental development without architectural refactoring — Features were added alongside existing code rather than replacing it, creating parallel systems. Dual hasPermission (#8), dual tables (#7), dead normalizer (#22), isDeleted + deletedAt (#26)

Prevention Table

Prevention Cost Would Have Caught
1 integration test with a non-super_admin user 30 min Format mismatch, broken RBAC, most permission issues
Separate validation schemas for admin vs self-service 15 min Self-role-change vulnerability (most severe issue)
ESLint rule: no console.log in production 5 min All 5 debug log leaks
// TODO grep in CI that blocks merge 5 min Commented-out permission checks
Code review checklist: "does this endpoint check permissions?" 0 cost Missing permission checks on 4 endpoints

21. Sprint Plan

Cross-functional war room triaged all 70 issues into 4 phases. Full sprint plan with verification checklists, acceptance tests, and sprint board at RBAC Sprint Plan.

Phase Items Effort Timeline Exit Criteria
Ship Blockers 14 ~1 hr Day 1 All 6 attack vectors blocked, core features work
Sprint 1 14 ~7 hrs Week 1 Non-super_admin admin can CRUD all management pages
Sprint 2 20 ~12 hrs Week 2-3 isDeleted removed, seed works, integration test passes, docs match
Backlog 18 ~3 hrs Ongoing Fix when touching adjacent files
Total 66 ~23 hrs ~3 weeks

4 issues auto-resolved by Sprint 1 deletions (UserRole entity, normalizer).


Generated by BMAD Document Project workflow v1.2.0 — Exhaustive deep-dive, 2026-02-25