{{-- Row actions for the Users DataTable — docs/wireframes-v12/user-management.html:
v12 shows view + edit. **Two controls are added**: the status toggle (decision D1 — the client asked for activate/deactivate, and the edit footer's own button is not reachable from a listing) and Delete (decision D7). Both wear the same `.btn-icon` with the wireframe's own `.text-danger` / `.text-success` utilities, which is the colouring v12 gives an icon button elsewhere — the same addition, for the same reason and in the same markup, that brokers/partials/actions.blade.php records. Rendered by UsersDataTable through rawColumns(['actions']), which means this output is injected into the page unescaped. Nothing user-controlled may reach it unescaped: the user's name only appears inside Js::from(), which emits it as a JSON string literal with < > & " escaped to \u sequences. `{{ }}` escapes for HTML and not for Javascript, and commit 3492ec1 is the stored XSS that proved the difference. ## Two gates per control, not one Every control is gated on its own ability, so a holder of `users.view` alone gets the eye and nothing else — and the two destructive ones are gated a SECOND time, on the target (guard §4.6): `isDestructibleBy()` is false for the actor's own row and false for a Super Admin's row unless the actor is one too. The Edit pencil uses the weaker of the pair, `isManageableBy()`, because a user may edit their own profile — they simply cannot change their own role (UpdateUserRequest enforces that half). Hiding is only the visible half. UserController answers 403 on every one of these paths for the same targets, and the predicates are the same two methods — so a hand-rolled POST is refused by the same rule that removed the button. Bare permission gates and not a policy — CR-13 §0.2. Both write actions are real POST forms with confirm() guards, never links: a GET that changes state would be triggered by any crawler or prefetch. The delete is a SOFT one and there is no in-use guard — a deleted user's name survives on everything they touched (CR-13 §2.4) — which is why the confirmation says so rather than warning about data that is not going anywhere. The roles relation is eager-loaded by UsersDataTable::query(), so the two guard calls below cost no query per row. yajra hands the row's model in as $model. --}} @php /** @var \App\Models\User $model */ $actor = auth()->user(); $actor = $actor instanceof \App\Models\User ? $actor : null; $manageable = $model->isManageableBy($actor); $destructible = $model->isDestructibleBy($actor); $activating = $model->status === \App\Enums\UserStatus::Inactive; @endphp