From bd2800837fc1a746a146474d1ca54af508dfce5f Mon Sep 17 00:00:00 2001 From: nns <278048682+nurdotnet@users.noreply.github.com> Date: Sun, 26 Apr 2026 16:11:14 +0500 Subject: [PATCH] =?UTF-8?q?feat(roles):=20=D1=81=D0=B8=D1=81=D1=82=D0=B5?= =?UTF-8?q?=D0=BC=D0=BD=D1=8B=D0=B5=20=D1=80=D0=BE=D0=BB=D0=B8=20read-only?= =?UTF-8?q?=20+=20=D1=80=D1=83=D1=81=D1=81=D0=BA=D0=B8=D0=B5=20=D0=B8?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=B0=20+=20=D1=87=D0=B8=D1=81=D1=82=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=B4=D1=83=D0=B1=D0=BB=D0=B8=D0=BA=D0=B0=D1=82=D0=B0?= =?UTF-8?q?=20=D1=83=20admin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Концепция: «Супер администратор» — платформенная 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) --- src/food-market.api/Seed/DevDataSeeder.cs | 12 ++++++++---- .../src/components/AppLayout.tsx | 18 +++++++++++++++++- .../src/pages/EmployeeRolesPage.tsx | 14 ++++++++++---- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/food-market.api/Seed/DevDataSeeder.cs b/src/food-market.api/Seed/DevDataSeeder.cs index 517535e..e792d66 100644 --- a/src/food-market.api/Seed/DevDataSeeder.cs +++ b/src/food-market.api/Seed/DevDataSeeder.cs @@ -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 — догоняем (для уже развёрнутых стендов). - await userMgr.AddToRoleAsync(admin, SystemRoles.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); diff --git a/src/food-market.web/src/components/AppLayout.tsx b/src/food-market.web/src/components/AppLayout.tsx index 69f34ed..394301d 100644 --- a/src/food-market.web/src/components/AppLayout.tsx +++ b/src/food-market.web/src/components/AppLayout.tsx @@ -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 = { + 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 && (
{me.name}
-
{me.roles.join(', ')}
+
{translateRoles(me.roles)}
)}