RequestContext
Tổng quan
Tiện ích useRequestContext cung cấp quyền truy cập an toàn kiểu vào request context HTTP hiện tại, bao gồm người dùng đã xác thực, thông tin role, các helper phân quyền và các hàm định dạng phản hồi. Nó bọc useRequestContextInfra() của IGNIS Framework với các kiểu riêng của ứng dụng cùng các quy ước của BANA.
Nguồn: packages/core/src/utilities/request.utility.ts (89 dòng)
Import
import { useRequestContext } from '@nx/core';
// or
import { useRequestContext } from '@nx/core/utilities';Kiểu trả về
Hàm useRequestContext() trả về một object với các thuộc tính sau:
| Thuộc tính | Kiểu | Mô tả |
|---|---|---|
context | Context | Object request context Hono nguyên gốc |
currentUser | IJWTTokenPayload | Payload JWT đã giải mã đầy đủ |
userId | string | Lối tắt cho currentUser.userId |
roles | string[] | Mảng các chuỗi định danh role được trích xuất từ currentUser.roles |
isAlwaysAllowed | boolean | true nếu người dùng có role SUPER_ADMIN hoặc ADMIN |
useCountData | boolean | Có bọc phản hồi mảng theo định dạng { data, count } hay không |
normalizeCountableData | <T>(opts) => T[] | { data: T[], count: number } | Chuẩn hoá kết quả danh sách kèm header Content-Range |
formatResponse | <T>(result, statusCode?) => Response | Định dạng phản hồi cho một mục đơn lẻ |
formatArrayResponse | <T>(result, statusCode?) => Response | Định dạng phản hồi mảng |
Cách dùng cơ bản
import { useRequestContext } from '@nx/core';
@service()
export class ProductService {
async createProduct(data: CreateProductDto): Promise<TProduct> {
const { userId, roles, isAlwaysAllowed } = useRequestContext();
// Check authorization
if (!isAlwaysAllowed && !roles.includes('merchant-admin')) {
throw new ForbiddenError('Insufficient permissions');
}
// Use userId for audit trail
return this.productRepository.create({
data: {
...data,
createdBy: userId,
},
});
}
}Các thuộc tính của context
currentUser
Payload token JWT đã giải mã đầy đủ:
interface IJWTTokenPayload {
userId: string;
email?: string;
username?: string;
roles: Array<{
id: string;
name: string;
identifier: string;
}>;
organizerId?: string;
merchantId?: string;
iat: number; // Issued at
exp: number; // Expiration
}Cách dùng:
const { currentUser } = useRequestContext();
console.log(currentUser.userId); // "user-123"
console.log(currentUser.email); // "user@example.com"
console.log(currentUser.organizerId); // "org-456"
console.log(currentUser.roles);
// [{ id: "...", name: "Admin", identifier: "900_admin" }]userId
Lối tắt cho currentUser.userId:
const { userId } = useRequestContext();
await this.repository.updateById({
id: recordId,
data: { updatedBy: userId },
});roles
Mảng các chuỗi định danh role, được trích xuất qua currentUser.roles.map(r => r.identifier):
const { roles } = useRequestContext();
// ["999_super-admin", "500_organizer-owner"]
if (roles.includes('500_organizer-owner')) {
// Allow organizer-specific actions
}isAlwaysAllowed
Kiểm tra xem người dùng có role được phép luôn hay không. Được tính bằng cách lấy giao của binding AuthorizeBindingKeys.ALWAYS_ALLOW_ROLES của application (cấu hình theo từng app - thường là SUPER_ADMIN / ADMIN) với các định danh role của người dùng:
// Implementation detail:
const allowedRoles = getApplication().get<string[]>({
key: AuthorizeBindingKeys.ALWAYS_ALLOW_ROLES,
});
const isAlwaysAllowed = intersection(allowedRoles, roles).length > 0;
// vd: allowedRoles = ['999_super-admin', '900_admin']Cách dùng:
const { isAlwaysAllowed, roles } = useRequestContext();
// Skip detailed checks for admins
if (isAlwaysAllowed) {
return this.performAction();
}
// Otherwise, check specific permissions
if (!roles.includes('editor')) {
throw new ForbiddenError();
}useCountData
Một giá trị boolean được suy ra từ header HTTP X-Request-Count-Data (mặc định là true). Nó kiểm soát việc phản hồi mảng được bọc trong { data, count } hay trả về dưới dạng mảng thuần:
const { useCountData } = useRequestContext();
// true => responses are { data: [...], count: N }
// false => responses are plain [...]Client có thể tắt việc bọc bằng cách gửi:
X-Request-Count-Data: falsenormalizeCountableData
Chuẩn hoá kết quả truy vấn danh sách. Nó thiết lập các header phản hồi Content-Range, X-Response-Format và X-Response-Count-Data, sau đó trả về { data, count } hoặc một mảng thuần tuỳ theo useCountData:
const { normalizeCountableData } = useRequestContext();
const result = await this.repository.find({ where, limit, offset });
const total = await this.repository.count({ where });
return normalizeCountableData({
data: result,
range: { start: offset, end: offset + result.length - 1, total },
});
// If useCountData=true: { data: [...], count: N }
// If useCountData=false: [...]
// Headers set: Content-Range: records 0-9/100formatResponse và formatArrayResponse
Các phương thức tiện lợi gọi context.json() với hình dạng phù hợp dựa trên useCountData:
const { formatResponse, formatArrayResponse } = useRequestContext();
// Single item
return formatResponse({ data: product, count: 1 });
// If useCountData=true: json({ data: product, count: 1 })
// If useCountData=false: json(product)
// Array
return formatArrayResponse({ data: products, count: products.length });
// If useCountData=true: json({ data: products, count: N })
// If useCountData=false: json(products)
// Custom status code
return formatResponse({ data: created, count: 1 }, 201);Luồng request context
Các mẫu phân quyền
Kiểm soát truy cập dựa trên role
@service()
export class MerchantService {
async updateMerchant(id: string, data: UpdateMerchantDto): Promise<TMerchant> {
const { isAlwaysAllowed, roles, currentUser } = useRequestContext();
// Super admins can update any merchant
if (isAlwaysAllowed) {
return this.merchantRepository.updateById({ id, data });
}
// Organizer owners can update their own merchants
if (roles.includes('500_organizer-owner')) {
const merchant = await this.merchantRepository.findById({ id });
if (merchant.organizerId !== currentUser.organizerId) {
throw new ForbiddenError('Cannot update merchant from another organization');
}
return this.merchantRepository.updateById({ id, data });
}
throw new ForbiddenError('Insufficient permissions');
}
}Kiểm tra quyền sở hữu resource
@service()
export class SaleOrderService {
async getOrderDetails(orderId: string): Promise<TSaleOrder> {
const { userId, isAlwaysAllowed } = useRequestContext();
const order = await this.saleOrderRepository.findById({ id: orderId });
// Admins can see all orders
if (isAlwaysAllowed) {
return order;
}
// Regular users can only see their own orders
if (order.customerId !== userId) {
throw new ForbiddenError('Cannot access order from another user');
}
return order;
}
}Tham chiếu Fixed User Roles
// Mở rộng từ AuthorizationRoles của IGNIS (SUPER_ADMIN 999, ADMIN 900, GUEST 1).
// Mỗi role là một AuthorizationRole; identifier = `<priority pad 3 chữ số>_<name>`.
export class AppFixedRoles extends AuthorizationRoles {
static readonly OPERATOR = AuthorizationRole.build({ name: 'operator', priority: 600 }); // 600_operator
static readonly OWNER = AuthorizationRole.build({ name: 'organizer-owner', priority: 500 }); // 500_organizer-owner
static readonly CASHIER = AuthorizationRole.build({ name: 'cashier', priority: 110 }); // 110_cashier
static readonly EMPLOYEE = AuthorizationRole.build({ name: 'employee', priority: 100 }); // 100_employee
static readonly CUSTOMER = AuthorizationRole.build({ name: 'customer', priority: 10 }); // 010_customer
static readonly SYSTEM_ROLE_IDENTIFIERS = new Set([
this.SUPER_ADMIN.identifier, // 999_super-admin
this.ADMIN.identifier, // 900_admin
this.OPERATOR.identifier, // 600_operator
]);
static isSystemUser(roles?: { identifier: string }[]): boolean { /* ... */ }
static isOrganizerOwner(roles?: { identifier: string }[]): boolean { /* ... */ }
}
// Lưu ý: `isAlwaysAllowed` KHÔNG dẫn xuất từ class này - nó lấy giao của binding
// `AuthorizeBindingKeys.ALWAYS_ALLOW_ROLES` (theo từng app) với các role của user.Các mẫu nhật ký kiểm toán
Tạo kèm kiểm toán
async createProduct(data: CreateProductDto): Promise<TProduct> {
const { userId } = useRequestContext();
return this.productRepository.create({
data: {
id: IdGenerator.getInstance().nextId(),
...data,
createdBy: userId,
createdAt: new Date(),
},
});
}Cập nhật kèm kiểm toán
async updateProduct(id: string, data: UpdateProductDto): Promise<TProduct> {
const { userId } = useRequestContext();
return this.productRepository.updateById({
id,
data: {
...data,
updatedBy: userId,
updatedAt: new Date(),
},
});
}Xoá mềm kèm kiểm toán
async deleteProduct(id: string): Promise<void> {
const { userId } = useRequestContext();
await this.productRepository.updateById({
id,
data: {
deletedBy: userId,
deletedAt: new Date(),
},
});
}Xử lý lỗi
Thiếu context
Nếu useRequestContext() được gọi bên ngoài vòng đời của một request HTTP (ví dụ: trong một background job hoặc khi khởi động ứng dụng), nó sẽ ném lỗi:
try {
const { userId } = useRequestContext();
} catch (error) {
// [useRequestContext] Request context is undefined.
}Phương án dự phòng an toàn cho background job
function safeGetContext() {
try {
return useRequestContext();
} catch {
// In non-request context (e.g., background job, event handler)
return {
userId: 'system',
roles: ['system'],
isAlwaysAllowed: true,
currentUser: null,
context: null,
};
}
}Tích hợp với Controller
Hàm useRequestContext() dựa vào việc middleware xác thực đã điền CURRENT_USER vào context Hono. Luôn dùng @authenticate trên các route của controller:
@controller({ basePath: '/products' })
export class ProductController {
constructor(private productService: ProductService) {}
@post('/')
@authenticate(['jwt'])
async create(@body() data: CreateProductDto): Promise<TProduct> {
// useRequestContext() is safe to call in the service layer
return this.productService.createProduct(data);
}
@get('/:id')
@authenticate(['jwt', 'basic'])
async getById(@param('id') id: string): Promise<TProduct> {
return this.productService.getProduct(id);
}
}Kiểm thử
Mock request context
import { useRequestContext } from '@nx/core/utilities';
jest.mock('@nx/core/utilities', () => ({
useRequestContext: jest.fn(),
}));
describe('ProductService', () => {
beforeEach(() => {
(useRequestContext as jest.Mock).mockReturnValue({
userId: 'test-user-123',
roles: ['500_organizer-owner'],
isAlwaysAllowed: false,
useCountData: true,
currentUser: {
userId: 'test-user-123',
organizerId: 'test-org-456',
roles: [{ identifier: '500_organizer-owner' }],
},
});
});
it('creates product with audit trail', async () => {
const result = await productService.createProduct({ name: 'Test' });
expect(result.createdBy).toBe('test-user-123');
});
});Thực hành tốt nhất
1. Kiểm tra phân quyền sớm
// Correct -- check at start of method
async updateProduct(id: string, data: UpdateProductDto) {
const { isAlwaysAllowed, roles } = useRequestContext();
if (!isAlwaysAllowed && !roles.includes('editor')) {
throw new ForbiddenError();
}
// Then proceed with business logic
}2. Dùng isAlwaysAllowed để bỏ qua kiểm tra cho admin
// Correct -- clean admin bypass
if (isAlwaysAllowed) {
return this.performAction();
}
// Avoid -- checking individual admin roles
if (roles.includes('999_super-admin') || roles.includes('900_admin')) {
// ...
}3. Đưa context vào log
async processOrder(orderId: string) {
const { userId } = useRequestContext();
this.logger.info('Processing order | orderId: %s | userId: %s', orderId, userId);
}