87 lines
3.5 KiB
C#
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,
|
|
};
|
|
}
|