104 lines
4.6 KiB
C#
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));
|
|
}
|
|
}
|