Role Matrix — Access Control
Purpose: ma trận quyền của toàn bộ API endpoint, quyết dứt khoát role nào được làm gì. Là nguồn chính thức trước khi mobile app triển khai role-switch (xem
docs/mobile/feature-plan.md §0).Status: 🚧 Draft — review + implementation phase 2 pending
Last updated: 2026-04-23
1. Nguyên tắc
1.1 Role hierarchy (đã cài đặt)
booking-api/src/auth/guards/roles.guard.ts xếp hạng role theo thứ bậc, so sánh >=:
ADMIN (4) > OWNER (3) > STAFF (2) > CUSTOMER (1)
→ @Roles('OWNER') ngầm cho phép ADMIN. @Roles('STAFF') ngầm cho phép STAFF, OWNER, ADMIN.
1.2 Quy tắc decorator
| Decorator | Ý nghĩa | Ai đi qua |
|---|---|---|
@Public() |
Bypass cả JwtAuthGuard + RolesGuard |
Mọi request (không cần token) |
| (không decorator) | JwtAuthGuard active, RolesGuard returns true |
ADMIN/OWNER/STAFF authenticated (CUSTOMER token bị JwtAuthGuard reject ở line 44–47) |
@Roles('STAFF') |
STAFF + OWNER + ADMIN (hierarchy) | STAFF, OWNER, ADMIN |
@Roles('OWNER') |
OWNER + ADMIN (hierarchy) | OWNER, ADMIN |
@Roles('OWNER','STAFF') |
Tương đương @Roles('STAFF') (redundant nhưng explicit — khuyến khích) |
STAFF, OWNER, ADMIN |
@Roles('ADMIN') |
Chỉ ADMIN | ADMIN |
@CustomerAuth() |
Riêng cho customer JWT (type: customer) |
CUSTOMER |
1.3 Convention decide cho ma trận
- Explicit > implicit — mọi admin controller PHẢI khai báo
@Roles()tường minh, không dựa hierarchy. Dễ audit. - STAFF read mặc định = true cho dữ liệu vận hành hàng ngày (services, resources, customers, bookings, tenant settings). STAFF là frontline, cần context để phục vụ khách.
- STAFF write/mutate = scoped — chỉ được sửa tài nguyên mình đang đảm nhận (booking
resourceId = staff.resource.idhoặc unassigned). - OWNER full trên tenant mình — tạo/sửa/xoá staff, service, schedule, settings, payment config.
- ADMIN = superadmin — chỉ cross-tenant (list tenants, impersonate). KHÔNG nên dùng ADMIN để làm việc trong 1 tenant cụ thể (phải impersonate OWNER — feature chưa có).
- CUSTOMER = tách biệt hoàn toàn — dùng
@CustomerAuth(), không bao giờ truy cập admin endpoints.
1.4 Resource-scoping (business rule cho STAFF)
STAFF chỉ được phép đọc/sửa booking:
WHERE tenantId = staff.tenantId
AND ( resourceId = staff.resource.id
OR resourceId IS NULL -- unassigned, cho self-pick
)
Áp dụng ở service layer (không phải guard), trong:
BookingService.findAllByTenant()BookingService.findById()BookingService.update()BookingService.updateStatus()BookingService.selfPick()— đã có, validate skill
Tenant-customer notes/tags STAFF sửa được, metrics (visitCount, totalSpent) KHÔNG sửa được.
2. Ma trận — Admin endpoints
Cột:
C= CURRENT (trạng thái code hiện tại — đã grep từ*.controller.ts)T= TARGET (sau khi audit xong, theo design bên dưới)✅= allow,❌= deny,🔒= allow + scoped ở service layer
2.1 Tenant / Onboarding / Settings
| Method | Endpoint | ADMIN (C→T) | OWNER (C→T) | STAFF (C→T) | Ghi chú |
|---|---|---|---|---|---|
| GET | /tenants |
✅ → ✅ | ❌ → ❌ | ❌ → ❌ | Superadmin list tenants |
| GET | /tenants/:id |
✅ → ✅ | ✅ → ✅ | ✅ → ✅ | Đọc tenant info (currently no @Roles — ok, cần decorator tường minh) |
| GET | /tenants/slug/:slug |
✅ → ✅ (Public) | ✅ → ✅ | ✅ → ✅ | @Public() — customer portal subdomain resolve |
| POST | /tenants |
✅ → ✅ | ❌ → ❌ | ❌ → ❌ | Superadmin tạo tenant |
| PATCH | /tenants/:id |
✅ → ✅ | ✅ → ✅ | ❌ → ❌ | Update tenant/settings/branding (OWNER) |
| GET | /tenants/me/onboarding |
❌ → ❌ | ✅ → ✅ | ❌ → ❌ | Owner-only wizard |
| PATCH | /tenants/me/onboarding/step |
❌ → ❌ | ✅ → ✅ | ❌ → ❌ | |
| POST | /tenants/me/onboarding/complete |
❌ → ❌ | ✅ → ✅ | ❌ → ❌ |
Change needed:
GET /tenants/:id— thêm@Roles('STAFF','OWNER','ADMIN')tường minh (hiện không có).
2.2 Resource (Staff)
| Method | Endpoint | ADMIN | OWNER | STAFF (C→T) | Ghi chú |
|---|---|---|---|---|---|
| GET | /resources |
✅ | ✅ | ✅ → ✅ | Read — list đồng nghiệp, hiện không @Roles (STAFF qua được) |
| GET | /resources/:id |
✅ | ✅ | ✅ → ✅ | Read detail |
| POST | /resources |
✅ | ✅ | ❌ → ❌ | OWNER tạo staff mới — optional login: { email?, phone?, password (≥6), role: 'OWNER'|'STAFF' } tạo luôn User linked qua Resource.userId trong cùng transaction. Email/phone unique per-tenant (@@unique([email, tenantId])). |
| PATCH | /resources/:id |
✅ | ✅ | ❌ → 🔒 | TARGET: STAFF sửa được resource của chính mình (metadata phone/email, color) |
| POST | /resources/:id/skills |
✅ | ✅ | ❌ → ❌ | OWNER assign skills |
| PUT | /resources/:id/skills |
✅ | ✅ | ❌ → ❌ | OWNER replace skills |
| DELETE | /resources/:id/skills/:serviceId |
✅ | ✅ | ❌ → ❌ | |
| GET | /resources/:id/schedules |
✅ | ✅ | ✅ → ✅ | Read schedule |
| POST | /resources/:id/schedules |
✅ | ✅ | ❌ → ❌ | OWNER set schedule |
| POST | /resources/:id/schedules/bulk |
✅ | ✅ | ❌ → ❌ | |
| PATCH | /resources/:id/schedules/:scheduleId |
✅ | ✅ | ❌ → ❌ | |
| DELETE | /resources/:id/schedules/:scheduleId |
✅ | ✅ | ❌ → ❌ | |
| GET | /resources/:id/schedule-overrides |
✅ | ✅ | ✅ → ✅ | |
| POST | /resources/:id/schedule-overrides |
✅ | ✅ | ❌ → ❌ | |
| PATCH / DELETE | /resources/:id/schedule-overrides/:overrideId |
✅ | ✅ | ❌ → ❌ | |
| GET | /resources/:id/time-offs |
✅ | ✅ | ✅ → ✅ | |
| POST | /resources/:id/time-offs |
✅ | ✅ | ❌ → 🔒 | TARGET: STAFF request time-off cho chính mình (:id = staff.resource.id, isApproved=false) |
| PATCH | /resources/:id/time-offs/:timeOffId |
✅ | ✅ | ❌ → 🔒 | TARGET: STAFF sửa request của mình khi chưa approved; OWNER approve/reject |
| DELETE | /resources/:id/time-offs/:timeOffId |
✅ | ✅ | ❌ → 🔒 | TARGET: STAFF huỷ request của mình khi chưa approved |
Change needed:
- Thêm
@Roles()tường minh cho các GET endpoint (hiện không có → implicit). - Thêm service-layer scoping cho STAFF self time-off.
- Thêm service-layer scoping
PATCH /resources/:idcho STAFF (limit fields, chỉ own resource).
2.3 Service & Category
| Method | Endpoint | ADMIN | OWNER | STAFF (C→T) | Ghi chú |
|---|---|---|---|---|---|
| GET | /services |
✅ | ✅ | ✅ → ✅ | Staff cần list để walk-in |
| GET | /services/:id |
✅ | ✅ | ✅ → ✅ | |
| POST | /services |
✅ | ✅ | ❌ → ❌ | OWNER CRUD catalog |
| PATCH | /services/:id |
✅ | ✅ | ❌ → ❌ | |
| GET | /services/:id/resources |
✅ | ✅ | ✅ → ✅ | Ai làm được service này |
| PUT | /services/:id/resources |
✅ | ✅ | ❌ → ❌ | |
| GET | /service-categories |
✅ | ✅ | ✅ → ✅ | |
| GET | /service-categories/:id |
✅ | ✅ | ✅ → ✅ | |
| POST / PATCH / DELETE | /service-categories* |
✅ | ✅ | ❌ → ❌ | OWNER only |
Change needed:
- Thêm
@Roles('STAFF','OWNER','ADMIN')tường minh cho các GET.
2.4 Booking (CRITICAL — cần resource-scoping)
| Method | Endpoint | ADMIN | OWNER | STAFF (C→T) | Scope rule |
|---|---|---|---|---|---|
| GET | /bookings |
✅ | ✅ | ✅ → 🔒 | TARGET: STAFF list tự động filter resourceId = staff.resource.id OR resourceId IS NULL |
| GET | /bookings/:id |
✅ | ✅ | ✅ → 🔒 | TARGET: STAFF 403 nếu booking.resourceId ≠ mình và ≠ NULL |
| POST | /bookings |
✅ | ✅ | ✅ → 🔒 | TARGET: STAFF chỉ tạo booking có items[].resourceId = staff.resource.id |
| POST | /bookings/walk-in |
✅ | ✅ | ✅ → 🔒 | TARGET: STAFF chỉ auto-assign cho mình (resourceId = staff.resource.id forced) |
| PATCH | /bookings/:id |
✅ | ✅ | ✅ → 🔒 | TARGET: STAFF 403 nếu booking không thuộc mình; cấm thay resourceId sang người khác |
| POST | /bookings/:id/status/:status |
✅ | ✅ | ✅ → 🔒 | TARGET: STAFF 403 nếu booking không thuộc mình |
| GET | /bookings/:id/audit-log |
✅ | ✅ | ✅ → 🔒 | TARGET: STAFF chỉ xem audit booking của mình |
| POST | /bookings/:id/self-pick |
✅ | ✅ | ✅ → ✅ | Đã có logic: validate skill, chỉ pick được khi resourceId = NULL |
| GET | /availability |
✅ | ✅ | ✅ → ✅ | Staff cần check availability để walk-in/reschedule |
Change needed:
- Thêm
@Roles()tường minh choGET /bookings,GET /bookings/:id,POST /bookings,GET /availability. - Resource-scoping ở
BookingService— implement 6 method (list, findById, create, update, updateStatus, getAuditLog). - Unit test 3 case/method: own / others / unassigned.
2.5 Customer
| Method | Endpoint | ADMIN | OWNER | STAFF (C→T) | Ghi chú |
|---|---|---|---|---|---|
| GET | /customers |
✅ | ✅ | ✅ → ✅ | STAFF cần xem khách để chuẩn bị |
| GET | /customers/:id |
✅ | ✅ | ✅ → ✅ | |
| POST | /customers |
✅ | ✅ | ✅ → ✅ | Walk-in tạo khách mới |
| PATCH | /customers/:id |
✅ | ✅ | ✅ → ✅ | Sửa name/phone/email |
2.6 TenantCustomer (per-salon metrics)
| Method | Endpoint | ADMIN | OWNER | STAFF (C→T) | Scope rule |
|---|---|---|---|---|---|
| GET | /tenant-customers |
✅ | ✅ | ✅ → ✅ | |
| GET | /tenant-customers/:id |
✅ | ✅ | ✅ → ✅ | |
| PATCH | /tenant-customers/:id |
✅ | ✅ | ✅ → 🔒 | TARGET: STAFF sửa notes + tags OK; visitCount, totalSpent, firstVisit, lastVisit — chỉ OWNER+ADMIN |
Change needed: service-layer whitelist field cho STAFF.
2.7 Tax & Accounting
| Method | Endpoint | ADMIN | OWNER | STAFF (C→T) |
|---|---|---|---|---|
| * | /taxes/** |
✅ | ✅ | ❌ → ❌ |
| * | /accounting-accounts/** |
✅ | ✅ | ❌ → ❌ |
OWNER-only. STAFF không cần chạm tới accountant data.
2.8 Payment
| Method | Endpoint | ADMIN | OWNER | STAFF (C→T) | Ghi chú |
|---|---|---|---|---|---|
| GET | /admin/payments |
✅ | ✅ | ❌ → ❌ | List all — OWNER only (có thể revenue sensitive) |
| GET | /admin/payments/:id |
✅ | ✅ | ✅ → ✅ | Chi tiết 1 payment (STAFF cần xem khi check booking) |
| GET | /admin/payments/by-booking/:bookingId |
✅ | ✅ | ✅ → 🔒 | TARGET: STAFF chỉ xem payment của booking thuộc mình |
| POST | /admin/payments/:id/refund |
✅ | ✅ | ❌ → ❌ | Tiền bạc — OWNER |
| POST | /admin/payments/:id/void |
✅ | ✅ | ❌ → ❌ | OWNER |
| POST | /admin/payments/:id/capture |
✅ | ✅ | ❌ → ❌ | OWNER (có thể mở cho STAFF ở V2 nếu cần) |
| POST | /admin/payments/remaining |
✅ | ✅ | ✅ → 🔒 | TARGET: STAFF collect remaining cho booking của mình |
2.9 Payment config
| Method | Endpoint | ADMIN | OWNER | STAFF |
|---|---|---|---|---|
| * | /admin/payment-configs/** |
❌ → ❌ | ✅ → ✅ | ❌ → ❌ |
Note: hiện code là @Roles('OWNER') — ADMIN cũng đi qua vì hierarchy. TARGET: khai báo explicit @Roles('OWNER') là đủ, nhưng review lại có muốn ADMIN vào không. Đề xuất: ADMIN bị chặn (ngoại lệ hierarchy — payment credentials cực sensitive). Cần bổ sung logic deny ADMIN hoặc dùng decorator khác.
2.10 Loyalty
| Method | Endpoint | ADMIN | OWNER | STAFF (C→T) | Ghi chú |
|---|---|---|---|---|---|
| POST | /loyalty-cards |
✅ | ✅ | ❌ → ❌ | Setup program — OWNER |
| GET | /loyalty-cards |
✅ | ✅ | ✅ → ✅ | STAFF list cards |
| GET | /loyalty-cards/:id |
✅ | ✅ | ✅ → ✅ | |
| PATCH | /loyalty-cards/:id |
✅ | ✅ | ❌ → ❌ | |
| DELETE | /loyalty-cards/:id |
✅ | ✅ | ❌ → ❌ | |
| GET | /loyalty-cards/:id/stamps/:tenantCustomerId |
✅ | ✅ | ✅ → ✅ | Check stamps khách |
| POST | /loyalty-cards/:id/redeem |
✅ | ✅ | ✅ → ✅ | Staff apply redemption khi check-out |
| GET | /loyalty-cards/:id/points/:tenantCustomerId |
✅ | ✅ | ✅ → ✅ | |
| POST | /loyalty-cards/:id/redeem-points |
✅ | ✅ | ✅ → ✅ | |
| POST | /loyalty-cards/:id/adjust-points |
✅ | ✅ | ❌ → ❌ | Manual adjust — OWNER only |
| GET | /loyalty-cards/customer/:tenantCustomerId |
✅ | ✅ | ✅ → ✅ |
2.11 Upload
| Method | Endpoint | ADMIN | OWNER | STAFF (C→T) | Ghi chú |
|---|---|---|---|---|---|
| POST | /upload |
✅ | ✅ | ❌ → 🔒 | TARGET: STAFF upload avatar cho chính mình (validate prefix/owner) |
| DELETE | /upload |
✅ | ✅ | ❌ → 🔒 | STAFF xoá avatar của mình |
2.12 Auth (admin)
| Method | Endpoint | ADMIN | OWNER | STAFF | CUSTOMER | Ghi chú |
|---|---|---|---|---|---|---|
| POST | /auth/login |
✅ (Public) | ✅ | ✅ | ❌ | Admin login |
| POST | /auth/register |
✅ (Public) | ✅ | ✅ | ❌ | Owner register |
| POST | /auth/refresh |
✅ (Public) | ✅ | ✅ | ❌ | |
| POST | /auth/logout |
✅ (Public) | ✅ | ✅ | ❌ | |
| GET | /auth/me |
✅ | ✅ | ✅ | ❌ | |
| PATCH | /auth/profile |
✅ | ✅ | ✅ | ❌ | |
| POST | /auth/change-password |
✅ | ✅ | ✅ | ❌ | |
| POST | /auth/forgot-password |
✅ (Public) | ✅ | ✅ | ❌ | |
| POST | /auth/reset-password |
✅ (Public) | ✅ | ✅ | ❌ |
3. Ma trận — Public + Customer endpoints
3.1 Public booking (customer portal subdomain)
@Public() — không cần auth.
| Method | Endpoint | Ghi chú |
|---|---|---|
| GET | /public/tenants |
List tenants với slug (customer search) |
| GET | /public/tenants/:slug |
Salon profile |
| GET | /public/tenants/:slug/services |
Catalog |
| GET | /public/tenants/:slug/resources |
Staff list |
| GET | /public/tenants/:slug/availability |
Time slots |
| POST | /public/tenants/:slug/bookings |
Guest or authenticated booking |
| GET | /public/tenants/:slug/bookings/:id |
Booking confirmation / ticket |
| GET | /public/payments/:tenantId/status |
Payment status poll |
| GET | /public/tenants/:slug/bookings/:bookingId/payment |
Payment redirect data |
| POST / GET | /webhooks/payments/:provider/:tenantId |
Provider webhooks |
3.2 Customer portal (authenticated CUSTOMER)
@CustomerAuth() — JWT type: customer.
| Method | Endpoint | Ghi chú |
|---|---|---|
| POST | /auth/customer/google (Public) |
OAuth start |
| POST | /auth/customer/refresh (Public) |
|
| POST | /auth/customer/logout (Public) |
|
| GET | /auth/customer/me |
Customer profile |
| GET | /customer/me |
|
| PATCH | /customer/me/profile |
|
| GET | /customer/me/bookings |
My bookings |
| GET | /customer/me/loyalty |
My loyalty balance |
4. Implementation plan (Phase 2 của audit)
4.1 Changes — API layer
| File | Thay đổi | Test |
|---|---|---|
tenant.controller.ts GET :id |
Thêm @Roles('STAFF','OWNER','ADMIN') |
integration test |
resource.controller.ts GETs (4 endpoints) |
Thêm @Roles('STAFF','OWNER','ADMIN') |
integration |
resource.controller.ts PATCH :id |
@Roles('STAFF','OWNER','ADMIN') + scoping trong service |
unit |
resource.controller.ts POST/PATCH/DELETE time-offs |
@Roles('STAFF','OWNER','ADMIN') + scoping |
unit |
service.controller.ts GETs |
Thêm @Roles('STAFF','OWNER','ADMIN') |
integration |
service.controller.ts GET :id/resources, /service-categories* GETs |
Thêm @Roles() |
integration |
booking.controller.ts GET + POST + availability |
Thêm @Roles('STAFF','OWNER','ADMIN') |
integration |
booking.service.ts — 6 method |
Resource-scoping cho STAFF | unit (3 case/method) |
tenant-customer.service.ts PATCH |
Whitelist field cho STAFF | unit |
payment.controller.ts GET by-booking, remaining |
Scoping STAFF theo booking ownership | unit |
upload.controller.ts POST/DELETE |
@Roles('STAFF','OWNER','ADMIN') + resource ownership |
unit |
payment-config.controller.ts |
Review có deny ADMIN không (ngoại lệ hierarchy) | — |
4.2 Changes — Web layer (booking-web)
| File | Thay đổi |
|---|---|
app/admin/(dashboard)/layout.tsx |
allowedRoles={["ADMIN","OWNER","STAFF"]} |
layout/AppSidebar.tsx |
Filter menu theo role — STAFF ẩn: Staff management, Services CRUD, Tax, Payment config, Loyalty CRUD, Settings (trừ Profile) |
components/bookings/BookingsContent.tsx / Calendar |
STAFF mặc định filter resourceId = self + "Unassigned" tab |
components/bookings/BookingDrawer.tsx |
STAFF không thấy dropdown "Chọn staff khác" cho items |
components/staff/StaffContent.tsx |
STAFF không thấy (hide route) |
components/services/ServicesContent.tsx |
STAFF chỉ read-only, ẩn nút New/Edit/Delete |
Sidebar item Settings, Loyalty (CRUD) |
Ẩn cho STAFF |
4.3 Tests — E2E (Playwright)
Fixture loginAsStaff():
- Sidebar không hiện Settings/Tax/Payment config/Loyalty/Staff/Services CRUD.
-
GET /admin/bookingschỉ thấy booking của staff fixture + unassigned. - Booking của staff khác không xuất hiện trong list.
- Navigate trực tiếp tới booking của người khác → 403 hoặc redirect.
- STAFF tạo walk-in →
resourceIdauto bằng chính mình. - STAFF self-pick booking unassigned có skill → OK.
- STAFF self-pick booking unassigned không có skill → 403 + error message i18n.
- STAFF PATCH status booking của mình → OK.
- STAFF PATCH booking của người khác → 403.
- CUSTOMER JWT gọi bất kỳ admin endpoint nào → 401.
4.4 Tests — Unit (booking-api)
BookingService với mock performer STAFF:
-
findAllByTenantfilterresourceId IN [staffResourceId, null]. -
findById403 khi booking của người khác. -
createwalk-in auto-assign mình. -
update403 khi booking của người khác. -
updatecấm đổiresourceIdsang người khác. -
updateStatus403 khi booking của người khác. -
selfPick403 khi booking đã assigned cho người khác. -
selfPick403 khi không có skill matching.
TenantCustomerService:
- STAFF update notes/tags → OK.
- STAFF update visitCount/totalSpent → 403 hoặc silent drop (prefer 400
FORBIDDEN_FIELD).
4.5 Tests — Integration (booking-api supertest)
Mỗi endpoint mới mở cho STAFF: 1 test happy path (200) + 1 test CUSTOMER token bị reject (401).
4.6 Done criteria (recap)
- Matrix này đã review + quyết, mọi decision đã commit vào docs.
- Tất cả
@Roles()tường minh (không còn endpoint no-decorator implicit cho admin). - Service layer scoping cho 6 method booking + tenant-customer whitelist + time-off ownership.
- Web
/adminlogin được bằng STAFF + sidebar đúng + booking filter đúng. - 10+ E2E Playwright scenario pass.
- 10+ unit test scenario cho BookingService pass.
- Integration test cho các endpoint vừa mở/đổi quyền pass.
- OpenAPI regen +
booking-webtypes sync. - Update
docs/progress/features.md— chuyển "Role-based access control" sang ✅ Done. - Update
docs/progress/changelog.mdvới entry sprint này.
5. Câu hỏi mở — cần user decide
ADMIN có quyền vào endpoint cụ thể của tenant không?
- Hiện hierarchy cho ADMIN đi qua mọi
@Roles('OWNER'). Có vẻ đúng với "login as tenant" sau này. - Nhưng: payment-config (credentials), tenant delete, settings thay đổi → ADMIN tự ý sửa mà không log là rủi ro. Đề xuất: mọi thao tác ADMIN trên tenant PHẢI qua impersonate flow với audit log mandatory.
- Quyết: chấp nhận hierarchy cho đến khi có impersonate flow? (khuyến nghị Y).
- Hiện hierarchy cho ADMIN đi qua mọi
STAFF có quyền sửa metadata của chính mình (phone/email/avatar)?
- Đề xuất: CÓ — nhưng chỉ allow-list fields (name KHÔNG, jobTitle có? → discuss).
STAFF được xem doanh thu tổng hay chỉ của mình?
- Dashboard metrics: nếu STAFF thấy revenue today → có thể là info sensitive.
- Đề xuất: STAFF chỉ thấy metrics scope = self (booking today của mình, revenue của mình nếu commission có).
OWNER có phải quản lý nhiều location không?
- Hiện 1 tenant = 1 salon. Multi-location qua Organization layer là roadmap xa.
- Matrix này không cover OWNER multi-tenant.
ADMIN role có dùng trong thực tế ngay bây giờ không?
- Chưa có "login as tenant". ADMIN hiện chỉ dùng khi seed/debug.
- Đề xuất: ADMIN là "superadmin platform", chỉ dùng tools riêng, không lẫn vào tenant operation.
@Roles('OWNER')có nên viết thành@Roles('OWNER','ADMIN')để rõ ràng?- Hiện 2 cách đều làm same effect do hierarchy.
- Đề xuất: chuẩn hoá luôn viết rõ tất cả roles được phép → dễ audit, không phụ thuộc hierarchy.
6. Liên kết
docs/mobile/feature-plan.md §0— plan sprint role auditdocs/progress/features.md— section "Role-based Access Control — Audit & Test"booking-api/src/auth/guards/roles.guard.ts— guard logicbooking-api/src/auth/guards/jwt-auth.guard.ts— customer token rejectdocs/flows/booking-flow.md— settings enforcement