# Phân quyền LCMS

Tài liệu làm việc: mô tả luồng phân quyền đang chạy, nhóm quyền, bảng liên quan và sơ đồ.

Nguồn sự thật trong code:

| Việc | File |
| --- | --- |
| Danh mục quyền (group + route) | `getPermissionRoutes()` trong `app/Helpers/Helper.php` |
| Kiểm tra quyền theo request | `checkPermissionUser()` cùng file |
| Middleware chặn route | `app/Http/Middleware/CheckPermission.php` (alias `check.permission`) |
| Lọc menu sidebar | `buildPermissionChecker()` trong `app/Providers/AppServiceProvider.php` |
| Gán quyền cho vai trò | `RoleController::syncRolePermissions()` |
| Gán user vào vai trò | `RoleController::assignUsers()` |
| Tenant được phép vào nhóm tính năng | `app/Http/Middleware/EnsureTenantHasAccess.php` (alias `tenant.access`) |

Quyền gắn với **tên route Laravel** (`route_name`), không gắn với URL. Một user có nhiều vai trò. Quyền của user là hợp của quyền các vai trò đó.

---

## 1. Hai lớp phân quyền

Hệ thống kiểm tra hai việc độc lập. Cả hai phải qua thì request mới vào controller.

### Lớp 1 — User có quyền làm việc này không

RBAC: user thuộc vai trò, vai trò giữ danh sách permission. Permission khớp khi `permission.route_name` bằng tên route hiện tại.

Dữ liệu nằm **trong database của tenant đang chọn** (`session('tenant_connection')`). User đăng nhập lấy từ DB master, sau đó map sang user tenant bằng email (`HelperTenant::getUserFromTenant`). `user_role.user_id` là id user **trong DB tenant**, không phải id user master.

`HelperTenant::isUserAllowedForTenant()` hiện luôn trả `true`, nên nhánh master DB trong `checkPermissionUser()` không còn chạy. Nhánh đó vẫn còn trong code (kèm bypass cứng hai email `kienpn@ctv.hocmai.vn` và `kienp2901@gmail.com`).

### Lớp 2 — Tenant có được mở tính năng này không

Middleware `tenant.access` đọc `hocmai_tenant_groups` theo `tenant_code`, lấy `group_key`, rồi đối chiếu route hiện tại với `config/tenant_groups.php`. Tenant không thuộc group chứa route đó thì bị 403, kể cả khi user có permission.

Đây là quyền theo **tenant / gói tính năng**, không theo user.

---

## 2. Luồng khi user gọi một route

```mermaid
flowchart TD
    A[Request tới route web] --> B{Đã đăng nhập?}
    B -->|Không| C[Redirect login]
    B -->|Có| D{Route có middleware tenant.access?}
    D -->|Có| E[Đọc hocmai_tenant_groups theo tenant_code]
    E --> F{group_key có route này trong config tenant_groups?}
    F -->|Không| G[403 Tenant has no access]
    F -->|Có| H{Route có middleware check.permission?}
    D -->|Không| H
    H -->|Không| Z[Vào controller]
    H -->|Có| I{Route có name?}
    I -->|Không| Z
    I -->|Có| J[checkPermissionUser route name]
    J --> K[Lấy user tenant theo email]
    K --> L{Có user trong DB tenant?}
    L -->|Không| M[Từ chối]
    L -->|Có| N[user_role → roles]
    N --> O{Có role name = admin<br/>và route không thuộc danh sách bắt buộc kiểm?}
    O -->|Có| Z
    O -->|Không| P[role_permission → permission]
    P --> Q{Có permission.route_name = route hiện tại<br/>và role_permission chưa xóa mềm?}
    Q -->|Có| Z
    Q -->|Không| M
    M --> R{Request JSON?}
    R -->|Có| S[403 JSON]
    R -->|Không, có trang trước| T[Back kèm flash error]
    R -->|Không, vào thẳng URL| U[Redirect dashboard]
```

Thứ tự trong `CheckPermission`:

1. Chưa login → `login`.
2. Route không có `name` → cho qua.
3. `checkPermissionUser($routeName)` trả false:
   - AJAX / JSON → HTTP 403, message `Bạn không có quyền thực hiện hành động này.`
   - Có trang trước khác URL hiện tại → `back()` kèm flash error.
   - Vào thẳng URL → redirect `dashboard`.

Menu sidebar dùng cùng quy tắc nhưng preload một lần trong `buildPermissionChecker()` để không query từng mục menu.

---

## 3. Quy tắc đặc biệt khi kiểm tra

| Quy tắc | Chi tiết |
| --- | --- |
| Role `admin` | `roles.name = 'admin'` được mọi route, trừ route trong `$routeNameNeedCheck`. |
| Route admin vẫn phải có quyền | Hiện chỉ `lcms-overview-report.index`. |
| Khớp quyền | Chỉ so `permission.route_name`. `method` và `action` dùng lúc tạo bản ghi, không dùng lúc kiểm tra. |
| Soft delete | `user_role` và `role_permission` có `deleted_at`. Eloquent bỏ bản ghi đã xóa. Gỡ user khỏi role là xóa mềm, gán lại thì restore. |
| Bảng `permission` | Không soft delete. Bỏ tick trên form role chỉ gỡ `role_permission`, không xóa dòng permission. |
| Route chỉ ICC | Menu ẩn các route OA / learning object / `exam-histories.index` nếu connection không phải `lms_tenant_icc`. Danh sách nằm trong `checkPermissionTenantICC()` và `$iccOnlyRoutes` của `buildPermissionChecker()`. |
| Menu OA | Nhóm menu OA chỉ ghép vào sidebar khi email nằm trong `HelperTenant::isUserAllowedForTenantICC()`. |
| Side effect chấm bài | Gán user vào role có `permission.route_name` dạng `exam-grading%` sẽ tạo hoặc restore dòng `teachers`. Gỡ hết role chấm bài thì xóa teacher. Logic: `RoleController::syncTeacherFromRole()`. |

---

## 4. Luồng gán quyền trên giao diện

### 4.1 Tạo / sửa vai trò

1. Form `role.add` / `role.edit` render checkbox từ `getPermissionRoutes()`, nhóm theo `label`.
2. Submit gửi mảng `permission_route_name`.
3. `Common::storeRole()` / `updateRole()` ghi `roles` (có `short_name` sinh từ tên).
4. `syncRolePermissions()`:
   - Bỏ route không có trong config.
   - Tìm `permission` theo `route_name + action + method`. Chưa có thì tạo mới, `name` = label trong config.
   - `role_permission` thừa so với lựa chọn mới → set `deleted_at`.
   - `role_permission` mới → insert, hoặc restore nếu trước đó đã xóa mềm.

Permission **không seed sẵn**. Dòng `permission` xuất hiện lần đầu khi có người lưu role (hoặc tạo tay ở màn Quản lý quyền).

### 4.2 Gán user vào vai trò

Trang `role.assigns` → `assignUsers()`:

- Thêm: insert `user_role`, hoặc `deleted_at = null` nếu đã từng gán.
- Gỡ: set `deleted_at`.
- `user_id` là id trong bảng `users` của **DB tenant**.

### 4.3 Tạo permission thủ công

`permission.store` cũng đọc `getPermissionRoutes()`, tạo dòng `permission`, và có thể gắn luôn vào các `role_id` được chọn. Route đã tồn tại cùng `route_name + action + method` thì bỏ qua.

---

## 5. Sơ đồ bảng

### 5.1 RBAC trong từng database tenant

Các bảng này có cả trên DB master (luồng cũ) và trên mỗi DB tenant. Luồng đang chạy đọc/ghi bản tenant.

```mermaid
erDiagram
    users ||--o{ user_role : "user_id"
    roles ||--o{ user_role : "role_id"
    roles ||--o{ role_permission : "role_id"
    permission ||--o{ role_permission : "permission_id"
    users ||--o| teachers : "user_id khi role có exam-grading"

    users {
        bigint id PK
        string email
        string name
    }
    roles {
        bigint id PK
        string name "admin là bypass"
        string short_name
        text description
        datetime deleted_at
    }
    permission {
        bigint id PK
        string name "label hiển thị"
        string route_name "khóa so khớp"
        string action
        string method
        text description
    }
    user_role {
        bigint id PK
        bigint role_id
        bigint user_id
        datetime deleted_at
    }
    role_permission {
        bigint id PK
        bigint role_id
        bigint permission_id
        datetime deleted_at
    }
    teachers {
        bigint id PK
        bigint user_id
        string email
        datetime deleted_at
    }
```

Không có foreign key giữa các bảng này. Quan hệ do code giữ.

| Bảng | Cột dùng cho phân quyền | Ghi chú |
| --- | --- | --- |
| `users` | `id`, `email` | Trong DB tenant. Map từ user master bằng email. |
| `roles` | `name`, `description`, `short_name`, `deleted_at` | `name = admin` bỏ qua kiểm tra permission. Model `App\Models\Roles`, soft delete. |
| `permission` | `name`, `route_name`, `action`, `method`, `description` | Model `App\Models\Permission`. Không soft delete. |
| `user_role` | `role_id`, `user_id`, `deleted_at` | Model `App\Models\UserRole`. |
| `role_permission` | `role_id`, `permission_id`, `deleted_at` | Model `App\Models\RolePermission`. |
| `teachers` | `user_id`, `email` | Không phải bảng quyền. Được đồng bộ khi role có route `exam-grading%`. |

Model dùng trait `UsesTenantConnection` nên query đi theo connection tenant hiện tại khi gọi `Model::on($tenantConnection)`.

### 5.2 Bảng master — tenant được mở tính năng nào

Nằm trên DB master, không nằm trong DB tenant.

```mermaid
erDiagram
    hocmai_tenants ||--o{ hocmai_tenant_domains : tenant_id
    hocmai_tenants ||--o{ user_tenants : tenant_id
    hocmai_tenants ||--o{ hocmai_tenant_groups : tenant_id
    users ||--o{ user_tenants : user_id

    hocmai_tenants {
        bigint id PK
        string code UK
        string name
        string db_name
        tinyint status
    }
    hocmai_tenant_domains {
        bigint id PK
        bigint tenant_id FK
        string db_name
        string name
    }
    user_tenants {
        bigint id PK
        bigint user_id
        bigint tenant_id
    }
    hocmai_tenant_groups {
        bigint id PK
        bigint tenant_id
        string tenant_code
        string db_name
        string group_key
    }
```

| Bảng | Vai trò trong phân quyền |
| --- | --- |
| `hocmai_tenants` | Tenant (`code`, `db_name`). |
| `hocmai_tenant_domains` | Domain + `db_name` để suy ra connection (`HelperTenant::getTenantByConnection`). |
| `user_tenants` | User master được vào tenant nào. Unique `(user_id, tenant_id)`. |
| `hocmai_tenant_groups` | Tenant thuộc những `group_key` nào. `tenant.access` và sidebar (`getSidebarMenu`) đọc bảng này, rồi lấy route/menu từ `config/tenant_groups.php`. |

`user_tenants` quyết định user **được chọn tenant**. `hocmai_tenant_groups` quyết định tenant **được mở module**. RBAC trong DB tenant quyết định user **được bấm route**.

---

## 6. Nhóm quyền

Catalog dưới đây là các group đang bật trong `getPermissionRoutes()`. Group `class_enrol` đang comment, không hiện trên form.

Khi thêm quyền mới: thêm item vào đúng group trong hàm này, bảo vệ route bằng `check.permission`, rồi lưu lại role để sinh dòng `permission`. Nếu route thuộc module theo tenant, thêm route vào `config/tenant_groups.php` và gán `group_key` cho tenant.

### 6.1 Hệ thống và tài khoản

**`user_management` — Quản lý người dùng**

| route_name | Method | Nhãn |
| --- | --- | --- |
| `admin/users` | GET | Danh sách người dùng |
| `admin/users/create` | GET | Tạo người dùng |
| `admin/users/save` | POST | Lưu người dùng |
| `admin/users/edit` | GET | Chỉnh sửa người dùng |
| `admin/users/update` | PUT | Cập nhật người dùng |
| `admin/users/delete` | GET | Xóa người dùng |
| `users.index` | GET | Danh sách người dùng theo quyền |

**`role_management` — Quản lý vai trò**

| route_name | Method | Nhãn |
| --- | --- | --- |
| `role.index` | GET | Danh sách vai trò |
| `role.add` | GET | Thêm vai trò |
| `role.store` | POST | Lưu vai trò |
| `role.edit` | GET | Chỉnh sửa vai trò |
| `role.update` | POST | Cập nhật vai trò |
| `role.delete` | POST | Xóa vai trò |
| `role.assigns` | GET | Phân quyền người dùng |
| `role.assignUser` | POST | Gán người dùng vào vai trò |

**`permission_management` — Quản lý quyền**

| route_name | Method | Nhãn |
| --- | --- | --- |
| `permission.index` | GET | Danh sách quyền |
| `permission.add` | GET | Thêm quyền |
| `permission.store` | POST | Lưu quyền |
| `permission.edit` | GET | Chỉnh sửa quyền |
| `permission.update` | POST | Cập nhật quyền |
| `permission.delete` | POST | Xóa quyền |

**`tenant_assignment` — Quản lý phân gán Tenant**

Đang bật một route. Các route index / assign / remove đang comment trong config.

| route_name | Method | Nhãn |
| --- | --- | --- |
| `tenant-assignment.search-users` | GET | Tìm kiếm người dùng |

### 6.2 Vận hành lớp, học sinh, giáo viên

**`class_management` — Quản lý lớp học**

| route_name | Method | Nhãn |
| --- | --- | --- |
| `class.index` | GET | Danh sách lớp học |
| `class.add` | GET | Thêm lớp học |
| `class.store` | POST | Lưu lớp học |
| `class.search` | GET | Tìm kiếm lớp học |
| `class.edit` | GET | Chỉnh sửa lớp học |
| `class.update` | POST | Cập nhật lớp học |
| `class.delete` | POST | Xóa lớp học |
| `class.import` | POST | Import lớp học |
| `class.searchClasses` | GET | Tìm kiếm lớp học (lịch thi và kết quả) |

**`student_management` — Quản lý học sinh**

| route_name | Method | Nhãn |
| --- | --- | --- |
| `student.index` | GET | Danh sách học sinh |
| `student.searchStudents` | GET | Tìm kiếm học sinh |

**`teacher_management` — Quản lý giáo viên**

| route_name | Method | Nhãn |
| --- | --- | --- |
| `teacher.index` | GET | Danh sách giáo viên |
| `teacher.delete` | POST | Xóa giáo viên |
| `teacher.searchTeachers` | GET | Tìm kiếm giáo viên |

**`teams` — Quản lý nhóm giáo viên**

| route_name | Method | Nhãn |
| --- | --- | --- |
| `teams.index` | GET | Trang danh sách nhóm giáo viên |
| `teams.getTeams` | GET | Lấy danh sách nhóm giáo viên |
| `teams.show` | GET | Xem chi tiết nhóm giáo viên |
| `teams.getTeachersInOATeam` | GET | Lấy danh sách giáo viên trong nhóm |
| `teams.store` | POST | Tạo nhóm giáo viên |
| `teams.update` | POST | Cập nhật nhóm giáo viên |
| `teams.destroy` | POST | Xoá nhóm giáo viên |
| `teams.addTeacherToTeam` | POST | Thêm giáo viên vào nhóm |

**`user_learning` — Quản lý học tập người dùng**

| route_name | Method | Nhãn |
| --- | --- | --- |
| `user_learning.courses` | GET | Danh sách khóa học của người dùng |

### 6.3 Sản phẩm và nội dung

**`product_management` — Quản lý sản phẩm/khoá học**

| route_name | Method | Nhãn |
| --- | --- | --- |
| `course.index` | GET | Danh sách sản phẩm/khoá học |
| `course.detail` | GET | Chi tiết sản phẩm/khoá học |
| `course.attach-lor-to-activity` | POST | Gán LOR File vào hoạt động |
| `course.open-section` | POST | Mở section |
| `course.set-ems-avg-score` | POST | Cập nhật điểm trung bình EMS |
| `product.list` | GET | Danh sách sản phẩm vận hành |
| `product.student` | GET | Danh sách học sinh theo sản phẩm |
| `product.teacher` | GET | Danh sách giáo viên theo sản phẩm |
| `course.searchCourses` | GET | Tìm kiếm khoá học |
| `course.searchSections` | GET | Tìm kiếm sections |
| `course.searchQuizzes` | GET | Tìm kiếm quizzes |
| `course.searchCategories` | GET | Tìm kiếm categories |

`course.open-section` và `course.set-ems-avg-score` còn được kiểm tra riêng trong `ProductController` bằng `checkPermissionUserByEmail()`.

### 6.4 Báo cáo

**`study_progress_report` — Báo cáo tiến độ học tập**

| route_name | Method | Nhãn |
| --- | --- | --- |
| `study-progress-report.index` | GET | Báo cáo tình hình học tập |
| `homework-quiz-report.index` | GET | Báo cáo BTVN & Bài kiểm tra |
| `lcms-overview-report.index` | GET | Tổng quan tiến độ LCMS |

`lcms-overview-report.index` là route admin vẫn phải được gán tường minh.

### 6.5 Thi, chấm bài, lịch thi

**`exam_permission` — Quản lý quyền thi** (cấp lượt, thi lại, deadline section)

| route_name | Method | Nhãn |
| --- | --- | --- |
| `exam-permission.new-attempt.index` | GET | Cấp lượt thi mới |
| `exam-permission.retake.index` | GET | Thi lại |
| `exam-permission.search-students` | GET | Tìm kiếm học sinh |
| `exam-permission.student-results` | GET | Kết quả thi học sinh |
| `exam-permission.user-attempts.search` | POST | Tìm kiếm lượt làm bài |
| `exam-permission.user-attempts.update-reset-allowance` | POST | Cập nhật lượt cộng thêm |
| `exam-permission.sections` | GET | Lấy danh sách section cấp lượt mới |
| `exam-permission.search-classes-for-section` | GET | Tìm kiếm lớp cho section |
| `exam-permission.save-deadlines` | POST | Lưu deadline section |
| `exam-permission.class-assignment-history` | GET | Lịch sử gán lớp |

**`exam-histories` — Danh sách bài thi**

| route_name | Method | Nhãn |
| --- | --- | --- |
| `exam-histories.index` | GET | Danh sách bài thi |
| `exam-histories.detail` | GET | Trang chi tiết bài thi |
| `exam-histories.assessment` | GET | Trang round thi cần chấm |
| `exam-histories.basicExamHistory` | GET | Trang basic exam history |
| `exam-histories.getBasicExamHistories` | GET | Lấy danh sách basic exam history |
| `exam-histories.getExamHistories` | GET | Lấy danh sách bài thi |
| `exam-histories.getStudentResults` | GET | Lấy danh sách kết quả học viên |
| `exam-histories.getExamHistoryDetail` | GET | Lấy chi tiết bài thi |
| `exam-histories.submitExamNeedAssignment` | POST | Gửi yêu cầu xác nhận bài thi cần chấm |
| `exam-histories.submitRevertExamNeedAssignment` | POST | Thu hồi yêu cầu xác nhận bài thi cần chấm |
| `exam-histories.export` | GET | Xuất kết quả thi |
| `exam-histories.export-progress` | GET | Lấy tiến độ xuất |
| `exam-histories.download` | GET | Tải xuống kết quả thi |

`exam-histories.index` thuộc danh sách route chỉ hiện với tenant ICC.

**`student-exam-histories` — Lịch sử bài thi học viên**

| route_name | Method | Nhãn |
| --- | --- | --- |
| `student-exam-histories.detail` | GET | Trang chi tiết bài thi học viên |
| `student-exam-histories.edit` | GET | Trang chấm lại bài thi học viên |
| `student-exam-histories.getStudentExamHistoryDetail` | GET | Lấy thông tin chấm bài trang chi tiết học viên |
| `student-exam-histories.assignTeacher` | POST | Phân công giáo viên chấm bài |
| `student-exam-histories.calculateWritingTaskFinalScore` | POST | Tính điểm cuối Writing |
| `student-exam-histories.calculateSpeakingTaskFinalScore` | POST | Tính điểm cuối Speaking |
| `student-exam-histories.getTeacherAssignedOverview` | GET | Lấy thông tin bài thi cho GV chấm |
| `student-exam-histories.getSkillsForAssignment` | GET | Lấy thông tin kỹ năng cho việc gán chấm |
| `student-exam-histories.receiveGrading` | GET | GV nhận chấm bài |
| `student-exam-histories.QAReceiveGrading` | GET | QA nhận chấm bài |
| `student-exam-histories.teacherWritingReceiveGrading` | GET | Giáo viên nhận chấm bài writing |
| `student-exam-histories.teacherSpeakingReceiveGrading` | GET | Giáo viên nhận chấm bài speaking |
| `student-exam-histories.VHReviewGrading` | GET | VH review bài thi |
| `student-exam-histories.gradingTeacher` | GET | Nhận chấm bài |
| `student-exam-histories.assignRandomQa` | POST | Gán QA phúc khảo |
| `student-exam-histories.getExamProgress` | POST | Lấy tiến bộ bài thi |
| `student-exam-histories.createRetakeSession` | POST | Tạo lượt thi lại |

**`student-exam-details` — Chi tiết bài thi**

| route_name | Method | Nhãn |
| --- | --- | --- |
| `student-exam-details.assessment` | GET | Trang bài thi cần chấm của tôi |
| `student-exam-details.getMyAssessments` | GET | Lấy danh sách bài được phân công cho tôi |
| `student-exam-details.getStudentExamDetails` | GET | Lấy danh sách round thi cần chấm |
| `student-exam-details.getRatingLabels` | GET | Lấy danh sách nhãn đánh giá |
| `student-exam-details.returnAssessment` | POST | Hoàn chuyển yêu cầu chấm bài |
| `student-exam-details.saveDataRescored` | POST | Lưu dữ liệu chấm lại |

**`exam-grading` — Quản lý chấm bài thi**

Có bất kỳ route `exam-grading%` trên role thì gán user vào role đó sẽ đồng bộ bảng `teachers`.

| route_name | Method | Nhãn |
| --- | --- | --- |
| `exam-grading.index` | GET | Truy cập trang quản lý chấm bài thi |
| `exam-grading.getExamGrading` | GET | Lấy danh sách quản lý chấm bài thi |
| `exam-grading.export` | GET | Xuất chấm bài thi |
| `exam-grading.export-progress` | GET | Lấy tiến độ xuất |
| `exam-grading.download` | GET | Tải xuống chấm bài thi |
| `exam-grading.viewAllGrading` | GET | Xem tất cả bài thi cần chấm |

`exam-grading.viewAllGrading` được `ExamGradingController` kiểm tra thêm trong action, không chỉ ở middleware.

**`exam-regrading` — Quản lý phúc khảo**

| route_name | Method | Nhãn |
| --- | --- | --- |
| `exam-regrading.index` | GET | Truy cập trang quản lý phúc khảo |
| `exam-regrading.getExamRegrading` | GET | Lấy danh sách quản lý phúc khảo |

**`exam-schedule` — Danh sách học viên đến lịch thi**

| route_name | Method | Nhãn |
| --- | --- | --- |
| `exam-schedule.index` | GET | Truy cập trang |
| `exam-schedule.getExamSchedule` | GET | Lấy danh sách học viên đến lịch thi |
| `exam-schedule.storeExamSchedule` | POST | Tạo danh sách |
| `exam-schedule.updateExamSchedule` | POST | Cập nhật danh sách |
| `exam-schedule.export` | POST | Xuất lịch thi |
| `exam-schedule.export-progress` | GET | Lấy tiến độ xuất |
| `exam-schedule.download` | GET | Tải xuống lịch thi |
| `exam-schedule.extendExamSchedule` | POST | Gia hạn lịch thi |
| `exam-schedule.deleteExamSchedule` | POST | Xóa lịch thi |

`ExamScheduleController` còn gọi `checkPermissionUser` cho `store` / `update` / `extend` bên trong action.

**`exam-schedule-result` — Lịch thi và kết quả thi**

| route_name | Method | Nhãn |
| --- | --- | --- |
| `exam-schedule-result.index` | GET | Truy cập trang |
| `exam-schedule-result.getExamScheduleResult` | GET | Lấy danh sách lịch thi |
| `exam-schedule-result.getStudentsBySchedule` | GET | Lấy danh sách học viên theo lịch thi |
| `exam-schedule-result.createNewSchedule` | POST | Tạo lịch thi mới |
| `exam-schedule-result.export` | POST | Xuất file dữ liệu |
| `exam-schedule-result.export-progress` | GET | Kiểm tra tiến trình xuất file |
| `exam-schedule-result.download` | GET | Tải xuống file đã xuất |
| `exam-schedule-result.loadTab` | GET | Tải dữ liệu tab |

**`skills` — Quản lý kỹ năng**

| route_name | Method | Nhãn |
| --- | --- | --- |
| `skills.getSkills` | GET | Lấy danh sách kỹ năng |

**`contest-type` — Loại kỳ thi**

| route_name | Method | Nhãn |
| --- | --- | --- |
| `contest-type.getContestTypes` | GET | Lấy danh sách loại kỳ thi |

### 6.6 OA, Learning Object, AI

Các route index của OA và Learning Object nằm trong danh sách chỉ ICC (`checkPermissionTenantICC`).

**`oa` — Quản lý OA**

| route_name | Method | Nhãn |
| --- | --- | --- |
| `oa-exams.index` | GET | Danh sách bài thi |
| `oa-groups.index` | GET | Danh sách gói đề |
| `oa-packages.index` | GET | Danh sách sản phẩm |
| `oa-exam-histories.index` | GET | Danh sách lịch sử làm bài thi |
| `oa-exam-histories.detail` | GET | Chi tiết bài thi |
| `oa-student-exam-histories.detail` | GET | Chi tiết kết quả làm bài thi |

**`learning_object` — Quản lý Learning Object**

| route_name | Method | Nhãn |
| --- | --- | --- |
| `learning-object.index` | GET | Danh sách Learning Object |
| `learning-object.show` | GET | Chi tiết Learning Object |
| `learning-object.getLearningObjectVersions` | GET | Lấy danh sách phiên bản |
| `learning-object.store` | POST | Tạo Learning Object |
| `learning-object.update` | POST | Cập nhật Learning Object |
| `learning-object.revertVersion` | POST | Khôi phục phiên bản |
| `learning-object.previewFile` | GET | Xem trước file |
| `learning-object.downloadFile` | GET | Tải xuống file |

**`folders` — Quản lý Folder**

| route_name | Method | Nhãn |
| --- | --- | --- |
| `folders.children` | GET | Lấy danh sách folder con |
| `folders.search` | GET | Tìm kiếm folder |
| `folders.store` | POST | Tạo folder |
| `folders.update` | POST | Cập nhật folder |
| `folders.destroy` | POST | Xóa folder |

**`allowed_domain` — Quản lý Allowed Domain**

| route_name | Method | Nhãn |
| --- | --- | --- |
| `allowed-domain.index` | GET | Danh sách Allowed Domain |
| `allowed-domain.show` | GET | Chi tiết Allowed Domain |
| `allowed-domain.store` | POST | Tạo Allowed Domain |
| `allowed-domain.update` | POST | Cập nhật Allowed Domain |
| `allowed-domain.destroy` | POST | Xóa Allowed Domain |

**`ai_provider` — Quản lý AI Provider**

| route_name | Method | Nhãn |
| --- | --- | --- |
| `aiProviderIndex` | GET | Danh sách |
| `aiProviderCreate` | GET | Tạo |
| `aiProviderStore` | POST | Lưu |
| `aiProviderEdit` | GET | Chỉnh sửa |
| `aiProviderUpdate` | PUT | Cập nhật |
| `aiProviderDestroy` | DELETE | Xóa |

**`ai_service` — Quản lý AI Service**

| route_name | Method | Nhãn |
| --- | --- | --- |
| `serviceIndex` | GET | Danh sách |
| `serviceCreate` | GET | Tạo |
| `serviceStore` | POST | Lưu |
| `serviceEdit` | GET | Chỉnh sửa |
| `serviceUpdate` | PUT | Cập nhật |
| `serviceDestroy` | DELETE | Xóa |

**`ai_service_provider` — Quản lý AI Service Provider**

| route_name | Method | Nhãn |
| --- | --- | --- |
| `serviceProviderIndex` | GET | Danh sách |
| `serviceProviderCreate` | GET | Tạo |
| `serviceProviderStore` | POST | Lưu |
| `serviceProviderEdit` | GET | Chỉnh sửa |
| `serviceProviderUpdate` | PUT | Cập nhật |
| `serviceProviderDestroy` | DELETE | Xóa |

---

## 7. Việc cần nhớ khi sửa phân quyền

1. Sửa danh mục ở `getPermissionRoutes()`, không sửa tay từng môi trường rồi quên config. Form role đọc hàm này.
2. Route muốn chặn phải có `->name(...)` và middleware `check.permission`. Route không name bị middleware cho qua.
3. Sau khi thêm item mới, mở sửa role và lưu lại để `syncRolePermissions()` tạo dòng `permission` trên DB tenant đó. Mỗi tenant một bộ bảng.
4. Role `admin` không cần tick, trừ `lcms-overview-report.index`.
5. Module theo gói tenant phải có mặt trong `config/tenant_groups.php` và `hocmai_tenant_groups`, nếu không `tenant.access` chặn trước cả RBAC.
6. Menu và middleware cùng so `route_name`, nhưng menu còn lọc thêm route ICC và allowlist email OA. Có quyền trong DB mà menu không hiện thì kiểm tra hai chỗ đó.
