Files
jizhi/backend/MiaoJiZhang.Api/Controllers/AdminAccountsController.cs
T

146 lines
5.6 KiB
C#

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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<int> ActiveSuperAdminCount(CancellationToken ct) => db.AdminUsers.CountAsync(
item => item.IsActive && item.Role == AdminRoles.SuperAdmin,
ct);
private Task<int> 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);
}
}