Files
jizhi/backend/MiaoJiZhang.Api/Controllers/AuthController.cs
T
2026-07-24 23:11:20 +08:00

104 lines
4.6 KiB
C#

using MiaoJiZhang.Api.Contracts;
using MiaoJiZhang.Api.Services;
using MiaoJiZhang.Domain.Entities;
using MiaoJiZhang.Domain.Enums;
using MiaoJiZhang.Infrastructure.Persistence;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore;
namespace MiaoJiZhang.Api.Controllers;
[ApiController]
[EnableRateLimiting("auth")]
[Route("api/auth")]
public class AuthController(AppDbContext db, JwtService jwt) : ControllerBase
{
[HttpPost("register")]
public async Task<ActionResult<AuthResponse>> Register(RegisterRequest req)
{
if (!req.AgreedToTerms)
return BadRequest(new ApiError("TERMS_NOT_ACCEPTED", "请先阅读并同意用户协议与隐私政策"));
if (req.Username.Length is < 3 or > 32) return BadRequest(new ApiError("USERNAME_INVALID", "用户名长度需在 3-32 之间"));
if (req.Password.Length < 6) return BadRequest(new ApiError("PASSWORD_TOO_SHORT", "密码至少 6 位"));
if (await db.Users.AnyAsync(u => u.Username == req.Username)) return Conflict(new ApiError("USERNAME_TAKEN", "用户名已被占用"));
var now = DateTime.UtcNow;
var defaultAiEnabled = !string.Equals(
(await db.AppConfigs.FirstOrDefaultAsync(c => c.Key == "permission.default.ai_enabled"))?.Value,
"false",
StringComparison.OrdinalIgnoreCase);
var quotaConfig = await db.AppConfigs
.Where(c => c.Key == "quota.default_ai_chat_limit" ||
c.Key == "quota.default_ai_chat_period")
.ToDictionaryAsync(c => c.Key, c => c.Value);
var defaultAiChatLimit =
quotaConfig.TryGetValue("quota.default_ai_chat_limit", out var limitValue) &&
int.TryParse(limitValue, out var parsedLimit)
? Math.Clamp(parsedLimit, 0, 1_000_000)
: 50;
var periodValue = quotaConfig.GetValueOrDefault(
"quota.default_ai_chat_period",
"day");
if (!AiChatQuotaService.TryParsePeriod(periodValue, out var defaultAiChatPeriod))
defaultAiChatPeriod = AiQuotaPeriod.Day;
var (quotaWindowStart, _) =
AiChatQuotaService.CurrentWindow(defaultAiChatPeriod);
var user = new User
{
Username = req.Username,
PasswordHash = BCrypt.Net.BCrypt.HashPassword(req.Password),
AiChatLimit = defaultAiChatLimit,
AiChatPeriod = defaultAiChatPeriod,
AiChatWindowStartedAt = quotaWindowStart,
CreatedAt = now,
};
user.FeaturePermissions.Add(new UserFeaturePermission
{
PermissionKey = FeaturePermissionKeys.Ai,
IsEnabled = defaultAiEnabled,
CreatedAt = now,
UpdatedAt = now,
});
var defaultLedgerName = (await db.AppConfigs.FirstOrDefaultAsync(c => c.Key == "system.default_ledger_name"))?.Value ?? "日常账本";
user.Ledgers.Add(new Ledger { Name = defaultLedgerName, IsDefault = true, CreatedAt = DateTime.UtcNow });
db.Users.Add(user);
await db.SaveChangesAsync();
var (token, expires) = jwt.Issue(user);
return Ok(new AuthResponse(user.Id, user.Username, token, expires));
}
[HttpPost("login")]
public async Task<ActionResult<AuthResponse>> Login(LoginRequest req)
{
var user = await db.Users.FirstOrDefaultAsync(u => u.Username == req.Username);
if (user is null || !BCrypt.Net.BCrypt.Verify(req.Password, user.PasswordHash)) return Unauthorized(new ApiError("BAD_CREDENTIALS", "用户名或密码错误"));
if (user.IsBanned)
return Unauthorized(new ApiError("USER_BANNED", "账号已被封禁"));
var closureCancelled = false;
if (user.AccountClosureScheduledAt.HasValue)
{
if (user.AccountClosureScheduledAt <= DateTime.UtcNow)
return Unauthorized(new ApiError(
"ACCOUNT_CLOSED",
"账号注销等待期已结束,无法恢复"));
user.AccountClosureRequestedAt = null;
user.AccountClosureScheduledAt = null;
user.AuthVersion++;
closureCancelled = true;
}
user.LastLoginAt = DateTime.UtcNow;
await db.SaveChangesAsync();
var (token, expires) = jwt.Issue(user);
return Ok(new AuthResponse(
user.Id,
user.Username,
token,
expires,
closureCancelled));
}
}