Skip to content

Chặn theo quyền

Ba điểm chặn, khai báo độc lập nhau. Một màn hình thường dùng cả ba: trang cần quyền xem, mục menu ẩn theo cùng quyền đó, và nút hành động theo quyền sửa/xoá.

1. Truy cập trang

ts
// Trang công khai — bỏ qua cả kiểm tra đăng nhập
definePageMeta({ auth: false })

// Cần đăng nhập (mặc định, không cần khai)
definePageMeta({ auth: true })

// Cần một quyền
definePageMeta({ permission: '/orders/search' })

// Cần TẤT CẢ các quyền
definePageMeta({ permissions: ['/orders/search', '/orders/export'] })

Hai middleware chạy theo thứ tự:

  1. auth.global — chưa đăng nhập và route không khai auth: false → chuyển tới /auth/login?redirect=<đường dẫn hiện tại>.
  2. permission.global — thiếu quyền → ném lỗi 403, người dùng thấy trang lỗi của app.

Cần "chỉ cần một trong các quyền" thì middleware chung không làm được (nó luôn yêu cầu tất cả); tự kiểm trong trang:

ts
const { canAny } = usePermission()
if (!canAny(['/orders/search', '/orders/view-own'])) {
  throw createError({ statusCode: 403, statusMessage: 'Bạn không có quyền xem đơn hàng.' })
}

2. Mục menu

Khai permission trên NavItem; layout tự lọc trước khi hiển thị:

ts
// app/app.config.ts
export default defineAppConfig({
  tasco: {
    nav: [
      { label: 'Đơn hàng', path: '/orders', permission: '/orders/search' },
      {
        label: 'Quản trị',
        permission: '/admin/access',
        children: [
          { label: 'Người dùng', path: '/admin/users', permission: '/admin/users/search' },
        ],
      },
    ],
  },
})

Lọc theo chiều sâu: nhóm cha bị ẩn thì cả nhánh biến mất; nhóm cha không khai permission vẫn hiện với những mục con mà người dùng được phép thấy.

Danh sách link ở nơi khác (tab, thẻ điều hướng nhanh) lọc bằng cùng một hàm:

ts
const { filterByPermission } = usePermission()
const tabs = computed(() => filterByPermission(ALL_TABS))

3. Nút và phần tử trong trang

Directive v-can

vue
<!-- Không có quyền → ẩn hẳn -->
<CButton v-can="'/orders/create'" variant="primary">Tạo đơn</CButton>

<!-- Không có quyền → vẫn hiện nhưng khoá lại -->
<CButton v-can:disable="'/orders/delete'" danger>Xoá</CButton>

<!-- Cần TẤT CẢ các quyền -->
<CButton v-can="['/orders/update', '/orders/approve']">Duyệt</CButton>

v-can:disable gắn thuộc tính disabled, thêm class is-disabled, chặn con trỏ và giảm độ mờ — dùng khi muốn người dùng biết chức năng tồn tại nhưng chưa được cấp quyền.

Component <Can>

Dùng khi cần bọc nhiều phần tử, cần "chỉ cần một quyền", hoặc cần nội dung thay thế:

vue
<Can permission="/orders/update">
  <CButton @click="edit">Sửa</CButton>
  <CButton danger @click="remove">Xoá</CButton>
</Can>

<Can :permissions="['/orders/approve', '/orders/reject']" any>
  <CCard title="Phê duyệt">…</CCard>
  <template #fallback>
    <CEmpty description="Bạn không có quyền phê duyệt đơn này." />
  </template>
</Can>
v-can<Can>
Ẩn / khoáCả hai (v-canv-can:disable)Chỉ ẩn
"Chỉ cần một quyền"KhôngCó, qua any
Nội dung thay thếKhôngCó, qua slot #fallback
Áp cho một phần tửBọc được nhiều phần tử

Kiểm tra trong script

ts
const { can } = usePermission()

async function remove(id: string) {
  if (!can('/orders/delete')) return
  // ...
}

Chọn cách nào

Tình huốngCách
Cả trang không được xemdefinePageMeta({ permissions })
Mục menu không nên hiệnpermission trong NavItem
Nút hành động không được phépv-can (ẩn) hoặc v-can:disable (khoá)
Cả khối nội dung, cần lời giải thích<Can> + #fallback
Quyết định trong logicusePermission().can()

Nguyên tắc: ẩn thứ người dùng không bao giờ được dùng, khoá thứ họ có thể được cấp quyền sau này. Đừng để nút hiện bình thường rồi mới báo lỗi sau khi bấm.

Bẫy thường gặp

  • Khai quyền ở trang nhưng quên ở menu — người dùng thấy mục menu, bấm vào thì gặp 403.
  • Gõ sai URI: quyền so khớp chính xác, /orders/searchs chỉ đơn giản là không có ai có.
  • Trang đăng nhập quên auth: false — middleware đẩy vòng lại chính nó.
  • Tin rằng ẩn nút là đã an toàn — backend vẫn phải kiểm tra.

Liên quan