重构页面目录与 RBAC:扁平配置入口并对齐管理/运营页映射。
下线运营总览幽灵页,补 ExpandKeysForScope / PlatformToMonitor,同步导航与角色配置 UI。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+124
-18
@@ -31,8 +31,12 @@ public static class PageCatalog
|
||||
new("admin-dashboard", "总览", "概览", ScopePlatform),
|
||||
new("admin-setup", "初始配置", "概览", ScopePlatform),
|
||||
new("admin-map-monitor", "地图监控", "概览", ScopePlatform),
|
||||
new("admin-playback", "调度回放", "概览", ScopePlatform),
|
||||
new("admin-tasks", "任务管理", "概览", ScopePlatform),
|
||||
new("admin-alarms", "报警管理", "概览", ScopePlatform),
|
||||
new("admin-ops-log", "运维记录", "概览", ScopePlatform),
|
||||
new("admin-notes", "运维备注", "概览", ScopePlatform),
|
||||
new("admin-logs", "日志管理", "概览", ScopePlatform),
|
||||
|
||||
// ── 管理端 / Platform:设计与编排 ──
|
||||
new("admin-maps", "地图管理", "设计与编排", ScopePlatform),
|
||||
@@ -50,20 +54,18 @@ public static class PageCatalog
|
||||
new("admin-data-center", "数据中心", "数据中心", ScopePlatform),
|
||||
|
||||
// ── 管理端 / Platform:平台配置中心(聚合页,每个 Key 对齐前端聚合路由 route.name) ──
|
||||
new("admin-config-strategy", "调度策略", "平台配置中心", ScopePlatform),
|
||||
new("admin-vehicle-hub", "车辆运维", "平台配置中心", ScopePlatform),
|
||||
new("admin-config-facility", "设备接入", "平台配置中心", ScopePlatform),
|
||||
new("admin-config-warehouse", "库位管理", "平台配置中心", ScopePlatform),
|
||||
new("admin-config-business", "业务与集成", "平台配置中心", ScopePlatform),
|
||||
new("admin-config-ops-center", "运维与回放", "平台配置中心", ScopePlatform),
|
||||
new("admin-config-system-center", "系统与权限", "平台配置中心", ScopePlatform),
|
||||
new("admin-config-ops-center", "监控配置", "平台配置中心", ScopePlatform),
|
||||
new("admin-config-system", "系统配置", "平台配置中心", ScopePlatform),
|
||||
new("admin-config-auth", "权限与角色", "平台配置中心", ScopePlatform),
|
||||
|
||||
// ── 运营端 / RCSMonitor ──
|
||||
new("monitor-dashboard", "运营总览", "运营监控", ScopeMonitor),
|
||||
new("monitor-vehicle-hub", "车辆运维", "运营监控", ScopeMonitor),
|
||||
new("monitor-map", "地图监控", "运营监控", ScopeMonitor),
|
||||
new("monitor-ops", "运维操作", "运营监控", ScopeMonitor),
|
||||
new("monitor-notes", "运营备注", "运营监控", ScopeMonitor),
|
||||
new("monitor-ops", "运维记录", "运营监控", ScopeMonitor),
|
||||
new("monitor-notes", "运维备注", "运营监控", ScopeMonitor),
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> _keys =
|
||||
@@ -76,21 +78,14 @@ public static class PageCatalog
|
||||
["admin-config-vehicle"] = "admin-vehicle-hub",
|
||||
["admin-config-fleet"] = "admin-vehicle-hub",
|
||||
// 会话 16:平台配置中心入口按业务收敛为 6 个聚合页,旧 Key 迁移到对应聚合页 Key。
|
||||
["admin-config-routing"] = "admin-config-strategy",
|
||||
["admin-config-task"] = "admin-config-strategy",
|
||||
["admin-config-traffic"] = "admin-config-strategy",
|
||||
["admin-config-charge"] = "admin-config-strategy",
|
||||
["admin-config-device"] = "admin-config-facility",
|
||||
["admin-config-location"] = "admin-config-warehouse",
|
||||
["admin-config-integrations"] = "admin-config-business",
|
||||
["admin-config-scenario"] = "admin-config-business",
|
||||
["admin-config-widget"] = "admin-config-business",
|
||||
["admin-playback"] = "admin-config-ops-center",
|
||||
["admin-config-ops"] = "admin-config-ops-center",
|
||||
["admin-config-logs"] = "admin-config-ops-center",
|
||||
["admin-config-logs"] = "admin-logs",
|
||||
["admin-config-map-monitor"] = "admin-config-ops-center",
|
||||
["admin-config-system"] = "admin-config-system-center",
|
||||
["admin-config-auth"] = "admin-config-system-center",
|
||||
["admin-config-system-center"] = "admin-config-system",
|
||||
// 运营总览页已下线:旧角色勾选迁到地图监控,避免权限清单里出现幽灵页面。
|
||||
["monitor-dashboard"] = "monitor-map",
|
||||
["admin-data-center-stations"] = "admin-data-center",
|
||||
["admin-data-center-docks"] = "admin-data-center",
|
||||
["admin-data-center-handshake"] = "admin-data-center",
|
||||
@@ -98,6 +93,64 @@ public static class PageCatalog
|
||||
["admin-data-center-mag-control"] = "admin-data-center",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 管理端页面 → 运营端对应页面。给运营角色勾页时,只能从「当前管理员已有的管理端页」映射出来。
|
||||
/// 无对应运营页的管理端入口(如任务/报警)不出现在映射里。
|
||||
/// </summary>
|
||||
public static readonly IReadOnlyDictionary<string, string> PlatformToMonitor =
|
||||
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["admin-map-monitor"] = "monitor-map",
|
||||
["admin-vehicle-hub"] = "monitor-vehicle-hub",
|
||||
["admin-ops-log"] = "monitor-ops",
|
||||
["admin-notes"] = "monitor-notes",
|
||||
};
|
||||
|
||||
/// <summary>仅运营端有的页面:不依赖管理端对应页,始终可授给运营角色。</summary>
|
||||
public static readonly IReadOnlyList<string> MonitorOnlyPages =
|
||||
new[] { "monitor-ops", "monitor-notes" };
|
||||
|
||||
/// <summary>
|
||||
/// 把角色里勾选的页面展开到指定登录域:勾了运营监控的「运维记录」时,
|
||||
/// 管理账号/通用角色进管理壳也能拿到对应的 admin-ops-log。
|
||||
/// </summary>
|
||||
public static HashSet<string> ExpandKeysForScope(IEnumerable<string> keys, string scope)
|
||||
{
|
||||
var scopeKeys = KeysForScope(scope).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var raw in keys ?? Array.Empty<string>())
|
||||
{
|
||||
if (string.Equals(raw, Wildcard, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
foreach (var k in scopeKeys) set.Add(k);
|
||||
return set;
|
||||
}
|
||||
var key = NormalizeKey(raw);
|
||||
if (scopeKeys.Contains(key)) set.Add(key);
|
||||
var counterpart = CounterpartForScope(key, scope);
|
||||
if (counterpart != null && scopeKeys.Contains(counterpart)) set.Add(counterpart);
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
/// <summary>管理页 ↔ 运营页互认:monitor-ops → admin-ops-log(当前域是 Platform 时)。</summary>
|
||||
public static string? CounterpartForScope(string key, string targetScope)
|
||||
{
|
||||
if (string.Equals(targetScope, ScopePlatform, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
foreach (var kv in PlatformToMonitor)
|
||||
{
|
||||
if (string.Equals(kv.Value, key, StringComparison.OrdinalIgnoreCase))
|
||||
return kv.Key;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (string.Equals(targetScope, ScopeMonitor, StringComparison.OrdinalIgnoreCase)
|
||||
&& PlatformToMonitor.TryGetValue(key, out var monitorKey))
|
||||
return monitorKey;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>判断页面 Key 是否合法(用于角色保存时过滤掉脏数据 / 已下线页面)。</summary>
|
||||
public static bool IsValidKey(string key) => _keys.Contains(key);
|
||||
|
||||
@@ -110,4 +163,57 @@ public static class PageCatalog
|
||||
All.Where(p => string.Equals(p.Scope, scope, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(p => p.Key)
|
||||
.ToList();
|
||||
|
||||
/// <summary>当前管理员在管理端拥有的页面 → 可授给运营端角色的页面集合。</summary>
|
||||
public static HashSet<string> GrantableMonitorPages(IEnumerable<string>? actorPlatformPages)
|
||||
{
|
||||
var set = new HashSet<string>(MonitorOnlyPages, StringComparer.OrdinalIgnoreCase);
|
||||
var list = (actorPlatformPages ?? Array.Empty<string>()).ToList();
|
||||
if (list.Contains(Wildcard, StringComparer.OrdinalIgnoreCase)
|
||||
|| HasAllKeys(list, KeysForScope(ScopePlatform)))
|
||||
{
|
||||
foreach (var k in KeysForScope(ScopeMonitor)) set.Add(k);
|
||||
return set;
|
||||
}
|
||||
foreach (var p in list)
|
||||
{
|
||||
var key = NormalizeKey(p);
|
||||
if (PlatformToMonitor.TryGetValue(key, out var monitorKey) && IsValidKey(monitorKey))
|
||||
set.Add(monitorKey);
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
/// <summary>当前管理员可授给管理端角色的页面(不能超权:只能授自己已有的)。</summary>
|
||||
public static HashSet<string> GrantablePlatformPages(IEnumerable<string>? actorPlatformPages)
|
||||
{
|
||||
var list = (actorPlatformPages ?? Array.Empty<string>()).ToList();
|
||||
var all = KeysForScope(ScopePlatform);
|
||||
if (list.Contains(Wildcard, StringComparer.OrdinalIgnoreCase) || HasAllKeys(list, all))
|
||||
return all.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
return list
|
||||
.Select(NormalizeKey)
|
||||
.Where(k => IsValidKey(k) && all.Contains(k, StringComparer.OrdinalIgnoreCase))
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>按角色归属域,算出当前管理员可勾选的页面 Key。</summary>
|
||||
public static HashSet<string> GrantablePagesForRoleScope(
|
||||
string roleScope, IEnumerable<string>? actorPlatformPages)
|
||||
{
|
||||
if (string.Equals(roleScope, ScopeMonitor, StringComparison.OrdinalIgnoreCase))
|
||||
return GrantableMonitorPages(actorPlatformPages);
|
||||
if (string.Equals(roleScope, ScopePlatform, StringComparison.OrdinalIgnoreCase))
|
||||
return GrantablePlatformPages(actorPlatformPages);
|
||||
// 通用域:管理端可授 + 运营端可授
|
||||
var set = GrantablePlatformPages(actorPlatformPages);
|
||||
set.UnionWith(GrantableMonitorPages(actorPlatformPages));
|
||||
return set;
|
||||
}
|
||||
|
||||
private static bool HasAllKeys(IEnumerable<string> have, IEnumerable<string> all)
|
||||
{
|
||||
var set = have.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
return all.All(k => set.Contains(k));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace MiGu.Server.Auth;
|
||||
///
|
||||
/// 首次启动(rbac.json 不存在)时 seed 两个内置账号:
|
||||
/// admin(超级管理员,scope=*,全部页面 / 操作)
|
||||
/// ops (运营人员,scope=RCSMonitor,运营四页 + 运维操作码)
|
||||
/// ops (运营人员,scope=RCSMonitor,运营监控页 + 运维操作码)
|
||||
/// 初始密码取 appsettings <c>Auth:Users:{name}:Password</c>,缺省 admin/ops(开发弱口令,生产须改)。
|
||||
/// </summary>
|
||||
public sealed class RbacStore
|
||||
@@ -58,9 +58,14 @@ public sealed class RbacStore
|
||||
var snap = JsonSerializer.Deserialize<RbacSnapshot>(json, _jsonOpts);
|
||||
if (snap is { Users.Count: > 0 })
|
||||
{
|
||||
_snapshot = Normalize(snap);
|
||||
_snapshot = Normalize(snap, out var migrated);
|
||||
_logger.LogInformation("RBAC 从 {File} 载入:{Users} 用户 / {Roles} 角色。",
|
||||
_file, _snapshot.Users.Count, _snapshot.Roles.Count);
|
||||
if (migrated)
|
||||
{
|
||||
Persist();
|
||||
_logger.LogInformation("RBAC 已将下线页面 Key 迁移并写回 {File}。", _file);
|
||||
}
|
||||
return;
|
||||
}
|
||||
_logger.LogWarning("RBAC 文件 {File} 内容为空或无用户,回退到默认 seed。", _file);
|
||||
@@ -82,21 +87,30 @@ public sealed class RbacStore
|
||||
}
|
||||
|
||||
/// <summary>清洗加载结果:补默认、去重、过滤非法页面 Key,保证内置角色存在。</summary>
|
||||
private static RbacSnapshot Normalize(RbacSnapshot snap)
|
||||
/// <param name="migrated">任一角色页面 Key 被别名替换 / 丢弃 / 补齐时为 true,调用方应落盘。</param>
|
||||
private static RbacSnapshot Normalize(RbacSnapshot snap, out bool migrated)
|
||||
{
|
||||
migrated = false;
|
||||
snap.Roles ??= new();
|
||||
snap.Users ??= new();
|
||||
foreach (var r in snap.Roles)
|
||||
{
|
||||
r.Pages = (r.Pages ?? new())
|
||||
var before = r.Pages ?? new();
|
||||
var hadSystemHub = before.Any(p =>
|
||||
string.Equals(p, "admin-config-system-center", StringComparison.OrdinalIgnoreCase));
|
||||
var after = before
|
||||
.Select(p => p == PageCatalog.Wildcard ? p : PageCatalog.NormalizeKey(p))
|
||||
.Where(p => p == PageCatalog.Wildcard || PageCatalog.IsValidKey(p))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
if (!SamePages(before, after)) migrated = true;
|
||||
r.Pages = after;
|
||||
r.Ops = (r.Ops ?? new()).Distinct().ToList();
|
||||
r.WidgetGrants ??= new();
|
||||
if (string.IsNullOrWhiteSpace(r.Scope)) r.Scope = PageCatalog.ScopePlatform;
|
||||
BackfillKnownPageMigrations(r);
|
||||
var countBeforeBackfill = r.Pages.Count;
|
||||
BackfillKnownPageMigrations(r, hadSystemHub);
|
||||
if (r.Pages.Count != countBeforeBackfill) migrated = true;
|
||||
}
|
||||
foreach (var u in snap.Users)
|
||||
{
|
||||
@@ -105,7 +119,12 @@ public sealed class RbacStore
|
||||
return snap;
|
||||
}
|
||||
|
||||
private static void BackfillKnownPageMigrations(RbacRole r)
|
||||
private static bool SamePages(IReadOnlyList<string> a, IReadOnlyList<string> b) =>
|
||||
a.Count == b.Count
|
||||
&& !a.Except(b, StringComparer.OrdinalIgnoreCase).Any()
|
||||
&& !b.Except(a, StringComparer.OrdinalIgnoreCase).Any();
|
||||
|
||||
private static void BackfillKnownPageMigrations(RbacRole r, bool hadSystemHub)
|
||||
{
|
||||
if (!string.Equals(r.Scope, PageCatalog.ScopePlatform, StringComparison.OrdinalIgnoreCase)
|
||||
&& r.Scope != PageCatalog.Wildcard) return;
|
||||
@@ -132,6 +151,25 @@ public sealed class RbacStore
|
||||
&& r.Pages.Contains("admin-cars", StringComparer.OrdinalIgnoreCase)
|
||||
&& r.Pages.Contains("admin-maps", StringComparer.OrdinalIgnoreCase))
|
||||
r.Pages.Add("admin-setup");
|
||||
|
||||
// 日志管理、调度回放从监控配置聚合页拆出:原先有聚合页权限的角色补上新入口。
|
||||
if (!r.Pages.Contains(PageCatalog.Wildcard)
|
||||
&& r.Pages.Contains("admin-config-ops-center", StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!r.Pages.Contains("admin-logs", StringComparer.OrdinalIgnoreCase))
|
||||
r.Pages.Add("admin-logs");
|
||||
if (!r.Pages.Contains("admin-playback", StringComparer.OrdinalIgnoreCase))
|
||||
r.Pages.Add("admin-playback");
|
||||
}
|
||||
|
||||
// 「系统与权限」拆成系统配置 / 权限与角色。
|
||||
if (!r.Pages.Contains(PageCatalog.Wildcard) && hadSystemHub)
|
||||
{
|
||||
if (!r.Pages.Contains("admin-config-system", StringComparer.OrdinalIgnoreCase))
|
||||
r.Pages.Add("admin-config-system");
|
||||
if (!r.Pages.Contains("admin-config-auth", StringComparer.OrdinalIgnoreCase))
|
||||
r.Pages.Add("admin-config-auth");
|
||||
}
|
||||
}
|
||||
|
||||
private RbacSnapshot SeedDefault(IConfiguration config)
|
||||
@@ -159,7 +197,7 @@ public sealed class RbacStore
|
||||
{
|
||||
Id = RoleOpsId, Name = "运营人员", Description = "运营监控端默认角色:可执行运维操作、查看监控",
|
||||
Scope = PageCatalog.ScopeMonitor,
|
||||
Pages = new() { "monitor-dashboard", "monitor-vehicle-hub", "monitor-map", "monitor-ops", "monitor-notes" },
|
||||
Pages = new() { "monitor-vehicle-hub", "monitor-map", "monitor-ops", "monitor-notes" },
|
||||
Ops = new()
|
||||
{
|
||||
"ops.car.pause", "ops.car.resume", "ops.car.gohome", "ops.car.resetSession",
|
||||
@@ -250,6 +288,20 @@ public sealed class RbacStore
|
||||
public bool CanUseScope(RbacUser user, string scope) =>
|
||||
UsableScopes(user).Contains(scope, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// 登录按账号角色自动选域:能进管理端则进管理端,否则进运营权限页。
|
||||
/// 不再接受客户端挑选「运营端 / 管理端」。
|
||||
/// </summary>
|
||||
public string? ResolveLoginScope(RbacUser user)
|
||||
{
|
||||
var scopes = UsableScopes(user);
|
||||
if (scopes.Contains(PageCatalog.ScopePlatform, StringComparer.OrdinalIgnoreCase))
|
||||
return PageCatalog.ScopePlatform;
|
||||
if (scopes.Contains(PageCatalog.ScopeMonitor, StringComparer.OrdinalIgnoreCase))
|
||||
return PageCatalog.ScopeMonitor;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>角色名(展示用,写入 AuthUserDto.Roles / JWT role claim)。</summary>
|
||||
public List<string> RoleNamesOf(RbacUser user)
|
||||
{
|
||||
@@ -276,7 +328,7 @@ public sealed class RbacStore
|
||||
foreach (var r in roles)
|
||||
{
|
||||
if (r.Pages.Contains(PageCatalog.Wildcard)) pages.UnionWith(scopeKeys);
|
||||
else foreach (var p in r.Pages) if (scopeKeys.Contains(p)) pages.Add(p);
|
||||
else pages.UnionWith(PageCatalog.ExpandKeysForScope(r.Pages, scope));
|
||||
|
||||
foreach (var o in r.Ops)
|
||||
{
|
||||
@@ -486,7 +538,11 @@ public sealed class RbacStore
|
||||
{
|
||||
if (pages is null) return new();
|
||||
if (pages.Contains(PageCatalog.Wildcard)) return new() { PageCatalog.Wildcard };
|
||||
return pages.Where(PageCatalog.IsValidKey).Distinct().ToList();
|
||||
return pages
|
||||
.Select(PageCatalog.NormalizeKey)
|
||||
.Where(p => p == PageCatalog.Wildcard || PageCatalog.IsValidKey(p))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static string NormalizeScope(string? scope) => scope switch
|
||||
|
||||
@@ -29,6 +29,7 @@ public class RbacController : ControllerBase
|
||||
new("ops.car.gohome", "车辆 · 回库"),
|
||||
new("ops.car.resetSession", "车辆 · 重置会话"),
|
||||
new("ops.car.manualCharge", "车辆 · 手动充电"),
|
||||
new("ops.car.execute", "车辆 · 地图监控动作(按管理端配置)"),
|
||||
new("ops.task.pause", "任务 · 暂停"),
|
||||
new("ops.task.cancel", "任务 · 取消"),
|
||||
new("ops.task.reassign", "任务 · 改派"),
|
||||
@@ -61,18 +62,35 @@ public class RbacController : ControllerBase
|
||||
|
||||
/// <summary>权限「字典」:页面清单 + 可选操作码 + 可选控件 + scope 选项。前端角色编辑器据此渲染勾选项。</summary>
|
||||
[HttpGet("catalog")]
|
||||
public IActionResult Catalog() => Ok(new
|
||||
public IActionResult Catalog()
|
||||
{
|
||||
pages = PageCatalog.All,
|
||||
ops = KnownOps,
|
||||
widgets = KnownWidgets,
|
||||
scopes = new[]
|
||||
var actorPages = ActorPlatformPages();
|
||||
var grantablePlatform = PageCatalog.GrantablePlatformPages(actorPages).OrderBy(x => x).ToList();
|
||||
var grantableMonitor = PageCatalog.GrantableMonitorPages(actorPages).OrderBy(x => x).ToList();
|
||||
return Ok(new
|
||||
{
|
||||
new { value = PageCatalog.ScopePlatform, label = "管理端 (Platform)" },
|
||||
new { value = PageCatalog.ScopeMonitor, label = "运营端 (RCSMonitor)" },
|
||||
new { value = PageCatalog.Wildcard, label = "通用 (全部域)" },
|
||||
}
|
||||
});
|
||||
pages = PageCatalog.All,
|
||||
ops = KnownOps,
|
||||
widgets = KnownWidgets,
|
||||
scopes = new[]
|
||||
{
|
||||
new { value = PageCatalog.ScopePlatform, label = "管理权限 (Platform)" },
|
||||
new { value = PageCatalog.ScopeMonitor, label = "运营权限 (RCSMonitor)" },
|
||||
new { value = PageCatalog.Wildcard, label = "通用 (全部域)" },
|
||||
},
|
||||
// 当前登录管理员可勾选的页面(运营端由管理端已有页映射而来)。
|
||||
grantablePages = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
[PageCatalog.ScopePlatform] = grantablePlatform,
|
||||
[PageCatalog.ScopeMonitor] = grantableMonitor,
|
||||
[PageCatalog.Wildcard] = grantablePlatform.Concat(grantableMonitor).Distinct(StringComparer.OrdinalIgnoreCase).OrderBy(x => x).ToList(),
|
||||
},
|
||||
platformToMonitor = PageCatalog.PlatformToMonitor
|
||||
.Select(kv => new { platform = kv.Key, monitor = kv.Value })
|
||||
.ToList(),
|
||||
monitorOnlyPages = PageCatalog.MonitorOnlyPages,
|
||||
});
|
||||
}
|
||||
|
||||
// ───────────────────────── 角色 ─────────────────────────
|
||||
|
||||
@@ -80,10 +98,12 @@ public class RbacController : ControllerBase
|
||||
public IActionResult ListRoles() => Ok(_store.ListRoles());
|
||||
|
||||
[HttpPost("roles")]
|
||||
public IActionResult CreateRole([FromBody] SaveRoleRequest req) => Guard(() => Ok(_store.CreateRole(req)));
|
||||
public IActionResult CreateRole([FromBody] SaveRoleRequest req) => Guard(() =>
|
||||
Ok(_store.CreateRole(ClampPages(req))));
|
||||
|
||||
[HttpPut("roles/{id}")]
|
||||
public IActionResult UpdateRole(string id, [FromBody] SaveRoleRequest req) => Guard(() => Ok(_store.UpdateRole(id, req)));
|
||||
public IActionResult UpdateRole(string id, [FromBody] SaveRoleRequest req) => Guard(() =>
|
||||
Ok(_store.UpdateRole(id, ClampPages(req))));
|
||||
|
||||
[HttpDelete("roles/{id}")]
|
||||
public IActionResult DeleteRole(string id) => Guard(() =>
|
||||
@@ -136,4 +156,43 @@ public class RbacController : ControllerBase
|
||||
|
||||
private string? CurrentUserId() =>
|
||||
User.FindFirstValue("sub") ?? User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
|
||||
/// <summary>当前登录管理员在管理端的有效页面集合。</summary>
|
||||
private List<string> ActorPlatformPages()
|
||||
{
|
||||
var id = CurrentUserId();
|
||||
if (string.IsNullOrEmpty(id)) return new();
|
||||
var user = _store.FindUserById(id);
|
||||
if (user is null) return new();
|
||||
return _store.ComputeEffective(user, PageCatalog.ScopePlatform).Pages;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存角色时按「当前管理员可授页面」裁剪:运营端页只能选自管理端已有页的映射 + 运营专属页。
|
||||
/// 通配 <c>*</c> 仅当可授集合已覆盖该域全部页面时才保留。
|
||||
/// </summary>
|
||||
private SaveRoleRequest ClampPages(SaveRoleRequest req)
|
||||
{
|
||||
var scope = string.IsNullOrWhiteSpace(req.Scope) ? PageCatalog.ScopePlatform : req.Scope.Trim();
|
||||
var grantable = PageCatalog.GrantablePagesForRoleScope(scope, ActorPlatformPages());
|
||||
var pages = req.Pages ?? new List<string>();
|
||||
if (pages.Contains(PageCatalog.Wildcard, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var scopeKeys = scope == PageCatalog.Wildcard
|
||||
? PageCatalog.All.Select(p => p.Key).ToList()
|
||||
: PageCatalog.KeysForScope(scope).ToList();
|
||||
if (scopeKeys.All(k => grantable.Contains(k)))
|
||||
return req with { Pages = new List<string> { PageCatalog.Wildcard } };
|
||||
return req with
|
||||
{
|
||||
Pages = scopeKeys.Where(k => grantable.Contains(k)).Distinct(StringComparer.OrdinalIgnoreCase).ToList()
|
||||
};
|
||||
}
|
||||
var clamped = pages
|
||||
.Select(PageCatalog.NormalizeKey)
|
||||
.Where(p => p == PageCatalog.Wildcard || (PageCatalog.IsValidKey(p) && grantable.Contains(p)))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
return req with { Pages = clamped };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +17,12 @@ public static class DashboardShortcutCatalog
|
||||
["platform-config"] = "admin-map-editor",
|
||||
["mission"] = "admin-task-templates",
|
||||
["cars"] = "admin-cars",
|
||||
["auth"] = "admin-config-system-center",
|
||||
["system"] = "admin-config-system-center",
|
||||
["auth"] = "admin-config-auth",
|
||||
["system"] = "admin-config-system",
|
||||
["admin-config-system-center"] = "admin-config-auth",
|
||||
["ops"] = "admin-config-ops-center",
|
||||
["tasks"] = "admin-config-strategy",
|
||||
["tasks"] = "admin-task-templates",
|
||||
["admin-config-strategy"] = "admin-task-templates",
|
||||
};
|
||||
|
||||
private static readonly ShortcutDef[] PlatformShortcuts =
|
||||
@@ -28,6 +30,10 @@ public static class DashboardShortcutCatalog
|
||||
new("admin-dashboard", "admin-dashboard", PageCatalog.ScopePlatform),
|
||||
new("admin-setup", "admin-setup", PageCatalog.ScopePlatform),
|
||||
new("admin-map-monitor", "admin-map-monitor", PageCatalog.ScopePlatform),
|
||||
new("admin-playback", "admin-playback", PageCatalog.ScopePlatform),
|
||||
new("admin-ops-log", "admin-ops-log", PageCatalog.ScopePlatform),
|
||||
new("admin-notes", "admin-notes", PageCatalog.ScopePlatform),
|
||||
new("admin-logs", "admin-logs", PageCatalog.ScopePlatform),
|
||||
new("admin-maps", "admin-maps", PageCatalog.ScopePlatform),
|
||||
new("admin-map-editor", "admin-map-editor", PageCatalog.ScopePlatform),
|
||||
new("admin-project-properties", "admin-project-properties", PageCatalog.ScopePlatform),
|
||||
@@ -38,17 +44,17 @@ public static class DashboardShortcutCatalog
|
||||
new("admin-task-templates", "admin-task-templates", PageCatalog.ScopePlatform),
|
||||
new("admin-simple-fields", "admin-simple-fields", PageCatalog.ScopePlatform),
|
||||
new("admin-data-center", "admin-data-center", PageCatalog.ScopePlatform),
|
||||
new("admin-config-strategy", "admin-config-strategy", PageCatalog.ScopePlatform),
|
||||
new("admin-setup", "admin-setup", PageCatalog.ScopePlatform),
|
||||
new("admin-wcs-template-proto", "admin-wcs-template-proto", PageCatalog.ScopePlatform),
|
||||
new("admin-vehicle-hub", "admin-vehicle-hub", PageCatalog.ScopePlatform),
|
||||
new("admin-config-facility", "admin-config-facility", PageCatalog.ScopePlatform),
|
||||
new("admin-config-business", "admin-config-business", PageCatalog.ScopePlatform),
|
||||
new("admin-config-ops-center", "admin-config-ops-center", PageCatalog.ScopePlatform),
|
||||
new("admin-config-system-center", "admin-config-system-center", PageCatalog.ScopePlatform),
|
||||
new("admin-config-system", "admin-config-system", PageCatalog.ScopePlatform),
|
||||
new("admin-config-auth", "admin-config-auth", PageCatalog.ScopePlatform),
|
||||
];
|
||||
|
||||
private static readonly ShortcutDef[] MonitorShortcuts =
|
||||
[
|
||||
new("monitor-dashboard", "monitor-dashboard", PageCatalog.ScopeMonitor),
|
||||
new("monitor-vehicle-hub", "monitor-vehicle-hub", PageCatalog.ScopeMonitor),
|
||||
new("monitor-map", "monitor-map", PageCatalog.ScopeMonitor),
|
||||
new("monitor-ops", "monitor-ops", PageCatalog.ScopeMonitor),
|
||||
@@ -80,16 +86,16 @@ public static class DashboardShortcutCatalog
|
||||
"admin-map-monitor",
|
||||
"admin-vehicle-hub",
|
||||
"admin-task-templates",
|
||||
"admin-config-system-center",
|
||||
"admin-config-auth",
|
||||
"admin-config-ops-center",
|
||||
"admin-config-strategy"
|
||||
"admin-config-facility"
|
||||
];
|
||||
|
||||
public static readonly IReadOnlyList<string> DefaultMonitorKeys =
|
||||
["monitor-vehicle-hub", "monitor-map", "monitor-ops"];
|
||||
|
||||
private static readonly HashSet<string> ExcludedKeys =
|
||||
new(StringComparer.OrdinalIgnoreCase) { "admin-dashboard", "monitor-dashboard" };
|
||||
new(StringComparer.OrdinalIgnoreCase) { "admin-dashboard" };
|
||||
|
||||
public static bool IsValidKey(string key) =>
|
||||
!ExcludedKeys.Contains(key) && ByKey.ContainsKey(key);
|
||||
|
||||
@@ -19,12 +19,18 @@
|
||||
"id": "role-ops",
|
||||
"name": "\u8FD0\u8425\u4EBA\u5458",
|
||||
"description": "\u8FD0\u8425\u76D1\u63A7\u7AEF\u9ED8\u8BA4\u89D2\u8272\uFF1A\u53EF\u6267\u884C\u8FD0\u7EF4\u64CD\u4F5C\u3001\u67E5\u770B\u76D1\u63A7",
|
||||
"scope": "RCSMonitor",
|
||||
"scope": "*",
|
||||
"pages": [
|
||||
"monitor-dashboard",
|
||||
"monitor-map",
|
||||
"monitor-notes",
|
||||
"admin-dashboard",
|
||||
"admin-map-monitor",
|
||||
"admin-tasks",
|
||||
"admin-alarms",
|
||||
"admin-config-warehouse",
|
||||
"monitor-ops",
|
||||
"monitor-notes"
|
||||
"admin-config-ops-center",
|
||||
"admin-logs",
|
||||
"admin-playback"
|
||||
],
|
||||
"ops": [
|
||||
"ops.car.pause",
|
||||
@@ -87,8 +93,8 @@
|
||||
"roleIds": [
|
||||
"role-ops"
|
||||
],
|
||||
"salt": "G6c5\u002BnLEjf3fEor1LgNjyg==",
|
||||
"passwordHash": "Zb1QiDzHctduFbZGKniTEy4OLJIvIOeoyLfc\u002Bo0y\u002BVg="
|
||||
"salt": "F6wxGwnf/nvdbbAKp6hYoQ==",
|
||||
"passwordHash": "\u002BFvJCZgUYReNSr4T6xIpZU7FoajVHe067JVaHf/Evb8="
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user