mirror of
https://github.com/chylex/Minecraft-Phantom-Panel.git
synced 2024-11-25 16:42:54 +01:00
Compare commits
2 Commits
149bb6e0f1
...
d9c187994b
Author | SHA1 | Date | |
---|---|---|---|
d9c187994b | |||
3a18d2067f |
21
Common/Phantom.Common.Data.Web/Agent/AgentWithStats.cs
Normal file
21
Common/Phantom.Common.Data.Web/Agent/AgentWithStats.cs
Normal 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;
|
||||||
|
}
|
15
Common/Phantom.Common.Data.Web/Instance/Instance.cs
Normal file
15
Common/Phantom.Common.Data.Web/Instance/Instance.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
||||||
|
9
Common/Phantom.Common.Data/Agent/AgentStats.cs
Normal file
9
Common/Phantom.Common.Data/Agent/AgentStats.cs
Normal 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
|
||||||
|
);
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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?> {
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
using Phantom.Common.Data;
|
|
||||||
|
|
||||||
namespace Phantom.Controller.Services.Agents;
|
|
||||||
|
|
||||||
public sealed record AgentStats(
|
|
||||||
int RunningInstanceCount,
|
|
||||||
RamAllocationUnits RunningInstanceMemory
|
|
||||||
);
|
|
@ -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() {
|
||||||
|
@ -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) {}
|
|
||||||
}
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
20
Utils/Phantom.Utils.Events/SimpleObservableState.cs
Normal file
20
Utils/Phantom.Utils.Events/SimpleObservableState.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
20
Web/Phantom.Web.Services/Agents/AgentManager.cs
Normal file
20
Web/Phantom.Web.Services/Agents/AgentManager.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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())));
|
||||||
|
}
|
||||||
|
}
|
@ -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())));
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
23
Web/Phantom.Web.Services/Authentication/UserInfo.cs
Normal file
23
Web/Phantom.Web.Services/Authentication/UserInfo.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
63
Web/Phantom.Web.Services/Authentication/UserLoginManager.cs
Normal file
63
Web/Phantom.Web.Services/Authentication/UserLoginManager.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
Web/Phantom.Web.Services/Authentication/UserSessions.cs
Normal file
54
Web/Phantom.Web.Services/Authentication/UserSessions.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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>();
|
||||||
|
@ -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);
|
||||||
|
@ -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) {
|
||||||
|
@ -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 => {
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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.");
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user