89 lines
3.2 KiB
C#
89 lines
3.2 KiB
C#
using Microsoft.Extensions.Caching.Distributed;
|
|
using QRRapidoApp.Models;
|
|
|
|
namespace QRRapidoApp.Services
|
|
{
|
|
/// <summary>
|
|
/// Fixed-window rate limiter backed by IDistributedCache (works with MemoryCache or Redis).
|
|
/// Minor race conditions at window boundaries are acceptable for rate limiting purposes.
|
|
/// </summary>
|
|
public class ApiRateLimitService : IApiRateLimitService
|
|
{
|
|
private readonly IDistributedCache _cache;
|
|
private readonly ILogger<ApiRateLimitService> _logger;
|
|
|
|
public ApiRateLimitService(IDistributedCache cache, ILogger<ApiRateLimitService> logger)
|
|
{
|
|
_cache = cache;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<RateLimitResult> CheckAndIncrementAsync(string keyPrefix, ApiPlanTier tier)
|
|
{
|
|
var (perMin, perMonth) = ApiPlanLimits.GetLimits(tier);
|
|
var now = DateTime.UtcNow;
|
|
|
|
var minuteKey = $"rl:min:{keyPrefix}:{now:yyyyMMddHHmm}";
|
|
var monthKey = $"rl:mo:{keyPrefix}:{now:yyyyMM}";
|
|
|
|
var nextMinute = new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, 0, DateTimeKind.Utc)
|
|
.AddMinutes(1);
|
|
var resetTimestamp = ((DateTimeOffset)nextMinute).ToUnixTimeSeconds();
|
|
|
|
var minStr = await _cache.GetStringAsync(minuteKey);
|
|
var moStr = await _cache.GetStringAsync(monthKey);
|
|
|
|
int minCount = int.TryParse(minStr, out var mc) ? mc : 0;
|
|
int moCount = int.TryParse(moStr, out var mo) ? mo : 0;
|
|
|
|
bool monthlyExceeded = false;
|
|
bool allowed = true;
|
|
|
|
if (tier != ApiPlanTier.Enterprise)
|
|
{
|
|
if (moCount >= perMonth)
|
|
{
|
|
monthlyExceeded = true;
|
|
allowed = false;
|
|
}
|
|
else if (minCount >= perMin)
|
|
{
|
|
allowed = false;
|
|
}
|
|
}
|
|
|
|
if (allowed)
|
|
{
|
|
var minOpts = new DistributedCacheEntryOptions
|
|
{
|
|
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2)
|
|
};
|
|
var moOpts = new DistributedCacheEntryOptions
|
|
{
|
|
AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(33)
|
|
};
|
|
|
|
await Task.WhenAll(
|
|
_cache.SetStringAsync(minuteKey, (minCount + 1).ToString(), minOpts),
|
|
_cache.SetStringAsync(monthKey, (moCount + 1).ToString(), moOpts)
|
|
);
|
|
}
|
|
|
|
int reportedMinLimit = perMin == int.MaxValue ? -1 : perMin;
|
|
int reportedMoLimit = perMonth == int.MaxValue ? -1 : perMonth;
|
|
int usedMin = minCount + (allowed ? 1 : 0);
|
|
int usedMo = moCount + (allowed ? 1 : 0);
|
|
|
|
return new RateLimitResult(
|
|
Allowed: allowed,
|
|
PerMinuteLimit: reportedMinLimit,
|
|
PerMinuteUsed: usedMin,
|
|
ResetUnixTimestamp: resetTimestamp,
|
|
MonthlyLimit: reportedMoLimit,
|
|
MonthlyUsed: usedMo,
|
|
MonthlyExceeded: monthlyExceeded
|
|
);
|
|
}
|
|
}
|
|
}
|