Implemented user logout and deletion service. Added Auth, Deletion and Logout hook. Restructed controllers

This commit is contained in:
2026-02-01 16:26:11 +01:00
parent 4daf986f3e
commit ac1c28d20d
11 changed files with 260 additions and 67 deletions

View File

@@ -6,6 +6,7 @@ using Microsoft.Extensions.Options;
using Moonlight.Api.Configuration;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Api.Interfaces;
using Moonlight.Shared;
namespace Moonlight.Api.Services;
@@ -16,21 +17,25 @@ public class UserAuthService
private readonly IMemoryCache Cache;
private readonly ILogger<UserAuthService> Logger;
private readonly IOptions<SessionOptions> Options;
private readonly IEnumerable<IUserAuthHook> Hooks;
private const string UserIdClaim = "UserId";
private const string IssuedAtClaim = "IssuedAt";
public const string CacheKeyPattern = $"Moonlight.{nameof(UserAuthService)}.{nameof(ValidateAsync)}-{{0}}";
public UserAuthService(
DatabaseRepository<User> userRepository,
ILogger<UserAuthService> logger,
IMemoryCache cache, IOptions<SessionOptions> options)
IMemoryCache cache, IOptions<SessionOptions> options,
IEnumerable<IUserAuthHook> hooks
)
{
UserRepository = userRepository;
Logger = logger;
Cache = cache;
Options = options;
Hooks = hooks;
}
public async Task<bool> SyncAsync(ClaimsPrincipal? principal)
@@ -50,7 +55,6 @@ public class UserAuthService
// We use email as the primary identifier here
var user = await UserRepository
.Query()
.AsNoTracking()
.FirstOrDefaultAsync(user => user.Email == email);
if (user == null) // Sync user if not already existing in the database
@@ -74,6 +78,13 @@ public class UserAuthService
new Claim(IssuedAtClaim, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString())
]);
foreach (var hook in Hooks)
{
// Run every hook and if any returns false we return false as well
if(!await hook.SyncAsync(principal, user))
return false;
}
return true;
}
@@ -89,7 +100,7 @@ public class UserAuthService
return false;
var cacheKey = string.Format(CacheKeyPattern, userId);
if (!Cache.TryGetValue<UserSession>(cacheKey, out var user))
{
user = await UserRepository
@@ -131,9 +142,17 @@ public class UserAuthService
if (issuedAt < user.InvalidateTimestamp)
return false;
// Load every permission as claim
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))
return false;
}
return true;
}

View File

@@ -0,0 +1,61 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Api.Interfaces;
namespace Moonlight.Api.Services;
public class UserDeletionService
{
private readonly DatabaseRepository<User> Repository;
private readonly IEnumerable<IUserDeletionHook> Hooks;
private readonly IMemoryCache Cache;
public UserDeletionService(DatabaseRepository<User> repository, IEnumerable<IUserDeletionHook> hooks, IMemoryCache cache)
{
Repository = repository;
Hooks = hooks;
Cache = cache;
}
public async Task<UserDeletionValidationResult> ValidateAsync(int userId)
{
var user = await Repository
.Query()
.FirstOrDefaultAsync(x => x.Id == userId);
if(user == null)
throw new AggregateException($"User with id {userId} not found");
var errorMessages = new List<string>();
foreach (var hook in Hooks)
{
if (await hook.ValidateAsync(user, errorMessages))
continue;
return new UserDeletionValidationResult(false, errorMessages);
}
return new UserDeletionValidationResult(true, []);
}
public async Task DeleteAsync(int userId)
{
var user = await Repository
.Query()
.FirstOrDefaultAsync(x => x.Id == userId);
if(user == null)
throw new AggregateException($"User with id {userId} not found");
foreach (var hook in Hooks)
await hook.ExecuteAsync(user);
await Repository.RemoveAsync(user);
Cache.Remove(string.Format(UserAuthService.CacheKeyPattern, userId));
}
}
public record UserDeletionValidationResult(bool IsValid, IEnumerable<string> ErrorMessages);

View File

@@ -0,0 +1,43 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities;
using Moonlight.Api.Interfaces;
namespace Moonlight.Api.Services;
public class UserLogoutService
{
private readonly DatabaseRepository<User> Repository;
private readonly IEnumerable<IUserLogoutHook> Hooks;
private readonly IMemoryCache Cache;
public UserLogoutService(
DatabaseRepository<User> repository,
IEnumerable<IUserLogoutHook> hooks,
IMemoryCache cache
)
{
Repository = repository;
Hooks = hooks;
Cache = cache;
}
public async Task LogoutAsync(int userId)
{
var user = await Repository
.Query()
.FirstOrDefaultAsync(x => x.Id == userId);
if(user == null)
throw new AggregateException($"User with id {userId} not found");
foreach (var hook in Hooks)
await hook.ExecuteAsync(user);
user.InvalidateTimestamp = DateTimeOffset.UtcNow;
await Repository.UpdateAsync(user);
Cache.Remove(string.Format(UserAuthService.CacheKeyPattern, userId));
}
}