using System.Security.Claims; using Microsoft.EntityFrameworkCore; using Rmsys.Domain.Entities; using Rmsys.Infrastructure; namespace Rmsys.Api.Endpoints; /// /// 用户与登录管理(P0-7):开户、改资料、重置口令、停用/启用、删除。 /// 停用与重置口令都会 TokenVersion+1,已签发的令牌立即失效。 /// public static class UserEndpoints { private const string DefaultPassword = "Hd@2026"; public static void MapUserEndpoints(this WebApplication app) { // ============ 新增账号 ============ app.MapPost("/api/admin/users", async (UserCreateReq req, ClaimsPrincipal p, RmsysDbContext db) => { var acc = (req.account ?? "").Trim(); var name = (req.name ?? "").Trim(); if (acc.Length < 3 || acc.Length > 30) return Results.Ok(new { ok = false, message = "登录账号需 3~30 位" }); if (!System.Text.RegularExpressions.Regex.IsMatch(acc, "^[A-Za-z0-9_.]+$")) return Results.Ok(new { ok = false, message = "登录账号只能包含字母、数字、下划线" }); if (string.IsNullOrWhiteSpace(name)) return Results.Ok(new { ok = false, message = "姓名不能为空" }); if (await db.SysUsers.AnyAsync(x => x.Account == acc)) return Results.Ok(new { ok = false, message = "登录账号已存在" }); var role = await FindRole(db, req.role); if (role == null) return Results.Ok(new { ok = false, message = "角色不存在" }); var pwd = string.IsNullOrWhiteSpace(req.password) ? DefaultPassword : req.password!.Trim(); if (pwd.Length < 6) return Results.Ok(new { ok = false, message = "初始口令至少 6 位" }); var (hash, salt) = Auth.PasswordHasher.Hash(pwd); var u = new SysUser { Account = acc, RealName = name, DeptId = await DeptIdOf(db, req.dept), DataScope = string.IsNullOrWhiteSpace(req.scope) ? null : req.scope!.Trim(), PwdHash = hash, PwdSalt = salt, PwdUpdatedAt = DateTime.Now, Enabled = req.enabled == 0 ? 0 : 1, CreatedAt = DateTime.Now }; db.SysUsers.Add(u); await db.SaveChangesAsync(); db.SysUserRoles.Add(new SysUserRole { UserId = u.Id, RoleId = role.Id }); await db.SaveChangesAsync(); await Audit(db, p, "新增账号", acc, $"{name} / 角色={role.Name} / 初始口令已设置"); return Results.Ok(new { ok = true, id = u.Id, message = "账号已创建" }); }).RequireAuthorization(); // ============ 编辑资料(账号不可改)============ app.MapPut("/api/admin/users/{acc}", async (string acc, UserUpdateReq req, ClaimsPrincipal p, RmsysDbContext db) => { var u = await db.SysUsers.FirstOrDefaultAsync(x => x.Account == acc); if (u == null) return Results.Ok(new { ok = false, message = "账号不存在" }); if (!string.IsNullOrWhiteSpace(req.name)) u.RealName = req.name!.Trim(); if (req.dept != null) u.DeptId = await DeptIdOf(db, req.dept); if (req.scope != null) u.DataScope = string.IsNullOrWhiteSpace(req.scope) ? null : req.scope!.Trim(); var disable = false; if (req.enabled.HasValue && req.enabled.Value != u.Enabled) { if (req.enabled.Value == 0) { var guard = await LastAdminGuard(db, u.Id, "停用"); if (guard != null) return Results.Ok(new { ok = false, message = guard }); disable = true; } u.Enabled = req.enabled.Value; u.TokenVersion += 1; // 状态变化立即踢掉已登录会话 } if (!string.IsNullOrWhiteSpace(req.role)) { var role = await FindRole(db, req.role); if (role == null) return Results.Ok(new { ok = false, message = "角色不存在" }); var old = await db.SysUserRoles.Where(x => x.UserId == u.Id).ToListAsync(); if (!old.Any(x => x.RoleId == role.Id)) { // 换角色前也要保证不会把最后一个管理员改掉 var guard2 = await LastAdminGuard(db, u.Id, "改角色"); if (guard2 != null) return Results.Ok(new { ok = false, message = guard2 }); foreach (var x in old) db.SysUserRoles.Remove(x); db.SysUserRoles.Add(new SysUserRole { UserId = u.Id, RoleId = role.Id }); } } await db.SaveChangesAsync(); await Audit(db, p, "编辑账号", acc, $"{u.RealName} / 角色={(await FindRole(db, req.role))?.Name ?? "不变"} / 状态={(u.Enabled == 1 ? "启用" : "停用")}" + (disable ? "(已强制下线)" : "")); return Results.Ok(new { ok = true, message = "账号已保存" }); }).RequireAuthorization(); // ============ 重置口令 ============ app.MapPost("/api/admin/users/{acc}/reset-password", async (string acc, UserPwdReq req, ClaimsPrincipal p, RmsysDbContext db) => { var u = await db.SysUsers.FirstOrDefaultAsync(x => x.Account == acc); if (u == null) return Results.Ok(new { ok = false, message = "账号不存在" }); var pwd = string.IsNullOrWhiteSpace(req.password) ? DefaultPassword : req.password!.Trim(); if (pwd.Length < 6) return Results.Ok(new { ok = false, message = "口令至少 6 位" }); var (hash, salt) = Auth.PasswordHasher.Hash(pwd); u.PwdHash = hash; u.PwdSalt = salt; u.PwdUpdatedAt = DateTime.Now; u.FailCount = 0; u.LockedUntil = null; u.TokenVersion += 1; // 旧口令签发的令牌立即失效 await db.SaveChangesAsync(); await Audit(db, p, "重置口令", acc, $"{u.RealName}(已强制下线,旧令牌失效)"); return Results.Ok(new { ok = true, message = "口令已重置", password = pwd }); }).RequireAuthorization(); // ============ 删除账号 ============ app.MapDelete("/api/admin/users/{acc}", async (string acc, ClaimsPrincipal p, RmsysDbContext db) => { var u = await db.SysUsers.FirstOrDefaultAsync(x => x.Account == acc); if (u == null) return Results.Ok(new { ok = false, message = "账号不存在" }); var selfId = int.TryParse(p.FindFirst("uid")?.Value, out var uid) ? uid : 0; if (u.Id == selfId) return Results.Ok(new { ok = false, message = "不能删除当前登录的账号" }); var guard = await LastAdminGuard(db, u.Id, "删除"); if (guard != null) return Results.Ok(new { ok = false, message = guard }); foreach (var x in await db.SysUserRoles.Where(x => x.UserId == u.Id).ToListAsync()) db.SysUserRoles.Remove(x); db.SysUsers.Remove(u); await db.SaveChangesAsync(); await Audit(db, p, "删除账号", acc, u.RealName); return Results.Ok(new { ok = true, message = "账号已删除" }); }).RequireAuthorization(); } // ---------------- 辅助 ---------------- private static async Task FindRole(RmsysDbContext db, string? role) { if (string.IsNullOrWhiteSpace(role)) return null; var r = role.Trim(); return await db.SysRoles.FirstOrDefaultAsync(x => x.Name == r || x.Code == r); } private static async Task DeptIdOf(RmsysDbContext db, string? dept) { if (string.IsNullOrWhiteSpace(dept) || dept == "all") return null; var d = await db.Depts.FirstOrDefaultAsync(x => x.Code == dept); return d?.Id; } /// 要动的是最后一个启用的管理员时,拒绝操作 private static async Task LastAdminGuard(RmsysDbContext db, int userId, string action) { var adminRoleIds = await db.SysRoles.Where(x => x.Code == "admin").Select(x => x.Id).ToListAsync(); if (adminRoleIds.Count == 0) return null; var isTargetAdmin = await db.SysUserRoles.AnyAsync(x => x.UserId == userId && adminRoleIds.Contains(x.RoleId)); if (!isTargetAdmin) return null; var enabledAdminIds = await db.SysUsers.Where(x => x.Enabled == 1) .Where(x => db.SysUserRoles.Any(r => r.UserId == x.Id && adminRoleIds.Contains(r.RoleId))) .Select(x => x.Id).ToListAsync(); if (enabledAdminIds.Count <= 1 && enabledAdminIds.Contains(userId)) return $"这是最后一个启用的系统管理员,不能{action};请先新增或启用另一个管理员"; return null; } private static async Task Audit(RmsysDbContext db, ClaimsPrincipal p, string action, string target, string? detail) { db.AuditLogs.Add(new AuditLog { At = DateTime.Now, UserId = int.TryParse(p.FindFirst("uid")?.Value, out var uid) ? uid : null, UserName = p.FindFirst(ClaimTypes.Name)?.Value, Role = p.FindFirst(ClaimTypes.Role)?.Value, Action = action, Target = target, Detail = detail, Device = "API" }); await db.SaveChangesAsync(); } public record UserCreateReq(string? account, string? name, string? role, string? dept, string? scope, int? enabled, string? password); public record UserUpdateReq(string? name, string? role, string? dept, string? scope, int? enabled); public record UserPwdReq(string? password); }