Add transfer tracking and secure admin access
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
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 < 12 || request.NewPassword.Length > 128)
|
||||
return BadRequest(new ApiError("ADMIN_PASSWORD_INVALID", "新密码长度必须为 12 到 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user