重构页面目录与 RBAC:扁平配置入口并对齐管理/运营页映射。
下线运营总览幽灵页,补 ExpandKeysForScope / PlatformToMonitor,同步导航与角色配置 UI。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user