[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:
@ -10,6 +10,7 @@ import { ProjectsPage } from '@/pages/master/ProjectsPage'
|
|||||||
import { DepartmentsPage } from '@/pages/master/DepartmentsPage'
|
import { DepartmentsPage } from '@/pages/master/DepartmentsPage'
|
||||||
import { CatalogsPage } from '@/pages/master/CatalogsPage'
|
import { CatalogsPage } from '@/pages/master/CatalogsPage'
|
||||||
import { PermissionsPage } from '@/pages/system/PermissionsPage'
|
import { PermissionsPage } from '@/pages/system/PermissionsPage'
|
||||||
|
import { RolesPage } from '@/pages/system/RolesPage'
|
||||||
import { WorkflowsPage } from '@/pages/system/WorkflowsPage'
|
import { WorkflowsPage } from '@/pages/system/WorkflowsPage'
|
||||||
import { FormsPage } from '@/pages/forms/FormsPage'
|
import { FormsPage } from '@/pages/forms/FormsPage'
|
||||||
import { ContractsListPage } from '@/pages/contracts/ContractsListPage'
|
import { ContractsListPage } from '@/pages/contracts/ContractsListPage'
|
||||||
@ -38,6 +39,7 @@ function App() {
|
|||||||
<Route path="/master/catalogs" element={<Navigate to="/master/catalogs/units" replace />} />
|
<Route path="/master/catalogs" element={<Navigate to="/master/catalogs/units" replace />} />
|
||||||
<Route path="/master/catalogs/:kind" element={<CatalogsPage />} />
|
<Route path="/master/catalogs/:kind" element={<CatalogsPage />} />
|
||||||
<Route path="/system/users" element={<UsersPage />} />
|
<Route path="/system/users" element={<UsersPage />} />
|
||||||
|
<Route path="/system/roles" element={<RolesPage />} />
|
||||||
<Route path="/system/permissions" element={<PermissionsPage />} />
|
<Route path="/system/permissions" element={<PermissionsPage />} />
|
||||||
<Route path="/system/workflows" element={<WorkflowsPage />} />
|
<Route path="/system/workflows" element={<WorkflowsPage />} />
|
||||||
<Route path="/system/workflows/:typeCode" element={<WorkflowsPage />} />
|
<Route path="/system/workflows/:typeCode" element={<WorkflowsPage />} />
|
||||||
|
|||||||
268
fe-admin/src/pages/system/RolesPage.tsx
Normal file
268
fe-admin/src/pages/system/RolesPage.tsx
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
// Quản lý 12 role mặc định + custom role admin tự thêm. Edit chỉ ShortName +
|
||||||
|
// Description (Mã = Identity Name là FK + [Authorize] attr — không cho đổi).
|
||||||
|
// Delete chỉ cho custom role chưa có user assigned (BE block 12 hardcoded).
|
||||||
|
import { useState, type FormEvent } from 'react'
|
||||||
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { Pencil, Plus, Shield, Trash2, AlertCircle } from 'lucide-react'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
import { PageHeader } from '@/components/PageHeader'
|
||||||
|
import { Button } from '@/components/ui/Button'
|
||||||
|
import { Input } from '@/components/ui/Input'
|
||||||
|
import { Label } from '@/components/ui/Label'
|
||||||
|
import { Textarea } from '@/components/ui/Textarea'
|
||||||
|
import { Dialog } from '@/components/ui/Dialog'
|
||||||
|
import { api } from '@/lib/api'
|
||||||
|
import { getErrorMessage } from '@/lib/apiError'
|
||||||
|
import { AVAILABLE_ROLES } from '@/types/users'
|
||||||
|
import type { Role } from '@/types/menu'
|
||||||
|
|
||||||
|
const HARDCODED_ROLES = new Set<string>(AVAILABLE_ROLES)
|
||||||
|
|
||||||
|
const fmtDate = (s: string) => new Date(s).toLocaleDateString('vi-VN')
|
||||||
|
|
||||||
|
export function RolesPage() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
|
||||||
|
const [createOpen, setCreateOpen] = useState(false)
|
||||||
|
const [createForm, setCreateForm] = useState({ name: '', shortName: '', description: '' })
|
||||||
|
|
||||||
|
const [editTarget, setEditTarget] = useState<Role | null>(null)
|
||||||
|
const [editForm, setEditForm] = useState({ shortName: '', description: '' })
|
||||||
|
|
||||||
|
const list = useQuery({
|
||||||
|
queryKey: ['roles'],
|
||||||
|
queryFn: async () => (await api.get<Role[]>('/roles')).data,
|
||||||
|
})
|
||||||
|
|
||||||
|
const createMut = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
await api.post('/roles', {
|
||||||
|
name: createForm.name.trim(),
|
||||||
|
shortName: createForm.shortName.trim() || null,
|
||||||
|
description: createForm.description.trim() || null,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
qc.invalidateQueries({ queryKey: ['roles'] })
|
||||||
|
toast.success('Đã tạo role')
|
||||||
|
setCreateOpen(false)
|
||||||
|
setCreateForm({ name: '', shortName: '', description: '' })
|
||||||
|
},
|
||||||
|
onError: err => toast.error(getErrorMessage(err)),
|
||||||
|
})
|
||||||
|
|
||||||
|
const editMut = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
if (!editTarget) return
|
||||||
|
await api.put(`/roles/${editTarget.id}`, {
|
||||||
|
id: editTarget.id,
|
||||||
|
shortName: editForm.shortName.trim() || null,
|
||||||
|
description: editForm.description.trim() || null,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
qc.invalidateQueries({ queryKey: ['roles'] })
|
||||||
|
toast.success('Đã lưu')
|
||||||
|
setEditTarget(null)
|
||||||
|
},
|
||||||
|
onError: err => toast.error(getErrorMessage(err)),
|
||||||
|
})
|
||||||
|
|
||||||
|
const deleteMut = useMutation({
|
||||||
|
mutationFn: async (id: string) => { await api.delete(`/roles/${id}`) },
|
||||||
|
onSuccess: () => {
|
||||||
|
qc.invalidateQueries({ queryKey: ['roles'] })
|
||||||
|
toast.success('Đã xóa role')
|
||||||
|
},
|
||||||
|
onError: err => toast.error(getErrorMessage(err)),
|
||||||
|
})
|
||||||
|
|
||||||
|
function openEdit(r: Role) {
|
||||||
|
setEditTarget(r)
|
||||||
|
setEditForm({ shortName: r.shortName ?? '', description: r.description ?? '' })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<PageHeader
|
||||||
|
title={
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<Shield className="h-5 w-5" />
|
||||||
|
Vai trò (Roles)
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
description="12 role mặc định seed lúc startup + custom role admin thêm. Chỉ sửa Mã viết tắt + Tên đầy đủ; không đổi Mã code (FK)."
|
||||||
|
actions={
|
||||||
|
<Button onClick={() => setCreateOpen(true)}>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Thêm role tùy chỉnh
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mb-3 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-700">
|
||||||
|
<AlertCircle className="mr-1 inline h-3.5 w-3.5" />
|
||||||
|
Mã code (Name) là khóa kỹ thuật — KHÔNG đổi sau khi tạo (tham chiếu UserRoles + WorkflowStepApprover + [Authorize]).
|
||||||
|
Chỉ Mã viết tắt + Tên đầy đủ tiếng Việt được sửa.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto rounded-lg border border-slate-200 bg-white">
|
||||||
|
<table className="min-w-full text-sm">
|
||||||
|
<thead className="bg-slate-50 text-[11px] uppercase tracking-wider text-slate-500">
|
||||||
|
<tr>
|
||||||
|
<th className="px-3 py-2 text-left">Mã code</th>
|
||||||
|
<th className="px-3 py-2 text-left">Mã viết tắt</th>
|
||||||
|
<th className="px-3 py-2 text-left">Tên đầy đủ</th>
|
||||||
|
<th className="px-3 py-2 text-left">Loại</th>
|
||||||
|
<th className="w-28 px-3 py-2 text-left">Ngày tạo</th>
|
||||||
|
<th className="w-24 px-3 py-2"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-slate-100">
|
||||||
|
{list.isLoading && <tr><td colSpan={6} className="p-6 text-center text-slate-400">Đang tải…</td></tr>}
|
||||||
|
{!list.isLoading && (list.data?.length ?? 0) === 0 && (
|
||||||
|
<tr><td colSpan={6} className="p-6 text-center text-slate-400">Không có role.</td></tr>
|
||||||
|
)}
|
||||||
|
{list.data?.map(r => {
|
||||||
|
const isSystem = HARDCODED_ROLES.has(r.name)
|
||||||
|
return (
|
||||||
|
<tr key={r.id} className="hover:bg-slate-50">
|
||||||
|
<td className="px-3 py-2 font-mono text-xs">{r.name}</td>
|
||||||
|
<td className="px-3 py-2 font-semibold text-brand-700">{r.shortName ?? '—'}</td>
|
||||||
|
<td className="px-3 py-2">{r.description ?? <span className="text-slate-400">—</span>}</td>
|
||||||
|
<td className="px-3 py-2">
|
||||||
|
{isSystem ? (
|
||||||
|
<span className="rounded-full bg-slate-100 px-2 py-0.5 text-[10px] text-slate-600">Mặc định</span>
|
||||||
|
) : (
|
||||||
|
<span className="rounded-full bg-brand-50 px-2 py-0.5 text-[10px] text-brand-700">Tùy chỉnh</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 text-xs text-slate-500">{fmtDate(r.createdAt)}</td>
|
||||||
|
<td className="px-3 py-2">
|
||||||
|
<div className="flex justify-end gap-1">
|
||||||
|
<button
|
||||||
|
onClick={() => openEdit(r)}
|
||||||
|
title="Sửa Mã viết tắt + Tên đầy đủ"
|
||||||
|
className="rounded p-1 text-slate-500 hover:bg-slate-100 hover:text-brand-600"
|
||||||
|
>
|
||||||
|
<Pencil className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (isSystem) {
|
||||||
|
toast.error('Không xóa được role mặc định')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (confirm(`Xóa role "${r.name}"?`)) deleteMut.mutate(r.id)
|
||||||
|
}}
|
||||||
|
title={isSystem ? 'Role mặc định — không xóa được' : 'Xóa role'}
|
||||||
|
disabled={isSystem || deleteMut.isPending}
|
||||||
|
className={`rounded p-1 transition ${
|
||||||
|
isSystem
|
||||||
|
? 'cursor-not-allowed text-slate-300'
|
||||||
|
: 'text-slate-500 hover:bg-slate-100 hover:text-red-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Create custom role */}
|
||||||
|
<Dialog
|
||||||
|
open={createOpen}
|
||||||
|
onClose={() => setCreateOpen(false)}
|
||||||
|
title="Thêm role tùy chỉnh"
|
||||||
|
size="md"
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button variant="outline" onClick={() => setCreateOpen(false)}>Hủy</Button>
|
||||||
|
<Button onClick={(e: FormEvent) => { e.preventDefault(); createMut.mutate() }} disabled={createMut.isPending}>
|
||||||
|
{createMut.isPending ? 'Đang tạo…' : 'Tạo'}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<form className="space-y-3" onSubmit={e => { e.preventDefault(); createMut.mutate() }}>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label>Mã code (English, không đổi sau tạo) *</Label>
|
||||||
|
<Input
|
||||||
|
value={createForm.name}
|
||||||
|
onChange={e => setCreateForm(f => ({ ...f, name: e.target.value }))}
|
||||||
|
placeholder="Auditor, ITSupport, Reception..."
|
||||||
|
required
|
||||||
|
pattern="^[A-Za-z][A-Za-z0-9_]*$"
|
||||||
|
/>
|
||||||
|
<div className="text-xs text-slate-500">
|
||||||
|
Chỉ chữ + số + underscore, bắt đầu bằng chữ. Dùng cho [Authorize(Roles="...")] + workflow guard.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label>Mã viết tắt (Vietnamese)</Label>
|
||||||
|
<Input
|
||||||
|
value={createForm.shortName}
|
||||||
|
onChange={e => setCreateForm(f => ({ ...f, shortName: e.target.value }))}
|
||||||
|
placeholder="vd: KSV, IT, LT..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label>Tên đầy đủ tiếng Việt</Label>
|
||||||
|
<Textarea
|
||||||
|
rows={2}
|
||||||
|
value={createForm.description}
|
||||||
|
onChange={e => setCreateForm(f => ({ ...f, description: e.target.value }))}
|
||||||
|
placeholder="vd: Kiểm soát viên nội bộ"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Edit existing role */}
|
||||||
|
<Dialog
|
||||||
|
open={!!editTarget}
|
||||||
|
onClose={() => setEditTarget(null)}
|
||||||
|
title={`Sửa role: ${editTarget?.name}`}
|
||||||
|
size="md"
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button variant="outline" onClick={() => setEditTarget(null)}>Hủy</Button>
|
||||||
|
<Button onClick={() => editMut.mutate()} disabled={editMut.isPending}>
|
||||||
|
{editMut.isPending ? 'Đang lưu…' : 'Lưu'}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{editTarget && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label>Mã code</Label>
|
||||||
|
<Input value={editTarget.name} disabled className="bg-slate-50 font-mono" />
|
||||||
|
<div className="text-xs text-slate-500">Không đổi được sau khi tạo.</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label>Mã viết tắt</Label>
|
||||||
|
<Input
|
||||||
|
value={editForm.shortName}
|
||||||
|
onChange={e => setEditForm(f => ({ ...f, shortName: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label>Tên đầy đủ tiếng Việt</Label>
|
||||||
|
<Textarea
|
||||||
|
rows={2}
|
||||||
|
value={editForm.description}
|
||||||
|
onChange={e => setEditForm(f => ({ ...f, description: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -14,4 +14,29 @@ public class RolesController(IMediator mediator) : ControllerBase
|
|||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<ActionResult<List<RoleDto>>> List(CancellationToken ct)
|
public async Task<ActionResult<List<RoleDto>>> List(CancellationToken ct)
|
||||||
=> Ok(await mediator.Send(new ListRolesQuery(), 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);
|
.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