using MiaoJiZhang.Api.Contracts; using MiaoJiZhang.Api.Services; using MiaoJiZhang.Domain.Entities; using MiaoJiZhang.Infrastructure.Persistence; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace MiaoJiZhang.Api.Controllers; [ApiController] [AdminAuth(AdminRoles.SuperAdmin)] [Route("api/admin/security")] public sealed class AdminAccountsController(AppDbContext db) : ControllerBase { [HttpGet("accounts")] public async Task Accounts(CancellationToken ct) { var users = await db.AdminUsers.AsNoTracking() .OrderBy(item => item.Username) .Select(item => new { item.Id, item.Username, item.Role, item.IsActive, item.MustChangePassword, item.LastLoginAt, item.CreatedAt, activeSessions = item.Sessions.Count(session => !session.RevokedAt.HasValue && session.ExpiresAt > DateTime.UtcNow), }) .ToListAsync(ct); return Ok(users); } [HttpPost("accounts")] public async Task Create(CreateAdminUserRequest request, CancellationToken ct) { var username = request.Username.Trim(); if (username.Length is < 3 or > 64 || request.Password.Length is < 12 or > 128 || !AdminRoles.All.Contains(request.Role)) return BadRequest(new ApiError("ADMIN_ACCOUNT_INVALID", "管理员账号、密码或角色无效")); if (await db.AdminUsers.AnyAsync(item => item.Username == username, ct)) return Conflict(new ApiError("ADMIN_ACCOUNT_EXISTS", "管理员用户名已存在")); var now = DateTime.UtcNow; var user = new AdminUser { Username = username, PasswordHash = AdminSessionService.HashPassword(request.Password), Role = request.Role, IsActive = true, MustChangePassword = true, CreatedAt = now, UpdatedAt = now, }; db.AdminUsers.Add(user); await db.SaveChangesAsync(ct); return Ok(new { user.Id, user.Username, user.Role, user.IsActive, user.MustChangePassword }); } [HttpPut("accounts/{id:long}")] public async Task Update( long id, UpdateAdminUserRequest request, CancellationToken ct) { if (!AdminRoles.All.Contains(request.Role)) return BadRequest(new ApiError("ADMIN_ROLE_INVALID", "管理员角色无效")); var user = await db.AdminUsers.FindAsync([id], ct); if (user is null) return NotFound(); if (user.Role == AdminRoles.SuperAdmin && (!request.IsActive || request.Role != AdminRoles.SuperAdmin) && await ActiveSuperAdminCount(ct) <= 1) { return Conflict(new ApiError("LAST_SUPER_ADMIN", "不能停用或降级最后一个超级管理员")); } user.Role = request.Role; user.IsActive = request.IsActive; user.MustChangePassword = request.MustChangePassword; user.AuthVersion++; user.UpdatedAt = DateTime.UtcNow; await RevokeSessions(id, ct); await db.SaveChangesAsync(ct); return Ok(new { user.Id, user.Username, user.Role, user.IsActive, user.MustChangePassword }); } [HttpPut("accounts/{id:long}/password")] public async Task ResetPassword( long id, ResetAdminPasswordRequest request, CancellationToken ct) { if (request.Password.Length is < 12 or > 128) return BadRequest(new ApiError("ADMIN_PASSWORD_INVALID", "密码长度必须为 12 到 128 位")); var user = await db.AdminUsers.FindAsync([id], ct); if (user is null) return NotFound(); user.PasswordHash = AdminSessionService.HashPassword(request.Password); user.MustChangePassword = true; user.AuthVersion++; user.UpdatedAt = DateTime.UtcNow; await RevokeSessions(id, ct); await db.SaveChangesAsync(ct); return NoContent(); } [HttpPost("accounts/{id:long}/revoke-sessions")] public async Task Revoke(long id, CancellationToken ct) { if (!await db.AdminUsers.AnyAsync(item => item.Id == id, ct)) return NotFound(); await RevokeSessions(id, ct); return NoContent(); } [HttpGet("audit")] public async Task Audit( [FromQuery] string? username, [FromQuery] int page = 1, [FromQuery] int limit = 50, CancellationToken ct = default) { page = Math.Max(1, page); limit = Math.Clamp(limit, 1, 100); var query = db.AdminAuditLogs.AsNoTracking(); if (!string.IsNullOrWhiteSpace(username)) { var term = username.Trim(); query = query.Where(item => item.Username != null && item.Username.Contains(term)); } var total = await query.CountAsync(ct); var list = await query.OrderByDescending(item => item.CreatedAt) .Skip((page - 1) * limit).Take(limit).ToListAsync(ct); return Ok(new { total, page, list }); } private Task ActiveSuperAdminCount(CancellationToken ct) => db.AdminUsers.CountAsync( item => item.IsActive && item.Role == AdminRoles.SuperAdmin, ct); private Task RevokeSessions(long userId, CancellationToken ct) { var now = DateTime.UtcNow; return db.AdminSessions.Where(item => item.AdminUserId == userId && !item.RevokedAt.HasValue) .ExecuteUpdateAsync(setters => setters.SetProperty(item => item.RevokedAt, now), ct); } }