1
0
mirror of https://github.com/chylex/Minecraft-Phantom-Panel.git synced 2024-11-25 16:42:54 +01:00

Compare commits

..

2 Commits

Author SHA1 Message Date
e796a364f4
WIP 2023-11-05 14:37:56 +01:00
901dcbf721
Reimplement Web service 2023-11-05 07:30:13 +01:00
39 changed files with 504 additions and 737 deletions

View File

@ -1,14 +1,5 @@
using MemoryPack;
using System.Text.Json;
namespace Phantom.Common.Data.Web.AuditLog;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record AuditLogItem(
[property: MemoryPackOrder(0)] DateTime UtcTime,
[property: MemoryPackOrder(1)] Guid? UserGuid,
[property: MemoryPackOrder(2)] string? UserName,
[property: MemoryPackOrder(3)] AuditLogEventType EventType,
[property: MemoryPackOrder(4)] AuditLogSubjectType SubjectType,
[property: MemoryPackOrder(5)] string? SubjectId,
[property: MemoryPackOrder(6)] string? JsonData
);
public sealed record AuditLogItem(DateTime UtcTime, Guid? UserGuid, string? UserName, AuditLogEventType EventType, AuditLogSubjectType SubjectType, string? SubjectId, JsonDocument? Data);

View File

@ -2,7 +2,6 @@
using Phantom.Common.Data.Java;
using Phantom.Common.Data.Minecraft;
using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Web.AuditLog;
using Phantom.Common.Data.Web.Instance;
using Phantom.Common.Data.Web.Users;
using Phantom.Common.Messages.Web.BiDirectional;
@ -17,11 +16,7 @@ public interface IMessageToControllerListener {
Task<CreateOrUpdateAdministratorUserResult> HandleCreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUserMessage message);
Task<InstanceActionResult<CreateOrUpdateInstanceResult>> HandleCreateOrUpdateInstance(CreateOrUpdateInstanceMessage message);
Task<InstanceActionResult<LaunchInstanceResult>> HandleLaunchInstance(LaunchInstanceMessage message);
Task<InstanceActionResult<StopInstanceResult>> HandleStopInstance(StopInstanceMessage message);
Task<InstanceActionResult<SendCommandToInstanceResult>> HandleSendCommandToInstance(SendCommandToInstanceMessage message);
Task<ImmutableArray<MinecraftVersion>> HandleGetMinecraftVersions(GetMinecraftVersionsMessage message);
Task<ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>> HandleGetAgentJavaRuntimes(GetAgentJavaRuntimes message);
Task<ImmutableArray<UserInfo>> HandleGetUsers(GetUsersMessage message);
Task<ImmutableArray<AuditLogItem>> HandleGetAuditLog(GetAuditLogMessage message);
Task<NoReply> HandleReply(ReplyMessage message);
}

View File

@ -9,4 +9,4 @@ public sealed partial record GetAgentJavaRuntimes : IMessageToController<Immutab
public Task<ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>> Accept(IMessageToControllerListener listener) {
return listener.HandleGetAgentJavaRuntimes(this);
}
}
};

View File

@ -1,14 +0,0 @@
using System.Collections.Immutable;
using MemoryPack;
using Phantom.Common.Data.Web.AuditLog;
namespace Phantom.Common.Messages.Web.ToController;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record GetAuditLogMessage(
[property: MemoryPackOrder(0)] int Count
) : IMessageToController<ImmutableArray<AuditLogItem>> {
public Task<ImmutableArray<AuditLogItem>> Accept(IMessageToControllerListener listener) {
return listener.HandleGetAuditLog(this);
}
}

View File

@ -9,4 +9,4 @@ public sealed partial record GetMinecraftVersionsMessage : IMessageToController<
public Task<ImmutableArray<MinecraftVersion>> Accept(IMessageToControllerListener listener) {
return listener.HandleGetMinecraftVersions(this);
}
}
};

View File

@ -1,12 +0,0 @@
using System.Collections.Immutable;
using MemoryPack;
using Phantom.Common.Data.Web.Users;
namespace Phantom.Common.Messages.Web.ToController;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record GetUsersMessage : IMessageToController<ImmutableArray<UserInfo>> {
public Task<ImmutableArray<UserInfo>> Accept(IMessageToControllerListener listener) {
return listener.HandleGetUsers(this);
}
}

View File

@ -11,4 +11,4 @@ public sealed partial record LogInMessage(
public Task<LogInSuccess?> Accept(IMessageToControllerListener listener) {
return listener.HandleLogIn(this);
}
}
};

View File

@ -1,15 +0,0 @@
using MemoryPack;
using Phantom.Common.Data.Replies;
namespace Phantom.Common.Messages.Web.ToController;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record SendCommandToInstanceMessage(
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
[property: MemoryPackOrder(1)] Guid InstanceGuid,
[property: MemoryPackOrder(2)] string Command
) : IMessageToController<InstanceActionResult<SendCommandToInstanceResult>> {
public Task<InstanceActionResult<SendCommandToInstanceResult>> Accept(IMessageToControllerListener listener) {
return listener.HandleSendCommandToInstance(this);
}
}

View File

@ -1,16 +0,0 @@
using MemoryPack;
using Phantom.Common.Data.Minecraft;
using Phantom.Common.Data.Replies;
namespace Phantom.Common.Messages.Web.ToController;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record StopInstanceMessage(
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
[property: MemoryPackOrder(1)] Guid InstanceGuid,
[property: MemoryPackOrder(2)] MinecraftStopStrategy StopStrategy
) : IMessageToController<InstanceActionResult<StopInstanceResult>> {
public Task<InstanceActionResult<StopInstanceResult>> Accept(IMessageToControllerListener listener) {
return listener.HandleStopInstance(this);
}
}

View File

@ -2,7 +2,6 @@
using Phantom.Common.Data.Java;
using Phantom.Common.Data.Minecraft;
using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Web.AuditLog;
using Phantom.Common.Data.Web.Instance;
using Phantom.Common.Data.Web.Users;
using Phantom.Common.Logging;
@ -25,11 +24,8 @@ public static class WebMessageRegistries {
ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(2);
ToController.Add<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(3);
ToController.Add<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(4);
ToController.Add<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(5);
ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(6);
ToController.Add<GetAgentJavaRuntimes, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(7);
ToController.Add<GetUsersMessage, ImmutableArray<UserInfo>>(8);
ToController.Add<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(9);
ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(5);
ToController.Add<GetAgentJavaRuntimes, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(6);
ToController.Add<ReplyMessage>(127);
ToWeb.Add<RegisterWebResultMessage>(0);

View File

@ -3,19 +3,7 @@ using Phantom.Controller.Database.Entities;
namespace Phantom.Controller.Database.Repositories;
public sealed class AuditLogRepositoryWriter {
private readonly ILazyDbContext db;
private readonly Guid? currentUserGuid;
public AuditLogRepositoryWriter(ILazyDbContext db, Guid? currentUserGuid) {
this.db = db;
this.currentUserGuid = currentUserGuid;
}
private void AddItem(AuditLogEventType eventType, string subjectId, Dictionary<string, object?>? extra = null) {
db.Ctx.AuditLog.Add(new AuditLogEntity(currentUserGuid, eventType, subjectId, extra));
}
sealed partial class AuditLogRepository {
public void AddUserLoggedInEvent(UserEntity user) {
AddItem(AuditLogEventType.UserLoggedIn, user.UserGuid.ToString());
}

View File

@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore;
using Phantom.Common.Data.Web.AuditLog;
using Phantom.Controller.Database.Entities;
namespace Phantom.Controller.Database.Repositories;
public sealed partial class AuditLogRepository {
private readonly ILazyDbContext db;
private readonly Guid? currentUserGuid;
public AuditLogRepository(ILazyDbContext db, Guid? currentUserGuid) {
this.db = db;
this.currentUserGuid = currentUserGuid;
}
private void AddItem(AuditLogEventType eventType, string subjectId, Dictionary<string, object?>? extra = null) {
db.Ctx.AuditLog.Add(new AuditLogEntity(currentUserGuid, eventType, subjectId, extra));
}
public Task<AuditLogItem[]> GetItems(int count, CancellationToken cancellationToken) {
return db.Ctx
.AuditLog
.Include(static entity => entity.User)
.AsQueryable()
.OrderByDescending(static entity => entity.UtcTime)
.Take(count)
.Select(static entity => new AuditLogItem(entity.UtcTime, entity.UserGuid, entity.User == null ? null : entity.User.Name, entity.EventType, entity.SubjectType, entity.SubjectId, entity.Data))
.ToArrayAsync(cancellationToken);
}
}

View File

@ -1,26 +0,0 @@
using System.Collections.Immutable;
using Microsoft.EntityFrameworkCore;
using Phantom.Common.Data.Web.AuditLog;
using Phantom.Utils.Collections;
namespace Phantom.Controller.Database.Repositories;
public sealed class AuditLogRepositoryReader {
private readonly ILazyDbContext db;
public AuditLogRepositoryReader(ILazyDbContext db) {
this.db = db;
}
public Task<ImmutableArray<AuditLogItem>> GetMostRecentItems(int count, CancellationToken cancellationToken) {
return db.Ctx
.AuditLog
.Include(static entity => entity.User)
.AsQueryable()
.OrderByDescending(static entity => entity.UtcTime)
.Take(count)
.Select(static entity => new AuditLogItem(entity.UtcTime, entity.UserGuid, entity.User == null ? null : entity.User.Name, entity.EventType, entity.SubjectType, entity.SubjectId, entity.Data == null ? null : entity.Data.RootElement.ToString()))
.AsAsyncEnumerable()
.ToImmutableArrayAsync(cancellationToken);
}
}

View File

@ -50,8 +50,8 @@ public sealed class UserRepository {
private readonly ILazyDbContext db;
private AuditLogRepositoryWriter? auditLogWriter;
private AuditLogRepositoryWriter AuditLogWriter => this.auditLogWriter ??= new AuditLogRepositoryWriter(db, null);
private AuditLogRepository? auditLog;
private AuditLogRepository AuditLogRepository => this.auditLog ??= new AuditLogRepository(db, null);
public UserRepository(ILazyDbContext db) {
this.db = db;
@ -91,7 +91,7 @@ public sealed class UserRepository {
var user = new UserEntity(Guid.NewGuid(), username, UserPasswords.Hash(password));
db.Ctx.Users.Add(user);
AuditLogWriter.AddUserCreatedEvent(user);
AuditLogRepository.AddUserCreatedEvent(user);
return user;
}
@ -103,13 +103,13 @@ public sealed class UserRepository {
}
user.PasswordHash = UserPasswords.Hash(password);
AuditLogWriter.AddUserPasswordChangedEvent(user);
AuditLogRepository.AddUserPasswordChangedEvent(user);
return Result.Ok<SetUserPasswordError>();
}
public void DeleteUser(UserEntity user) {
db.Ctx.Users.Remove(user);
AuditLogWriter.AddUserDeletedEvent(user);
AuditLogRepository.AddUserDeletedEvent(user);
}
}

View File

@ -29,7 +29,6 @@ public sealed class ControllerServices {
private PermissionManager PermissionManager { get; }
private UserLoginManager UserLoginManager { get; }
private AuditLogManager AuditLogManager { get; }
private readonly IDbContextProvider dbProvider;
private readonly AuthToken webAuthToken;
@ -50,7 +49,6 @@ public sealed class ControllerServices {
this.PermissionManager = new PermissionManager(dbProvider);
this.UserLoginManager = new UserLoginManager(UserManager, PermissionManager);
this.AuditLogManager = new AuditLogManager(dbProvider);
this.dbProvider = dbProvider;
this.webAuthToken = webAuthToken;
@ -62,7 +60,7 @@ public sealed class ControllerServices {
}
public WebMessageListener CreateWebMessageListener(RpcConnectionToClient<IMessageToWebListener> connection) {
return new WebMessageListener(connection, webAuthToken, UserManager, UserLoginManager, AuditLogManager, AgentManager, AgentJavaRuntimesManager, InstanceManager, MinecraftVersions, TaskManager);
return new WebMessageListener(connection, webAuthToken, UserManager, UserLoginManager, AgentManager, AgentJavaRuntimesManager, InstanceManager, MinecraftVersions, TaskManager);
}
public async Task Initialize() {

View File

@ -114,12 +114,12 @@ sealed class InstanceManager {
entity.JavaRuntimeGuid = configuration.JavaRuntimeGuid;
entity.JvmArguments = JvmArgumentsHelper.Join(configuration.JvmArguments);
var auditLogWriter = new AuditLogRepositoryWriter(db, auditLogUserGuid);
var auditLogRepository = new AuditLogRepository(db, auditLogUserGuid);
if (isNewInstance) {
auditLogWriter.AddInstanceCreatedEvent(configuration.InstanceGuid);
auditLogRepository.AddInstanceCreatedEvent(configuration.InstanceGuid);
}
else {
auditLogWriter.AddInstanceEditedEvent(configuration.InstanceGuid);
auditLogRepository.AddInstanceEditedEvent(configuration.InstanceGuid);
}
await db.Ctx.SaveChangesAsync(cancellationToken);
@ -188,13 +188,13 @@ sealed class InstanceManager {
public async Task<InstanceActionResult<StopInstanceResult>> StopInstance(Guid auditLogUserGuid, Guid instanceGuid, MinecraftStopStrategy stopStrategy) {
var result = await SendInstanceActionMessage<StopInstanceMessage, StopInstanceResult>(instanceGuid, new StopInstanceMessage(instanceGuid, stopStrategy));
if (result.Is(StopInstanceResult.StopInitiated)) {
await HandleInstanceManuallyLaunchedOrStopped(instanceGuid, false, auditLogUserGuid, auditLogRepository => auditLogRepository.AddInstanceStoppedEvent(instanceGuid, stopStrategy.Seconds));
await HandleInstanceManuallyLaunchedOrStopped(instanceGuid, false, auditLogUserGuid, auditLogRepository => auditLogRepository.AddInstanceLaunchedEvent(instanceGuid));
}
return result;
}
private async Task HandleInstanceManuallyLaunchedOrStopped(Guid instanceGuid, bool wasLaunched, Guid auditLogUserGuid, Action<AuditLogRepositoryWriter> addAuditEvent) {
private async Task HandleInstanceManuallyLaunchedOrStopped(Guid instanceGuid, bool wasLaunched, Guid auditLogUserGuid, Action<AuditLogRepository> addAuditEvent) {
await modifyInstancesSemaphore.WaitAsync(cancellationToken);
try {
instances.ByGuid.TryReplace(instanceGuid, instance => instance with { LaunchAutomatically = wasLaunched });
@ -203,7 +203,10 @@ sealed class InstanceManager {
var entity = await db.Ctx.Instances.FindAsync(new object[] { instanceGuid }, cancellationToken);
if (entity != null) {
entity.LaunchAutomatically = wasLaunched;
addAuditEvent(new AuditLogRepositoryWriter(db, auditLogUserGuid));
var auditLogRepository = new AuditLogRepository(db, auditLogUserGuid);
addAuditEvent(auditLogRepository);
await db.Ctx.SaveChangesAsync(cancellationToken);
}
} finally {
@ -211,12 +214,10 @@ sealed class InstanceManager {
}
}
public async Task<InstanceActionResult<SendCommandToInstanceResult>> SendCommand(Guid auditLogUserId, Guid instanceGuid, string command) {
public async Task<InstanceActionResult<SendCommandToInstanceResult>> SendCommand(Guid instanceGuid, string command) {
var result = await SendInstanceActionMessage<SendCommandToInstanceMessage, SendCommandToInstanceResult>(instanceGuid, new SendCommandToInstanceMessage(instanceGuid, command));
if (result.Is(SendCommandToInstanceResult.Success)) {
await using var db = dbProvider.Lazy();
new AuditLogRepositoryWriter(db, auditLogUserId).AddInstanceCommandExecutedEvent(instanceGuid, command);
await db.Ctx.SaveChangesAsync(cancellationToken);
// TODO audit log
}
return result;

View File

@ -4,7 +4,6 @@ using Phantom.Common.Data.Java;
using Phantom.Common.Data.Minecraft;
using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Web.Agent;
using Phantom.Common.Data.Web.AuditLog;
using Phantom.Common.Data.Web.Instance;
using Phantom.Common.Data.Web.Users;
using Phantom.Common.Logging;
@ -26,34 +25,21 @@ namespace Phantom.Controller.Services.Rpc;
public sealed class WebMessageListener : IMessageToControllerListener {
private static readonly ILogger Logger = PhantomLogger.Create<WebMessageListener>();
private readonly RpcConnectionToClient<IMessageToWebListener> connection; // TODO use single queue
private readonly RpcConnectionToClient<IMessageToWebListener> connection;
private readonly AuthToken authToken;
private readonly UserManager userManager;
private readonly UserLoginManager userLoginManager;
private readonly AuditLogManager auditLogManager;
private readonly AgentManager agentManager;
private readonly AgentJavaRuntimesManager agentJavaRuntimesManager;
private readonly InstanceManager instanceManager;
private readonly MinecraftVersions minecraftVersions;
private readonly TaskManager taskManager;
internal WebMessageListener(
RpcConnectionToClient<IMessageToWebListener> connection,
AuthToken authToken,
UserManager userManager,
UserLoginManager userLoginManager,
AuditLogManager auditLogManager,
AgentManager agentManager,
AgentJavaRuntimesManager agentJavaRuntimesManager,
InstanceManager instanceManager,
MinecraftVersions minecraftVersions,
TaskManager taskManager
) {
internal WebMessageListener(RpcConnectionToClient<IMessageToWebListener> connection, AuthToken authToken, UserManager userManager, UserLoginManager userLoginManager, AgentManager agentManager, AgentJavaRuntimesManager agentJavaRuntimesManager, InstanceManager instanceManager, MinecraftVersions minecraftVersions, TaskManager taskManager) {
this.connection = connection;
this.authToken = authToken;
this.userManager = userManager;
this.userLoginManager = userLoginManager;
this.auditLogManager = auditLogManager;
this.agentManager = agentManager;
this.agentJavaRuntimesManager = agentJavaRuntimesManager;
this.instanceManager = instanceManager;
@ -75,7 +61,6 @@ public sealed class WebMessageListener : IMessageToControllerListener {
agentManager.AgentsChanged.Subscribe(this, HandleAgentsChanged);
instanceManager.InstancesChanged.Subscribe(this, HandleInstancesChanged);
// TODO unsubscribe on closed
return NoReply.Instance;
}
@ -101,14 +86,6 @@ public sealed class WebMessageListener : IMessageToControllerListener {
return instanceManager.LaunchInstance(message.LoggedInUserGuid, message.InstanceGuid);
}
public Task<InstanceActionResult<StopInstanceResult>> HandleStopInstance(StopInstanceMessage message) {
return instanceManager.StopInstance(message.LoggedInUserGuid, message.InstanceGuid, message.StopStrategy);
}
public Task<InstanceActionResult<SendCommandToInstanceResult>> HandleSendCommandToInstance(SendCommandToInstanceMessage message) {
return instanceManager.SendCommand(message.LoggedInUserGuid, message.InstanceGuid, message.Command);
}
public Task<ImmutableArray<MinecraftVersion>> HandleGetMinecraftVersions(GetMinecraftVersionsMessage message) {
return minecraftVersions.GetVersions(CancellationToken.None);
}
@ -117,14 +94,6 @@ public sealed class WebMessageListener : IMessageToControllerListener {
return Task.FromResult(agentJavaRuntimesManager.All);
}
public Task<ImmutableArray<UserInfo>> HandleGetUsers(GetUsersMessage message) {
return userManager.GetAll();
}
public Task<ImmutableArray<AuditLogItem>> HandleGetAuditLog(GetAuditLogMessage message) {
return auditLogManager.GetMostRecentItems(message.Count);
}
public Task<LogInSuccess?> HandleLogIn(LogInMessage message) {
return userLoginManager.LogIn(message.Username, message.Password);
}

View File

@ -1,19 +0,0 @@
using System.Collections.Immutable;
using Phantom.Common.Data.Web.AuditLog;
using Phantom.Controller.Database;
using Phantom.Controller.Database.Repositories;
namespace Phantom.Controller.Services.Users;
sealed class AuditLogManager {
private readonly IDbContextProvider dbProvider;
public AuditLogManager(IDbContextProvider dbProvider) {
this.dbProvider = dbProvider;
}
public async Task<ImmutableArray<AuditLogItem>> GetMostRecentItems(int count) {
await using var db = dbProvider.Lazy();
return await new AuditLogRepositoryReader(db).GetMostRecentItems(count, CancellationToken.None);
}
}

View File

@ -36,20 +36,6 @@ sealed class PermissionManager {
}
}
public async Task<PermissionSet> FetchPermissionsForAllUsers(Guid userId) {
await using var ctx = dbProvider.Eager();
var userPermissions = ctx.UserPermissions
.Where(up => up.UserGuid == userId)
.Select(static up => up.PermissionId);
var rolePermissions = ctx.UserRoles
.Where(ur => ur.UserGuid == userId)
.Join(ctx.RolePermissions, static ur => ur.RoleGuid, static rp => rp.RoleGuid, static (ur, rp) => rp.PermissionId);
return new PermissionSet(await userPermissions.Union(rolePermissions).AsAsyncEnumerable().ToImmutableSetAsync());
}
public async Task<PermissionSet> FetchPermissionsForUserId(Guid userId) {
await using var ctx = dbProvider.Eager();

View File

@ -1,5 +1,4 @@
using System.Collections.Immutable;
using Phantom.Common.Data.Web.Users;
using Phantom.Common.Data.Web.Users;
using Phantom.Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults;
using Phantom.Common.Logging;
using Phantom.Controller.Database;
@ -18,32 +17,24 @@ sealed class UserManager {
this.dbProvider = dbProvider;
}
public async Task<ImmutableArray<UserInfo>> GetAll() {
await using var db = dbProvider.Lazy();
var userRepository = new UserRepository(db);
var allUsers = await userRepository.GetAll();
return allUsers.Select(static user => user.ToUserInfo()).ToImmutableArray();
}
public async Task<UserEntity?> GetAuthenticated(string username, string password) {
await using var db = dbProvider.Lazy();
var userRepository = new UserRepository(db);
var repository = new UserRepository(db);
var user = await userRepository.GetByName(username);
var user = await repository.GetByName(username);
return user != null && UserPasswords.Verify(password, user.PasswordHash) ? user : null;
}
public async Task<CreateOrUpdateAdministratorUserResult> CreateOrUpdateAdministrator(string username, string password) {
await using var db = dbProvider.Lazy();
var userRepository = new UserRepository(db);
var repository = new UserRepository(db);
try {
bool wasCreated;
var user = await userRepository.GetByName(username);
var user = await repository.GetByName(username);
if (user == null) {
var result = await userRepository.CreateUser(username, password);
var result = await repository.CreateUser(username, password);
if (result) {
user = result.Value;
wasCreated = true;
@ -53,7 +44,7 @@ sealed class UserManager {
}
}
else {
var result = userRepository.SetUserPassword(user, password);
var result = repository.SetUserPassword(user, password);
if (!result) {
return new UpdatingFailed(result.Error);
}
@ -86,15 +77,15 @@ sealed class UserManager {
public async Task<DeleteUserResult> DeleteByGuid(Guid guid) {
await using var db = dbProvider.Lazy();
var userRepository = new UserRepository(db);
var repository = new UserRepository(db);
var user = await userRepository.GetByGuid(guid);
var user = await repository.GetByGuid(guid);
if (user == null) {
return DeleteUserResult.NotFound;
}
try {
userRepository.DeleteUser(user);
repository.DeleteUser(user);
await db.Ctx.SaveChangesAsync();
Logger.Information("Deleted user \"{Username}\" (GUID {Guid}).", user.Name, user.UserGuid);

View File

@ -1,6 +1,5 @@
using System.Collections.Immutable;
using Phantom.Common.Data.Instance;
using Phantom.Common.Data.Minecraft;
using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Web.Instance;
using Phantom.Common.Logging;
@ -26,31 +25,17 @@ public sealed class InstanceManager {
instances.SetTo(newInstances.ToImmutableDictionary(static instance => instance.Configuration.InstanceGuid));
}
public InstanceDictionary GetAll() {
return instances.Value;
}
public Instance? GetByGuid(Guid instanceGuid) {
return instances.Value.GetValueOrDefault(instanceGuid);
}
public Task<InstanceActionResult<CreateOrUpdateInstanceResult>> CreateOrUpdateInstance(Guid loggedInUserGuid, InstanceConfiguration configuration, CancellationToken cancellationToken) {
public Task<InstanceActionResult<CreateOrUpdateInstanceResult>> CreateOrUpdateInstance(Guid loggedInUserGuid, InstanceConfiguration configuration) {
var message = new CreateOrUpdateInstanceMessage(loggedInUserGuid, configuration);
return controllerConnection.Send<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(message, cancellationToken);
return controllerConnection.Send<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(message, TimeSpan.FromSeconds(30));
}
public Task<InstanceActionResult<LaunchInstanceResult>> LaunchInstance(Guid loggedInUserGuid, Guid instanceGuid, CancellationToken cancellationToken) {
public Task<InstanceActionResult<LaunchInstanceResult>> LaunchInstance(Guid loggedInUserGuid, Guid instanceGuid) {
var message = new LaunchInstanceMessage(loggedInUserGuid, instanceGuid);
return controllerConnection.Send<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(message, cancellationToken);
}
public Task<InstanceActionResult<StopInstanceResult>> StopInstance(Guid loggedInUserGuid, Guid instanceGuid, MinecraftStopStrategy stopStrategy, CancellationToken cancellationToken) {
var message = new StopInstanceMessage(loggedInUserGuid, instanceGuid, stopStrategy);
return controllerConnection.Send<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(message, cancellationToken);
}
public Task<InstanceActionResult<SendCommandToInstanceResult>> SendCommandToInstance(Guid loggedInUserGuid, Guid instanceGuid, string command, CancellationToken cancellationToken) {
var message = new SendCommandToInstanceMessage(loggedInUserGuid, instanceGuid, command);
return controllerConnection.Send<SendCommandToInstanceMessage, InstanceActionResult<SendCommandToInstanceResult>>(message, cancellationToken);
return controllerConnection.Send<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(message, TimeSpan.FromSeconds(30));
}
}

View File

@ -6,7 +6,6 @@ using Phantom.Web.Services.Authentication;
using Phantom.Web.Services.Authorization;
using Phantom.Web.Services.Instances;
using Phantom.Web.Services.Rpc;
using Phantom.Web.Services.Users;
namespace Phantom.Web.Services;
@ -19,11 +18,9 @@ public static class PhantomWebServices {
services.AddSingleton<InstanceManager>();
services.AddSingleton<PermissionManager>();
services.AddSingleton<UserManager>();
services.AddSingleton<UserSessionManager>();
services.AddSingleton<AuditLogManager>();
services.AddScoped<UserLoginManager>();
services.AddScoped<UserSessionBrowserStorage>();
services.AddScoped<UserLoginManager>();
services.AddScoped<CustomAuthenticationStateProvider>();
services.AddScoped<AuthenticationStateProvider>(static services => services.GetRequiredService<CustomAuthenticationStateProvider>());

View File

@ -17,8 +17,4 @@ public sealed class ControllerConnection {
public Task<TReply> Send<TMessage, TReply>(TMessage message, TimeSpan waitForReplyTime, CancellationToken waitForReplyCancellationToken = default) where TMessage : IMessageToController<TReply> {
return connection.Send<TMessage, TReply>(message, waitForReplyTime, waitForReplyCancellationToken);
}
public Task<TReply> Send<TMessage, TReply>(TMessage message, CancellationToken waitForReplyCancellationToken) where TMessage : IMessageToController<TReply> {
return connection.Send<TMessage, TReply>(message, Timeout.InfiniteTimeSpan, waitForReplyCancellationToken);
}
}

View File

@ -1,19 +0,0 @@
using System.Collections.Immutable;
using Phantom.Common.Data.Web.AuditLog;
using Phantom.Common.Messages.Web.ToController;
using Phantom.Web.Services.Rpc;
namespace Phantom.Web.Services.Users;
public sealed class AuditLogManager {
private readonly ControllerConnection controllerConnection;
public AuditLogManager(ControllerConnection controllerConnection) {
this.controllerConnection = controllerConnection;
}
public Task<ImmutableArray<AuditLogItem>> GetMostRecentItems(int count, CancellationToken cancellationToken) {
var message = new GetAuditLogMessage(count);
return controllerConnection.Send<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(message, cancellationToken);
}
}

View File

@ -1,18 +1,5 @@
using System.Collections.Immutable;
using Phantom.Common.Data.Web.Users;
using Phantom.Common.Messages.Web.ToController;
using Phantom.Web.Services.Rpc;
namespace Phantom.Web.Services.Users;
namespace Phantom.Web.Services.Users;
public sealed class UserManager {
private readonly ControllerConnection controllerConnection;
public UserManager(ControllerConnection controllerConnection) {
this.controllerConnection = controllerConnection;
}
public Task<ImmutableArray<UserInfo>> GetAll(CancellationToken cancellationToken) {
return controllerConnection.Send<GetUsersMessage, ImmutableArray<UserInfo>>(new GetUsersMessage(), cancellationToken);
}
}

View File

@ -8,7 +8,7 @@ using UserInfo = Phantom.Web.Services.Authentication.UserInfo;
namespace Phantom.Web.Base;
public abstract class PhantomComponent : ComponentBase, IDisposable {
public abstract class PhantomComponent : ComponentBase {
private static readonly ILogger Logger = PhantomLogger.Create<PhantomComponent>();
[CascadingParameter]
@ -17,10 +17,6 @@ public abstract class PhantomComponent : ComponentBase, IDisposable {
[Inject]
public PermissionManager PermissionManager { get; set; } = null!;
private readonly CancellationTokenSource cancellationTokenSource = new ();
public CancellationToken CancellationToken => cancellationTokenSource.Token;
public async Task<Guid?> GetUserGuid() {
var authenticationState = await AuthenticationStateTask;
return UserInfo.TryGetGuid(authenticationState.User);
@ -34,13 +30,4 @@ public abstract class PhantomComponent : ComponentBase, IDisposable {
protected void InvokeAsyncChecked(Func<Task> task) {
InvokeAsync(task).ContinueWith(static t => Logger.Error(t.Exception, "Caught exception in async task."), TaskContinuationOptions.OnlyOnFaulted);
}
public void Dispose() {
cancellationTokenSource.Cancel();
cancellationTokenSource.Dispose();
OnDisposed();
GC.SuppressFinalize(this);
}
protected virtual void OnDisposed() {}
}

View File

@ -2,7 +2,7 @@
@using Phantom.Common.Data.Web.Agent
@using Phantom.Utils.Collections
@using Phantom.Web.Services.Agents
@inherits PhantomComponent
@implements IDisposable
@inject AgentManager AgentManager
<h1>Agents</h1>
@ -85,7 +85,7 @@
});
}
protected override void OnDisposed() {
void IDisposable.Dispose() {
AgentManager.AgentsChanged.Unsubscribe(this);
}

View File

@ -1,12 +1,11 @@
@page "/audit"
@attribute [Authorize(Permission.ViewAuditPolicy)]
@using System.Collections.Immutable
@using Phantom.Common.Data.Web.AuditLog
@using Phantom.Common.Data.Web.Users
@using Phantom.Web.Services.Users
@using System.Collections.Immutable
@using Phantom.Web.Services.Instances
@inherits PhantomComponent
@inject AuditLogManager AuditLogManager
@implements IDisposable
@inject AuditLog AuditLog
@inject InstanceManager InstanceManager
@inject UserManager UserManager
@ -43,7 +42,7 @@
<code class="text-uppercase">@(logItem.SubjectId ?? "-")</code>
</td>
<td>
<code>@logItem.JsonData</code>
<code>@logItem.Data?.RootElement.ToString()</code>
</td>
</tr>
}
@ -53,8 +52,8 @@
@code {
private CancellationTokenSource? initializationCancellationTokenSource;
private ImmutableArray<AuditLogItem> logItems = ImmutableArray<AuditLogItem>.Empty;
private ImmutableDictionary<Guid, string>? userNamesByGuid;
private AuditLogItem[] logItems = Array.Empty<AuditLogItem>();
private Dictionary<Guid, string>? userNamesByGuid;
private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
protected override async Task OnInitializedAsync() {
@ -62,9 +61,9 @@
var cancellationToken = initializationCancellationTokenSource.Token;
try {
logItems = await AuditLogManager.GetMostRecentItems(50, cancellationToken);
userNamesByGuid = (await UserManager.GetAll(cancellationToken)).ToImmutableDictionary(static user => user.Guid, static user => user.Name);
instanceNamesByGuid = InstanceManager.GetAll().Values.ToImmutableDictionary(static instance => instance.Configuration.InstanceGuid, static instance => instance.Configuration.InstanceName);
logItems = await AuditLog.GetItems(50, cancellationToken);
userNamesByGuid = await UserManager.GetAllByGuid(static user => user.Name, cancellationToken);
instanceNamesByGuid = InstanceManager.GetInstanceNames();
} finally {
initializationCancellationTokenSource.Dispose();
}
@ -78,7 +77,7 @@
};
}
protected override void OnDisposed() {
public void Dispose() {
try {
initializationCancellationTokenSource?.Cancel();
} catch (ObjectDisposedException) {}

View File

@ -1,95 +1,95 @@
@* @page "/events" *@
@* @attribute [Authorize(Permission.ViewEventsPolicy)] *@
@* @inherits PhantomComponent *@
@* @using Phantom.Common.Data.Web.EventLog *@
@* @using Phantom.Common.Data.Web.Users *@
@* @using System.Collections.Immutable *@
@* @using System.Diagnostics *@
@* @using Phantom.Web.Services.Instances *@
@* @inject AgentManager AgentManager *@
@* @inject EventLog EventLog *@
@* @inject InstanceManager InstanceManager *@
@* *@
@* <h1>Event Log</h1> *@
@* *@
@* <table class="table"> *@
@* <thead> *@
@* <tr> *@
@* <Column Width="165px" Class="text-end">Time</Column> *@
@* <Column Width="320px; 20%">Agent</Column> *@
@* <Column Width="160px">Event Type</Column> *@
@* <Column Width="320px; 20%">Subject</Column> *@
@* <Column Width="100px; 60%">Data</Column> *@
@* </tr> *@
@* </thead> *@
@* <tbody> *@
@* @foreach (var logItem in logItems) { *@
@* DateTimeOffset time = logItem.UtcTime.ToLocalTime(); *@
@* <tr> *@
@* <td class="text-end"> *@
@* <time datetime="@time.ToString("o")">@time.ToString()</time> *@
@* </td> *@
@* <td> *@
@* @if (logItem.AgentGuid is {} agentGuid) { *@
@* @(GetAgentName(agentGuid)) *@
@* <br> *@
@* <code class="text-uppercase">@agentGuid</code> *@
@* } *@
@* else { *@
@* <text>-</text> *@
@* } *@
@* </td> *@
@* <td>@logItem.EventType.ToNiceString()</td> *@
@* <td> *@
@* @if (GetSubjectName(logItem.SubjectType, logItem.SubjectId) is {} subjectName) { *@
@* @subjectName *@
@* <br> *@
@* } *@
@* <code class="text-uppercase">@logItem.SubjectId</code> *@
@* </td> *@
@* <td> *@
@* <code>@logItem.Data?.RootElement.ToString()</code> *@
@* </td> *@
@* </tr> *@
@* } *@
@* </tbody> *@
@* </table> *@
@* *@
@* @code { *@
@* *@
@* private CancellationTokenSource? initializationCancellationTokenSource; *@
@* private EventLogItem[] logItems = Array.Empty<EventLogItem>(); *@
@* private ImmutableDictionary<Guid, string> agentNamesByGuid = ImmutableDictionary<Guid, string>.Empty; *@
@* private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty; *@
@* *@
@* protected override async Task OnInitializedAsync() { *@
@* initializationCancellationTokenSource = new CancellationTokenSource(); *@
@* var cancellationToken = initializationCancellationTokenSource.Token; *@
@* *@
@* try { *@
@* logItems = await EventLog.GetItems(50, cancellationToken); *@
@* agentNamesByGuid = AgentManager.GetAgents().ToImmutableDictionary(static kvp => kvp.Key, static kvp => kvp.Value.Name); *@
@* instanceNamesByGuid = InstanceManager.GetInstanceNames(); *@
@* } finally { *@
@* initializationCancellationTokenSource.Dispose(); *@
@* } *@
@* } *@
@* *@
@* private string GetAgentName(Guid agentGuid) { *@
@* return agentNamesByGuid.TryGetValue(agentGuid, out var name) ? name : "?"; *@
@* } *@
@* *@
@* private string? GetSubjectName(EventLogSubjectType type, string id) { *@
@* return type switch { *@
@* EventLogSubjectType.Instance => instanceNamesByGuid.TryGetValue(Guid.Parse(id), out var name) ? name : null, *@
@* _ => null *@
@* }; *@
@* } *@
@* *@
@* public void Dispose() { *@
@* try { *@
@* initializationCancellationTokenSource?.Cancel(); *@
@* } catch (ObjectDisposedException) {} *@
@* } *@
@* *@
@* } *@
@page "/events"
@attribute [Authorize(Permission.ViewEventsPolicy)]
@using Phantom.Common.Data.Web.EventLog
@using Phantom.Common.Data.Web.Users
@using System.Collections.Immutable
@using System.Diagnostics
@using Phantom.Web.Services.Instances
@implements IDisposable
@inject AgentManager AgentManager
@inject EventLog EventLog
@inject InstanceManager InstanceManager
<h1>Event Log</h1>
<table class="table">
<thead>
<tr>
<Column Width="165px" Class="text-end">Time</Column>
<Column Width="320px; 20%">Agent</Column>
<Column Width="160px">Event Type</Column>
<Column Width="320px; 20%">Subject</Column>
<Column Width="100px; 60%">Data</Column>
</tr>
</thead>
<tbody>
@foreach (var logItem in logItems) {
DateTimeOffset time = logItem.UtcTime.ToLocalTime();
<tr>
<td class="text-end">
<time datetime="@time.ToString("o")">@time.ToString()</time>
</td>
<td>
@if (logItem.AgentGuid is {} agentGuid) {
@(GetAgentName(agentGuid))
<br>
<code class="text-uppercase">@agentGuid</code>
}
else {
<text>-</text>
}
</td>
<td>@logItem.EventType.ToNiceString()</td>
<td>
@if (GetSubjectName(logItem.SubjectType, logItem.SubjectId) is {} subjectName) {
@subjectName
<br>
}
<code class="text-uppercase">@logItem.SubjectId</code>
</td>
<td>
<code>@logItem.Data?.RootElement.ToString()</code>
</td>
</tr>
}
</tbody>
</table>
@code {
private CancellationTokenSource? initializationCancellationTokenSource;
private EventLogItem[] logItems = Array.Empty<EventLogItem>();
private ImmutableDictionary<Guid, string> agentNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
protected override async Task OnInitializedAsync() {
initializationCancellationTokenSource = new CancellationTokenSource();
var cancellationToken = initializationCancellationTokenSource.Token;
try {
logItems = await EventLog.GetItems(50, cancellationToken);
agentNamesByGuid = AgentManager.GetAgents().ToImmutableDictionary(static kvp => kvp.Key, static kvp => kvp.Value.Name);
instanceNamesByGuid = InstanceManager.GetInstanceNames();
} finally {
initializationCancellationTokenSource.Dispose();
}
}
private string GetAgentName(Guid agentGuid) {
return agentNamesByGuid.TryGetValue(agentGuid, out var name) ? name : "?";
}
private string? GetSubjectName(EventLogSubjectType type, string id) {
return type switch {
EventLogSubjectType.Instance => instanceNamesByGuid.TryGetValue(Guid.Parse(id), out var name) ? name : null,
_ => null
};
}
public void Dispose() {
try {
initializationCancellationTokenSource?.Cancel();
} catch (ObjectDisposedException) {}
}
}

View File

@ -1,11 +1,12 @@
@page "/instances/{InstanceGuid:guid}"
@attribute [Authorize(Permission.ViewInstancesPolicy)]
@inherits PhantomComponent
@using Phantom.Common.Data.Instance
@using Phantom.Common.Data.Replies
@using Phantom.Common.Data.Web.Instance
@using Phantom.Common.Data.Web.Users
@using Phantom.Web.Services.Instances
@inherits PhantomComponent
@implements IDisposable
@inject InstanceManager InstanceManager
@if (Instance == null) {
@ -77,7 +78,7 @@ else {
return;
}
var result = await InstanceManager.LaunchInstance(loggedInUserGuid.Value, InstanceGuid, CancellationToken);
var result = await InstanceManager.LaunchInstance(loggedInUserGuid.Value, InstanceGuid);
if (!result.Is(LaunchInstanceResult.LaunchInitiated)) {
lastError = result.ToSentence(Messages.ToSentence);
}
@ -86,7 +87,7 @@ else {
}
}
protected override void OnDisposed() {
public void Dispose() {
InstanceManager.InstancesChanged.Unsubscribe(this);
}

View File

@ -5,7 +5,7 @@
@using Phantom.Common.Data.Web.Users
@using Phantom.Web.Services.Agents
@using Phantom.Web.Services.Instances
@inherits PhantomComponent
@implements IDisposable
@inject AgentManager AgentManager
@inject InstanceManager InstanceManager
@ -91,7 +91,7 @@
});
}
protected override void OnDisposed() {
void IDisposable.Dispose() {
AgentManager.AgentsChanged.Unsubscribe(this);
InstanceManager.InstancesChanged.Unsubscribe(this);
}

View File

@ -1,110 +1,110 @@
@* @page "/users" *@
@* @using Phantom.Common.Data.Web.Users *@
@* @using System.Collections.Immutable *@
@* @using Phantom.Web.Services.Authorization *@
@* @attribute [Authorize(Permission.ViewUsersPolicy)] *@
@* @inject UserManager UserManager *@
@* @inject UserRoleManager UserRoleManager *@
@* @inject PermissionManager PermissionManager *@
@* *@
@* <h1>Users</h1> *@
@* *@
@* <PermissionView Permission="Permission.EditUsers"> *@
@* <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-user">Add User...</button> *@
@* </PermissionView> *@
@* *@
@* <AuthorizeView> *@
@* <Authorized> *@
@* @{ var canEdit = PermissionManager.CheckPermission(context.User, Permission.EditUsers); } *@
@* <table class="table align-middle"> *@
@* <thead> *@
@* <tr> *@
@* <Column Width="320px">Identifier</Column> *@
@* <Column Width="125px; 40%">Username</Column> *@
@* <Column Width="125px; 60%">Roles</Column> *@
@* @if (canEdit) { *@
@* <Column Width="175px">Actions</Column> *@
@* } *@
@* </tr> *@
@* </thead> *@
@* <tbody> *@
@* @{ var myUserId = UserManager.GetAuthenticatedUserId(context.User); } *@
@* @foreach (var user in allUsers) { *@
@* var isMe = myUserId == user.Guid; *@
@* <tr> *@
@* <td> *@
@* <code class="text-uppercase">@user.Guid</code> *@
@* </td> *@
@* @if (isMe) { *@
@* <td class="fw-semibold">@user.Name</td> *@
@* } *@
@* else { *@
@* <td>@user.Name</td> *@
@* } *@
@* <td>@(userGuidToRoleDescription.TryGetValue(user.Guid, out var roles) ? roles : "?")</td> *@
@* @if (canEdit) { *@
@* <td> *@
@* @if (!isMe) { *@
@* <button class="btn btn-primary btn-sm" @onclick="() => userRolesDialog.Show(user)">Edit Roles</button> *@
@* <button class="btn btn-danger btn-sm" @onclick="() => userDeleteDialog.Show(user)">Delete...</button> *@
@* } *@
@* </td> *@
@* } *@
@* </tr> *@
@* } *@
@* </tbody> *@
@* </table> *@
@* </Authorized> *@
@* </AuthorizeView> *@
@* *@
@* <PermissionView Permission="Permission.EditUsers"> *@
@* <UserAddDialog ModalId="add-user" UserAdded="OnUserAdded" /> *@
@* <UserRolesDialog @ref="userRolesDialog" ModalId="manage-user-roles" UserModified="OnUserRolesChanged" /> *@
@* <UserDeleteDialog @ref="userDeleteDialog" ModalId="delete-user" UserModified="OnUserDeleted" /> *@
@* </PermissionView> *@
@* *@
@* @code { *@
@* *@
@* private ImmutableArray<UserInfo> allUsers = ImmutableArray<UserInfo>.Empty; *@
@* private readonly Dictionary<Guid, string> userGuidToRoleDescription = new(); *@
@* *@
@* private UserRolesDialog userRolesDialog = null!; *@
@* private UserDeleteDialog userDeleteDialog = null!; *@
@* *@
@* protected override async Task OnInitializedAsync() { *@
@* var unsortedUsers = await UserManager.GetAll(); *@
@* allUsers = unsortedUsers.Sort(static (a, b) => a.Name.CompareTo(b.Name)); *@
@* *@
@* foreach (var (userGuid, roles) in await UserRoleManager.GetAllByUserGuid()) { *@
@* userGuidToRoleDescription[userGuid] = StringifyRoles(roles); *@
@* } *@
@* *@
@* foreach (var user in allUsers) { *@
@* await RefreshUserRoles(user); *@
@* } *@
@* } *@
@* *@
@* private async Task RefreshUserRoles(UserInfo user) { *@
@* var roles = await UserRoleManager.GetUserRoles(user); *@
@* userGuidToRoleDescription[user.Guid] = StringifyRoles(roles); *@
@* } *@
@* *@
@* private static string StringifyRoles(ImmutableArray<RoleInfo> roles) { *@
@* return roles.IsEmpty ? "-" : string.Join(", ", roles.Select(static role => role.Name)); *@
@* } *@
@* *@
@* private Task OnUserAdded(UserInfo user) { *@
@* allUsers = allUsers.Add(user); *@
@* return RefreshUserRoles(user); *@
@* } *@
@* *@
@* private Task OnUserRolesChanged(UserInfo user) { *@
@* return RefreshUserRoles(user); *@
@* } *@
@* *@
@* private void OnUserDeleted(UserInfo user) { *@
@* allUsers = allUsers.Remove(user); *@
@* userGuidToRoleDescription.Remove(user.Guid); *@
@* } *@
@* *@
@* } *@
@page "/users"
@using Phantom.Common.Data.Web.Users
@using System.Collections.Immutable
@using Phantom.Web.Services.Authorization
@attribute [Authorize(Permission.ViewUsersPolicy)]
@inject UserManager UserManager
@inject UserRoleManager UserRoleManager
@inject PermissionManager PermissionManager
<h1>Users</h1>
<PermissionView Permission="Permission.EditUsers">
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-user">Add User...</button>
</PermissionView>
<AuthorizeView>
<Authorized>
@{ var canEdit = PermissionManager.CheckPermission(context.User, Permission.EditUsers); }
<table class="table align-middle">
<thead>
<tr>
<Column Width="320px">Identifier</Column>
<Column Width="125px; 40%">Username</Column>
<Column Width="125px; 60%">Roles</Column>
@if (canEdit) {
<Column Width="175px">Actions</Column>
}
</tr>
</thead>
<tbody>
@{ var myUserId = UserManager.GetAuthenticatedUserId(context.User); }
@foreach (var user in allUsers) {
var isMe = myUserId == user.Guid;
<tr>
<td>
<code class="text-uppercase">@user.Guid</code>
</td>
@if (isMe) {
<td class="fw-semibold">@user.Name</td>
}
else {
<td>@user.Name</td>
}
<td>@(userGuidToRoleDescription.TryGetValue(user.Guid, out var roles) ? roles : "?")</td>
@if (canEdit) {
<td>
@if (!isMe) {
<button class="btn btn-primary btn-sm" @onclick="() => userRolesDialog.Show(user)">Edit Roles</button>
<button class="btn btn-danger btn-sm" @onclick="() => userDeleteDialog.Show(user)">Delete...</button>
}
</td>
}
</tr>
}
</tbody>
</table>
</Authorized>
</AuthorizeView>
<PermissionView Permission="Permission.EditUsers">
<UserAddDialog ModalId="add-user" UserAdded="OnUserAdded" />
<UserRolesDialog @ref="userRolesDialog" ModalId="manage-user-roles" UserModified="OnUserRolesChanged" />
<UserDeleteDialog @ref="userDeleteDialog" ModalId="delete-user" UserModified="OnUserDeleted" />
</PermissionView>
@code {
private ImmutableArray<UserInfo> allUsers = ImmutableArray<UserInfo>.Empty;
private readonly Dictionary<Guid, string> userGuidToRoleDescription = new();
private UserRolesDialog userRolesDialog = null!;
private UserDeleteDialog userDeleteDialog = null!;
protected override async Task OnInitializedAsync() {
var unsortedUsers = await UserManager.GetAll();
allUsers = unsortedUsers.Sort(static (a, b) => a.Name.CompareTo(b.Name));
foreach (var (userGuid, roles) in await UserRoleManager.GetAllByUserGuid()) {
userGuidToRoleDescription[userGuid] = StringifyRoles(roles);
}
foreach (var user in allUsers) {
await RefreshUserRoles(user);
}
}
private async Task RefreshUserRoles(UserInfo user) {
var roles = await UserRoleManager.GetUserRoles(user);
userGuidToRoleDescription[user.Guid] = StringifyRoles(roles);
}
private static string StringifyRoles(ImmutableArray<RoleInfo> roles) {
return roles.IsEmpty ? "-" : string.Join(", ", roles.Select(static role => role.Name));
}
private Task OnUserAdded(UserInfo user) {
allUsers = allUsers.Add(user);
return RefreshUserRoles(user);
}
private Task OnUserRolesChanged(UserInfo user) {
return RefreshUserRoles(user);
}
private void OnUserDeleted(UserInfo user) {
allUsers = allUsers.Remove(user);
userGuidToRoleDescription.Remove(user.Guid);
}
}

View File

@ -338,7 +338,7 @@
JvmArgumentsHelper.Split(form.JvmArguments)
);
var result = await InstanceManager.CreateOrUpdateInstance(loggedInUserGuid.Value, instance, CancellationToken);
var result = await InstanceManager.CreateOrUpdateInstance(loggedInUserGuid.Value, instance);
if (result.Is(CreateOrUpdateInstanceResult.Success)) {
await Nav.NavigateTo("instances/" + instance.InstanceGuid);
}

View File

@ -31,11 +31,6 @@
private ElementReference commandInputElement;
private async Task ExecuteCommand(EditContext context) {
var loggedInUserGuid = await GetUserGuid();
if (loggedInUserGuid == null) {
return;
}
await form.SubmitModel.StartSubmitting();
if (!await CheckPermission(Permission.ControlInstances)) {
@ -43,7 +38,7 @@
return;
}
var result = await InstanceManager.SendCommandToInstance(loggedInUserGuid.Value, InstanceGuid, form.Command, CancellationToken);
var result = await InstanceManager.SendCommand(InstanceGuid, form.Command);
if (result.Is(SendCommandToInstanceResult.Success)) {
form.Command = string.Empty;
form.SubmitModel.StopSubmitting();

View File

@ -1,9 +1,10 @@
@using Phantom.Utils.Collections
@using Phantom.Utils.Events
@inherits PhantomComponent
@using Phantom.Utils.Collections
@using System.Diagnostics
@using Phantom.Common.Data.Web.Users
@inherits PhantomComponent
@implements IDisposable
@inject IJSRuntime Js;
@inject InstanceLogManager InstanceLogManager
<div id="log" class="font-monospace mb-3">
@foreach (var line in instanceLogs.EnumerateLast(uint.MaxValue)) {
@ -14,7 +15,7 @@
@code {
[Parameter, EditorRequired]
public Guid InstanceGuid { get; init; }
public Guid InstanceGuid { get; set; }
private IJSObjectReference? PageJs { get; set; }
@ -24,13 +25,11 @@
private readonly Stopwatch recheckPermissionsStopwatch = Stopwatch.StartNew();
protected override void OnInitialized() {
/*
instanceLogsSubs = InstanceLogManager.GetSubs(InstanceGuid);
instanceLogsSubs.Subscribe(this, buffer => {
instanceLogs = buffer;
InvokeAsyncChecked(RefreshLog);
});
*/
}
protected override async Task OnAfterRenderAsync(bool firstRender) {
@ -65,7 +64,7 @@
}
}
protected override void OnDisposed() {
public void Dispose() {
instanceLogsSubs.Unsubscribe(this);
}

View File

@ -6,6 +6,7 @@
@inherits PhantomComponent
@inject IJSRuntime Js;
@inject InstanceManager InstanceManager;
@inject AuditLog AuditLog
<Form Model="form" OnSubmit="StopInstance">
<Modal Id="@ModalId" TitleText="Stop Instance">
@ -32,13 +33,13 @@
@code {
[Parameter, EditorRequired]
public Guid InstanceGuid { get; init; }
public Guid InstanceGuid { get; set; }
[Parameter, EditorRequired]
public string ModalId { get; init; } = string.Empty;
public string ModalId { get; set; } = string.Empty;
[Parameter]
public bool Disabled { get; init; }
public bool Disabled { get; set; }
private readonly StopInstanceFormModel form = new ();
@ -48,11 +49,6 @@
}
private async Task StopInstance(EditContext context) {
var loggedInUserGuid = await GetUserGuid();
if (loggedInUserGuid == null) {
return;
}
await form.SubmitModel.StartSubmitting();
if (!await CheckPermission(Permission.ControlInstances)) {
@ -60,8 +56,9 @@
return;
}
var result = await InstanceManager.StopInstance(loggedInUserGuid.Value, InstanceGuid, new MinecraftStopStrategy(form.StopInSeconds), CancellationToken);
var result = await InstanceManager.StopInstance(InstanceGuid, new MinecraftStopStrategy(form.StopInSeconds));
if (result.Is(StopInstanceResult.StopInitiated)) {
await AuditLog.AddInstanceStoppedEvent(InstanceGuid, form.StopInSeconds);
await Js.InvokeVoidAsync("closeModal", ModalId);
form.SubmitModel.StopSubmitting();
}

View File

@ -1,72 +1,72 @@
@* @using Phantom.Common.Data.Web.Users *@
@* @using Phantom.Utils.Tasks *@
@* @using System.ComponentModel.DataAnnotations *@
@* @inherits PhantomComponent *@
@* @inject IJSRuntime Js; *@
@* *@
@* <Form Model="form" OnSubmit="AddUser"> *@
@* <Modal Id="@ModalId" TitleText="Add User"> *@
@* <Body> *@
@* *@
@* <div class="row"> *@
@* <div class="mb-3"> *@
@* <FormTextInput Id="account-username" Label="Username" @bind-Value="form.Username" autocomplete="off" /> *@
@* </div> *@
@* </div> *@
@* *@
@* <div class="row"> *@
@* <div class="mb-3"> *@
@* <FormTextInput Id="account-password" Label="Password" Type="FormTextInputType.Password" autocomplete="new-password" @bind-Value="form.Password" /> *@
@* </div> *@
@* </div> *@
@* *@
@* </Body> *@
@* <Footer> *@
@* <FormSubmitError /> *@
@* <FormButtonSubmit Label="Add User" class="btn btn-primary" /> *@
@* <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> *@
@* </Footer> *@
@* </Modal> *@
@* </Form> *@
@* *@
@* @code { *@
@* *@
@* [Parameter, EditorRequired] *@
@* public string ModalId { get; set; } = string.Empty; *@
@* *@
@* [Parameter] *@
@* public EventCallback<UserInfo> UserAdded { get; set; } *@
@* *@
@* private readonly AddUserFormModel form = new(); *@
@* *@
@* private sealed class AddUserFormModel : FormModel { *@
@* [Required] *@
@* public string Username { get; set; } = string.Empty; *@
@* *@
@* [Required] *@
@* public string Password { get; set; } = string.Empty; *@
@* } *@
@* *@
@* private async Task AddUser(EditContext context) { *@
@* await form.SubmitModel.StartSubmitting(); *@
@* *@
@* if (!await CheckPermission(Permission.EditUsers)) { *@
@* form.SubmitModel.StopSubmitting("You do not have permission to add users."); *@
@* return; *@
@* } *@
@* *@
@* switch (await UserManager.CreateUser(form.Username, form.Password)) { *@
@* case Result<UserInfo, AddUserError>.Ok ok: *@
@* await AuditLog.AddUserCreatedEvent(ok.Value); *@
@* await UserAdded.InvokeAsync(ok.Value); *@
@* await Js.InvokeVoidAsync("closeModal", ModalId); *@
@* form.SubmitModel.StopSubmitting(); *@
@* break; *@
@* *@
@* case Result<UserInfo, AddUserError>.Fail fail: *@
@* form.SubmitModel.StopSubmitting(fail.Error.ToSentences("\n")); *@
@* break; *@
@* } *@
@* } *@
@* *@
@* } *@
@using Phantom.Common.Data.Web.Users
@using Phantom.Utils.Tasks
@using System.ComponentModel.DataAnnotations
@inherits PhantomComponent
@inject IJSRuntime Js;
<Form Model="form" OnSubmit="AddUser">
<Modal Id="@ModalId" TitleText="Add User">
<Body>
<div class="row">
<div class="mb-3">
<FormTextInput Id="account-username" Label="Username" @bind-Value="form.Username" autocomplete="off" />
</div>
</div>
<div class="row">
<div class="mb-3">
<FormTextInput Id="account-password" Label="Password" Type="FormTextInputType.Password" autocomplete="new-password" @bind-Value="form.Password" />
</div>
</div>
</Body>
<Footer>
<FormSubmitError />
<FormButtonSubmit Label="Add User" class="btn btn-primary" />
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
</Footer>
</Modal>
</Form>
@code {
[Parameter, EditorRequired]
public string ModalId { get; set; } = string.Empty;
[Parameter]
public EventCallback<UserInfo> UserAdded { get; set; }
private readonly AddUserFormModel form = new();
private sealed class AddUserFormModel : FormModel {
[Required]
public string Username { get; set; } = string.Empty;
[Required]
public string Password { get; set; } = string.Empty;
}
private async Task AddUser(EditContext context) {
await form.SubmitModel.StartSubmitting();
if (!await CheckPermission(Permission.EditUsers)) {
form.SubmitModel.StopSubmitting("You do not have permission to add users.");
return;
}
switch (await UserManager.CreateUser(form.Username, form.Password)) {
case Result<UserInfo, AddUserError>.Ok ok:
await AuditLog.AddUserCreatedEvent(ok.Value);
await UserAdded.InvokeAsync(ok.Value);
await Js.InvokeVoidAsync("closeModal", ModalId);
form.SubmitModel.StopSubmitting();
break;
case Result<UserInfo, AddUserError>.Fail fail:
form.SubmitModel.StopSubmitting(fail.Error.ToSentences("\n"));
break;
}
}
}

View File

@ -1,37 +1,37 @@
@* @using Phantom.Common.Data.Web.Users *@
@* @inherits UserEditDialogBase *@
@* @inject UserManager UserManager *@
@* @inject AuditLog AuditLog *@
@* *@
@* <Modal Id="@ModalId" TitleText="Delete User"> *@
@* <Body> *@
@* You are about to delete the user <strong class="fw-semibold">@EditedUserName</strong>.<br> *@
@* This action cannot be undone. *@
@* </Body> *@
@* <Footer> *@
@* <FormSubmitError Model="SubmitModel" /> *@
@* <FormButtonSubmit Model="SubmitModel" Label="Delete User" type="button" class="btn btn-danger" @onclick="Submit" /> *@
@* <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="OnClosed">Cancel</button> *@
@* </Footer> *@
@* </Modal> *@
@* *@
@* @code { *@
@* *@
@* protected override async Task DoEdit(UserInfo user) { *@
@* switch (await UserManager.DeleteByGuid(user.Guid)) { *@
@* case DeleteUserResult.Deleted: *@
@* await AuditLog.AddUserDeletedEvent(user); *@
@* await OnEditSuccess(); *@
@* break; *@
@* *@
@* case DeleteUserResult.NotFound: *@
@* await OnEditSuccess(); *@
@* break; *@
@* *@
@* case DeleteUserResult.Failed: *@
@* OnEditFailure("Could not delete user."); *@
@* break; *@
@* } *@
@* } *@
@* *@
@* } *@
@using Phantom.Common.Data.Web.Users
@inherits UserEditDialogBase
@inject UserManager UserManager
@inject AuditLog AuditLog
<Modal Id="@ModalId" TitleText="Delete User">
<Body>
You are about to delete the user <strong class="fw-semibold">@EditedUserName</strong>.<br>
This action cannot be undone.
</Body>
<Footer>
<FormSubmitError Model="SubmitModel" />
<FormButtonSubmit Model="SubmitModel" Label="Delete User" type="button" class="btn btn-danger" @onclick="Submit" />
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="OnClosed">Cancel</button>
</Footer>
</Modal>
@code {
protected override async Task DoEdit(UserInfo user) {
switch (await UserManager.DeleteByGuid(user.Guid)) {
case DeleteUserResult.Deleted:
await AuditLog.AddUserDeletedEvent(user);
await OnEditSuccess();
break;
case DeleteUserResult.NotFound:
await OnEditSuccess();
break;
case DeleteUserResult.Failed:
OnEditFailure("Could not delete user.");
break;
}
}
}

View File

@ -1,76 +1,76 @@
@* @using Phantom.Common.Data.Web.Users *@
@* @inherits UserEditDialogBase *@
@* *@
@* <Modal Id="@ModalId" TitleText="Manage User Roles"> *@
@* <Body> *@
@* Roles for user: <strong class="fw-semibold">@EditedUserName</strong><br> *@
@* @for (var index = 0; index < items.Count; index++) { *@
@* var item = items[index]; *@
@* <div class="mt-1"> *@
@* <input id="role-@index" type="checkbox" class="form-check-input" @bind="@item.Checked" /> *@
@* <label for="role-@index" class="form-check-label">@item.Role.Name</label> *@
@* </div> *@
@* } *@
@* </Body> *@
@* <Footer> *@
@* <FormSubmitError Model="SubmitModel" /> *@
@* <FormButtonSubmit Model="SubmitModel" Label="Save Roles" type="button" class="btn btn-success" @onclick="Submit" /> *@
@* <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="OnClosed">Cancel</button> *@
@* </Footer> *@
@* </Modal> *@
@* *@
@* @code { *@
@* *@
@* private List<RoleItem> items = new(); *@
@* *@
@* protected override async Task BeforeShown(UserInfo user) { *@
@* var userRoles = await UserRoleManager.GetUserRoleGuids(user); *@
@* var allRoles = await RoleManager.GetAll(); *@
@* this.items = allRoles.Select(role => new RoleItem(role, userRoles.Contains(role.RoleGuid))).ToList(); *@
@* } *@
@* *@
@* protected override async Task DoEdit(UserInfo user) { *@
@* var userRoles = await UserRoleManager.GetUserRoleGuids(user); *@
@* var addedToRoles = new List<string>(); *@
@* var removedFromRoles = new List<string>(); *@
@* var errors = new List<string>(); *@
@* *@
@* foreach (var item in items) { *@
@* var shouldHaveRole = item.Checked; *@
@* if (shouldHaveRole == userRoles.Contains(item.Role.Guid)) { *@
@* continue; *@
@* } *@
@* *@
@* bool success = shouldHaveRole ? await UserRoleManager.Add(user, item.Role) : await UserRoleManager.Remove(user, item.Role); *@
@* if (success) { *@
@* var modifiedList = shouldHaveRole ? addedToRoles : removedFromRoles; *@
@* modifiedList.Add(item.Role.Name); *@
@* } *@
@* else if (shouldHaveRole) { *@
@* errors.Add("Could not add role " + item.Role.Name + " to user."); *@
@* } *@
@* else { *@
@* errors.Add("Could not remove role " + item.Role.Name + " from user."); *@
@* } *@
@* } *@
@* *@
@* if (errors.Count == 0) { *@
@* await AuditLog.AddUserRolesChangedEvent(user, addedToRoles, removedFromRoles); *@
@* await OnEditSuccess(); *@
@* } *@
@* else { *@
@* OnEditFailure(string.Join("\n", errors)); *@
@* } *@
@* } *@
@* *@
@* private sealed class RoleItem { *@
@* public RoleInfo Role { get; } *@
@* public bool Checked { get; set; } *@
@* *@
@* public RoleItem(RoleInfo role, bool @checked) { *@
@* this.Role = role; *@
@* this.Checked = @checked; *@
@* } *@
@* } *@
@* *@
@* } *@
@using Phantom.Common.Data.Web.Users
@inherits UserEditDialogBase
<Modal Id="@ModalId" TitleText="Manage User Roles">
<Body>
Roles for user: <strong class="fw-semibold">@EditedUserName</strong><br>
@for (var index = 0; index < items.Count; index++) {
var item = items[index];
<div class="mt-1">
<input id="role-@index" type="checkbox" class="form-check-input" @bind="@item.Checked" />
<label for="role-@index" class="form-check-label">@item.Role.Name</label>
</div>
}
</Body>
<Footer>
<FormSubmitError Model="SubmitModel" />
<FormButtonSubmit Model="SubmitModel" Label="Save Roles" type="button" class="btn btn-success" @onclick="Submit" />
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="OnClosed">Cancel</button>
</Footer>
</Modal>
@code {
private List<RoleItem> items = new();
protected override async Task BeforeShown(UserInfo user) {
var userRoles = await UserRoleManager.GetUserRoleGuids(user);
var allRoles = await RoleManager.GetAll();
this.items = allRoles.Select(role => new RoleItem(role, userRoles.Contains(role.RoleGuid))).ToList();
}
protected override async Task DoEdit(UserInfo user) {
var userRoles = await UserRoleManager.GetUserRoleGuids(user);
var addedToRoles = new List<string>();
var removedFromRoles = new List<string>();
var errors = new List<string>();
foreach (var item in items) {
var shouldHaveRole = item.Checked;
if (shouldHaveRole == userRoles.Contains(item.Role.Guid)) {
continue;
}
bool success = shouldHaveRole ? await UserRoleManager.Add(user, item.Role) : await UserRoleManager.Remove(user, item.Role);
if (success) {
var modifiedList = shouldHaveRole ? addedToRoles : removedFromRoles;
modifiedList.Add(item.Role.Name);
}
else if (shouldHaveRole) {
errors.Add("Could not add role " + item.Role.Name + " to user.");
}
else {
errors.Add("Could not remove role " + item.Role.Name + " from user.");
}
}
if (errors.Count == 0) {
await AuditLog.AddUserRolesChangedEvent(user, addedToRoles, removedFromRoles);
await OnEditSuccess();
}
else {
OnEditFailure(string.Join("\n", errors));
}
}
private sealed class RoleItem {
public RoleInfo Role { get; }
public bool Checked { get; set; }
public RoleItem(RoleInfo role, bool @checked) {
this.Role = role;
this.Checked = @checked;
}
}
}