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:
MingNian
2026-05-26 13:48:53 +08:00
parent 39ab6062b5
commit d5f167167a
25 changed files with 613 additions and 47 deletions

View File

@@ -0,0 +1,55 @@
using Microsoft.EntityFrameworkCore;
namespace HealthManager.Infrastructure.Data;
public static class MigrationHelper
{
public static async Task EnsureNewTablesAsync(AppDbContext db)
{
var sql = """
CREATE TABLE IF NOT EXISTS "VerificationCodes" (
"Id" uuid PRIMARY KEY,
"Phone" text NOT NULL,
"Code" text NOT NULL,
"Type" text NOT NULL DEFAULT 'login',
"ExpiresAt" timestamptz NOT NULL,
"IsUsed" boolean NOT NULL DEFAULT FALSE,
"CreatedAt" timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS "IX_VerificationCodes_ExpiresAt" ON "VerificationCodes" ("ExpiresAt");
CREATE INDEX IF NOT EXISTS "IX_VerificationCodes_Phone_Type" ON "VerificationCodes" ("Phone", "Type");
CREATE TABLE IF NOT EXISTS "RateLimitEntries" (
"Id" uuid PRIMARY KEY,
"Key" text NOT NULL,
"Count" integer NOT NULL,
"WindowStart" timestamptz NOT NULL,
"ExpiresAt" timestamptz NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS "IX_RateLimitEntries_Key_WindowStart" ON "RateLimitEntries" ("Key", "WindowStart");
CREATE INDEX IF NOT EXISTS "IX_RateLimitEntries_ExpiresAt" ON "RateLimitEntries" ("ExpiresAt");
CREATE TABLE IF NOT EXISTS "TokenBlacklistEntries" (
"Id" uuid PRIMARY KEY,
"Jti" text NOT NULL,
"UserId" uuid NOT NULL,
"ExpiresAt" timestamptz NOT NULL,
"CreatedAt" timestamptz NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX IF NOT EXISTS "IX_TokenBlacklistEntries_Jti" ON "TokenBlacklistEntries" ("Jti");
CREATE INDEX IF NOT EXISTS "IX_TokenBlacklistEntries_ExpiresAt" ON "TokenBlacklistEntries" ("ExpiresAt");
CREATE TABLE IF NOT EXISTS "CacheEntries" (
"Id" uuid PRIMARY KEY,
"Key" text NOT NULL,
"Value" jsonb NOT NULL,
"ExpiresAt" timestamptz NOT NULL,
"CreatedAt" timestamptz NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX IF NOT EXISTS "IX_CacheEntries_Key" ON "CacheEntries" ("Key");
CREATE INDEX IF NOT EXISTS "IX_CacheEntries_ExpiresAt" ON "CacheEntries" ("ExpiresAt");
""";
await db.Database.ExecuteSqlRawAsync(sql);
}
}