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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -406,3 +406,4 @@ FodyWeavers.xsd
|
|||||||
**/.env
|
**/.env
|
||||||
**/appsettings.json
|
**/appsettings.json
|
||||||
**/appsettings.Development.json
|
**/appsettings.Development.json
|
||||||
|
**/storage
|
||||||
@@ -2,5 +2,6 @@ namespace Moonlight.Api.Configuration;
|
|||||||
|
|
||||||
public class ApiOptions
|
public class ApiOptions
|
||||||
{
|
{
|
||||||
public int LookupCacheMinutes { get; set; } = 3;
|
public TimeSpan LookupCacheL1Expiry { get; set; } = TimeSpan.FromSeconds(30);
|
||||||
|
public TimeSpan LookupCacheL2Expiry { get; set; } = TimeSpan.FromMinutes(3);
|
||||||
}
|
}
|
||||||
6
Moonlight.Api/Configuration/CacheOptions.cs
Normal file
6
Moonlight.Api/Configuration/CacheOptions.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Moonlight.Api.Configuration;
|
||||||
|
|
||||||
|
public class CacheOptions
|
||||||
|
{
|
||||||
|
public bool EnableLayer2 { get; set; }
|
||||||
|
}
|
||||||
7
Moonlight.Api/Configuration/RedisOptions.cs
Normal file
7
Moonlight.Api/Configuration/RedisOptions.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Moonlight.Api.Configuration;
|
||||||
|
|
||||||
|
public class RedisOptions
|
||||||
|
{
|
||||||
|
public bool Enable { get; set; }
|
||||||
|
public string ConnectionString { get; set; }
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace Moonlight.Api.Configuration;
|
|
||||||
|
|
||||||
public class SessionOptions
|
|
||||||
{
|
|
||||||
public int ValidationCacheMinutes { get; set; } = 3;
|
|
||||||
}
|
|
||||||
@@ -2,5 +2,6 @@
|
|||||||
|
|
||||||
public class SettingsOptions
|
public class SettingsOptions
|
||||||
{
|
{
|
||||||
public int CacheMinutes { get; set; } = 3;
|
public TimeSpan LookupL1CacheTime { get; set; } = TimeSpan.FromMinutes(1);
|
||||||
|
public TimeSpan LookupL2CacheTime { get; set; } = TimeSpan.FromMinutes(5);
|
||||||
}
|
}
|
||||||
7
Moonlight.Api/Configuration/UserOptions.cs
Normal file
7
Moonlight.Api/Configuration/UserOptions.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Moonlight.Api.Configuration;
|
||||||
|
|
||||||
|
public class UserOptions
|
||||||
|
{
|
||||||
|
public TimeSpan ValidationCacheL1Expiry { get; set; } = TimeSpan.FromSeconds(30);
|
||||||
|
public TimeSpan ValidationCacheL2Expiry { get; set; } = TimeSpan.FromMinutes(3);
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Caching.Hybrid;
|
||||||
using Moonlight.Api.Database;
|
using Moonlight.Api.Database;
|
||||||
using Moonlight.Api.Database.Entities;
|
using Moonlight.Api.Database.Entities;
|
||||||
|
using Moonlight.Api.Implementations.ApiKeyScheme;
|
||||||
using Moonlight.Api.Mappers;
|
using Moonlight.Api.Mappers;
|
||||||
using Moonlight.Shared;
|
using Moonlight.Shared;
|
||||||
using Moonlight.Shared.Http.Requests;
|
using Moonlight.Shared.Http.Requests;
|
||||||
@@ -18,10 +20,12 @@ namespace Moonlight.Api.Http.Controllers.Admin;
|
|||||||
public class ApiKeyController : Controller
|
public class ApiKeyController : Controller
|
||||||
{
|
{
|
||||||
private readonly DatabaseRepository<ApiKey> KeyRepository;
|
private readonly DatabaseRepository<ApiKey> KeyRepository;
|
||||||
|
private readonly HybridCache HybridCache;
|
||||||
|
|
||||||
public ApiKeyController(DatabaseRepository<ApiKey> keyRepository)
|
public ApiKeyController(DatabaseRepository<ApiKey> keyRepository, HybridCache hybridCache)
|
||||||
{
|
{
|
||||||
KeyRepository = keyRepository;
|
KeyRepository = keyRepository;
|
||||||
|
HybridCache = hybridCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
@@ -114,6 +118,8 @@ public class ApiKeyController : Controller
|
|||||||
ApiKeyMapper.Merge(apiKey, request);
|
ApiKeyMapper.Merge(apiKey, request);
|
||||||
await KeyRepository.UpdateAsync(apiKey);
|
await KeyRepository.UpdateAsync(apiKey);
|
||||||
|
|
||||||
|
await HybridCache.RemoveAsync(string.Format(ApiKeySchemeHandler.CacheKeyFormat, apiKey.Key));
|
||||||
|
|
||||||
return ApiKeyMapper.ToDto(apiKey);
|
return ApiKeyMapper.ToDto(apiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +135,9 @@ public class ApiKeyController : Controller
|
|||||||
return Problem("No API key with this id found", statusCode: 404);
|
return Problem("No API key with this id found", statusCode: 404);
|
||||||
|
|
||||||
await KeyRepository.RemoveAsync(apiKey);
|
await KeyRepository.RemoveAsync(apiKey);
|
||||||
|
|
||||||
|
await HybridCache.RemoveAsync(string.Format(ApiKeySchemeHandler.CacheKeyFormat, apiKey.Key));
|
||||||
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@ using System.Security.Claims;
|
|||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Hybrid;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Moonlight.Api.Database;
|
using Moonlight.Api.Database;
|
||||||
@@ -14,20 +14,20 @@ namespace Moonlight.Api.Implementations.ApiKeyScheme;
|
|||||||
public class ApiKeySchemeHandler : AuthenticationHandler<ApiKeySchemeOptions>
|
public class ApiKeySchemeHandler : AuthenticationHandler<ApiKeySchemeOptions>
|
||||||
{
|
{
|
||||||
private readonly DatabaseRepository<ApiKey> ApiKeyRepository;
|
private readonly DatabaseRepository<ApiKey> ApiKeyRepository;
|
||||||
private readonly IMemoryCache MemoryCache;
|
private readonly HybridCache HybridCache;
|
||||||
|
|
||||||
private const string CacheKeyFormat = $"Moonlight.Api.{nameof(ApiKeySchemeHandler)}.{{0}}";
|
public const string CacheKeyFormat = $"Moonlight.Api.{nameof(ApiKeySchemeHandler)}.{{0}}";
|
||||||
|
|
||||||
public ApiKeySchemeHandler(
|
public ApiKeySchemeHandler(
|
||||||
IOptionsMonitor<ApiKeySchemeOptions> options,
|
IOptionsMonitor<ApiKeySchemeOptions> options,
|
||||||
ILoggerFactory logger,
|
ILoggerFactory logger,
|
||||||
UrlEncoder encoder,
|
UrlEncoder encoder,
|
||||||
DatabaseRepository<ApiKey> apiKeyRepository,
|
DatabaseRepository<ApiKey> apiKeyRepository,
|
||||||
IMemoryCache memoryCache
|
HybridCache hybridCache
|
||||||
) : base(options, logger, encoder)
|
) : base(options, logger, encoder)
|
||||||
{
|
{
|
||||||
ApiKeyRepository = apiKeyRepository;
|
ApiKeyRepository = apiKeyRepository;
|
||||||
MemoryCache = memoryCache;
|
HybridCache = hybridCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||||
@@ -42,24 +42,29 @@ public class ApiKeySchemeHandler : AuthenticationHandler<ApiKeySchemeOptions>
|
|||||||
|
|
||||||
var cacheKey = string.Format(CacheKeyFormat, authHeaderValue);
|
var cacheKey = string.Format(CacheKeyFormat, authHeaderValue);
|
||||||
|
|
||||||
if (!MemoryCache.TryGetValue<ApiKeySession>(cacheKey, out var apiKey))
|
var apiKey = await HybridCache.GetOrCreateAsync<ApiKeySession?>(
|
||||||
{
|
cacheKey,
|
||||||
apiKey = await ApiKeyRepository
|
async ct =>
|
||||||
.Query()
|
{
|
||||||
.Where(x => x.Key == authHeaderValue)
|
var x = await ApiKeyRepository
|
||||||
.Select(x => new ApiKeySession(x.Permissions, x.ValidUntil))
|
.Query()
|
||||||
.FirstOrDefaultAsync();
|
.Where(x => x.Key == authHeaderValue)
|
||||||
|
.Select(x => new ApiKeySession(x.Permissions, x.ValidUntil))
|
||||||
|
.FirstOrDefaultAsync(cancellationToken: ct);
|
||||||
|
|
||||||
if (apiKey == null)
|
Console.WriteLine($"API: {x?.ValidUntil}");
|
||||||
return AuthenticateResult.Fail("Invalid api key specified");
|
|
||||||
|
|
||||||
MemoryCache.Set(cacheKey, apiKey, Options.LookupCacheTime);
|
return x;
|
||||||
}
|
},
|
||||||
else
|
new HybridCacheEntryOptions()
|
||||||
{
|
{
|
||||||
if (apiKey == null)
|
LocalCacheExpiration = Options.LookupL1CacheTime,
|
||||||
return AuthenticateResult.Fail("Invalid api key specified");
|
Expiration = Options.LookupL2CacheTime
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (apiKey == null)
|
||||||
|
return AuthenticateResult.Fail("Invalid api key specified");
|
||||||
|
|
||||||
if (DateTimeOffset.UtcNow > apiKey.ValidUntil)
|
if (DateTimeOffset.UtcNow > apiKey.ValidUntil)
|
||||||
return AuthenticateResult.Fail("Api key expired");
|
return AuthenticateResult.Fail("Api key expired");
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ namespace Moonlight.Api.Implementations.ApiKeyScheme;
|
|||||||
|
|
||||||
public class ApiKeySchemeOptions : AuthenticationSchemeOptions
|
public class ApiKeySchemeOptions : AuthenticationSchemeOptions
|
||||||
{
|
{
|
||||||
public TimeSpan LookupCacheTime { get; set; }
|
public TimeSpan LookupL1CacheTime { get; set; }
|
||||||
|
public TimeSpan LookupL2CacheTime { get; set; }
|
||||||
}
|
}
|
||||||
@@ -26,6 +26,8 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.1"/>
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.1"/>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1"/>
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1"/>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="10.3.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.3" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0"/>
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0"/>
|
||||||
<PackageReference Include="Riok.Mapperly" Version="4.3.1-next.0"/>
|
<PackageReference Include="Riok.Mapperly" Version="4.3.1-next.0"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Hybrid;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Moonlight.Api.Configuration;
|
using Moonlight.Api.Configuration;
|
||||||
using Moonlight.Api.Database;
|
using Moonlight.Api.Database;
|
||||||
@@ -12,18 +12,18 @@ public class SettingsService
|
|||||||
{
|
{
|
||||||
private readonly DatabaseRepository<SettingsOption> Repository;
|
private readonly DatabaseRepository<SettingsOption> Repository;
|
||||||
private readonly IOptions<SettingsOptions> Options;
|
private readonly IOptions<SettingsOptions> Options;
|
||||||
private readonly IMemoryCache Cache;
|
private readonly HybridCache HybridCache;
|
||||||
|
|
||||||
private const string CacheKey = "Moonlight.Api.SettingsService.{0}";
|
private const string CacheKey = "Moonlight.Api.SettingsService.{0}";
|
||||||
|
|
||||||
public SettingsService(
|
public SettingsService(
|
||||||
DatabaseRepository<SettingsOption> repository,
|
DatabaseRepository<SettingsOption> repository,
|
||||||
IOptions<SettingsOptions> options,
|
IOptions<SettingsOptions> options,
|
||||||
IMemoryCache cache
|
HybridCache hybridCache
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Repository = repository;
|
Repository = repository;
|
||||||
Cache = cache;
|
HybridCache = hybridCache;
|
||||||
Options = options;
|
Options = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,24 +31,26 @@ public class SettingsService
|
|||||||
{
|
{
|
||||||
var cacheKey = string.Format(CacheKey, key);
|
var cacheKey = string.Format(CacheKey, key);
|
||||||
|
|
||||||
if (Cache.TryGetValue<string>(cacheKey, out var value))
|
var value = await HybridCache.GetOrCreateAsync<string?>(
|
||||||
return JsonSerializer.Deserialize<T>(value!);
|
|
||||||
|
|
||||||
value = await Repository
|
|
||||||
.Query()
|
|
||||||
.Where(x => x.Key == key)
|
|
||||||
.Select(o => o.ValueJson)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
|
|
||||||
if(string.IsNullOrEmpty(value))
|
|
||||||
return default;
|
|
||||||
|
|
||||||
Cache.Set(
|
|
||||||
cacheKey,
|
cacheKey,
|
||||||
value,
|
async ct =>
|
||||||
TimeSpan.FromMinutes(Options.Value.CacheMinutes)
|
{
|
||||||
|
return await Repository
|
||||||
|
.Query()
|
||||||
|
.Where(x => x.Key == key)
|
||||||
|
.Select(o => o.ValueJson)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken: ct);
|
||||||
|
},
|
||||||
|
new HybridCacheEntryOptions()
|
||||||
|
{
|
||||||
|
LocalCacheExpiration = Options.Value.LookupL1CacheTime,
|
||||||
|
Expiration = Options.Value.LookupL2CacheTime
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
return default;
|
||||||
|
|
||||||
return JsonSerializer.Deserialize<T>(value);
|
return JsonSerializer.Deserialize<T>(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,6 +80,6 @@ public class SettingsService
|
|||||||
await Repository.AddAsync(option);
|
await Repository.AddAsync(option);
|
||||||
}
|
}
|
||||||
|
|
||||||
Cache.Remove(cacheKey);
|
await HybridCache.RemoveAsync(cacheKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Hybrid;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Moonlight.Api.Configuration;
|
using Moonlight.Api.Configuration;
|
||||||
@@ -14,10 +14,10 @@ namespace Moonlight.Api.Services;
|
|||||||
public class UserAuthService
|
public class UserAuthService
|
||||||
{
|
{
|
||||||
private readonly DatabaseRepository<User> UserRepository;
|
private readonly DatabaseRepository<User> UserRepository;
|
||||||
private readonly IMemoryCache Cache;
|
|
||||||
private readonly ILogger<UserAuthService> Logger;
|
private readonly ILogger<UserAuthService> Logger;
|
||||||
private readonly IOptions<SessionOptions> Options;
|
private readonly IOptions<UserOptions> Options;
|
||||||
private readonly IEnumerable<IUserAuthHook> Hooks;
|
private readonly IEnumerable<IUserAuthHook> Hooks;
|
||||||
|
private readonly HybridCache HybridCache;
|
||||||
|
|
||||||
private const string UserIdClaim = "UserId";
|
private const string UserIdClaim = "UserId";
|
||||||
private const string IssuedAtClaim = "IssuedAt";
|
private const string IssuedAtClaim = "IssuedAt";
|
||||||
@@ -27,15 +27,16 @@ public class UserAuthService
|
|||||||
public UserAuthService(
|
public UserAuthService(
|
||||||
DatabaseRepository<User> userRepository,
|
DatabaseRepository<User> userRepository,
|
||||||
ILogger<UserAuthService> logger,
|
ILogger<UserAuthService> logger,
|
||||||
IMemoryCache cache, IOptions<SessionOptions> options,
|
IOptions<UserOptions> options,
|
||||||
IEnumerable<IUserAuthHook> hooks
|
IEnumerable<IUserAuthHook> hooks,
|
||||||
|
HybridCache hybridCache
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
UserRepository = userRepository;
|
UserRepository = userRepository;
|
||||||
Logger = logger;
|
Logger = logger;
|
||||||
Cache = cache;
|
|
||||||
Options = options;
|
Options = options;
|
||||||
Hooks = hooks;
|
Hooks = hooks;
|
||||||
|
HybridCache = hybridCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> SyncAsync(ClaimsPrincipal? principal)
|
public async Task<bool> SyncAsync(ClaimsPrincipal? principal)
|
||||||
@@ -80,8 +81,8 @@ public class UserAuthService
|
|||||||
|
|
||||||
foreach (var hook in Hooks)
|
foreach (var hook in Hooks)
|
||||||
{
|
{
|
||||||
// Run every hook and if any returns false we return false as well
|
// Run every hook, and if any returns false, we return false as well
|
||||||
if(!await hook.SyncAsync(principal, user))
|
if (!await hook.SyncAsync(principal, user))
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,32 +102,29 @@ public class UserAuthService
|
|||||||
|
|
||||||
var cacheKey = string.Format(CacheKeyPattern, userId);
|
var cacheKey = string.Format(CacheKeyPattern, userId);
|
||||||
|
|
||||||
if (!Cache.TryGetValue<UserSession>(cacheKey, out var user))
|
var user = await HybridCache.GetOrCreateAsync<UserSession?>(
|
||||||
{
|
cacheKey,
|
||||||
user = await UserRepository
|
async ct =>
|
||||||
.Query()
|
{
|
||||||
.AsNoTracking()
|
return await UserRepository
|
||||||
.Where(u => u.Id == userId)
|
.Query()
|
||||||
.Select(u => new UserSession(
|
.AsNoTracking()
|
||||||
u.InvalidateTimestamp,
|
.Where(u => u.Id == userId)
|
||||||
u.RoleMemberships.SelectMany(x => x.Role.Permissions).ToArray())
|
.Select(u => new UserSession(
|
||||||
)
|
u.InvalidateTimestamp,
|
||||||
.FirstOrDefaultAsync();
|
u.RoleMemberships.SelectMany(x => x.Role.Permissions).ToArray())
|
||||||
|
)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken: ct);
|
||||||
|
},
|
||||||
|
new HybridCacheEntryOptions()
|
||||||
|
{
|
||||||
|
LocalCacheExpiration = Options.Value.ValidationCacheL1Expiry,
|
||||||
|
Expiration = Options.Value.ValidationCacheL2Expiry
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (user == null)
|
if (user == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
Cache.Set(
|
|
||||||
cacheKey,
|
|
||||||
user,
|
|
||||||
TimeSpan.FromMinutes(Options.Value.ValidationCacheMinutes)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (user == null)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var issuedAtString = principal.FindFirstValue(IssuedAtClaim);
|
var issuedAtString = principal.FindFirstValue(IssuedAtClaim);
|
||||||
|
|
||||||
@@ -149,8 +147,8 @@ public class UserAuthService
|
|||||||
|
|
||||||
foreach (var hook in Hooks)
|
foreach (var hook in Hooks)
|
||||||
{
|
{
|
||||||
// Run every hook and if any returns false we return false as well
|
// Run every hook, and if any returns false we return false as well
|
||||||
if(!await hook.ValidateAsync(principal, userId))
|
if (!await hook.ValidateAsync(principal, userId))
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Hybrid;
|
||||||
using Moonlight.Api.Database;
|
using Moonlight.Api.Database;
|
||||||
using Moonlight.Api.Database.Entities;
|
using Moonlight.Api.Database.Entities;
|
||||||
using Moonlight.Api.Interfaces;
|
using Moonlight.Api.Interfaces;
|
||||||
@@ -10,13 +10,17 @@ public class UserDeletionService
|
|||||||
{
|
{
|
||||||
private readonly DatabaseRepository<User> Repository;
|
private readonly DatabaseRepository<User> Repository;
|
||||||
private readonly IEnumerable<IUserDeletionHook> Hooks;
|
private readonly IEnumerable<IUserDeletionHook> Hooks;
|
||||||
private readonly IMemoryCache Cache;
|
private readonly HybridCache HybridCache;
|
||||||
|
|
||||||
public UserDeletionService(DatabaseRepository<User> repository, IEnumerable<IUserDeletionHook> hooks, IMemoryCache cache)
|
public UserDeletionService(
|
||||||
|
DatabaseRepository<User> repository,
|
||||||
|
IEnumerable<IUserDeletionHook> hooks,
|
||||||
|
HybridCache hybridCache
|
||||||
|
)
|
||||||
{
|
{
|
||||||
Repository = repository;
|
Repository = repository;
|
||||||
Hooks = hooks;
|
Hooks = hooks;
|
||||||
Cache = cache;
|
HybridCache = hybridCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<UserDeletionValidationResult> ValidateAsync(int userId)
|
public async Task<UserDeletionValidationResult> ValidateAsync(int userId)
|
||||||
@@ -54,7 +58,8 @@ public class UserDeletionService
|
|||||||
await hook.ExecuteAsync(user);
|
await hook.ExecuteAsync(user);
|
||||||
|
|
||||||
await Repository.RemoveAsync(user);
|
await Repository.RemoveAsync(user);
|
||||||
Cache.Remove(string.Format(UserAuthService.CacheKeyPattern, userId));
|
|
||||||
|
await HybridCache.RemoveAsync(string.Format(UserAuthService.CacheKeyPattern, user.Id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Hybrid;
|
||||||
using Moonlight.Api.Database;
|
using Moonlight.Api.Database;
|
||||||
using Moonlight.Api.Database.Entities;
|
using Moonlight.Api.Database.Entities;
|
||||||
using Moonlight.Api.Interfaces;
|
using Moonlight.Api.Interfaces;
|
||||||
@@ -10,17 +10,17 @@ public class UserLogoutService
|
|||||||
{
|
{
|
||||||
private readonly DatabaseRepository<User> Repository;
|
private readonly DatabaseRepository<User> Repository;
|
||||||
private readonly IEnumerable<IUserLogoutHook> Hooks;
|
private readonly IEnumerable<IUserLogoutHook> Hooks;
|
||||||
private readonly IMemoryCache Cache;
|
private readonly HybridCache HybridCache;
|
||||||
|
|
||||||
public UserLogoutService(
|
public UserLogoutService(
|
||||||
DatabaseRepository<User> repository,
|
DatabaseRepository<User> repository,
|
||||||
IEnumerable<IUserLogoutHook> hooks,
|
IEnumerable<IUserLogoutHook> hooks,
|
||||||
IMemoryCache cache
|
HybridCache hybridCache
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Repository = repository;
|
Repository = repository;
|
||||||
Hooks = hooks;
|
Hooks = hooks;
|
||||||
Cache = cache;
|
HybridCache = hybridCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LogoutAsync(int userId)
|
public async Task LogoutAsync(int userId)
|
||||||
@@ -29,7 +29,7 @@ public class UserLogoutService
|
|||||||
.Query()
|
.Query()
|
||||||
.FirstOrDefaultAsync(x => x.Id == userId);
|
.FirstOrDefaultAsync(x => x.Id == userId);
|
||||||
|
|
||||||
if(user == null)
|
if (user == null)
|
||||||
throw new AggregateException($"User with id {userId} not found");
|
throw new AggregateException($"User with id {userId} not found");
|
||||||
|
|
||||||
foreach (var hook in Hooks)
|
foreach (var hook in Hooks)
|
||||||
@@ -38,6 +38,6 @@ public class UserLogoutService
|
|||||||
user.InvalidateTimestamp = DateTimeOffset.UtcNow;
|
user.InvalidateTimestamp = DateTimeOffset.UtcNow;
|
||||||
await Repository.UpdateAsync(user);
|
await Repository.UpdateAsync(user);
|
||||||
|
|
||||||
Cache.Remove(string.Format(UserAuthService.CacheKeyPattern, userId));
|
await HybridCache.RemoveAsync(string.Format(UserAuthService.CacheKeyPattern, user.Id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,20 +17,26 @@ public partial class Startup
|
|||||||
{
|
{
|
||||||
private static void AddAuth(WebApplicationBuilder builder)
|
private static void AddAuth(WebApplicationBuilder builder)
|
||||||
{
|
{
|
||||||
|
// OIDC
|
||||||
var oidcOptions = new OidcOptions();
|
var oidcOptions = new OidcOptions();
|
||||||
builder.Configuration.GetSection("Moonlight:Oidc").Bind(oidcOptions);
|
builder.Configuration.GetSection("Moonlight:Oidc").Bind(oidcOptions);
|
||||||
|
|
||||||
|
// API Key
|
||||||
var apiKeyOptions = new ApiOptions();
|
var apiKeyOptions = new ApiOptions();
|
||||||
builder.Configuration.GetSection("Moonlight:Api").Bind(apiKeyOptions);
|
builder.Configuration.GetSection("Moonlight:Api").Bind(apiKeyOptions);
|
||||||
builder.Services.AddOptions<ApiOptions>().BindConfiguration("Moonlight:Api");
|
builder.Services.AddOptions<ApiOptions>().BindConfiguration("Moonlight:Api");
|
||||||
|
|
||||||
builder.Services.AddScoped<UserAuthService>();
|
// Session
|
||||||
|
builder.Services.AddOptions<UserOptions>().BindConfiguration("Moonlight:User");
|
||||||
|
|
||||||
|
// Authentication
|
||||||
builder.Services.AddAuthentication("Main")
|
builder.Services.AddAuthentication("Main")
|
||||||
.AddPolicyScheme("Main", null, options =>
|
.AddPolicyScheme("Main", null,
|
||||||
{
|
options =>
|
||||||
options.ForwardDefaultSelector += context => context.Request.Headers.Authorization.Count > 0 ? "ApiKey" : "Session";
|
{
|
||||||
})
|
options.ForwardDefaultSelector += context =>
|
||||||
|
context.Request.Headers.Authorization.Count > 0 ? "ApiKey" : "Session";
|
||||||
|
})
|
||||||
.AddCookie("Session", null, options =>
|
.AddCookie("Session", null, options =>
|
||||||
{
|
{
|
||||||
options.Events.OnSigningIn += async context =>
|
options.Events.OnSigningIn += async context =>
|
||||||
@@ -97,18 +103,26 @@ public partial class Startup
|
|||||||
|
|
||||||
options.GetClaimsFromUserInfoEndpoint = true;
|
options.GetClaimsFromUserInfoEndpoint = true;
|
||||||
})
|
})
|
||||||
.AddScheme<ApiKeySchemeOptions, ApiKeySchemeHandler>("ApiKey", null, options =>
|
.AddScheme<ApiKeySchemeOptions, ApiKeySchemeHandler>("ApiKey", null,
|
||||||
{
|
options =>
|
||||||
options.LookupCacheTime = TimeSpan.FromMinutes(apiKeyOptions.LookupCacheMinutes);
|
{
|
||||||
});
|
options.LookupL1CacheTime = apiKeyOptions.LookupCacheL1Expiry;
|
||||||
|
options.LookupL2CacheTime = apiKeyOptions.LookupCacheL2Expiry;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Authorization
|
||||||
|
builder.Services.AddAuthorization();
|
||||||
|
|
||||||
|
// Reduce log noise
|
||||||
builder.Logging.AddFilter("Moonlight.Api.Implementations.ApiKeyScheme.ApiKeySchemeHandler", LogLevel.Warning);
|
builder.Logging.AddFilter("Moonlight.Api.Implementations.ApiKeyScheme.ApiKeySchemeHandler", LogLevel.Warning);
|
||||||
|
|
||||||
|
// Custom permission handling using named policies
|
||||||
builder.Services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();
|
builder.Services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();
|
||||||
builder.Services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
|
builder.Services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
|
||||||
|
|
||||||
builder.Services.AddOptions<SettingsOptions>().BindConfiguration("Moonlight:Settings");
|
builder.Services.AddScoped<UserDeletionService>();
|
||||||
builder.Services.AddScoped<SettingsService>();
|
builder.Services.AddScoped<UserLogoutService>();
|
||||||
|
builder.Services.AddScoped<UserAuthService>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void UseAuth(WebApplication application)
|
private static void UseAuth(WebApplication application)
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Console;
|
using Microsoft.Extensions.Logging.Console;
|
||||||
@@ -9,7 +11,6 @@ using Moonlight.Api.Helpers;
|
|||||||
using Moonlight.Api.Implementations;
|
using Moonlight.Api.Implementations;
|
||||||
using Moonlight.Api.Interfaces;
|
using Moonlight.Api.Interfaces;
|
||||||
using Moonlight.Api.Services;
|
using Moonlight.Api.Services;
|
||||||
using SessionOptions = Moonlight.Api.Configuration.SessionOptions;
|
|
||||||
|
|
||||||
namespace Moonlight.Api.Startup;
|
namespace Moonlight.Api.Startup;
|
||||||
|
|
||||||
@@ -17,44 +18,63 @@ public partial class Startup
|
|||||||
{
|
{
|
||||||
private static void AddBase(WebApplicationBuilder builder)
|
private static void AddBase(WebApplicationBuilder builder)
|
||||||
{
|
{
|
||||||
|
// Create the base directory
|
||||||
|
Directory.CreateDirectory("storage");
|
||||||
|
|
||||||
|
// Hook up source-generated serialization
|
||||||
builder.Services.AddControllers().AddJsonOptions(options =>
|
builder.Services.AddControllers().AddJsonOptions(options =>
|
||||||
{
|
{
|
||||||
options.JsonSerializerOptions.TypeInfoResolverChain.Add(SerializationContext.Default);
|
options.JsonSerializerOptions.TypeInfoResolverChain.Add(SerializationContext.Default);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Configure logging
|
||||||
builder.Logging.ClearProviders();
|
builder.Logging.ClearProviders();
|
||||||
builder.Logging.AddConsole(options => { options.FormatterName = nameof(AppConsoleFormatter); });
|
builder.Logging.AddConsole(options => { options.FormatterName = nameof(AppConsoleFormatter); });
|
||||||
builder.Logging.AddConsoleFormatter<AppConsoleFormatter, ConsoleFormatterOptions>();
|
builder.Logging.AddConsoleFormatter<AppConsoleFormatter, ConsoleFormatterOptions>();
|
||||||
|
|
||||||
|
// Application service
|
||||||
builder.Services.AddSingleton<ApplicationService>();
|
builder.Services.AddSingleton<ApplicationService>();
|
||||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<ApplicationService>());
|
builder.Services.AddHostedService(sp => sp.GetRequiredService<ApplicationService>());
|
||||||
|
|
||||||
|
// Diagnose
|
||||||
builder.Services.AddSingleton<DiagnoseService>();
|
builder.Services.AddSingleton<DiagnoseService>();
|
||||||
|
|
||||||
builder.Services.AddSingleton<IDiagnoseProvider, UpdateDiagnoseProvider>();
|
builder.Services.AddSingleton<IDiagnoseProvider, UpdateDiagnoseProvider>();
|
||||||
|
|
||||||
builder.Services.AddMemoryCache();
|
// Frontend
|
||||||
builder.Services.AddOptions<SessionOptions>().BindConfiguration("Moonlight:Session");
|
|
||||||
|
|
||||||
builder.Services.AddOptions<FrontendOptions>().BindConfiguration("Moonlight:Frontend");
|
builder.Services.AddOptions<FrontendOptions>().BindConfiguration("Moonlight:Frontend");
|
||||||
builder.Services.AddScoped<FrontendService>();
|
builder.Services.AddScoped<FrontendService>();
|
||||||
|
|
||||||
|
// HTTP Client
|
||||||
builder.Services.AddHttpClient();
|
builder.Services.AddHttpClient();
|
||||||
|
|
||||||
|
// Version fetching configuration
|
||||||
builder.Services.AddOptions<VersionOptions>().BindConfiguration("Moonlight:Version");
|
builder.Services.AddOptions<VersionOptions>().BindConfiguration("Moonlight:Version");
|
||||||
builder.Services.AddSingleton<VersionService>();
|
builder.Services.AddSingleton<VersionService>();
|
||||||
|
|
||||||
|
// Container Helper Options
|
||||||
|
builder.Configuration.GetSection("Moonlight:ContainerHelper").Bind(builder.Configuration);
|
||||||
|
|
||||||
builder.Services.AddOptions<ContainerHelperOptions>().BindConfiguration("Moonlight:ContainerHelper");
|
builder.Services.AddOptions<ContainerHelperOptions>().BindConfiguration("Moonlight:ContainerHelper");
|
||||||
builder.Services.AddSingleton<ContainerHelperService>();
|
builder.Services.AddSingleton<ContainerHelperService>();
|
||||||
|
|
||||||
builder.Services.AddHttpClient("ContainerHelper", (provider, client) =>
|
builder.Services.AddHttpClient("ContainerHelper", (provider, client) =>
|
||||||
{
|
{
|
||||||
var options = provider.GetRequiredService<IOptions<ContainerHelperOptions>>();
|
var options = provider.GetRequiredService<IOptions<ContainerHelperOptions>>();
|
||||||
client.BaseAddress = new Uri(options.Value.IsEnabled ? options.Value.Url : "http://you-should-fail.invalid");
|
client.BaseAddress =
|
||||||
|
new Uri(options.Value.IsEnabled ? options.Value.Url : "http://you-should-fail.invalid");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// User management services
|
||||||
builder.Services.AddScoped<UserDeletionService>();
|
builder.Services.AddScoped<UserDeletionService>();
|
||||||
builder.Services.AddScoped<UserLogoutService>();
|
builder.Services.AddScoped<UserLogoutService>();
|
||||||
|
|
||||||
|
// Settings options
|
||||||
|
builder.Services.AddOptions<SettingsOptions>().BindConfiguration("Moonlight:Settings");
|
||||||
|
builder.Services.AddScoped<SettingsService>();
|
||||||
|
|
||||||
|
// Setup key loading
|
||||||
|
var keysDirectory = new DirectoryInfo(Path.Combine("storage", "keys"));
|
||||||
|
builder.Services.AddDataProtection().PersistKeysToFileSystem(keysDirectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void UseBase(WebApplication application)
|
private static void UseBase(WebApplication application)
|
||||||
@@ -68,7 +88,7 @@ public partial class Startup
|
|||||||
|
|
||||||
var options = application.Services.GetRequiredService<IOptions<FrontendOptions>>();
|
var options = application.Services.GetRequiredService<IOptions<FrontendOptions>>();
|
||||||
|
|
||||||
if(options.Value.Enabled)
|
if (options.Value.Enabled)
|
||||||
application.MapFallbackToFile("index.html");
|
application.MapFallbackToFile("index.html");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
34
Moonlight.Api/Startup/Startup.Cache.cs
Normal file
34
Moonlight.Api/Startup/Startup.Cache.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Moonlight.Api.Configuration;
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Startup;
|
||||||
|
|
||||||
|
public partial class Startup
|
||||||
|
{
|
||||||
|
private static void AddCache(WebApplicationBuilder builder)
|
||||||
|
{
|
||||||
|
// Load cache options
|
||||||
|
var cacheOptions = new CacheOptions();
|
||||||
|
builder.Configuration.GetSection("Moonlight:Cache").Bind(cacheOptions);
|
||||||
|
|
||||||
|
builder.Services.AddMemoryCache();
|
||||||
|
builder.Services.AddHybridCache();
|
||||||
|
|
||||||
|
if (!cacheOptions.EnableLayer2)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var redisOptions = new RedisOptions();
|
||||||
|
builder.Configuration.GetSection("Moonlight:Redis").Bind(redisOptions);
|
||||||
|
|
||||||
|
if(!redisOptions.Enable)
|
||||||
|
return;
|
||||||
|
|
||||||
|
builder.Services.AddStackExchangeRedisCache(options =>
|
||||||
|
{
|
||||||
|
options.Configuration = redisOptions.ConnectionString;
|
||||||
|
options.InstanceName = "Moonlight:";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ public partial class Startup : IAppStartup
|
|||||||
AddBase(builder);
|
AddBase(builder);
|
||||||
AddAuth(builder);
|
AddAuth(builder);
|
||||||
AddDatabase(builder);
|
AddDatabase(builder);
|
||||||
|
AddCache(builder);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void PostBuild(WebApplication application)
|
public void PostBuild(WebApplication application)
|
||||||
|
|||||||
Reference in New Issue
Block a user