{
  "id": "admin-rbac",
  "version": "1.4.0",
  "description": "Feature-based RBAC (AM/USM/RAP) — optional JM job admin when jobs pack + env detected",
  "requires": ["admin", "database", "vue"],
  "npm": {
    "dependencies": {
      "class-validator": "^0.15.1",
      "class-transformer": "^0.5.1"
    }
  },
  "files": {
    "from": "files",
    "overwrite": [
      "configs/routes/admin/index.ts",
      "configs/routes/admin/adminUser.route.ts",
      "configs/routes/admin/adminRole.route.ts",
      "app/views/admin/user.view/index.pug",
      "app/views/layouts/admin/layout.pug"
    ]
  },
  "conditionalFiles": [
    {
      "from": "files-jobs",
      "when": { "jobsEnabled": true },
      "overwrite": [
        "configs/routes/admin/index.ts",
        "configs/db/seeders/features.ts",
        "app/controllers/admin/adminJob.controller.ts",
        "configs/routes/admin/adminJob.route.ts"
      ]
    }
  ],
  "patches": [
    {
      "file": "configs/db/schema.prisma",
      "ops": [
        {
          "type": "insertAfter",
          "marker": "  passwords Password[]",
          "content": "\n  roles       UserToRole[]\n  permissions UserToPermission[]"
        },
        {
          "type": "append",
          "content": "\nmodel Role {\n  id          String   @id @default(uuid()) @map(\"id\")\n  createdAt   DateTime @default(now()) @map(\"created_at\")\n  updatedAt   DateTime @updatedAt @map(\"updated_at\")\n  deleted     Boolean  @default(false) @map(\"deleted\")\n  code        String   @unique @map(\"code\")\n  name        String   @map(\"name\")\n  description String?  @map(\"description\")\n  isReadOnly  Boolean  @default(false) @map(\"is_read_only\")\n  users       UserToRole[]\n  permissions RoleToPermission[]\n  @@map(\"roles\")\n}\n\nmodel UserToRole {\n  userId    String   @map(\"user_id\")\n  roleId    String   @map(\"role_id\")\n  createdAt DateTime @default(now()) @map(\"created_at\")\n  user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n  role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)\n  @@id([userId, roleId])\n  @@map(\"user_to_role\")\n}\n\nmodel Feature {\n  id          String   @id @default(uuid()) @map(\"id\")\n  createdAt   DateTime @default(now()) @map(\"created_at\")\n  updatedAt   DateTime @updatedAt @map(\"updated_at\")\n  deleted     Boolean  @default(false) @map(\"deleted\")\n  code        String   @map(\"code\")\n  name        String   @map(\"name\")\n  description String?  @map(\"description\")\n  type        String   @default(\"FEATURE\") @map(\"type\")\n  parentId    String?  @map(\"parent_id\")\n  sortOrder   Int      @default(0) @map(\"sort_order\")\n  parent      Feature?  @relation(\"featureTree\", fields: [parentId], references: [id])\n  children    Feature[] @relation(\"featureTree\")\n  permissions Permission[]\n  @@map(\"features\")\n}\n\nmodel Permission {\n  id          String   @id @default(uuid()) @map(\"id\")\n  createdAt   DateTime @default(now()) @map(\"created_at\")\n  updatedAt   DateTime @updatedAt @map(\"updated_at\")\n  deleted     Boolean  @default(false) @map(\"deleted\")\n  code        String   @map(\"code\")\n  name        String   @map(\"name\")\n  description String?  @map(\"description\")\n  featureId   String   @map(\"feature_id\")\n  feature     Feature  @relation(fields: [featureId], references: [id], onDelete: Cascade)\n  users       UserToPermission[]\n  roles       RoleToPermission[]\n  @@map(\"permissions\")\n}\n\nmodel UserToPermission {\n  userId       String @map(\"user_id\")\n  permissionId String @map(\"permission_id\")\n  user       User       @relation(fields: [userId], references: [id], onDelete: Cascade)\n  permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade)\n  @@id([userId, permissionId])\n  @@map(\"user_to_permissions\")\n}\n\nmodel RoleToPermission {\n  roleId       String @map(\"role_id\")\n  permissionId String @map(\"permission_id\")\n  role       Role       @relation(fields: [roleId], references: [id], onDelete: Cascade)\n  permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade)\n  @@id([roleId, permissionId])\n  @@map(\"role_to_permissions\")\n}\n"
        }
      ]
    },
    {
      "file": "configs/enum/index.ts",
      "ops": [
        {
          "type": "insertBefore",
          "marker": "/** Flash categories",
          "content": "export enum Feature {\n  AdministrationManagement = \"AM\",\n  UserManagement = \"USM\",\n  RoleAndPermission = \"RAP\",\n}\n\n"
        }
      ]
    },
    {
      "file": "configs/enum/index.ts",
      "when": { "jobsEnabled": true },
      "ops": [
        {
          "type": "insertAfter",
          "marker": "RoleAndPermission = \"RAP\",",
          "content": "\n  JobManagement = \"JM\","
        }
      ]
    },
    {
      "file": "configs/routes/index.ts",
      "ops": [
        {
          "type": "insertAfter",
          "marker": "// @irwin:rails-route-permissions",
          "content": "\nimport {\n  Permission,\n  ValidateAnyPermissionMiddleware,\n  ValidateUserPermissionMiddleware,\n} from \"@middlewares\";\n\nRailsRoute.permissionFactory = {\n  forUser: (permission: string) => {\n    const m = new ValidateUserPermissionMiddleware(permission);\n    return m.execute.bind(m);\n  },\n  forAny: (permissions: string[]) => {\n    const m = new ValidateAnyPermissionMiddleware(permissions);\n    return m.execute.bind(m);\n  },\n};\n\nRailsRoute.actionPermissionMap = {\n  read: Permission.Read,\n  create: Permission.Create,\n  update: Permission.Update,\n  delete: Permission.Delete,\n};\n"
        }
      ]
    },
    {
      "file": "app/middlewares/index.ts",
      "ops": [
        {
          "type": "append",
          "content": "export * from \"./enums/permissions\";\nexport * from \"./validatePermission.middleware\";\n"
        }
      ]
    },
    {
      "file": "app/controllers/index.ts",
      "ops": [
        {
          "type": "append",
          "content": "export * from \"./admin/adminRole.controller\";\nexport * from \"./admin/adminFeature.controller\";\n"
        }
      ]
    },
    {
      "file": "app/controllers/index.ts",
      "when": { "jobsEnabled": true },
      "ops": [
        {
          "type": "append",
          "content": "export * from \"./admin/adminJob.controller\";\n"
        }
      ]
    },
    {
      "file": "app/controllers/admin/index.ts",
      "ops": [
        {
          "type": "append",
          "content": "export * from \"./adminRole.controller\";\nexport * from \"./adminFeature.controller\";\n"
        }
      ]
    },
    {
      "file": "app/controllers/admin/index.ts",
      "when": { "jobsEnabled": true },
      "ops": [
        {
          "type": "append",
          "content": "export * from \"./adminJob.controller\";\n"
        }
      ]
    },
    {
      "file": "lib/index.ts",
      "ops": [
        {
          "type": "insertAfter",
          "marker": "// @irwin:lib-exports",
          "content": "\nexport * from \"./utils/userPermissions\";\n"
        }
      ]
    },
    {
      "file": "tsconfig.json",
      "ops": [
        {
          "type": "insertAfter",
          "marker": "// @irwin:tsconfig-paths",
          "content": "\n      \"@validators\": [\"./app/validators\"],\n      \"@validators/*\": [\"./app/validators/*\"],"
        }
      ]
    },
    {
      "file": "configs/locales/en/admin.json",
      "ops": [
        {
          "type": "insertAfter",
          "marker": "\"back_to_users\": \"Back to users\"",
          "content": ",\n    \"roles\": \"Roles\",\n    \"features\": \"Features\",\n    \"code\": \"Code\",\n    \"view\": \"View\",\n    \"no_roles\": \"No roles yet\",\n    \"no_features\": \"No features\",\n    \"back_to_roles\": \"Back to roles\",\n    \"back_to_features\": \"Back to features\",\n    \"new_role\": \"New role\",\n    \"edit_role\": \"Edit role\",\n    \"role_created\": \"Role created\",\n    \"role_updated\": \"Role updated\",\n    \"role_deleted\": \"Role deleted\",\n    \"role_code_taken\": \"Role code already exists\",\n    \"role_read_only\": \"This role cannot be changed\",\n    \"permissions\": \"Permissions\",\n    \"save_permissions\": \"Save permissions\",\n    \"permissions_updated\": \"Permissions updated\",\n    \"search\": \"Search\",\n    \"role_information\": \"Role information\",\n    \"assigned_users\": \"Assigned users\",\n    \"permission_information\": \"Permissions by feature\",\n    \"assign_user\": \"Assign user\",\n    \"assign_user_to\": \"Assign user to {{name}}\",\n    \"assign_selected\": \"Assign selected\",\n    \"select_one_user\": \"Select at least one user\",\n    \"users_assigned\": \"{{count}} user(s) assigned\",\n    \"user_unassigned\": \"User unassigned\",\n    \"no_users_assigned\": \"No users assigned\",\n    \"no_users_available\": \"No users available to assign\",\n    \"unassign\": \"Unassign\",\n    \"delete_role_confirm\": \"Delete this role?\",\n    \"users_count\": \"Users\",\n    \"apply_permissions\": \"Apply permissions\",\n    \"modules_list\": \"Modules\",\n    \"modules_list_hint\": \"Select a module, then tick permissions on the right\",\n    \"functions_components\": \"Functions & permissions\",\n    \"functions_components_hint\": \"Expand each feature to choose READ, CREATE, UPDATE, DELETE\",\n    \"perm_pick_menu_group\": \"Select a module on the left\",\n    \"perm_no_child_features\": \"No features under this module\",\n    \"role_perm_functions_intro\": \"Tick each permission this role should have\""
        },
        {
          "type": "insertAfter",
          "marker": "    \"users\": \"Users\"\n  }\n}",
          "content": ",\n    \"roles\": \"Roles\",\n    \"features\": \"Features\",\n    \"admin_management\": \"Administration\",\n    \"back_home\": \"Back to site\""
        }
      ]
    },
    {
      "file": "configs/locales/en/common.json",
      "ops": [
        {
          "type": "insertAfter",
          "marker": "\"login_first\": \"Please sign in first\"",
          "content": ",\n    \"no_permission\": \"You do not have permission for this action\""
        },
        {
          "type": "insertAfter",
          "marker": "\"back_to_home\": \"Back to home\"",
          "skipIfContains": "\"select_all\"",
          "content": ",\n  \"select_all\": \"Select all\""
        }
      ]
    },
    {
      "file": "configs/locales/vi/common.json",
      "ops": [
        {
          "type": "insertAfter",
          "marker": "\"back_to_home\": \"Về trang chủ\"",
          "skipIfContains": "\"select_all\"",
          "content": ",\n  \"select_all\": \"Chọn tất cả\""
        }
      ]
    },
    {
      "file": "configs/locales/vi/admin.json",
      "ops": [
        {
          "type": "insertAfter",
          "marker": "\"back_to_users\": \"Quay lại danh sách\"",
          "skipIfContains": "\"permission_information\"",
          "content": ",\n    \"roles\": \"Vai trò\",\n    \"features\": \"Tính năng\",\n    \"code\": \"Mã\",\n    \"view\": \"Xem\",\n    \"no_roles\": \"Chưa có vai trò\",\n    \"no_features\": \"Chưa có tính năng\",\n    \"back_to_roles\": \"Quay lại danh sách vai trò\",\n    \"back_to_features\": \"Quay lại danh sách tính năng\",\n    \"new_role\": \"Thêm vai trò\",\n    \"edit_role\": \"Sửa vai trò\",\n    \"role_created\": \"Đã tạo vai trò\",\n    \"role_updated\": \"Đã cập nhật vai trò\",\n    \"role_deleted\": \"Đã xóa vai trò\",\n    \"role_code_taken\": \"Mã vai trò đã tồn tại\",\n    \"role_read_only\": \"Vai trò này không thể thay đổi\",\n    \"permissions\": \"Quyền\",\n    \"save_permissions\": \"Lưu quyền\",\n    \"permissions_updated\": \"Đã cập nhật quyền\",\n    \"search\": \"Tìm kiếm\",\n    \"role_information\": \"Thông tin vai trò\",\n    \"assigned_users\": \"Người dùng được gán\",\n    \"permission_information\": \"Quyền theo tính năng\",\n    \"assign_user\": \"Gán người dùng\",\n    \"assign_user_to\": \"Gán người dùng cho {{name}}\",\n    \"assign_selected\": \"Gán đã chọn\",\n    \"select_one_user\": \"Chọn ít nhất một người dùng\",\n    \"users_assigned\": \"Đã gán {{count}} người dùng\",\n    \"user_unassigned\": \"Đã bỏ gán người dùng\",\n    \"no_users_assigned\": \"Chưa gán người dùng\",\n    \"no_users_available\": \"Không có người dùng để gán\",\n    \"unassign\": \"Bỏ gán\",\n    \"delete_role_confirm\": \"Xóa vai trò này?\",\n    \"users_count\": \"Người dùng\",\n    \"apply_permissions\": \"Áp dụng quyền\",\n    \"modules_list\": \"Module\",\n    \"modules_list_hint\": \"Chọn module, sau đó tick quyền bên phải\",\n    \"functions_components\": \"Chức năng & quyền\",\n    \"functions_components_hint\": \"Mở rộng từng tính năng để chọn READ, CREATE, UPDATE, DELETE\",\n    \"perm_pick_menu_group\": \"Chọn module bên trái\",\n    \"perm_no_child_features\": \"Không có tính năng trong module này\",\n    \"role_perm_functions_intro\": \"Tick từng quyền mà vai trò này được phép\""
        }
      ]
    },
    {
      "file": "app/views/layouts/admin/partials/sidebar.pug",
      "when": { "jobsEnabled": true },
      "ops": [
        {
          "type": "insertAfter",
          "marker": "span.admin-sidebar__label= t('sidebar.features')",
          "content": "\n        if can('JM::READ')\n          a.admin-sidebar__link(href=\"/admin/jobs\" class=activeMenu === 'jobs' ? 'is-active' : '' title=t('sidebar.jobs'))\n            span.admin-sidebar__icon(aria-hidden=\"true\")\n              +iconFeatures()\n            span.admin-sidebar__label= t('sidebar.jobs')"
        }
      ]
    },
    {
      "file": "configs/locales/en/admin.json",
      "when": { "jobsEnabled": true },
      "ops": [
        {
          "type": "insertAfter",
          "marker": "\"back_to_users\": \"Back to users\"",
          "content": ",\n    \"job_management\": \"Background Jobs\",\n    \"job_search_placeholder\": \"Search by job class or error\",\n    \"job_class\": \"Job class\",\n    \"job_details\": \"Job details\",\n    \"job_arguments\": \"Arguments\",\n    \"job_retried\": \"Job queued for retry\",\n    \"job_deleted\": \"Job record deleted\",\n    \"retry_job\": \"Retry\",\n    \"delete_job_confirm\": \"Delete this job record?\",\n    \"no_jobs\": \"No jobs found\",\n    \"all_queues\": \"All queues\",\n    \"source\": \"Source\",\n    \"attempts\": \"Attempts\",\n    \"created_at\": \"Created\",\n    \"started_at\": \"Started\",\n    \"completed_at\": \"Completed\",\n    \"failed_at\": \"Failed\",\n    \"last_error\": \"Last error\",\n    \"external_id\": \"External ID\",\n    \"back_to_jobs\": \"Back to jobs\",\n    \"jobs_mode_redis\": \"Queue: BullMQ + Redis (JOBS_USE_REDIS=true)\",\n    \"jobs_mode_database\": \"Queue: database (default). Set JOBS_USE_REDIS=true for Redis.\",\n    \"job_status_pending\": \"Pending\",\n    \"job_status_processing\": \"Processing\",\n    \"job_status_completed\": \"Completed\",\n    \"job_status_failed\": \"Failed\",\n    \"queue_database\": \"Database\",\n    \"queue_bullmq\": \"BullMQ\",\n    \"job_source_manual\": \"Manual\",\n    \"job_source_cron\": \"Scheduled\",\n    \"delete\": \"Delete\",\n    \"jobs_pagination\": \"Page {{page}} / {{totalPages}} ({{total}})\",\n    \"pagination_prev\": \"Previous\",\n    \"pagination_next\": \"Next\""
        },
        {
          "type": "insertAfter",
          "marker": "\"features\": \"Features\"",
          "content": ",\n    \"jobs\": \"Background Jobs\""
        }
      ]
    },
    {
      "file": "configs/locales/vi/admin.json",
      "when": { "jobsEnabled": true },
      "ops": [
        {
          "type": "insertAfter",
          "marker": "\"back_to_users\": \"Quay lại danh sách\"",
          "content": ",\n    \"job_management\": \"Job nền\",\n    \"job_search_placeholder\": \"Tìm theo tên job hoặc lỗi\",\n    \"job_class\": \"Job class\",\n    \"job_details\": \"Chi tiết job\",\n    \"job_arguments\": \"Tham số\",\n    \"job_retried\": \"Đã xếp hàng chạy lại job\",\n    \"job_deleted\": \"Đã xóa bản ghi job\",\n    \"retry_job\": \"Chạy lại\",\n    \"delete_job_confirm\": \"Xóa bản ghi job này?\",\n    \"no_jobs\": \"Chưa có job\",\n    \"all_queues\": \"Tất cả queue\",\n    \"source\": \"Nguồn\",\n    \"attempts\": \"Lần thử\",\n    \"created_at\": \"Tạo lúc\",\n    \"started_at\": \"Bắt đầu\",\n    \"completed_at\": \"Hoàn thành\",\n    \"failed_at\": \"Thất bại\",\n    \"last_error\": \"Lỗi cuối\",\n    \"external_id\": \"External ID\",\n    \"back_to_jobs\": \"Quay lại danh sách job\",\n    \"jobs_mode_redis\": \"Queue: BullMQ + Redis (JOBS_USE_REDIS=true)\",\n    \"jobs_mode_database\": \"Queue: database (mặc định). Đặt JOBS_USE_REDIS=true để dùng Redis.\",\n    \"job_status_pending\": \"Đang chờ\",\n    \"job_status_processing\": \"Đang chạy\",\n    \"job_status_completed\": \"Hoàn thành\",\n    \"job_status_failed\": \"Thất bại\",\n    \"queue_database\": \"Database\",\n    \"queue_bullmq\": \"BullMQ\",\n    \"job_source_manual\": \"Thủ công\",\n    \"job_source_cron\": \"Lịch\",\n    \"delete\": \"Xóa\",\n    \"jobs_pagination\": \"Trang {{page}} / {{totalPages}} ({{total}})\",\n    \"pagination_prev\": \"Trước\",\n    \"pagination_next\": \"Sau\""
        },
        {
          "type": "insertAfter",
          "marker": "\"features\": \"Tính năng\"",
          "content": ",\n    \"jobs\": \"Job nền\""
        }
      ]
    },
    {
      "file": "package.json",
      "ops": [
        {
          "type": "insertAfter",
          "marker": "\"db:seed\": \"tsx configs/db/seed.ts\"",
          "content": ",\n    \"db:seed:rbac\": \"tsx configs/db/seed-rbac.ts\""
        }
      ]
    },
    {
      "file": "app/assets/javascripts/main.ts",
      "ops": [
        {
          "type": "append",
          "content": "\nimport \"./admin/rbac\";\n"
        }
      ]
    }
  ],
  "postInstall": {
    "install": true,
    "scripts": ["db:generate", "db:push", "db:seed:rbac", "build:client"],
    "messages": [
      "Requires: admin, database, vue. API-only: `irwin add admin-rbac-api` (no admin/vue).",
      "Jobs UI: run `irwin add jobs` first (reads .env JOBS_USE_REDIS).",
      "Features: AM (menu), USM (users), RAP (roles). JM + /admin/jobs when jobs detected.",
      "Role detail: Vue permission matrix — run pnpm build:client after changes",
      "Login: auth/db:seed user, then pnpm run db:seed:rbac for ADMIN role"
    ]
  }
}
