146 lines
5.6 KiB
C#
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);
|
|
}
|
|
}
|