using System.Security.Claims; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using MiGu.Server.Auth; using MiGu.Server.Configs; namespace MiGu.Server.Controllers; /// /// RBAC 管理端:用户 / 角色 / 权限页面分配。整个控制器要求 RbacAdmin 策略 /// (JWT 的 ops claim 含 *auth.manage),即只有「超级管理员」类账号可访问。 /// /// 对应前端「平台配置中心 → 权限与角色」页(/admin/config/auth)。 /// [ApiController] [Authorize(Policy = "RbacAdmin")] [Route("api/rbac")] public class RbacController : ControllerBase { public sealed record OpDef(string Code, string Label); public sealed record WidgetDef(string Id, string Label); /// 可分配的操作码候选(管理端配置角色时下拉/勾选用)。 private static readonly OpDef[] KnownOps = { new("*", "全部操作(通配)"), new("ops.car.pause", "车辆 · 暂停"), new("ops.car.resume", "车辆 · 恢复"), 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", "任务 · 改派"), new("ops.task.boostPriority", "任务 · 提升优先级"), new("ops.ota", "OTA · 运维读写"), new("ops.ota.write", "OTA · 写操作"), new("monitor.note.write", "监控 · 写运营备注"), new("auth.manage", "系统 · 权限与角色管理"), }; /// 可配置可见性的控件候选。 private static readonly WidgetDef[] KnownWidgets = { new("MapEditor", "地图编辑器"), new("CadToolbar", "CAD 工具栏"), new("CarPanel", "车辆面板"), new("MissionEditor", "任务编辑器"), new("OpsActionPanel", "运维操作面板"), new("ConfigCenter", "配置中心"), }; private readonly RbacStore _store; private readonly ILogger _log; public RbacController(RbacStore store, ILogger log) { _store = store; _log = log; } /// 权限「字典」:页面清单 + 可选操作码 + 可选控件 + scope 选项。前端角色编辑器据此渲染勾选项。 [HttpGet("catalog")] public IActionResult Catalog() { var actorPages = ActorPlatformPages(); var grantablePlatform = PageCatalog.GrantablePlatformPages(actorPages).OrderBy(x => x).ToList(); var grantableMonitor = PageCatalog.GrantableMonitorPages(actorPages).OrderBy(x => x).ToList(); return Ok(new { 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>(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, }); } // ───────────────────────── 角色 ───────────────────────── [HttpGet("roles")] public IActionResult ListRoles() => Ok(_store.ListRoles()); [HttpPost("roles")] 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, ClampPages(req)))); [HttpDelete("roles/{id}")] public IActionResult DeleteRole(string id) => Guard(() => { _store.DeleteRole(id); return Ok(new { ok = true }); }); // ───────────────────────── 用户 ───────────────────────── [HttpGet("users")] public IActionResult ListUsers() => Ok(_store.ListUsers()); [HttpPost("users")] public IActionResult CreateUser([FromBody] CreateUserRequest req) => Guard(() => Ok(_store.CreateUser(req))); [HttpPut("users/{id}")] public IActionResult UpdateUser(string id, [FromBody] UpdateUserRequest req) => Guard(() => { // 自我保护:禁止把当前登录账号自己停用,避免管理员把自己锁在门外。 if (id == CurrentUserId() && req.Enabled == false) return (IActionResult)BadRequest(new { message = "不能停用当前登录的账号" }); return Ok(_store.UpdateUser(id, req)); }); [HttpPut("users/{id}/password")] public IActionResult SetPassword(string id, [FromBody] SetPasswordRequest req) => Guard(() => { _store.SetPassword(id, req.Password); return Ok(new { ok = true }); }); [HttpDelete("users/{id}")] public IActionResult DeleteUser(string id) => Guard(() => { if (id == CurrentUserId()) return (IActionResult)BadRequest(new { message = "不能删除当前登录的账号" }); _store.DeleteUser(id); return Ok(new { ok = true }); }); // ───────────────────────── 工具 ───────────────────────── /// 统一把 翻译成 400 + message,其余异常向上抛。 private IActionResult Guard(Func action) { try { return action(); } catch (RbacException ex) { return BadRequest(new { message = ex.Message }); } } private string? CurrentUserId() => User.FindFirstValue("sub") ?? User.FindFirstValue(ClaimTypes.NameIdentifier); /// 当前登录管理员在管理端的有效页面集合。 private List 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; } /// /// 保存角色时按「当前管理员可授页面」裁剪:运营端页只能选自管理端已有页的映射 + 运营专属页。 /// 通配 * 仅当可授集合已覆盖该域全部页面时才保留。 /// 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(); 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 { 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 }; } }