[CLAUDE] App+Api+FE-Admin: RolesPage CRUD (/system/roles)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m58s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m58s
User feedback: /system/roles trỏ tới placeholder "chưa được build" — build trang quản lý 12 role mặc định + custom role admin tự thêm. ## BE — PermissionFeatures.cs 3 command mới: - CreateRoleCommand — Name regex `^[A-Za-z][A-Za-z0-9_]*$` (chỉ chữ/số/ underscore, bắt đầu chữ), throw ConflictException nếu code đã tồn tại - UpdateRoleCommand — CHỈ update ShortName + Description. KHÔNG đổi Name (Identity FK trong UserRoles + WorkflowStepApprover.AssignmentValue + [Authorize(Roles="...")] attr — đổi = data corruption widespread) - DeleteRoleCommand — block 2 trường hợp: * Role thuộc AppRoles.All hardcoded (workflow guard reference) * Còn user assigned (UserManager.GetUsersInRoleAsync count > 0) ValidationException reference fully-qualified để tránh ambiguous với FluentValidation.ValidationException. ## BE — RolesController 3 endpoint mới (POST/PUT/DELETE) — Authorize Admin role. ## FE — RolesPage Table list 12 + custom roles với 5 column (Mã code / Mã viết tắt / Tên đầy đủ / Loại badge / Ngày tạo) + actions Edit/Delete: - Edit dialog: chỉ ShortName + Description editable, Name disabled với hint "Không đổi được sau khi tạo" - Delete: block với toast nếu role mặc định (HARDCODED_ROLES set check client-side trước khi gọi BE — UX faster, BE vẫn double-check) - Create dialog: 3 field Name (regex pattern HTML5) + ShortName + Description - Banner amber warning về Mã code FK constraint - Loại badge: Mặc định (slate) vs Tùy chỉnh (brand) ## FE — App.tsx + import RolesPage + route /system/roles → RolesPage. ## Build - BE: dotnet build pass (0 error) - fe-admin: tsc + vite pass (13.88s) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -14,4 +14,29 @@ public class RolesController(IMediator mediator) : ControllerBase
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<List<RoleDto>>> List(CancellationToken ct)
|
||||
=> Ok(await mediator.Send(new ListRolesQuery(), ct));
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Roles = "Admin")]
|
||||
public async Task<IActionResult> Create([FromBody] CreateRoleCommand body, CancellationToken ct)
|
||||
{
|
||||
var id = await mediator.Send(body, ct);
|
||||
return CreatedAtAction(nameof(List), new { id }, new { id });
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[Authorize(Roles = "Admin")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateRoleCommand body, CancellationToken ct)
|
||||
{
|
||||
if (id != body.Id) return BadRequest("Id mismatch");
|
||||
await mediator.Send(body, ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[Authorize(Roles = "Admin")]
|
||||
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
||||
{
|
||||
await mediator.Send(new DeleteRoleCommand(id), ct);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,3 +119,106 @@ public class ListRolesQueryHandler(RoleManager<Role> roleManager) : IRequestHand
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Create custom role ==========
|
||||
// Hardcoded roles (AppRoles.All) đã seed lúc startup. Endpoint này cho phép
|
||||
// admin tạo role tùy chỉnh ngoài 12 mặc định (vd Auditor, ITSupport, Reception,
|
||||
// ContractManager — phục vụ nhu cầu tổ chức cụ thể).
|
||||
public record CreateRoleCommand(string Name, string? ShortName, string? Description) : IRequest<Guid>;
|
||||
|
||||
public class CreateRoleCommandValidator : AbstractValidator<CreateRoleCommand>
|
||||
{
|
||||
public CreateRoleCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(100)
|
||||
.Matches("^[A-Za-z][A-Za-z0-9_]*$")
|
||||
.WithMessage("Mã role chỉ chứa chữ + số + underscore, bắt đầu bằng chữ.");
|
||||
RuleFor(x => x.ShortName).MaximumLength(50);
|
||||
RuleFor(x => x.Description).MaximumLength(500);
|
||||
}
|
||||
}
|
||||
|
||||
public class CreateRoleCommandHandler(RoleManager<Role> roleManager) : IRequestHandler<CreateRoleCommand, Guid>
|
||||
{
|
||||
public async Task<Guid> Handle(CreateRoleCommand req, CancellationToken ct)
|
||||
{
|
||||
if (await roleManager.RoleExistsAsync(req.Name))
|
||||
throw new ConflictException($"Role '{req.Name}' đã tồn tại.");
|
||||
|
||||
var role = new Role
|
||||
{
|
||||
Name = req.Name,
|
||||
ShortName = req.ShortName,
|
||||
Description = req.Description,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
};
|
||||
var result = await roleManager.CreateAsync(role);
|
||||
if (!result.Succeeded)
|
||||
throw new SolutionErp.Application.Common.Exceptions.ValidationException(result.Errors.Select(e =>
|
||||
new FluentValidation.Results.ValidationFailure("Role", e.Description)));
|
||||
return role.Id;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Update role labels ==========
|
||||
// CHỈ update ShortName + Description. Không update Name (Identity FK trong
|
||||
// UserRoles, Permission, WorkflowStepApprover.AssignmentValue, [Authorize]
|
||||
// attr — đổi tên = data corruption widespread).
|
||||
public record UpdateRoleCommand(Guid Id, string? ShortName, string? Description) : IRequest;
|
||||
|
||||
public class UpdateRoleCommandValidator : AbstractValidator<UpdateRoleCommand>
|
||||
{
|
||||
public UpdateRoleCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.ShortName).MaximumLength(50);
|
||||
RuleFor(x => x.Description).MaximumLength(500);
|
||||
}
|
||||
}
|
||||
|
||||
public class UpdateRoleCommandHandler(RoleManager<Role> roleManager) : IRequestHandler<UpdateRoleCommand>
|
||||
{
|
||||
public async Task Handle(UpdateRoleCommand req, CancellationToken ct)
|
||||
{
|
||||
var role = await roleManager.FindByIdAsync(req.Id.ToString())
|
||||
?? throw new NotFoundException("Role", req.Id);
|
||||
role.ShortName = req.ShortName;
|
||||
role.Description = req.Description;
|
||||
var result = await roleManager.UpdateAsync(role);
|
||||
if (!result.Succeeded)
|
||||
throw new SolutionErp.Application.Common.Exceptions.ValidationException(result.Errors.Select(e =>
|
||||
new FluentValidation.Results.ValidationFailure("Role", e.Description)));
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Delete custom role ==========
|
||||
// Block delete nếu:
|
||||
// - Role thuộc AppRoles.All (hardcoded — workflow guard reference)
|
||||
// - Còn user assigned (UserRoles FK)
|
||||
public record DeleteRoleCommand(Guid Id) : IRequest;
|
||||
|
||||
public class DeleteRoleCommandHandler(
|
||||
RoleManager<Role> roleManager,
|
||||
UserManager<User> userManager) : IRequestHandler<DeleteRoleCommand>
|
||||
{
|
||||
public async Task Handle(DeleteRoleCommand req, CancellationToken ct)
|
||||
{
|
||||
var role = await roleManager.FindByIdAsync(req.Id.ToString())
|
||||
?? throw new NotFoundException("Role", req.Id);
|
||||
|
||||
if (AppRoles.All.Contains(role.Name))
|
||||
throw new ConflictException(
|
||||
$"Không xóa được role '{role.Name}' — thuộc 12 role mặc định, " +
|
||||
"có tham chiếu trong workflow guard + [Authorize] attribute.");
|
||||
|
||||
// Check user assigned
|
||||
var users = await userManager.GetUsersInRoleAsync(role.Name!);
|
||||
if (users.Count > 0)
|
||||
throw new ConflictException(
|
||||
$"Role '{role.Name}' đang gán cho {users.Count} user — bỏ gán trước khi xóa.");
|
||||
|
||||
var result = await roleManager.DeleteAsync(role);
|
||||
if (!result.Succeeded)
|
||||
throw new SolutionErp.Application.Common.Exceptions.ValidationException(result.Errors.Select(e =>
|
||||
new FluentValidation.Results.ValidationFailure("Role", e.Description)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user