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