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!");
|
var result = await userMgr.CreateAsync(admin, "Admin12345!");
|
||||||
if (result.Succeeded)
|
if (result.Succeeded)
|
||||||
{
|
{
|
||||||
await userMgr.AddToRoleAsync(admin, SystemRoles.Admin);
|
// Только SuperAdmin как Identity-роль. «Администратор» —
|
||||||
|
// организационная роль внутри Employee, не Identity.
|
||||||
await userMgr.AddToRoleAsync(admin, SystemRoles.SuperAdmin);
|
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);
|
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);
|
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 NavItem = { to: string; icon: typeof LayoutDashboard; label: string; end?: boolean }
|
||||||
type NavSection = { group: string; items: NavItem[] }
|
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[] {
|
function buildNav(isSuperAdmin: boolean): NavSection[] {
|
||||||
const catalog: NavItem[] = [
|
const catalog: NavItem[] = [
|
||||||
{ to: '/catalog/products', icon: Package, label: 'Товары' },
|
{ to: '/catalog/products', icon: Package, label: 'Товары' },
|
||||||
|
|
@ -147,7 +163,7 @@ export function AppLayout() {
|
||||||
{me && (
|
{me && (
|
||||||
<div className="px-2 pb-2 text-xs text-slate-500">
|
<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 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>
|
</div>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -157,10 +157,16 @@ export function EmployeeRolesPage() {
|
||||||
sortKey={sortKey}
|
sortKey={sortKey}
|
||||||
sortOrder={sortOrder}
|
sortOrder={sortOrder}
|
||||||
onSortChange={setSort}
|
onSortChange={setSort}
|
||||||
onRowClick={(r) => setForm({
|
onRowClick={(r) => {
|
||||||
id: r.id, name: r.name, description: r.description ?? '',
|
if (r.isSystem) {
|
||||||
isSystem: r.isSystem, permissions: { ...blankPerms(), ...r.permissions },
|
alert('Системная роль, изменения недоступны.')
|
||||||
})}
|
return
|
||||||
|
}
|
||||||
|
setForm({
|
||||||
|
id: r.id, name: r.name, description: r.description ?? '',
|
||||||
|
isSystem: r.isSystem, permissions: { ...blankPerms(), ...r.permissions },
|
||||||
|
})
|
||||||
|
}}
|
||||||
columns={[
|
columns={[
|
||||||
{ header: 'Название', cell: (r) => (
|
{ header: 'Название', cell: (r) => (
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue