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

87 lines
3.5 KiB
C#

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<IActionResult> 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<IActionResult> 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<IActionResult> Logout(CancellationToken ct)
{
var principal = AdminRequestContext.Principal(HttpContext)!;
await sessions.LogoutAsync(HttpContext, principal.SessionId, ct);
return NoContent();
}
[HttpPut("password")]
[AdminAuth]
public async Task<IActionResult> 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,
};
}