using MiaoJiZhang.Api.Contracts; using MiaoJiZhang.Api.Services; using MiaoJiZhang.Infrastructure.Persistence; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.RateLimiting; using Microsoft.EntityFrameworkCore; namespace MiaoJiZhang.Api.Controllers; [ApiController] [Route("api/admin/auth")] public sealed class AdminAuthController( AppDbContext db, AdminSessionService sessions) : ControllerBase { [HttpPost("login")] [EnableRateLimiting("admin-auth")] public async Task Login(AdminLoginRequest request, CancellationToken ct) { if (string.IsNullOrWhiteSpace(request.Username) || string.IsNullOrEmpty(request.Password)) return BadRequest(new ApiError("ADMIN_LOGIN_INVALID", "请输入用户名和密码")); var result = await sessions.LoginAsync( HttpContext, request.Username, request.Password, ct); return result is null ? Unauthorized(new ApiError("ADMIN_LOGIN_FAILED", "用户名或密码错误,账号也可能已锁定")) : Ok(ToResponse(result.Principal, result.CsrfToken)); } [HttpGet("me")] [AdminAuth] public async Task Me(CancellationToken ct) { var principal = AdminRequestContext.Principal(HttpContext)!; var csrfToken = await sessions.RotateCsrfAsync(principal.SessionId, ct); return Ok(ToResponse(principal, csrfToken)); } [HttpPost("logout")] [AdminAuth] public async Task Logout(CancellationToken ct) { var principal = AdminRequestContext.Principal(HttpContext)!; await sessions.LogoutAsync(HttpContext, principal.SessionId, ct); return NoContent(); } [HttpPut("password")] [AdminAuth] public async Task ChangePassword( AdminChangePasswordRequest request, CancellationToken ct) { if (request.NewPassword.Length < AdminPasswordPolicy.MinLength || request.NewPassword.Length > AdminPasswordPolicy.MaxLength) return BadRequest(new ApiError("ADMIN_PASSWORD_INVALID", "新密码长度必须为 6 到 128 位")); var principal = AdminRequestContext.Principal(HttpContext)!; var user = await db.AdminUsers.FirstAsync(item => item.Id == principal.UserId, ct); if (!BCrypt.Net.BCrypt.Verify(request.CurrentPassword, user.PasswordHash)) return BadRequest(new ApiError("ADMIN_PASSWORD_INCORRECT", "当前密码不正确")); if (BCrypt.Net.BCrypt.Verify(request.NewPassword, user.PasswordHash)) return BadRequest(new ApiError("ADMIN_PASSWORD_UNCHANGED", "新密码不能与当前密码相同")); user.PasswordHash = AdminSessionService.HashPassword(request.NewPassword); user.MustChangePassword = false; user.AuthVersion++; user.UpdatedAt = DateTime.UtcNow; await sessions.RevokeOtherSessionsAsync(user.Id, principal.SessionId, user.AuthVersion, ct); await db.SaveChangesAsync(ct); var csrfToken = await sessions.RotateCsrfAsync(principal.SessionId, ct); var updated = principal with { MustChangePassword = false }; HttpContext.Items[AdminRequestContext.PrincipalKey] = updated; return Ok(ToResponse(updated, csrfToken)); } private static object ToResponse(AdminPrincipal principal, string csrfToken) => new { id = principal.UserId, principal.Username, principal.Role, principal.MustChangePassword, csrfToken, }; }