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

Compare commits

..

2 Commits

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

View File

@ -17,5 +17,6 @@ public sealed partial record AgentWithStats(
[property: MemoryPackOrder(9)] DateTimeOffset? LastPing, [property: MemoryPackOrder(9)] DateTimeOffset? LastPing,
[property: MemoryPackOrder(10)] bool IsOnline [property: MemoryPackOrder(10)] bool IsOnline
) { ) {
[MemoryPackIgnore]
public RamAllocationUnits? AvailableMemory => MaxMemory - Stats?.RunningInstanceMemory; public RamAllocationUnits? AvailableMemory => MaxMemory - Stats?.RunningInstanceMemory;
} }

View File

@ -0,0 +1,23 @@
namespace Phantom.Common.Data.Web.Instance;
public enum CreateOrUpdateInstanceResult : byte {
UnknownError,
Success,
InstanceNameMustNotBeEmpty,
InstanceMemoryMustNotBeZero,
MinecraftVersionDownloadInfoNotFound,
AgentNotFound
}
public static class CreateOrUpdateInstanceResultExtensions {
public static string ToSentence(this CreateOrUpdateInstanceResult reason) {
return reason switch {
CreateOrUpdateInstanceResult.Success => "Success.",
CreateOrUpdateInstanceResult.InstanceNameMustNotBeEmpty => "Instance name must not be empty.",
CreateOrUpdateInstanceResult.InstanceMemoryMustNotBeZero => "Memory must not be 0 MB.",
CreateOrUpdateInstanceResult.MinecraftVersionDownloadInfoNotFound => "Could not find download information for the selected Minecraft version.",
CreateOrUpdateInstanceResult.AgentNotFound => "Agent not found.",
_ => "Unknown error."
};
}
}

View File

@ -1,7 +1,10 @@
namespace Phantom.Common.Data.Minecraft; using MemoryPack;
public sealed record MinecraftVersion( namespace Phantom.Common.Data.Minecraft;
string Id,
MinecraftVersionType Type, [MemoryPackable(GenerateType.VersionTolerant)]
string MetadataUrl public sealed partial record MinecraftVersion(
[property: MemoryPackOrder(0)] string Id,
[property: MemoryPackOrder(1)] MinecraftVersionType Type,
[property: MemoryPackOrder(2)] string MetadataUrl
); );

View File

@ -1,4 +1,7 @@
using Phantom.Common.Data.Replies; using System.Collections.Immutable;
using Phantom.Common.Data.Java;
using Phantom.Common.Data.Minecraft;
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.Common.Messages.Web.BiDirectional; using Phantom.Common.Messages.Web.BiDirectional;
@ -12,5 +15,8 @@ public interface IMessageToControllerListener {
Task<LogInSuccess?> HandleLogIn(LogInMessage message); Task<LogInSuccess?> HandleLogIn(LogInMessage message);
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<ImmutableArray<MinecraftVersion>> HandleGetMinecraftVersions(GetMinecraftVersionsMessage message);
Task<ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>> HandleGetAgentJavaRuntimes(GetAgentJavaRuntimes message);
Task<NoReply> HandleReply(ReplyMessage message); Task<NoReply> HandleReply(ReplyMessage message);
} }

View File

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

View File

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

View File

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

View File

@ -1,4 +1,7 @@
using Phantom.Common.Data.Replies; using System.Collections.Immutable;
using Phantom.Common.Data.Java;
using Phantom.Common.Data.Minecraft;
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.Common.Logging; using Phantom.Common.Logging;
@ -20,6 +23,9 @@ public static class WebMessageRegistries {
ToController.Add<LogInMessage, LogInSuccess?>(1); ToController.Add<LogInMessage, LogInSuccess?>(1);
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<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(5);
ToController.Add<GetAgentJavaRuntimes, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(6);
ToController.Add<ReplyMessage>(127); ToController.Add<ReplyMessage>(127);
ToWeb.Add<RegisterWebResultMessage>(0); ToWeb.Add<RegisterWebResultMessage>(0);

View File

@ -60,7 +60,7 @@ public sealed class ControllerServices {
} }
public WebMessageListener CreateWebMessageListener(RpcConnectionToClient<IMessageToWebListener> connection) { public WebMessageListener CreateWebMessageListener(RpcConnectionToClient<IMessageToWebListener> connection) {
return new WebMessageListener(connection, webAuthToken, UserManager, UserLoginManager, AgentManager, InstanceManager, TaskManager); return new WebMessageListener(connection, webAuthToken, UserManager, UserLoginManager, AgentManager, AgentJavaRuntimesManager, InstanceManager, MinecraftVersions, TaskManager);
} }
public async Task Initialize() { public async Task Initialize() {

View File

@ -1,23 +0,0 @@
namespace Phantom.Controller.Services.Instances;
public enum AddOrEditInstanceResult : byte {
UnknownError,
Success,
InstanceNameMustNotBeEmpty,
InstanceMemoryMustNotBeZero,
MinecraftVersionDownloadInfoNotFound,
AgentNotFound
}
public static class AddOrEditInstanceResultExtensions {
public static string ToSentence(this AddOrEditInstanceResult reason) {
return reason switch {
AddOrEditInstanceResult.Success => "Success.",
AddOrEditInstanceResult.InstanceNameMustNotBeEmpty => "Instance name must not be empty.",
AddOrEditInstanceResult.InstanceMemoryMustNotBeZero => "Memory must not be 0 MB.",
AddOrEditInstanceResult.MinecraftVersionDownloadInfoNotFound => "Could not find download information for the selected Minecraft version.",
AddOrEditInstanceResult.AgentNotFound => "Agent not found.",
_ => "Unknown error."
};
}
}

View File

@ -63,7 +63,7 @@ sealed class InstanceManager {
} }
[SuppressMessage("ReSharper", "ConvertIfStatementToConditionalTernaryExpression")] [SuppressMessage("ReSharper", "ConvertIfStatementToConditionalTernaryExpression")]
public async Task<InstanceActionResult<CreateOrUpdateInstanceResult>> CreateOrUpdateInstance(InstanceConfiguration configuration, Guid auditLogUserGuid) { public async Task<InstanceActionResult<CreateOrUpdateInstanceResult>> CreateOrUpdateInstance(Guid auditLogUserGuid, InstanceConfiguration configuration) {
var agent = agentManager.GetAgent(configuration.AgentGuid); var agent = agentManager.GetAgent(configuration.AgentGuid);
if (agent == null) { if (agent == null) {
return InstanceActionResult.Concrete(CreateOrUpdateInstanceResult.AgentNotFound); return InstanceActionResult.Concrete(CreateOrUpdateInstanceResult.AgentNotFound);
@ -176,34 +176,38 @@ sealed class InstanceManager {
return instances.ByGuid.TryGetValue(instanceGuid, out var instance) ? await SendInstanceActionMessage<TMessage, TReply>(instance, message) : InstanceActionResult.General<TReply>(InstanceActionGeneralResult.InstanceDoesNotExist); return instances.ByGuid.TryGetValue(instanceGuid, out var instance) ? await SendInstanceActionMessage<TMessage, TReply>(instance, message) : InstanceActionResult.General<TReply>(InstanceActionGeneralResult.InstanceDoesNotExist);
} }
public async Task<InstanceActionResult<LaunchInstanceResult>> LaunchInstance(Guid instanceGuid) { public async Task<InstanceActionResult<LaunchInstanceResult>> LaunchInstance(Guid auditLogUserGuid, Guid instanceGuid) {
var result = await SendInstanceActionMessage<LaunchInstanceMessage, LaunchInstanceResult>(instanceGuid, new LaunchInstanceMessage(instanceGuid)); var result = await SendInstanceActionMessage<LaunchInstanceMessage, LaunchInstanceResult>(instanceGuid, new LaunchInstanceMessage(instanceGuid));
if (result.Is(LaunchInstanceResult.LaunchInitiated)) { if (result.Is(LaunchInstanceResult.LaunchInitiated)) {
await SetInstanceShouldLaunchAutomatically(instanceGuid, true); await HandleInstanceManuallyLaunchedOrStopped(instanceGuid, true, auditLogUserGuid, auditLogRepository => auditLogRepository.AddInstanceLaunchedEvent(instanceGuid));
} }
return result; return result;
} }
public async Task<InstanceActionResult<StopInstanceResult>> StopInstance(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 SetInstanceShouldLaunchAutomatically(instanceGuid, false); await HandleInstanceManuallyLaunchedOrStopped(instanceGuid, false, auditLogUserGuid, auditLogRepository => auditLogRepository.AddInstanceLaunchedEvent(instanceGuid));
} }
return result; return result;
} }
private async Task SetInstanceShouldLaunchAutomatically(Guid instanceGuid, bool shouldLaunchAutomatically) { private async Task HandleInstanceManuallyLaunchedOrStopped(Guid instanceGuid, bool wasLaunched, Guid auditLogUserGuid, Action<AuditLogRepository> addAuditEvent) {
await modifyInstancesSemaphore.WaitAsync(cancellationToken); await modifyInstancesSemaphore.WaitAsync(cancellationToken);
try { try {
instances.ByGuid.TryReplace(instanceGuid, instance => instance with { LaunchAutomatically = shouldLaunchAutomatically }); instances.ByGuid.TryReplace(instanceGuid, instance => instance with { LaunchAutomatically = wasLaunched });
await using var ctx = dbProvider.Eager(); await using var db = dbProvider.Lazy();
var entity = await 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 = shouldLaunchAutomatically; entity.LaunchAutomatically = wasLaunched;
await ctx.SaveChangesAsync(cancellationToken);
var auditLogRepository = new AuditLogRepository(db, auditLogUserGuid);
addAuditEvent(auditLogRepository);
await db.Ctx.SaveChangesAsync(cancellationToken);
} }
} finally { } finally {
modifyInstancesSemaphore.Release(); modifyInstancesSemaphore.Release();

View File

@ -1,5 +1,7 @@
using System.Collections.Immutable; using System.Collections.Immutable;
using Phantom.Common.Data; using Phantom.Common.Data;
using Phantom.Common.Data.Java;
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.Instance; using Phantom.Common.Data.Web.Instance;
@ -9,6 +11,7 @@ using Phantom.Common.Messages.Web;
using Phantom.Common.Messages.Web.BiDirectional; using Phantom.Common.Messages.Web.BiDirectional;
using Phantom.Common.Messages.Web.ToController; using Phantom.Common.Messages.Web.ToController;
using Phantom.Common.Messages.Web.ToWeb; using Phantom.Common.Messages.Web.ToWeb;
using Phantom.Controller.Minecraft;
using Phantom.Controller.Rpc; using Phantom.Controller.Rpc;
using Phantom.Controller.Services.Agents; using Phantom.Controller.Services.Agents;
using Phantom.Controller.Services.Instances; using Phantom.Controller.Services.Instances;
@ -27,16 +30,20 @@ public sealed class WebMessageListener : IMessageToControllerListener {
private readonly UserManager userManager; private readonly UserManager userManager;
private readonly UserLoginManager userLoginManager; private readonly UserLoginManager userLoginManager;
private readonly AgentManager agentManager; private readonly AgentManager agentManager;
private readonly AgentJavaRuntimesManager agentJavaRuntimesManager;
private readonly InstanceManager instanceManager; private readonly InstanceManager instanceManager;
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, InstanceManager instanceManager, TaskManager taskManager) { internal WebMessageListener(RpcConnectionToClient<IMessageToWebListener> connection, AuthToken authToken, UserManager userManager, UserLoginManager userLoginManager, AgentManager agentManager, AgentJavaRuntimesManager agentJavaRuntimesManager, InstanceManager instanceManager, MinecraftVersions minecraftVersions, TaskManager taskManager) {
this.connection = connection; this.connection = connection;
this.authToken = authToken; this.authToken = authToken;
this.userManager = userManager; this.userManager = userManager;
this.userLoginManager = userLoginManager; this.userLoginManager = userLoginManager;
this.agentManager = agentManager; this.agentManager = agentManager;
this.agentJavaRuntimesManager = agentJavaRuntimesManager;
this.instanceManager = instanceManager; this.instanceManager = instanceManager;
this.minecraftVersions = minecraftVersions;
this.taskManager = taskManager; this.taskManager = taskManager;
} }
@ -72,7 +79,19 @@ public sealed class WebMessageListener : IMessageToControllerListener {
} }
public Task<InstanceActionResult<CreateOrUpdateInstanceResult>> HandleCreateOrUpdateInstance(CreateOrUpdateInstanceMessage message) { public Task<InstanceActionResult<CreateOrUpdateInstanceResult>> HandleCreateOrUpdateInstance(CreateOrUpdateInstanceMessage message) {
return instanceManager.CreateOrUpdateInstance(message.Configuration, message.LoggedInUserGuid); return instanceManager.CreateOrUpdateInstance( message.LoggedInUserGuid, message.Configuration);
}
public Task<InstanceActionResult<LaunchInstanceResult>> HandleLaunchInstance(LaunchInstanceMessage message) {
return instanceManager.LaunchInstance(message.LoggedInUserGuid, message.InstanceGuid);
}
public Task<ImmutableArray<MinecraftVersion>> HandleGetMinecraftVersions(GetMinecraftVersionsMessage message) {
return minecraftVersions.GetVersions(CancellationToken.None);
}
public Task<ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>> HandleGetAgentJavaRuntimes(GetAgentJavaRuntimes message) {
return Task.FromResult(agentJavaRuntimesManager.All);
} }
public Task<LogInSuccess?> HandleLogIn(LogInMessage message) { public Task<LogInSuccess?> HandleLogIn(LogInMessage message) {

View File

@ -10,11 +10,11 @@ public sealed class AgentManager {
public EventSubscribers<ImmutableArray<AgentWithStats>> AgentsChanged => agents.Subs; public EventSubscribers<ImmutableArray<AgentWithStats>> AgentsChanged => agents.Subs;
public ImmutableDictionary<Guid, AgentWithStats> ToDictionaryByGuid() {
return agents.Value.ToImmutableDictionary(static agent => agent.Guid);
}
internal void RefreshAgents(ImmutableArray<AgentWithStats> newAgents) { internal void RefreshAgents(ImmutableArray<AgentWithStats> newAgents) {
agents.SetTo(newAgents); agents.SetTo(newAgents);
} }
public ImmutableDictionary<Guid, AgentWithStats> ToDictionaryByGuid() {
return agents.Value.ToImmutableDictionary(static agent => agent.Guid);
}
} }

View File

@ -3,10 +3,10 @@ using Phantom.Common.Data.Web.Users;
namespace Phantom.Web.Services.Authentication; namespace Phantom.Web.Services.Authentication;
sealed record UserInfo(Guid UserGuid, string Username, PermissionSet Permissions) { public sealed record UserInfo(Guid UserGuid, string Username, PermissionSet Permissions) {
private const string AuthenticationType = "Phantom"; private const string AuthenticationType = "Phantom";
public ClaimsPrincipal AsClaimsPrincipal { internal ClaimsPrincipal AsClaimsPrincipal {
get { get {
var identity = new ClaimsIdentity(AuthenticationType); var identity = new ClaimsIdentity(AuthenticationType);
@ -18,6 +18,6 @@ sealed record UserInfo(Guid UserGuid, string Username, PermissionSet Permissions
} }
public static Guid? TryGetGuid(ClaimsPrincipal principal) { public static Guid? TryGetGuid(ClaimsPrincipal principal) {
return principal.FindFirstValue(ClaimTypes.NameIdentifier) is {} guidStr && Guid.TryParse(guidStr, out var guid) ? guid : null; return principal.Identity is { IsAuthenticated: true, AuthenticationType: AuthenticationType } && principal.FindFirstValue(ClaimTypes.NameIdentifier) is {} guidStr && Guid.TryParse(guidStr, out var guid) ? guid : null;
} }
} }

View File

@ -9,21 +9,33 @@ using Phantom.Web.Services.Rpc;
namespace Phantom.Web.Services.Instances; namespace Phantom.Web.Services.Instances;
using InstanceDictionary = ImmutableDictionary<Guid, Instance>;
public sealed class InstanceManager { public sealed class InstanceManager {
private readonly ControllerConnection controllerConnection; private readonly ControllerConnection controllerConnection;
private readonly SimpleObservableState<ImmutableArray<Instance>> instances = new (PhantomLogger.Create<InstanceManager>("Instances"), ImmutableArray<Instance>.Empty); private readonly SimpleObservableState<InstanceDictionary> instances = new (PhantomLogger.Create<InstanceManager>("Instances"), InstanceDictionary.Empty);
public InstanceManager(ControllerConnection controllerConnection) { public InstanceManager(ControllerConnection controllerConnection) {
this.controllerConnection = controllerConnection; this.controllerConnection = controllerConnection;
} }
public EventSubscribers<ImmutableArray<Instance>> InstancesChanged => instances.Subs; public EventSubscribers<InstanceDictionary> InstancesChanged => instances.Subs;
internal void RefreshInstances(ImmutableArray<Instance> newInstances) { internal void RefreshInstances(ImmutableArray<Instance> newInstances) {
instances.SetTo(newInstances); instances.SetTo(newInstances.ToImmutableDictionary(static instance => instance.Configuration.InstanceGuid));
} }
public Task<InstanceActionResult<CreateOrUpdateInstanceResult>> CreateOrUpdateInstance(InstanceConfiguration configuration) { public Instance? GetByGuid(Guid instanceGuid) {
return controllerConnection.Send<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(new CreateOrUpdateInstanceMessage(configuration), TimeSpan.FromSeconds(30)); return instances.Value.GetValueOrDefault(instanceGuid);
}
public Task<InstanceActionResult<CreateOrUpdateInstanceResult>> CreateOrUpdateInstance(Guid loggedInUserGuid, InstanceConfiguration configuration) {
var message = new CreateOrUpdateInstanceMessage(loggedInUserGuid, configuration);
return controllerConnection.Send<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(message, TimeSpan.FromSeconds(30));
}
public Task<InstanceActionResult<LaunchInstanceResult>> LaunchInstance(Guid loggedInUserGuid, Guid instanceGuid) {
var message = new LaunchInstanceMessage(loggedInUserGuid, instanceGuid);
return controllerConnection.Send<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(message, TimeSpan.FromSeconds(30));
} }
} }

View File

@ -14,7 +14,7 @@ public sealed class ControllerConnection {
return connection.Send(message); return connection.Send(message);
} }
public Task<TReply> Send<TMessage, TReply>(TMessage message, TimeSpan timeout) 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, timeout, CancellationToken.None); return connection.Send<TMessage, TReply>(message, waitForReplyTime, waitForReplyCancellationToken);
} }
} }

View File

@ -4,6 +4,7 @@ using Phantom.Common.Data.Web.Users;
using Phantom.Common.Logging; using Phantom.Common.Logging;
using Phantom.Web.Services.Authorization; using Phantom.Web.Services.Authorization;
using ILogger = Serilog.ILogger; using ILogger = Serilog.ILogger;
using UserInfo = Phantom.Web.Services.Authentication.UserInfo;
namespace Phantom.Web.Base; namespace Phantom.Web.Base;
@ -16,6 +17,11 @@ public abstract class PhantomComponent : ComponentBase {
[Inject] [Inject]
public PermissionManager PermissionManager { get; set; } = null!; public PermissionManager PermissionManager { get; set; } = null!;
public async Task<Guid?> GetUserGuid() {
var authenticationState = await AuthenticationStateTask;
return UserInfo.TryGetGuid(authenticationState.User);
}
protected async Task<bool> CheckPermission(Permission permission) { protected async Task<bool> CheckPermission(Permission permission) {
var authenticationState = await AuthenticationStateTask; var authenticationState = await AuthenticationStateTask;
return PermissionManager.CheckPermission(authenticationState.User, permission); return PermissionManager.CheckPermission(authenticationState.User, permission);

View File

@ -1,12 +1,13 @@
@page "/instances/{InstanceGuid:guid}" @page "/instances/{InstanceGuid:guid}"
@attribute [Authorize(Permission.ViewInstancesPolicy)] @attribute [Authorize(Permission.ViewInstancesPolicy)]
@inherits PhantomComponent @inherits PhantomComponent
@using Phantom.Web.Services.Instances @using Phantom.Common.Data.Instance
@using Phantom.Common.Data.Web.Users
@using Phantom.Common.Data.Replies @using Phantom.Common.Data.Replies
@using Phantom.Common.Data.Web.Instance
@using Phantom.Common.Data.Web.Users
@using Phantom.Web.Services.Instances
@implements IDisposable @implements IDisposable
@inject InstanceManager InstanceManager @inject InstanceManager InstanceManager
@inject AuditLog AuditLog
@if (Instance == null) { @if (Instance == null) {
<h1>Instance Not Found</h1> <h1>Instance Not Found</h1>
@ -45,7 +46,7 @@ else {
@code { @code {
[Parameter] [Parameter]
public Guid InstanceGuid { get; set; } public Guid InstanceGuid { get; init; }
private string? lastError = null; private string? lastError = null;
private bool isLaunchingInstance = false; private bool isLaunchingInstance = false;
@ -63,6 +64,11 @@ else {
} }
private async Task LaunchInstance() { private async Task LaunchInstance() {
var loggedInUserGuid = await GetUserGuid();
if (loggedInUserGuid == null) {
return;
}
isLaunchingInstance = true; isLaunchingInstance = true;
lastError = null; lastError = null;
@ -72,11 +78,8 @@ else {
return; return;
} }
var result = await InstanceManager.LaunchInstance(InstanceGuid); var result = await InstanceManager.LaunchInstance(loggedInUserGuid.Value, InstanceGuid);
if (result.Is(LaunchInstanceResult.LaunchInitiated)) { if (!result.Is(LaunchInstanceResult.LaunchInitiated)) {
await AuditLog.AddInstanceLaunchedEvent(InstanceGuid);
}
else {
lastError = result.ToSentence(Messages.ToSentence); lastError = result.ToSentence(Messages.ToSentence);
} }
} finally { } finally {

View File

@ -18,12 +18,12 @@ else {
@code { @code {
[Parameter] [Parameter]
public Guid InstanceGuid { get; set; } public Guid InstanceGuid { get; init; }
private InstanceConfiguration? InstanceConfiguration { get; set; } private InstanceConfiguration? InstanceConfiguration { get; set; }
protected override void OnInitialized() { protected override void OnInitialized() {
InstanceConfiguration = InstanceManager.GetInstanceConfiguration(InstanceGuid); InstanceConfiguration = InstanceManager.GetByGuid(InstanceGuid)?.Configuration;
} }
} }

View File

@ -83,7 +83,8 @@
}); });
InstanceManager.InstancesChanged.Subscribe(this, instances => { InstanceManager.InstancesChanged.Subscribe(this, instances => {
this.instances = instances.OrderBy(instance => agentNames.TryGetValue(instance.Configuration.AgentGuid, out var agentName) ? agentName : string.Empty) this.instances = instances.Values
.OrderBy(instance => agentNames.TryGetValue(instance.Configuration.AgentGuid, out var agentName) ? agentName : string.Empty)
.ThenBy(static instance => instance.Configuration.InstanceName) .ThenBy(static instance => instance.Configuration.InstanceName)
.ToImmutableArray(); .ToImmutableArray();
InvokeAsync(StateHasChanged); InvokeAsync(StateHasChanged);

View File

@ -87,7 +87,7 @@
} }
private async Task<Result<string>> CreateOrUpdateAdministrator() { private async Task<Result<string>> CreateOrUpdateAdministrator() {
var reply = await ControllerConnection.Send<CreateOrUpdateAdministratorUser, CreateOrUpdateAdministratorUserResult>(new CreateOrUpdateAdministratorUser(form.Username, form.Password), Timeout.InfiniteTimeSpan); var reply = await ControllerConnection.Send<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(new CreateOrUpdateAdministratorUserMessage(form.Username, form.Password), Timeout.InfiniteTimeSpan);
return reply switch { return reply switch {
Success => Result.Ok<string>(), Success => Result.Ok<string>(),
CreationFailed fail => fail.Error.ToSentences("\n"), CreationFailed fail => fail.Error.ToSentences("\n"),

View File

@ -1,27 +1,31 @@
@using Phantom.Web.Components.Utils @using Phantom.Web.Components.Utils
@using Phantom.Common.Data.Minecraft
@using Phantom.Common.Data.Web.Minecraft
@using Phantom.Common.Data.Instance
@using Phantom.Common.Data.Java
@using System.Collections.Immutable @using System.Collections.Immutable
@using System.ComponentModel.DataAnnotations @using System.ComponentModel.DataAnnotations
@using System.Diagnostics.CodeAnalysis @using System.Diagnostics.CodeAnalysis
@using Phantom.Common.Data.Minecraft
@using Phantom.Common.Data.Web.Agent
@using Phantom.Common.Data.Web.Instance
@using Phantom.Common.Data.Web.Minecraft
@using Phantom.Common.Messages.Web.ToController
@using Phantom.Common.Data.Instance
@using Phantom.Common.Data.Java
@using Phantom.Common.Data @using Phantom.Common.Data
@using Phantom.Web.Services @using Phantom.Web.Services
@using Phantom.Web.Services.Agents
@using Phantom.Web.Services.Instances @using Phantom.Web.Services.Instances
@using Phantom.Web.Services.Rpc
@inherits PhantomComponent
@inject INavigation Nav @inject INavigation Nav
@inject MinecraftVersions MinecraftVersions @inject ControllerConnection ControllerConnection
@inject AgentManager AgentManager @inject AgentManager AgentManager
@inject AgentJavaRuntimesManager AgentJavaRuntimesManager
@inject InstanceManager InstanceManager @inject InstanceManager InstanceManager
@inject AuditLog AuditLog
<Form Model="form" OnSubmit="AddOrEditInstance"> <Form Model="form" OnSubmit="AddOrEditInstance">
@{ var selectedAgent = form.SelectedAgent; } @{ var selectedAgent = form.SelectedAgent; }
<div class="row"> <div class="row">
<div class="col-xl-7 mb-3"> <div class="col-xl-7 mb-3">
@{ @{
static RenderFragment GetAgentOption(Agent agent) { static RenderFragment GetAgentOption(AgentWithStats agent) {
return @<option value="@agent.Guid"> return @<option value="@agent.Guid">
@agent.Name @agent.Name
&bullet; &bullet;
@ -34,14 +38,14 @@
@if (EditedInstanceConfiguration == null) { @if (EditedInstanceConfiguration == null) {
<FormSelectInput Id="instance-agent" Label="Agent" @bind-Value="form.SelectedAgentGuid"> <FormSelectInput Id="instance-agent" Label="Agent" @bind-Value="form.SelectedAgentGuid">
<option value="" selected>Select which agent will run the instance...</option> <option value="" selected>Select which agent will run the instance...</option>
@foreach (var agent in form.AgentsByGuid.Values.Where(static agent => agent.IsOnline).OrderBy(static agent => agent.Name)) { @foreach (var agent in allAgentsByGuid.Values.Where(static agent => agent.IsOnline).OrderBy(static agent => agent.Name)) {
@GetAgentOption(agent) @GetAgentOption(agent)
} }
</FormSelectInput> </FormSelectInput>
} }
else { else {
<FormSelectInput Id="instance-agent" Label="Agent" @bind-Value="form.SelectedAgentGuid" disabled="true"> <FormSelectInput Id="instance-agent" Label="Agent" @bind-Value="form.SelectedAgentGuid" disabled="true">
@if (form.SelectedAgentGuid is {} guid && form.AgentsByGuid.TryGetValue(guid, out var agent)) { @if (form.SelectedAgentGuid is {} guid && allAgentsByGuid.TryGetValue(guid, out var agent)) {
@GetAgentOption(agent) @GetAgentOption(agent)
} }
</FormSelectInput> </FormSelectInput>
@ -160,23 +164,25 @@
@code { @code {
[Parameter, EditorRequired] [Parameter, EditorRequired]
public InstanceConfiguration? EditedInstanceConfiguration { get; set; } public InstanceConfiguration? EditedInstanceConfiguration { get; init; }
private ConfigureInstanceFormModel form = null!; private ConfigureInstanceFormModel form = null!;
private ImmutableDictionary<Guid, AgentWithStats> allAgentsByGuid = ImmutableDictionary<Guid, AgentWithStats>.Empty;
private ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>> allAgentJavaRuntimes = ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>.Empty;
private MinecraftVersionType minecraftVersionType = MinecraftVersionType.Release; private MinecraftVersionType minecraftVersionType = MinecraftVersionType.Release;
private ImmutableArray<MinecraftVersion> allMinecraftVersions = ImmutableArray<MinecraftVersion>.Empty;
private ImmutableArray<MinecraftVersion> availableMinecraftVersions = ImmutableArray<MinecraftVersion>.Empty; private ImmutableArray<MinecraftVersion> availableMinecraftVersions = ImmutableArray<MinecraftVersion>.Empty;
private bool IsSubmittable => form.SelectedAgentGuid != null && !form.EditContext.GetValidationMessages(form.EditContext.Field(nameof(ConfigureInstanceFormModel.SelectedAgentGuid))).Any(); private bool IsSubmittable => form.SelectedAgentGuid != null && !form.EditContext.GetValidationMessages(form.EditContext.Field(nameof(ConfigureInstanceFormModel.SelectedAgentGuid))).Any();
private sealed class ConfigureInstanceFormModel : FormModel { private sealed class ConfigureInstanceFormModel : FormModel {
public ImmutableDictionary<Guid, Agent> AgentsByGuid { get; } private readonly InstanceAddOrEditForm page;
private readonly ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>> javaRuntimesByAgentGuid;
private readonly RamAllocationUnits? editedInstanceRamAllocation; private readonly RamAllocationUnits? editedInstanceRamAllocation;
public ConfigureInstanceFormModel(AgentManager agentManager, AgentJavaRuntimesManager agentJavaRuntimesManager, RamAllocationUnits? editedInstanceRamAllocation) { public ConfigureInstanceFormModel(InstanceAddOrEditForm page, RamAllocationUnits? editedInstanceRamAllocation) {
this.AgentsByGuid = agentManager.GetAgents().ToImmutableDictionary(); this.page = page;
this.javaRuntimesByAgentGuid = agentJavaRuntimesManager.All;
this.editedInstanceRamAllocation = editedInstanceRamAllocation; this.editedInstanceRamAllocation = editedInstanceRamAllocation;
} }
@ -190,14 +196,14 @@
} }
} }
private bool TryGetAgent(Guid? agentGuid, [NotNullWhen(true)] out Agent? agent) { private bool TryGetAgent(Guid? agentGuid, [NotNullWhen(true)] out AgentWithStats? agent) {
return TryGet(AgentsByGuid, agentGuid, out agent); return TryGet(page.allAgentsByGuid, agentGuid, out agent);
} }
public Agent? SelectedAgent => TryGetAgent(SelectedAgentGuid, out var agent) ? agent : null; public AgentWithStats? SelectedAgent => TryGetAgent(SelectedAgentGuid, out var agent) ? agent : null;
public ImmutableArray<TaggedJavaRuntime> JavaRuntimesForSelectedAgent => TryGet(javaRuntimesByAgentGuid, SelectedAgentGuid, out var javaRuntimes) ? javaRuntimes : ImmutableArray<TaggedJavaRuntime>.Empty; public ImmutableArray<TaggedJavaRuntime> JavaRuntimesForSelectedAgent => TryGet(page.allAgentJavaRuntimes, SelectedAgentGuid, out var javaRuntimes) ? javaRuntimes : ImmutableArray<TaggedJavaRuntime>.Empty;
public ushort MaximumMemoryUnits => SelectedAgent?.MaxMemory.RawValue ?? 0; public ushort MaximumMemoryUnits => SelectedAgent?.MaxMemory.RawValue ?? 0;
public ushort AvailableMemoryUnits => Math.Min((SelectedAgent?.AvailableMemory + editedInstanceRamAllocation)?.RawValue ?? MaximumMemoryUnits, MaximumMemoryUnits); public ushort AvailableMemoryUnits => Math.Min((SelectedAgent?.AvailableMemory + editedInstanceRamAllocation)?.RawValue ?? MaximumMemoryUnits, MaximumMemoryUnits);
private ushort selectedMemoryUnits = 4; private ushort selectedMemoryUnits = 4;
@ -263,8 +269,17 @@
} }
protected override void OnInitialized() { protected override void OnInitialized() {
form = new ConfigureInstanceFormModel(AgentManager, AgentJavaRuntimesManager, EditedInstanceConfiguration?.MemoryAllocation); form = new ConfigureInstanceFormModel(this, EditedInstanceConfiguration?.MemoryAllocation);
}
protected override async Task OnInitializedAsync() {
var agentJavaRuntimesTask = ControllerConnection.Send<GetAgentJavaRuntimes, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(new GetAgentJavaRuntimes(), TimeSpan.FromSeconds(30));
var minecraftVersionsTask = ControllerConnection.Send<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(new GetMinecraftVersionsMessage(), TimeSpan.FromSeconds(30));
allAgentsByGuid = AgentManager.ToDictionaryByGuid();
allAgentJavaRuntimes = await agentJavaRuntimesTask;
allMinecraftVersions = await minecraftVersionsTask;
if (EditedInstanceConfiguration != null) { if (EditedInstanceConfiguration != null) {
form.SelectedAgentGuid = EditedInstanceConfiguration.AgentGuid; form.SelectedAgentGuid = EditedInstanceConfiguration.AgentGuid;
form.InstanceName = EditedInstanceConfiguration.InstanceName; form.InstanceName = EditedInstanceConfiguration.InstanceName;
@ -275,28 +290,21 @@
form.MemoryUnits = EditedInstanceConfiguration.MemoryAllocation.RawValue; form.MemoryUnits = EditedInstanceConfiguration.MemoryAllocation.RawValue;
form.JavaRuntimeGuid = EditedInstanceConfiguration.JavaRuntimeGuid; form.JavaRuntimeGuid = EditedInstanceConfiguration.JavaRuntimeGuid;
form.JvmArguments = JvmArgumentsHelper.Join(EditedInstanceConfiguration.JvmArguments); form.JvmArguments = JvmArgumentsHelper.Join(EditedInstanceConfiguration.JvmArguments);
minecraftVersionType = allMinecraftVersions.FirstOrDefault(version => version.Id == EditedInstanceConfiguration.MinecraftVersion)?.Type ?? minecraftVersionType;
} }
form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.SelectedAgentGuid), revalidated: nameof(ConfigureInstanceFormModel.MemoryUnits)); form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.SelectedAgentGuid), revalidated: nameof(ConfigureInstanceFormModel.MemoryUnits));
form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.SelectedAgentGuid), revalidated: nameof(ConfigureInstanceFormModel.JavaRuntimeGuid)); form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.SelectedAgentGuid), revalidated: nameof(ConfigureInstanceFormModel.JavaRuntimeGuid));
form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.SelectedAgentGuid), revalidated: nameof(ConfigureInstanceFormModel.ServerPort)); form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.SelectedAgentGuid), revalidated: nameof(ConfigureInstanceFormModel.ServerPort));
form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.SelectedAgentGuid), revalidated: nameof(ConfigureInstanceFormModel.RconPort)); form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.SelectedAgentGuid), revalidated: nameof(ConfigureInstanceFormModel.RconPort));
form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.ServerPort), revalidated: nameof(ConfigureInstanceFormModel.RconPort)); form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.ServerPort), revalidated: nameof(ConfigureInstanceFormModel.RconPort));
SetMinecraftVersionType(minecraftVersionType);
} }
protected override async Task OnInitializedAsync() { private void SetMinecraftVersionType(MinecraftVersionType type) {
if (EditedInstanceConfiguration != null) {
var allMinecraftVersions = await MinecraftVersions.GetVersions(CancellationToken.None);
minecraftVersionType = allMinecraftVersions.FirstOrDefault(version => version.Id == EditedInstanceConfiguration.MinecraftVersion)?.Type ?? minecraftVersionType;
}
await SetMinecraftVersionType(minecraftVersionType);
}
private async Task SetMinecraftVersionType(MinecraftVersionType type) {
minecraftVersionType = type; minecraftVersionType = type;
var allMinecraftVersions = await MinecraftVersions.GetVersions(CancellationToken.None);
availableMinecraftVersions = allMinecraftVersions.Where(version => version.Type == type).ToImmutableArray(); availableMinecraftVersions = allMinecraftVersions.Where(version => version.Type == type).ToImmutableArray();
if (!availableMinecraftVersions.IsEmpty && !availableMinecraftVersions.Any(version => version.Id == form.MinecraftVersion)) { if (!availableMinecraftVersions.IsEmpty && !availableMinecraftVersions.Any(version => version.Id == form.MinecraftVersion)) {
@ -305,6 +313,11 @@
} }
private async Task AddOrEditInstance(EditContext context) { private async Task AddOrEditInstance(EditContext context) {
var loggedInUserGuid = await GetUserGuid();
if (loggedInUserGuid == null) {
return;
}
var selectedAgent = form.SelectedAgent; var selectedAgent = form.SelectedAgent;
if (selectedAgent == null) { if (selectedAgent == null) {
return; return;
@ -325,14 +338,13 @@
JvmArgumentsHelper.Split(form.JvmArguments) JvmArgumentsHelper.Split(form.JvmArguments)
); );
var result = await InstanceManager.AddOrEditInstance(instance); var result = await InstanceManager.CreateOrUpdateInstance(loggedInUserGuid.Value, instance);
if (result.Is(AddOrEditInstanceResult.Success)) { if (result.Is(CreateOrUpdateInstanceResult.Success)) {
await (EditedInstanceConfiguration == null ? AuditLog.AddInstanceCreatedEvent(instance.InstanceGuid) : AuditLog.AddInstanceEditedEvent(instance.InstanceGuid)); await Nav.NavigateTo("instances/" + instance.InstanceGuid);
Nav.NavigateTo("instances/" + instance.InstanceGuid);
} }
else { else {
form.SubmitModel.StopSubmitting(result.ToSentence(AddOrEditInstanceResultExtensions.ToSentence)); form.SubmitModel.StopSubmitting(result.ToSentence(CreateOrUpdateInstanceResultExtensions.ToSentence));
} }
} }
} }