mirror of
				https://github.com/chylex/Minecraft-Phantom-Panel.git
				synced 2025-11-04 03:40:15 +01:00 
			
		
		
		
	Compare commits
	
		
			2 Commits
		
	
	
		
			main
			...
			f50f5341da
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						f50f5341da
	
				 | 
					
					
						|||
| 
						
						
							
						
						f14d7f5590
	
				 | 
					
					
						
@@ -1,9 +1,10 @@
 | 
				
			|||||||
<component name="ProjectRunConfigurationManager">
 | 
					<component name="ProjectRunConfigurationManager">
 | 
				
			||||||
  <configuration default="false" name="Controller + Agent x3" type="CompoundRunConfigurationType">
 | 
					  <configuration default="false" name="Controller + Web + Agent x3" type="CompoundRunConfigurationType">
 | 
				
			||||||
    <toRun name="Agent 1" type="DotNetProject" />
 | 
					    <toRun name="Agent 1" type="DotNetProject" />
 | 
				
			||||||
    <toRun name="Agent 2" type="DotNetProject" />
 | 
					    <toRun name="Agent 2" type="DotNetProject" />
 | 
				
			||||||
    <toRun name="Agent 3" type="DotNetProject" />
 | 
					    <toRun name="Agent 3" type="DotNetProject" />
 | 
				
			||||||
    <toRun name="Controller" type="DotNetProject" />
 | 
					    <toRun name="Controller" type="DotNetProject" />
 | 
				
			||||||
 | 
					    <toRun name="Web" type="DotNetProject" />
 | 
				
			||||||
    <method v="2" />
 | 
					    <method v="2" />
 | 
				
			||||||
  </configuration>
 | 
					  </configuration>
 | 
				
			||||||
</component>
 | 
					</component>
 | 
				
			||||||
@@ -1,7 +1,8 @@
 | 
				
			|||||||
<component name="ProjectRunConfigurationManager">
 | 
					<component name="ProjectRunConfigurationManager">
 | 
				
			||||||
  <configuration default="false" name="Controller + Agent" type="CompoundRunConfigurationType">
 | 
					  <configuration default="false" name="Controller + Web + Agent" type="CompoundRunConfigurationType">
 | 
				
			||||||
    <toRun name="Agent 1" type="DotNetProject" />
 | 
					    <toRun name="Agent 1" type="DotNetProject" />
 | 
				
			||||||
    <toRun name="Controller" type="DotNetProject" />
 | 
					    <toRun name="Controller" type="DotNetProject" />
 | 
				
			||||||
 | 
					    <toRun name="Web" type="DotNetProject" />
 | 
				
			||||||
    <method v="2" />
 | 
					    <method v="2" />
 | 
				
			||||||
  </configuration>
 | 
					  </configuration>
 | 
				
			||||||
</component>
 | 
					</component>
 | 
				
			||||||
							
								
								
									
										26
									
								
								.run/Web.run.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								.run/Web.run.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					<component name="ProjectRunConfigurationManager">
 | 
				
			||||||
 | 
					  <configuration default="false" name="Web" type="DotNetProject" factoryName=".NET Project">
 | 
				
			||||||
 | 
					    <option name="EXE_PATH" value="$PROJECT_DIR$/.artifacts/bin/Phantom.Web/debug/Phantom.Web.exe" />
 | 
				
			||||||
 | 
					    <option name="PROGRAM_PARAMETERS" value="" />
 | 
				
			||||||
 | 
					    <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Web" />
 | 
				
			||||||
 | 
					    <option name="PASS_PARENT_ENVS" value="1" />
 | 
				
			||||||
 | 
					    <envs>
 | 
				
			||||||
 | 
					      <env name="ASPNETCORE_ENVIRONMENT" value="Development" />
 | 
				
			||||||
 | 
					      <env name="CONTROLLER_HOST" value="localhost" />
 | 
				
			||||||
 | 
					      <env name="WEB_KEY" value="BMNHM9RRPMCBBY29D9XHS6KBKZSRY7F5XFN27YZX96XXWJC2NM2D6YRHM9PZN9JGQGCSJ6FMB2GGZ" />
 | 
				
			||||||
 | 
					      <env name="WEB_SERVER_HOST" value="localhost" />
 | 
				
			||||||
 | 
					    </envs>
 | 
				
			||||||
 | 
					    <option name="USE_EXTERNAL_CONSOLE" value="0" />
 | 
				
			||||||
 | 
					    <option name="USE_MONO" value="0" />
 | 
				
			||||||
 | 
					    <option name="RUNTIME_ARGUMENTS" value="" />
 | 
				
			||||||
 | 
					    <option name="PROJECT_PATH" value="$PROJECT_DIR$/Web/Phantom.Web/Phantom.Web.csproj" />
 | 
				
			||||||
 | 
					    <option name="PROJECT_EXE_PATH_TRACKING" value="1" />
 | 
				
			||||||
 | 
					    <option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
 | 
				
			||||||
 | 
					    <option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="0" />
 | 
				
			||||||
 | 
					    <option name="PROJECT_KIND" value="DotNetCore" />
 | 
				
			||||||
 | 
					    <option name="PROJECT_TFM" value="net8.0" />
 | 
				
			||||||
 | 
					    <method v="2">
 | 
				
			||||||
 | 
					      <option name="Build" />
 | 
				
			||||||
 | 
					    </method>
 | 
				
			||||||
 | 
					  </configuration>
 | 
				
			||||||
 | 
					</component>
 | 
				
			||||||
@@ -18,8 +18,4 @@ public sealed class ControllerConnection {
 | 
				
			|||||||
	public Task Send<TMessage>(TMessage message) where TMessage : IMessageToController {
 | 
						public Task Send<TMessage>(TMessage message) where TMessage : IMessageToController {
 | 
				
			||||||
		return connection.Send(message);
 | 
							return connection.Send(message);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					 | 
				
			||||||
	public Task<TReply?> Send<TMessage, TReply>(TMessage message, TimeSpan waitForReplyTime, CancellationToken waitForReplyCancellationToken) where TMessage : IMessageToController<TReply> where TReply : class {
 | 
					 | 
				
			||||||
		return connection.Send<TMessage, TReply>(message, waitForReplyTime, waitForReplyCancellationToken);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
using NetMQ;
 | 
					using NetMQ;
 | 
				
			||||||
using Phantom.Common.Data.Agent;
 | 
					using Phantom.Common.Data;
 | 
				
			||||||
using Phantom.Common.Logging;
 | 
					using Phantom.Common.Logging;
 | 
				
			||||||
using Phantom.Utils.Cryptography;
 | 
					using Phantom.Utils.Cryptography;
 | 
				
			||||||
using Phantom.Utils.IO;
 | 
					using Phantom.Utils.IO;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										22
									
								
								Common/Phantom.Common.Data.Web/Agent/AgentWithStats.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								Common/Phantom.Common.Data.Web/Agent/AgentWithStats.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					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
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						[MemoryPackIgnore]
 | 
				
			||||||
 | 
						public RamAllocationUnits? AvailableMemory => MaxMemory - Stats?.RunningInstanceMemory;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
namespace Phantom.Controller.Database.Enums;
 | 
					namespace Phantom.Common.Data.Web.AuditLog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public enum AuditLogEventType {
 | 
					public enum AuditLogEventType {
 | 
				
			||||||
	AdministratorUserCreated,
 | 
						AdministratorUserCreated,
 | 
				
			||||||
@@ -6,6 +6,7 @@ public enum AuditLogEventType {
 | 
				
			|||||||
	UserLoggedIn,
 | 
						UserLoggedIn,
 | 
				
			||||||
	UserLoggedOut,
 | 
						UserLoggedOut,
 | 
				
			||||||
	UserCreated,
 | 
						UserCreated,
 | 
				
			||||||
 | 
						UserPasswordChanged,
 | 
				
			||||||
	UserRolesChanged,
 | 
						UserRolesChanged,
 | 
				
			||||||
	UserDeleted,
 | 
						UserDeleted,
 | 
				
			||||||
	InstanceCreated,
 | 
						InstanceCreated,
 | 
				
			||||||
@@ -15,13 +16,14 @@ public enum AuditLogEventType {
 | 
				
			|||||||
	InstanceCommandExecuted
 | 
						InstanceCommandExecuted
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static class AuditLogEventTypeExtensions {
 | 
					public static class AuditLogEventTypeExtensions {
 | 
				
			||||||
	private static readonly Dictionary<AuditLogEventType, AuditLogSubjectType> SubjectTypes = new () {
 | 
						private static readonly Dictionary<AuditLogEventType, AuditLogSubjectType> SubjectTypes = new () {
 | 
				
			||||||
		{ AuditLogEventType.AdministratorUserCreated,  AuditLogSubjectType.User },
 | 
							{ AuditLogEventType.AdministratorUserCreated,  AuditLogSubjectType.User },
 | 
				
			||||||
		{ AuditLogEventType.AdministratorUserModified, AuditLogSubjectType.User },
 | 
							{ AuditLogEventType.AdministratorUserModified, AuditLogSubjectType.User },
 | 
				
			||||||
		{ AuditLogEventType.UserLoggedIn,              AuditLogSubjectType.User },
 | 
							{ AuditLogEventType.UserLoggedIn,              AuditLogSubjectType.User },
 | 
				
			||||||
		{ AuditLogEventType.UserLoggedOut,             AuditLogSubjectType.User },
 | 
							{ AuditLogEventType.UserLoggedOut,             AuditLogSubjectType.User },
 | 
				
			||||||
		{ AuditLogEventType.UserCreated,               AuditLogSubjectType.User },
 | 
							{ AuditLogEventType.UserCreated,               AuditLogSubjectType.User },
 | 
				
			||||||
 | 
							{ AuditLogEventType.UserPasswordChanged,       AuditLogSubjectType.User },
 | 
				
			||||||
		{ AuditLogEventType.UserRolesChanged,          AuditLogSubjectType.User },
 | 
							{ AuditLogEventType.UserRolesChanged,          AuditLogSubjectType.User },
 | 
				
			||||||
		{ AuditLogEventType.UserDeleted,               AuditLogSubjectType.User },
 | 
							{ AuditLogEventType.UserDeleted,               AuditLogSubjectType.User },
 | 
				
			||||||
		{ AuditLogEventType.InstanceCreated,           AuditLogSubjectType.Instance },
 | 
							{ AuditLogEventType.InstanceCreated,           AuditLogSubjectType.Instance },
 | 
				
			||||||
@@ -39,7 +41,7 @@ static class AuditLogEventTypeExtensions {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	internal static AuditLogSubjectType GetSubjectType(this AuditLogEventType type) {
 | 
						public static AuditLogSubjectType GetSubjectType(this AuditLogEventType type) {
 | 
				
			||||||
		return SubjectTypes[type];
 | 
							return SubjectTypes[type];
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
							
								
								
									
										14
									
								
								Common/Phantom.Common.Data.Web/AuditLog/AuditLogItem.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								Common/Phantom.Common.Data.Web/AuditLog/AuditLogItem.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					using MemoryPack;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Common.Data.Web.AuditLog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
					public sealed partial record AuditLogItem(
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(0)] DateTime UtcTime,
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(1)] Guid? UserGuid,
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(2)] string? UserName,
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(3)] AuditLogEventType EventType,
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(4)] AuditLogSubjectType SubjectType,
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(5)] string? SubjectId,
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(6)] string? JsonData
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
namespace Phantom.Controller.Database.Enums;
 | 
					namespace Phantom.Common.Data.Web.AuditLog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public enum AuditLogSubjectType {
 | 
					public enum AuditLogSubjectType {
 | 
				
			||||||
	User,
 | 
						User,
 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
namespace Phantom.Controller.Database.Enums;
 | 
					namespace Phantom.Common.Data.Web.EventLog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public enum EventLogEventType {
 | 
					public enum EventLogEventType {
 | 
				
			||||||
	InstanceLaunchSucceded,
 | 
						InstanceLaunchSucceded,
 | 
				
			||||||
@@ -10,7 +10,7 @@ public enum EventLogEventType {
 | 
				
			|||||||
	InstanceBackupFailed,
 | 
						InstanceBackupFailed,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static class EventLogEventTypeExtensions {
 | 
					public static class EventLogEventTypeExtensions {
 | 
				
			||||||
	private static readonly Dictionary<EventLogEventType, EventLogSubjectType> SubjectTypes = new () {
 | 
						private static readonly Dictionary<EventLogEventType, EventLogSubjectType> SubjectTypes = new () {
 | 
				
			||||||
		{ EventLogEventType.InstanceLaunchSucceded, EventLogSubjectType.Instance },
 | 
							{ EventLogEventType.InstanceLaunchSucceded, EventLogSubjectType.Instance },
 | 
				
			||||||
		{ EventLogEventType.InstanceLaunchFailed, EventLogSubjectType.Instance },
 | 
							{ EventLogEventType.InstanceLaunchFailed, EventLogSubjectType.Instance },
 | 
				
			||||||
							
								
								
									
										13
									
								
								Common/Phantom.Common.Data.Web/EventLog/EventLogItem.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								Common/Phantom.Common.Data.Web/EventLog/EventLogItem.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					using MemoryPack;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Common.Data.Web.EventLog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
					public sealed record EventLogItem(
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(0)] DateTime UtcTime,
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(1)] Guid? AgentGuid,
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(2)] EventLogEventType EventType,
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(3)] EventLogSubjectType SubjectType,
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(4)] string SubjectId,
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(5)] string? JsonData
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					namespace Phantom.Common.Data.Web.EventLog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public enum EventLogSubjectType {
 | 
				
			||||||
 | 
						Instance
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					namespace Phantom.Common.Data.Web.Instance;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public enum CreateOrUpdateInstanceResult : byte {
 | 
				
			||||||
 | 
						UnknownError,
 | 
				
			||||||
 | 
						Success,
 | 
				
			||||||
 | 
						InstanceNameMustNotBeEmpty,
 | 
				
			||||||
 | 
						InstanceMemoryMustNotBeZero,
 | 
				
			||||||
 | 
						MinecraftVersionDownloadInfoNotFound,
 | 
				
			||||||
 | 
						AgentNotFound
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public static class CreateOrUpdateInstanceResultExtensions {
 | 
				
			||||||
 | 
						public static string ToSentence(this CreateOrUpdateInstanceResult reason) {
 | 
				
			||||||
 | 
							return reason switch {
 | 
				
			||||||
 | 
								CreateOrUpdateInstanceResult.Success                              => "Success.",
 | 
				
			||||||
 | 
								CreateOrUpdateInstanceResult.InstanceNameMustNotBeEmpty           => "Instance name must not be empty.",
 | 
				
			||||||
 | 
								CreateOrUpdateInstanceResult.InstanceMemoryMustNotBeZero          => "Memory must not be 0 MB.",
 | 
				
			||||||
 | 
								CreateOrUpdateInstanceResult.MinecraftVersionDownloadInfoNotFound => "Could not find download information for the selected Minecraft version.",
 | 
				
			||||||
 | 
								CreateOrUpdateInstanceResult.AgentNotFound                        => "Agent not found.",
 | 
				
			||||||
 | 
								_                                                                 => "Unknown error."
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										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);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
using System.Collections.Immutable;
 | 
					using System.Collections.Immutable;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Phantom.Controller.Minecraft;
 | 
					namespace Phantom.Common.Data.Web.Minecraft;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public static class JvmArgumentsHelper {
 | 
					public static class JvmArgumentsHelper {
 | 
				
			||||||
	public static ImmutableArray<string> Split(string arguments) {
 | 
						public static ImmutableArray<string> Split(string arguments) {
 | 
				
			||||||
@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					<Project Sdk="Microsoft.NET.Sdk">
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  <PropertyGroup>
 | 
				
			||||||
 | 
					    <ImplicitUsings>enable</ImplicitUsings>
 | 
				
			||||||
 | 
					    <Nullable>enable</Nullable>
 | 
				
			||||||
 | 
					  </PropertyGroup>
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  <ItemGroup>
 | 
				
			||||||
 | 
					    <PackageReference Include="BCrypt.Net-Next.StrongName" />
 | 
				
			||||||
 | 
					    <PackageReference Include="MemoryPack" />
 | 
				
			||||||
 | 
					  </ItemGroup>
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  <ItemGroup>
 | 
				
			||||||
 | 
					    <ProjectReference Include="..\Phantom.Common.Data\Phantom.Common.Data.csproj" />
 | 
				
			||||||
 | 
					  </ItemGroup>
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					</Project>
 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
namespace Phantom.Controller.Services.Users.Roles;
 | 
					namespace Phantom.Common.Data.Web.Users;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public enum AddRoleError : byte {
 | 
					public enum AddRoleError : byte {
 | 
				
			||||||
	NameIsEmpty,
 | 
						NameIsEmpty,
 | 
				
			||||||
							
								
								
									
										28
									
								
								Common/Phantom.Common.Data.Web/Users/AddUserError.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								Common/Phantom.Common.Data.Web/Users/AddUserError.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					using System.Collections.Immutable;
 | 
				
			||||||
 | 
					using MemoryPack;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.Users.AddUserErrors;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Common.Data.Web.Users {
 | 
				
			||||||
 | 
						[MemoryPackable]
 | 
				
			||||||
 | 
						[MemoryPackUnion(0, typeof(NameIsInvalid))]
 | 
				
			||||||
 | 
						[MemoryPackUnion(1, typeof(PasswordIsInvalid))]
 | 
				
			||||||
 | 
						[MemoryPackUnion(2, typeof(NameAlreadyExists))]
 | 
				
			||||||
 | 
						[MemoryPackUnion(3, typeof(UnknownError))]
 | 
				
			||||||
 | 
						public abstract partial record AddUserError {
 | 
				
			||||||
 | 
							internal AddUserError() {}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Common.Data.Web.Users.AddUserErrors {
 | 
				
			||||||
 | 
						[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
						public sealed partial record NameIsInvalid([property: MemoryPackOrder(0)] UsernameRequirementViolation Violation) : AddUserError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
						public sealed partial record PasswordIsInvalid([property: MemoryPackOrder(0)] ImmutableArray<PasswordRequirementViolation> Violations) : AddUserError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
						public sealed partial record NameAlreadyExists : AddUserError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
						public sealed partial record UnknownError : AddUserError;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					using MemoryPack;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Common.Data.Web.Users {
 | 
				
			||||||
 | 
						[MemoryPackable]
 | 
				
			||||||
 | 
						[MemoryPackUnion(0, typeof(Success))]
 | 
				
			||||||
 | 
						[MemoryPackUnion(1, typeof(CreationFailed))]
 | 
				
			||||||
 | 
						[MemoryPackUnion(2, typeof(UpdatingFailed))]
 | 
				
			||||||
 | 
						[MemoryPackUnion(3, typeof(AddingToRoleFailed))]
 | 
				
			||||||
 | 
						[MemoryPackUnion(4, typeof(UnknownError))]
 | 
				
			||||||
 | 
						public abstract partial record CreateOrUpdateAdministratorUserResult {
 | 
				
			||||||
 | 
							internal CreateOrUpdateAdministratorUserResult() {}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults {
 | 
				
			||||||
 | 
						[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
						public sealed partial record Success([property: MemoryPackOrder(0)] UserInfo User) : CreateOrUpdateAdministratorUserResult;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
						public sealed partial record CreationFailed([property: MemoryPackOrder(0)] AddUserError Error) : CreateOrUpdateAdministratorUserResult;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
						public sealed partial record UpdatingFailed([property: MemoryPackOrder(0)] SetUserPasswordError Error) : CreateOrUpdateAdministratorUserResult;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
						public sealed partial record AddingToRoleFailed : CreateOrUpdateAdministratorUserResult;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
						public sealed partial record UnknownError : CreateOrUpdateAdministratorUserResult;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
namespace Phantom.Controller.Services.Users;
 | 
					namespace Phantom.Common.Data.Web.Users;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public enum DeleteUserResult : byte {
 | 
					public enum DeleteUserResult : byte {
 | 
				
			||||||
	Deleted,
 | 
						Deleted,
 | 
				
			||||||
							
								
								
									
										11
									
								
								Common/Phantom.Common.Data.Web/Users/LogInSuccess.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								Common/Phantom.Common.Data.Web/Users/LogInSuccess.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					using System.Collections.Immutable;
 | 
				
			||||||
 | 
					using MemoryPack;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Common.Data.Web.Users;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
					public sealed partial record LogInSuccess (
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(0)] Guid UserGuid,
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(1)] PermissionSet Permissions,
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(2)] ImmutableArray<byte> Token
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
@@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					using MemoryPack;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.Users.PasswordRequirementViolations;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Common.Data.Web.Users {
 | 
				
			||||||
 | 
						[MemoryPackable]
 | 
				
			||||||
 | 
						[MemoryPackUnion(0, typeof(TooShort))]
 | 
				
			||||||
 | 
						[MemoryPackUnion(1, typeof(MustContainLowercaseLetter))]
 | 
				
			||||||
 | 
						[MemoryPackUnion(2, typeof(MustContainUppercaseLetter))]
 | 
				
			||||||
 | 
						[MemoryPackUnion(3, typeof(MustContainDigit))]
 | 
				
			||||||
 | 
						public abstract partial record PasswordRequirementViolation {
 | 
				
			||||||
 | 
							internal PasswordRequirementViolation() {}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Common.Data.Web.Users.PasswordRequirementViolations {
 | 
				
			||||||
 | 
						[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
						public sealed partial record TooShort([property: MemoryPackOrder(0)] int MinimumLength) : PasswordRequirementViolation;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
						public sealed partial record MustContainLowercaseLetter : PasswordRequirementViolation;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
						public sealed partial record MustContainUppercaseLetter : PasswordRequirementViolation;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
						public sealed partial record MustContainDigit : PasswordRequirementViolation;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
namespace Phantom.Controller.Services.Users.Permissions;
 | 
					namespace Phantom.Common.Data.Web.Users;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public sealed record Permission(string Id, Permission? Parent) {
 | 
					public sealed record Permission(string Id, Permission? Parent) {
 | 
				
			||||||
	private static readonly List<Permission> AllPermissions = new ();
 | 
						private static readonly List<Permission> AllPermissions = new ();
 | 
				
			||||||
							
								
								
									
										29
									
								
								Common/Phantom.Common.Data.Web/Users/PermissionSet.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								Common/Phantom.Common.Data.Web/Users/PermissionSet.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
				
			|||||||
 | 
					using System.Collections.Immutable;
 | 
				
			||||||
 | 
					using MemoryPack;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Common.Data.Web.Users;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
					public sealed partial class PermissionSet {
 | 
				
			||||||
 | 
						public static PermissionSet None { get; } = new (ImmutableHashSet<string>.Empty);
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						[MemoryPackOrder(0)]
 | 
				
			||||||
 | 
						[MemoryPackInclude]
 | 
				
			||||||
 | 
						private readonly ImmutableHashSet<string> permissionIds;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public PermissionSet(ImmutableHashSet<string> permissionIds) {
 | 
				
			||||||
 | 
							this.permissionIds = permissionIds;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public bool Check(Permission? permission) {
 | 
				
			||||||
 | 
							while (permission != null) {
 | 
				
			||||||
 | 
								if (!permissionIds.Contains(permission.Id)) {
 | 
				
			||||||
 | 
									return false;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								permission = permission.Parent;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return true;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										9
									
								
								Common/Phantom.Common.Data.Web/Users/RoleInfo.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								Common/Phantom.Common.Data.Web/Users/RoleInfo.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					using MemoryPack;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Common.Data.Web.Users;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
					public sealed partial record RoleInfo(
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(0)] Guid Guid,
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(1)] string Name
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
							
								
								
									
										24
									
								
								Common/Phantom.Common.Data.Web/Users/SetUserPasswordError.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								Common/Phantom.Common.Data.Web/Users/SetUserPasswordError.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					using System.Collections.Immutable;
 | 
				
			||||||
 | 
					using MemoryPack;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.Users.SetUserPasswordErrors;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Common.Data.Web.Users {
 | 
				
			||||||
 | 
						[MemoryPackable]
 | 
				
			||||||
 | 
						[MemoryPackUnion(0, typeof(UserNotFound))]
 | 
				
			||||||
 | 
						[MemoryPackUnion(1, typeof(PasswordIsInvalid))]
 | 
				
			||||||
 | 
						[MemoryPackUnion(2, typeof(UnknownError))]
 | 
				
			||||||
 | 
						public abstract partial record SetUserPasswordError {
 | 
				
			||||||
 | 
							internal SetUserPasswordError() {}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Common.Data.Web.Users.SetUserPasswordErrors {
 | 
				
			||||||
 | 
						[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
						public sealed partial record UserNotFound : SetUserPasswordError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
						public sealed partial record PasswordIsInvalid([property: MemoryPackOrder(0)] ImmutableArray<PasswordRequirementViolation> Violations) : SetUserPasswordError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
						public sealed partial record UnknownError : SetUserPasswordError;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										9
									
								
								Common/Phantom.Common.Data.Web/Users/UserInfo.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								Common/Phantom.Common.Data.Web/Users/UserInfo.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					using MemoryPack;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Common.Data.Web.Users;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
					public sealed partial record UserInfo(
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(0)] Guid Guid,
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(1)] string Name
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
							
								
								
									
										12
									
								
								Common/Phantom.Common.Data.Web/Users/UserPasswords.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								Common/Phantom.Common.Data.Web/Users/UserPasswords.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					namespace Phantom.Common.Data.Web.Users;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public static class UserPasswords {
 | 
				
			||||||
 | 
						public static string Hash(string password) {
 | 
				
			||||||
 | 
							return BCrypt.Net.BCrypt.HashPassword(password, workFactor: 12);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public static bool Verify(string password, string hash) {
 | 
				
			||||||
 | 
							// TODO rehash
 | 
				
			||||||
 | 
							return BCrypt.Net.BCrypt.Verify(password, hash);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					using MemoryPack;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.Users.UsernameRequirementViolations;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Common.Data.Web.Users {
 | 
				
			||||||
 | 
						[MemoryPackable]
 | 
				
			||||||
 | 
						[MemoryPackUnion(0, typeof(IsEmpty))]
 | 
				
			||||||
 | 
						[MemoryPackUnion(1, typeof(TooLong))]
 | 
				
			||||||
 | 
						public abstract partial record UsernameRequirementViolation {
 | 
				
			||||||
 | 
							internal UsernameRequirementViolation() {}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Common.Data.Web.Users.UsernameRequirementViolations {
 | 
				
			||||||
 | 
						[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
						public sealed partial record IsEmpty : UsernameRequirementViolation;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
						public sealed partial record TooLong([property: MemoryPackOrder(0)] int MaxLength) : UsernameRequirementViolation;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										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
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
@@ -2,7 +2,7 @@
 | 
				
			|||||||
using System.Security.Cryptography;
 | 
					using System.Security.Cryptography;
 | 
				
			||||||
using MemoryPack;
 | 
					using MemoryPack;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Phantom.Common.Data.Agent;
 | 
					namespace Phantom.Common.Data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[MemoryPackable(GenerateType.VersionTolerant)]
 | 
					[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
 | 
					[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
namespace Phantom.Common.Data.Agent;
 | 
					namespace Phantom.Common.Data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public readonly record struct ConnectionCommonKey(byte[] CertificatePublicKey, AuthToken AuthToken) {
 | 
					public readonly record struct ConnectionCommonKey(byte[] CertificatePublicKey, AuthToken AuthToken) {
 | 
				
			||||||
	private const byte TokenLength = AuthToken.Length;
 | 
						private const byte TokenLength = AuthToken.Length;
 | 
				
			||||||
@@ -1,7 +1,10 @@
 | 
				
			|||||||
namespace Phantom.Common.Data.Minecraft;
 | 
					using MemoryPack;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public sealed record MinecraftVersion(
 | 
					namespace Phantom.Common.Data.Minecraft;
 | 
				
			||||||
	string Id,
 | 
					
 | 
				
			||||||
	MinecraftVersionType Type,
 | 
					[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
	string MetadataUrl
 | 
					public sealed partial record MinecraftVersion(
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(0)] string Id,
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(1)] MinecraftVersionType Type,
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(2)] string MetadataUrl
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
using MemoryPack;
 | 
					using MemoryPack;
 | 
				
			||||||
 | 
					using Phantom.Common.Data;
 | 
				
			||||||
using Phantom.Common.Data.Agent;
 | 
					using Phantom.Common.Data.Agent;
 | 
				
			||||||
using Phantom.Utils.Rpc.Message;
 | 
					using Phantom.Utils.Rpc.Message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,29 @@
 | 
				
			|||||||
using Phantom.Common.Messages.Web.BiDirectional;
 | 
					using System.Collections.Immutable;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Java;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Minecraft;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Replies;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.AuditLog;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.EventLog;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.Instance;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.Users;
 | 
				
			||||||
 | 
					using Phantom.Common.Messages.Web.BiDirectional;
 | 
				
			||||||
 | 
					using Phantom.Common.Messages.Web.ToController;
 | 
				
			||||||
using Phantom.Utils.Rpc.Message;
 | 
					using Phantom.Utils.Rpc.Message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Phantom.Common.Messages.Web;
 | 
					namespace Phantom.Common.Messages.Web;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public interface IMessageToControllerListener {
 | 
					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<InstanceActionResult<LaunchInstanceResult>> HandleLaunchInstance(LaunchInstanceMessage message);
 | 
				
			||||||
 | 
						Task<InstanceActionResult<StopInstanceResult>> HandleStopInstance(StopInstanceMessage message);
 | 
				
			||||||
 | 
						Task<InstanceActionResult<SendCommandToInstanceResult>> HandleSendCommandToInstance(SendCommandToInstanceMessage message);
 | 
				
			||||||
 | 
						Task<ImmutableArray<MinecraftVersion>> HandleGetMinecraftVersions(GetMinecraftVersionsMessage message);
 | 
				
			||||||
 | 
						Task<ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>> HandleGetAgentJavaRuntimes(GetAgentJavaRuntimesMessage message);
 | 
				
			||||||
 | 
						Task<ImmutableArray<UserInfo>> HandleGetUsers(GetUsersMessage message);
 | 
				
			||||||
 | 
						Task<ImmutableArray<AuditLogItem>> HandleGetAuditLog(GetAuditLogMessage message);
 | 
				
			||||||
 | 
						Task<ImmutableArray<EventLogItem>> HandleGetEventLog(GetEventLogMessage message);
 | 
				
			||||||
	Task<NoReply> HandleReply(ReplyMessage message);
 | 
						Task<NoReply> HandleReply(ReplyMessage message);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,12 @@
 | 
				
			|||||||
using Phantom.Common.Messages.Web.BiDirectional;
 | 
					using Phantom.Common.Messages.Web.BiDirectional;
 | 
				
			||||||
 | 
					using Phantom.Common.Messages.Web.ToWeb;
 | 
				
			||||||
using Phantom.Utils.Rpc.Message;
 | 
					using Phantom.Utils.Rpc.Message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Phantom.Common.Messages.Web;
 | 
					namespace Phantom.Common.Messages.Web;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public interface IMessageToWebListener {
 | 
					public interface IMessageToWebListener {
 | 
				
			||||||
 | 
						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);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,7 @@
 | 
				
			|||||||
  <ItemGroup>
 | 
					  <ItemGroup>
 | 
				
			||||||
    <ProjectReference Include="..\Phantom.Common.Logging\Phantom.Common.Logging.csproj" />
 | 
					    <ProjectReference Include="..\Phantom.Common.Logging\Phantom.Common.Logging.csproj" />
 | 
				
			||||||
    <ProjectReference Include="..\Phantom.Common.Data\Phantom.Common.Data.csproj" />
 | 
					    <ProjectReference Include="..\Phantom.Common.Data\Phantom.Common.Data.csproj" />
 | 
				
			||||||
 | 
					    <ProjectReference Include="..\Phantom.Common.Data.Web\Phantom.Common.Data.Web.csproj" />
 | 
				
			||||||
    <ProjectReference Include="..\..\Utils\Phantom.Utils.Rpc\Phantom.Utils.Rpc.csproj" />
 | 
					    <ProjectReference Include="..\..\Utils\Phantom.Utils.Rpc\Phantom.Utils.Rpc.csproj" />
 | 
				
			||||||
  </ItemGroup>
 | 
					  </ItemGroup>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					using MemoryPack;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.Users;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Common.Messages.Web.ToController;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
					public sealed partial record CreateOrUpdateAdministratorUserMessage(
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(0)] string Username,
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(1)] string Password
 | 
				
			||||||
 | 
					) : IMessageToController<CreateOrUpdateAdministratorUserResult> {
 | 
				
			||||||
 | 
						public Task<CreateOrUpdateAdministratorUserResult> Accept(IMessageToControllerListener listener) {
 | 
				
			||||||
 | 
							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);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					using System.Collections.Immutable;
 | 
				
			||||||
 | 
					using MemoryPack;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Java;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Common.Messages.Web.ToController;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
					public sealed partial record GetAgentJavaRuntimesMessage : IMessageToController<ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>> {
 | 
				
			||||||
 | 
						public Task<ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>> Accept(IMessageToControllerListener listener) {
 | 
				
			||||||
 | 
							return listener.HandleGetAgentJavaRuntimes(this);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					using System.Collections.Immutable;
 | 
				
			||||||
 | 
					using MemoryPack;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.AuditLog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Common.Messages.Web.ToController;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
					public sealed partial record GetAuditLogMessage(
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(0)] int Count
 | 
				
			||||||
 | 
					) : IMessageToController<ImmutableArray<AuditLogItem>> {
 | 
				
			||||||
 | 
						public Task<ImmutableArray<AuditLogItem>> Accept(IMessageToControllerListener listener) {
 | 
				
			||||||
 | 
							return listener.HandleGetAuditLog(this);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					using System.Collections.Immutable;
 | 
				
			||||||
 | 
					using MemoryPack;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.EventLog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Common.Messages.Web.ToController;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
					public sealed partial record GetEventLogMessage(
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(0)] int Count
 | 
				
			||||||
 | 
					) : IMessageToController<ImmutableArray<EventLogItem>> {
 | 
				
			||||||
 | 
						public Task<ImmutableArray<EventLogItem>> Accept(IMessageToControllerListener listener) {
 | 
				
			||||||
 | 
							return listener.HandleGetEventLog(this);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					using System.Collections.Immutable;
 | 
				
			||||||
 | 
					using MemoryPack;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Minecraft;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Common.Messages.Web.ToController;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
					public sealed partial record GetMinecraftVersionsMessage : IMessageToController<ImmutableArray<MinecraftVersion>> {
 | 
				
			||||||
 | 
						public Task<ImmutableArray<MinecraftVersion>> Accept(IMessageToControllerListener listener) {
 | 
				
			||||||
 | 
							return listener.HandleGetMinecraftVersions(this);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					using System.Collections.Immutable;
 | 
				
			||||||
 | 
					using MemoryPack;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.Users;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Common.Messages.Web.ToController;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
					public sealed partial record GetUsersMessage : IMessageToController<ImmutableArray<UserInfo>> {
 | 
				
			||||||
 | 
						public Task<ImmutableArray<UserInfo>> Accept(IMessageToControllerListener listener) {
 | 
				
			||||||
 | 
							return listener.HandleGetUsers(this);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					using MemoryPack;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Replies;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Common.Messages.Web.ToController; 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
					public sealed partial record LaunchInstanceMessage(
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(1)] Guid InstanceGuid
 | 
				
			||||||
 | 
					) : IMessageToController<InstanceActionResult<LaunchInstanceResult>> {
 | 
				
			||||||
 | 
						public Task<InstanceActionResult<LaunchInstanceResult>> Accept(IMessageToControllerListener listener) {
 | 
				
			||||||
 | 
							return listener.HandleLaunchInstance(this);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					using MemoryPack;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.Users;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Common.Messages.Web.ToController;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
					public sealed partial record LogInMessage(
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(0)] string Username,
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(1)] string Password
 | 
				
			||||||
 | 
					) : IMessageToController<LogInSuccess?> {
 | 
				
			||||||
 | 
						public Task<LogInSuccess?> Accept(IMessageToControllerListener listener) {
 | 
				
			||||||
 | 
							return listener.HandleLogIn(this);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					using MemoryPack;
 | 
				
			||||||
 | 
					using Phantom.Common.Data;
 | 
				
			||||||
 | 
					using Phantom.Utils.Rpc.Message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Common.Messages.Web.ToController;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
					public sealed partial record RegisterWebMessage(
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(0)] AuthToken AuthToken
 | 
				
			||||||
 | 
					) : IMessageToController {
 | 
				
			||||||
 | 
						public Task<NoReply> Accept(IMessageToControllerListener listener) {
 | 
				
			||||||
 | 
							return listener.HandleRegisterWeb(this);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					using MemoryPack;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Replies;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Common.Messages.Web.ToController; 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
					public sealed partial record SendCommandToInstanceMessage(
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(1)] Guid InstanceGuid,
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(2)] string Command
 | 
				
			||||||
 | 
					) : IMessageToController<InstanceActionResult<SendCommandToInstanceResult>> {
 | 
				
			||||||
 | 
						public Task<InstanceActionResult<SendCommandToInstanceResult>> Accept(IMessageToControllerListener listener) {
 | 
				
			||||||
 | 
							return listener.HandleSendCommandToInstance(this);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					using MemoryPack;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Minecraft;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Replies;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Common.Messages.Web.ToController; 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
					public sealed partial record StopInstanceMessage(
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(0)] Guid LoggedInUserGuid,
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(1)] Guid InstanceGuid,
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(2)] MinecraftStopStrategy StopStrategy
 | 
				
			||||||
 | 
					) : IMessageToController<InstanceActionResult<StopInstanceResult>> {
 | 
				
			||||||
 | 
						public Task<InstanceActionResult<StopInstanceResult>> Accept(IMessageToControllerListener listener) {
 | 
				
			||||||
 | 
							return listener.HandleStopInstance(this);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					using MemoryPack;
 | 
				
			||||||
 | 
					using Phantom.Utils.Rpc.Message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Common.Messages.Web.ToWeb; 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[MemoryPackable(GenerateType.VersionTolerant)]
 | 
				
			||||||
 | 
					public sealed partial record RegisterWebResultMessage(
 | 
				
			||||||
 | 
						[property: MemoryPackOrder(0)] bool Success
 | 
				
			||||||
 | 
					) : IMessageToWeb {
 | 
				
			||||||
 | 
						public Task<NoReply> Accept(IMessageToWebListener listener) {
 | 
				
			||||||
 | 
							return listener.HandleRegisterWebResult(this);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,5 +1,15 @@
 | 
				
			|||||||
using Phantom.Common.Logging;
 | 
					using System.Collections.Immutable;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Java;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Minecraft;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Replies;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.AuditLog;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.EventLog;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.Instance;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.Users;
 | 
				
			||||||
 | 
					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.ToWeb;
 | 
				
			||||||
using Phantom.Utils.Rpc.Message;
 | 
					using Phantom.Utils.Rpc.Message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Phantom.Common.Messages.Web;
 | 
					namespace Phantom.Common.Messages.Web;
 | 
				
			||||||
@@ -11,8 +21,22 @@ public static class WebMessageRegistries {
 | 
				
			|||||||
	public static IMessageDefinitions<IMessageToWebListener, IMessageToControllerListener, ReplyMessage> Definitions { get; } = new MessageDefinitions();
 | 
						public static IMessageDefinitions<IMessageToWebListener, IMessageToControllerListener, ReplyMessage> Definitions { get; } = new MessageDefinitions();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static 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<LaunchInstanceMessage, InstanceActionResult<LaunchInstanceResult>>(4);
 | 
				
			||||||
 | 
							ToController.Add<StopInstanceMessage, InstanceActionResult<StopInstanceResult>>(5);
 | 
				
			||||||
 | 
							ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(6);
 | 
				
			||||||
 | 
							ToController.Add<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(7);
 | 
				
			||||||
 | 
							ToController.Add<GetUsersMessage, ImmutableArray<UserInfo>>(8);
 | 
				
			||||||
 | 
							ToController.Add<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(9);
 | 
				
			||||||
 | 
							ToController.Add<GetEventLogMessage, ImmutableArray<EventLogItem>>(10);
 | 
				
			||||||
		ToController.Add<ReplyMessage>(127);
 | 
							ToController.Add<ReplyMessage>(127);
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
 | 
							ToWeb.Add<RegisterWebResultMessage>(0);
 | 
				
			||||||
 | 
							ToWeb.Add<RefreshAgentsMessage>(1);
 | 
				
			||||||
 | 
							ToWeb.Add<RefreshInstancesMessage>(2);
 | 
				
			||||||
		ToWeb.Add<ReplyMessage>(127);
 | 
							ToWeb.Add<ReplyMessage>(127);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -21,7 +45,7 @@ public static class WebMessageRegistries {
 | 
				
			|||||||
		public MessageRegistry<IMessageToControllerListener> ToServer => ToController;
 | 
							public MessageRegistry<IMessageToControllerListener> ToServer => ToController;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		public bool IsRegistrationMessage(Type messageType) {
 | 
							public bool IsRegistrationMessage(Type messageType) {
 | 
				
			||||||
			return false;
 | 
								return messageType == typeof(RegisterWebMessage);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		public ReplyMessage CreateReplyMessage(uint sequenceId, byte[] serializedReply) {
 | 
							public ReplyMessage CreateReplyMessage(uint sequenceId, byte[] serializedReply) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,17 +4,21 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
namespace Phantom.Controller.Database.Postgres;
 | 
					namespace Phantom.Controller.Database.Postgres;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public sealed class ApplicationDbContextFactory : IDatabaseProvider {
 | 
					public sealed class ApplicationDbContextFactory : IDbContextProvider {
 | 
				
			||||||
	private readonly PooledDbContextFactory<ApplicationDbContext> factory;
 | 
						private readonly PooledDbContextFactory<ApplicationDbContext> factory;
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	public ApplicationDbContextFactory(string connectionString) {
 | 
						public ApplicationDbContextFactory(string connectionString) {
 | 
				
			||||||
		this.factory = new PooledDbContextFactory<ApplicationDbContext>(CreateOptions(connectionString), poolSize: 32);
 | 
							this.factory = new PooledDbContextFactory<ApplicationDbContext>(CreateOptions(connectionString), poolSize: 32);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public ApplicationDbContext Provide() {
 | 
						public ApplicationDbContext Eager() {
 | 
				
			||||||
		return factory.CreateDbContext();
 | 
							return factory.CreateDbContext();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public ILazyDbContext Lazy() {
 | 
				
			||||||
 | 
							return new LazyDbContext(this);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private static DbContextOptions<ApplicationDbContext> CreateOptions(string connectionString) {
 | 
						private static DbContextOptions<ApplicationDbContext> CreateOptions(string connectionString) {
 | 
				
			||||||
		var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
 | 
							var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
 | 
				
			||||||
		builder.UseNpgsql(connectionString, ConfigureOptions);
 | 
							builder.UseNpgsql(connectionString, ConfigureOptions);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					namespace Phantom.Controller.Database.Postgres;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					sealed class LazyDbContext : ILazyDbContext {
 | 
				
			||||||
 | 
						public ApplicationDbContext Ctx => cachedContext ??= contextFactory.Eager();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private readonly ApplicationDbContextFactory contextFactory;
 | 
				
			||||||
 | 
						private ApplicationDbContext? cachedContext;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						internal LazyDbContext(ApplicationDbContextFactory contextFactory) {
 | 
				
			||||||
 | 
							this.contextFactory = contextFactory;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public ValueTask DisposeAsync() {
 | 
				
			||||||
 | 
							return cachedContext?.DisposeAsync() ?? ValueTask.CompletedTask;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -3,9 +3,10 @@ using Microsoft.EntityFrameworkCore;
 | 
				
			|||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 | 
					using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 | 
				
			||||||
using Phantom.Common.Data;
 | 
					using Phantom.Common.Data;
 | 
				
			||||||
using Phantom.Common.Data.Minecraft;
 | 
					using Phantom.Common.Data.Minecraft;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.AuditLog;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.EventLog;
 | 
				
			||||||
using Phantom.Controller.Database.Converters;
 | 
					using Phantom.Controller.Database.Converters;
 | 
				
			||||||
using Phantom.Controller.Database.Entities;
 | 
					using Phantom.Controller.Database.Entities;
 | 
				
			||||||
using Phantom.Controller.Database.Enums;
 | 
					 | 
				
			||||||
using Phantom.Controller.Database.Factories;
 | 
					using Phantom.Controller.Database.Factories;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Phantom.Controller.Database;
 | 
					namespace Phantom.Controller.Database;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,8 +8,8 @@ namespace Phantom.Controller.Database;
 | 
				
			|||||||
public static class DatabaseMigrator {
 | 
					public static class DatabaseMigrator {
 | 
				
			||||||
	private static readonly ILogger Logger = PhantomLogger.Create(nameof(DatabaseMigrator));
 | 
						private static readonly ILogger Logger = PhantomLogger.Create(nameof(DatabaseMigrator));
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	public static async Task Run(IDatabaseProvider databaseProvider, CancellationToken cancellationToken) {
 | 
						public static async Task Run(IDbContextProvider dbProvider, CancellationToken cancellationToken) {
 | 
				
			||||||
		await using var ctx = databaseProvider.Provide();
 | 
							await using var ctx = dbProvider.Eager();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		Logger.Information("Connecting to database...");
 | 
							Logger.Information("Connecting to database...");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,7 @@
 | 
				
			|||||||
using System.ComponentModel.DataAnnotations.Schema;
 | 
					using System.ComponentModel.DataAnnotations.Schema;
 | 
				
			||||||
using System.Diagnostics.CodeAnalysis;
 | 
					using System.Diagnostics.CodeAnalysis;
 | 
				
			||||||
using System.Text.Json;
 | 
					using System.Text.Json;
 | 
				
			||||||
using Phantom.Controller.Database.Enums;
 | 
					using Phantom.Common.Data.Web.AuditLog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Phantom.Controller.Database.Entities;
 | 
					namespace Phantom.Controller.Database.Entities;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,7 @@
 | 
				
			|||||||
using System.ComponentModel.DataAnnotations.Schema;
 | 
					using System.ComponentModel.DataAnnotations.Schema;
 | 
				
			||||||
using System.Diagnostics.CodeAnalysis;
 | 
					using System.Diagnostics.CodeAnalysis;
 | 
				
			||||||
using System.Text.Json;
 | 
					using System.Text.Json;
 | 
				
			||||||
using Phantom.Controller.Database.Enums;
 | 
					using Phantom.Common.Data.Web.EventLog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Phantom.Controller.Database.Entities;
 | 
					namespace Phantom.Controller.Database.Entities;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
using System.ComponentModel.DataAnnotations;
 | 
					using System.ComponentModel.DataAnnotations;
 | 
				
			||||||
using System.ComponentModel.DataAnnotations.Schema;
 | 
					using System.ComponentModel.DataAnnotations.Schema;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.Users;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Phantom.Controller.Database.Entities;
 | 
					namespace Phantom.Controller.Database.Entities;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -11,9 +12,13 @@ public sealed class UserEntity {
 | 
				
			|||||||
	public string Name { get; set; }
 | 
						public string Name { get; set; }
 | 
				
			||||||
	public string PasswordHash { get; set; }
 | 
						public string PasswordHash { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public UserEntity(Guid userGuid, string name) {
 | 
						public UserEntity(Guid userGuid, string name, string passwordHash) {
 | 
				
			||||||
		UserGuid = userGuid;
 | 
							UserGuid = userGuid;
 | 
				
			||||||
		Name = name;
 | 
							Name = name;
 | 
				
			||||||
		PasswordHash = null!;
 | 
							PasswordHash = passwordHash;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						public UserInfo ToUserInfo() {
 | 
				
			||||||
 | 
							return new UserInfo(UserGuid, Name);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +0,0 @@
 | 
				
			|||||||
namespace Phantom.Controller.Database.Enums;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
public enum EventLogSubjectType {
 | 
					 | 
				
			||||||
	Instance
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,5 +0,0 @@
 | 
				
			|||||||
namespace Phantom.Controller.Database;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
public interface IDatabaseProvider {
 | 
					 | 
				
			||||||
	ApplicationDbContext Provide();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					namespace Phantom.Controller.Database;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public interface IDbContextProvider {
 | 
				
			||||||
 | 
						ApplicationDbContext Eager();
 | 
				
			||||||
 | 
						ILazyDbContext Lazy();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										5
									
								
								Controller/Phantom.Controller.Database/ILazyDbContext.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								Controller/Phantom.Controller.Database/ILazyDbContext.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					namespace Phantom.Controller.Database;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public interface ILazyDbContext : IAsyncDisposable {
 | 
				
			||||||
 | 
						ApplicationDbContext Ctx { get; }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -15,6 +15,7 @@
 | 
				
			|||||||
  
 | 
					  
 | 
				
			||||||
  <ItemGroup>
 | 
					  <ItemGroup>
 | 
				
			||||||
    <ProjectReference Include="..\..\Common\Phantom.Common.Data\Phantom.Common.Data.csproj" />
 | 
					    <ProjectReference Include="..\..\Common\Phantom.Common.Data\Phantom.Common.Data.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" />
 | 
				
			||||||
  </ItemGroup>
 | 
					  </ItemGroup>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,78 @@
 | 
				
			|||||||
 | 
					using Phantom.Common.Data.Web.AuditLog;
 | 
				
			||||||
 | 
					using Phantom.Controller.Database.Entities;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Controller.Database.Repositories;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public sealed class AuditLogRepositoryWriter {
 | 
				
			||||||
 | 
						private readonly ILazyDbContext db;
 | 
				
			||||||
 | 
						private readonly Guid? currentUserGuid;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public AuditLogRepositoryWriter(ILazyDbContext db, Guid? currentUserGuid) {
 | 
				
			||||||
 | 
							this.db = db;
 | 
				
			||||||
 | 
							this.currentUserGuid = currentUserGuid;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private void AddItem(AuditLogEventType eventType, string subjectId, Dictionary<string, object?>? extra = null) {
 | 
				
			||||||
 | 
							db.Ctx.AuditLog.Add(new AuditLogEntity(currentUserGuid, eventType, subjectId, extra));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						public void AddUserLoggedInEvent(UserEntity user) {
 | 
				
			||||||
 | 
							AddItem(AuditLogEventType.UserLoggedIn, user.UserGuid.ToString());
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void AddUserLoggedOutEvent(Guid userGuid) {
 | 
				
			||||||
 | 
							AddItem(AuditLogEventType.UserLoggedOut, userGuid.ToString());
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						public void AddUserCreatedEvent(UserEntity user) {
 | 
				
			||||||
 | 
							AddItem(AuditLogEventType.UserCreated, user.UserGuid.ToString());
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						public void AddUserPasswordChangedEvent(UserEntity user) {
 | 
				
			||||||
 | 
							AddItem(AuditLogEventType.UserCreated, user.UserGuid.ToString());
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void AddUserRolesChangedEvent(UserEntity user, List<string> addedToRoles, List<string> removedFromRoles) {
 | 
				
			||||||
 | 
							var extra = new Dictionary<string, object?>();
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							if (addedToRoles.Count > 0) {
 | 
				
			||||||
 | 
								extra["addedToRoles"] = addedToRoles;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							if (removedFromRoles.Count > 0) {
 | 
				
			||||||
 | 
								extra["removedFromRoles"] = removedFromRoles;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							AddItem(AuditLogEventType.UserRolesChanged, user.UserGuid.ToString(), extra);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						public void AddUserDeletedEvent(UserEntity user) {
 | 
				
			||||||
 | 
							AddItem(AuditLogEventType.UserDeleted, user.UserGuid.ToString(), new Dictionary<string, object?> {
 | 
				
			||||||
 | 
								{ "username", user.Name }
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void AddInstanceCreatedEvent(Guid instanceGuid) {
 | 
				
			||||||
 | 
							AddItem(AuditLogEventType.InstanceCreated, instanceGuid.ToString());
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void AddInstanceEditedEvent(Guid instanceGuid) {
 | 
				
			||||||
 | 
							AddItem(AuditLogEventType.InstanceEdited, instanceGuid.ToString());
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						public void AddInstanceLaunchedEvent(Guid instanceGuid) {
 | 
				
			||||||
 | 
							AddItem(AuditLogEventType.InstanceLaunched, instanceGuid.ToString());
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void AddInstanceCommandExecutedEvent(Guid instanceGuid, string command) {
 | 
				
			||||||
 | 
							AddItem(AuditLogEventType.InstanceCommandExecuted, instanceGuid.ToString(), new Dictionary<string, object?> {
 | 
				
			||||||
 | 
								{ "command", command }
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void AddInstanceStoppedEvent(Guid instanceGuid, int stopInSeconds) {
 | 
				
			||||||
 | 
							AddItem(AuditLogEventType.InstanceStopped, instanceGuid.ToString(), new Dictionary<string, object?> {
 | 
				
			||||||
 | 
								{ "stop_in_seconds", stopInSeconds.ToString() }
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					using System.Collections.Immutable;
 | 
				
			||||||
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.AuditLog;
 | 
				
			||||||
 | 
					using Phantom.Utils.Collections;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Controller.Database.Repositories;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public sealed class AuditLogRepositoryReader {
 | 
				
			||||||
 | 
						private readonly ILazyDbContext db;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public AuditLogRepositoryReader(ILazyDbContext db) {
 | 
				
			||||||
 | 
							this.db = db;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Task<ImmutableArray<AuditLogItem>> GetMostRecentItems(int count, CancellationToken cancellationToken) {
 | 
				
			||||||
 | 
							return db.Ctx
 | 
				
			||||||
 | 
							         .AuditLog
 | 
				
			||||||
 | 
							         .Include(static entity => entity.User)
 | 
				
			||||||
 | 
							         .AsQueryable()
 | 
				
			||||||
 | 
							         .OrderByDescending(static entity => entity.UtcTime)
 | 
				
			||||||
 | 
							         .Take(count)
 | 
				
			||||||
 | 
							         .Select(static entity => new AuditLogItem(entity.UtcTime, entity.UserGuid, entity.User == null ? null : entity.User.Name, entity.EventType, entity.SubjectType, entity.SubjectId, entity.Data == null ? null : entity.Data.RootElement.ToString()))
 | 
				
			||||||
 | 
							         .AsAsyncEnumerable()
 | 
				
			||||||
 | 
							         .ToImmutableArrayAsync(cancellationToken);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					using System.Collections.Immutable;
 | 
				
			||||||
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.Users;
 | 
				
			||||||
 | 
					using Phantom.Controller.Database.Entities;
 | 
				
			||||||
 | 
					using Phantom.Utils.Collections;
 | 
				
			||||||
 | 
					using Phantom.Utils.Tasks;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Controller.Database.Repositories;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public sealed class RoleRepository {
 | 
				
			||||||
 | 
						private const int MaxRoleNameLength = 40;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private readonly ILazyDbContext db;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public RoleRepository(ILazyDbContext db) {
 | 
				
			||||||
 | 
							this.db = db;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Task<List<RoleEntity>> GetAll() {
 | 
				
			||||||
 | 
							return db.Ctx.Roles.ToListAsync();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Task<ImmutableHashSet<string>> GetAllNames() {
 | 
				
			||||||
 | 
							return db.Ctx.Roles.Select(static role => role.Name).AsAsyncEnumerable().ToImmutableSetAsync();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public ValueTask<RoleEntity?> GetByGuid(Guid guid) {
 | 
				
			||||||
 | 
							return db.Ctx.Roles.FindAsync(guid);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public async Task<Result<RoleEntity, AddRoleError>> Create(string name) {
 | 
				
			||||||
 | 
							if (string.IsNullOrWhiteSpace(name)) {
 | 
				
			||||||
 | 
								return AddRoleError.NameIsEmpty;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							else if (name.Length > MaxRoleNameLength) {
 | 
				
			||||||
 | 
								return AddRoleError.NameIsTooLong;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (await db.Ctx.Roles.AnyAsync(role => role.Name == name)) {
 | 
				
			||||||
 | 
								return AddRoleError.NameAlreadyExists;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var role = new RoleEntity(Guid.NewGuid(), name);
 | 
				
			||||||
 | 
							db.Ctx.Roles.Add(role);
 | 
				
			||||||
 | 
							return role;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,115 @@
 | 
				
			|||||||
 | 
					using System.Collections.Immutable;
 | 
				
			||||||
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.Users;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.Users.AddUserErrors;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.Users.PasswordRequirementViolations;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.Users.UsernameRequirementViolations;
 | 
				
			||||||
 | 
					using Phantom.Controller.Database.Entities;
 | 
				
			||||||
 | 
					using Phantom.Utils.Collections;
 | 
				
			||||||
 | 
					using Phantom.Utils.Tasks;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Controller.Database.Repositories;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public sealed class UserRepository {
 | 
				
			||||||
 | 
						private const int MaxUserNameLength = 40;
 | 
				
			||||||
 | 
						private const int MinimumPasswordLength = 16;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private static UsernameRequirementViolation? CheckUsernameRequirements(string username) {
 | 
				
			||||||
 | 
							if (string.IsNullOrWhiteSpace(username)) {
 | 
				
			||||||
 | 
								return new IsEmpty();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							else if (username.Length > MaxUserNameLength) {
 | 
				
			||||||
 | 
								return new TooLong(MaxUserNameLength);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							else {
 | 
				
			||||||
 | 
								return null;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private static ImmutableArray<PasswordRequirementViolation> CheckPasswordRequirements(string password) {
 | 
				
			||||||
 | 
							var violations = ImmutableArray.CreateBuilder<PasswordRequirementViolation>();
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							if (password.Length < MinimumPasswordLength) {
 | 
				
			||||||
 | 
								violations.Add(new TooShort(MinimumPasswordLength));
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (!password.Any(char.IsLower)) {
 | 
				
			||||||
 | 
								violations.Add(new MustContainLowercaseLetter());
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (!password.Any(char.IsUpper)) {
 | 
				
			||||||
 | 
								violations.Add(new MustContainUppercaseLetter());
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (!password.Any(char.IsDigit)) {
 | 
				
			||||||
 | 
								violations.Add(new MustContainDigit());
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							return violations.ToImmutable();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						private readonly ILazyDbContext db;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private AuditLogRepositoryWriter? auditLogWriter;
 | 
				
			||||||
 | 
						private AuditLogRepositoryWriter AuditLogWriter => this.auditLogWriter ??= new AuditLogRepositoryWriter(db, null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public UserRepository(ILazyDbContext db) {
 | 
				
			||||||
 | 
							this.db = db;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Task<ImmutableArray<UserEntity>> GetAll() {
 | 
				
			||||||
 | 
							return db.Ctx.Users.AsAsyncEnumerable().ToImmutableArrayAsync();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Task<Dictionary<Guid, T>> GetAllByGuid<T>(Func<UserEntity, T> valueSelector, CancellationToken cancellationToken = default) {
 | 
				
			||||||
 | 
							return db.Ctx.Users.ToDictionaryAsync(static user => user.UserGuid, valueSelector, cancellationToken);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public async Task<UserEntity?> GetByGuid(Guid guid) {
 | 
				
			||||||
 | 
							return await db.Ctx.Users.FindAsync(guid);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Task<UserEntity?> GetByName(string username) {
 | 
				
			||||||
 | 
							return db.Ctx.Users.FirstOrDefaultAsync(user => user.Name == username);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						public async Task<Result<UserEntity, AddUserError>> CreateUser(string username, string password) {
 | 
				
			||||||
 | 
							var usernameRequirementViolation = CheckUsernameRequirements(username);
 | 
				
			||||||
 | 
							if (usernameRequirementViolation != null) {
 | 
				
			||||||
 | 
								return new NameIsInvalid(usernameRequirementViolation);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var passwordRequirementViolations = CheckPasswordRequirements(password);
 | 
				
			||||||
 | 
							if (!passwordRequirementViolations.IsEmpty) {
 | 
				
			||||||
 | 
								return new PasswordIsInvalid(passwordRequirementViolations);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (await db.Ctx.Users.AnyAsync(user => user.Name == username)) {
 | 
				
			||||||
 | 
								return new NameAlreadyExists();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var user = new UserEntity(Guid.NewGuid(), username, UserPasswords.Hash(password));
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							db.Ctx.Users.Add(user);
 | 
				
			||||||
 | 
							AuditLogWriter.AddUserCreatedEvent(user);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return user;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Result<SetUserPasswordError> SetUserPassword(UserEntity user, string password) {
 | 
				
			||||||
 | 
							var requirementViolations = CheckPasswordRequirements(password);
 | 
				
			||||||
 | 
							if (!requirementViolations.IsEmpty) {
 | 
				
			||||||
 | 
								return new Common.Data.Web.Users.SetUserPasswordErrors.PasswordIsInvalid(requirementViolations);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							user.PasswordHash = UserPasswords.Hash(password);
 | 
				
			||||||
 | 
							AuditLogWriter.AddUserPasswordChangedEvent(user);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return Result.Ok<SetUserPasswordError>();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void DeleteUser(UserEntity user) {
 | 
				
			||||||
 | 
							db.Ctx.Users.Remove(user);
 | 
				
			||||||
 | 
							AuditLogWriter.AddUserDeletedEvent(user);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,56 @@
 | 
				
			|||||||
 | 
					using System.Collections.Immutable;
 | 
				
			||||||
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
 | 
					using Phantom.Controller.Database.Entities;
 | 
				
			||||||
 | 
					using Phantom.Utils.Collections;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Controller.Database.Repositories;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public sealed class UserRoleRepository {
 | 
				
			||||||
 | 
						private readonly ILazyDbContext db;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public UserRoleRepository(ILazyDbContext db) {
 | 
				
			||||||
 | 
							this.db = db;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Task<Dictionary<Guid, ImmutableArray<RoleEntity>>> GetAllByUserGuid() {
 | 
				
			||||||
 | 
							return db.Ctx.UserRoles
 | 
				
			||||||
 | 
							         .Include(static ur => ur.Role)
 | 
				
			||||||
 | 
							         .GroupBy(static ur => ur.UserGuid, static ur => ur.Role)
 | 
				
			||||||
 | 
							         .ToDictionaryAsync(static group => group.Key, static group => group.ToImmutableArray());
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Task<ImmutableArray<RoleEntity>> GetUserRoles(UserEntity user) {
 | 
				
			||||||
 | 
							return db.Ctx.UserRoles
 | 
				
			||||||
 | 
							         .Include(static ur => ur.Role)
 | 
				
			||||||
 | 
							         .Where(ur => ur.UserGuid == user.UserGuid)
 | 
				
			||||||
 | 
							         .Select(static ur => ur.Role)
 | 
				
			||||||
 | 
							         .AsAsyncEnumerable()
 | 
				
			||||||
 | 
							         .ToImmutableArrayAsync();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Task<ImmutableHashSet<Guid>> GetUserRoleGuids(UserEntity user) {
 | 
				
			||||||
 | 
							return db.Ctx.UserRoles
 | 
				
			||||||
 | 
							         .Where(ur => ur.UserGuid == user.UserGuid)
 | 
				
			||||||
 | 
							         .Select(static ur => ur.RoleGuid)
 | 
				
			||||||
 | 
							         .AsAsyncEnumerable()
 | 
				
			||||||
 | 
							         .ToImmutableSetAsync();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public async Task Add(UserEntity user, RoleEntity role) {
 | 
				
			||||||
 | 
							var userRole = await db.Ctx.UserRoles.FindAsync(user.UserGuid, role.RoleGuid);
 | 
				
			||||||
 | 
							if (userRole == null) {
 | 
				
			||||||
 | 
								db.Ctx.UserRoles.Add(new UserRoleEntity(user.UserGuid, role.RoleGuid));
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public async Task<UserRoleEntity?> Remove(UserEntity user, RoleEntity role) {
 | 
				
			||||||
 | 
							var userRole = await db.Ctx.UserRoles.FindAsync(user.UserGuid, role.RoleGuid);
 | 
				
			||||||
 | 
							if (userRole == null) {
 | 
				
			||||||
 | 
								return null;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							else {
 | 
				
			||||||
 | 
								db.Ctx.UserRoles.Remove(userRole);
 | 
				
			||||||
 | 
								return userRole;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,9 +1,9 @@
 | 
				
			|||||||
namespace Phantom.Controller.Rpc;
 | 
					namespace Phantom.Controller.Rpc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
sealed class RpcClientConnectionClosedEventArgs : EventArgs {
 | 
					public sealed class RpcClientConnectionClosedEventArgs : EventArgs {
 | 
				
			||||||
	public uint RoutingId { get; }
 | 
						internal uint RoutingId { get; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public RpcClientConnectionClosedEventArgs(uint routingId) {
 | 
						internal RpcClientConnectionClosedEventArgs(uint routingId) {
 | 
				
			||||||
		RoutingId = routingId;
 | 
							RoutingId = routingId;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,7 +18,7 @@ public sealed class RpcConnectionToClient<TListener> {
 | 
				
			|||||||
		set => isAuthorized = value;
 | 
							set => isAuthorized = value;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	internal event EventHandler<RpcClientConnectionClosedEventArgs>? Closed;
 | 
						public event EventHandler<RpcClientConnectionClosedEventArgs>? Closed;
 | 
				
			||||||
	private bool isClosed;
 | 
						private bool isClosed;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	internal RpcConnectionToClient(ServerSocket socket, uint routingId, MessageRegistry<TListener> messageRegistry, MessageReplyTracker messageReplyTracker) {
 | 
						internal RpcConnectionToClient(ServerSocket socket, uint routingId, MessageRegistry<TListener> messageRegistry, MessageReplyTracker messageReplyTracker) {
 | 
				
			||||||
@@ -33,12 +33,18 @@ public sealed class RpcConnectionToClient<TListener> {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public void Close() {
 | 
						public void Close() {
 | 
				
			||||||
 | 
							bool hasClosed = false;
 | 
				
			||||||
 | 
							
 | 
				
			||||||
		lock (this) {
 | 
							lock (this) {
 | 
				
			||||||
			if (!isClosed) {
 | 
								if (!isClosed) {
 | 
				
			||||||
				isClosed = true;
 | 
									isClosed = true;
 | 
				
			||||||
				Closed?.Invoke(this, new RpcClientConnectionClosedEventArgs(routingId));
 | 
									hasClosed = true;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (hasClosed) {
 | 
				
			||||||
 | 
								Closed?.Invoke(this, new RpcClientConnectionClosedEventArgs(routingId));
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public async Task Send<TMessage>(TMessage message) where TMessage : IMessage<TListener, NoReply> {
 | 
						public async Task Send<TMessage>(TMessage message) where TMessage : IMessage<TListener, NoReply> {
 | 
				
			||||||
@@ -66,7 +72,7 @@ public sealed class RpcConnectionToClient<TListener> {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		await socket.SendAsync(routingId, bytes);
 | 
							await socket.SendAsync(routingId, bytes);
 | 
				
			||||||
		return await messageReplyTracker.WaitForReply<TReply>(sequenceId, waitForReplyTime, waitForReplyCancellationToken);
 | 
							return await messageReplyTracker.TryWaitForReply<TReply>(sequenceId, waitForReplyTime, waitForReplyCancellationToken);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public void Receive(IReply message) {
 | 
						public void Receive(IReply message) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,7 +11,7 @@ using Phantom.Controller.Services.Instances;
 | 
				
			|||||||
using Phantom.Utils.Collections;
 | 
					using Phantom.Utils.Collections;
 | 
				
			||||||
using Phantom.Utils.Events;
 | 
					using Phantom.Utils.Events;
 | 
				
			||||||
using Phantom.Utils.Tasks;
 | 
					using Phantom.Utils.Tasks;
 | 
				
			||||||
using ILogger = Serilog.ILogger;
 | 
					using Serilog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Phantom.Controller.Services.Agents;
 | 
					namespace Phantom.Controller.Services.Agents;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -27,17 +27,17 @@ public sealed class AgentManager {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	private readonly CancellationToken cancellationToken;
 | 
						private readonly CancellationToken cancellationToken;
 | 
				
			||||||
	private readonly AuthToken authToken;
 | 
						private readonly AuthToken authToken;
 | 
				
			||||||
	private readonly IDatabaseProvider databaseProvider;
 | 
						private readonly IDbContextProvider dbProvider;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public AgentManager(AuthToken authToken, IDatabaseProvider databaseProvider, TaskManager taskManager, CancellationToken cancellationToken) {
 | 
						public AgentManager(AuthToken authToken, IDbContextProvider dbProvider, TaskManager taskManager, CancellationToken cancellationToken) {
 | 
				
			||||||
		this.authToken = authToken;
 | 
							this.authToken = authToken;
 | 
				
			||||||
		this.databaseProvider = databaseProvider;
 | 
							this.dbProvider = dbProvider;
 | 
				
			||||||
		this.cancellationToken = cancellationToken;
 | 
							this.cancellationToken = cancellationToken;
 | 
				
			||||||
		taskManager.Run("Refresh agent status loop", RefreshAgentStatus);
 | 
							taskManager.Run("Refresh agent status loop", RefreshAgentStatus);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	internal async Task Initialize() {
 | 
						internal async Task Initialize() {
 | 
				
			||||||
		await using var ctx = databaseProvider.Provide();
 | 
							await using var ctx = dbProvider.Eager();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		await foreach (var entity in ctx.Agents.AsAsyncEnumerable().WithCancellation(cancellationToken)) {
 | 
							await foreach (var entity in ctx.Agents.AsAsyncEnumerable().WithCancellation(cancellationToken)) {
 | 
				
			||||||
			var agent = new Agent(entity.AgentGuid, entity.Name, entity.ProtocolVersion, entity.BuildVersion, entity.MaxInstances, entity.MaxMemory);
 | 
								var agent = new Agent(entity.AgentGuid, entity.Name, entity.ProtocolVersion, entity.BuildVersion, entity.MaxInstances, entity.MaxMemory);
 | 
				
			||||||
@@ -68,7 +68,7 @@ public sealed class AgentManager {
 | 
				
			|||||||
			oldAgent.Connection?.Close();
 | 
								oldAgent.Connection?.Close();
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		await using (var ctx = databaseProvider.Provide()) {
 | 
							await using (var ctx = dbProvider.Eager()) {
 | 
				
			||||||
			var entity = ctx.AgentUpsert.Fetch(agent.Guid);
 | 
								var entity = ctx.AgentUpsert.Fetch(agent.Guid);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			entity.Name = agent.Name;
 | 
								entity.Name = agent.Name;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +0,0 @@
 | 
				
			|||||||
using Phantom.Common.Data;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Phantom.Controller.Services.Agents;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
public sealed record AgentStats(
 | 
					 | 
				
			||||||
	int RunningInstanceCount,
 | 
					 | 
				
			||||||
	RamAllocationUnits RunningInstanceMemory
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
@@ -1,70 +0,0 @@
 | 
				
			|||||||
using Phantom.Controller.Database.Entities;
 | 
					 | 
				
			||||||
using Phantom.Controller.Database.Enums;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Phantom.Controller.Services.Audit;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
public sealed partial class AuditLog {
 | 
					 | 
				
			||||||
	public Task AddAdministratorUserCreatedEvent(UserEntity administratorUser) {
 | 
					 | 
				
			||||||
		return AddItem(AuditLogEventType.AdministratorUserCreated, administratorUser.UserGuid.ToString());
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public Task AddAdministratorUserModifiedEvent(UserEntity administratorUser) {
 | 
					 | 
				
			||||||
		return AddItem(AuditLogEventType.AdministratorUserModified, administratorUser.UserGuid.ToString());
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public void AddUserLoggedInEvent(UserEntity user) {
 | 
					 | 
				
			||||||
		AddItem(user.UserGuid, AuditLogEventType.UserLoggedIn, user.UserGuid.ToString());
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public void AddUserLoggedOutEvent(Guid userGuid) {
 | 
					 | 
				
			||||||
		AddItem(userGuid, AuditLogEventType.UserLoggedOut, userGuid.ToString());
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	public Task AddUserCreatedEvent(UserEntity user) {
 | 
					 | 
				
			||||||
		return AddItem(AuditLogEventType.UserCreated, user.UserGuid.ToString());
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public Task AddUserRolesChangedEvent(UserEntity user, List<string> addedToRoles, List<string> removedFromRoles) {
 | 
					 | 
				
			||||||
		var extra = new Dictionary<string, object?>();
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		if (addedToRoles.Count > 0) {
 | 
					 | 
				
			||||||
			extra["addedToRoles"] = addedToRoles;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		if (removedFromRoles.Count > 0) {
 | 
					 | 
				
			||||||
			extra["removedFromRoles"] = removedFromRoles;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		return AddItem(AuditLogEventType.UserRolesChanged, user.UserGuid.ToString(), extra);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	public Task AddUserDeletedEvent(UserEntity user) {
 | 
					 | 
				
			||||||
		return AddItem(AuditLogEventType.UserDeleted, user.UserGuid.ToString(), new Dictionary<string, object?> {
 | 
					 | 
				
			||||||
			{ "username", user.Name }
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public Task AddInstanceCreatedEvent(Guid instanceGuid) {
 | 
					 | 
				
			||||||
		return AddItem(AuditLogEventType.InstanceCreated, instanceGuid.ToString());
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public Task AddInstanceEditedEvent(Guid instanceGuid) {
 | 
					 | 
				
			||||||
		return AddItem(AuditLogEventType.InstanceEdited, instanceGuid.ToString());
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	public Task AddInstanceLaunchedEvent(Guid instanceGuid) {
 | 
					 | 
				
			||||||
		return AddItem(AuditLogEventType.InstanceLaunched, instanceGuid.ToString());
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public Task AddInstanceCommandExecutedEvent(Guid instanceGuid, string command) {
 | 
					 | 
				
			||||||
		return AddItem(AuditLogEventType.InstanceCommandExecuted, instanceGuid.ToString(), new Dictionary<string, object?> {
 | 
					 | 
				
			||||||
			{ "command", command }
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public Task AddInstanceStoppedEvent(Guid instanceGuid, int stopInSeconds) {
 | 
					 | 
				
			||||||
		return AddItem(AuditLogEventType.InstanceStopped, instanceGuid.ToString(), new Dictionary<string, object?> {
 | 
					 | 
				
			||||||
			{ "stop_in_seconds", stopInSeconds.ToString() }
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,49 +0,0 @@
 | 
				
			|||||||
using Microsoft.EntityFrameworkCore;
 | 
					 | 
				
			||||||
using Phantom.Controller.Database;
 | 
					 | 
				
			||||||
using Phantom.Controller.Database.Entities;
 | 
					 | 
				
			||||||
using Phantom.Controller.Database.Enums;
 | 
					 | 
				
			||||||
using Phantom.Utils.Tasks;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Phantom.Controller.Services.Audit;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
public sealed partial class AuditLog {
 | 
					 | 
				
			||||||
	private readonly IDatabaseProvider databaseProvider;
 | 
					 | 
				
			||||||
	private readonly TaskManager taskManager;
 | 
					 | 
				
			||||||
	private readonly CancellationToken cancellationToken;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public AuditLog(IDatabaseProvider databaseProvider, TaskManager taskManager, CancellationToken cancellationToken) {
 | 
					 | 
				
			||||||
		this.databaseProvider = databaseProvider;
 | 
					 | 
				
			||||||
		this.taskManager = taskManager;
 | 
					 | 
				
			||||||
		this.cancellationToken = cancellationToken;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	private Task<Guid?> GetCurrentAuthenticatedUserId() {
 | 
					 | 
				
			||||||
		return Task.FromResult<Guid?>(null); // TODO
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	private async Task AddEntityToDatabase(AuditLogEntity logEntity) {
 | 
					 | 
				
			||||||
		await using var ctx = databaseProvider.Provide();
 | 
					 | 
				
			||||||
		ctx.AuditLog.Add(logEntity);
 | 
					 | 
				
			||||||
		await ctx.SaveChangesAsync(cancellationToken);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	private void AddItem(Guid? userGuid, AuditLogEventType eventType, string subjectId, Dictionary<string, object?>? extra = null) {
 | 
					 | 
				
			||||||
		var logEntity = new AuditLogEntity(userGuid, eventType, subjectId, extra);
 | 
					 | 
				
			||||||
		taskManager.Run("Store audit log item to database", () => AddEntityToDatabase(logEntity));
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	private async Task AddItem(AuditLogEventType eventType, string subjectId, Dictionary<string, object?>? extra = null) {
 | 
					 | 
				
			||||||
		AddItem(await GetCurrentAuthenticatedUserId(), eventType, subjectId, extra);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public async Task<AuditLogItem[]> GetItems(int count, CancellationToken cancellationToken) {
 | 
					 | 
				
			||||||
		await using var ctx = databaseProvider.Provide();
 | 
					 | 
				
			||||||
		return await ctx.AuditLog
 | 
					 | 
				
			||||||
		                .Include(static entity => entity.User)
 | 
					 | 
				
			||||||
		                .AsQueryable()
 | 
					 | 
				
			||||||
		                .OrderByDescending(static entity => entity.UtcTime)
 | 
					 | 
				
			||||||
		                .Take(count)
 | 
					 | 
				
			||||||
		                .Select(static entity => new AuditLogItem(entity.UtcTime, entity.UserGuid, entity.User == null ? null : entity.User.Name, entity.EventType, entity.SubjectType, entity.SubjectId, entity.Data))
 | 
					 | 
				
			||||||
		                .ToArrayAsync(cancellationToken);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,6 +0,0 @@
 | 
				
			|||||||
using System.Text.Json;
 | 
					 | 
				
			||||||
using Phantom.Controller.Database.Enums;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Phantom.Controller.Services.Audit;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
public sealed record AuditLogItem(DateTime UtcTime, Guid? UserGuid, string? UserName, AuditLogEventType EventType, AuditLogSubjectType SubjectType, string? SubjectId, JsonDocument? Data);
 | 
					 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
using Phantom.Common.Data.Agent;
 | 
					using Phantom.Common.Data;
 | 
				
			||||||
using Phantom.Common.Logging;
 | 
					using Phantom.Common.Logging;
 | 
				
			||||||
using Phantom.Common.Messages.Agent;
 | 
					using Phantom.Common.Messages.Agent;
 | 
				
			||||||
using Phantom.Common.Messages.Web;
 | 
					using Phantom.Common.Messages.Web;
 | 
				
			||||||
@@ -10,8 +10,6 @@ using Phantom.Controller.Services.Events;
 | 
				
			|||||||
using Phantom.Controller.Services.Instances;
 | 
					using Phantom.Controller.Services.Instances;
 | 
				
			||||||
using Phantom.Controller.Services.Rpc;
 | 
					using Phantom.Controller.Services.Rpc;
 | 
				
			||||||
using Phantom.Controller.Services.Users;
 | 
					using Phantom.Controller.Services.Users;
 | 
				
			||||||
using Phantom.Controller.Services.Users.Permissions;
 | 
					 | 
				
			||||||
using Phantom.Controller.Services.Users.Roles;
 | 
					 | 
				
			||||||
using Phantom.Utils.Tasks;
 | 
					using Phantom.Utils.Tasks;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Phantom.Controller.Services;
 | 
					namespace Phantom.Controller.Services;
 | 
				
			||||||
@@ -25,31 +23,39 @@ public sealed class ControllerServices {
 | 
				
			|||||||
	private EventLog EventLog { get; }
 | 
						private EventLog EventLog { get; }
 | 
				
			||||||
	private InstanceManager InstanceManager { get; }
 | 
						private InstanceManager InstanceManager { get; }
 | 
				
			||||||
	private InstanceLogManager InstanceLogManager { get; }
 | 
						private InstanceLogManager InstanceLogManager { get; }
 | 
				
			||||||
 | 
						private EventLogManager EventLogManager { get; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private UserManager UserManager { get; }
 | 
						private UserManager UserManager { get; }
 | 
				
			||||||
	private RoleManager RoleManager { get; }
 | 
						private RoleManager RoleManager { get; }
 | 
				
			||||||
	private UserRoleManager UserRoleManager { get; }
 | 
					 | 
				
			||||||
	private PermissionManager PermissionManager { get; }
 | 
						private PermissionManager PermissionManager { get; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private readonly IDatabaseProvider databaseProvider;
 | 
						private UserLoginManager UserLoginManager { get; }
 | 
				
			||||||
 | 
						private AuditLogManager AuditLogManager { get; }
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						private readonly IDbContextProvider dbProvider;
 | 
				
			||||||
 | 
						private readonly AuthToken webAuthToken;
 | 
				
			||||||
	private readonly CancellationToken cancellationToken;
 | 
						private readonly CancellationToken cancellationToken;
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	public ControllerServices(IDatabaseProvider databaseProvider, AuthToken agentAuthToken, CancellationToken shutdownCancellationToken) {
 | 
						public ControllerServices(IDbContextProvider dbProvider, AuthToken agentAuthToken, AuthToken webAuthToken, CancellationToken shutdownCancellationToken) {
 | 
				
			||||||
		this.TaskManager = new TaskManager(PhantomLogger.Create<TaskManager, ControllerServices>());
 | 
							this.TaskManager = new TaskManager(PhantomLogger.Create<TaskManager, ControllerServices>());
 | 
				
			||||||
		this.MinecraftVersions = new MinecraftVersions();
 | 
							this.MinecraftVersions = new MinecraftVersions();
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		this.AgentManager = new AgentManager(agentAuthToken, databaseProvider, TaskManager, shutdownCancellationToken);
 | 
							this.AgentManager = new AgentManager(agentAuthToken, dbProvider, TaskManager, shutdownCancellationToken);
 | 
				
			||||||
		this.AgentJavaRuntimesManager = new AgentJavaRuntimesManager();
 | 
							this.AgentJavaRuntimesManager = new AgentJavaRuntimesManager();
 | 
				
			||||||
		this.EventLog = new EventLog(databaseProvider, TaskManager, shutdownCancellationToken);
 | 
							this.EventLog = new EventLog(dbProvider, TaskManager, shutdownCancellationToken);
 | 
				
			||||||
		this.InstanceManager = new InstanceManager(AgentManager, MinecraftVersions, databaseProvider, shutdownCancellationToken);
 | 
							this.InstanceManager = new InstanceManager(AgentManager, MinecraftVersions, dbProvider, shutdownCancellationToken);
 | 
				
			||||||
		this.InstanceLogManager = new InstanceLogManager();
 | 
							this.InstanceLogManager = new InstanceLogManager();
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		this.UserManager = new UserManager(databaseProvider);
 | 
							this.UserManager = new UserManager(dbProvider);
 | 
				
			||||||
		this.RoleManager = new RoleManager(databaseProvider);
 | 
							this.RoleManager = new RoleManager(dbProvider);
 | 
				
			||||||
		this.UserRoleManager = new UserRoleManager(databaseProvider);
 | 
							this.PermissionManager = new PermissionManager(dbProvider);
 | 
				
			||||||
		this.PermissionManager = new PermissionManager(databaseProvider);
 | 
					 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		this.databaseProvider = databaseProvider;
 | 
							this.UserLoginManager = new UserLoginManager(UserManager, PermissionManager);
 | 
				
			||||||
 | 
							this.AuditLogManager = new AuditLogManager(dbProvider);
 | 
				
			||||||
 | 
							this.EventLogManager = new EventLogManager(dbProvider);
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							this.dbProvider = dbProvider;
 | 
				
			||||||
 | 
							this.webAuthToken = webAuthToken;
 | 
				
			||||||
		this.cancellationToken = shutdownCancellationToken;
 | 
							this.cancellationToken = shutdownCancellationToken;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -58,11 +64,11 @@ public sealed class ControllerServices {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public WebMessageListener CreateWebMessageListener(RpcConnectionToClient<IMessageToWebListener> connection) {
 | 
						public WebMessageListener CreateWebMessageListener(RpcConnectionToClient<IMessageToWebListener> connection) {
 | 
				
			||||||
		return new WebMessageListener(connection);
 | 
							return new WebMessageListener(connection, webAuthToken, UserManager, UserLoginManager, AuditLogManager, AgentManager, AgentJavaRuntimesManager, InstanceManager, MinecraftVersions, EventLogManager, TaskManager);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public async Task Initialize() {
 | 
						public async Task Initialize() {
 | 
				
			||||||
		await DatabaseMigrator.Run(databaseProvider, cancellationToken);
 | 
							await DatabaseMigrator.Run(dbProvider, cancellationToken);
 | 
				
			||||||
		await PermissionManager.Initialize();
 | 
							await PermissionManager.Initialize();
 | 
				
			||||||
		await RoleManager.Initialize();
 | 
							await RoleManager.Initialize();
 | 
				
			||||||
		await AgentManager.Initialize();
 | 
							await AgentManager.Initialize();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
using Phantom.Common.Data.Backups;
 | 
					using Phantom.Common.Data.Backups;
 | 
				
			||||||
using Phantom.Common.Data.Instance;
 | 
					using Phantom.Common.Data.Instance;
 | 
				
			||||||
using Phantom.Controller.Database.Enums;
 | 
					using Phantom.Common.Data.Web.EventLog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Phantom.Controller.Services.Events;
 | 
					namespace Phantom.Controller.Services.Events;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,26 +1,26 @@
 | 
				
			|||||||
using System.Collections.Immutable;
 | 
					using System.Collections.Immutable;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.EventLog;
 | 
				
			||||||
using Phantom.Controller.Database;
 | 
					using Phantom.Controller.Database;
 | 
				
			||||||
using Phantom.Controller.Database.Entities;
 | 
					using Phantom.Controller.Database.Entities;
 | 
				
			||||||
using Phantom.Controller.Database.Enums;
 | 
					 | 
				
			||||||
using Phantom.Utils.Collections;
 | 
					using Phantom.Utils.Collections;
 | 
				
			||||||
using Phantom.Utils.Tasks;
 | 
					using Phantom.Utils.Tasks;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Phantom.Controller.Services.Events;
 | 
					namespace Phantom.Controller.Services.Events;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public sealed partial class EventLog {
 | 
					public sealed partial class EventLog {
 | 
				
			||||||
	private readonly IDatabaseProvider databaseProvider;
 | 
						private readonly IDbContextProvider dbProvider;
 | 
				
			||||||
	private readonly TaskManager taskManager;
 | 
						private readonly TaskManager taskManager;
 | 
				
			||||||
	private readonly CancellationToken cancellationToken;
 | 
						private readonly CancellationToken cancellationToken;
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	public EventLog(IDatabaseProvider databaseProvider, TaskManager taskManager, CancellationToken cancellationToken) {
 | 
						public EventLog(IDbContextProvider dbProvider, TaskManager taskManager, CancellationToken cancellationToken) {
 | 
				
			||||||
		this.databaseProvider = databaseProvider;
 | 
							this.dbProvider = dbProvider;
 | 
				
			||||||
		this.taskManager = taskManager;
 | 
							this.taskManager = taskManager;
 | 
				
			||||||
		this.cancellationToken = cancellationToken;
 | 
							this.cancellationToken = cancellationToken;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private async Task AddEntityToDatabase(EventLogEntity logEntity) {
 | 
						private async Task AddEntityToDatabase(EventLogEntity logEntity) {
 | 
				
			||||||
		await using var ctx = databaseProvider.Provide();
 | 
							await using var ctx = dbProvider.Eager();
 | 
				
			||||||
		ctx.EventLog.Add(logEntity);
 | 
							ctx.EventLog.Add(logEntity);
 | 
				
			||||||
		await ctx.SaveChangesAsync(cancellationToken);
 | 
							await ctx.SaveChangesAsync(cancellationToken);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -31,7 +31,7 @@ public sealed partial class EventLog {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public async Task<ImmutableArray<EventLogItem>> GetItems(int count, CancellationToken cancellationToken) {
 | 
						public async Task<ImmutableArray<EventLogItem>> GetItems(int count, CancellationToken cancellationToken) {
 | 
				
			||||||
		await using var ctx = databaseProvider.Provide();
 | 
							await using var ctx = dbProvider.Eager();
 | 
				
			||||||
		return await ctx.EventLog
 | 
							return await ctx.EventLog
 | 
				
			||||||
		                .AsQueryable()
 | 
							                .AsQueryable()
 | 
				
			||||||
		                .OrderByDescending(static entity => entity.UtcTime)
 | 
							                .OrderByDescending(static entity => entity.UtcTime)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +0,0 @@
 | 
				
			|||||||
using System.Text.Json;
 | 
					 | 
				
			||||||
using Phantom.Controller.Database.Enums;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Phantom.Controller.Services.Events;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
public sealed record EventLogItem(DateTime UtcTime, Guid? AgentGuid, EventLogEventType EventType, EventLogSubjectType SubjectType, string SubjectId, JsonDocument? Data);
 | 
					 | 
				
			||||||
@@ -1,23 +0,0 @@
 | 
				
			|||||||
namespace Phantom.Controller.Services.Instances;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
public enum AddOrEditInstanceResult : byte {
 | 
					 | 
				
			||||||
	UnknownError,
 | 
					 | 
				
			||||||
	Success,
 | 
					 | 
				
			||||||
	InstanceNameMustNotBeEmpty,
 | 
					 | 
				
			||||||
	InstanceMemoryMustNotBeZero,
 | 
					 | 
				
			||||||
	MinecraftVersionDownloadInfoNotFound,
 | 
					 | 
				
			||||||
	AgentNotFound
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
public static class AddOrEditInstanceResultExtensions {
 | 
					 | 
				
			||||||
	public static string ToSentence(this AddOrEditInstanceResult reason) {
 | 
					 | 
				
			||||||
		return reason switch {
 | 
					 | 
				
			||||||
			AddOrEditInstanceResult.Success                              => "Success.",
 | 
					 | 
				
			||||||
			AddOrEditInstanceResult.InstanceNameMustNotBeEmpty           => "Instance name must not be empty.",
 | 
					 | 
				
			||||||
			AddOrEditInstanceResult.InstanceMemoryMustNotBeZero          => "Memory must not be 0 MB.",
 | 
					 | 
				
			||||||
			AddOrEditInstanceResult.MinecraftVersionDownloadInfoNotFound => "Could not find download information for the selected Minecraft version.",
 | 
					 | 
				
			||||||
			AddOrEditInstanceResult.AgentNotFound                        => "Agent not found.",
 | 
					 | 
				
			||||||
			_                                                            => "Unknown error."
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -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) {}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -3,7 +3,7 @@ using System.Collections.Immutable;
 | 
				
			|||||||
using Phantom.Common.Logging;
 | 
					using Phantom.Common.Logging;
 | 
				
			||||||
using Phantom.Utils.Collections;
 | 
					using Phantom.Utils.Collections;
 | 
				
			||||||
using Phantom.Utils.Events;
 | 
					using Phantom.Utils.Events;
 | 
				
			||||||
using ILogger = Serilog.ILogger;
 | 
					using Serilog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Phantom.Controller.Services.Instances;
 | 
					namespace Phantom.Controller.Services.Instances;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,20 +4,23 @@ 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.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;
 | 
				
			||||||
using Phantom.Utils.Events;
 | 
					using Phantom.Utils.Events;
 | 
				
			||||||
using ILogger = Serilog.ILogger;
 | 
					using Serilog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Phantom.Controller.Services.Instances;
 | 
					namespace Phantom.Controller.Services.Instances;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public sealed class InstanceManager {
 | 
					sealed class InstanceManager {
 | 
				
			||||||
	private static readonly ILogger Logger = PhantomLogger.Create<InstanceManager>();
 | 
						private static readonly ILogger Logger = PhantomLogger.Create<InstanceManager>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private readonly ObservableInstances instances = new (PhantomLogger.Create<InstanceManager, ObservableInstances>());
 | 
						private readonly ObservableInstances instances = new (PhantomLogger.Create<InstanceManager, ObservableInstances>());
 | 
				
			||||||
@@ -26,20 +29,20 @@ public sealed class InstanceManager {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	private readonly AgentManager agentManager;
 | 
						private readonly AgentManager agentManager;
 | 
				
			||||||
	private readonly MinecraftVersions minecraftVersions;
 | 
						private readonly MinecraftVersions minecraftVersions;
 | 
				
			||||||
	private readonly IDatabaseProvider databaseProvider;
 | 
						private readonly IDbContextProvider dbProvider;
 | 
				
			||||||
	private readonly CancellationToken cancellationToken;
 | 
						private readonly CancellationToken cancellationToken;
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	private readonly SemaphoreSlim modifyInstancesSemaphore = new (1, 1);
 | 
						private readonly SemaphoreSlim modifyInstancesSemaphore = new (1, 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public InstanceManager(AgentManager agentManager, MinecraftVersions minecraftVersions, IDatabaseProvider databaseProvider, CancellationToken cancellationToken) {
 | 
						public InstanceManager(AgentManager agentManager, MinecraftVersions minecraftVersions, IDbContextProvider dbProvider, CancellationToken cancellationToken) {
 | 
				
			||||||
		this.agentManager = agentManager;
 | 
							this.agentManager = agentManager;
 | 
				
			||||||
		this.minecraftVersions = minecraftVersions;
 | 
							this.minecraftVersions = minecraftVersions;
 | 
				
			||||||
		this.databaseProvider = databaseProvider;
 | 
							this.dbProvider = dbProvider;
 | 
				
			||||||
		this.cancellationToken = cancellationToken;
 | 
							this.cancellationToken = cancellationToken;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public async Task Initialize() {
 | 
						public async Task Initialize() {
 | 
				
			||||||
		await using var ctx = databaseProvider.Provide();
 | 
							await using var ctx = dbProvider.Eager();
 | 
				
			||||||
		await foreach (var entity in ctx.Instances.AsAsyncEnumerable().WithCancellation(cancellationToken)) {
 | 
							await foreach (var entity in ctx.Instances.AsAsyncEnumerable().WithCancellation(cancellationToken)) {
 | 
				
			||||||
			var configuration = new InstanceConfiguration(
 | 
								var configuration = new InstanceConfiguration(
 | 
				
			||||||
				entity.AgentGuid,
 | 
									entity.AgentGuid,
 | 
				
			||||||
@@ -54,53 +57,53 @@ public 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(Guid auditLogUserGuid, InstanceConfiguration configuration) {
 | 
				
			||||||
		var agent = agentManager.GetAgent(configuration.AgentGuid);
 | 
							var agent = agentManager.GetAgent(configuration.AgentGuid);
 | 
				
			||||||
		if (agent == null) {
 | 
							if (agent == null) {
 | 
				
			||||||
			return InstanceActionResult.Concrete(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 = databaseProvider.Provide();
 | 
									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;
 | 
				
			||||||
@@ -111,7 +114,15 @@ public 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 auditLogWriter = new AuditLogRepositoryWriter(db, auditLogUserGuid);
 | 
				
			||||||
 | 
									if (isNewInstance) {
 | 
				
			||||||
 | 
										auditLogWriter.AddInstanceCreatedEvent(configuration.InstanceGuid);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									else {
 | 
				
			||||||
 | 
										auditLogWriter.AddInstanceEditedEvent(configuration.InstanceGuid);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									await db.Ctx.SaveChangesAsync(cancellationToken);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			else if (isNewInstance) {
 | 
								else if (isNewInstance) {
 | 
				
			||||||
				instances.ByGuid.Remove(configuration.InstanceGuid);
 | 
									instances.ByGuid.Remove(configuration.InstanceGuid);
 | 
				
			||||||
@@ -120,7 +131,7 @@ public 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);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
@@ -130,10 +141,10 @@ public 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));
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -165,42 +176,50 @@ public sealed class InstanceManager {
 | 
				
			|||||||
		return instances.ByGuid.TryGetValue(instanceGuid, out var instance) ? await SendInstanceActionMessage<TMessage, TReply>(instance, message) : InstanceActionResult.General<TReply>(InstanceActionGeneralResult.InstanceDoesNotExist);
 | 
							return instances.ByGuid.TryGetValue(instanceGuid, out var instance) ? await SendInstanceActionMessage<TMessage, TReply>(instance, message) : InstanceActionResult.General<TReply>(InstanceActionGeneralResult.InstanceDoesNotExist);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public async Task<InstanceActionResult<LaunchInstanceResult>> LaunchInstance(Guid instanceGuid) {
 | 
						public async Task<InstanceActionResult<LaunchInstanceResult>> LaunchInstance(Guid auditLogUserGuid, Guid instanceGuid) {
 | 
				
			||||||
		var result = await SendInstanceActionMessage<LaunchInstanceMessage, LaunchInstanceResult>(instanceGuid, new LaunchInstanceMessage(instanceGuid));
 | 
							var result = await SendInstanceActionMessage<LaunchInstanceMessage, LaunchInstanceResult>(instanceGuid, new LaunchInstanceMessage(instanceGuid));
 | 
				
			||||||
		if (result.Is(LaunchInstanceResult.LaunchInitiated)) {
 | 
							if (result.Is(LaunchInstanceResult.LaunchInitiated)) {
 | 
				
			||||||
			await SetInstanceShouldLaunchAutomatically(instanceGuid, true);
 | 
								await HandleInstanceManuallyLaunchedOrStopped(instanceGuid, true, auditLogUserGuid, auditLogRepository => auditLogRepository.AddInstanceLaunchedEvent(instanceGuid));
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return result;
 | 
							return result;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public async Task<InstanceActionResult<StopInstanceResult>> StopInstance(Guid instanceGuid, MinecraftStopStrategy stopStrategy) {
 | 
						public async Task<InstanceActionResult<StopInstanceResult>> StopInstance(Guid auditLogUserGuid, Guid instanceGuid, MinecraftStopStrategy stopStrategy) {
 | 
				
			||||||
		var result = await SendInstanceActionMessage<StopInstanceMessage, StopInstanceResult>(instanceGuid, new StopInstanceMessage(instanceGuid, stopStrategy));
 | 
							var result = await SendInstanceActionMessage<StopInstanceMessage, StopInstanceResult>(instanceGuid, new StopInstanceMessage(instanceGuid, stopStrategy));
 | 
				
			||||||
		if (result.Is(StopInstanceResult.StopInitiated)) {
 | 
							if (result.Is(StopInstanceResult.StopInitiated)) {
 | 
				
			||||||
			await SetInstanceShouldLaunchAutomatically(instanceGuid, false);
 | 
								await HandleInstanceManuallyLaunchedOrStopped(instanceGuid, false, auditLogUserGuid, auditLogRepository => auditLogRepository.AddInstanceStoppedEvent(instanceGuid, stopStrategy.Seconds));
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return result;
 | 
							return result;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private async Task SetInstanceShouldLaunchAutomatically(Guid instanceGuid, bool shouldLaunchAutomatically) {
 | 
						private async Task HandleInstanceManuallyLaunchedOrStopped(Guid instanceGuid, bool wasLaunched, Guid auditLogUserGuid, Action<AuditLogRepositoryWriter> addAuditEvent) {
 | 
				
			||||||
		await modifyInstancesSemaphore.WaitAsync(cancellationToken);
 | 
							await modifyInstancesSemaphore.WaitAsync(cancellationToken);
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			instances.ByGuid.TryReplace(instanceGuid, instance => instance with { LaunchAutomatically = shouldLaunchAutomatically });
 | 
								instances.ByGuid.TryReplace(instanceGuid, instance => instance with { LaunchAutomatically = wasLaunched });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			await using var ctx = databaseProvider.Provide();
 | 
								await using var db = dbProvider.Lazy();
 | 
				
			||||||
			var entity = await ctx.Instances.FindAsync(instanceGuid, cancellationToken);
 | 
								var entity = await db.Ctx.Instances.FindAsync(new object[] { instanceGuid }, cancellationToken);
 | 
				
			||||||
			if (entity != null) {
 | 
								if (entity != null) {
 | 
				
			||||||
				entity.LaunchAutomatically = shouldLaunchAutomatically;
 | 
									entity.LaunchAutomatically = wasLaunched;
 | 
				
			||||||
				await ctx.SaveChangesAsync(cancellationToken);
 | 
									addAuditEvent(new AuditLogRepositoryWriter(db, auditLogUserGuid));
 | 
				
			||||||
 | 
									await db.Ctx.SaveChangesAsync(cancellationToken);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		} finally {
 | 
							} finally {
 | 
				
			||||||
			modifyInstancesSemaphore.Release();
 | 
								modifyInstancesSemaphore.Release();
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public async Task<InstanceActionResult<SendCommandToInstanceResult>> SendCommand(Guid instanceGuid, string command) {
 | 
						public async Task<InstanceActionResult<SendCommandToInstanceResult>> SendCommand(Guid auditLogUserId, Guid instanceGuid, string command) {
 | 
				
			||||||
		return await SendInstanceActionMessage<SendCommandToInstanceMessage, SendCommandToInstanceResult>(instanceGuid, new SendCommandToInstanceMessage(instanceGuid, command));
 | 
							var result = await SendInstanceActionMessage<SendCommandToInstanceMessage, SendCommandToInstanceResult>(instanceGuid, new SendCommandToInstanceMessage(instanceGuid, command));
 | 
				
			||||||
 | 
							if (result.Is(SendCommandToInstanceResult.Success)) {
 | 
				
			||||||
 | 
								await using var db = dbProvider.Lazy();
 | 
				
			||||||
 | 
								new AuditLogRepositoryWriter(db, auditLogUserId).AddInstanceCommandExecutedEvent(instanceGuid, command);
 | 
				
			||||||
 | 
								await db.Ctx.SaveChangesAsync(cancellationToken);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return result;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	internal async Task<ImmutableArray<ConfigureInstanceMessage>> GetInstanceConfigurationsForAgent(Guid agentGuid) {
 | 
						internal async Task<ImmutableArray<ConfigureInstanceMessage>> GetInstanceConfigurationsForAgent(Guid agentGuid) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,6 +11,7 @@
 | 
				
			|||||||
  
 | 
					  
 | 
				
			||||||
  <ItemGroup>
 | 
					  <ItemGroup>
 | 
				
			||||||
    <ProjectReference Include="..\..\Common\Phantom.Common.Data\Phantom.Common.Data.csproj" />
 | 
					    <ProjectReference Include="..\..\Common\Phantom.Common.Data\Phantom.Common.Data.csproj" />
 | 
				
			||||||
 | 
					    <ProjectReference Include="..\..\Common\Phantom.Common.Data.Web\Phantom.Common.Data.Web.csproj" />
 | 
				
			||||||
    <ProjectReference Include="..\..\Utils\Phantom.Utils.Events\Phantom.Utils.Events.csproj" />
 | 
					    <ProjectReference Include="..\..\Utils\Phantom.Utils.Events\Phantom.Utils.Events.csproj" />
 | 
				
			||||||
    <ProjectReference Include="..\Phantom.Controller.Database\Phantom.Controller.Database.csproj" />
 | 
					    <ProjectReference Include="..\Phantom.Controller.Database\Phantom.Controller.Database.csproj" />
 | 
				
			||||||
    <ProjectReference Include="..\Phantom.Controller.Minecraft\Phantom.Controller.Minecraft.csproj" />
 | 
					    <ProjectReference Include="..\Phantom.Controller.Minecraft\Phantom.Controller.Minecraft.csproj" />
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,15 +1,138 @@
 | 
				
			|||||||
using Phantom.Common.Messages.Web;
 | 
					using System.Collections.Immutable;
 | 
				
			||||||
 | 
					using Phantom.Common.Data;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Java;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Minecraft;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Replies;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.Agent;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.AuditLog;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.Instance;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.Users;
 | 
				
			||||||
 | 
					using Phantom.Common.Logging;
 | 
				
			||||||
 | 
					using Phantom.Common.Messages.Web;
 | 
				
			||||||
using Phantom.Common.Messages.Web.BiDirectional;
 | 
					using Phantom.Common.Messages.Web.BiDirectional;
 | 
				
			||||||
 | 
					using Phantom.Common.Messages.Web.ToController;
 | 
				
			||||||
 | 
					using Phantom.Common.Messages.Web.ToWeb;
 | 
				
			||||||
 | 
					using Phantom.Controller.Minecraft;
 | 
				
			||||||
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.Utils.Rpc.Message;
 | 
					using Phantom.Utils.Rpc.Message;
 | 
				
			||||||
 | 
					using Phantom.Utils.Tasks;
 | 
				
			||||||
 | 
					using Serilog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Phantom.Controller.Services.Rpc;
 | 
					namespace Phantom.Controller.Services.Rpc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public sealed class WebMessageListener : IMessageToControllerListener {
 | 
					public sealed class WebMessageListener : IMessageToControllerListener {
 | 
				
			||||||
	private readonly RpcConnectionToClient<IMessageToWebListener> connection;
 | 
						private static readonly ILogger Logger = PhantomLogger.Create<WebMessageListener>();
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	internal WebMessageListener(RpcConnectionToClient<IMessageToWebListener> connection) {
 | 
						private readonly RpcConnectionToClient<IMessageToWebListener> connection;
 | 
				
			||||||
 | 
						private readonly AuthToken authToken;
 | 
				
			||||||
 | 
						private readonly UserManager userManager;
 | 
				
			||||||
 | 
						private readonly UserLoginManager userLoginManager;
 | 
				
			||||||
 | 
						private readonly AuditLogManager auditLogManager;
 | 
				
			||||||
 | 
						private readonly AgentManager agentManager;
 | 
				
			||||||
 | 
						private readonly AgentJavaRuntimesManager agentJavaRuntimesManager;
 | 
				
			||||||
 | 
						private readonly InstanceManager instanceManager;
 | 
				
			||||||
 | 
						private readonly MinecraftVersions minecraftVersions;
 | 
				
			||||||
 | 
						private readonly TaskManager taskManager;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						internal WebMessageListener(
 | 
				
			||||||
 | 
							RpcConnectionToClient<IMessageToWebListener> connection,
 | 
				
			||||||
 | 
							AuthToken authToken,
 | 
				
			||||||
 | 
							UserManager userManager,
 | 
				
			||||||
 | 
							UserLoginManager userLoginManager,
 | 
				
			||||||
 | 
							AuditLogManager auditLogManager,
 | 
				
			||||||
 | 
							AgentManager agentManager,
 | 
				
			||||||
 | 
							AgentJavaRuntimesManager agentJavaRuntimesManager,
 | 
				
			||||||
 | 
							InstanceManager instanceManager,
 | 
				
			||||||
 | 
							MinecraftVersions minecraftVersions,
 | 
				
			||||||
 | 
							TaskManager taskManager
 | 
				
			||||||
 | 
						) {
 | 
				
			||||||
		this.connection = connection;
 | 
							this.connection = connection;
 | 
				
			||||||
 | 
							this.connection.Closed += OnConnectionClosed;
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							this.authToken = authToken;
 | 
				
			||||||
 | 
							this.userManager = userManager;
 | 
				
			||||||
 | 
							this.userLoginManager = userLoginManager;
 | 
				
			||||||
 | 
							this.auditLogManager = auditLogManager;
 | 
				
			||||||
 | 
							this.agentManager = agentManager;
 | 
				
			||||||
 | 
							this.agentJavaRuntimesManager = agentJavaRuntimesManager;
 | 
				
			||||||
 | 
							this.instanceManager = instanceManager;
 | 
				
			||||||
 | 
							this.minecraftVersions = minecraftVersions;
 | 
				
			||||||
 | 
							this.taskManager = taskManager;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private void OnConnectionClosed(object? sender, RpcClientConnectionClosedEventArgs e) {
 | 
				
			||||||
 | 
							agentManager.AgentsChanged.Unsubscribe(this);
 | 
				
			||||||
 | 
							instanceManager.InstancesChanged.Unsubscribe(this);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public async Task<NoReply> HandleRegisterWeb(RegisterWebMessage message) {
 | 
				
			||||||
 | 
							if (authToken.FixedTimeEquals(message.AuthToken)) {
 | 
				
			||||||
 | 
								Logger.Information("Web authorized successfully.");
 | 
				
			||||||
 | 
								connection.IsAuthorized = true;
 | 
				
			||||||
 | 
								await connection.Send(new RegisterWebResultMessage(true));
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							else {
 | 
				
			||||||
 | 
								Logger.Warning("Web failed to authorize, invalid token.");
 | 
				
			||||||
 | 
								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) {
 | 
				
			||||||
 | 
							return userManager.CreateOrUpdateAdministrator(message.Username, message.Password);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Task<InstanceActionResult<CreateOrUpdateInstanceResult>> HandleCreateOrUpdateInstance(CreateOrUpdateInstanceMessage message) {
 | 
				
			||||||
 | 
							return instanceManager.CreateOrUpdateInstance( message.LoggedInUserGuid, message.Configuration);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Task<InstanceActionResult<LaunchInstanceResult>> HandleLaunchInstance(LaunchInstanceMessage message) {
 | 
				
			||||||
 | 
							return instanceManager.LaunchInstance(message.LoggedInUserGuid, message.InstanceGuid);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Task<InstanceActionResult<StopInstanceResult>> HandleStopInstance(StopInstanceMessage message) {
 | 
				
			||||||
 | 
							return instanceManager.StopInstance(message.LoggedInUserGuid, message.InstanceGuid, message.StopStrategy);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Task<InstanceActionResult<SendCommandToInstanceResult>> HandleSendCommandToInstance(SendCommandToInstanceMessage message) {
 | 
				
			||||||
 | 
							return instanceManager.SendCommand(message.LoggedInUserGuid, message.InstanceGuid, message.Command);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Task<ImmutableArray<MinecraftVersion>> HandleGetMinecraftVersions(GetMinecraftVersionsMessage message) {
 | 
				
			||||||
 | 
							return minecraftVersions.GetVersions(CancellationToken.None);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Task<ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>> HandleGetAgentJavaRuntimes(GetAgentJavaRuntimesMessage message) {
 | 
				
			||||||
 | 
							return Task.FromResult(agentJavaRuntimesManager.All);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Task<ImmutableArray<UserInfo>> HandleGetUsers(GetUsersMessage message) {
 | 
				
			||||||
 | 
							return userManager.GetAll();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Task<ImmutableArray<AuditLogItem>> HandleGetAuditLog(GetAuditLogMessage message) {
 | 
				
			||||||
 | 
							return auditLogManager.GetMostRecentItems(message.Count);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Task<LogInSuccess?> HandleLogIn(LogInMessage message) {
 | 
				
			||||||
 | 
							return userLoginManager.LogIn(message.Username, message.Password);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public Task<NoReply> HandleReply(ReplyMessage message) {
 | 
						public Task<NoReply> HandleReply(ReplyMessage message) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,17 +0,0 @@
 | 
				
			|||||||
using System.Collections.Immutable;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Phantom.Controller.Services.Users;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
public abstract record AddUserError {
 | 
					 | 
				
			||||||
	private AddUserError() {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public sealed record NameIsEmpty : AddUserError;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public sealed record NameIsTooLong(int MaximumLength) : AddUserError;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public sealed record NameAlreadyExists : AddUserError;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public sealed record PasswordIsInvalid(ImmutableArray<PasswordRequirementViolation> Violations) : AddUserError;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public sealed record UnknownError : AddUserError;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					using System.Collections.Immutable;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.AuditLog;
 | 
				
			||||||
 | 
					using Phantom.Controller.Database;
 | 
				
			||||||
 | 
					using Phantom.Controller.Database.Repositories;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Controller.Services.Users; 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					sealed class AuditLogManager {
 | 
				
			||||||
 | 
						private readonly IDbContextProvider dbProvider;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public AuditLogManager(IDbContextProvider dbProvider) {
 | 
				
			||||||
 | 
							this.dbProvider = dbProvider;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public async Task<ImmutableArray<AuditLogItem>> GetMostRecentItems(int count) {
 | 
				
			||||||
 | 
							await using var db = dbProvider.Lazy();
 | 
				
			||||||
 | 
							return await new AuditLogRepositoryReader(db).GetMostRecentItems(count, CancellationToken.None);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,13 +0,0 @@
 | 
				
			|||||||
namespace Phantom.Controller.Services.Users;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
public abstract record PasswordRequirementViolation {
 | 
					 | 
				
			||||||
	private PasswordRequirementViolation() {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public sealed record TooShort(int MinimumLength) : PasswordRequirementViolation;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public sealed record LowercaseLetterRequired : PasswordRequirementViolation;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public sealed record UppercaseLetterRequired : PasswordRequirementViolation;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public sealed record DigitRequired : PasswordRequirementViolation;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -0,0 +1,70 @@
 | 
				
			|||||||
 | 
					using System.Collections.Immutable;
 | 
				
			||||||
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.Users;
 | 
				
			||||||
 | 
					using Phantom.Common.Logging;
 | 
				
			||||||
 | 
					using Phantom.Controller.Database;
 | 
				
			||||||
 | 
					using Phantom.Controller.Database.Entities;
 | 
				
			||||||
 | 
					using Phantom.Utils.Collections;
 | 
				
			||||||
 | 
					using Serilog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Controller.Services.Users;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					sealed class PermissionManager {
 | 
				
			||||||
 | 
						private static readonly ILogger Logger = PhantomLogger.Create<PermissionManager>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private readonly IDbContextProvider dbProvider;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public PermissionManager(IDbContextProvider dbProvider) {
 | 
				
			||||||
 | 
							this.dbProvider = dbProvider;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public async Task Initialize() {
 | 
				
			||||||
 | 
							Logger.Information("Adding default permissions to database.");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							await using var ctx = dbProvider.Eager();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var existingPermissionIds = await ctx.Permissions.Select(static p => p.Id).AsAsyncEnumerable().ToImmutableSetAsync();
 | 
				
			||||||
 | 
							var missingPermissionIds = GetMissingPermissionsOrdered(Permission.All, existingPermissionIds);
 | 
				
			||||||
 | 
							if (!missingPermissionIds.IsEmpty) {
 | 
				
			||||||
 | 
								Logger.Information("Adding default permissions: {Permissions}", string.Join(", ", missingPermissionIds));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								foreach (var permissionId in missingPermissionIds) {
 | 
				
			||||||
 | 
									ctx.Permissions.Add(new PermissionEntity(permissionId));
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								await ctx.SaveChangesAsync();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public async Task<PermissionSet> FetchPermissionsForAllUsers(Guid userId) {
 | 
				
			||||||
 | 
							await using var ctx = dbProvider.Eager();
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							var userPermissions = ctx.UserPermissions
 | 
				
			||||||
 | 
							                         .Where(up => up.UserGuid == userId)
 | 
				
			||||||
 | 
							                         .Select(static up => up.PermissionId);
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							var rolePermissions = ctx.UserRoles
 | 
				
			||||||
 | 
							                         .Where(ur => ur.UserGuid == userId)
 | 
				
			||||||
 | 
							                         .Join(ctx.RolePermissions, static ur => ur.RoleGuid, static rp => rp.RoleGuid, static (ur, rp) => rp.PermissionId);
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							return new PermissionSet(await userPermissions.Union(rolePermissions).AsAsyncEnumerable().ToImmutableSetAsync());
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						public async Task<PermissionSet> FetchPermissionsForUserId(Guid userId) {
 | 
				
			||||||
 | 
							await using var ctx = dbProvider.Eager();
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							var userPermissions = ctx.UserPermissions
 | 
				
			||||||
 | 
							                         .Where(up => up.UserGuid == userId)
 | 
				
			||||||
 | 
							                         .Select(static up => up.PermissionId);
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							var rolePermissions = ctx.UserRoles
 | 
				
			||||||
 | 
							                         .Where(ur => ur.UserGuid == userId)
 | 
				
			||||||
 | 
							                         .Join(ctx.RolePermissions, static ur => ur.RoleGuid, static rp => rp.RoleGuid, static (ur, rp) => rp.PermissionId);
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							return new PermissionSet(await userPermissions.Union(rolePermissions).AsAsyncEnumerable().ToImmutableSetAsync());
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public static ImmutableArray<string> GetMissingPermissionsOrdered(IEnumerable<Permission> allPermissions, ImmutableHashSet<string> existingPermissionIds) {
 | 
				
			||||||
 | 
							return allPermissions.Select(static permission => permission.Id).Except(existingPermissionIds).Order().ToImmutableArray();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,29 +0,0 @@
 | 
				
			|||||||
using System.Collections.Immutable;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Phantom.Controller.Services.Users.Permissions;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
public sealed class IdentityPermissions {
 | 
					 | 
				
			||||||
	internal static IdentityPermissions None { get; } = new ();
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	private readonly ImmutableHashSet<string> permissionIds;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	internal IdentityPermissions(IQueryable<string> permissionIdsQuery) {
 | 
					 | 
				
			||||||
		this.permissionIds = permissionIdsQuery.ToImmutableHashSet();
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	private IdentityPermissions() {
 | 
					 | 
				
			||||||
		this.permissionIds = ImmutableHashSet<string>.Empty;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public bool Check(Permission? permission) {
 | 
					 | 
				
			||||||
		while (permission != null) {
 | 
					 | 
				
			||||||
			if (!permissionIds.Contains(permission.Id)) {
 | 
					 | 
				
			||||||
				return false;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			permission = permission.Parent;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return true;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,68 +0,0 @@
 | 
				
			|||||||
using System.Collections.Immutable;
 | 
					 | 
				
			||||||
using System.Security.Claims;
 | 
					 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					 | 
				
			||||||
using Phantom.Common.Logging;
 | 
					 | 
				
			||||||
using Phantom.Controller.Database;
 | 
					 | 
				
			||||||
using Phantom.Controller.Database.Entities;
 | 
					 | 
				
			||||||
using Phantom.Utils.Collections;
 | 
					 | 
				
			||||||
using ILogger = Serilog.ILogger;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Phantom.Controller.Services.Users.Permissions;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
public sealed class PermissionManager {
 | 
					 | 
				
			||||||
	private static readonly ILogger Logger = PhantomLogger.Create<PermissionManager>();
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	private readonly IDatabaseProvider databaseProvider;
 | 
					 | 
				
			||||||
	private readonly Dictionary<Guid, IdentityPermissions> userIdsToPermissionIds = new ();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public PermissionManager(IDatabaseProvider databaseProvider) {
 | 
					 | 
				
			||||||
		this.databaseProvider = databaseProvider;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	internal async Task Initialize() {
 | 
					 | 
				
			||||||
		Logger.Information("Adding default permissions to database.");
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		await using var ctx = databaseProvider.Provide();
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		var existingPermissionIds = await ctx.Permissions.Select(static p => p.Id).AsAsyncEnumerable().ToImmutableSetAsync();
 | 
					 | 
				
			||||||
		var missingPermissionIds = GetMissingPermissionsOrdered(Permission.All, existingPermissionIds);
 | 
					 | 
				
			||||||
		if (!missingPermissionIds.IsEmpty) {
 | 
					 | 
				
			||||||
			Logger.Information("Adding default permissions: {Permissions}", string.Join(", ", missingPermissionIds));
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			foreach (var permissionId in missingPermissionIds) {
 | 
					 | 
				
			||||||
				ctx.Permissions.Add(new PermissionEntity(permissionId));
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			await ctx.SaveChangesAsync();
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	internal static ImmutableArray<string> GetMissingPermissionsOrdered(IEnumerable<Permission> allPermissions, ImmutableHashSet<string> existingPermissionIds) {
 | 
					 | 
				
			||||||
		return allPermissions.Select(static permission => permission.Id).Except(existingPermissionIds).Order().ToImmutableArray();
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	private IdentityPermissions FetchPermissionsForUserId(Guid userId) {
 | 
					 | 
				
			||||||
		using var ctx = databaseProvider.Provide();
 | 
					 | 
				
			||||||
		var userPermissions = ctx.UserPermissions.Where(up => up.UserGuid == userId).Select(static up => up.PermissionId);
 | 
					 | 
				
			||||||
		var rolePermissions = ctx.UserRoles.Where(ur => ur.UserGuid == userId).Join(ctx.RolePermissions, static ur => ur.RoleGuid, static rp => rp.RoleGuid, static (ur, rp) => rp.PermissionId);
 | 
					 | 
				
			||||||
		return new IdentityPermissions(userPermissions.Union(rolePermissions));
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	private IdentityPermissions GetPermissionsForUserId(Guid userId, bool refreshCache) {
 | 
					 | 
				
			||||||
		if (!refreshCache && userIdsToPermissionIds.TryGetValue(userId, out var userPermissions)) {
 | 
					 | 
				
			||||||
			return userPermissions;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		else {
 | 
					 | 
				
			||||||
			return userIdsToPermissionIds[userId] = FetchPermissionsForUserId(userId);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public IdentityPermissions GetPermissions(ClaimsPrincipal user, bool refreshCache = false) {
 | 
					 | 
				
			||||||
		Guid? userId = UserManager.GetAuthenticatedUserId(user);
 | 
					 | 
				
			||||||
		return userId == null ? IdentityPermissions.None : GetPermissionsForUserId(userId.Value, refreshCache);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public bool CheckPermission(ClaimsPrincipal user, Permission permission, bool refreshCache = false) {
 | 
					 | 
				
			||||||
		return GetPermissions(user, refreshCache).Check(permission);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
using System.Collections.Immutable;
 | 
					using System.Collections.Immutable;
 | 
				
			||||||
using Phantom.Controller.Services.Users.Permissions;
 | 
					using Phantom.Common.Data.Web.Users;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Phantom.Controller.Services.Users.Roles;
 | 
					namespace Phantom.Controller.Services.Users;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public sealed record Role(Guid Guid, string Name, ImmutableArray<Permission> Permissions) {
 | 
					public sealed record Role(Guid Guid, string Name, ImmutableArray<Permission> Permissions) {
 | 
				
			||||||
	private static readonly List<Role> AllRoles = new ();
 | 
						private static readonly List<Role> AllRoles = new ();
 | 
				
			||||||
							
								
								
									
										52
									
								
								Controller/Phantom.Controller.Services/Users/RoleManager.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								Controller/Phantom.Controller.Services/Users/RoleManager.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
				
			|||||||
 | 
					using System.Collections.Immutable;
 | 
				
			||||||
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
 | 
					using Phantom.Common.Logging;
 | 
				
			||||||
 | 
					using Phantom.Controller.Database;
 | 
				
			||||||
 | 
					using Phantom.Controller.Database.Entities;
 | 
				
			||||||
 | 
					using Phantom.Utils.Collections;
 | 
				
			||||||
 | 
					using Serilog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Controller.Services.Users;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					sealed class RoleManager {
 | 
				
			||||||
 | 
						private static readonly ILogger Logger = PhantomLogger.Create<RoleManager>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private readonly IDbContextProvider dbProvider;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public RoleManager(IDbContextProvider dbProvider) {
 | 
				
			||||||
 | 
							this.dbProvider = dbProvider;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						internal async Task Initialize() {
 | 
				
			||||||
 | 
							Logger.Information("Adding default roles to database.");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							await using var ctx = dbProvider.Eager();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var existingRoleNames = await ctx.Roles
 | 
				
			||||||
 | 
							                                 .Select(static role => role.Name)
 | 
				
			||||||
 | 
							                                 .AsAsyncEnumerable()
 | 
				
			||||||
 | 
							                                 .ToImmutableSetAsync();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var existingPermissionIdsByRoleGuid = await ctx.RolePermissions
 | 
				
			||||||
 | 
							                                               .GroupBy(static rp => rp.RoleGuid, static rp => rp.PermissionId)
 | 
				
			||||||
 | 
							                                               .ToDictionaryAsync(static g => g.Key, static g => g.ToImmutableHashSet());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							foreach (var role in Role.All) {
 | 
				
			||||||
 | 
								if (!existingRoleNames.Contains(role.Name)) {
 | 
				
			||||||
 | 
									Logger.Information("Adding default role \"{Name}\".", role.Name);
 | 
				
			||||||
 | 
									ctx.Roles.Add(new RoleEntity(role.Guid, role.Name));
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								var existingPermissionIds = existingPermissionIdsByRoleGuid.TryGetValue(role.Guid, out var ids) ? ids : ImmutableHashSet<string>.Empty;
 | 
				
			||||||
 | 
								var missingPermissionIds = PermissionManager.GetMissingPermissionsOrdered(role.Permissions, existingPermissionIds);
 | 
				
			||||||
 | 
								if (!missingPermissionIds.IsEmpty) {
 | 
				
			||||||
 | 
									Logger.Information("Assigning default permission to role \"{Name}\": {Permissions}", role.Name, string.Join(", ", missingPermissionIds));
 | 
				
			||||||
 | 
									foreach (var permissionId in missingPermissionIds) {
 | 
				
			||||||
 | 
										ctx.RolePermissions.Add(new RolePermissionEntity(role.Guid, permissionId));
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							await ctx.SaveChangesAsync();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,99 +0,0 @@
 | 
				
			|||||||
using System.Collections.Immutable;
 | 
					 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					 | 
				
			||||||
using Phantom.Common.Logging;
 | 
					 | 
				
			||||||
using Phantom.Controller.Database;
 | 
					 | 
				
			||||||
using Phantom.Controller.Database.Entities;
 | 
					 | 
				
			||||||
using Phantom.Controller.Services.Users.Permissions;
 | 
					 | 
				
			||||||
using Phantom.Utils.Collections;
 | 
					 | 
				
			||||||
using Phantom.Utils.Tasks;
 | 
					 | 
				
			||||||
using ILogger = Serilog.ILogger;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Phantom.Controller.Services.Users.Roles;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
public sealed class RoleManager {
 | 
					 | 
				
			||||||
	private static readonly ILogger Logger = PhantomLogger.Create<RoleManager>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	private const int MaxRoleNameLength = 40;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	private readonly IDatabaseProvider databaseProvider;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public RoleManager(IDatabaseProvider databaseProvider) {
 | 
					 | 
				
			||||||
		this.databaseProvider = databaseProvider;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	internal async Task Initialize() {
 | 
					 | 
				
			||||||
		Logger.Information("Adding default roles to database.");
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		await using var ctx = databaseProvider.Provide();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		var existingRoleNames = await ctx.Roles
 | 
					 | 
				
			||||||
		                                 .Select(static role => role.Name)
 | 
					 | 
				
			||||||
		                                 .AsAsyncEnumerable()
 | 
					 | 
				
			||||||
		                                 .ToImmutableSetAsync();
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		var existingPermissionIdsByRoleGuid = await ctx.RolePermissions
 | 
					 | 
				
			||||||
		                                               .GroupBy(static rp => rp.RoleGuid, static rp => rp.PermissionId)
 | 
					 | 
				
			||||||
		                                               .ToDictionaryAsync(static g => g.Key, static g => g.ToImmutableHashSet());
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		foreach (var role in Role.All) {
 | 
					 | 
				
			||||||
			if (!existingRoleNames.Contains(role.Name)) {
 | 
					 | 
				
			||||||
				Logger.Information("Adding default role \"{Name}\".", role.Name);
 | 
					 | 
				
			||||||
				ctx.Roles.Add(new RoleEntity(role.Guid, role.Name));
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			var existingPermissionIds = existingPermissionIdsByRoleGuid.TryGetValue(role.Guid, out var ids) ? ids : ImmutableHashSet<string>.Empty;
 | 
					 | 
				
			||||||
			var missingPermissionIds = PermissionManager.GetMissingPermissionsOrdered(role.Permissions, existingPermissionIds);
 | 
					 | 
				
			||||||
			if (!missingPermissionIds.IsEmpty) {
 | 
					 | 
				
			||||||
				Logger.Information("Assigning default permission to role \"{Name}\": {Permissions}", role.Name, string.Join(", ", missingPermissionIds));
 | 
					 | 
				
			||||||
				foreach (var permissionId in missingPermissionIds) {
 | 
					 | 
				
			||||||
					ctx.RolePermissions.Add(new RolePermissionEntity(role.Guid, permissionId));
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		await ctx.SaveChangesAsync();
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public async Task<List<RoleEntity>> GetAll() {
 | 
					 | 
				
			||||||
		await using var ctx = databaseProvider.Provide();
 | 
					 | 
				
			||||||
		return await ctx.Roles.ToListAsync();
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public async Task<ImmutableHashSet<string>> GetAllNames() {
 | 
					 | 
				
			||||||
		await using var ctx = databaseProvider.Provide();
 | 
					 | 
				
			||||||
		return await ctx.Roles.Select(static role => role.Name).AsAsyncEnumerable().ToImmutableSetAsync();
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public async ValueTask<RoleEntity?> GetByGuid(Guid guid) {
 | 
					 | 
				
			||||||
		await using var ctx = databaseProvider.Provide();
 | 
					 | 
				
			||||||
		return await ctx.Roles.FindAsync(guid);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public async Task<Result<RoleEntity, AddRoleError>> Create(string name) {
 | 
					 | 
				
			||||||
		if (string.IsNullOrWhiteSpace(name)) {
 | 
					 | 
				
			||||||
			return Result.Fail<RoleEntity, AddRoleError>(AddRoleError.NameIsEmpty);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		else if (name.Length > MaxRoleNameLength) {
 | 
					 | 
				
			||||||
			return Result.Fail<RoleEntity, AddRoleError>(AddRoleError.NameIsTooLong);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		RoleEntity newRole;
 | 
					 | 
				
			||||||
		try {
 | 
					 | 
				
			||||||
			await using var ctx = databaseProvider.Provide();
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			if (await ctx.Roles.AnyAsync(role => role.Name == name)) {
 | 
					 | 
				
			||||||
				return Result.Fail<RoleEntity, AddRoleError>(AddRoleError.NameAlreadyExists);
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
				
 | 
					 | 
				
			||||||
			newRole = new RoleEntity(Guid.NewGuid(), name);
 | 
					 | 
				
			||||||
			ctx.Roles.Add(newRole);
 | 
					 | 
				
			||||||
			await ctx.SaveChangesAsync();
 | 
					 | 
				
			||||||
		} catch (Exception e) {
 | 
					 | 
				
			||||||
			Logger.Error(e, "Could not create role \"{Name}\".", name);
 | 
					 | 
				
			||||||
			return Result.Fail<RoleEntity, AddRoleError>(AddRoleError.UnknownError);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		Logger.Information("Created role \"{Name}\" (GUID {Guid}).", name, newRole.RoleGuid);
 | 
					 | 
				
			||||||
		return Result.Ok<RoleEntity, AddRoleError>(newRole);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,83 +0,0 @@
 | 
				
			|||||||
using System.Collections.Immutable;
 | 
					 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					 | 
				
			||||||
using Phantom.Common.Logging;
 | 
					 | 
				
			||||||
using Phantom.Controller.Database;
 | 
					 | 
				
			||||||
using Phantom.Controller.Database.Entities;
 | 
					 | 
				
			||||||
using Phantom.Utils.Collections;
 | 
					 | 
				
			||||||
using ILogger = Serilog.ILogger;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Phantom.Controller.Services.Users.Roles;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
public sealed class UserRoleManager {
 | 
					 | 
				
			||||||
	private static readonly ILogger Logger = PhantomLogger.Create<UserRoleManager>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	private readonly IDatabaseProvider databaseProvider;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public UserRoleManager(IDatabaseProvider databaseProvider) {
 | 
					 | 
				
			||||||
		this.databaseProvider = databaseProvider;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public async Task<Dictionary<Guid, ImmutableArray<RoleEntity>>> GetAllByUserGuid() {
 | 
					 | 
				
			||||||
		await using var ctx = databaseProvider.Provide();
 | 
					 | 
				
			||||||
		return await ctx.UserRoles
 | 
					 | 
				
			||||||
		                .Include(static ur => ur.Role)
 | 
					 | 
				
			||||||
		                .GroupBy(static ur => ur.UserGuid, static ur => ur.Role)
 | 
					 | 
				
			||||||
		                .ToDictionaryAsync(static group => group.Key, static group => group.ToImmutableArray());
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public async Task<ImmutableArray<RoleEntity>> GetUserRoles(UserEntity user) {
 | 
					 | 
				
			||||||
		await using var ctx = databaseProvider.Provide();
 | 
					 | 
				
			||||||
		return await ctx.UserRoles
 | 
					 | 
				
			||||||
		                .Include(static ur => ur.Role)
 | 
					 | 
				
			||||||
		                .Where(ur => ur.UserGuid == user.UserGuid)
 | 
					 | 
				
			||||||
		                .Select(static ur => ur.Role)
 | 
					 | 
				
			||||||
		                .AsAsyncEnumerable()
 | 
					 | 
				
			||||||
		                .ToImmutableArrayAsync();
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public async Task<ImmutableHashSet<Guid>> GetUserRoleGuids(UserEntity user) {
 | 
					 | 
				
			||||||
		await using var ctx = databaseProvider.Provide();
 | 
					 | 
				
			||||||
		return await ctx.UserRoles
 | 
					 | 
				
			||||||
		                .Where(ur => ur.UserGuid == user.UserGuid)
 | 
					 | 
				
			||||||
		                .Select(static ur => ur.RoleGuid)
 | 
					 | 
				
			||||||
		                .AsAsyncEnumerable()
 | 
					 | 
				
			||||||
		                .ToImmutableSetAsync();
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public async Task<bool> Add(UserEntity user, RoleEntity role) {
 | 
					 | 
				
			||||||
		try {
 | 
					 | 
				
			||||||
			await using var ctx = databaseProvider.Provide();
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			var userRole = await ctx.UserRoles.FindAsync(user.UserGuid, role.RoleGuid);
 | 
					 | 
				
			||||||
			if (userRole == null) {
 | 
					 | 
				
			||||||
				userRole = new UserRoleEntity(user.UserGuid, role.RoleGuid);
 | 
					 | 
				
			||||||
				ctx.UserRoles.Add(userRole);
 | 
					 | 
				
			||||||
				await ctx.SaveChangesAsync();
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		} catch (Exception e) {
 | 
					 | 
				
			||||||
			Logger.Error(e, "Could not add user \"{UserName}\" (GUID {UserGuid}) to role \"{RoleName}\" (GUID {RoleGuid}).", user.Name, user.UserGuid, role.Name, role.RoleGuid);
 | 
					 | 
				
			||||||
			return false;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		Logger.Information("Added user \"{UserName}\" (GUID {UserGuid}) to role \"{RoleName}\" (GUID {RoleGuid}).", user.Name, user.UserGuid, role.Name, role.RoleGuid);
 | 
					 | 
				
			||||||
		return true;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public async Task<bool> Remove(UserEntity user, RoleEntity role) {
 | 
					 | 
				
			||||||
		try {
 | 
					 | 
				
			||||||
			await using var ctx = databaseProvider.Provide();
 | 
					 | 
				
			||||||
			
 | 
					 | 
				
			||||||
			var userRole = await ctx.UserRoles.FindAsync(user.UserGuid, role.RoleGuid);
 | 
					 | 
				
			||||||
			if (userRole != null) {
 | 
					 | 
				
			||||||
				ctx.UserRoles.Remove(userRole);
 | 
					 | 
				
			||||||
				await ctx.SaveChangesAsync();
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		} catch (Exception e) {
 | 
					 | 
				
			||||||
			Logger.Error(e, "Could not remove user \"{UserName}\" (GUID {UserGuid}) from role \"{RoleName}\" (GUID {RoleGuid}).", user.Name, user.UserGuid, role.Name, role.RoleGuid);
 | 
					 | 
				
			||||||
			return false;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		Logger.Information("Removed user \"{UserName}\" (GUID {UserGuid}) from role \"{RoleName}\" (GUID {RoleGuid}).", user.Name, user.UserGuid, role.Name, role.RoleGuid);
 | 
					 | 
				
			||||||
		return true;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,13 +0,0 @@
 | 
				
			|||||||
using System.Collections.Immutable;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Phantom.Controller.Services.Users;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
public abstract record SetUserPasswordError {
 | 
					 | 
				
			||||||
	private SetUserPasswordError() {}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public sealed record UserNotFound : SetUserPasswordError;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	public sealed record PasswordIsInvalid(ImmutableArray<PasswordRequirementViolation> Violations) : SetUserPasswordError;
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	public sealed record UnknownError : SetUserPasswordError;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -0,0 +1,34 @@
 | 
				
			|||||||
 | 
					using System.Collections.Concurrent;
 | 
				
			||||||
 | 
					using System.Collections.Immutable;
 | 
				
			||||||
 | 
					using System.Security.Cryptography;
 | 
				
			||||||
 | 
					using Phantom.Common.Data.Web.Users;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Phantom.Controller.Services.Users; 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					sealed class UserLoginManager {
 | 
				
			||||||
 | 
						private const int SessionIdBytes = 20;
 | 
				
			||||||
 | 
						private readonly ConcurrentDictionary<string, List<ImmutableArray<byte>>> sessionTokensByUsername = new ();
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						private readonly UserManager userManager;
 | 
				
			||||||
 | 
						private readonly PermissionManager permissionManager;
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						public UserLoginManager(UserManager userManager, PermissionManager permissionManager) {
 | 
				
			||||||
 | 
							this.userManager = userManager;
 | 
				
			||||||
 | 
							this.permissionManager = permissionManager;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public async Task<LogInSuccess?> LogIn(string username, string password) {
 | 
				
			||||||
 | 
							var user = await userManager.GetAuthenticated(username, password);
 | 
				
			||||||
 | 
							if (user == null) {
 | 
				
			||||||
 | 
								return null;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var token = ImmutableArray.Create(RandomNumberGenerator.GetBytes(SessionIdBytes));
 | 
				
			||||||
 | 
							var sessionTokens = sessionTokensByUsername.GetOrAdd(username, static _ => new List<ImmutableArray<byte>>());
 | 
				
			||||||
 | 
							lock (sessionTokens) {
 | 
				
			||||||
 | 
								sessionTokens.Add(token);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							return new LogInSuccess(user.UserGuid, await permissionManager.FetchPermissionsForUserId(user.UserGuid), token);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user