feat(roles): системные роли read-only + русские имена + чистка дубликата у admin
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 40s
CI / Web (React + Vite) (push) Successful in 39s
Docker API / Build + push API (push) Successful in 45s
Docker Web / Build + push Web (push) Successful in 34s
Docker API / Deploy API on stage (push) Successful in 17s
Docker Web / Deploy Web on stage (push) Successful in 12s
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 40s
CI / Web (React + Vite) (push) Successful in 39s
Docker API / Build + push API (push) Successful in 45s
Docker Web / Build + push Web (push) Successful in 34s
Docker API / Deploy API on stage (push) Successful in 17s
Docker Web / Deploy Web on stage (push) Successful in 12s
Концепция: «Супер администратор» — платформенная Identity-роль SuperAdmin. «Администратор» — организационная роль внутри Employee (IsSystem=true в EmployeeRole). Они НЕ должны дублироваться у одного юзера. - Сидер: admin@food-market.local получает только Identity-роль SuperAdmin. Догоняющая ветка для существующих стендов: если есть Identity-роль Admin — RemoveFromRoleAsync. На стенде AspNetUserRoles почищен SQL'ом. - AppLayout: translateRoles() переводит SuperAdmin → «Супер администратор», скрывает Identity-роль Admin (org-уровень показывается через Employee/Role, не через Identity). - EmployeeRolesPage: клик по строке системной роли → alert «Системная роль, изменения недоступны». Edit-модалка для системных была частично defensive (disabled чекбоксы Phase 2c), теперь точка входа закрыта целиком. Кастомные роли — без изменений. EmployeeRole.IsSystem поле уже было — миграция не нужна. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6395cf348d
commit
8466cf928c
|
|
@ -78,14 +78,18 @@ public async Task StartAsync(CancellationToken ct)
|
|||
var result = await userMgr.CreateAsync(admin, "Admin12345!");
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await userMgr.AddToRoleAsync(admin, SystemRoles.Admin);
|
||||
// Только SuperAdmin как Identity-роль. «Администратор» —
|
||||
// организационная роль внутри Employee, не Identity.
|
||||
await userMgr.AddToRoleAsync(admin, SystemRoles.SuperAdmin);
|
||||
}
|
||||
}
|
||||
else if (!await userMgr.IsInRoleAsync(admin, SystemRoles.SuperAdmin))
|
||||
else
|
||||
{
|
||||
// Существующий admin без SuperAdmin — догоняем (для уже развёрнутых стендов).
|
||||
if (!await userMgr.IsInRoleAsync(admin, SystemRoles.SuperAdmin))
|
||||
await userMgr.AddToRoleAsync(admin, SystemRoles.SuperAdmin);
|
||||
// Чистим дублирующую Identity-роль Admin (если оставалась с прошлых сидов).
|
||||
if (await userMgr.IsInRoleAsync(admin, SystemRoles.Admin))
|
||||
await userMgr.RemoveFromRoleAsync(admin, SystemRoles.Admin);
|
||||
}
|
||||
|
||||
await SeedAdminEmployeeAsync(db, demoOrg.Id, admin?.Id, ct);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,22 @@ interface MeResponse {
|
|||
type NavItem = { to: string; icon: typeof LayoutDashboard; label: string; end?: boolean }
|
||||
type NavSection = { group: string; items: NavItem[] }
|
||||
|
||||
const ROLE_RU: Record<string, string> = {
|
||||
SuperAdmin: 'Супер администратор',
|
||||
Admin: 'Администратор',
|
||||
Manager: 'Менеджер',
|
||||
Cashier: 'Кассир',
|
||||
Storekeeper: 'Кладовщик',
|
||||
}
|
||||
const HIDDEN_ROLES = new Set(['Admin']) // org-уровневая роль показывается через Employee, не Identity
|
||||
|
||||
function translateRoles(roles: string[]): string {
|
||||
return roles
|
||||
.filter((r) => !HIDDEN_ROLES.has(r))
|
||||
.map((r) => ROLE_RU[r] ?? r)
|
||||
.join(', ')
|
||||
}
|
||||
|
||||
function buildNav(isSuperAdmin: boolean): NavSection[] {
|
||||
const catalog: NavItem[] = [
|
||||
{ to: '/catalog/products', icon: Package, label: 'Товары' },
|
||||
|
|
@ -147,7 +163,7 @@ export function AppLayout() {
|
|||
{me && (
|
||||
<div className="px-2 pb-2 text-xs text-slate-500">
|
||||
<div className="truncate font-medium text-slate-700 dark:text-slate-200">{me.name}</div>
|
||||
<div className="truncate">{me.roles.join(', ')}</div>
|
||||
<div className="truncate">{translateRoles(me.roles)}</div>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -157,10 +157,16 @@ export function EmployeeRolesPage() {
|
|||
sortKey={sortKey}
|
||||
sortOrder={sortOrder}
|
||||
onSortChange={setSort}
|
||||
onRowClick={(r) => setForm({
|
||||
onRowClick={(r) => {
|
||||
if (r.isSystem) {
|
||||
alert('Системная роль, изменения недоступны.')
|
||||
return
|
||||
}
|
||||
setForm({
|
||||
id: r.id, name: r.name, description: r.description ?? '',
|
||||
isSystem: r.isSystem, permissions: { ...blankPerms(), ...r.permissions },
|
||||
})}
|
||||
})
|
||||
}}
|
||||
columns={[
|
||||
{ header: 'Название', cell: (r) => (
|
||||
<div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue