Implemented hybrid cache for user sessions, api keys and database provided settings. Cleaned up startup and adjusted caching option models for features

This commit is contained in:
2026-02-12 15:29:35 +01:00
parent dd44e5bb86
commit 741a60adc6
19 changed files with 240 additions and 132 deletions

View File

@@ -1,6 +1,6 @@
using System.Security.Claims;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Caching.Hybrid;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moonlight.Api.Configuration;
@@ -14,10 +14,10 @@ namespace Moonlight.Api.Services;
public class UserAuthService
{
private readonly DatabaseRepository<User> UserRepository;
private readonly IMemoryCache Cache;
private readonly ILogger<UserAuthService> Logger;
private readonly IOptions<SessionOptions> Options;
private readonly IOptions<UserOptions> Options;
private readonly IEnumerable<IUserAuthHook> Hooks;
private readonly HybridCache HybridCache;
private const string UserIdClaim = "UserId";
private const string IssuedAtClaim = "IssuedAt";
@@ -27,15 +27,16 @@ public class UserAuthService
public UserAuthService(
DatabaseRepository<User> userRepository,
ILogger<UserAuthService> logger,
IMemoryCache cache, IOptions<SessionOptions> options,
IEnumerable<IUserAuthHook> hooks
IOptions<UserOptions> options,
IEnumerable<IUserAuthHook> hooks,
HybridCache hybridCache
)
{
UserRepository = userRepository;
Logger = logger;
Cache = cache;
Options = options;
Hooks = hooks;
HybridCache = hybridCache;
}
public async Task<bool> SyncAsync(ClaimsPrincipal? principal)
@@ -80,8 +81,8 @@ public class UserAuthService
foreach (var hook in Hooks)
{
// Run every hook and if any returns false we return false as well
if(!await hook.SyncAsync(principal, user))
// Run every hook, and if any returns false, we return false as well
if (!await hook.SyncAsync(principal, user))
return false;
}
@@ -101,32 +102,29 @@ public class UserAuthService
var cacheKey = string.Format(CacheKeyPattern, userId);
if (!Cache.TryGetValue<UserSession>(cacheKey, out var user))
{
user = await UserRepository
.Query()
.AsNoTracking()
.Where(u => u.Id == userId)
.Select(u => new UserSession(
u.InvalidateTimestamp,
u.RoleMemberships.SelectMany(x => x.Role.Permissions).ToArray())
)
.FirstOrDefaultAsync();
var user = await HybridCache.GetOrCreateAsync<UserSession?>(
cacheKey,
async ct =>
{
return await UserRepository
.Query()
.AsNoTracking()
.Where(u => u.Id == userId)
.Select(u => new UserSession(
u.InvalidateTimestamp,
u.RoleMemberships.SelectMany(x => x.Role.Permissions).ToArray())
)
.FirstOrDefaultAsync(cancellationToken: ct);
},
new HybridCacheEntryOptions()
{
LocalCacheExpiration = Options.Value.ValidationCacheL1Expiry,
Expiration = Options.Value.ValidationCacheL2Expiry
}
);
if (user == null)
return false;
Cache.Set(
cacheKey,
user,
TimeSpan.FromMinutes(Options.Value.ValidationCacheMinutes)
);
}
else
{
if (user == null)
return false;
}
if (user == null)
return false;
var issuedAtString = principal.FindFirstValue(IssuedAtClaim);
@@ -146,11 +144,11 @@ public class UserAuthService
principal.Identities.First().AddClaims(
user.Permissions.Select(x => new Claim(Permissions.ClaimType, x))
);
foreach (var hook in Hooks)
{
// Run every hook and if any returns false we return false as well
if(!await hook.ValidateAsync(principal, userId))
// Run every hook, and if any returns false we return false as well
if (!await hook.ValidateAsync(principal, userId))
return false;
}