mirror of
https://github.com/chylex/Minecraft-Phantom-Panel.git
synced 2024-11-25 16:42:54 +01:00
Compare commits
2 Commits
e796a364f4
...
5e11a83d1b
Author | SHA1 | Date | |
---|---|---|---|
5e11a83d1b | |||
4df279a249 |
@ -1,5 +1,14 @@
|
||||
using System.Text.Json;
|
||||
using MemoryPack;
|
||||
|
||||
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
|
||||
);
|
||||
|
@ -2,6 +2,7 @@
|
||||
using Phantom.Common.Data.Java;
|
||||
using Phantom.Common.Data.Minecraft;
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Common.Data.Web.AuditLog;
|
||||
using Phantom.Common.Data.Web.Instance;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
using Phantom.Common.Messages.Web.BiDirectional;
|
||||
@ -16,7 +17,11 @@ public interface IMessageToControllerListener {
|
||||
Task<CreateOrUpdateAdministratorUserResult> HandleCreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUserMessage message);
|
||||
Task<InstanceActionResult<CreateOrUpdateInstanceResult>> HandleCreateOrUpdateInstance(CreateOrUpdateInstanceMessage message);
|
||||
Task<InstanceActionResult<LaunchInstanceResult>> HandleLaunchInstance(LaunchInstanceMessage message);
|
||||
Task<InstanceActionResult<StopInstanceResult>> HandleStopInstance(StopInstanceMessage message);
|
||||
Task<InstanceActionResult<SendCommandToInstanceResult>> HandleSendCommandToInstance(SendCommandToInstanceMessage message);
|
||||
Task<ImmutableArray<MinecraftVersion>> HandleGetMinecraftVersions(GetMinecraftVersionsMessage message);
|
||||
Task<ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>> HandleGetAgentJavaRuntimes(GetAgentJavaRuntimes message);
|
||||
Task<ImmutableArray<UserInfo>> HandleGetUsers(GetUsersMessage message);
|
||||
Task<ImmutableArray<AuditLogItem>> HandleGetAuditLog(GetAuditLogMessage message);
|
||||
Task<NoReply> HandleReply(ReplyMessage message);
|
||||
}
|
||||
|
@ -9,4 +9,4 @@ public sealed partial record GetAgentJavaRuntimes : IMessageToController<Immutab
|
||||
public Task<ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>> Accept(IMessageToControllerListener listener) {
|
||||
return listener.HandleGetAgentJavaRuntimes(this);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -9,4 +9,4 @@ public sealed partial record GetMinecraftVersionsMessage : IMessageToController<
|
||||
public Task<ImmutableArray<MinecraftVersion>> Accept(IMessageToControllerListener listener) {
|
||||
return listener.HandleGetMinecraftVersions(this);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -11,4 +11,4 @@ public sealed partial record LogInMessage(
|
||||
public Task<LogInSuccess?> Accept(IMessageToControllerListener listener) {
|
||||
return listener.HandleLogIn(this);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
using Phantom.Common.Data.Java;
|
||||
using Phantom.Common.Data.Minecraft;
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Common.Data.Web.AuditLog;
|
||||
using Phantom.Common.Data.Web.Instance;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
using Phantom.Common.Logging;
|
||||
@ -24,8 +25,11 @@ public static class WebMessageRegistries {
|
||||
ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(2);
|
||||
ToController.Add<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(3);
|
||||
ToController.Add<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(4);
|
||||
ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(5);
|
||||
ToController.Add<GetAgentJavaRuntimes, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(6);
|
||||
ToController.Add<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(5);
|
||||
ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(6);
|
||||
ToController.Add<GetAgentJavaRuntimes, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(7);
|
||||
ToController.Add<GetUsersMessage, ImmutableArray<UserInfo>>(8);
|
||||
ToController.Add<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(9);
|
||||
ToController.Add<ReplyMessage>(127);
|
||||
|
||||
ToWeb.Add<RegisterWebResultMessage>(0);
|
||||
|
@ -3,7 +3,19 @@ using Phantom.Controller.Database.Entities;
|
||||
|
||||
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) {
|
||||
AddItem(AuditLogEventType.UserLoggedIn, user.UserGuid.ToString());
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -50,8 +50,8 @@ public sealed class UserRepository {
|
||||
|
||||
private readonly ILazyDbContext db;
|
||||
|
||||
private AuditLogRepository? auditLog;
|
||||
private AuditLogRepository AuditLogRepository => this.auditLog ??= new AuditLogRepository(db, null);
|
||||
private AuditLogRepositoryWriter? auditLogWriter;
|
||||
private AuditLogRepositoryWriter AuditLogWriter => this.auditLogWriter ??= new AuditLogRepositoryWriter(db, null);
|
||||
|
||||
public UserRepository(ILazyDbContext db) {
|
||||
this.db = db;
|
||||
@ -91,7 +91,7 @@ public sealed class UserRepository {
|
||||
var user = new UserEntity(Guid.NewGuid(), username, UserPasswords.Hash(password));
|
||||
|
||||
db.Ctx.Users.Add(user);
|
||||
AuditLogRepository.AddUserCreatedEvent(user);
|
||||
AuditLogWriter.AddUserCreatedEvent(user);
|
||||
|
||||
return user;
|
||||
}
|
||||
@ -103,13 +103,13 @@ public sealed class UserRepository {
|
||||
}
|
||||
|
||||
user.PasswordHash = UserPasswords.Hash(password);
|
||||
AuditLogRepository.AddUserPasswordChangedEvent(user);
|
||||
AuditLogWriter.AddUserPasswordChangedEvent(user);
|
||||
|
||||
return Result.Ok<SetUserPasswordError>();
|
||||
}
|
||||
|
||||
public void DeleteUser(UserEntity user) {
|
||||
db.Ctx.Users.Remove(user);
|
||||
AuditLogRepository.AddUserDeletedEvent(user);
|
||||
AuditLogWriter.AddUserDeletedEvent(user);
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ public sealed class ControllerServices {
|
||||
private PermissionManager PermissionManager { get; }
|
||||
|
||||
private UserLoginManager UserLoginManager { get; }
|
||||
private AuditLogManager AuditLogManager { get; }
|
||||
|
||||
private readonly IDbContextProvider dbProvider;
|
||||
private readonly AuthToken webAuthToken;
|
||||
@ -49,6 +50,7 @@ public sealed class ControllerServices {
|
||||
this.PermissionManager = new PermissionManager(dbProvider);
|
||||
|
||||
this.UserLoginManager = new UserLoginManager(UserManager, PermissionManager);
|
||||
this.AuditLogManager = new AuditLogManager(dbProvider);
|
||||
|
||||
this.dbProvider = dbProvider;
|
||||
this.webAuthToken = webAuthToken;
|
||||
@ -60,7 +62,7 @@ public sealed class ControllerServices {
|
||||
}
|
||||
|
||||
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() {
|
||||
|
@ -114,12 +114,12 @@ sealed class InstanceManager {
|
||||
entity.JavaRuntimeGuid = configuration.JavaRuntimeGuid;
|
||||
entity.JvmArguments = JvmArgumentsHelper.Join(configuration.JvmArguments);
|
||||
|
||||
var auditLogRepository = new AuditLogRepository(db, auditLogUserGuid);
|
||||
var auditLogWriter = new AuditLogRepositoryWriter(db, auditLogUserGuid);
|
||||
if (isNewInstance) {
|
||||
auditLogRepository.AddInstanceCreatedEvent(configuration.InstanceGuid);
|
||||
auditLogWriter.AddInstanceCreatedEvent(configuration.InstanceGuid);
|
||||
}
|
||||
else {
|
||||
auditLogRepository.AddInstanceEditedEvent(configuration.InstanceGuid);
|
||||
auditLogWriter.AddInstanceEditedEvent(configuration.InstanceGuid);
|
||||
}
|
||||
|
||||
await db.Ctx.SaveChangesAsync(cancellationToken);
|
||||
@ -188,13 +188,13 @@ sealed class InstanceManager {
|
||||
public async Task<InstanceActionResult<StopInstanceResult>> StopInstance(Guid auditLogUserGuid, Guid instanceGuid, MinecraftStopStrategy stopStrategy) {
|
||||
var result = await SendInstanceActionMessage<StopInstanceMessage, StopInstanceResult>(instanceGuid, new StopInstanceMessage(instanceGuid, stopStrategy));
|
||||
if (result.Is(StopInstanceResult.StopInitiated)) {
|
||||
await HandleInstanceManuallyLaunchedOrStopped(instanceGuid, false, auditLogUserGuid, auditLogRepository => auditLogRepository.AddInstanceLaunchedEvent(instanceGuid));
|
||||
await HandleInstanceManuallyLaunchedOrStopped(instanceGuid, false, auditLogUserGuid, auditLogRepository => auditLogRepository.AddInstanceStoppedEvent(instanceGuid, stopStrategy.Seconds));
|
||||
}
|
||||
|
||||
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);
|
||||
try {
|
||||
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);
|
||||
if (entity != null) {
|
||||
entity.LaunchAutomatically = wasLaunched;
|
||||
|
||||
var auditLogRepository = new AuditLogRepository(db, auditLogUserGuid);
|
||||
addAuditEvent(auditLogRepository);
|
||||
|
||||
addAuditEvent(new AuditLogRepositoryWriter(db, auditLogUserGuid));
|
||||
await db.Ctx.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
} 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));
|
||||
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;
|
||||
|
@ -4,6 +4,7 @@ using Phantom.Common.Data.Java;
|
||||
using Phantom.Common.Data.Minecraft;
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Common.Data.Web.Agent;
|
||||
using Phantom.Common.Data.Web.AuditLog;
|
||||
using Phantom.Common.Data.Web.Instance;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
using Phantom.Common.Logging;
|
||||
@ -25,21 +26,34 @@ namespace Phantom.Controller.Services.Rpc;
|
||||
public sealed class WebMessageListener : IMessageToControllerListener {
|
||||
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 UserManager userManager;
|
||||
private readonly UserLoginManager userLoginManager;
|
||||
private readonly AuditLogManager auditLogManager;
|
||||
private readonly AgentManager agentManager;
|
||||
private readonly AgentJavaRuntimesManager agentJavaRuntimesManager;
|
||||
private readonly InstanceManager instanceManager;
|
||||
private readonly MinecraftVersions minecraftVersions;
|
||||
private readonly TaskManager taskManager;
|
||||
|
||||
internal WebMessageListener(RpcConnectionToClient<IMessageToWebListener> connection, AuthToken authToken, UserManager userManager, UserLoginManager userLoginManager, 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.authToken = authToken;
|
||||
this.userManager = userManager;
|
||||
this.userLoginManager = userLoginManager;
|
||||
this.auditLogManager = auditLogManager;
|
||||
this.agentManager = agentManager;
|
||||
this.agentJavaRuntimesManager = agentJavaRuntimesManager;
|
||||
this.instanceManager = instanceManager;
|
||||
@ -61,6 +75,7 @@ public sealed class WebMessageListener : IMessageToControllerListener {
|
||||
|
||||
agentManager.AgentsChanged.Subscribe(this, HandleAgentsChanged);
|
||||
instanceManager.InstancesChanged.Subscribe(this, HandleInstancesChanged);
|
||||
// TODO unsubscribe on closed
|
||||
return NoReply.Instance;
|
||||
}
|
||||
|
||||
@ -86,6 +101,14 @@ public sealed class WebMessageListener : IMessageToControllerListener {
|
||||
return instanceManager.LaunchInstance(message.LoggedInUserGuid, message.InstanceGuid);
|
||||
}
|
||||
|
||||
public Task<InstanceActionResult<StopInstanceResult>> HandleStopInstance(StopInstanceMessage message) {
|
||||
return instanceManager.StopInstance(message.LoggedInUserGuid, message.InstanceGuid, message.StopStrategy);
|
||||
}
|
||||
|
||||
public Task<InstanceActionResult<SendCommandToInstanceResult>> HandleSendCommandToInstance(SendCommandToInstanceMessage message) {
|
||||
return instanceManager.SendCommand(message.LoggedInUserGuid, message.InstanceGuid, message.Command);
|
||||
}
|
||||
|
||||
public Task<ImmutableArray<MinecraftVersion>> HandleGetMinecraftVersions(GetMinecraftVersionsMessage message) {
|
||||
return minecraftVersions.GetVersions(CancellationToken.None);
|
||||
}
|
||||
@ -94,6 +117,14 @@ public sealed class WebMessageListener : IMessageToControllerListener {
|
||||
return Task.FromResult(agentJavaRuntimesManager.All);
|
||||
}
|
||||
|
||||
public Task<ImmutableArray<UserInfo>> HandleGetUsers(GetUsersMessage message) {
|
||||
return userManager.GetAll();
|
||||
}
|
||||
|
||||
public Task<ImmutableArray<AuditLogItem>> HandleGetAuditLog(GetAuditLogMessage message) {
|
||||
return auditLogManager.GetMostRecentItems(message.Count);
|
||||
}
|
||||
|
||||
public Task<LogInSuccess?> HandleLogIn(LogInMessage message) {
|
||||
return userLoginManager.LogIn(message.Username, message.Password);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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) {
|
||||
await using var ctx = dbProvider.Eager();
|
||||
|
||||
|
@ -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.Logging;
|
||||
using Phantom.Controller.Database;
|
||||
@ -17,24 +18,32 @@ sealed class UserManager {
|
||||
this.dbProvider = dbProvider;
|
||||
}
|
||||
|
||||
public async Task<ImmutableArray<UserInfo>> GetAll() {
|
||||
await using var db = dbProvider.Lazy();
|
||||
var userRepository = new UserRepository(db);
|
||||
|
||||
var allUsers = await userRepository.GetAll();
|
||||
return allUsers.Select(static user => user.ToUserInfo()).ToImmutableArray();
|
||||
}
|
||||
|
||||
public async Task<UserEntity?> GetAuthenticated(string username, string password) {
|
||||
await using var db = dbProvider.Lazy();
|
||||
var 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;
|
||||
}
|
||||
|
||||
public async Task<CreateOrUpdateAdministratorUserResult> CreateOrUpdateAdministrator(string username, string password) {
|
||||
await using var db = dbProvider.Lazy();
|
||||
var repository = new UserRepository(db);
|
||||
var userRepository = new UserRepository(db);
|
||||
|
||||
try {
|
||||
bool wasCreated;
|
||||
|
||||
var user = await repository.GetByName(username);
|
||||
var user = await userRepository.GetByName(username);
|
||||
if (user == null) {
|
||||
var result = await repository.CreateUser(username, password);
|
||||
var result = await userRepository.CreateUser(username, password);
|
||||
if (result) {
|
||||
user = result.Value;
|
||||
wasCreated = true;
|
||||
@ -44,7 +53,7 @@ sealed class UserManager {
|
||||
}
|
||||
}
|
||||
else {
|
||||
var result = repository.SetUserPassword(user, password);
|
||||
var result = userRepository.SetUserPassword(user, password);
|
||||
if (!result) {
|
||||
return new UpdatingFailed(result.Error);
|
||||
}
|
||||
@ -77,15 +86,15 @@ sealed class UserManager {
|
||||
|
||||
public async Task<DeleteUserResult> DeleteByGuid(Guid guid) {
|
||||
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) {
|
||||
return DeleteUserResult.NotFound;
|
||||
}
|
||||
|
||||
try {
|
||||
repository.DeleteUser(user);
|
||||
userRepository.DeleteUser(user);
|
||||
await db.Ctx.SaveChangesAsync();
|
||||
|
||||
Logger.Information("Deleted user \"{Username}\" (GUID {Guid}).", user.Name, user.UserGuid);
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.Collections.Immutable;
|
||||
using Phantom.Common.Data.Instance;
|
||||
using Phantom.Common.Data.Minecraft;
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Common.Data.Web.Instance;
|
||||
using Phantom.Common.Logging;
|
||||
@ -25,17 +26,31 @@ public sealed class InstanceManager {
|
||||
instances.SetTo(newInstances.ToImmutableDictionary(static instance => instance.Configuration.InstanceGuid));
|
||||
}
|
||||
|
||||
public InstanceDictionary GetAll() {
|
||||
return instances.Value;
|
||||
}
|
||||
|
||||
public Instance? GetByGuid(Guid instanceGuid) {
|
||||
return instances.Value.GetValueOrDefault(instanceGuid);
|
||||
}
|
||||
|
||||
public Task<InstanceActionResult<CreateOrUpdateInstanceResult>> CreateOrUpdateInstance(Guid loggedInUserGuid, InstanceConfiguration configuration) {
|
||||
public Task<InstanceActionResult<CreateOrUpdateInstanceResult>> CreateOrUpdateInstance(Guid loggedInUserGuid, InstanceConfiguration configuration, CancellationToken cancellationToken) {
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ using Phantom.Web.Services.Authentication;
|
||||
using Phantom.Web.Services.Authorization;
|
||||
using Phantom.Web.Services.Instances;
|
||||
using Phantom.Web.Services.Rpc;
|
||||
using Phantom.Web.Services.Users;
|
||||
|
||||
namespace Phantom.Web.Services;
|
||||
|
||||
@ -18,9 +19,11 @@ public static class PhantomWebServices {
|
||||
services.AddSingleton<InstanceManager>();
|
||||
services.AddSingleton<PermissionManager>();
|
||||
|
||||
services.AddSingleton<UserManager>();
|
||||
services.AddSingleton<UserSessionManager>();
|
||||
services.AddScoped<UserSessionBrowserStorage>();
|
||||
services.AddSingleton<AuditLogManager>();
|
||||
services.AddScoped<UserLoginManager>();
|
||||
services.AddScoped<UserSessionBrowserStorage>();
|
||||
|
||||
services.AddScoped<CustomAuthenticationStateProvider>();
|
||||
services.AddScoped<AuthenticationStateProvider>(static services => services.GetRequiredService<CustomAuthenticationStateProvider>());
|
||||
|
@ -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> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
19
Web/Phantom.Web.Services/Users/AuditLogManager.cs
Normal file
19
Web/Phantom.Web.Services/Users/AuditLogManager.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ using UserInfo = Phantom.Web.Services.Authentication.UserInfo;
|
||||
|
||||
namespace Phantom.Web.Base;
|
||||
|
||||
public abstract class PhantomComponent : ComponentBase {
|
||||
public abstract class PhantomComponent : ComponentBase, IDisposable {
|
||||
private static readonly ILogger Logger = PhantomLogger.Create<PhantomComponent>();
|
||||
|
||||
[CascadingParameter]
|
||||
@ -17,6 +17,10 @@ public abstract class PhantomComponent : ComponentBase {
|
||||
[Inject]
|
||||
public PermissionManager PermissionManager { get; set; } = null!;
|
||||
|
||||
private readonly CancellationTokenSource cancellationTokenSource = new ();
|
||||
|
||||
public CancellationToken CancellationToken => cancellationTokenSource.Token;
|
||||
|
||||
public async Task<Guid?> GetUserGuid() {
|
||||
var authenticationState = await AuthenticationStateTask;
|
||||
return UserInfo.TryGetGuid(authenticationState.User);
|
||||
@ -30,4 +34,13 @@ public abstract class PhantomComponent : ComponentBase {
|
||||
protected void InvokeAsyncChecked(Func<Task> task) {
|
||||
InvokeAsync(task).ContinueWith(static t => Logger.Error(t.Exception, "Caught exception in async task."), TaskContinuationOptions.OnlyOnFaulted);
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
cancellationTokenSource.Cancel();
|
||||
cancellationTokenSource.Dispose();
|
||||
OnDisposed();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void OnDisposed() {}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
@using Phantom.Common.Data.Web.Agent
|
||||
@using Phantom.Utils.Collections
|
||||
@using Phantom.Web.Services.Agents
|
||||
@implements IDisposable
|
||||
@inherits PhantomComponent
|
||||
@inject AgentManager AgentManager
|
||||
|
||||
<h1>Agents</h1>
|
||||
@ -85,7 +85,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
void IDisposable.Dispose() {
|
||||
protected override void OnDisposed() {
|
||||
AgentManager.AgentsChanged.Unsubscribe(this);
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,12 @@
|
||||
@page "/audit"
|
||||
@attribute [Authorize(Permission.ViewAuditPolicy)]
|
||||
@using System.Collections.Immutable
|
||||
@using Phantom.Common.Data.Web.AuditLog
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@using System.Collections.Immutable
|
||||
@using Phantom.Web.Services.Users
|
||||
@using Phantom.Web.Services.Instances
|
||||
@implements IDisposable
|
||||
@inject AuditLog AuditLog
|
||||
@inherits PhantomComponent
|
||||
@inject AuditLogManager AuditLogManager
|
||||
@inject InstanceManager InstanceManager
|
||||
@inject UserManager UserManager
|
||||
|
||||
@ -42,7 +43,7 @@
|
||||
<code class="text-uppercase">@(logItem.SubjectId ?? "-")</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>@logItem.Data?.RootElement.ToString()</code>
|
||||
<code>@logItem.JsonData</code>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@ -52,8 +53,8 @@
|
||||
@code {
|
||||
|
||||
private CancellationTokenSource? initializationCancellationTokenSource;
|
||||
private AuditLogItem[] logItems = Array.Empty<AuditLogItem>();
|
||||
private Dictionary<Guid, string>? userNamesByGuid;
|
||||
private ImmutableArray<AuditLogItem> logItems = ImmutableArray<AuditLogItem>.Empty;
|
||||
private ImmutableDictionary<Guid, string>? userNamesByGuid;
|
||||
private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
@ -61,14 +62,14 @@
|
||||
var cancellationToken = initializationCancellationTokenSource.Token;
|
||||
|
||||
try {
|
||||
logItems = await AuditLog.GetItems(50, cancellationToken);
|
||||
userNamesByGuid = await UserManager.GetAllByGuid(static user => user.Name, cancellationToken);
|
||||
instanceNamesByGuid = InstanceManager.GetInstanceNames();
|
||||
logItems = await AuditLogManager.GetMostRecentItems(50, cancellationToken);
|
||||
userNamesByGuid = (await UserManager.GetAll(cancellationToken)).ToImmutableDictionary(static user => user.Guid, static user => user.Name);
|
||||
instanceNamesByGuid = InstanceManager.GetAll().Values.ToImmutableDictionary(static instance => instance.Configuration.InstanceGuid, static instance => instance.Configuration.InstanceName);
|
||||
} finally {
|
||||
initializationCancellationTokenSource.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private string? GetSubjectName(AuditLogSubjectType type, string id) {
|
||||
return type switch {
|
||||
AuditLogSubjectType.Instance => instanceNamesByGuid.TryGetValue(Guid.Parse(id), out var name) ? name : null,
|
||||
@ -77,7 +78,7 @@
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
protected override void OnDisposed() {
|
||||
try {
|
||||
initializationCancellationTokenSource?.Cancel();
|
||||
} catch (ObjectDisposedException) {}
|
||||
|
@ -1,95 +1,95 @@
|
||||
@page "/events"
|
||||
@attribute [Authorize(Permission.ViewEventsPolicy)]
|
||||
@using Phantom.Common.Data.Web.EventLog
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@using System.Collections.Immutable
|
||||
@using System.Diagnostics
|
||||
@using Phantom.Web.Services.Instances
|
||||
@implements IDisposable
|
||||
@inject AgentManager AgentManager
|
||||
@inject EventLog EventLog
|
||||
@inject InstanceManager InstanceManager
|
||||
|
||||
<h1>Event Log</h1>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<Column Width="165px" Class="text-end">Time</Column>
|
||||
<Column Width="320px; 20%">Agent</Column>
|
||||
<Column Width="160px">Event Type</Column>
|
||||
<Column Width="320px; 20%">Subject</Column>
|
||||
<Column Width="100px; 60%">Data</Column>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var logItem in logItems) {
|
||||
DateTimeOffset time = logItem.UtcTime.ToLocalTime();
|
||||
<tr>
|
||||
<td class="text-end">
|
||||
<time datetime="@time.ToString("o")">@time.ToString()</time>
|
||||
</td>
|
||||
<td>
|
||||
@if (logItem.AgentGuid is {} agentGuid) {
|
||||
@(GetAgentName(agentGuid))
|
||||
<br>
|
||||
<code class="text-uppercase">@agentGuid</code>
|
||||
}
|
||||
else {
|
||||
<text>-</text>
|
||||
}
|
||||
</td>
|
||||
<td>@logItem.EventType.ToNiceString()</td>
|
||||
<td>
|
||||
@if (GetSubjectName(logItem.SubjectType, logItem.SubjectId) is {} subjectName) {
|
||||
@subjectName
|
||||
<br>
|
||||
}
|
||||
<code class="text-uppercase">@logItem.SubjectId</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>@logItem.Data?.RootElement.ToString()</code>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@code {
|
||||
|
||||
private CancellationTokenSource? initializationCancellationTokenSource;
|
||||
private EventLogItem[] logItems = Array.Empty<EventLogItem>();
|
||||
private ImmutableDictionary<Guid, string> agentNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
|
||||
private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
initializationCancellationTokenSource = new CancellationTokenSource();
|
||||
var cancellationToken = initializationCancellationTokenSource.Token;
|
||||
|
||||
try {
|
||||
logItems = await EventLog.GetItems(50, cancellationToken);
|
||||
agentNamesByGuid = AgentManager.GetAgents().ToImmutableDictionary(static kvp => kvp.Key, static kvp => kvp.Value.Name);
|
||||
instanceNamesByGuid = InstanceManager.GetInstanceNames();
|
||||
} finally {
|
||||
initializationCancellationTokenSource.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private string GetAgentName(Guid agentGuid) {
|
||||
return agentNamesByGuid.TryGetValue(agentGuid, out var name) ? name : "?";
|
||||
}
|
||||
|
||||
private string? GetSubjectName(EventLogSubjectType type, string id) {
|
||||
return type switch {
|
||||
EventLogSubjectType.Instance => instanceNamesByGuid.TryGetValue(Guid.Parse(id), out var name) ? name : null,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
try {
|
||||
initializationCancellationTokenSource?.Cancel();
|
||||
} catch (ObjectDisposedException) {}
|
||||
}
|
||||
|
||||
}
|
||||
@* @page "/events" *@
|
||||
@* @attribute [Authorize(Permission.ViewEventsPolicy)] *@
|
||||
@* @inherits PhantomComponent *@
|
||||
@* @using Phantom.Common.Data.Web.EventLog *@
|
||||
@* @using Phantom.Common.Data.Web.Users *@
|
||||
@* @using System.Collections.Immutable *@
|
||||
@* @using System.Diagnostics *@
|
||||
@* @using Phantom.Web.Services.Instances *@
|
||||
@* @inject AgentManager AgentManager *@
|
||||
@* @inject EventLog EventLog *@
|
||||
@* @inject InstanceManager InstanceManager *@
|
||||
@* *@
|
||||
@* <h1>Event Log</h1> *@
|
||||
@* *@
|
||||
@* <table class="table"> *@
|
||||
@* <thead> *@
|
||||
@* <tr> *@
|
||||
@* <Column Width="165px" Class="text-end">Time</Column> *@
|
||||
@* <Column Width="320px; 20%">Agent</Column> *@
|
||||
@* <Column Width="160px">Event Type</Column> *@
|
||||
@* <Column Width="320px; 20%">Subject</Column> *@
|
||||
@* <Column Width="100px; 60%">Data</Column> *@
|
||||
@* </tr> *@
|
||||
@* </thead> *@
|
||||
@* <tbody> *@
|
||||
@* @foreach (var logItem in logItems) { *@
|
||||
@* DateTimeOffset time = logItem.UtcTime.ToLocalTime(); *@
|
||||
@* <tr> *@
|
||||
@* <td class="text-end"> *@
|
||||
@* <time datetime="@time.ToString("o")">@time.ToString()</time> *@
|
||||
@* </td> *@
|
||||
@* <td> *@
|
||||
@* @if (logItem.AgentGuid is {} agentGuid) { *@
|
||||
@* @(GetAgentName(agentGuid)) *@
|
||||
@* <br> *@
|
||||
@* <code class="text-uppercase">@agentGuid</code> *@
|
||||
@* } *@
|
||||
@* else { *@
|
||||
@* <text>-</text> *@
|
||||
@* } *@
|
||||
@* </td> *@
|
||||
@* <td>@logItem.EventType.ToNiceString()</td> *@
|
||||
@* <td> *@
|
||||
@* @if (GetSubjectName(logItem.SubjectType, logItem.SubjectId) is {} subjectName) { *@
|
||||
@* @subjectName *@
|
||||
@* <br> *@
|
||||
@* } *@
|
||||
@* <code class="text-uppercase">@logItem.SubjectId</code> *@
|
||||
@* </td> *@
|
||||
@* <td> *@
|
||||
@* <code>@logItem.Data?.RootElement.ToString()</code> *@
|
||||
@* </td> *@
|
||||
@* </tr> *@
|
||||
@* } *@
|
||||
@* </tbody> *@
|
||||
@* </table> *@
|
||||
@* *@
|
||||
@* @code { *@
|
||||
@* *@
|
||||
@* private CancellationTokenSource? initializationCancellationTokenSource; *@
|
||||
@* private EventLogItem[] logItems = Array.Empty<EventLogItem>(); *@
|
||||
@* private ImmutableDictionary<Guid, string> agentNamesByGuid = ImmutableDictionary<Guid, string>.Empty; *@
|
||||
@* private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty; *@
|
||||
@* *@
|
||||
@* protected override async Task OnInitializedAsync() { *@
|
||||
@* initializationCancellationTokenSource = new CancellationTokenSource(); *@
|
||||
@* var cancellationToken = initializationCancellationTokenSource.Token; *@
|
||||
@* *@
|
||||
@* try { *@
|
||||
@* logItems = await EventLog.GetItems(50, cancellationToken); *@
|
||||
@* agentNamesByGuid = AgentManager.GetAgents().ToImmutableDictionary(static kvp => kvp.Key, static kvp => kvp.Value.Name); *@
|
||||
@* instanceNamesByGuid = InstanceManager.GetInstanceNames(); *@
|
||||
@* } finally { *@
|
||||
@* initializationCancellationTokenSource.Dispose(); *@
|
||||
@* } *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* private string GetAgentName(Guid agentGuid) { *@
|
||||
@* return agentNamesByGuid.TryGetValue(agentGuid, out var name) ? name : "?"; *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* private string? GetSubjectName(EventLogSubjectType type, string id) { *@
|
||||
@* return type switch { *@
|
||||
@* EventLogSubjectType.Instance => instanceNamesByGuid.TryGetValue(Guid.Parse(id), out var name) ? name : null, *@
|
||||
@* _ => null *@
|
||||
@* }; *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* public void Dispose() { *@
|
||||
@* try { *@
|
||||
@* initializationCancellationTokenSource?.Cancel(); *@
|
||||
@* } catch (ObjectDisposedException) {} *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* } *@
|
||||
|
@ -1,12 +1,11 @@
|
||||
@page "/instances/{InstanceGuid:guid}"
|
||||
@attribute [Authorize(Permission.ViewInstancesPolicy)]
|
||||
@inherits PhantomComponent
|
||||
@using Phantom.Common.Data.Instance
|
||||
@using Phantom.Common.Data.Replies
|
||||
@using Phantom.Common.Data.Web.Instance
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@using Phantom.Web.Services.Instances
|
||||
@implements IDisposable
|
||||
@inherits PhantomComponent
|
||||
@inject InstanceManager InstanceManager
|
||||
|
||||
@if (Instance == null) {
|
||||
@ -78,7 +77,7 @@ else {
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await InstanceManager.LaunchInstance(loggedInUserGuid.Value, InstanceGuid);
|
||||
var result = await InstanceManager.LaunchInstance(loggedInUserGuid.Value, InstanceGuid, CancellationToken);
|
||||
if (!result.Is(LaunchInstanceResult.LaunchInitiated)) {
|
||||
lastError = result.ToSentence(Messages.ToSentence);
|
||||
}
|
||||
@ -87,7 +86,7 @@ else {
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
protected override void OnDisposed() {
|
||||
InstanceManager.InstancesChanged.Unsubscribe(this);
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@using Phantom.Web.Services.Agents
|
||||
@using Phantom.Web.Services.Instances
|
||||
@implements IDisposable
|
||||
@inherits PhantomComponent
|
||||
@inject AgentManager AgentManager
|
||||
@inject InstanceManager InstanceManager
|
||||
|
||||
@ -91,7 +91,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
void IDisposable.Dispose() {
|
||||
protected override void OnDisposed() {
|
||||
AgentManager.AgentsChanged.Unsubscribe(this);
|
||||
InstanceManager.InstancesChanged.Unsubscribe(this);
|
||||
}
|
||||
|
@ -1,110 +1,110 @@
|
||||
@page "/users"
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@using System.Collections.Immutable
|
||||
@using Phantom.Web.Services.Authorization
|
||||
@attribute [Authorize(Permission.ViewUsersPolicy)]
|
||||
@inject UserManager UserManager
|
||||
@inject UserRoleManager UserRoleManager
|
||||
@inject PermissionManager PermissionManager
|
||||
|
||||
<h1>Users</h1>
|
||||
|
||||
<PermissionView Permission="Permission.EditUsers">
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-user">Add User...</button>
|
||||
</PermissionView>
|
||||
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
@{ var canEdit = PermissionManager.CheckPermission(context.User, Permission.EditUsers); }
|
||||
<table class="table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<Column Width="320px">Identifier</Column>
|
||||
<Column Width="125px; 40%">Username</Column>
|
||||
<Column Width="125px; 60%">Roles</Column>
|
||||
@if (canEdit) {
|
||||
<Column Width="175px">Actions</Column>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@{ var myUserId = UserManager.GetAuthenticatedUserId(context.User); }
|
||||
@foreach (var user in allUsers) {
|
||||
var isMe = myUserId == user.Guid;
|
||||
<tr>
|
||||
<td>
|
||||
<code class="text-uppercase">@user.Guid</code>
|
||||
</td>
|
||||
@if (isMe) {
|
||||
<td class="fw-semibold">@user.Name</td>
|
||||
}
|
||||
else {
|
||||
<td>@user.Name</td>
|
||||
}
|
||||
<td>@(userGuidToRoleDescription.TryGetValue(user.Guid, out var roles) ? roles : "?")</td>
|
||||
@if (canEdit) {
|
||||
<td>
|
||||
@if (!isMe) {
|
||||
<button class="btn btn-primary btn-sm" @onclick="() => userRolesDialog.Show(user)">Edit Roles</button>
|
||||
<button class="btn btn-danger btn-sm" @onclick="() => userDeleteDialog.Show(user)">Delete...</button>
|
||||
}
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</Authorized>
|
||||
</AuthorizeView>
|
||||
|
||||
<PermissionView Permission="Permission.EditUsers">
|
||||
<UserAddDialog ModalId="add-user" UserAdded="OnUserAdded" />
|
||||
<UserRolesDialog @ref="userRolesDialog" ModalId="manage-user-roles" UserModified="OnUserRolesChanged" />
|
||||
<UserDeleteDialog @ref="userDeleteDialog" ModalId="delete-user" UserModified="OnUserDeleted" />
|
||||
</PermissionView>
|
||||
|
||||
@code {
|
||||
|
||||
private ImmutableArray<UserInfo> allUsers = ImmutableArray<UserInfo>.Empty;
|
||||
private readonly Dictionary<Guid, string> userGuidToRoleDescription = new();
|
||||
|
||||
private UserRolesDialog userRolesDialog = null!;
|
||||
private UserDeleteDialog userDeleteDialog = null!;
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
var unsortedUsers = await UserManager.GetAll();
|
||||
allUsers = unsortedUsers.Sort(static (a, b) => a.Name.CompareTo(b.Name));
|
||||
|
||||
foreach (var (userGuid, roles) in await UserRoleManager.GetAllByUserGuid()) {
|
||||
userGuidToRoleDescription[userGuid] = StringifyRoles(roles);
|
||||
}
|
||||
|
||||
foreach (var user in allUsers) {
|
||||
await RefreshUserRoles(user);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshUserRoles(UserInfo user) {
|
||||
var roles = await UserRoleManager.GetUserRoles(user);
|
||||
userGuidToRoleDescription[user.Guid] = StringifyRoles(roles);
|
||||
}
|
||||
|
||||
private static string StringifyRoles(ImmutableArray<RoleInfo> roles) {
|
||||
return roles.IsEmpty ? "-" : string.Join(", ", roles.Select(static role => role.Name));
|
||||
}
|
||||
|
||||
private Task OnUserAdded(UserInfo user) {
|
||||
allUsers = allUsers.Add(user);
|
||||
return RefreshUserRoles(user);
|
||||
}
|
||||
|
||||
private Task OnUserRolesChanged(UserInfo user) {
|
||||
return RefreshUserRoles(user);
|
||||
}
|
||||
|
||||
private void OnUserDeleted(UserInfo user) {
|
||||
allUsers = allUsers.Remove(user);
|
||||
userGuidToRoleDescription.Remove(user.Guid);
|
||||
}
|
||||
|
||||
}
|
||||
@* @page "/users" *@
|
||||
@* @using Phantom.Common.Data.Web.Users *@
|
||||
@* @using System.Collections.Immutable *@
|
||||
@* @using Phantom.Web.Services.Authorization *@
|
||||
@* @attribute [Authorize(Permission.ViewUsersPolicy)] *@
|
||||
@* @inject UserManager UserManager *@
|
||||
@* @inject UserRoleManager UserRoleManager *@
|
||||
@* @inject PermissionManager PermissionManager *@
|
||||
@* *@
|
||||
@* <h1>Users</h1> *@
|
||||
@* *@
|
||||
@* <PermissionView Permission="Permission.EditUsers"> *@
|
||||
@* <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-user">Add User...</button> *@
|
||||
@* </PermissionView> *@
|
||||
@* *@
|
||||
@* <AuthorizeView> *@
|
||||
@* <Authorized> *@
|
||||
@* @{ var canEdit = PermissionManager.CheckPermission(context.User, Permission.EditUsers); } *@
|
||||
@* <table class="table align-middle"> *@
|
||||
@* <thead> *@
|
||||
@* <tr> *@
|
||||
@* <Column Width="320px">Identifier</Column> *@
|
||||
@* <Column Width="125px; 40%">Username</Column> *@
|
||||
@* <Column Width="125px; 60%">Roles</Column> *@
|
||||
@* @if (canEdit) { *@
|
||||
@* <Column Width="175px">Actions</Column> *@
|
||||
@* } *@
|
||||
@* </tr> *@
|
||||
@* </thead> *@
|
||||
@* <tbody> *@
|
||||
@* @{ var myUserId = UserManager.GetAuthenticatedUserId(context.User); } *@
|
||||
@* @foreach (var user in allUsers) { *@
|
||||
@* var isMe = myUserId == user.Guid; *@
|
||||
@* <tr> *@
|
||||
@* <td> *@
|
||||
@* <code class="text-uppercase">@user.Guid</code> *@
|
||||
@* </td> *@
|
||||
@* @if (isMe) { *@
|
||||
@* <td class="fw-semibold">@user.Name</td> *@
|
||||
@* } *@
|
||||
@* else { *@
|
||||
@* <td>@user.Name</td> *@
|
||||
@* } *@
|
||||
@* <td>@(userGuidToRoleDescription.TryGetValue(user.Guid, out var roles) ? roles : "?")</td> *@
|
||||
@* @if (canEdit) { *@
|
||||
@* <td> *@
|
||||
@* @if (!isMe) { *@
|
||||
@* <button class="btn btn-primary btn-sm" @onclick="() => userRolesDialog.Show(user)">Edit Roles</button> *@
|
||||
@* <button class="btn btn-danger btn-sm" @onclick="() => userDeleteDialog.Show(user)">Delete...</button> *@
|
||||
@* } *@
|
||||
@* </td> *@
|
||||
@* } *@
|
||||
@* </tr> *@
|
||||
@* } *@
|
||||
@* </tbody> *@
|
||||
@* </table> *@
|
||||
@* </Authorized> *@
|
||||
@* </AuthorizeView> *@
|
||||
@* *@
|
||||
@* <PermissionView Permission="Permission.EditUsers"> *@
|
||||
@* <UserAddDialog ModalId="add-user" UserAdded="OnUserAdded" /> *@
|
||||
@* <UserRolesDialog @ref="userRolesDialog" ModalId="manage-user-roles" UserModified="OnUserRolesChanged" /> *@
|
||||
@* <UserDeleteDialog @ref="userDeleteDialog" ModalId="delete-user" UserModified="OnUserDeleted" /> *@
|
||||
@* </PermissionView> *@
|
||||
@* *@
|
||||
@* @code { *@
|
||||
@* *@
|
||||
@* private ImmutableArray<UserInfo> allUsers = ImmutableArray<UserInfo>.Empty; *@
|
||||
@* private readonly Dictionary<Guid, string> userGuidToRoleDescription = new(); *@
|
||||
@* *@
|
||||
@* private UserRolesDialog userRolesDialog = null!; *@
|
||||
@* private UserDeleteDialog userDeleteDialog = null!; *@
|
||||
@* *@
|
||||
@* protected override async Task OnInitializedAsync() { *@
|
||||
@* var unsortedUsers = await UserManager.GetAll(); *@
|
||||
@* allUsers = unsortedUsers.Sort(static (a, b) => a.Name.CompareTo(b.Name)); *@
|
||||
@* *@
|
||||
@* foreach (var (userGuid, roles) in await UserRoleManager.GetAllByUserGuid()) { *@
|
||||
@* userGuidToRoleDescription[userGuid] = StringifyRoles(roles); *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* foreach (var user in allUsers) { *@
|
||||
@* await RefreshUserRoles(user); *@
|
||||
@* } *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* private async Task RefreshUserRoles(UserInfo user) { *@
|
||||
@* var roles = await UserRoleManager.GetUserRoles(user); *@
|
||||
@* userGuidToRoleDescription[user.Guid] = StringifyRoles(roles); *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* private static string StringifyRoles(ImmutableArray<RoleInfo> roles) { *@
|
||||
@* return roles.IsEmpty ? "-" : string.Join(", ", roles.Select(static role => role.Name)); *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* private Task OnUserAdded(UserInfo user) { *@
|
||||
@* allUsers = allUsers.Add(user); *@
|
||||
@* return RefreshUserRoles(user); *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* private Task OnUserRolesChanged(UserInfo user) { *@
|
||||
@* return RefreshUserRoles(user); *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* private void OnUserDeleted(UserInfo user) { *@
|
||||
@* allUsers = allUsers.Remove(user); *@
|
||||
@* userGuidToRoleDescription.Remove(user.Guid); *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* } *@
|
||||
|
@ -338,7 +338,7 @@
|
||||
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)) {
|
||||
await Nav.NavigateTo("instances/" + instance.InstanceGuid);
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
||||
</Form>
|
||||
|
||||
@code {
|
||||
|
||||
|
||||
[Parameter]
|
||||
public Guid InstanceGuid { get; set; }
|
||||
|
||||
@ -23,14 +23,19 @@
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
private readonly SendCommandFormModel form = new ();
|
||||
|
||||
|
||||
private sealed class SendCommandFormModel : FormModel {
|
||||
public string Command { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
|
||||
private ElementReference commandInputElement;
|
||||
|
||||
private async Task ExecuteCommand(EditContext context) {
|
||||
var loggedInUserGuid = await GetUserGuid();
|
||||
if (loggedInUserGuid == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await form.SubmitModel.StartSubmitting();
|
||||
|
||||
if (!await CheckPermission(Permission.ControlInstances)) {
|
||||
@ -38,7 +43,7 @@
|
||||
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)) {
|
||||
form.Command = string.Empty;
|
||||
form.SubmitModel.StopSubmitting();
|
||||
@ -49,5 +54,5 @@
|
||||
|
||||
await commandInputElement.FocusAsync(preventScroll: true);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -1,10 +1,9 @@
|
||||
@inherits PhantomComponent
|
||||
@using Phantom.Utils.Collections
|
||||
@using Phantom.Utils.Collections
|
||||
@using Phantom.Utils.Events
|
||||
@using System.Diagnostics
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@implements IDisposable
|
||||
@inherits PhantomComponent
|
||||
@inject IJSRuntime Js;
|
||||
@inject InstanceLogManager InstanceLogManager
|
||||
|
||||
<div id="log" class="font-monospace mb-3">
|
||||
@foreach (var line in instanceLogs.EnumerateLast(uint.MaxValue)) {
|
||||
@ -15,7 +14,7 @@
|
||||
@code {
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public Guid InstanceGuid { get; set; }
|
||||
public Guid InstanceGuid { get; init; }
|
||||
|
||||
private IJSObjectReference? PageJs { get; set; }
|
||||
|
||||
@ -25,11 +24,13 @@
|
||||
private readonly Stopwatch recheckPermissionsStopwatch = Stopwatch.StartNew();
|
||||
|
||||
protected override void OnInitialized() {
|
||||
/*
|
||||
instanceLogsSubs = InstanceLogManager.GetSubs(InstanceGuid);
|
||||
instanceLogsSubs.Subscribe(this, buffer => {
|
||||
instanceLogs = buffer;
|
||||
InvokeAsyncChecked(RefreshLog);
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender) {
|
||||
@ -56,7 +57,7 @@
|
||||
|
||||
private async Task RecheckPermissions() {
|
||||
recheckPermissionsStopwatch.Restart();
|
||||
|
||||
|
||||
if (!await CheckPermission(Permission.ViewInstanceLogs)) {
|
||||
await Task.Yield();
|
||||
Dispose();
|
||||
@ -64,7 +65,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
protected override void OnDisposed() {
|
||||
instanceLogsSubs.Unsubscribe(this);
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,6 @@
|
||||
@inherits PhantomComponent
|
||||
@inject IJSRuntime Js;
|
||||
@inject InstanceManager InstanceManager;
|
||||
@inject AuditLog AuditLog
|
||||
|
||||
<Form Model="form" OnSubmit="StopInstance">
|
||||
<Modal Id="@ModalId" TitleText="Stop Instance">
|
||||
@ -33,13 +32,13 @@
|
||||
@code {
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public Guid InstanceGuid { get; set; }
|
||||
public Guid InstanceGuid { get; init; }
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public string ModalId { get; set; } = string.Empty;
|
||||
public string ModalId { get; init; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public bool Disabled { get; set; }
|
||||
public bool Disabled { get; init; }
|
||||
|
||||
private readonly StopInstanceFormModel form = new ();
|
||||
|
||||
@ -49,16 +48,20 @@
|
||||
}
|
||||
|
||||
private async Task StopInstance(EditContext context) {
|
||||
var loggedInUserGuid = await GetUserGuid();
|
||||
if (loggedInUserGuid == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await form.SubmitModel.StartSubmitting();
|
||||
|
||||
if (!await CheckPermission(Permission.ControlInstances)) {
|
||||
form.SubmitModel.StopSubmitting("You do not have permission to stop instances.");
|
||||
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)) {
|
||||
await AuditLog.AddInstanceStoppedEvent(InstanceGuid, form.StopInSeconds);
|
||||
await Js.InvokeVoidAsync("closeModal", ModalId);
|
||||
form.SubmitModel.StopSubmitting();
|
||||
}
|
||||
|
@ -1,72 +1,72 @@
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@using Phantom.Utils.Tasks
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@inherits PhantomComponent
|
||||
@inject IJSRuntime Js;
|
||||
|
||||
<Form Model="form" OnSubmit="AddUser">
|
||||
<Modal Id="@ModalId" TitleText="Add User">
|
||||
<Body>
|
||||
|
||||
<div class="row">
|
||||
<div class="mb-3">
|
||||
<FormTextInput Id="account-username" Label="Username" @bind-Value="form.Username" autocomplete="off" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="mb-3">
|
||||
<FormTextInput Id="account-password" Label="Password" Type="FormTextInputType.Password" autocomplete="new-password" @bind-Value="form.Password" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</Body>
|
||||
<Footer>
|
||||
<FormSubmitError />
|
||||
<FormButtonSubmit Label="Add User" class="btn btn-primary" />
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
</Footer>
|
||||
</Modal>
|
||||
</Form>
|
||||
|
||||
@code {
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public string ModalId { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<UserInfo> UserAdded { get; set; }
|
||||
|
||||
private readonly AddUserFormModel form = new();
|
||||
|
||||
private sealed class AddUserFormModel : FormModel {
|
||||
[Required]
|
||||
public string Username { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string Password { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private async Task AddUser(EditContext context) {
|
||||
await form.SubmitModel.StartSubmitting();
|
||||
|
||||
if (!await CheckPermission(Permission.EditUsers)) {
|
||||
form.SubmitModel.StopSubmitting("You do not have permission to add users.");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (await UserManager.CreateUser(form.Username, form.Password)) {
|
||||
case Result<UserInfo, AddUserError>.Ok ok:
|
||||
await AuditLog.AddUserCreatedEvent(ok.Value);
|
||||
await UserAdded.InvokeAsync(ok.Value);
|
||||
await Js.InvokeVoidAsync("closeModal", ModalId);
|
||||
form.SubmitModel.StopSubmitting();
|
||||
break;
|
||||
|
||||
case Result<UserInfo, AddUserError>.Fail fail:
|
||||
form.SubmitModel.StopSubmitting(fail.Error.ToSentences("\n"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@* @using Phantom.Common.Data.Web.Users *@
|
||||
@* @using Phantom.Utils.Tasks *@
|
||||
@* @using System.ComponentModel.DataAnnotations *@
|
||||
@* @inherits PhantomComponent *@
|
||||
@* @inject IJSRuntime Js; *@
|
||||
@* *@
|
||||
@* <Form Model="form" OnSubmit="AddUser"> *@
|
||||
@* <Modal Id="@ModalId" TitleText="Add User"> *@
|
||||
@* <Body> *@
|
||||
@* *@
|
||||
@* <div class="row"> *@
|
||||
@* <div class="mb-3"> *@
|
||||
@* <FormTextInput Id="account-username" Label="Username" @bind-Value="form.Username" autocomplete="off" /> *@
|
||||
@* </div> *@
|
||||
@* </div> *@
|
||||
@* *@
|
||||
@* <div class="row"> *@
|
||||
@* <div class="mb-3"> *@
|
||||
@* <FormTextInput Id="account-password" Label="Password" Type="FormTextInputType.Password" autocomplete="new-password" @bind-Value="form.Password" /> *@
|
||||
@* </div> *@
|
||||
@* </div> *@
|
||||
@* *@
|
||||
@* </Body> *@
|
||||
@* <Footer> *@
|
||||
@* <FormSubmitError /> *@
|
||||
@* <FormButtonSubmit Label="Add User" class="btn btn-primary" /> *@
|
||||
@* <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> *@
|
||||
@* </Footer> *@
|
||||
@* </Modal> *@
|
||||
@* </Form> *@
|
||||
@* *@
|
||||
@* @code { *@
|
||||
@* *@
|
||||
@* [Parameter, EditorRequired] *@
|
||||
@* public string ModalId { get; set; } = string.Empty; *@
|
||||
@* *@
|
||||
@* [Parameter] *@
|
||||
@* public EventCallback<UserInfo> UserAdded { get; set; } *@
|
||||
@* *@
|
||||
@* private readonly AddUserFormModel form = new(); *@
|
||||
@* *@
|
||||
@* private sealed class AddUserFormModel : FormModel { *@
|
||||
@* [Required] *@
|
||||
@* public string Username { get; set; } = string.Empty; *@
|
||||
@* *@
|
||||
@* [Required] *@
|
||||
@* public string Password { get; set; } = string.Empty; *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* private async Task AddUser(EditContext context) { *@
|
||||
@* await form.SubmitModel.StartSubmitting(); *@
|
||||
@* *@
|
||||
@* if (!await CheckPermission(Permission.EditUsers)) { *@
|
||||
@* form.SubmitModel.StopSubmitting("You do not have permission to add users."); *@
|
||||
@* return; *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* switch (await UserManager.CreateUser(form.Username, form.Password)) { *@
|
||||
@* case Result<UserInfo, AddUserError>.Ok ok: *@
|
||||
@* await AuditLog.AddUserCreatedEvent(ok.Value); *@
|
||||
@* await UserAdded.InvokeAsync(ok.Value); *@
|
||||
@* await Js.InvokeVoidAsync("closeModal", ModalId); *@
|
||||
@* form.SubmitModel.StopSubmitting(); *@
|
||||
@* break; *@
|
||||
@* *@
|
||||
@* case Result<UserInfo, AddUserError>.Fail fail: *@
|
||||
@* form.SubmitModel.StopSubmitting(fail.Error.ToSentences("\n")); *@
|
||||
@* break; *@
|
||||
@* } *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* } *@
|
||||
|
@ -1,37 +1,37 @@
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@inherits UserEditDialogBase
|
||||
@inject UserManager UserManager
|
||||
@inject AuditLog AuditLog
|
||||
|
||||
<Modal Id="@ModalId" TitleText="Delete User">
|
||||
<Body>
|
||||
You are about to delete the user <strong class="fw-semibold">@EditedUserName</strong>.<br>
|
||||
This action cannot be undone.
|
||||
</Body>
|
||||
<Footer>
|
||||
<FormSubmitError Model="SubmitModel" />
|
||||
<FormButtonSubmit Model="SubmitModel" Label="Delete User" type="button" class="btn btn-danger" @onclick="Submit" />
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="OnClosed">Cancel</button>
|
||||
</Footer>
|
||||
</Modal>
|
||||
|
||||
@code {
|
||||
|
||||
protected override async Task DoEdit(UserInfo user) {
|
||||
switch (await UserManager.DeleteByGuid(user.Guid)) {
|
||||
case DeleteUserResult.Deleted:
|
||||
await AuditLog.AddUserDeletedEvent(user);
|
||||
await OnEditSuccess();
|
||||
break;
|
||||
|
||||
case DeleteUserResult.NotFound:
|
||||
await OnEditSuccess();
|
||||
break;
|
||||
|
||||
case DeleteUserResult.Failed:
|
||||
OnEditFailure("Could not delete user.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@* @using Phantom.Common.Data.Web.Users *@
|
||||
@* @inherits UserEditDialogBase *@
|
||||
@* @inject UserManager UserManager *@
|
||||
@* @inject AuditLog AuditLog *@
|
||||
@* *@
|
||||
@* <Modal Id="@ModalId" TitleText="Delete User"> *@
|
||||
@* <Body> *@
|
||||
@* You are about to delete the user <strong class="fw-semibold">@EditedUserName</strong>.<br> *@
|
||||
@* This action cannot be undone. *@
|
||||
@* </Body> *@
|
||||
@* <Footer> *@
|
||||
@* <FormSubmitError Model="SubmitModel" /> *@
|
||||
@* <FormButtonSubmit Model="SubmitModel" Label="Delete User" type="button" class="btn btn-danger" @onclick="Submit" /> *@
|
||||
@* <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="OnClosed">Cancel</button> *@
|
||||
@* </Footer> *@
|
||||
@* </Modal> *@
|
||||
@* *@
|
||||
@* @code { *@
|
||||
@* *@
|
||||
@* protected override async Task DoEdit(UserInfo user) { *@
|
||||
@* switch (await UserManager.DeleteByGuid(user.Guid)) { *@
|
||||
@* case DeleteUserResult.Deleted: *@
|
||||
@* await AuditLog.AddUserDeletedEvent(user); *@
|
||||
@* await OnEditSuccess(); *@
|
||||
@* break; *@
|
||||
@* *@
|
||||
@* case DeleteUserResult.NotFound: *@
|
||||
@* await OnEditSuccess(); *@
|
||||
@* break; *@
|
||||
@* *@
|
||||
@* case DeleteUserResult.Failed: *@
|
||||
@* OnEditFailure("Could not delete user."); *@
|
||||
@* break; *@
|
||||
@* } *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* } *@
|
||||
|
@ -1,76 +1,76 @@
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@inherits UserEditDialogBase
|
||||
|
||||
<Modal Id="@ModalId" TitleText="Manage User Roles">
|
||||
<Body>
|
||||
Roles for user: <strong class="fw-semibold">@EditedUserName</strong><br>
|
||||
@for (var index = 0; index < items.Count; index++) {
|
||||
var item = items[index];
|
||||
<div class="mt-1">
|
||||
<input id="role-@index" type="checkbox" class="form-check-input" @bind="@item.Checked" />
|
||||
<label for="role-@index" class="form-check-label">@item.Role.Name</label>
|
||||
</div>
|
||||
}
|
||||
</Body>
|
||||
<Footer>
|
||||
<FormSubmitError Model="SubmitModel" />
|
||||
<FormButtonSubmit Model="SubmitModel" Label="Save Roles" type="button" class="btn btn-success" @onclick="Submit" />
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="OnClosed">Cancel</button>
|
||||
</Footer>
|
||||
</Modal>
|
||||
|
||||
@code {
|
||||
|
||||
private List<RoleItem> items = new();
|
||||
|
||||
protected override async Task BeforeShown(UserInfo user) {
|
||||
var userRoles = await UserRoleManager.GetUserRoleGuids(user);
|
||||
var allRoles = await RoleManager.GetAll();
|
||||
this.items = allRoles.Select(role => new RoleItem(role, userRoles.Contains(role.RoleGuid))).ToList();
|
||||
}
|
||||
|
||||
protected override async Task DoEdit(UserInfo user) {
|
||||
var userRoles = await UserRoleManager.GetUserRoleGuids(user);
|
||||
var addedToRoles = new List<string>();
|
||||
var removedFromRoles = new List<string>();
|
||||
var errors = new List<string>();
|
||||
|
||||
foreach (var item in items) {
|
||||
var shouldHaveRole = item.Checked;
|
||||
if (shouldHaveRole == userRoles.Contains(item.Role.Guid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool success = shouldHaveRole ? await UserRoleManager.Add(user, item.Role) : await UserRoleManager.Remove(user, item.Role);
|
||||
if (success) {
|
||||
var modifiedList = shouldHaveRole ? addedToRoles : removedFromRoles;
|
||||
modifiedList.Add(item.Role.Name);
|
||||
}
|
||||
else if (shouldHaveRole) {
|
||||
errors.Add("Could not add role " + item.Role.Name + " to user.");
|
||||
}
|
||||
else {
|
||||
errors.Add("Could not remove role " + item.Role.Name + " from user.");
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.Count == 0) {
|
||||
await AuditLog.AddUserRolesChangedEvent(user, addedToRoles, removedFromRoles);
|
||||
await OnEditSuccess();
|
||||
}
|
||||
else {
|
||||
OnEditFailure(string.Join("\n", errors));
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RoleItem {
|
||||
public RoleInfo Role { get; }
|
||||
public bool Checked { get; set; }
|
||||
|
||||
public RoleItem(RoleInfo role, bool @checked) {
|
||||
this.Role = role;
|
||||
this.Checked = @checked;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@* @using Phantom.Common.Data.Web.Users *@
|
||||
@* @inherits UserEditDialogBase *@
|
||||
@* *@
|
||||
@* <Modal Id="@ModalId" TitleText="Manage User Roles"> *@
|
||||
@* <Body> *@
|
||||
@* Roles for user: <strong class="fw-semibold">@EditedUserName</strong><br> *@
|
||||
@* @for (var index = 0; index < items.Count; index++) { *@
|
||||
@* var item = items[index]; *@
|
||||
@* <div class="mt-1"> *@
|
||||
@* <input id="role-@index" type="checkbox" class="form-check-input" @bind="@item.Checked" /> *@
|
||||
@* <label for="role-@index" class="form-check-label">@item.Role.Name</label> *@
|
||||
@* </div> *@
|
||||
@* } *@
|
||||
@* </Body> *@
|
||||
@* <Footer> *@
|
||||
@* <FormSubmitError Model="SubmitModel" /> *@
|
||||
@* <FormButtonSubmit Model="SubmitModel" Label="Save Roles" type="button" class="btn btn-success" @onclick="Submit" /> *@
|
||||
@* <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="OnClosed">Cancel</button> *@
|
||||
@* </Footer> *@
|
||||
@* </Modal> *@
|
||||
@* *@
|
||||
@* @code { *@
|
||||
@* *@
|
||||
@* private List<RoleItem> items = new(); *@
|
||||
@* *@
|
||||
@* protected override async Task BeforeShown(UserInfo user) { *@
|
||||
@* var userRoles = await UserRoleManager.GetUserRoleGuids(user); *@
|
||||
@* var allRoles = await RoleManager.GetAll(); *@
|
||||
@* this.items = allRoles.Select(role => new RoleItem(role, userRoles.Contains(role.RoleGuid))).ToList(); *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* protected override async Task DoEdit(UserInfo user) { *@
|
||||
@* var userRoles = await UserRoleManager.GetUserRoleGuids(user); *@
|
||||
@* var addedToRoles = new List<string>(); *@
|
||||
@* var removedFromRoles = new List<string>(); *@
|
||||
@* var errors = new List<string>(); *@
|
||||
@* *@
|
||||
@* foreach (var item in items) { *@
|
||||
@* var shouldHaveRole = item.Checked; *@
|
||||
@* if (shouldHaveRole == userRoles.Contains(item.Role.Guid)) { *@
|
||||
@* continue; *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* bool success = shouldHaveRole ? await UserRoleManager.Add(user, item.Role) : await UserRoleManager.Remove(user, item.Role); *@
|
||||
@* if (success) { *@
|
||||
@* var modifiedList = shouldHaveRole ? addedToRoles : removedFromRoles; *@
|
||||
@* modifiedList.Add(item.Role.Name); *@
|
||||
@* } *@
|
||||
@* else if (shouldHaveRole) { *@
|
||||
@* errors.Add("Could not add role " + item.Role.Name + " to user."); *@
|
||||
@* } *@
|
||||
@* else { *@
|
||||
@* errors.Add("Could not remove role " + item.Role.Name + " from user."); *@
|
||||
@* } *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* if (errors.Count == 0) { *@
|
||||
@* await AuditLog.AddUserRolesChangedEvent(user, addedToRoles, removedFromRoles); *@
|
||||
@* await OnEditSuccess(); *@
|
||||
@* } *@
|
||||
@* else { *@
|
||||
@* OnEditFailure(string.Join("\n", errors)); *@
|
||||
@* } *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* private sealed class RoleItem { *@
|
||||
@* public RoleInfo Role { get; } *@
|
||||
@* public bool Checked { get; set; } *@
|
||||
@* *@
|
||||
@* public RoleItem(RoleInfo role, bool @checked) { *@
|
||||
@* this.Role = role; *@
|
||||
@* this.Checked = @checked; *@
|
||||
@* } *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* } *@
|
||||
|
Loading…
Reference in New Issue
Block a user