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> 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> 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)); } }