Skip to content

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

typescript
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ínhKiểuMô tả
contextContextObject request context Hono nguyên gốc
currentUserIJWTTokenPayloadPayload JWT đã giải mã đầy đủ
userIdstringLối tắt cho currentUser.userId
rolesstring[]Mảng các chuỗi định danh role được trích xuất từ currentUser.roles
isAlwaysAllowedbooleantrue nếu người dùng có role SUPER_ADMIN hoặc ADMIN
useCountDatabooleanCó 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

typescript
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 đủ:

typescript
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:

typescript
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:

typescript
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):

typescript
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:

typescript
// 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:

typescript
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:

typescript
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: false

normalizeCountableData

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-FormatX-Response-Count-Data, sau đó trả về { data, count } hoặc một mảng thuần tuỳ theo useCountData:

typescript
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/100

formatResponse 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:

typescript
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

typescript
@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

typescript
@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

typescript
// 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

typescript
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

typescript
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

typescript
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:

typescript
try {
  const { userId } = useRequestContext();
} catch (error) {
  // [useRequestContext] Request context is undefined.
}

Phương án dự phòng an toàn cho background job

typescript
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:

typescript
@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

typescript
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

typescript
// 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

typescript
// 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

typescript
async processOrder(orderId: string) {
  const { userId } = useRequestContext();
  this.logger.info('Processing order | orderId: %s | userId: %s', orderId, userId);
}

Tài liệu liên quan

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.