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; namespace Phantom.Common.Data.Web.AuditLog;
[MemoryPackable(GenerateType.VersionTolerant)] public sealed record AuditLogItem(DateTime UtcTime, Guid? UserGuid, string? UserName, AuditLogEventType EventType, AuditLogSubjectType SubjectType, string? SubjectId, JsonDocument? Data);
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
);

View File

@ -2,7 +2,6 @@
using Phantom.Common.Data.Java; using Phantom.Common.Data.Java;
using Phantom.Common.Data.Minecraft; using Phantom.Common.Data.Minecraft;
using Phantom.Common.Data.Replies; using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Web.AuditLog;
using Phantom.Common.Data.Web.Instance; using Phantom.Common.Data.Web.Instance;
using Phantom.Common.Data.Web.Users; using Phantom.Common.Data.Web.Users;
using Phantom.Common.Messages.Web.BiDirectional; using Phantom.Common.Messages.Web.BiDirectional;
@ -17,11 +16,7 @@ public interface IMessageToControllerListener {
Task<CreateOrUpdateAdministratorUserResult> HandleCreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUserMessage message); Task<CreateOrUpdateAdministratorUserResult> HandleCreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUserMessage message);
Task<InstanceActionResult<CreateOrUpdateInstanceResult>> HandleCreateOrUpdateInstance(CreateOrUpdateInstanceMessage message); Task<InstanceActionResult<CreateOrUpdateInstanceResult>> HandleCreateOrUpdateInstance(CreateOrUpdateInstanceMessage message);
Task<InstanceActionResult<LaunchInstanceResult>> HandleLaunchInstance(LaunchInstanceMessage 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<ImmutableArray<MinecraftVersion>> HandleGetMinecraftVersions(GetMinecraftVersionsMessage message);
Task<ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>> HandleGetAgentJavaRuntimes(GetAgentJavaRuntimes 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); 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) { public Task<ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>> Accept(IMessageToControllerListener listener) {
return listener.HandleGetAgentJavaRuntimes(this); 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) { public Task<ImmutableArray<MinecraftVersion>> Accept(IMessageToControllerListener listener) {
return listener.HandleGetMinecraftVersions(this); 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) { public Task<LogInSuccess?> Accept(IMessageToControllerListener listener) {
return listener.HandleLogIn(this); 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.Java;
using Phantom.Common.Data.Minecraft; using Phantom.Common.Data.Minecraft;
using Phantom.Common.Data.Replies; using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Web.AuditLog;
using Phantom.Common.Data.Web.Instance; using Phantom.Common.Data.Web.Instance;
using Phantom.Common.Data.Web.Users; using Phantom.Common.Data.Web.Users;
using Phantom.Common.Logging; using Phantom.Common.Logging;
@ -25,11 +24,8 @@ public static class WebMessageRegistries {
ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(2); ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(2);
ToController.Add<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(3); ToController.Add<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(3);
ToController.Add<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(4); ToController.Add<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(4);
ToController.Add<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(5); ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(5);
ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(6); ToController.Add<GetAgentJavaRuntimes, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(6);
ToController.Add<GetAgentJavaRuntimes, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(7);
ToController.Add<GetUsersMessage, ImmutableArray<UserInfo>>(8);
ToController.Add<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(9);
ToController.Add<ReplyMessage>(127); ToController.Add<ReplyMessage>(127);
ToWeb.Add<RegisterWebResultMessage>(0); ToWeb.Add<RegisterWebResultMessage>(0);

View File

@ -3,19 +3,7 @@ using Phantom.Controller.Database.Entities;
namespace Phantom.Controller.Database.Repositories; namespace Phantom.Controller.Database.Repositories;
public sealed class AuditLogRepositoryWriter { sealed partial class AuditLogRepository {
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));
}
public void AddUserLoggedInEvent(UserEntity user) { public void AddUserLoggedInEvent(UserEntity user) {
AddItem(AuditLogEventType.UserLoggedIn, user.UserGuid.ToString()); 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 readonly ILazyDbContext db;
private AuditLogRepositoryWriter? auditLogWriter; private AuditLogRepository? auditLog;
private AuditLogRepositoryWriter AuditLogWriter => this.auditLogWriter ??= new AuditLogRepositoryWriter(db, null); private AuditLogRepository AuditLogRepository => this.auditLog ??= new AuditLogRepository(db, null);
public UserRepository(ILazyDbContext db) { public UserRepository(ILazyDbContext db) {
this.db = db; this.db = db;
@ -91,7 +91,7 @@ public sealed class UserRepository {
var user = new UserEntity(Guid.NewGuid(), username, UserPasswords.Hash(password)); var user = new UserEntity(Guid.NewGuid(), username, UserPasswords.Hash(password));
db.Ctx.Users.Add(user); db.Ctx.Users.Add(user);
AuditLogWriter.AddUserCreatedEvent(user); AuditLogRepository.AddUserCreatedEvent(user);
return user; return user;
} }
@ -103,13 +103,13 @@ public sealed class UserRepository {
} }
user.PasswordHash = UserPasswords.Hash(password); user.PasswordHash = UserPasswords.Hash(password);
AuditLogWriter.AddUserPasswordChangedEvent(user); AuditLogRepository.AddUserPasswordChangedEvent(user);
return Result.Ok<SetUserPasswordError>(); return Result.Ok<SetUserPasswordError>();
} }
public void DeleteUser(UserEntity user) { public void DeleteUser(UserEntity user) {
db.Ctx.Users.Remove(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 PermissionManager PermissionManager { get; }
private UserLoginManager UserLoginManager { get; } private UserLoginManager UserLoginManager { get; }
private AuditLogManager AuditLogManager { get; }
private readonly IDbContextProvider dbProvider; private readonly IDbContextProvider dbProvider;
private readonly AuthToken webAuthToken; private readonly AuthToken webAuthToken;
@ -50,7 +49,6 @@ public sealed class ControllerServices {
this.PermissionManager = new PermissionManager(dbProvider); this.PermissionManager = new PermissionManager(dbProvider);
this.UserLoginManager = new UserLoginManager(UserManager, PermissionManager); this.UserLoginManager = new UserLoginManager(UserManager, PermissionManager);
this.AuditLogManager = new AuditLogManager(dbProvider);
this.dbProvider = dbProvider; this.dbProvider = dbProvider;
this.webAuthToken = webAuthToken; this.webAuthToken = webAuthToken;
@ -62,7 +60,7 @@ public sealed class ControllerServices {
} }
public WebMessageListener CreateWebMessageListener(RpcConnectionToClient<IMessageToWebListener> connection) { 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() { public async Task Initialize() {

View File

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

View File

@ -4,7 +4,6 @@ using Phantom.Common.Data.Java;
using Phantom.Common.Data.Minecraft; using Phantom.Common.Data.Minecraft;
using Phantom.Common.Data.Replies; using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Web.Agent; using Phantom.Common.Data.Web.Agent;
using Phantom.Common.Data.Web.AuditLog;
using Phantom.Common.Data.Web.Instance; using Phantom.Common.Data.Web.Instance;
using Phantom.Common.Data.Web.Users; using Phantom.Common.Data.Web.Users;
using Phantom.Common.Logging; using Phantom.Common.Logging;
@ -26,34 +25,21 @@ namespace Phantom.Controller.Services.Rpc;
public sealed class WebMessageListener : IMessageToControllerListener { public sealed class WebMessageListener : IMessageToControllerListener {
private static readonly ILogger Logger = PhantomLogger.Create<WebMessageListener>(); 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 AuthToken authToken;
private readonly UserManager userManager; private readonly UserManager userManager;
private readonly UserLoginManager userLoginManager; private readonly UserLoginManager userLoginManager;
private readonly AuditLogManager auditLogManager;
private readonly AgentManager agentManager; private readonly AgentManager agentManager;
private readonly AgentJavaRuntimesManager agentJavaRuntimesManager; private readonly AgentJavaRuntimesManager agentJavaRuntimesManager;
private readonly InstanceManager instanceManager; private readonly InstanceManager instanceManager;
private readonly MinecraftVersions minecraftVersions; private readonly MinecraftVersions minecraftVersions;
private readonly TaskManager taskManager; private readonly TaskManager taskManager;
internal WebMessageListener( internal WebMessageListener(RpcConnectionToClient<IMessageToWebListener> connection, AuthToken authToken, UserManager userManager, UserLoginManager userLoginManager, AgentManager agentManager, AgentJavaRuntimesManager agentJavaRuntimesManager, InstanceManager instanceManager, MinecraftVersions minecraftVersions, TaskManager taskManager) {
RpcConnectionToClient<IMessageToWebListener> connection,
AuthToken authToken,
UserManager userManager,
UserLoginManager userLoginManager,
AuditLogManager auditLogManager,
AgentManager agentManager,
AgentJavaRuntimesManager agentJavaRuntimesManager,
InstanceManager instanceManager,
MinecraftVersions minecraftVersions,
TaskManager taskManager
) {
this.connection = connection; this.connection = connection;
this.authToken = authToken; this.authToken = authToken;
this.userManager = userManager; this.userManager = userManager;
this.userLoginManager = userLoginManager; this.userLoginManager = userLoginManager;
this.auditLogManager = auditLogManager;
this.agentManager = agentManager; this.agentManager = agentManager;
this.agentJavaRuntimesManager = agentJavaRuntimesManager; this.agentJavaRuntimesManager = agentJavaRuntimesManager;
this.instanceManager = instanceManager; this.instanceManager = instanceManager;
@ -75,7 +61,6 @@ public sealed class WebMessageListener : IMessageToControllerListener {
agentManager.AgentsChanged.Subscribe(this, HandleAgentsChanged); agentManager.AgentsChanged.Subscribe(this, HandleAgentsChanged);
instanceManager.InstancesChanged.Subscribe(this, HandleInstancesChanged); instanceManager.InstancesChanged.Subscribe(this, HandleInstancesChanged);
// TODO unsubscribe on closed
return NoReply.Instance; return NoReply.Instance;
} }
@ -101,14 +86,6 @@ public sealed class WebMessageListener : IMessageToControllerListener {
return instanceManager.LaunchInstance(message.LoggedInUserGuid, message.InstanceGuid); 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) { public Task<ImmutableArray<MinecraftVersion>> HandleGetMinecraftVersions(GetMinecraftVersionsMessage message) {
return minecraftVersions.GetVersions(CancellationToken.None); return minecraftVersions.GetVersions(CancellationToken.None);
} }
@ -117,14 +94,6 @@ public sealed class WebMessageListener : IMessageToControllerListener {
return Task.FromResult(agentJavaRuntimesManager.All); 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) { public Task<LogInSuccess?> HandleLogIn(LogInMessage message) {
return userLoginManager.LogIn(message.Username, message.Password); 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) { public async Task<PermissionSet> FetchPermissionsForUserId(Guid userId) {
await using var ctx = dbProvider.Eager(); 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.Data.Web.Users.CreateOrUpdateAdministratorUserResults;
using Phantom.Common.Logging; using Phantom.Common.Logging;
using Phantom.Controller.Database; using Phantom.Controller.Database;
@ -18,32 +17,24 @@ sealed class UserManager {
this.dbProvider = dbProvider; 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) { public async Task<UserEntity?> GetAuthenticated(string username, string password) {
await using var db = dbProvider.Lazy(); 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; return user != null && UserPasswords.Verify(password, user.PasswordHash) ? user : null;
} }
public async Task<CreateOrUpdateAdministratorUserResult> CreateOrUpdateAdministrator(string username, string password) { public async Task<CreateOrUpdateAdministratorUserResult> CreateOrUpdateAdministrator(string username, string password) {
await using var db = dbProvider.Lazy(); await using var db = dbProvider.Lazy();
var userRepository = new UserRepository(db); var repository = new UserRepository(db);
try { try {
bool wasCreated; bool wasCreated;
var user = await userRepository.GetByName(username); var user = await repository.GetByName(username);
if (user == null) { if (user == null) {
var result = await userRepository.CreateUser(username, password); var result = await repository.CreateUser(username, password);
if (result) { if (result) {
user = result.Value; user = result.Value;
wasCreated = true; wasCreated = true;
@ -53,7 +44,7 @@ sealed class UserManager {
} }
} }
else { else {
var result = userRepository.SetUserPassword(user, password); var result = repository.SetUserPassword(user, password);
if (!result) { if (!result) {
return new UpdatingFailed(result.Error); return new UpdatingFailed(result.Error);
} }
@ -86,15 +77,15 @@ sealed class UserManager {
public async Task<DeleteUserResult> DeleteByGuid(Guid guid) { public async Task<DeleteUserResult> DeleteByGuid(Guid guid) {
await using var db = dbProvider.Lazy(); 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) { if (user == null) {
return DeleteUserResult.NotFound; return DeleteUserResult.NotFound;
} }
try { try {
userRepository.DeleteUser(user); repository.DeleteUser(user);
await db.Ctx.SaveChangesAsync(); await db.Ctx.SaveChangesAsync();
Logger.Information("Deleted user \"{Username}\" (GUID {Guid}).", user.Name, user.UserGuid); Logger.Information("Deleted user \"{Username}\" (GUID {Guid}).", user.Name, user.UserGuid);

View File

@ -1,6 +1,5 @@
using System.Collections.Immutable; using System.Collections.Immutable;
using Phantom.Common.Data.Instance; using Phantom.Common.Data.Instance;
using Phantom.Common.Data.Minecraft;
using Phantom.Common.Data.Replies; using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Web.Instance; using Phantom.Common.Data.Web.Instance;
using Phantom.Common.Logging; using Phantom.Common.Logging;
@ -26,31 +25,17 @@ public sealed class InstanceManager {
instances.SetTo(newInstances.ToImmutableDictionary(static instance => instance.Configuration.InstanceGuid)); instances.SetTo(newInstances.ToImmutableDictionary(static instance => instance.Configuration.InstanceGuid));
} }
public InstanceDictionary GetAll() {
return instances.Value;
}
public Instance? GetByGuid(Guid instanceGuid) { public Instance? GetByGuid(Guid instanceGuid) {
return instances.Value.GetValueOrDefault(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); 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); var message = new LaunchInstanceMessage(loggedInUserGuid, instanceGuid);
return controllerConnection.Send<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(message, cancellationToken); return controllerConnection.Send<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(message, TimeSpan.FromSeconds(30));
}
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);
} }
} }

View File

@ -6,7 +6,6 @@ using Phantom.Web.Services.Authentication;
using Phantom.Web.Services.Authorization; using Phantom.Web.Services.Authorization;
using Phantom.Web.Services.Instances; using Phantom.Web.Services.Instances;
using Phantom.Web.Services.Rpc; using Phantom.Web.Services.Rpc;
using Phantom.Web.Services.Users;
namespace Phantom.Web.Services; namespace Phantom.Web.Services;
@ -19,11 +18,9 @@ public static class PhantomWebServices {
services.AddSingleton<InstanceManager>(); services.AddSingleton<InstanceManager>();
services.AddSingleton<PermissionManager>(); services.AddSingleton<PermissionManager>();
services.AddSingleton<UserManager>();
services.AddSingleton<UserSessionManager>(); services.AddSingleton<UserSessionManager>();
services.AddSingleton<AuditLogManager>();
services.AddScoped<UserLoginManager>();
services.AddScoped<UserSessionBrowserStorage>(); services.AddScoped<UserSessionBrowserStorage>();
services.AddScoped<UserLoginManager>();
services.AddScoped<CustomAuthenticationStateProvider>(); services.AddScoped<CustomAuthenticationStateProvider>();
services.AddScoped<AuthenticationStateProvider>(static services => services.GetRequiredService<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> { 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); 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; namespace Phantom.Web.Services.Users;
using Phantom.Common.Data.Web.Users;
using Phantom.Common.Messages.Web.ToController;
using Phantom.Web.Services.Rpc;
namespace Phantom.Web.Services.Users;
public sealed class UserManager { 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; namespace Phantom.Web.Base;
public abstract class PhantomComponent : ComponentBase, IDisposable { public abstract class PhantomComponent : ComponentBase {
private static readonly ILogger Logger = PhantomLogger.Create<PhantomComponent>(); private static readonly ILogger Logger = PhantomLogger.Create<PhantomComponent>();
[CascadingParameter] [CascadingParameter]
@ -17,10 +17,6 @@ public abstract class PhantomComponent : ComponentBase, IDisposable {
[Inject] [Inject]
public PermissionManager PermissionManager { get; set; } = null!; public PermissionManager PermissionManager { get; set; } = null!;
private readonly CancellationTokenSource cancellationTokenSource = new ();
public CancellationToken CancellationToken => cancellationTokenSource.Token;
public async Task<Guid?> GetUserGuid() { public async Task<Guid?> GetUserGuid() {
var authenticationState = await AuthenticationStateTask; var authenticationState = await AuthenticationStateTask;
return UserInfo.TryGetGuid(authenticationState.User); return UserInfo.TryGetGuid(authenticationState.User);
@ -34,13 +30,4 @@ public abstract class PhantomComponent : ComponentBase, IDisposable {
protected void InvokeAsyncChecked(Func<Task> task) { protected void InvokeAsyncChecked(Func<Task> task) {
InvokeAsync(task).ContinueWith(static t => Logger.Error(t.Exception, "Caught exception in async task."), TaskContinuationOptions.OnlyOnFaulted); 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.Common.Data.Web.Agent
@using Phantom.Utils.Collections @using Phantom.Utils.Collections
@using Phantom.Web.Services.Agents @using Phantom.Web.Services.Agents
@inherits PhantomComponent @implements IDisposable
@inject AgentManager AgentManager @inject AgentManager AgentManager
<h1>Agents</h1> <h1>Agents</h1>
@ -85,7 +85,7 @@
}); });
} }
protected override void OnDisposed() { void IDisposable.Dispose() {
AgentManager.AgentsChanged.Unsubscribe(this); AgentManager.AgentsChanged.Unsubscribe(this);
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -338,7 +338,7 @@
JvmArgumentsHelper.Split(form.JvmArguments) 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)) { if (result.Is(CreateOrUpdateInstanceResult.Success)) {
await Nav.NavigateTo("instances/" + instance.InstanceGuid); await Nav.NavigateTo("instances/" + instance.InstanceGuid);
} }

View File

@ -15,7 +15,7 @@
</Form> </Form>
@code { @code {
[Parameter] [Parameter]
public Guid InstanceGuid { get; set; } public Guid InstanceGuid { get; set; }
@ -23,19 +23,14 @@
public bool Disabled { get; set; } public bool Disabled { get; set; }
private readonly SendCommandFormModel form = new (); private readonly SendCommandFormModel form = new ();
private sealed class SendCommandFormModel : FormModel { private sealed class SendCommandFormModel : FormModel {
public string Command { get; set; } = string.Empty; public string Command { get; set; } = string.Empty;
} }
private ElementReference commandInputElement; private ElementReference commandInputElement;
private async Task ExecuteCommand(EditContext context) { private async Task ExecuteCommand(EditContext context) {
var loggedInUserGuid = await GetUserGuid();
if (loggedInUserGuid == null) {
return;
}
await form.SubmitModel.StartSubmitting(); await form.SubmitModel.StartSubmitting();
if (!await CheckPermission(Permission.ControlInstances)) { if (!await CheckPermission(Permission.ControlInstances)) {
@ -43,7 +38,7 @@
return; 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)) { if (result.Is(SendCommandToInstanceResult.Success)) {
form.Command = string.Empty; form.Command = string.Empty;
form.SubmitModel.StopSubmitting(); form.SubmitModel.StopSubmitting();
@ -54,5 +49,5 @@
await commandInputElement.FocusAsync(preventScroll: true); await commandInputElement.FocusAsync(preventScroll: true);
} }
} }

View File

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

View File

@ -6,6 +6,7 @@
@inherits PhantomComponent @inherits PhantomComponent
@inject IJSRuntime Js; @inject IJSRuntime Js;
@inject InstanceManager InstanceManager; @inject InstanceManager InstanceManager;
@inject AuditLog AuditLog
<Form Model="form" OnSubmit="StopInstance"> <Form Model="form" OnSubmit="StopInstance">
<Modal Id="@ModalId" TitleText="Stop Instance"> <Modal Id="@ModalId" TitleText="Stop Instance">
@ -32,13 +33,13 @@
@code { @code {
[Parameter, EditorRequired] [Parameter, EditorRequired]
public Guid InstanceGuid { get; init; } public Guid InstanceGuid { get; set; }
[Parameter, EditorRequired] [Parameter, EditorRequired]
public string ModalId { get; init; } = string.Empty; public string ModalId { get; set; } = string.Empty;
[Parameter] [Parameter]
public bool Disabled { get; init; } public bool Disabled { get; set; }
private readonly StopInstanceFormModel form = new (); private readonly StopInstanceFormModel form = new ();
@ -48,20 +49,16 @@
} }
private async Task StopInstance(EditContext context) { private async Task StopInstance(EditContext context) {
var loggedInUserGuid = await GetUserGuid();
if (loggedInUserGuid == null) {
return;
}
await form.SubmitModel.StartSubmitting(); await form.SubmitModel.StartSubmitting();
if (!await CheckPermission(Permission.ControlInstances)) { if (!await CheckPermission(Permission.ControlInstances)) {
form.SubmitModel.StopSubmitting("You do not have permission to stop instances."); form.SubmitModel.StopSubmitting("You do not have permission to stop instances.");
return; 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)) { if (result.Is(StopInstanceResult.StopInitiated)) {
await AuditLog.AddInstanceStoppedEvent(InstanceGuid, form.StopInSeconds);
await Js.InvokeVoidAsync("closeModal", ModalId); await Js.InvokeVoidAsync("closeModal", ModalId);
form.SubmitModel.StopSubmitting(); form.SubmitModel.StopSubmitting();
} }

View File

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

View File

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

View File

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