Farm Owner Assignment Design

Date: 2026-04-23
Status: Approved

Business Logic

  • Admin tạo trang trại (Area), sau đó giao cho Owner quản lý
  • Admin có thể tạo account Owner, Staff và assign cho trang trại/ao
  • Owner: giao theo trang trại (Area) → tự động có quyền truy cập tất cả ao trong trang trại đó
  • Staff: giao theo từng ao (Pond) riêng lẻ → giữ nguyên behavior hiện tại
  • Quan hệ Area ↔ Owner là many-to-many (1 trang trại có nhiều Owner, 1 Owner quản lý nhiều trang trại)

Current State

Đã có:

  • pond_users junction table — Staff assignment per pond
  • UserRole.assignedResources — pond IDs trong JWT cho RBAC
  • AuthorizationPolicyService.applyReadScopeToQueryBuilder — filter theo assignedResources

Thiếu:

  • Cơ chế gán Owner vào Area (trang trại)
  • Area-level access control cho Owner

Design

1. Database

Bảng mới area_users — junction table (many-to-many, giống pond_users):

CREATE TABLE area_users (
  area_id UUID NOT NULL REFERENCES areas(id) ON DELETE CASCADE,
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  PRIMARY KEY (area_id, user_id)
);

2. Area Entity

Thêm relation vào farm/entities/area.entity.ts:

@ManyToMany(() => User)
@JoinTable({
  name: 'area_users',
  joinColumn: { name: 'area_id', referencedColumnName: 'id' },
  inverseJoinColumn: { name: 'user_id', referencedColumnName: 'id' },
})
assignedOwners: User[];

3. JWT & Auth Token

Chiến lược: Khi tạo JWT (login/refresh), expand area assignments thành pond IDs và merge vào assignedResources. Đây là cách tiếp cận zero-change với AuthorizationPolicyService — mọi resource by-pond (cycle, device, daily-log…) tự hoạt động đúng mà không cần sửa gì thêm.

CurrentUserData — thêm field:

assignedAreaIds?: string[];  // để UI hiển thị và audit

auth.service.ts — khi tạo token:

// 1. Lấy area IDs của user
const assignedAreas = await this.areaRepository
  .createQueryBuilder('area')
  .innerJoin('area.assignedOwners', 'user', 'user.id = :userId', { userId: user.id })
  .select(['area.id'])
  .getMany();
const assignedAreaIds = assignedAreas.map(a => a.id);

// 2. Expand thành pond IDs
const pondsFromAreas = assignedAreaIds.length > 0
  ? await this.pondRepository.find({ where: { areaId: In(assignedAreaIds) }, select: ['id'] })
  : [];

// 3. Merge vào assignedResources (dedup)
assignedResources.push(...pondsFromAreas.map(p => p.id));
// assignedResources = [...new Set(assignedResources)] — dedup đã có sẵn

// 4. Thêm vào payload
payload.assignedAreaIds = assignedAreaIds;

jwt.strategy.ts — map thêm:

assignedAreaIds: (payload.assignedAreaIds as string[]) || [],

Trade-off: Khi Admin thêm ao mới vào trang trại, Owner phải re-login để có quyền ao mới. Đây là behavior nhất quán với toàn bộ assignedResources hiện tại.

4. API Endpoints

POST   /api/v1/areas/:id/assign-owner           # Gán Owner vào trang trại
DELETE /api/v1/areas/:id/assign-owner/:userId   # Gỡ Owner khỏi trang trại
GET    /api/v1/areas/:id/owners                 # Danh sách Owners của trang trại

Permission: area:update:all (Admin only) cho assign/unassign; area:read:all hoặc area:read:assigned cho list owners.

Business rules:

  • User được gán phải thuộc cùng organization
  • Gán trùng → 400 Bad Request
  • Gỡ user không được gán → 404 Not Found

Response GET /areas/:id/owners:

{
  "data": [
    { "id": "uuid", "fullName": "Nguyen Van A", "email": "a@example.com" }
  ]
}

5. DTO

File mới farm/dto/assign-area.dto.ts:

export class AssignOwnerDto {
  @ApiProperty({ example: 'uuid' })
  @IsUUID()
  @IsNotEmpty()
  userId: string;
}

6. Files thay đổi

File Thay đổi
farm/entities/area.entity.ts Thêm assignedOwners ManyToMany
auth/decorators/current-user.decorator.ts Thêm assignedAreaIds?: string[]
auth/auth.service.ts Query area_users + expand pond IDs khi tạo token (2 nơi: login + refresh)
auth/strategies/jwt.strategy.ts Map assignedAreaIds từ JWT payload
farm/areas.service.ts Thêm assignOwner, unassignOwner, getOwners
farm/controllers/areas.controller.ts Thêm 3 endpoints mới
farm/dto/assign-area.dto.ts DTO mới
farm/dto/index.ts Export DTO mới
migrations/TIMESTAMP-AddAreaUsers.ts Tạo bảng area_users

Không thay đổi: authorization-policy.service.ts, ponds.service.ts, user-role.service.ts, resource scope map.

Out of Scope

  • Frontend UI cho farm assignment (spec riêng)
  • Notification khi Owner được gán vào trang trại
  • Tự động gán Staff khi thêm ao mới (Staff vẫn giao thủ công từng ao)