[CLAUDE] Office: IT staff tự reassign ticket — authz Admin-OR-IT + capability endpoint (S54)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m17s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m17s
- BE: GetAssignableItStaffQuery {canReassign,staff} capability endpoint + AssignItTicketHandler authz Admin-OR-dept-IT (Forbidden) + assignee-must-IT (Conflict); controller /assign hạ [Authorize(Roles=Admin)]→[Authorize] (handler enforce fine-grained data-driven)
- FE: fe-admin + fe-user ItTicketsPage SHA256-identical (reverse S53 divergence), nút gate by canReassign, dropdown từ /assignable-staff (không /users)
- Test: +13 authz guard (203→216 PASS), reviewer PASS (role-string Admin chain-verified real)
- No migration (DepartmentId reuse), no menu change
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -440,6 +440,40 @@ public class UpdateItTicketStatusHandler(IApplicationDbContext db, ICurrentUser
|
||||
}
|
||||
}
|
||||
|
||||
// S54: danh sách nhân viên tổ IT có thể nhận ticket + cờ canReassign (Admin HOẶC thành viên dept IT).
|
||||
// FE gate nút "Đổi người xử lý" theo CanReassign (BE-computed, tránh silent 403 gotcha #44).
|
||||
public record AssignableStaffDto(Guid Id, string FullName);
|
||||
public record AssignableStaffResult(bool CanReassign, IReadOnlyList<AssignableStaffDto> Staff);
|
||||
public record GetAssignableItStaffQuery : IRequest<AssignableStaffResult>;
|
||||
|
||||
public class GetAssignableItStaffHandler(IApplicationDbContext db, ICurrentUser cu)
|
||||
: IRequestHandler<GetAssignableItStaffQuery, AssignableStaffResult>
|
||||
{
|
||||
public async Task<AssignableStaffResult> Handle(GetAssignableItStaffQuery q, CancellationToken ct)
|
||||
{
|
||||
if (cu.UserId is null) throw new UnauthorizedException();
|
||||
var itDeptId = await db.Departments.AsNoTracking()
|
||||
.Where(d => d.Code == "IT" && !d.IsDeleted)
|
||||
.Select(d => (Guid?)d.Id)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
var isAdmin = cu.Roles.Contains("Admin");
|
||||
var myDeptId = await db.Users.AsNoTracking()
|
||||
.Where(u => u.Id == cu.UserId.Value)
|
||||
.Select(u => u.DepartmentId)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
var isItStaff = itDeptId is Guid d && myDeptId == d;
|
||||
var canReassign = isAdmin || isItStaff;
|
||||
if (!canReassign || itDeptId is not Guid deptId)
|
||||
return new AssignableStaffResult(canReassign, Array.Empty<AssignableStaffDto>());
|
||||
var staff = await db.Users.AsNoTracking()
|
||||
.Where(u => u.DepartmentId == deptId && u.IsActive)
|
||||
.OrderBy(u => u.FullName)
|
||||
.Select(u => new AssignableStaffDto(u.Id, u.FullName))
|
||||
.ToListAsync(ct);
|
||||
return new AssignableStaffResult(canReassign, staff);
|
||||
}
|
||||
}
|
||||
|
||||
// P11-D: admin re-assign ticket cho IT staff cụ thể (override round-robin auto-assign
|
||||
// lúc Create). Denorm AssignedToFullName từ User.FullName tại thời điểm gán.
|
||||
public record AssignItTicketCommand(Guid Id, Guid AssignedToUserId) : IRequest;
|
||||
@ -461,9 +495,24 @@ public class AssignItTicketHandler(IApplicationDbContext db, ICurrentUser cu, ID
|
||||
if (cu.UserId is null) throw new UnauthorizedException();
|
||||
var t = await db.ItTickets.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct);
|
||||
if (t is null) throw new NotFoundException("ItTicket", req.Id);
|
||||
|
||||
// S54: authz — chỉ Admin HOẶC thành viên tổ IT mới reassign (controller đã hạ [Authorize] any-auth).
|
||||
var itDeptId = await db.Departments.AsNoTracking()
|
||||
.Where(d => d.Code == "IT" && !d.IsDeleted)
|
||||
.Select(d => (Guid?)d.Id)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
var isAdmin = cu.Roles.Contains("Admin");
|
||||
var myDeptId = await db.Users.AsNoTracking()
|
||||
.Where(u => u.Id == cu.UserId.Value).Select(u => u.DepartmentId).FirstOrDefaultAsync(ct);
|
||||
if (!isAdmin && !(itDeptId is Guid mine && myDeptId == mine))
|
||||
throw new ForbiddenException("Chỉ Admin hoặc nhân viên tổ IT mới được gán lại ticket.");
|
||||
|
||||
var assignee = await db.Users.AsNoTracking()
|
||||
.FirstOrDefaultAsync(u => u.Id == req.AssignedToUserId && u.IsActive, ct);
|
||||
if (assignee is null) throw new NotFoundException("User", req.AssignedToUserId);
|
||||
// S54: assignee bắt buộc thuộc tổ IT (khớp dropdown scoped + round-robin).
|
||||
if (!(itDeptId is Guid itd && assignee.DepartmentId == itd))
|
||||
throw new ConflictException("Người được giao phải thuộc tổ IT.");
|
||||
t.AssignedToUserId = assignee.Id;
|
||||
t.AssignedToFullName = assignee.FullName;
|
||||
t.UpdatedAt = clock.UtcNow;
|
||||
|
||||
Reference in New Issue
Block a user