---
name: pattern-scoped-capability-endpoint-anti-silent-403
description: Capability GET endpoint returns {canFlag:bool, list:[...]} so FE gates the action button BE-side; prevents user clicking then hitting silent 403 (gotcha #44)
metadata:
  type: feedback
---

Khi action có authz fine-grained (handler enforce Admin-OR-dept), thêm 1 GET "capability" endpoint trả `{ CanReassign: bool, Staff: [...] }` (cờ + payload kèm). FE gate nút theo cờ BE-computed → user chỉ thấy/bấm được nút khi thực sự có quyền, tránh bấm rồi ăn 403 câm (gotcha #44 silent-403 UX).

**Why:** S54 ItTicket — handler đã throw `ForbiddenException` đúng, nhưng nếu FE không biết trước thì user vẫn thấy nút "Đổi người xử lý", bấm → 403 khó hiểu. BE là source-of-truth quyền; FE-guard chỉ là UX layer → BE phải cấp cờ để FE gate. Cờ + list trả CHUNG 1 call (1 round-trip): non-authorized vẫn trả `canReassign:false` + `Staff = Array.Empty<...>()` (không leak danh sách NV).

**How to apply:**
- Query record + 2 DTO: `AssignableStaffResult(bool CanReassign, IReadOnlyList<XDto> Staff)`. Compute `canReassign` y hệt logic handler enforce (single source — đừng để lệch giữa capability-check và enforce-check).
- Endpoint `[HttpGet("assignable-staff")]` thừa hưởng class `[Authorize]` (any-auth) — đúng ý đồ (mọi user gọi được, cờ tự false nếu không quyền).
- DTO contract phải khớp FE đang build song song. Camel-case JSON out: `{ canReassign, staff: [{ id, fullName }] }`. Báo NGAY parent nếu đổi field — FE wire theo contract này.
- `Staff` scope = chỉ NV dept IT đang `IsActive`, `OrderBy(FullName)` (UI dropdown ổn định).

Linked: [[pattern-controller-lower-authorize-handler-finegrained]].
