architecture/role-matrix.md

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

  1. Explicit > implicit — mọi admin controller PHẢI khai báo @Roles() tường minh, không dựa hierarchy. Dễ audit.
  2. 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.
  3. STAFF write/mutate = scoped — chỉ được sửa tài nguyên mình đang đảm nhận (booking resourceId = staff.resource.id hoặc unassigned).
  4. OWNER full trên tenant mình — tạo/sửa/xoá staff, service, schedule, settings, payment config.
  5. 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ó).
  6. 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/:id cho 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 cho GET /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/bookings chỉ 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 → resourceId auto 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:

  • findAllByTenant filter resourceId IN [staffResourceId, null].
  • findById 403 khi booking của người khác.
  • create walk-in auto-assign mình.
  • update 403 khi booking của người khác.
  • update cấm đổi resourceId sang người khác.
  • updateStatus 403 khi booking của người khác.
  • selfPick 403 khi booking đã assigned cho người khác.
  • selfPick 403 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 /admin login đượ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-web types sync.
  • Update docs/progress/features.md — chuyển "Role-based access control" sang ✅ Done.
  • Update docs/progress/changelog.md với entry sprint này.

5. Câu hỏi mở — cần user decide

  1. 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).
  2. 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).
  3. 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ó).
  4. 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.
  5. 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.
  6. @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