using System.Collections.Concurrent; using System.Diagnostics; using Microsoft.AspNetCore.Identity; using Phantom.Common.Logging; using Phantom.Utils.Tasks; using ILogger = Serilog.ILogger; namespace Phantom.Server.Web.Identity.Authentication; public sealed class PhantomLoginStore { private static readonly ILogger Logger = PhantomLogger.Create<PhantomLoginStore>(); private static readonly TimeSpan ExpirationTime = TimeSpan.FromMinutes(1); internal static Func<IServiceProvider, PhantomLoginStore> Create(CancellationToken cancellationToken) { return provider => new PhantomLoginStore(provider.GetRequiredService<TaskManager>(), cancellationToken); } private readonly ConcurrentDictionary<string, LoginEntry> loginEntries = new (); private readonly CancellationToken cancellationToken; private PhantomLoginStore(TaskManager taskManager, CancellationToken cancellationToken) { this.cancellationToken = cancellationToken; taskManager.Run("Web login entry expiration loop", RunExpirationLoop); } private async Task RunExpirationLoop() { try { while (true) { await Task.Delay(ExpirationTime, cancellationToken); foreach (var (token, entry) in loginEntries) { if (entry.IsExpired) { Logger.Debug("Expired login entry for {Username}.", entry.User.UserName); loginEntries.TryRemove(token, out _); } } } } finally { Logger.Information("Expiration loop stopped."); } } internal void Add(string token, IdentityUser user, string password, string returnUrl) { loginEntries[token] = new LoginEntry(user, password, returnUrl, Stopwatch.StartNew()); } internal LoginEntry? Pop(string token) { if (!loginEntries.TryRemove(token, out var entry)) { return null; } if (entry.IsExpired) { Logger.Debug("Expired login entry for {Username}.", entry.User.UserName); return null; } return entry; } internal sealed record LoginEntry(IdentityUser User, string Password, string ReturnUrl, Stopwatch AddedTime) { public bool IsExpired => AddedTime.Elapsed >= ExpirationTime; } }