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
5e11a83d1b
WIP 2023-11-24 17:48:15 +01:00
4df279a249
Reimplement Web service 2023-11-24 17:48:03 +01:00
39 changed files with 737 additions and 504 deletions

View File

@ -1,5 +1,14 @@
using System.Text.Json; using MemoryPack;
namespace Phantom.Common.Data.Web.AuditLog; namespace Phantom.Common.Data.Web.AuditLog;
public sealed record AuditLogItem(DateTime UtcTime, Guid? UserGuid, string? UserName, AuditLogEventType EventType, AuditLogSubjectType SubjectType, string? SubjectId, JsonDocument? Data); [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
);

View File

@ -2,6 +2,7 @@
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;
@ -16,7 +17,11 @@ 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

@ -0,0 +1,14 @@
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

@ -0,0 +1,12 @@
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

@ -0,0 +1,15 @@
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

@ -0,0 +1,16 @@
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,6 +2,7 @@
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;
@ -24,8 +25,11 @@ 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<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(5); ToController.Add<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(5);
ToController.Add<GetAgentJavaRuntimes, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(6); 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<ReplyMessage>(127); ToController.Add<ReplyMessage>(127);
ToWeb.Add<RegisterWebResultMessage>(0); ToWeb.Add<RegisterWebResultMessage>(0);

View File

@ -3,7 +3,19 @@ using Phantom.Controller.Database.Entities;
namespace Phantom.Controller.Database.Repositories; namespace Phantom.Controller.Database.Repositories;
sealed partial class AuditLogRepository { 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));
}
public void AddUserLoggedInEvent(UserEntity user) { public void AddUserLoggedInEvent(UserEntity user) {
AddItem(AuditLogEventType.UserLoggedIn, user.UserGuid.ToString()); AddItem(AuditLogEventType.UserLoggedIn, user.UserGuid.ToString());
} }

View File

@ -1,30 +0,0 @@
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

@ -0,0 +1,26 @@
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 AuditLogRepository? auditLog; private AuditLogRepositoryWriter? auditLogWriter;
private AuditLogRepository AuditLogRepository => this.auditLog ??= new AuditLogRepository(db, null); private AuditLogRepositoryWriter AuditLogWriter => this.auditLogWriter ??= new AuditLogRepositoryWriter(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);
AuditLogRepository.AddUserCreatedEvent(user); AuditLogWriter.AddUserCreatedEvent(user);
return user; return user;
} }
@ -103,13 +103,13 @@ public sealed class UserRepository {
} }
user.PasswordHash = UserPasswords.Hash(password); user.PasswordHash = UserPasswords.Hash(password);
AuditLogRepository.AddUserPasswordChangedEvent(user); AuditLogWriter.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);
AuditLogRepository.AddUserDeletedEvent(user); AuditLogWriter.AddUserDeletedEvent(user);
} }
} }

View File

@ -29,6 +29,7 @@ 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;
@ -49,6 +50,7 @@ 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;
@ -60,7 +62,7 @@ public sealed class ControllerServices {
} }
public WebMessageListener CreateWebMessageListener(RpcConnectionToClient<IMessageToWebListener> connection) { public WebMessageListener CreateWebMessageListener(RpcConnectionToClient<IMessageToWebListener> connection) {
return new WebMessageListener(connection, webAuthToken, UserManager, UserLoginManager, AgentManager, AgentJavaRuntimesManager, InstanceManager, MinecraftVersions, TaskManager); return new WebMessageListener(connection, webAuthToken, UserManager, UserLoginManager, AuditLogManager, 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 auditLogRepository = new AuditLogRepository(db, auditLogUserGuid); var auditLogWriter = new AuditLogRepositoryWriter(db, auditLogUserGuid);
if (isNewInstance) { if (isNewInstance) {
auditLogRepository.AddInstanceCreatedEvent(configuration.InstanceGuid); auditLogWriter.AddInstanceCreatedEvent(configuration.InstanceGuid);
} }
else { else {
auditLogRepository.AddInstanceEditedEvent(configuration.InstanceGuid); auditLogWriter.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.AddInstanceLaunchedEvent(instanceGuid)); await HandleInstanceManuallyLaunchedOrStopped(instanceGuid, false, auditLogUserGuid, auditLogRepository => auditLogRepository.AddInstanceStoppedEvent(instanceGuid, stopStrategy.Seconds));
} }
return result; return result;
} }
private async Task HandleInstanceManuallyLaunchedOrStopped(Guid instanceGuid, bool wasLaunched, Guid auditLogUserGuid, Action<AuditLogRepository> addAuditEvent) { private async Task HandleInstanceManuallyLaunchedOrStopped(Guid instanceGuid, bool wasLaunched, Guid auditLogUserGuid, Action<AuditLogRepositoryWriter> 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,10 +203,7 @@ 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 {
@ -214,10 +211,12 @@ sealed class InstanceManager {
} }
} }
public async Task<InstanceActionResult<SendCommandToInstanceResult>> SendCommand(Guid instanceGuid, string command) { public async Task<InstanceActionResult<SendCommandToInstanceResult>> SendCommand(Guid auditLogUserId, 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)) {
// TODO audit log await using var db = dbProvider.Lazy();
new AuditLogRepositoryWriter(db, auditLogUserId).AddInstanceCommandExecutedEvent(instanceGuid, command);
await db.Ctx.SaveChangesAsync(cancellationToken);
} }
return result; return result;

View File

@ -4,6 +4,7 @@ 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;
@ -25,21 +26,34 @@ 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; private readonly RpcConnectionToClient<IMessageToWebListener> connection; // TODO use single queue
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(RpcConnectionToClient<IMessageToWebListener> connection, AuthToken authToken, UserManager userManager, UserLoginManager userLoginManager, AgentManager agentManager, AgentJavaRuntimesManager agentJavaRuntimesManager, InstanceManager instanceManager, MinecraftVersions minecraftVersions, 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
) {
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;
@ -61,6 +75,7 @@ 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;
} }
@ -86,6 +101,14 @@ 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);
} }
@ -94,6 +117,14 @@ 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

@ -0,0 +1,19 @@
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,6 +36,20 @@ 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,4 +1,5 @@
using Phantom.Common.Data.Web.Users; using System.Collections.Immutable;
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;
@ -17,24 +18,32 @@ 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 repository = new UserRepository(db); var userRepository = new UserRepository(db);
var user = await repository.GetByName(username); var user = await userRepository.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 repository = new UserRepository(db); var userRepository = new UserRepository(db);
try { try {
bool wasCreated; bool wasCreated;
var user = await repository.GetByName(username); var user = await userRepository.GetByName(username);
if (user == null) { if (user == null) {
var result = await repository.CreateUser(username, password); var result = await userRepository.CreateUser(username, password);
if (result) { if (result) {
user = result.Value; user = result.Value;
wasCreated = true; wasCreated = true;
@ -44,7 +53,7 @@ sealed class UserManager {
} }
} }
else { else {
var result = repository.SetUserPassword(user, password); var result = userRepository.SetUserPassword(user, password);
if (!result) { if (!result) {
return new UpdatingFailed(result.Error); return new UpdatingFailed(result.Error);
} }
@ -77,15 +86,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 repository = new UserRepository(db); var userRepository = new UserRepository(db);
var user = await repository.GetByGuid(guid); var user = await userRepository.GetByGuid(guid);
if (user == null) { if (user == null) {
return DeleteUserResult.NotFound; return DeleteUserResult.NotFound;
} }
try { try {
repository.DeleteUser(user); userRepository.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,5 +1,6 @@
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;
@ -25,17 +26,31 @@ 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) { public Task<InstanceActionResult<CreateOrUpdateInstanceResult>> CreateOrUpdateInstance(Guid loggedInUserGuid, InstanceConfiguration configuration, CancellationToken cancellationToken) {
var message = new CreateOrUpdateInstanceMessage(loggedInUserGuid, configuration); var message = new CreateOrUpdateInstanceMessage(loggedInUserGuid, configuration);
return controllerConnection.Send<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(message, TimeSpan.FromSeconds(30)); return controllerConnection.Send<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(message, cancellationToken);
} }
public Task<InstanceActionResult<LaunchInstanceResult>> LaunchInstance(Guid loggedInUserGuid, Guid instanceGuid) { public Task<InstanceActionResult<LaunchInstanceResult>> LaunchInstance(Guid loggedInUserGuid, Guid instanceGuid, CancellationToken cancellationToken) {
var message = new LaunchInstanceMessage(loggedInUserGuid, instanceGuid); var message = new LaunchInstanceMessage(loggedInUserGuid, instanceGuid);
return controllerConnection.Send<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(message, TimeSpan.FromSeconds(30)); 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);
} }
} }

View File

@ -6,6 +6,7 @@ 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;
@ -18,9 +19,11 @@ 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.AddScoped<UserSessionBrowserStorage>(); services.AddSingleton<AuditLogManager>();
services.AddScoped<UserLoginManager>(); services.AddScoped<UserLoginManager>();
services.AddScoped<UserSessionBrowserStorage>();
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,4 +17,8 @@ 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

@ -0,0 +1,19 @@
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,5 +1,18 @@
namespace Phantom.Web.Services.Users; 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;
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 { public abstract class PhantomComponent : ComponentBase, IDisposable {
private static readonly ILogger Logger = PhantomLogger.Create<PhantomComponent>(); private static readonly ILogger Logger = PhantomLogger.Create<PhantomComponent>();
[CascadingParameter] [CascadingParameter]
@ -17,6 +17,10 @@ public abstract class PhantomComponent : ComponentBase {
[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);
@ -30,4 +34,13 @@ public abstract class PhantomComponent : ComponentBase {
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
@implements IDisposable @inherits PhantomComponent
@inject AgentManager AgentManager @inject AgentManager AgentManager
<h1>Agents</h1> <h1>Agents</h1>
@ -85,7 +85,7 @@
}); });
} }
void IDisposable.Dispose() { protected override void OnDisposed() {
AgentManager.AgentsChanged.Unsubscribe(this); AgentManager.AgentsChanged.Unsubscribe(this);
} }

View File

@ -1,11 +1,12 @@
@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 System.Collections.Immutable @using Phantom.Web.Services.Users
@using Phantom.Web.Services.Instances @using Phantom.Web.Services.Instances
@implements IDisposable @inherits PhantomComponent
@inject AuditLog AuditLog @inject AuditLogManager AuditLogManager
@inject InstanceManager InstanceManager @inject InstanceManager InstanceManager
@inject UserManager UserManager @inject UserManager UserManager
@ -42,7 +43,7 @@
<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.JsonData</code>
</td> </td>
</tr> </tr>
} }
@ -52,8 +53,8 @@
@code { @code {
private CancellationTokenSource? initializationCancellationTokenSource; private CancellationTokenSource? initializationCancellationTokenSource;
private AuditLogItem[] logItems = Array.Empty<AuditLogItem>(); private ImmutableArray<AuditLogItem> logItems = ImmutableArray<AuditLogItem>.Empty;
private Dictionary<Guid, string>? userNamesByGuid; private ImmutableDictionary<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() {
@ -61,14 +62,14 @@
var cancellationToken = initializationCancellationTokenSource.Token; var cancellationToken = initializationCancellationTokenSource.Token;
try { try {
logItems = await AuditLog.GetItems(50, cancellationToken); logItems = await AuditLogManager.GetMostRecentItems(50, cancellationToken);
userNamesByGuid = await UserManager.GetAllByGuid(static user => user.Name, cancellationToken); userNamesByGuid = (await UserManager.GetAll(cancellationToken)).ToImmutableDictionary(static user => user.Guid, static user => user.Name);
instanceNamesByGuid = InstanceManager.GetInstanceNames(); instanceNamesByGuid = InstanceManager.GetAll().Values.ToImmutableDictionary(static instance => instance.Configuration.InstanceGuid, static instance => instance.Configuration.InstanceName);
} 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,
@ -77,7 +78,7 @@
}; };
} }
public void Dispose() { protected override void OnDisposed() {
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)] *@
@using Phantom.Common.Data.Web.EventLog @* @inherits PhantomComponent *@
@using Phantom.Common.Data.Web.Users @* @using Phantom.Common.Data.Web.EventLog *@
@using System.Collections.Immutable @* @using Phantom.Common.Data.Web.Users *@
@using System.Diagnostics @* @using System.Collections.Immutable *@
@using Phantom.Web.Services.Instances @* @using System.Diagnostics *@
@implements IDisposable @* @using Phantom.Web.Services.Instances *@
@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,12 +1,11 @@
@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
@implements IDisposable @inherits PhantomComponent
@inject InstanceManager InstanceManager @inject InstanceManager InstanceManager
@if (Instance == null) { @if (Instance == null) {
@ -78,7 +77,7 @@ else {
return; return;
} }
var result = await InstanceManager.LaunchInstance(loggedInUserGuid.Value, InstanceGuid); var result = await InstanceManager.LaunchInstance(loggedInUserGuid.Value, InstanceGuid, CancellationToken);
if (!result.Is(LaunchInstanceResult.LaunchInitiated)) { if (!result.Is(LaunchInstanceResult.LaunchInitiated)) {
lastError = result.ToSentence(Messages.ToSentence); lastError = result.ToSentence(Messages.ToSentence);
} }
@ -87,7 +86,7 @@ else {
} }
} }
public void Dispose() { protected override void OnDisposed() {
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
@implements IDisposable @inherits PhantomComponent
@inject AgentManager AgentManager @inject AgentManager AgentManager
@inject InstanceManager InstanceManager @inject InstanceManager InstanceManager
@ -91,7 +91,7 @@
}); });
} }
void IDisposable.Dispose() { protected override void OnDisposed() {
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); var result = await InstanceManager.CreateOrUpdateInstance(loggedInUserGuid.Value, instance, CancellationToken);
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,14 +23,19 @@
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)) {
@ -38,7 +43,7 @@
return; return;
} }
var result = await InstanceManager.SendCommand(InstanceGuid, form.Command); var result = await InstanceManager.SendCommandToInstance(loggedInUserGuid.Value, InstanceGuid, form.Command, CancellationToken);
if (result.Is(SendCommandToInstanceResult.Success)) { if (result.Is(SendCommandToInstanceResult.Success)) {
form.Command = string.Empty; form.Command = string.Empty;
form.SubmitModel.StopSubmitting(); form.SubmitModel.StopSubmitting();
@ -49,5 +54,5 @@
await commandInputElement.FocusAsync(preventScroll: true); await commandInputElement.FocusAsync(preventScroll: true);
} }
} }

View File

@ -1,10 +1,9 @@
@inherits PhantomComponent @using Phantom.Utils.Collections
@using Phantom.Utils.Collections @using Phantom.Utils.Events
@using System.Diagnostics @using System.Diagnostics
@using Phantom.Common.Data.Web.Users @using Phantom.Common.Data.Web.Users
@implements IDisposable @inherits PhantomComponent
@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)) {
@ -15,7 +14,7 @@
@code { @code {
[Parameter, EditorRequired] [Parameter, EditorRequired]
public Guid InstanceGuid { get; set; } public Guid InstanceGuid { get; init; }
private IJSObjectReference? PageJs { get; set; } private IJSObjectReference? PageJs { get; set; }
@ -25,11 +24,13 @@
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) {
@ -56,7 +57,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();
@ -64,7 +65,7 @@
} }
} }
public void Dispose() { protected override void OnDisposed() {
instanceLogsSubs.Unsubscribe(this); instanceLogsSubs.Unsubscribe(this);
} }

View File

@ -6,7 +6,6 @@
@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">
@ -33,13 +32,13 @@
@code { @code {
[Parameter, EditorRequired] [Parameter, EditorRequired]
public Guid InstanceGuid { get; set; } public Guid InstanceGuid { get; init; }
[Parameter, EditorRequired] [Parameter, EditorRequired]
public string ModalId { get; set; } = string.Empty; public string ModalId { get; init; } = string.Empty;
[Parameter] [Parameter]
public bool Disabled { get; set; } public bool Disabled { get; init; }
private readonly StopInstanceFormModel form = new (); private readonly StopInstanceFormModel form = new ();
@ -49,16 +48,20 @@
} }
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(InstanceGuid, new MinecraftStopStrategy(form.StopInSeconds)); var result = await InstanceManager.StopInstance(loggedInUserGuid.Value, InstanceGuid, new MinecraftStopStrategy(form.StopInSeconds), CancellationToken);
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; *@
} @* } *@
} @* } *@
@* *@
} @* } *@