mirror of
https://github.com/chylex/Minecraft-Phantom-Panel.git
synced 2024-11-25 16:42:54 +01:00
Compare commits
No commits in common. "d9c187994bd2bbad1c5d36ec1067d6c77bb3f426" and "149bb6e0f17558575e766dc50a8e9eb80bd0cf87" have entirely different histories.
d9c187994b
...
149bb6e0f1
@ -1,21 +0,0 @@
|
||||
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;
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
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,8 +10,4 @@
|
||||
<PackageReference Include="MemoryPack" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Phantom.Common.Data\Phantom.Common.Data.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -1,9 +0,0 @@
|
||||
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,6 +1,4 @@
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Common.Data.Web.Instance;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
using Phantom.Common.Messages.Web.BiDirectional;
|
||||
using Phantom.Common.Messages.Web.ToController;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
@ -9,8 +7,7 @@ namespace Phantom.Common.Messages.Web;
|
||||
|
||||
public interface IMessageToControllerListener {
|
||||
Task<NoReply> HandleRegisterWeb(RegisterWebMessage message);
|
||||
Task<LogInSuccess?> HandleLogIn(LogInMessage message);
|
||||
Task<CreateOrUpdateAdministratorUserResult> HandleCreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUserMessage message);
|
||||
Task<InstanceActionResult<CreateOrUpdateInstanceResult>> HandleCreateOrUpdateInstance(CreateOrUpdateInstanceMessage message);
|
||||
Task<CreateOrUpdateAdministratorUserResult> CreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUser message);
|
||||
Task<LogInSuccess?> HandleLogIn(LogIn message);
|
||||
Task<NoReply> HandleReply(ReplyMessage message);
|
||||
}
|
||||
|
@ -6,7 +6,5 @@ namespace Phantom.Common.Messages.Web;
|
||||
|
||||
public interface IMessageToWebListener {
|
||||
Task<NoReply> HandleRegisterWebResult(RegisterWebResultMessage message);
|
||||
Task<NoReply> HandleRefreshAgents(RefreshAgentsMessage message);
|
||||
Task<NoReply> HandleRefreshInstances(RefreshInstancesMessage message);
|
||||
Task<NoReply> HandleReply(ReplyMessage message);
|
||||
}
|
||||
|
@ -4,11 +4,11 @@ using Phantom.Common.Data.Web.Users;
|
||||
namespace Phantom.Common.Messages.Web.ToController;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record CreateOrUpdateAdministratorUserMessage(
|
||||
public sealed partial record CreateOrUpdateAdministratorUser(
|
||||
[property: MemoryPackOrder(0)] string Username,
|
||||
[property: MemoryPackOrder(1)] string Password
|
||||
) : IMessageToController<CreateOrUpdateAdministratorUserResult> {
|
||||
public Task<CreateOrUpdateAdministratorUserResult> Accept(IMessageToControllerListener listener) {
|
||||
return listener.HandleCreateOrUpdateAdministratorUser(this);
|
||||
return listener.CreateOrUpdateAdministratorUser(this);
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
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;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial record LogInMessage(
|
||||
public sealed partial record LogIn(
|
||||
[property: MemoryPackOrder(0)] string Username,
|
||||
[property: MemoryPackOrder(1)] string Password
|
||||
) : IMessageToController<LogInSuccess?> {
|
@ -1,15 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
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,6 +1,4 @@
|
||||
using Phantom.Common.Data.Replies;
|
||||
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.Messages.Web.BiDirectional;
|
||||
using Phantom.Common.Messages.Web.ToController;
|
||||
@ -17,14 +15,11 @@ public static class WebMessageRegistries {
|
||||
|
||||
static WebMessageRegistries() {
|
||||
ToController.Add<RegisterWebMessage>(0);
|
||||
ToController.Add<LogInMessage, LogInSuccess?>(1);
|
||||
ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(2);
|
||||
ToController.Add<CreateOrUpdateInstanceMessage, InstanceActionResult<CreateOrUpdateInstanceResult>>(3);
|
||||
ToController.Add<CreateOrUpdateAdministratorUser, CreateOrUpdateAdministratorUserResult>(1);
|
||||
ToController.Add<LogIn, LogInSuccess?>(2);
|
||||
ToController.Add<ReplyMessage>(127);
|
||||
|
||||
ToWeb.Add<RegisterWebResultMessage>(0);
|
||||
ToWeb.Add<RefreshAgentsMessage>(1);
|
||||
ToWeb.Add<RefreshInstancesMessage>(2);
|
||||
ToWeb.Add<ReplyMessage>(127);
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,8 @@ public sealed record Agent(
|
||||
public bool IsOnline { get; internal init; }
|
||||
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 AsOnline(DateTimeOffset lastPing) => this with {
|
||||
|
@ -0,0 +1,8 @@
|
||||
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) {
|
||||
return new WebMessageListener(connection, webAuthToken, UserManager, UserLoginManager, AgentManager, InstanceManager, TaskManager);
|
||||
return new WebMessageListener(connection, webAuthToken, UserManager, UserLoginManager);
|
||||
}
|
||||
|
||||
public async Task Initialize() {
|
||||
|
11
Controller/Phantom.Controller.Services/Instances/Instance.cs
Normal file
11
Controller/Phantom.Controller.Services/Instances/Instance.cs
Normal file
@ -0,0 +1,11 @@
|
||||
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,14 +4,12 @@ using Phantom.Common.Data;
|
||||
using Phantom.Common.Data.Instance;
|
||||
using Phantom.Common.Data.Minecraft;
|
||||
using Phantom.Common.Data.Replies;
|
||||
using Phantom.Common.Data.Web.Instance;
|
||||
using Phantom.Common.Data.Web.Minecraft;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Common.Messages.Agent;
|
||||
using Phantom.Common.Messages.Agent.ToAgent;
|
||||
using Phantom.Controller.Database;
|
||||
using Phantom.Controller.Database.Entities;
|
||||
using Phantom.Controller.Database.Repositories;
|
||||
using Phantom.Controller.Minecraft;
|
||||
using Phantom.Controller.Services.Agents;
|
||||
using Phantom.Utils.Collections;
|
||||
@ -57,53 +55,53 @@ sealed class InstanceManager {
|
||||
JvmArgumentsHelper.Split(entity.JvmArguments)
|
||||
);
|
||||
|
||||
var instance = Instance.Offline(configuration, entity.LaunchAutomatically);
|
||||
var instance = new Instance(configuration, entity.LaunchAutomatically);
|
||||
instances.ByGuid[instance.Configuration.InstanceGuid] = instance;
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "ConvertIfStatementToConditionalTernaryExpression")]
|
||||
public async Task<InstanceActionResult<CreateOrUpdateInstanceResult>> CreateOrUpdateInstance(InstanceConfiguration configuration, Guid auditLogUserGuid) {
|
||||
public async Task<InstanceActionResult<AddOrEditInstanceResult>> AddOrEditInstance(InstanceConfiguration configuration) {
|
||||
var agent = agentManager.GetAgent(configuration.AgentGuid);
|
||||
if (agent == null) {
|
||||
return InstanceActionResult.Concrete(CreateOrUpdateInstanceResult.AgentNotFound);
|
||||
return InstanceActionResult.Concrete(AddOrEditInstanceResult.AgentNotFound);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(configuration.InstanceName)) {
|
||||
return InstanceActionResult.Concrete(CreateOrUpdateInstanceResult.InstanceNameMustNotBeEmpty);
|
||||
return InstanceActionResult.Concrete(AddOrEditInstanceResult.InstanceNameMustNotBeEmpty);
|
||||
}
|
||||
|
||||
if (configuration.MemoryAllocation <= RamAllocationUnits.Zero) {
|
||||
return InstanceActionResult.Concrete(CreateOrUpdateInstanceResult.InstanceMemoryMustNotBeZero);
|
||||
return InstanceActionResult.Concrete(AddOrEditInstanceResult.InstanceMemoryMustNotBeZero);
|
||||
}
|
||||
|
||||
var serverExecutableInfo = await minecraftVersions.GetServerExecutableInfo(configuration.MinecraftVersion, cancellationToken);
|
||||
if (serverExecutableInfo == null) {
|
||||
return InstanceActionResult.Concrete(CreateOrUpdateInstanceResult.MinecraftVersionDownloadInfoNotFound);
|
||||
return InstanceActionResult.Concrete(AddOrEditInstanceResult.MinecraftVersionDownloadInfoNotFound);
|
||||
}
|
||||
|
||||
InstanceActionResult<CreateOrUpdateInstanceResult> result;
|
||||
InstanceActionResult<AddOrEditInstanceResult> result;
|
||||
bool isNewInstance;
|
||||
|
||||
await modifyInstancesSemaphore.WaitAsync(cancellationToken);
|
||||
try {
|
||||
isNewInstance = !instances.ByGuid.TryReplace(configuration.InstanceGuid, instance => instance with { Configuration = configuration });
|
||||
if (isNewInstance) {
|
||||
instances.ByGuid.TryAdd(configuration.InstanceGuid, Instance.Offline(configuration));
|
||||
instances.ByGuid.TryAdd(configuration.InstanceGuid, new Instance(configuration));
|
||||
}
|
||||
|
||||
var message = new ConfigureInstanceMessage(configuration, new InstanceLaunchProperties(serverExecutableInfo));
|
||||
var reply = await agentManager.SendMessage<ConfigureInstanceMessage, InstanceActionResult<ConfigureInstanceResult>>(configuration.AgentGuid, message, TimeSpan.FromSeconds(10));
|
||||
|
||||
result = reply.DidNotReplyIfNull().Map(static result => result switch {
|
||||
ConfigureInstanceResult.Success => CreateOrUpdateInstanceResult.Success,
|
||||
_ => CreateOrUpdateInstanceResult.UnknownError
|
||||
ConfigureInstanceResult.Success => AddOrEditInstanceResult.Success,
|
||||
_ => AddOrEditInstanceResult.UnknownError
|
||||
});
|
||||
|
||||
if (result.Is(CreateOrUpdateInstanceResult.Success)) {
|
||||
await using var db = dbProvider.Lazy();
|
||||
if (result.Is(AddOrEditInstanceResult.Success)) {
|
||||
await using var ctx = dbProvider.Eager();
|
||||
InstanceEntity entity = ctx.InstanceUpsert.Fetch(configuration.InstanceGuid);
|
||||
|
||||
InstanceEntity entity = db.Ctx.InstanceUpsert.Fetch(configuration.InstanceGuid);
|
||||
entity.AgentGuid = configuration.AgentGuid;
|
||||
entity.InstanceName = configuration.InstanceName;
|
||||
entity.ServerPort = configuration.ServerPort;
|
||||
@ -114,15 +112,7 @@ sealed class InstanceManager {
|
||||
entity.JavaRuntimeGuid = configuration.JavaRuntimeGuid;
|
||||
entity.JvmArguments = JvmArgumentsHelper.Join(configuration.JvmArguments);
|
||||
|
||||
var auditLogRepository = new AuditLogRepository(db, auditLogUserGuid);
|
||||
if (isNewInstance) {
|
||||
auditLogRepository.AddInstanceCreatedEvent(configuration.InstanceGuid);
|
||||
}
|
||||
else {
|
||||
auditLogRepository.AddInstanceEditedEvent(configuration.InstanceGuid);
|
||||
}
|
||||
|
||||
await db.Ctx.SaveChangesAsync(cancellationToken);
|
||||
await ctx.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
else if (isNewInstance) {
|
||||
instances.ByGuid.Remove(configuration.InstanceGuid);
|
||||
@ -131,7 +121,7 @@ sealed class InstanceManager {
|
||||
modifyInstancesSemaphore.Release();
|
||||
}
|
||||
|
||||
if (result.Is(CreateOrUpdateInstanceResult.Success)) {
|
||||
if (result.Is(AddOrEditInstanceResult.Success)) {
|
||||
if (isNewInstance) {
|
||||
Logger.Information("Added instance \"{InstanceName}\" (GUID {InstanceGuid}) to agent \"{AgentName}\".", configuration.InstanceName, configuration.InstanceGuid, agent.Name);
|
||||
}
|
||||
@ -141,10 +131,10 @@ sealed class InstanceManager {
|
||||
}
|
||||
else {
|
||||
if (isNewInstance) {
|
||||
Logger.Information("Failed adding instance \"{InstanceName}\" (GUID {InstanceGuid}) to agent \"{AgentName}\". {ErrorMessage}", configuration.InstanceName, configuration.InstanceGuid, agent.Name, result.ToSentence(CreateOrUpdateInstanceResultExtensions.ToSentence));
|
||||
Logger.Information("Failed adding instance \"{InstanceName}\" (GUID {InstanceGuid}) to agent \"{AgentName}\". {ErrorMessage}", configuration.InstanceName, configuration.InstanceGuid, agent.Name, result.ToSentence(AddOrEditInstanceResultExtensions.ToSentence));
|
||||
}
|
||||
else {
|
||||
Logger.Information("Failed editing instance \"{InstanceName}\" (GUID {InstanceGuid}) in agent \"{AgentName}\". {ErrorMessage}", configuration.InstanceName, configuration.InstanceGuid, agent.Name, result.ToSentence(CreateOrUpdateInstanceResultExtensions.ToSentence));
|
||||
Logger.Information("Failed editing instance \"{InstanceName}\" (GUID {InstanceGuid}) in agent \"{AgentName}\". {ErrorMessage}", configuration.InstanceName, configuration.InstanceGuid, agent.Name, result.ToSentence(AddOrEditInstanceResultExtensions.ToSentence));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,4 @@
|
||||
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;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Common.Messages.Web;
|
||||
@ -10,11 +6,8 @@ using Phantom.Common.Messages.Web.BiDirectional;
|
||||
using Phantom.Common.Messages.Web.ToController;
|
||||
using Phantom.Common.Messages.Web.ToWeb;
|
||||
using Phantom.Controller.Rpc;
|
||||
using Phantom.Controller.Services.Agents;
|
||||
using Phantom.Controller.Services.Instances;
|
||||
using Phantom.Controller.Services.Users;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
using Phantom.Utils.Tasks;
|
||||
using Serilog;
|
||||
|
||||
namespace Phantom.Controller.Services.Rpc;
|
||||
@ -26,18 +19,12 @@ public sealed class WebMessageListener : IMessageToControllerListener {
|
||||
private readonly AuthToken authToken;
|
||||
private readonly UserManager userManager;
|
||||
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, AgentManager agentManager, InstanceManager instanceManager, TaskManager taskManager) {
|
||||
internal WebMessageListener(RpcConnectionToClient<IMessageToWebListener> connection, AuthToken authToken, UserManager userManager, UserLoginManager userLoginManager) {
|
||||
this.connection = connection;
|
||||
this.authToken = authToken;
|
||||
this.userManager = userManager;
|
||||
this.userLoginManager = userLoginManager;
|
||||
this.agentManager = agentManager;
|
||||
this.instanceManager = instanceManager;
|
||||
this.taskManager = taskManager;
|
||||
}
|
||||
|
||||
public async Task<NoReply> HandleRegisterWeb(RegisterWebMessage message) {
|
||||
@ -51,31 +38,14 @@ public sealed class WebMessageListener : IMessageToControllerListener {
|
||||
await connection.Send(new RegisterWebResultMessage(false));
|
||||
}
|
||||
|
||||
|
||||
agentManager.AgentsChanged.Subscribe(this, HandleAgentsChanged);
|
||||
instanceManager.InstancesChanged.Subscribe(this, HandleInstancesChanged);
|
||||
return NoReply.Instance;
|
||||
}
|
||||
|
||||
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) {
|
||||
public Task<CreateOrUpdateAdministratorUserResult> CreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUser message) {
|
||||
return userManager.CreateOrUpdateAdministrator(message.Username, message.Password);
|
||||
}
|
||||
|
||||
public Task<InstanceActionResult<CreateOrUpdateInstanceResult>> HandleCreateOrUpdateInstance(CreateOrUpdateInstanceMessage message) {
|
||||
return instanceManager.CreateOrUpdateInstance(message.Configuration, message.LoggedInUserGuid);
|
||||
}
|
||||
|
||||
public Task<LogInSuccess?> HandleLogIn(LogInMessage message) {
|
||||
public Task<LogInSuccess?> HandleLogIn(LogIn message) {
|
||||
return userLoginManager.LogIn(message.Username, message.Password);
|
||||
}
|
||||
|
||||
|
@ -1,20 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
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())));
|
||||
}
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
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())));
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
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();
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
32
Web/Phantom.Web.Services/Authentication/UserSession.cs
Normal file
32
Web/Phantom.Web.Services/Authentication/UserSession.cs
Normal file
@ -0,0 +1,32 @@
|
||||
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());
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
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,22 +1,15 @@
|
||||
using System.Security.Claims;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
using Phantom.Web.Services.Authentication;
|
||||
using UserInfo = Phantom.Web.Services.Authentication.UserInfo;
|
||||
|
||||
namespace Phantom.Web.Services.Authorization;
|
||||
|
||||
public sealed class PermissionManager {
|
||||
private readonly UserSessionManager sessionManager;
|
||||
// TODO
|
||||
public class PermissionManager {
|
||||
public IdentityPermissions GetPermissions(ClaimsPrincipal user, bool refreshCache = false) {
|
||||
|
||||
public PermissionManager(UserSessionManager sessionManager) {
|
||||
this.sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
public PermissionSet GetPermissions(ClaimsPrincipal user) {
|
||||
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);
|
||||
public bool CheckPermission(ClaimsPrincipal user, Permission permission, bool refreshCache = false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -1,29 +1,10 @@
|
||||
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;
|
||||
using Phantom.Common.Data.Replies;
|
||||
|
||||
namespace Phantom.Web.Services.Instances;
|
||||
|
||||
public sealed class InstanceManager {
|
||||
private readonly ControllerConnection controllerConnection;
|
||||
private readonly SimpleObservableState<ImmutableArray<Instance>> instances = new (PhantomLogger.Create<InstanceManager>("Instances"), ImmutableArray<Instance>.Empty);
|
||||
// TODO
|
||||
public class InstanceManager {
|
||||
public async Task<InstanceActionResult<SendCommandToInstanceResult>> SendCommand(Guid instanceGuid, string command) {
|
||||
|
||||
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,7 +14,6 @@
|
||||
<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.Messages.Web\Phantom.Common.Messages.Web.csproj" />
|
||||
<ProjectReference Include="..\..\Utils\Phantom.Utils.Events\Phantom.Utils.Events.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -1,29 +1,23 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
using Phantom.Web.Services.Agents;
|
||||
using Phantom.Web.Services.Authentication;
|
||||
using Phantom.Web.Services.Authorization;
|
||||
using Phantom.Web.Services.Instances;
|
||||
using Phantom.Web.Services.Rpc;
|
||||
|
||||
namespace Phantom.Web.Services;
|
||||
|
||||
public static class PhantomWebServices {
|
||||
public static void AddPhantomServices(this IServiceCollection services) {
|
||||
services.AddSingleton<ControllerConnection>();
|
||||
services.AddSingleton<MessageListener>();
|
||||
|
||||
services.AddSingleton<AgentManager>();
|
||||
services.AddSingleton<InstanceManager>();
|
||||
services.AddSingleton<ControllerConnection>();
|
||||
services.AddSingleton<PermissionManager>();
|
||||
|
||||
services.AddSingleton<UserSessionManager>();
|
||||
services.AddScoped<UserSessionBrowserStorage>();
|
||||
services.AddScoped<UserLoginManager>();
|
||||
|
||||
services.AddScoped<CustomAuthenticationStateProvider>();
|
||||
services.AddScoped<AuthenticationStateProvider>(static services => services.GetRequiredService<CustomAuthenticationStateProvider>());
|
||||
services.AddSingleton<PhantomLoginSessions>();
|
||||
services.AddScoped<PhantomLoginManager>();
|
||||
services.AddScoped<PhantomAuthenticationStateProvider>();
|
||||
services.AddScoped<AuthenticationStateProvider>(static services => services.GetRequiredService<PhantomAuthenticationStateProvider>());
|
||||
services.AddScoped<IHostEnvironmentAuthenticationStateProvider>(static services => services.GetRequiredService<PhantomAuthenticationStateProvider>());
|
||||
|
||||
services.AddAuthorization(ConfigureAuthorization);
|
||||
services.AddScoped<IAuthorizationHandler, PermissionBasedPolicyHandler>();
|
||||
|
@ -4,8 +4,6 @@ using Phantom.Common.Messages.Web.ToWeb;
|
||||
using Phantom.Utils.Rpc;
|
||||
using Phantom.Utils.Rpc.Message;
|
||||
using Phantom.Utils.Tasks;
|
||||
using Phantom.Web.Services.Agents;
|
||||
using Phantom.Web.Services.Instances;
|
||||
|
||||
namespace Phantom.Web.Services.Rpc;
|
||||
|
||||
@ -13,13 +11,9 @@ public sealed class MessageListener : IMessageToWebListener {
|
||||
public TaskCompletionSource<bool> RegisterSuccessWaiter { get; } = AsyncTasks.CreateCompletionSource<bool>();
|
||||
|
||||
private readonly RpcConnectionToServer<IMessageToControllerListener> connection;
|
||||
private readonly AgentManager agentManager;
|
||||
private readonly InstanceManager instanceManager;
|
||||
|
||||
public MessageListener(RpcConnectionToServer<IMessageToControllerListener> connection, AgentManager agentManager, InstanceManager instanceManager) {
|
||||
public MessageListener(RpcConnectionToServer<IMessageToControllerListener> connection) {
|
||||
this.connection = connection;
|
||||
this.agentManager = agentManager;
|
||||
this.instanceManager = instanceManager;
|
||||
}
|
||||
|
||||
public Task<NoReply> HandleRegisterWebResult(RegisterWebResultMessage message) {
|
||||
@ -27,16 +21,6 @@ public sealed class MessageListener : IMessageToWebListener {
|
||||
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) {
|
||||
connection.Receive(message);
|
||||
return Task.FromResult(NoReply.Instance);
|
||||
|
@ -18,7 +18,7 @@ public abstract class PhantomComponent : ComponentBase {
|
||||
|
||||
protected async Task<bool> CheckPermission(Permission permission) {
|
||||
var authenticationState = await AuthenticationStateTask;
|
||||
return PermissionManager.CheckPermission(authenticationState.User, permission);
|
||||
return PermissionManager.CheckPermission(authenticationState.User, permission, refreshCache: true);
|
||||
}
|
||||
|
||||
protected void InvokeAsyncChecked(Func<Task> task) {
|
||||
|
@ -1,7 +1,5 @@
|
||||
@page "/agents"
|
||||
@using Phantom.Common.Data.Web.Agent
|
||||
@using Phantom.Utils.Collections
|
||||
@using Phantom.Web.Services.Agents
|
||||
@implements IDisposable
|
||||
@inject AgentManager AgentManager
|
||||
|
||||
@ -75,7 +73,7 @@
|
||||
|
||||
@code {
|
||||
|
||||
private readonly Table<AgentWithStats, Guid> agentTable = new();
|
||||
private readonly Table<Agent, Guid> agentTable = new();
|
||||
|
||||
protected override void OnInitialized() {
|
||||
AgentManager.AgentsChanged.Subscribe(this, agents => {
|
||||
|
@ -1,10 +1,6 @@
|
||||
@page "/instances"
|
||||
@attribute [Authorize(Permission.ViewInstancesPolicy)]
|
||||
@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
|
||||
@inject AgentManager AgentManager
|
||||
@inject InstanceManager InstanceManager
|
||||
@ -83,9 +79,7 @@
|
||||
});
|
||||
|
||||
InstanceManager.InstancesChanged.Subscribe(this, instances => {
|
||||
this.instances = instances.OrderBy(instance => agentNames.TryGetValue(instance.Configuration.AgentGuid, out var agentName) ? agentName : string.Empty)
|
||||
.ThenBy(static instance => instance.Configuration.InstanceName)
|
||||
.ToImmutableArray();
|
||||
this.instances = instances.Values.OrderBy(instance => agentNames.TryGetValue(instance.Configuration.AgentGuid, out var agentName) ? agentName : string.Empty).ThenBy(static instance => instance.Configuration.InstanceName).ToImmutableArray();
|
||||
InvokeAsync(StateHasChanged);
|
||||
});
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@attribute [AllowAnonymous]
|
||||
@inject INavigation Navigation
|
||||
@inject UserLoginManager LoginManager
|
||||
@inject PhantomLoginManager LoginManager
|
||||
|
||||
<h1>Login</h1>
|
||||
|
||||
@ -51,7 +51,7 @@
|
||||
|
||||
string? returnUrl = Navigation.GetQueryParameter("return", out var url) ? url : null;
|
||||
|
||||
if (!await LoginManager.LogIn(form.Username, form.Password, returnUrl)) {
|
||||
if (!await LoginManager.SignIn(form.Username, form.Password, returnUrl)) {
|
||||
form.SubmitModel.StopSubmitting("Invalid username or password.");
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
@page "/logout"
|
||||
@using Phantom.Web.Services.Authentication
|
||||
@inject UserLoginManager LoginManager
|
||||
@inject PhantomLoginManager LoginManager
|
||||
|
||||
@code {
|
||||
|
||||
protected override Task OnInitializedAsync() {
|
||||
return LoginManager.LogOut();
|
||||
return LoginManager.SignOut();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -10,7 +10,7 @@
|
||||
@using Phantom.Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults
|
||||
@attribute [AllowAnonymous]
|
||||
@inject ServiceConfiguration ServiceConfiguration
|
||||
@inject UserLoginManager LoginManager
|
||||
@inject PhantomLoginManager LoginManager
|
||||
@inject ControllerConnection ControllerConnection
|
||||
|
||||
<h1>Administrator Setup</h1>
|
||||
@ -69,7 +69,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var signInResult = await LoginManager.LogIn(form.Username, form.Password);
|
||||
var signInResult = await LoginManager.SignIn(form.Username, form.Password);
|
||||
if (!signInResult) {
|
||||
form.SubmitModel.StopSubmitting("Error logging in.");
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user