Compare commits
2 Commits
4daf986f3e
...
6d854d82d3
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d854d82d3 | |||
| ac1c28d20d |
@@ -1,45 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
using Moonlight.Api.Database;
|
|
||||||
using Moonlight.Api.Database.Entities;
|
|
||||||
using Moonlight.Api.Services;
|
|
||||||
using Moonlight.Shared;
|
|
||||||
|
|
||||||
namespace Moonlight.Api.Http.Controllers.Admin;
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/admin/users/{id:int}")]
|
|
||||||
public class UserActionsController : Controller
|
|
||||||
{
|
|
||||||
// Consider building a service for deletion and logout or actions in general
|
|
||||||
|
|
||||||
private readonly DatabaseRepository<User> UsersRepository;
|
|
||||||
private readonly IMemoryCache Cache;
|
|
||||||
|
|
||||||
public UserActionsController(DatabaseRepository<User> usersRepository, IMemoryCache cache)
|
|
||||||
{
|
|
||||||
UsersRepository = usersRepository;
|
|
||||||
Cache = cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("logout")]
|
|
||||||
[Authorize(Policy = Permissions.Users.Logout)]
|
|
||||||
public async Task<ActionResult> LogoutAsync([FromRoute] int id)
|
|
||||||
{
|
|
||||||
var user = await UsersRepository
|
|
||||||
.Query()
|
|
||||||
.FirstOrDefaultAsync(u => u.Id == id);
|
|
||||||
|
|
||||||
if(user == null)
|
|
||||||
return Problem("User not found", statusCode: 404);
|
|
||||||
|
|
||||||
user.InvalidateTimestamp = DateTimeOffset.UtcNow;
|
|
||||||
await UsersRepository.UpdateAsync(user);
|
|
||||||
|
|
||||||
Cache.Remove(string.Format(UserAuthService.CacheKeyPattern, id));
|
|
||||||
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using System.Collections.Frozen;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Moonlight.Api.Database;
|
||||||
|
using Moonlight.Api.Database.Entities;
|
||||||
|
using Moonlight.Api.Services;
|
||||||
|
using Moonlight.Shared;
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Http.Controllers.Admin.Users;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/admin/users")]
|
||||||
|
[Authorize(Policy = Permissions.Users.Delete)]
|
||||||
|
public class UserDeletionController : Controller
|
||||||
|
{
|
||||||
|
private readonly UserDeletionService UserDeletionService;
|
||||||
|
private readonly DatabaseRepository<User> Repository;
|
||||||
|
|
||||||
|
public UserDeletionController(UserDeletionService userDeletionService, DatabaseRepository<User> repository)
|
||||||
|
{
|
||||||
|
UserDeletionService = userDeletionService;
|
||||||
|
Repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{id:int}")]
|
||||||
|
public async Task<ActionResult> DeleteAsync([FromRoute] int id)
|
||||||
|
{
|
||||||
|
var userExists = await Repository
|
||||||
|
.Query()
|
||||||
|
.AnyAsync(user => user.Id == id);
|
||||||
|
|
||||||
|
if (!userExists)
|
||||||
|
return Problem("No user with this id found", statusCode: 404);
|
||||||
|
|
||||||
|
var validationResult = await UserDeletionService.ValidateAsync(id);
|
||||||
|
|
||||||
|
if (!validationResult.IsValid)
|
||||||
|
{
|
||||||
|
return ValidationProblem(
|
||||||
|
new ValidationProblemDetails(
|
||||||
|
new Dictionary<string, string[]>()
|
||||||
|
{
|
||||||
|
{
|
||||||
|
string.Empty,
|
||||||
|
validationResult.ErrorMessages.ToArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await UserDeletionService.DeleteAsync(id);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Moonlight.Api.Database;
|
||||||
|
using Moonlight.Api.Database.Entities;
|
||||||
|
using Moonlight.Api.Services;
|
||||||
|
using Moonlight.Shared;
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Http.Controllers.Admin.Users;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/admin/users/{id:int}/logout")]
|
||||||
|
[Authorize(Policy = Permissions.Users.Logout)]
|
||||||
|
public class UserLogoutController : Controller
|
||||||
|
{
|
||||||
|
private readonly UserLogoutService LogoutService;
|
||||||
|
private readonly DatabaseRepository<User> Repository;
|
||||||
|
|
||||||
|
public UserLogoutController(
|
||||||
|
UserLogoutService logoutService,
|
||||||
|
DatabaseRepository<User> repository
|
||||||
|
)
|
||||||
|
{
|
||||||
|
LogoutService = logoutService;
|
||||||
|
Repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<ActionResult> LogoutAsync([FromRoute] int id)
|
||||||
|
{
|
||||||
|
var userExists = await Repository
|
||||||
|
.Query()
|
||||||
|
.AnyAsync(user => user.Id == id);
|
||||||
|
|
||||||
|
if (!userExists)
|
||||||
|
return Problem("No user with this id found", statusCode: 404);
|
||||||
|
|
||||||
|
await LogoutService.LogoutAsync(id);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ using Moonlight.Shared.Http.Requests.Admin.Users;
|
|||||||
using Moonlight.Shared.Http.Responses;
|
using Moonlight.Shared.Http.Responses;
|
||||||
using Moonlight.Shared.Http.Responses.Admin.Users;
|
using Moonlight.Shared.Http.Responses.Admin.Users;
|
||||||
|
|
||||||
namespace Moonlight.Api.Http.Controllers.Admin;
|
namespace Moonlight.Api.Http.Controllers.Admin.Users;
|
||||||
|
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
@@ -117,19 +117,4 @@ public class UsersController : Controller
|
|||||||
|
|
||||||
return UserMapper.ToDto(user);
|
return UserMapper.ToDto(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{id:int}")]
|
|
||||||
[Authorize(Policy = Permissions.Users.Delete)]
|
|
||||||
public async Task<ActionResult> DeleteAsync([FromRoute] int id)
|
|
||||||
{
|
|
||||||
var user = await UserRepository
|
|
||||||
.Query()
|
|
||||||
.FirstOrDefaultAsync(user => user.Id == id);
|
|
||||||
|
|
||||||
if (user == null)
|
|
||||||
return Problem("No user with this id found", statusCode: 404);
|
|
||||||
|
|
||||||
await UserRepository.RemoveAsync(user);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
13
Moonlight.Api/Interfaces/IUserAuthHook.cs
Normal file
13
Moonlight.Api/Interfaces/IUserAuthHook.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using Moonlight.Api.Database.Entities;
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Interfaces;
|
||||||
|
|
||||||
|
public interface IUserAuthHook
|
||||||
|
{
|
||||||
|
public Task<bool> SyncAsync(ClaimsPrincipal principal, User trackedUser);
|
||||||
|
|
||||||
|
// Every implementation of this function should execute as fast as possible
|
||||||
|
// as this directly impacts every api call
|
||||||
|
public Task<bool> ValidateAsync(ClaimsPrincipal principal, int userId);
|
||||||
|
}
|
||||||
9
Moonlight.Api/Interfaces/IUserDeletionHook.cs
Normal file
9
Moonlight.Api/Interfaces/IUserDeletionHook.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using Moonlight.Api.Database.Entities;
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Interfaces;
|
||||||
|
|
||||||
|
public interface IUserDeletionHook
|
||||||
|
{
|
||||||
|
public Task<bool> ValidateAsync(User user, List<string> errors);
|
||||||
|
public Task ExecuteAsync(User user);
|
||||||
|
}
|
||||||
8
Moonlight.Api/Interfaces/IUserLogoutHook.cs
Normal file
8
Moonlight.Api/Interfaces/IUserLogoutHook.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using Moonlight.Api.Database.Entities;
|
||||||
|
|
||||||
|
namespace Moonlight.Api.Interfaces;
|
||||||
|
|
||||||
|
public interface IUserLogoutHook
|
||||||
|
{
|
||||||
|
public Task ExecuteAsync(User user);
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ using Microsoft.Extensions.Options;
|
|||||||
using Moonlight.Api.Configuration;
|
using Moonlight.Api.Configuration;
|
||||||
using Moonlight.Api.Database;
|
using Moonlight.Api.Database;
|
||||||
using Moonlight.Api.Database.Entities;
|
using Moonlight.Api.Database.Entities;
|
||||||
|
using Moonlight.Api.Interfaces;
|
||||||
using Moonlight.Shared;
|
using Moonlight.Shared;
|
||||||
|
|
||||||
namespace Moonlight.Api.Services;
|
namespace Moonlight.Api.Services;
|
||||||
@@ -16,6 +17,7 @@ public class UserAuthService
|
|||||||
private readonly IMemoryCache Cache;
|
private readonly IMemoryCache Cache;
|
||||||
private readonly ILogger<UserAuthService> Logger;
|
private readonly ILogger<UserAuthService> Logger;
|
||||||
private readonly IOptions<SessionOptions> Options;
|
private readonly IOptions<SessionOptions> Options;
|
||||||
|
private readonly IEnumerable<IUserAuthHook> Hooks;
|
||||||
|
|
||||||
private const string UserIdClaim = "UserId";
|
private const string UserIdClaim = "UserId";
|
||||||
private const string IssuedAtClaim = "IssuedAt";
|
private const string IssuedAtClaim = "IssuedAt";
|
||||||
@@ -25,12 +27,15 @@ public class UserAuthService
|
|||||||
public UserAuthService(
|
public UserAuthService(
|
||||||
DatabaseRepository<User> userRepository,
|
DatabaseRepository<User> userRepository,
|
||||||
ILogger<UserAuthService> logger,
|
ILogger<UserAuthService> logger,
|
||||||
IMemoryCache cache, IOptions<SessionOptions> options)
|
IMemoryCache cache, IOptions<SessionOptions> options,
|
||||||
|
IEnumerable<IUserAuthHook> hooks
|
||||||
|
)
|
||||||
{
|
{
|
||||||
UserRepository = userRepository;
|
UserRepository = userRepository;
|
||||||
Logger = logger;
|
Logger = logger;
|
||||||
Cache = cache;
|
Cache = cache;
|
||||||
Options = options;
|
Options = options;
|
||||||
|
Hooks = hooks;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> SyncAsync(ClaimsPrincipal? principal)
|
public async Task<bool> SyncAsync(ClaimsPrincipal? principal)
|
||||||
@@ -50,7 +55,6 @@ public class UserAuthService
|
|||||||
// We use email as the primary identifier here
|
// We use email as the primary identifier here
|
||||||
var user = await UserRepository
|
var user = await UserRepository
|
||||||
.Query()
|
.Query()
|
||||||
.AsNoTracking()
|
|
||||||
.FirstOrDefaultAsync(user => user.Email == email);
|
.FirstOrDefaultAsync(user => user.Email == email);
|
||||||
|
|
||||||
if (user == null) // Sync user if not already existing in the database
|
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())
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,10 +142,18 @@ public class UserAuthService
|
|||||||
if (issuedAt < user.InvalidateTimestamp)
|
if (issuedAt < user.InvalidateTimestamp)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
// Load every permission as claim
|
||||||
principal.Identities.First().AddClaims(
|
principal.Identities.First().AddClaims(
|
||||||
user.Permissions.Select(x => new Claim(Permissions.ClaimType, x))
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
61
Moonlight.Api/Services/UserDeletionService.cs
Normal file
61
Moonlight.Api/Services/UserDeletionService.cs
Normal 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);
|
||||||
43
Moonlight.Api/Services/UserLogoutService.cs
Normal file
43
Moonlight.Api/Services/UserLogoutService.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ 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 = Microsoft.AspNetCore.Builder.SessionOptions;
|
using SessionOptions = Moonlight.Api.Configuration.SessionOptions;
|
||||||
|
|
||||||
namespace Moonlight.Api.Startup;
|
namespace Moonlight.Api.Startup;
|
||||||
|
|
||||||
@@ -52,6 +52,9 @@ public partial class Startup
|
|||||||
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");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder.Services.AddScoped<UserDeletionService>();
|
||||||
|
builder.Services.AddScoped<UserLogoutService>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void UseBase(WebApplication application)
|
private static void UseBase(WebApplication application)
|
||||||
|
|||||||
@@ -37,10 +37,18 @@
|
|||||||
|
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<DataGrid @ref="Grid" TGridItem="UserDto" Loader="LoadAsync" PageSize="10" ClassName="bg-card">
|
<DataGrid @ref="Grid" TGridItem="UserDto" Loader="LoadAsync" PageSize="10" ClassName="bg-card">
|
||||||
<PropertyColumn HeadClassName="text-left" CellClassName="text-left" Field="u => u.Id"/>
|
<PropertyColumn HeadClassName="text-left" Field="u => u.Id"/>
|
||||||
<PropertyColumn HeadClassName="text-left" CellClassName="text-left" IsFilterable="true"
|
<TemplateColumn IsFilterable="true" Identifier="@nameof(UserDto.Username)" Title="Username">
|
||||||
Identifier="@nameof(UserDto.Username)" Field="u => u.Username"/>
|
<CellTemplate>
|
||||||
<PropertyColumn HeadClassName="text-left" CellClassName="text-left" IsFilterable="true"
|
<TableCell>
|
||||||
|
<a class="text-primary" href="#"
|
||||||
|
@onclick="() => EditAsync(context)" @onclick:preventDefault>
|
||||||
|
@context.Username
|
||||||
|
</a>
|
||||||
|
</TableCell>
|
||||||
|
</CellTemplate>
|
||||||
|
</TemplateColumn>
|
||||||
|
<PropertyColumn HeadClassName="text-left" IsFilterable="true"
|
||||||
Identifier="@nameof(UserDto.Email)" Field="u => u.Email"/>
|
Identifier="@nameof(UserDto.Email)" Field="u => u.Email"/>
|
||||||
<TemplateColumn>
|
<TemplateColumn>
|
||||||
<CellTemplate>
|
<CellTemplate>
|
||||||
|
|||||||
Reference in New Issue
Block a user