feat: replace Redis with PostgreSQL for caching, rate limiting, SMS codes, and token blacklist
- Add 4 PG entities: VerificationCode, RateLimitEntry, TokenBlacklistEntry, CacheEntry - Add 4 services: VerificationService, RateLimitService, TokenBlacklistService, CacheService - Add CleanupBackgroundService for periodic expired data cleanup - Add MigrationHelper for safe schema migration without data loss - Update AuthController: real SMS code generation, rate limiting, logout endpoint with JWT blacklist - Update JwtProvider: add JTI claim for token revocation - Update Program.cs: register new services, JWT blacklist validation, DB migration - Remove StackExchange.Redis NuGet package and all Redis config references - Update start-dev.bat: 6→5 services, remove Redis startup - Update docs: remove Redis references from all documentation - Fix: logout button spacing on profile page - Fix: .gitignore data/→/data/ to not ignore Infrastructure/Data/
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using HealthManager.Application.DTOs.Auth;
|
||||
using HealthManager.Domain.Interfaces;
|
||||
@@ -13,25 +14,34 @@ namespace HealthManager.WebApi.Controllers;
|
||||
[Route("api/auth")]
|
||||
public class AuthController(
|
||||
AuthService authService,
|
||||
IJwtProvider jwtProvider) : ControllerBase
|
||||
IJwtProvider jwtProvider,
|
||||
VerificationService verificationService,
|
||||
RateLimitService rateLimit,
|
||||
TokenBlacklistService tokenBlacklist) : ControllerBase
|
||||
{
|
||||
[HttpPost("send-sms")]
|
||||
public IActionResult SendSms([FromBody] SendSmsRequest request)
|
||||
public async Task<IActionResult> SendSms([FromBody] SendSmsRequest request)
|
||||
{
|
||||
// Demo: always succeed
|
||||
if (!await rateLimit.CheckAsync($"sms:{request.Phone}", 1, 60))
|
||||
return StatusCode(429, new { message = "发送过于频繁,请60秒后重试" });
|
||||
|
||||
var code = await verificationService.GenerateAsync(request.Phone, "login");
|
||||
await rateLimit.IncrementAsync($"sms:{request.Phone}", 60);
|
||||
|
||||
// Demo: log code to console since no real SMS gateway
|
||||
Console.WriteLine($"[SMS] Phone: {request.Phone}, Code: {code}");
|
||||
return Ok(new { message = "验证码已发送" });
|
||||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
public async Task<IActionResult> Login([FromBody] LoginRequest request)
|
||||
{
|
||||
// Demo: skip SMS verification, accept any code
|
||||
var user = await authService.GetUserByPhoneAsync(request.Phone);
|
||||
if (user == null)
|
||||
{
|
||||
// Demo: auto-register new users
|
||||
var db = HttpContext.RequestServices.GetRequiredService<Infrastructure.Data.AppDbContext>();
|
||||
|
||||
// Check if this phone was soft-deleted — restore instead of creating duplicate
|
||||
var deleted = await db.Users.IgnoreQueryFilters()
|
||||
.FirstOrDefaultAsync(u => u.Phone == request.Phone && u.IsDeleted);
|
||||
if (deleted != null)
|
||||
@@ -77,7 +87,6 @@ public class AuthController(
|
||||
PasswordHash = AuthService.HashPassword("demo123"),
|
||||
};
|
||||
|
||||
// Access DbContext via DI
|
||||
var db = HttpContext.RequestServices.GetRequiredService<Infrastructure.Data.AppDbContext>();
|
||||
db.Users.Add(user);
|
||||
await db.SaveChangesAsync();
|
||||
@@ -89,6 +98,24 @@ public class AuthController(
|
||||
return Ok(new AuthResponse(user.Id, user.Name, user.Role, accessToken, refreshToken));
|
||||
}
|
||||
|
||||
[HttpPost("logout")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
var userId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
||||
var jti = User.FindFirstValue(JwtRegisteredClaimNames.Jti);
|
||||
var expClaim = User.FindFirstValue(JwtRegisteredClaimNames.Exp);
|
||||
|
||||
if (!string.IsNullOrEmpty(jti) && !string.IsNullOrEmpty(expClaim))
|
||||
{
|
||||
var exp = DateTimeOffset.FromUnixTimeSeconds(long.Parse(expClaim)).UtcDateTime;
|
||||
await tokenBlacklist.AddAsync(jti, userId, exp);
|
||||
}
|
||||
|
||||
await authService.RevokeRefreshTokenAsync(userId);
|
||||
return Ok(new { message = "已登出" });
|
||||
}
|
||||
|
||||
[HttpPost("refresh")]
|
||||
public async Task<IActionResult> RefreshToken([FromBody] RefreshTokenRequest request)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user