using MealPlan.Api.Data; using MealPlan.Api.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; namespace MealPlan.Api.Services; /// Persists refresh tokens in PostgreSQL (survives API restarts). public class DbRefreshTokenStore : IRefreshTokenStore { private readonly IServiceScopeFactory _scopeFactory; public DbRefreshTokenStore(IServiceScopeFactory scopeFactory) => _scopeFactory = scopeFactory; public void Add(RefreshToken token) { using var scope = _scopeFactory.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); db.RefreshTokens.Add(new RefreshTokenEntity { Token = token.Token, UserId = Guid.Parse(token.UserId), ExpiresAtUtc = token.ExpiresAtUtc, CreatedAtUtc = token.CreatedAtUtc, IsRevoked = false, }); db.SaveChanges(); } public RefreshToken? Get(string token) { using var scope = _scopeFactory.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var entity = db.RefreshTokens.AsNoTracking().FirstOrDefault(t => t.Token == token); return entity is null ? null : Map(entity); } public RefreshToken? Revoke(string token) { using var scope = _scopeFactory.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var entity = db.RefreshTokens.FirstOrDefault(t => t.Token == token); if (entity is null) return null; var wasActive = entity.IsActive; entity.IsRevoked = true; db.SaveChanges(); return wasActive ? Map(entity) : null; } public void RemoveExpired() { using var scope = _scopeFactory.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var cutoff = DateTime.UtcNow; db.RefreshTokens .Where(t => t.IsRevoked || t.ExpiresAtUtc <= cutoff) .ExecuteDelete(); } private static RefreshToken Map(RefreshTokenEntity entity) => new() { Token = entity.Token, UserId = entity.UserId.ToString(), ExpiresAtUtc = entity.ExpiresAtUtc, CreatedAtUtc = entity.CreatedAtUtc, IsRevoked = entity.IsRevoked, }; }