using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Tiku.Application.Learning; using Tiku.Domain.Identity; using Tiku.Domain.Tenancy; using Tiku.Infrastructure.Caching; using Tiku.Infrastructure.Persistence; using ZiggyCreatures.Caching.Fusion; namespace Tiku.Infrastructure.Learning; internal sealed class LearningStrongRevocationService( ILearningAccessPersistence persistence, [FromKeyedServices(BusinessCachingServiceCollectionExtensions.CacheName)] IFusionCache cache) : ILearningStrongRevocationService { private static readonly TimeSpan Lifetime = TimeSpan.FromSeconds(1); public async Task EnsureVersionAsync( LearningActor actor, long expectedVersion, CancellationToken cancellationToken = default) { var current = await cache.GetOrSetAsync( CacheKey(actor.TenantId, actor.UserId), (_, token) => LoadAsync(actor, token), options => options.SetDuration(Lifetime).SetFailSafe(false), token: cancellationToken); if (current != expectedVersion) throw new LearningAccessException( "practice_access_revoked", "The practice session was strongly revoked and can no longer be used."); } public Task InvalidateAsync( Guid tenantId, Guid userId, CancellationToken cancellationToken = default) { return cache.RemoveAsync(CacheKey(tenantId, userId), token: cancellationToken).AsTask(); } private async Task LoadAsync(LearningActor actor, CancellationToken cancellationToken) { var active = await ( from tenant in persistence.Tenants.AsNoTracking() join user in persistence.Users.AsNoTracking() on actor.UserId equals user.Id where tenant.Id == actor.TenantId && tenant.Status == TenantStatus.Active && user.Status == UserStatus.Active select tenant.Id) .AnyAsync(cancellationToken); if (!active) throw new LearningAccessException( "practice_access_revoked", "The tenant or student account is inactive."); return await persistence.LearningAccessVersions.AsNoTracking() .Where(item => item.TenantId == actor.TenantId && item.UserId == actor.UserId) .Select(item => (long?)item.StrongRevocationVersion) .SingleOrDefaultAsync(cancellationToken) ?? 1; } internal static string CacheKey(Guid tenantId, Guid userId) => $"learning-strong-revocation:v1:{tenantId:N}:{userId:N}"; }