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
d9c187994b
WIP 2023-11-03 23:32:40 +01:00
3a18d2067f
WIP 2023-11-01 10:39:23 +01:00
41 changed files with 561 additions and 305 deletions

View File

@ -0,0 +1,21 @@
using MemoryPack;
using Phantom.Common.Data.Agent;
namespace Phantom.Common.Data.Web.Agent;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record AgentWithStats(
[property: MemoryPackOrder(0)] Guid Guid,
[property: MemoryPackOrder(1)] string Name,
[property: MemoryPackOrder(2)] ushort ProtocolVersion,
[property: MemoryPackOrder(3)] string BuildVersion,
[property: MemoryPackOrder(4)] ushort MaxInstances,
[property: MemoryPackOrder(5)] RamAllocationUnits MaxMemory,
[property: MemoryPackOrder(6)] AllowedPorts? AllowedServerPorts,
[property: MemoryPackOrder(7)] AllowedPorts? AllowedRconPorts,
[property: MemoryPackOrder(8)] AgentStats? Stats,
[property: MemoryPackOrder(9)] DateTimeOffset? LastPing,
[property: MemoryPackOrder(10)] bool IsOnline
) {
public RamAllocationUnits? AvailableMemory => MaxMemory - Stats?.RunningInstanceMemory;
}

View File

@ -0,0 +1,15 @@
using MemoryPack;
using Phantom.Common.Data.Instance;
namespace Phantom.Common.Data.Web.Instance;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record Instance(
[property: MemoryPackOrder(0)] InstanceConfiguration Configuration,
[property: MemoryPackOrder(1)] IInstanceStatus Status,
[property: MemoryPackOrder(2)] bool LaunchAutomatically
) {
public static Instance Offline(InstanceConfiguration configuration, bool launchAutomatically = false) {
return new Instance(configuration, InstanceStatus.Offline, launchAutomatically);
}
}

View File

@ -10,4 +10,8 @@
<PackageReference Include="MemoryPack" /> <PackageReference Include="MemoryPack" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Phantom.Common.Data\Phantom.Common.Data.csproj" />
</ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,9 @@
using MemoryPack;
namespace Phantom.Common.Data.Agent;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record AgentStats(
[property: MemoryPackOrder(0)] int RunningInstanceCount,
[property: MemoryPackOrder(1)] RamAllocationUnits RunningInstanceMemory
);

View File

@ -1,4 +1,6 @@
using Phantom.Common.Data.Web.Users; using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Web.Instance;
using Phantom.Common.Data.Web.Users;
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.Utils.Rpc.Message; using Phantom.Utils.Rpc.Message;
@ -7,7 +9,8 @@ namespace Phantom.Common.Messages.Web;
public interface IMessageToControllerListener { public interface IMessageToControllerListener {
Task<NoReply> HandleRegisterWeb(RegisterWebMessage message); Task<NoReply> HandleRegisterWeb(RegisterWebMessage message);
Task<CreateOrUpdateAdministratorUserResult> CreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUser message); Task<LogInSuccess?> HandleLogIn(LogInMessage message);
Task<LogInSuccess?> HandleLogIn(LogIn message); Task<CreateOrUpdateAdministratorUserResult> HandleCreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUserMessage message);
Task<InstanceActionResult<CreateOrUpdateInstanceResult>> HandleCreateOrUpdateInstance(CreateOrUpdateInstanceMessage message);
Task<NoReply> HandleReply(ReplyMessage message); Task<NoReply> HandleReply(ReplyMessage message);
} }

View File

@ -6,5 +6,7 @@ namespace Phantom.Common.Messages.Web;
public interface IMessageToWebListener { public interface IMessageToWebListener {
Task<NoReply> HandleRegisterWebResult(RegisterWebResultMessage message); Task<NoReply> HandleRegisterWebResult(RegisterWebResultMessage message);
Task<NoReply> HandleRefreshAgents(RefreshAgentsMessage message);
Task<NoReply> HandleRefreshInstances(RefreshInstancesMessage message);
Task<NoReply> HandleReply(ReplyMessage message); Task<NoReply> HandleReply(ReplyMessage message);
} }

View File

@ -4,11 +4,11 @@ using Phantom.Common.Data.Web.Users;
namespace Phantom.Common.Messages.Web.ToController; namespace Phantom.Common.Messages.Web.ToController;
[MemoryPackable(GenerateType.VersionTolerant)] [MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record CreateOrUpdateAdministratorUser( public sealed partial record CreateOrUpdateAdministratorUserMessage(
[property: MemoryPackOrder(0)] string Username, [property: MemoryPackOrder(0)] string Username,
[property: MemoryPackOrder(1)] string Password [property: MemoryPackOrder(1)] string Password
) : IMessageToController<CreateOrUpdateAdministratorUserResult> { ) : IMessageToController<CreateOrUpdateAdministratorUserResult> {
public Task<CreateOrUpdateAdministratorUserResult> Accept(IMessageToControllerListener listener) { public Task<CreateOrUpdateAdministratorUserResult> Accept(IMessageToControllerListener listener) {
return listener.CreateOrUpdateAdministratorUser(this); return listener.HandleCreateOrUpdateAdministratorUser(this);
} }
} }

View File

@ -0,0 +1,16 @@
using MemoryPack;
using Phantom.Common.Data.Instance;
using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Web.Instance;
namespace Phantom.Common.Messages.Web.ToController;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record CreateOrUpdateInstanceMessage(
[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
[property: MemoryPackOrder(1)] InstanceConfiguration Configuration
) : IMessageToController<InstanceActionResult<CreateOrUpdateInstanceResult>> {
public Task<InstanceActionResult<CreateOrUpdateInstanceResult>> Accept(IMessageToControllerListener listener) {
return listener.HandleCreateOrUpdateInstance(this);
}
}

View File

@ -4,7 +4,7 @@ using Phantom.Common.Data.Web.Users;
namespace Phantom.Common.Messages.Web.ToController; namespace Phantom.Common.Messages.Web.ToController;
[MemoryPackable(GenerateType.VersionTolerant)] [MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record LogIn( public sealed partial record LogInMessage(
[property: MemoryPackOrder(0)] string Username, [property: MemoryPackOrder(0)] string Username,
[property: MemoryPackOrder(1)] string Password [property: MemoryPackOrder(1)] string Password
) : IMessageToController<LogInSuccess?> { ) : IMessageToController<LogInSuccess?> {

View File

@ -0,0 +1,15 @@
using System.Collections.Immutable;
using MemoryPack;
using Phantom.Common.Data.Web.Agent;
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages.Web.ToWeb;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record RefreshAgentsMessage(
[property: MemoryPackOrder(0)] ImmutableArray<AgentWithStats> Agents
) : IMessageToWeb {
public Task<NoReply> Accept(IMessageToWebListener listener) {
return listener.HandleRefreshAgents(this);
}
}

View File

@ -0,0 +1,15 @@
using System.Collections.Immutable;
using MemoryPack;
using Phantom.Common.Data.Web.Instance;
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages.Web.ToWeb;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record RefreshInstancesMessage(
[property: MemoryPackOrder(0)] ImmutableArray<Instance> Instances
) : IMessageToWeb {
public Task<NoReply> Accept(IMessageToWebListener listener) {
return listener.HandleRefreshInstances(this);
}
}

View File

@ -1,4 +1,6 @@
using Phantom.Common.Data.Web.Users; using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Web.Instance;
using Phantom.Common.Data.Web.Users;
using Phantom.Common.Logging; using Phantom.Common.Logging;
using Phantom.Common.Messages.Web.BiDirectional; using Phantom.Common.Messages.Web.BiDirectional;
using Phantom.Common.Messages.Web.ToController; using Phantom.Common.Messages.Web.ToController;
@ -15,11 +17,14 @@ public static class WebMessageRegistries {
static WebMessageRegistries() { static WebMessageRegistries() {
ToController.Add<RegisterWebMessage>(0); ToController.Add<RegisterWebMessage>(0);
ToController.Add<CreateOrUpdateAdministratorUser, CreateOrUpdateAdministratorUserResult>(1); ToController.Add<LogInMessage, LogInSuccess?>(1);
ToController.Add<LogIn, LogInSuccess?>(2); ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(2);
ToController.Add<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(3);
ToController.Add<ReplyMessage>(127); ToController.Add<ReplyMessage>(127);
ToWeb.Add<RegisterWebResultMessage>(0); ToWeb.Add<RegisterWebResultMessage>(0);
ToWeb.Add<RefreshAgentsMessage>(1);
ToWeb.Add<RefreshInstancesMessage>(2);
ToWeb.Add<ReplyMessage>(127); ToWeb.Add<ReplyMessage>(127);
} }

View File

@ -20,8 +20,6 @@ public sealed record Agent(
public bool IsOnline { get; internal init; } public bool IsOnline { get; internal init; }
public bool IsOffline => !IsOnline; public bool IsOffline => !IsOnline;
public RamAllocationUnits? AvailableMemory => MaxMemory - Stats?.RunningInstanceMemory;
internal Agent(AgentInfo info) : this(info.Guid, info.Name, info.ProtocolVersion, info.BuildVersion, info.MaxInstances, info.MaxMemory, info.AllowedServerPorts, info.AllowedRconPorts) {} internal Agent(AgentInfo info) : this(info.Guid, info.Name, info.ProtocolVersion, info.BuildVersion, info.MaxInstances, info.MaxMemory, info.AllowedServerPorts, info.AllowedRconPorts) {}
internal Agent AsOnline(DateTimeOffset lastPing) => this with { internal Agent AsOnline(DateTimeOffset lastPing) => this with {

View File

@ -1,8 +0,0 @@
using Phantom.Common.Data;
namespace Phantom.Controller.Services.Agents;
public sealed record AgentStats(
int RunningInstanceCount,
RamAllocationUnits RunningInstanceMemory
);

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); return new WebMessageListener(connection, webAuthToken, UserManager, UserLoginManager, AgentManager, InstanceManager, TaskManager);
} }
public async Task Initialize() { public async Task Initialize() {

View File

@ -1,11 +0,0 @@
using Phantom.Common.Data.Instance;
namespace Phantom.Controller.Services.Instances;
public sealed record Instance(
InstanceConfiguration Configuration,
IInstanceStatus Status,
bool LaunchAutomatically
) {
internal Instance(InstanceConfiguration configuration, bool launchAutomatically = false) : this(configuration, InstanceStatus.Offline, launchAutomatically) {}
}

View File

@ -4,12 +4,14 @@ using Phantom.Common.Data;
using Phantom.Common.Data.Instance; using Phantom.Common.Data.Instance;
using Phantom.Common.Data.Minecraft; using Phantom.Common.Data.Minecraft;
using Phantom.Common.Data.Replies; using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Web.Instance;
using Phantom.Common.Data.Web.Minecraft; using Phantom.Common.Data.Web.Minecraft;
using Phantom.Common.Logging; using Phantom.Common.Logging;
using Phantom.Common.Messages.Agent; using Phantom.Common.Messages.Agent;
using Phantom.Common.Messages.Agent.ToAgent; using Phantom.Common.Messages.Agent.ToAgent;
using Phantom.Controller.Database; using Phantom.Controller.Database;
using Phantom.Controller.Database.Entities; using Phantom.Controller.Database.Entities;
using Phantom.Controller.Database.Repositories;
using Phantom.Controller.Minecraft; using Phantom.Controller.Minecraft;
using Phantom.Controller.Services.Agents; using Phantom.Controller.Services.Agents;
using Phantom.Utils.Collections; using Phantom.Utils.Collections;
@ -55,53 +57,53 @@ sealed class InstanceManager {
JvmArgumentsHelper.Split(entity.JvmArguments) JvmArgumentsHelper.Split(entity.JvmArguments)
); );
var instance = new Instance(configuration, entity.LaunchAutomatically); var instance = Instance.Offline(configuration, entity.LaunchAutomatically);
instances.ByGuid[instance.Configuration.InstanceGuid] = instance; instances.ByGuid[instance.Configuration.InstanceGuid] = instance;
} }
} }
[SuppressMessage("ReSharper", "ConvertIfStatementToConditionalTernaryExpression")] [SuppressMessage("ReSharper", "ConvertIfStatementToConditionalTernaryExpression")]
public async Task<InstanceActionResult<AddOrEditInstanceResult>> AddOrEditInstance(InstanceConfiguration configuration) { public async Task<InstanceActionResult<CreateOrUpdateInstanceResult>> CreateOrUpdateInstance(InstanceConfiguration configuration, Guid auditLogUserGuid) {
var agent = agentManager.GetAgent(configuration.AgentGuid); var agent = agentManager.GetAgent(configuration.AgentGuid);
if (agent == null) { if (agent == null) {
return InstanceActionResult.Concrete(AddOrEditInstanceResult.AgentNotFound); return InstanceActionResult.Concrete(CreateOrUpdateInstanceResult.AgentNotFound);
} }
if (string.IsNullOrWhiteSpace(configuration.InstanceName)) { if (string.IsNullOrWhiteSpace(configuration.InstanceName)) {
return InstanceActionResult.Concrete(AddOrEditInstanceResult.InstanceNameMustNotBeEmpty); return InstanceActionResult.Concrete(CreateOrUpdateInstanceResult.InstanceNameMustNotBeEmpty);
} }
if (configuration.MemoryAllocation <= RamAllocationUnits.Zero) { if (configuration.MemoryAllocation <= RamAllocationUnits.Zero) {
return InstanceActionResult.Concrete(AddOrEditInstanceResult.InstanceMemoryMustNotBeZero); return InstanceActionResult.Concrete(CreateOrUpdateInstanceResult.InstanceMemoryMustNotBeZero);
} }
var serverExecutableInfo = await minecraftVersions.GetServerExecutableInfo(configuration.MinecraftVersion, cancellationToken); var serverExecutableInfo = await minecraftVersions.GetServerExecutableInfo(configuration.MinecraftVersion, cancellationToken);
if (serverExecutableInfo == null) { if (serverExecutableInfo == null) {
return InstanceActionResult.Concrete(AddOrEditInstanceResult.MinecraftVersionDownloadInfoNotFound); return InstanceActionResult.Concrete(CreateOrUpdateInstanceResult.MinecraftVersionDownloadInfoNotFound);
} }
InstanceActionResult<AddOrEditInstanceResult> result; InstanceActionResult<CreateOrUpdateInstanceResult> result;
bool isNewInstance; bool isNewInstance;
await modifyInstancesSemaphore.WaitAsync(cancellationToken); await modifyInstancesSemaphore.WaitAsync(cancellationToken);
try { try {
isNewInstance = !instances.ByGuid.TryReplace(configuration.InstanceGuid, instance => instance with { Configuration = configuration }); isNewInstance = !instances.ByGuid.TryReplace(configuration.InstanceGuid, instance => instance with { Configuration = configuration });
if (isNewInstance) { if (isNewInstance) {
instances.ByGuid.TryAdd(configuration.InstanceGuid, new Instance(configuration)); instances.ByGuid.TryAdd(configuration.InstanceGuid, Instance.Offline(configuration));
} }
var message = new ConfigureInstanceMessage(configuration, new InstanceLaunchProperties(serverExecutableInfo)); var message = new ConfigureInstanceMessage(configuration, new InstanceLaunchProperties(serverExecutableInfo));
var reply = await agentManager.SendMessage<ConfigureInstanceMessage, InstanceActionResult<ConfigureInstanceResult>>(configuration.AgentGuid, message, TimeSpan.FromSeconds(10)); var reply = await agentManager.SendMessage<ConfigureInstanceMessage, InstanceActionResult<ConfigureInstanceResult>>(configuration.AgentGuid, message, TimeSpan.FromSeconds(10));
result = reply.DidNotReplyIfNull().Map(static result => result switch { result = reply.DidNotReplyIfNull().Map(static result => result switch {
ConfigureInstanceResult.Success => AddOrEditInstanceResult.Success, ConfigureInstanceResult.Success => CreateOrUpdateInstanceResult.Success,
_ => AddOrEditInstanceResult.UnknownError _ => CreateOrUpdateInstanceResult.UnknownError
}); });
if (result.Is(AddOrEditInstanceResult.Success)) { if (result.Is(CreateOrUpdateInstanceResult.Success)) {
await using var ctx = dbProvider.Eager(); await using var db = dbProvider.Lazy();
InstanceEntity entity = ctx.InstanceUpsert.Fetch(configuration.InstanceGuid);
InstanceEntity entity = db.Ctx.InstanceUpsert.Fetch(configuration.InstanceGuid);
entity.AgentGuid = configuration.AgentGuid; entity.AgentGuid = configuration.AgentGuid;
entity.InstanceName = configuration.InstanceName; entity.InstanceName = configuration.InstanceName;
entity.ServerPort = configuration.ServerPort; entity.ServerPort = configuration.ServerPort;
@ -112,7 +114,15 @@ sealed class InstanceManager {
entity.JavaRuntimeGuid = configuration.JavaRuntimeGuid; entity.JavaRuntimeGuid = configuration.JavaRuntimeGuid;
entity.JvmArguments = JvmArgumentsHelper.Join(configuration.JvmArguments); entity.JvmArguments = JvmArgumentsHelper.Join(configuration.JvmArguments);
await ctx.SaveChangesAsync(cancellationToken); var auditLogRepository = new AuditLogRepository(db, auditLogUserGuid);
if (isNewInstance) {
auditLogRepository.AddInstanceCreatedEvent(configuration.InstanceGuid);
}
else {
auditLogRepository.AddInstanceEditedEvent(configuration.InstanceGuid);
}
await db.Ctx.SaveChangesAsync(cancellationToken);
} }
else if (isNewInstance) { else if (isNewInstance) {
instances.ByGuid.Remove(configuration.InstanceGuid); instances.ByGuid.Remove(configuration.InstanceGuid);
@ -121,7 +131,7 @@ sealed class InstanceManager {
modifyInstancesSemaphore.Release(); modifyInstancesSemaphore.Release();
} }
if (result.Is(AddOrEditInstanceResult.Success)) { if (result.Is(CreateOrUpdateInstanceResult.Success)) {
if (isNewInstance) { if (isNewInstance) {
Logger.Information("Added instance \"{InstanceName}\" (GUID {InstanceGuid}) to agent \"{AgentName}\".", configuration.InstanceName, configuration.InstanceGuid, agent.Name); Logger.Information("Added instance \"{InstanceName}\" (GUID {InstanceGuid}) to agent \"{AgentName}\".", configuration.InstanceName, configuration.InstanceGuid, agent.Name);
} }
@ -131,10 +141,10 @@ sealed class InstanceManager {
} }
else { else {
if (isNewInstance) { if (isNewInstance) {
Logger.Information("Failed adding instance \"{InstanceName}\" (GUID {InstanceGuid}) to agent \"{AgentName}\". {ErrorMessage}", configuration.InstanceName, configuration.InstanceGuid, agent.Name, result.ToSentence(AddOrEditInstanceResultExtensions.ToSentence)); Logger.Information("Failed adding instance \"{InstanceName}\" (GUID {InstanceGuid}) to agent \"{AgentName}\". {ErrorMessage}", configuration.InstanceName, configuration.InstanceGuid, agent.Name, result.ToSentence(CreateOrUpdateInstanceResultExtensions.ToSentence));
} }
else { else {
Logger.Information("Failed editing instance \"{InstanceName}\" (GUID {InstanceGuid}) in agent \"{AgentName}\". {ErrorMessage}", configuration.InstanceName, configuration.InstanceGuid, agent.Name, result.ToSentence(AddOrEditInstanceResultExtensions.ToSentence)); Logger.Information("Failed editing instance \"{InstanceName}\" (GUID {InstanceGuid}) in agent \"{AgentName}\". {ErrorMessage}", configuration.InstanceName, configuration.InstanceGuid, agent.Name, result.ToSentence(CreateOrUpdateInstanceResultExtensions.ToSentence));
} }
} }

View File

@ -1,4 +1,8 @@
using Phantom.Common.Data; using System.Collections.Immutable;
using Phantom.Common.Data;
using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Web.Agent;
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;
using Phantom.Common.Messages.Web; using Phantom.Common.Messages.Web;
@ -6,8 +10,11 @@ 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.Rpc; using Phantom.Controller.Rpc;
using Phantom.Controller.Services.Agents;
using Phantom.Controller.Services.Instances;
using Phantom.Controller.Services.Users; using Phantom.Controller.Services.Users;
using Phantom.Utils.Rpc.Message; using Phantom.Utils.Rpc.Message;
using Phantom.Utils.Tasks;
using Serilog; using Serilog;
namespace Phantom.Controller.Services.Rpc; namespace Phantom.Controller.Services.Rpc;
@ -19,12 +26,18 @@ public sealed class WebMessageListener : IMessageToControllerListener {
private readonly AuthToken authToken; private readonly AuthToken authToken;
private readonly UserManager userManager; private readonly UserManager userManager;
private readonly UserLoginManager userLoginManager; private readonly UserLoginManager userLoginManager;
private readonly AgentManager agentManager;
private readonly InstanceManager instanceManager;
private readonly TaskManager taskManager;
internal WebMessageListener(RpcConnectionToClient<IMessageToWebListener> connection, AuthToken authToken, UserManager userManager, UserLoginManager userLoginManager) { internal WebMessageListener(RpcConnectionToClient<IMessageToWebListener> connection, AuthToken authToken, UserManager userManager, UserLoginManager userLoginManager, AgentManager agentManager, InstanceManager instanceManager, 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.instanceManager = instanceManager;
this.taskManager = taskManager;
} }
public async Task<NoReply> HandleRegisterWeb(RegisterWebMessage message) { public async Task<NoReply> HandleRegisterWeb(RegisterWebMessage message) {
@ -38,14 +51,31 @@ public sealed class WebMessageListener : IMessageToControllerListener {
await connection.Send(new RegisterWebResultMessage(false)); await connection.Send(new RegisterWebResultMessage(false));
} }
agentManager.AgentsChanged.Subscribe(this, HandleAgentsChanged);
instanceManager.InstancesChanged.Subscribe(this, HandleInstancesChanged);
return NoReply.Instance; return NoReply.Instance;
} }
public Task<CreateOrUpdateAdministratorUserResult> CreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUser message) { private void HandleAgentsChanged(ImmutableArray<Agent> agents) {
var message = new RefreshAgentsMessage(agents.Select(static agent => new AgentWithStats(agent.Guid, agent.Name, agent.ProtocolVersion, agent.BuildVersion, agent.MaxInstances, agent.MaxMemory, agent.AllowedServerPorts, agent.AllowedRconPorts, agent.Stats, agent.LastPing, agent.IsOnline)).ToImmutableArray());
taskManager.Run("Send agents to web", () => connection.Send(message));
}
private void HandleInstancesChanged(ImmutableDictionary<Guid, Instance> instances) {
var message = new RefreshInstancesMessage(instances.Values.ToImmutableArray());
taskManager.Run("Send instances to web", () => connection.Send(message));
}
public Task<CreateOrUpdateAdministratorUserResult> HandleCreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUserMessage message) {
return userManager.CreateOrUpdateAdministrator(message.Username, message.Password); return userManager.CreateOrUpdateAdministrator(message.Username, message.Password);
} }
public Task<LogInSuccess?> HandleLogIn(LogIn message) { public Task<InstanceActionResult<CreateOrUpdateInstanceResult>> HandleCreateOrUpdateInstance(CreateOrUpdateInstanceMessage message) {
return instanceManager.CreateOrUpdateInstance(message.Configuration, message.LoggedInUserGuid);
}
public Task<LogInSuccess?> HandleLogIn(LogInMessage message) {
return userLoginManager.LogIn(message.Username, message.Password); return userLoginManager.LogIn(message.Username, message.Password);
} }

View File

@ -0,0 +1,20 @@
using Serilog;
namespace Phantom.Utils.Events;
public sealed class SimpleObservableState<T> : ObservableState<T> {
public T Value { get; private set; }
public SimpleObservableState(ILogger logger, T initialValue) : base(logger) {
this.Value = initialValue;
}
public void SetTo(T newValue) {
this.Value = newValue;
Update();
}
protected override T GetData() {
return Value;
}
}

View File

@ -0,0 +1,20 @@
using System.Collections.Immutable;
using Phantom.Common.Data.Web.Agent;
using Phantom.Common.Logging;
using Phantom.Utils.Events;
namespace Phantom.Web.Services.Agents;
public sealed class AgentManager {
private readonly SimpleObservableState<ImmutableArray<AgentWithStats>> agents = new (PhantomLogger.Create<AgentManager>("Agents"), ImmutableArray<AgentWithStats>.Empty);
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) {
agents.SetTo(newAgents);
}
}

View File

@ -0,0 +1,40 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
namespace Phantom.Web.Services.Authentication;
public sealed class CustomAuthenticationStateProvider : ServerAuthenticationStateProvider {
private readonly UserSessionManager sessionManager;
private readonly UserSessionBrowserStorage sessionBrowserStorage;
private bool isLoaded;
public CustomAuthenticationStateProvider(UserSessionManager sessionManager, UserSessionBrowserStorage sessionBrowserStorage) {
this.sessionManager = sessionManager;
this.sessionBrowserStorage = sessionBrowserStorage;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync() {
if (!isLoaded) {
var stored = await sessionBrowserStorage.Get();
if (stored != null) {
var session = sessionManager.FindWithToken(stored.UserGuid, stored.Token);
if (session != null) {
SetLoadedSession(session);
}
}
}
return await base.GetAuthenticationStateAsync();
}
internal void SetLoadedSession(UserInfo user) {
isLoaded = true;
SetAuthenticationState(Task.FromResult(new AuthenticationState(user.AsClaimsPrincipal)));
}
internal void SetUnloadedSession() {
isLoaded = false;
SetAuthenticationState(Task.FromResult(new AuthenticationState(new ClaimsPrincipal())));
}
}

View File

@ -1,90 +0,0 @@
using System.Collections.Immutable;
using System.Security.Claims;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
using Phantom.Common.Logging;
using ILogger = Serilog.ILogger;
namespace Phantom.Web.Services.Authentication;
public sealed class PhantomAuthenticationStateProvider : ServerAuthenticationStateProvider {
private const string SessionTokenKey = "PhantomSession";
private static readonly ILogger Logger = PhantomLogger.Create<PhantomAuthenticationStateProvider>();
private readonly PhantomLoginSessions loginSessions;
private readonly ProtectedLocalStorage localStorage;
private bool isLoaded;
public PhantomAuthenticationStateProvider(PhantomLoginSessions loginSessions, ProtectedLocalStorage localStorage) {
this.loginSessions = loginSessions;
this.localStorage = localStorage;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync() {
if (isLoaded) {
return await base.GetAuthenticationStateAsync();
}
LocalStorageEntry? stored;
try {
stored = await GetLocalStorageEntry();
} catch (InvalidOperationException) {
stored = null;
}
if (stored != null) {
var session = loginSessions.Find(stored.UserGuid, stored.Token);
if (session != null) {
SetLoadedSession(session);
}
}
return await base.GetAuthenticationStateAsync();
}
private sealed record LocalStorageEntry(Guid UserGuid, ImmutableArray<byte> Token);
private async Task<LocalStorageEntry?> GetLocalStorageEntry() {
try {
var result = await localStorage.GetAsync<LocalStorageEntry>(SessionTokenKey);
return result.Success ? result.Value : null;
} catch (InvalidOperationException) {
return null;
} catch (CryptographicException) {
return null;
} catch (Exception e) {
Logger.Error(e, "Could not read local storage entry.");
return null;
}
}
private void SetLoadedSession(UserSession session) {
isLoaded = true;
SetAuthenticationState(Task.FromResult(new AuthenticationState(session.AsClaimsPrincipal)));
}
internal async Task HandleLogin(UserSession session) {
await localStorage.SetAsync(SessionTokenKey, new LocalStorageEntry(session.UserGuid, session.Token));
loginSessions.Add(session);
SetLoadedSession(session);
}
internal async Task HandleLogout() {
if (!isLoaded) {
return;
}
await localStorage.DeleteAsync(SessionTokenKey);
var stored = await GetLocalStorageEntry();
if (stored != null) {
loginSessions.Remove(stored.UserGuid, stored.Token);
}
isLoaded = false;
SetAuthenticationState(Task.FromResult(new AuthenticationState(new ClaimsPrincipal())));
}
}

View File

@ -1,53 +0,0 @@
using System.Security.Claims;
using Phantom.Common.Data.Web.Users;
using Phantom.Common.Logging;
using Phantom.Common.Messages.Web.ToController;
using Phantom.Web.Services.Rpc;
using ILogger = Serilog.ILogger;
namespace Phantom.Web.Services.Authentication;
public sealed class PhantomLoginManager {
private static readonly ILogger Logger = PhantomLogger.Create<PhantomLoginManager>();
public const string AuthenticationType = "Phantom";
private readonly INavigation navigation;
private readonly PhantomAuthenticationStateProvider authenticationStateProvider;
private readonly ControllerConnection controllerConnection;
public PhantomLoginManager(INavigation navigation, PhantomAuthenticationStateProvider authenticationStateProvider, ControllerConnection controllerConnection) {
this.navigation = navigation;
this.authenticationStateProvider = authenticationStateProvider;
this.controllerConnection = controllerConnection;
}
public async Task<bool> SignIn(string username, string password, string? returnUrl = null) {
LogInSuccess? success;
try {
success = await controllerConnection.Send<LogIn, LogInSuccess?>(new LogIn(username, password), TimeSpan.FromSeconds(30));
} catch (Exception e) {
Logger.Error(e, "Could not log in {Username}.", username);
return false;
}
if (success == null) {
return false;
}
Logger.Information("Successfully logged in {Username}.", username);
var identity = new ClaimsIdentity(AuthenticationType);
identity.AddClaim(new Claim(ClaimTypes.Name, username));
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, success.UserGuid.ToString()));
await authenticationStateProvider.HandleLogin(new UserSession(success.UserGuid, username, success.Token));
await navigation.NavigateTo(returnUrl ?? string.Empty);
return true;
}
public async Task SignOut() {
await navigation.NavigateTo(string.Empty);
await authenticationStateProvider.HandleLogout();
}
}

View File

@ -1,49 +0,0 @@
using System.Collections.Concurrent;
using System.Collections.Immutable;
namespace Phantom.Web.Services.Authentication;
public sealed class PhantomLoginSessions {
private readonly ConcurrentDictionary<Guid, List<UserSession>> userSessions = new ();
internal void Add(UserSession session) {
var sessions = userSessions.GetOrAdd(session.UserGuid, static _ => new List<UserSession>());
lock (sessions) {
RemoveSessionInternal(sessions, session.Token);
sessions.Add(session);
}
}
internal UserSession? Find(Guid userGuid, ImmutableArray<byte> token) {
if (!userSessions.TryGetValue(userGuid, out var sessions)) {
return null;
}
lock (sessions) {
int index = FindSessionInternal(sessions, token);
return index == -1 ? null : sessions[index];
}
}
internal void Remove(Guid userGuid, ImmutableArray<byte> token) {
if (!userSessions.TryGetValue(userGuid, out var sessions)) {
return;
}
lock (sessions) {
RemoveSessionInternal(sessions, token);
}
}
private static int FindSessionInternal(List<UserSession> sessions, ImmutableArray<byte> token) {
return sessions.FindIndex(s => s.TokenEquals(token));
}
private static void RemoveSessionInternal(List<UserSession> sessions, ImmutableArray<byte> token) {
int index = FindSessionInternal(sessions, token);
if (index != -1) {
sessions.RemoveAt(index);
}
}
}

View File

@ -0,0 +1,23 @@
using System.Security.Claims;
using Phantom.Common.Data.Web.Users;
namespace Phantom.Web.Services.Authentication;
sealed record UserInfo(Guid UserGuid, string Username, PermissionSet Permissions) {
private const string AuthenticationType = "Phantom";
public ClaimsPrincipal AsClaimsPrincipal {
get {
var identity = new ClaimsIdentity(AuthenticationType);
identity.AddClaim(new Claim(ClaimTypes.Name, Username));
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, UserGuid.ToString()));
return new ClaimsPrincipal(identity);
}
}
public static Guid? TryGetGuid(ClaimsPrincipal principal) {
return principal.FindFirstValue(ClaimTypes.NameIdentifier) is {} guidStr && Guid.TryParse(guidStr, out var guid) ? guid : null;
}
}

View File

@ -0,0 +1,63 @@
using Phantom.Common.Data.Web.Users;
using Phantom.Common.Logging;
using Phantom.Common.Messages.Web.ToController;
using Phantom.Web.Services.Rpc;
using ILogger = Serilog.ILogger;
namespace Phantom.Web.Services.Authentication;
public sealed class UserLoginManager {
private static readonly ILogger Logger = PhantomLogger.Create<UserLoginManager>();
private readonly INavigation navigation;
private readonly UserSessionManager sessionManager;
private readonly UserSessionBrowserStorage sessionBrowserStorage;
private readonly CustomAuthenticationStateProvider authenticationStateProvider;
private readonly ControllerConnection controllerConnection;
public UserLoginManager(INavigation navigation, UserSessionManager sessionManager, UserSessionBrowserStorage sessionBrowserStorage, CustomAuthenticationStateProvider authenticationStateProvider, ControllerConnection controllerConnection) {
this.navigation = navigation;
this.sessionManager = sessionManager;
this.sessionBrowserStorage = sessionBrowserStorage;
this.authenticationStateProvider = authenticationStateProvider;
this.controllerConnection = controllerConnection;
}
public async Task<bool> LogIn(string username, string password, string? returnUrl = null) {
LogInSuccess? success;
try {
success = await controllerConnection.Send<LogInMessage, LogInSuccess?>(new LogInMessage(username, password), TimeSpan.FromSeconds(30));
} catch (Exception e) {
Logger.Error(e, "Could not log in {Username}.", username);
return false;
}
if (success == null) {
return false;
}
Logger.Information("Successfully logged in {Username}.", username);
var userGuid = success.UserGuid;
var userInfo = new UserInfo(userGuid, username, success.Permissions);
var token = success.Token;
await sessionBrowserStorage.Store(userGuid, token);
sessionManager.Add(userInfo, token);
authenticationStateProvider.SetLoadedSession(userInfo);
await navigation.NavigateTo(returnUrl ?? string.Empty);
return true;
}
public async Task LogOut() {
var stored = await sessionBrowserStorage.Delete();
if (stored != null) {
sessionManager.Remove(stored.UserGuid, stored.Token);
}
await navigation.NavigateTo(string.Empty);
authenticationStateProvider.SetUnloadedSession();
}
}

View File

@ -1,32 +0,0 @@
using System.Collections.Immutable;
using System.Security.Claims;
using System.Security.Cryptography;
namespace Phantom.Web.Services.Authentication;
sealed class UserSession {
public Guid UserGuid { get; }
public string Username { get; }
public ImmutableArray<byte> Token { get; }
public ClaimsPrincipal AsClaimsPrincipal {
get {
var identity = new ClaimsIdentity(PhantomLoginManager.AuthenticationType);
identity.AddClaim(new Claim(ClaimTypes.Name, Username));
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, UserGuid.ToString()));
return new ClaimsPrincipal(identity);
}
}
public UserSession(Guid userGuid, string username, ImmutableArray<byte> token) {
UserGuid = userGuid;
Username = username;
Token = token;
}
public bool TokenEquals(ImmutableArray<byte> other) {
return CryptographicOperations.FixedTimeEquals(Token.AsSpan(), other.AsSpan());
}
}

View File

@ -0,0 +1,48 @@
using System.Collections.Immutable;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
using Phantom.Common.Logging;
using ILogger = Serilog.ILogger;
namespace Phantom.Web.Services.Authentication;
public sealed class UserSessionBrowserStorage {
private static readonly ILogger Logger = PhantomLogger.Create<UserSessionBrowserStorage>();
private const string SessionTokenKey = "PhantomSession";
private readonly ProtectedLocalStorage localStorage;
public UserSessionBrowserStorage(ProtectedLocalStorage localStorage) {
this.localStorage = localStorage;
}
internal sealed record LocalStorageEntry(Guid UserGuid, ImmutableArray<byte> Token);
internal async Task<LocalStorageEntry?> Get() {
try {
var result = await localStorage.GetAsync<LocalStorageEntry>(SessionTokenKey);
return result.Success ? result.Value : null;
} catch (InvalidOperationException) {
return null;
} catch (CryptographicException) {
return null;
} catch (Exception e) {
Logger.Error(e, "Could not read local storage entry.");
return null;
}
}
internal async Task Store(Guid userGuid, ImmutableArray<byte> token) {
await localStorage.SetAsync(SessionTokenKey, new LocalStorageEntry(userGuid, token));
}
internal async Task<LocalStorageEntry?> Delete() {
var oldEntry = await Get();
if (oldEntry != null) {
await localStorage.DeleteAsync(SessionTokenKey);
}
return oldEntry;
}
}

View File

@ -0,0 +1,31 @@
using System.Collections.Concurrent;
using System.Collections.Immutable;
namespace Phantom.Web.Services.Authentication;
public sealed class UserSessionManager {
private readonly ConcurrentDictionary<Guid, UserSessions> userSessions = new ();
internal void Add(UserInfo user, ImmutableArray<byte> token) {
userSessions.AddOrUpdate(
user.UserGuid,
static (_, u) => new UserSessions(u),
static (_, sessions, u) => sessions.WithUserInfo(u),
user
).AddToken(token);
}
internal UserInfo? Find(Guid userGuid) {
return userSessions.TryGetValue(userGuid, out var sessions) ? sessions.UserInfo : null;
}
internal UserInfo? FindWithToken(Guid userGuid, ImmutableArray<byte> token) {
return userSessions.TryGetValue(userGuid, out var sessions) && sessions.HasToken(token) ? sessions.UserInfo : null;
}
internal void Remove(Guid userGuid, ImmutableArray<byte> token) {
if (userSessions.TryGetValue(userGuid, out var sessions)) {
sessions.RemoveToken(token);
}
}
}

View File

@ -0,0 +1,54 @@
using System.Collections.Immutable;
using System.Security.Cryptography;
namespace Phantom.Web.Services.Authentication;
sealed class UserSessions {
public UserInfo UserInfo { get; }
private readonly List<ImmutableArray<byte>> tokens = new ();
public UserSessions(UserInfo userInfo) {
UserInfo = userInfo;
}
private UserSessions(UserInfo userInfo, List<ImmutableArray<byte>> tokens) : this(userInfo) {
this.tokens.AddRange(tokens);
}
public UserSessions WithUserInfo(UserInfo user) {
List<ImmutableArray<byte>> tokensCopy;
lock (tokens) {
tokensCopy = new List<ImmutableArray<byte>>(tokens);
}
return new UserSessions(user, tokensCopy);
}
public void AddToken(ImmutableArray<byte> token) {
lock (tokens) {
if (!HasToken(token)) {
tokens.Add(token);
}
}
}
public bool HasToken(ImmutableArray<byte> token) {
return FindTokenIndex(token) != -1;
}
private int FindTokenIndex(ImmutableArray<byte> token) {
lock (tokens) {
return tokens.FindIndex(t => CryptographicOperations.FixedTimeEquals(t.AsSpan(), token.AsSpan()));
}
}
public void RemoveToken(ImmutableArray<byte> token) {
lock (tokens) {
int index = FindTokenIndex(token);
if (index != -1) {
tokens.RemoveAt(index);
}
}
}
}

View File

@ -1,15 +1,22 @@
using System.Security.Claims; using System.Security.Claims;
using Phantom.Common.Data.Web.Users; using Phantom.Common.Data.Web.Users;
using Phantom.Web.Services.Authentication;
using UserInfo = Phantom.Web.Services.Authentication.UserInfo;
namespace Phantom.Web.Services.Authorization; namespace Phantom.Web.Services.Authorization;
// TODO public sealed class PermissionManager {
public class PermissionManager { private readonly UserSessionManager sessionManager;
public IdentityPermissions GetPermissions(ClaimsPrincipal user, bool refreshCache = false) {
public PermissionManager(UserSessionManager sessionManager) {
this.sessionManager = sessionManager;
} }
public bool CheckPermission(ClaimsPrincipal user, Permission permission, bool refreshCache = false) { public PermissionSet GetPermissions(ClaimsPrincipal user) {
return false; return UserInfo.TryGetGuid(user) is {} guid && sessionManager.Find(guid) is {} info ? info.Permissions : PermissionSet.None;
}
public bool CheckPermission(ClaimsPrincipal user, Permission permission) {
return GetPermissions(user).Check(permission);
} }
} }

View File

@ -1,10 +1,29 @@
using Phantom.Common.Data.Replies; using System.Collections.Immutable;
using Phantom.Common.Data.Instance;
using Phantom.Common.Data.Replies;
using Phantom.Common.Data.Web.Instance;
using Phantom.Common.Logging;
using Phantom.Common.Messages.Web.ToController;
using Phantom.Utils.Events;
using Phantom.Web.Services.Rpc;
namespace Phantom.Web.Services.Instances; namespace Phantom.Web.Services.Instances;
// TODO public sealed class InstanceManager {
public class InstanceManager { private readonly ControllerConnection controllerConnection;
public async Task<InstanceActionResult<SendCommandToInstanceResult>> SendCommand(Guid instanceGuid, string command) { private readonly SimpleObservableState<ImmutableArray<Instance>> instances = new (PhantomLogger.Create<InstanceManager>("Instances"), ImmutableArray<Instance>.Empty);
public InstanceManager(ControllerConnection controllerConnection) {
this.controllerConnection = controllerConnection;
}
public EventSubscribers<ImmutableArray<Instance>> InstancesChanged => instances.Subs;
internal void RefreshInstances(ImmutableArray<Instance> newInstances) {
instances.SetTo(newInstances);
}
public Task<InstanceActionResult<CreateOrUpdateInstanceResult>> CreateOrUpdateInstance(InstanceConfiguration configuration) {
return controllerConnection.Send<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(new CreateOrUpdateInstanceMessage(configuration), TimeSpan.FromSeconds(30));
} }
} }

View File

@ -14,6 +14,7 @@
<ProjectReference Include="..\..\Common\Phantom.Common.Data.Web\Phantom.Common.Data.Web.csproj" /> <ProjectReference Include="..\..\Common\Phantom.Common.Data.Web\Phantom.Common.Data.Web.csproj" />
<ProjectReference Include="..\..\Common\Phantom.Common.Logging\Phantom.Common.Logging.csproj" /> <ProjectReference Include="..\..\Common\Phantom.Common.Logging\Phantom.Common.Logging.csproj" />
<ProjectReference Include="..\..\Common\Phantom.Common.Messages.Web\Phantom.Common.Messages.Web.csproj" /> <ProjectReference Include="..\..\Common\Phantom.Common.Messages.Web\Phantom.Common.Messages.Web.csproj" />
<ProjectReference Include="..\..\Utils\Phantom.Utils.Events\Phantom.Utils.Events.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,23 +1,29 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Authorization;
using Phantom.Common.Data.Web.Users; using Phantom.Common.Data.Web.Users;
using Phantom.Web.Services.Agents;
using Phantom.Web.Services.Authentication; using Phantom.Web.Services.Authentication;
using Phantom.Web.Services.Authorization; using Phantom.Web.Services.Authorization;
using Phantom.Web.Services.Instances;
using Phantom.Web.Services.Rpc; using Phantom.Web.Services.Rpc;
namespace Phantom.Web.Services; namespace Phantom.Web.Services;
public static class PhantomWebServices { public static class PhantomWebServices {
public static void AddPhantomServices(this IServiceCollection services) { public static void AddPhantomServices(this IServiceCollection services) {
services.AddSingleton<MessageListener>();
services.AddSingleton<ControllerConnection>(); services.AddSingleton<ControllerConnection>();
services.AddSingleton<MessageListener>();
services.AddSingleton<AgentManager>();
services.AddSingleton<InstanceManager>();
services.AddSingleton<PermissionManager>(); services.AddSingleton<PermissionManager>();
services.AddSingleton<PhantomLoginSessions>(); services.AddSingleton<UserSessionManager>();
services.AddScoped<PhantomLoginManager>(); services.AddScoped<UserSessionBrowserStorage>();
services.AddScoped<PhantomAuthenticationStateProvider>(); services.AddScoped<UserLoginManager>();
services.AddScoped<AuthenticationStateProvider>(static services => services.GetRequiredService<PhantomAuthenticationStateProvider>());
services.AddScoped<IHostEnvironmentAuthenticationStateProvider>(static services => services.GetRequiredService<PhantomAuthenticationStateProvider>()); services.AddScoped<CustomAuthenticationStateProvider>();
services.AddScoped<AuthenticationStateProvider>(static services => services.GetRequiredService<CustomAuthenticationStateProvider>());
services.AddAuthorization(ConfigureAuthorization); services.AddAuthorization(ConfigureAuthorization);
services.AddScoped<IAuthorizationHandler, PermissionBasedPolicyHandler>(); services.AddScoped<IAuthorizationHandler, PermissionBasedPolicyHandler>();

View File

@ -4,6 +4,8 @@ using Phantom.Common.Messages.Web.ToWeb;
using Phantom.Utils.Rpc; using Phantom.Utils.Rpc;
using Phantom.Utils.Rpc.Message; using Phantom.Utils.Rpc.Message;
using Phantom.Utils.Tasks; using Phantom.Utils.Tasks;
using Phantom.Web.Services.Agents;
using Phantom.Web.Services.Instances;
namespace Phantom.Web.Services.Rpc; namespace Phantom.Web.Services.Rpc;
@ -11,9 +13,13 @@ public sealed class MessageListener : IMessageToWebListener {
public TaskCompletionSource<bool> RegisterSuccessWaiter { get; } = AsyncTasks.CreateCompletionSource<bool>(); public TaskCompletionSource<bool> RegisterSuccessWaiter { get; } = AsyncTasks.CreateCompletionSource<bool>();
private readonly RpcConnectionToServer<IMessageToControllerListener> connection; private readonly RpcConnectionToServer<IMessageToControllerListener> connection;
private readonly AgentManager agentManager;
private readonly InstanceManager instanceManager;
public MessageListener(RpcConnectionToServer<IMessageToControllerListener> connection) { public MessageListener(RpcConnectionToServer<IMessageToControllerListener> connection, AgentManager agentManager, InstanceManager instanceManager) {
this.connection = connection; this.connection = connection;
this.agentManager = agentManager;
this.instanceManager = instanceManager;
} }
public Task<NoReply> HandleRegisterWebResult(RegisterWebResultMessage message) { public Task<NoReply> HandleRegisterWebResult(RegisterWebResultMessage message) {
@ -21,6 +27,16 @@ public sealed class MessageListener : IMessageToWebListener {
return Task.FromResult(NoReply.Instance); return Task.FromResult(NoReply.Instance);
} }
public Task<NoReply> HandleRefreshAgents(RefreshAgentsMessage message) {
agentManager.RefreshAgents(message.Agents);
return Task.FromResult(NoReply.Instance);
}
public Task<NoReply> HandleRefreshInstances(RefreshInstancesMessage message) {
instanceManager.RefreshInstances(message.Instances);
return Task.FromResult(NoReply.Instance);
}
public Task<NoReply> HandleReply(ReplyMessage message) { public Task<NoReply> HandleReply(ReplyMessage message) {
connection.Receive(message); connection.Receive(message);
return Task.FromResult(NoReply.Instance); return Task.FromResult(NoReply.Instance);

View File

@ -18,7 +18,7 @@ public abstract class PhantomComponent : ComponentBase {
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, refreshCache: true); return PermissionManager.CheckPermission(authenticationState.User, permission);
} }
protected void InvokeAsyncChecked(Func<Task> task) { protected void InvokeAsyncChecked(Func<Task> task) {

View File

@ -1,5 +1,7 @@
@page "/agents" @page "/agents"
@using Phantom.Common.Data.Web.Agent
@using Phantom.Utils.Collections @using Phantom.Utils.Collections
@using Phantom.Web.Services.Agents
@implements IDisposable @implements IDisposable
@inject AgentManager AgentManager @inject AgentManager AgentManager
@ -73,7 +75,7 @@
@code { @code {
private readonly Table<Agent, Guid> agentTable = new(); private readonly Table<AgentWithStats, Guid> agentTable = new();
protected override void OnInitialized() { protected override void OnInitialized() {
AgentManager.AgentsChanged.Subscribe(this, agents => { AgentManager.AgentsChanged.Subscribe(this, agents => {

View File

@ -1,6 +1,10 @@
@page "/instances" @page "/instances"
@attribute [Authorize(Permission.ViewInstancesPolicy)] @attribute [Authorize(Permission.ViewInstancesPolicy)]
@using System.Collections.Immutable @using System.Collections.Immutable
@using Phantom.Common.Data.Web.Instance
@using Phantom.Common.Data.Web.Users
@using Phantom.Web.Services.Agents
@using Phantom.Web.Services.Instances
@implements IDisposable @implements IDisposable
@inject AgentManager AgentManager @inject AgentManager AgentManager
@inject InstanceManager InstanceManager @inject InstanceManager InstanceManager
@ -79,7 +83,9 @@
}); });
InstanceManager.InstancesChanged.Subscribe(this, instances => { InstanceManager.InstancesChanged.Subscribe(this, instances => {
this.instances = instances.Values.OrderBy(instance => agentNames.TryGetValue(instance.Configuration.AgentGuid, out var agentName) ? agentName : string.Empty).ThenBy(static instance => instance.Configuration.InstanceName).ToImmutableArray(); this.instances = instances.OrderBy(instance => agentNames.TryGetValue(instance.Configuration.AgentGuid, out var agentName) ? agentName : string.Empty)
.ThenBy(static instance => instance.Configuration.InstanceName)
.ToImmutableArray();
InvokeAsync(StateHasChanged); InvokeAsync(StateHasChanged);
}); });
} }

View File

@ -4,7 +4,7 @@
@using System.ComponentModel.DataAnnotations @using System.ComponentModel.DataAnnotations
@attribute [AllowAnonymous] @attribute [AllowAnonymous]
@inject INavigation Navigation @inject INavigation Navigation
@inject PhantomLoginManager LoginManager @inject UserLoginManager LoginManager
<h1>Login</h1> <h1>Login</h1>
@ -51,7 +51,7 @@
string? returnUrl = Navigation.GetQueryParameter("return", out var url) ? url : null; string? returnUrl = Navigation.GetQueryParameter("return", out var url) ? url : null;
if (!await LoginManager.SignIn(form.Username, form.Password, returnUrl)) { if (!await LoginManager.LogIn(form.Username, form.Password, returnUrl)) {
form.SubmitModel.StopSubmitting("Invalid username or password."); form.SubmitModel.StopSubmitting("Invalid username or password.");
} }
} }

View File

@ -1,11 +1,11 @@
@page "/logout" @page "/logout"
@using Phantom.Web.Services.Authentication @using Phantom.Web.Services.Authentication
@inject PhantomLoginManager LoginManager @inject UserLoginManager LoginManager
@code { @code {
protected override Task OnInitializedAsync() { protected override Task OnInitializedAsync() {
return LoginManager.SignOut(); return LoginManager.LogOut();
} }
} }

View File

@ -10,7 +10,7 @@
@using Phantom.Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults @using Phantom.Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults
@attribute [AllowAnonymous] @attribute [AllowAnonymous]
@inject ServiceConfiguration ServiceConfiguration @inject ServiceConfiguration ServiceConfiguration
@inject PhantomLoginManager LoginManager @inject UserLoginManager LoginManager
@inject ControllerConnection ControllerConnection @inject ControllerConnection ControllerConnection
<h1>Administrator Setup</h1> <h1>Administrator Setup</h1>
@ -69,7 +69,7 @@
return; return;
} }
var signInResult = await LoginManager.SignIn(form.Username, form.Password); var signInResult = await LoginManager.LogIn(form.Username, form.Password);
if (!signInResult) { if (!signInResult) {
form.SubmitModel.StopSubmitting("Error logging in."); form.SubmitModel.StopSubmitting("Error logging in.");
} }