using System.Collections.Concurrent;
using System.Security.Cryptography;
using Microsoft.AspNetCore.WebUtilities;
namespace AIStudio.Assistants.VisualBriefing;
///
/// Issues and validates short-lived, non-guessable preview grants.
///
public sealed class VisualBriefingPreviewTokenService
{
///
/// Defines the maximum preview-grant lifetime.
///
private static readonly TimeSpan TOKEN_LIFETIME = TimeSpan.FromMinutes(2);
///
/// Stores active grants by opaque token.
///
private readonly ConcurrentDictionary grants = new(StringComparer.Ordinal);
///
/// Issues a preview token bound to one briefing revision.
///
/// The briefing identifier.
/// The revision identifier.
/// The opaque preview token.
public string Issue(Guid briefingId, Guid revisionId)
{
this.RemoveExpired();
var token = WebEncoders.Base64UrlEncode(RandomNumberGenerator.GetBytes(32));
this.grants[token] = new(briefingId, revisionId, DateTimeOffset.UtcNow.Add(TOKEN_LIFETIME));
return token;
}
///
/// Validates a token and its briefing/revision binding.
///
/// The opaque preview token.
/// The requested briefing identifier.
/// The requested revision identifier.
/// Whether the grant is valid and unexpired.
public bool Validate(string? token, Guid briefingId, Guid revisionId)
{
if (string.IsNullOrWhiteSpace(token) || !this.grants.TryGetValue(token, out var grant))
return false;
if (grant.ExpiresAtUtc <= DateTimeOffset.UtcNow)
{
this.grants.TryRemove(token, out _);
return false;
}
return grant.BriefingId == briefingId && grant.RevisionId == revisionId;
}
///
/// Removes expired grants.
///
private void RemoveExpired()
{
var now = DateTimeOffset.UtcNow;
foreach (var (token, grant) in this.grants)
if (grant.ExpiresAtUtc <= now)
this.grants.TryRemove(token, out _);
}
///
/// Stores one token binding and expiry.
///
/// The bound briefing identifier.
/// The bound revision identifier.
/// The token expiry.
private sealed record PreviewGrant(Guid BriefingId, Guid RevisionId, DateTimeOffset ExpiresAtUtc);
}