mirror of
				https://github.com/chylex/Minecraft-Phantom-Panel.git
				synced 2025-10-31 11:17:15 +01:00 
			
		
		
		
	Compare commits
	
		
			4 Commits
		
	
	
		
			wip-forge
			...
			15d45fe1a3
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 15d45fe1a3 | |||
| 0b8a870d10 | |||
| 16888c9b10 | |||
| bfefb9063b | 
| @@ -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> | ||||||
| @@ -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; | ||||||
|   | |||||||
| @@ -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]; | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| @@ -1,6 +1,5 @@ | |||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
| using Phantom.Controller.Database.Enums; |  | ||||||
| 
 | 
 | ||||||
| namespace Phantom.Controller.Services.Audit; | namespace Phantom.Common.Data.Web.AuditLog; | ||||||
| 
 | 
 | ||||||
| public sealed record AuditLogItem(DateTime UtcTime, Guid? UserGuid, string? UserName, AuditLogEventType EventType, AuditLogSubjectType SubjectType, string? SubjectId, JsonDocument? Data); | public sealed record AuditLogItem(DateTime UtcTime, Guid? UserGuid, string? UserName, AuditLogEventType EventType, AuditLogSubjectType SubjectType, string? SubjectId, JsonDocument? Data); | ||||||
| @@ -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 }, | ||||||
| @@ -1,6 +1,5 @@ | |||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
| using Phantom.Controller.Database.Enums; |  | ||||||
| 
 | 
 | ||||||
| namespace Phantom.Controller.Services.Events; | namespace Phantom.Common.Data.Web.EventLog; | ||||||
| 
 | 
 | ||||||
| public sealed record EventLogItem(DateTime UtcTime, Guid? AgentGuid, EventLogEventType EventType, EventLogSubjectType SubjectType, string SubjectId, JsonDocument? Data); | public sealed record EventLogItem(DateTime UtcTime, Guid? AgentGuid, EventLogEventType EventType, EventLogSubjectType SubjectType, string SubjectId, JsonDocument? Data); | ||||||
| @@ -0,0 +1,5 @@ | |||||||
|  | namespace Phantom.Common.Data.Web.EventLog; | ||||||
|  |  | ||||||
|  | public enum EventLogSubjectType { | ||||||
|  | 	Instance | ||||||
|  | } | ||||||
| @@ -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,13 @@ | |||||||
|  | <Project Sdk="Microsoft.NET.Sdk"> | ||||||
|  |    | ||||||
|  |   <PropertyGroup> | ||||||
|  |     <ImplicitUsings>enable</ImplicitUsings> | ||||||
|  |     <Nullable>enable</Nullable> | ||||||
|  |   </PropertyGroup> | ||||||
|  |    | ||||||
|  |   <ItemGroup> | ||||||
|  |     <PackageReference Include="BCrypt.Net-Next.StrongName" /> | ||||||
|  |     <PackageReference Include="MemoryPack" /> | ||||||
|  |   </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, | ||||||
| @@ -1,18 +1,14 @@ | |||||||
| using System.Collections.Immutable; | using System.Collections.Immutable; | ||||||
| 
 | 
 | ||||||
| namespace Phantom.Controller.Services.Users.Permissions; | namespace Phantom.Common.Data.Web.Users; | ||||||
| 
 | 
 | ||||||
| public sealed class IdentityPermissions { | public sealed class IdentityPermissions { | ||||||
| 	internal static IdentityPermissions None { get; } = new (); | 	public static IdentityPermissions None { get; } = new (ImmutableHashSet<string>.Empty); | ||||||
| 	 | 	 | ||||||
| 	private readonly ImmutableHashSet<string> permissionIds; | 	private readonly ImmutableHashSet<string> permissionIds; | ||||||
| 
 | 
 | ||||||
| 	internal IdentityPermissions(IQueryable<string> permissionIdsQuery) { | 	public IdentityPermissions(ImmutableHashSet<string> permissionIdsQuery) { | ||||||
| 		this.permissionIds = permissionIdsQuery.ToImmutableHashSet(); | 		this.permissionIds = permissionIdsQuery; | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	private IdentityPermissions() { |  | ||||||
| 		this.permissionIds = ImmutableHashSet<string>.Empty; |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	public bool Check(Permission? permission) { | 	public bool Check(Permission? permission) { | ||||||
| @@ -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 (); | ||||||
							
								
								
									
										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; | ||||||
|  | } | ||||||
| @@ -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,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,12 @@ | |||||||
| using Phantom.Common.Messages.Web.BiDirectional; | 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<CreateOrUpdateAdministratorUserResult> CreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUser 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 CreateOrUpdateAdministratorUser( | ||||||
|  | 	[property: MemoryPackOrder(0)] string Username, | ||||||
|  | 	[property: MemoryPackOrder(1)] string Password | ||||||
|  | ) : IMessageToController<CreateOrUpdateAdministratorUserResult> { | ||||||
|  | 	public Task<CreateOrUpdateAdministratorUserResult> Accept(IMessageToControllerListener listener) { | ||||||
|  | 		return listener.CreateOrUpdateAdministratorUser(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); | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -1,5 +1,7 @@ | |||||||
| using Phantom.Common.Logging; | 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.Utils.Rpc.Message; | using Phantom.Utils.Rpc.Message; | ||||||
|  |  | ||||||
| namespace Phantom.Common.Messages.Web; | namespace Phantom.Common.Messages.Web; | ||||||
| @@ -11,6 +13,8 @@ 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<CreateOrUpdateAdministratorUser, CreateOrUpdateAdministratorUserResult>(1); | ||||||
| 		ToController.Add<ReplyMessage>(127); | 		ToController.Add<ReplyMessage>(127); | ||||||
| 		 | 		 | ||||||
| 		ToWeb.Add<ReplyMessage>(127); | 		ToWeb.Add<ReplyMessage>(127); | ||||||
| @@ -21,7 +25,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,66 @@ | |||||||
|  | using Phantom.Common.Data.Web.AuditLog; | ||||||
|  | using Phantom.Controller.Database.Entities; | ||||||
|  |  | ||||||
|  | namespace Phantom.Controller.Database.Repositories; | ||||||
|  |  | ||||||
|  | sealed partial class AuditLogRepository { | ||||||
|  | 	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,30 @@ | |||||||
|  | using Microsoft.EntityFrameworkCore; | ||||||
|  | using Phantom.Common.Data.Web.AuditLog; | ||||||
|  | using Phantom.Controller.Database.Entities; | ||||||
|  |  | ||||||
|  | namespace Phantom.Controller.Database.Repositories; | ||||||
|  |  | ||||||
|  | public sealed partial class AuditLogRepository { | ||||||
|  | 	private readonly ILazyDbContext db; | ||||||
|  | 	private readonly Guid? currentUserGuid; | ||||||
|  |  | ||||||
|  | 	public AuditLogRepository(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 Task<AuditLogItem[]> GetItems(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)) | ||||||
|  | 		         .ToArrayAsync(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 AuditLogRepository? auditLog; | ||||||
|  | 	private AuditLogRepository AuditLogRepository => this.auditLog ??= new AuditLogRepository(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); | ||||||
|  | 		AuditLogRepository.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); | ||||||
|  | 		AuditLogRepository.AddUserPasswordChangedEvent(user); | ||||||
|  |  | ||||||
|  | 		return Result.Ok<SetUserPasswordError>(); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	public void DeleteUser(UserEntity user) { | ||||||
|  | 		db.Ctx.Users.Remove(user); | ||||||
|  | 		AuditLogRepository.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; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -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,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,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; | ||||||
| @@ -19,7 +17,7 @@ namespace Phantom.Controller.Services; | |||||||
| public sealed class ControllerServices { | public sealed class ControllerServices { | ||||||
| 	private TaskManager TaskManager { get; } | 	private TaskManager TaskManager { get; } | ||||||
| 	private MinecraftVersions MinecraftVersions { get; } | 	private MinecraftVersions MinecraftVersions { get; } | ||||||
| 	 |  | ||||||
| 	private AgentManager AgentManager { get; } | 	private AgentManager AgentManager { get; } | ||||||
| 	private AgentJavaRuntimesManager AgentJavaRuntimesManager { get; } | 	private AgentJavaRuntimesManager AgentJavaRuntimesManager { get; } | ||||||
| 	private EventLog EventLog { get; } | 	private EventLog EventLog { get; } | ||||||
| @@ -28,28 +26,26 @@ public sealed class ControllerServices { | |||||||
| 	 | 	 | ||||||
| 	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 readonly IDbContextProvider dbProvider; | ||||||
| 	private readonly CancellationToken cancellationToken; | 	private readonly CancellationToken cancellationToken; | ||||||
| 	 | 	 | ||||||
| 	public ControllerServices(IDatabaseProvider databaseProvider, AuthToken agentAuthToken, CancellationToken shutdownCancellationToken) { | 	public ControllerServices(IDbContextProvider dbProvider, AuthToken agentAuthToken, 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.dbProvider = dbProvider; | ||||||
| 		this.cancellationToken = shutdownCancellationToken; | 		this.cancellationToken = shutdownCancellationToken; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -58,11 +54,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, UserManager, RoleManager); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	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) | ||||||
|   | |||||||
| @@ -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,6 +4,7 @@ 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.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; | ||||||
| @@ -13,11 +14,11 @@ 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 +27,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, | ||||||
| @@ -98,7 +99,7 @@ public sealed class InstanceManager { | |||||||
| 			}); | 			}); | ||||||
| 			 | 			 | ||||||
| 			if (result.Is(AddOrEditInstanceResult.Success)) { | 			if (result.Is(AddOrEditInstanceResult.Success)) { | ||||||
| 				await using var ctx = databaseProvider.Provide(); | 				await using var ctx = dbProvider.Eager(); | ||||||
| 				InstanceEntity entity = ctx.InstanceUpsert.Fetch(configuration.InstanceGuid); | 				InstanceEntity entity = ctx.InstanceUpsert.Fetch(configuration.InstanceGuid); | ||||||
|  |  | ||||||
| 				entity.AgentGuid = configuration.AgentGuid; | 				entity.AgentGuid = configuration.AgentGuid; | ||||||
| @@ -188,8 +189,8 @@ public sealed class InstanceManager { | |||||||
| 		try { | 		try { | ||||||
| 			instances.ByGuid.TryReplace(instanceGuid, instance => instance with { LaunchAutomatically = shouldLaunchAutomatically }); | 			instances.ByGuid.TryReplace(instanceGuid, instance => instance with { LaunchAutomatically = shouldLaunchAutomatically }); | ||||||
|  |  | ||||||
| 			await using var ctx = databaseProvider.Provide(); | 			await using var ctx = dbProvider.Eager(); | ||||||
| 			var entity = await ctx.Instances.FindAsync(instanceGuid, cancellationToken); | 			var entity = await ctx.Instances.FindAsync(new object[] { instanceGuid }, cancellationToken); | ||||||
| 			if (entity != null) { | 			if (entity != null) { | ||||||
| 				entity.LaunchAutomatically = shouldLaunchAutomatically; | 				entity.LaunchAutomatically = shouldLaunchAutomatically; | ||||||
| 				await ctx.SaveChangesAsync(cancellationToken); | 				await ctx.SaveChangesAsync(cancellationToken); | ||||||
| @@ -200,7 +201,12 @@ public sealed class InstanceManager { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	public async Task<InstanceActionResult<SendCommandToInstanceResult>> SendCommand(Guid instanceGuid, string command) { | 	public async Task<InstanceActionResult<SendCommandToInstanceResult>> SendCommand(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)) { | ||||||
|  | 			// TODO audit log | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		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,30 @@ | |||||||
| using Phantom.Common.Messages.Web; | using Phantom.Common.Data.Web.Users; | ||||||
|  | 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.Controller.Rpc; | using Phantom.Controller.Rpc; | ||||||
|  | using Phantom.Controller.Services.Users; | ||||||
| using Phantom.Utils.Rpc.Message; | using Phantom.Utils.Rpc.Message; | ||||||
|  |  | ||||||
| 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 readonly RpcConnectionToClient<IMessageToWebListener> connection; | ||||||
|  | 	private readonly UserManager userManager; | ||||||
|  | 	private readonly RoleManager roleManager; | ||||||
| 	 | 	 | ||||||
| 	internal WebMessageListener(RpcConnectionToClient<IMessageToWebListener> connection) { | 	internal WebMessageListener(RpcConnectionToClient<IMessageToWebListener> connection, UserManager userManager, RoleManager roleManager) { | ||||||
| 		this.connection = connection; | 		this.connection = connection; | ||||||
|  | 		this.userManager = userManager; | ||||||
|  | 		this.roleManager = roleManager; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	public async Task<NoReply> HandleRegisterWeb(RegisterWebMessage message) { | ||||||
|  | 		return default; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	public Task<CreateOrUpdateAdministratorUserResult> CreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUser message) { | ||||||
|  | 		return userManager.CreateOrUpdateAdministrator(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; |  | ||||||
| } |  | ||||||
| @@ -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; |  | ||||||
| } |  | ||||||
| @@ -1,28 +1,28 @@ | |||||||
| using System.Collections.Immutable; | using System.Collections.Immutable; | ||||||
| using System.Security.Claims; |  | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
|  | using Phantom.Common.Data.Web.Users; | ||||||
| using Phantom.Common.Logging; | using Phantom.Common.Logging; | ||||||
| using Phantom.Controller.Database; | using Phantom.Controller.Database; | ||||||
| using Phantom.Controller.Database.Entities; | using Phantom.Controller.Database.Entities; | ||||||
| using Phantom.Utils.Collections; | using Phantom.Utils.Collections; | ||||||
| using ILogger = Serilog.ILogger; | using Serilog; | ||||||
| 
 | 
 | ||||||
| namespace Phantom.Controller.Services.Users.Permissions; | namespace Phantom.Controller.Services.Users; | ||||||
| 
 | 
 | ||||||
| public sealed class PermissionManager { | public sealed class PermissionManager { | ||||||
| 	private static readonly ILogger Logger = PhantomLogger.Create<PermissionManager>(); | 	private static readonly ILogger Logger = PhantomLogger.Create<PermissionManager>(); | ||||||
| 	 | 	 | ||||||
| 	private readonly IDatabaseProvider databaseProvider; | 	private readonly IDbContextProvider dbProvider; | ||||||
| 	private readonly Dictionary<Guid, IdentityPermissions> userIdsToPermissionIds = new (); | 	private readonly Dictionary<Guid, IdentityPermissions> userIdsToPermissionIds = new (); | ||||||
| 
 | 
 | ||||||
| 	public PermissionManager(IDatabaseProvider databaseProvider) { | 	public PermissionManager(IDbContextProvider dbProvider) { | ||||||
| 		this.databaseProvider = databaseProvider; | 		this.dbProvider = dbProvider; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	internal async Task Initialize() { | 	internal async Task Initialize() { | ||||||
| 		Logger.Information("Adding default permissions to database."); | 		Logger.Information("Adding default permissions to database."); | ||||||
| 		 | 		 | ||||||
| 		await using var ctx = databaseProvider.Provide(); | 		await using var ctx = dbProvider.Eager(); | ||||||
| 		 | 		 | ||||||
| 		var existingPermissionIds = await ctx.Permissions.Select(static p => p.Id).AsAsyncEnumerable().ToImmutableSetAsync(); | 		var existingPermissionIds = await ctx.Permissions.Select(static p => p.Id).AsAsyncEnumerable().ToImmutableSetAsync(); | ||||||
| 		var missingPermissionIds = GetMissingPermissionsOrdered(Permission.All, existingPermissionIds); | 		var missingPermissionIds = GetMissingPermissionsOrdered(Permission.All, existingPermissionIds); | ||||||
| @@ -41,28 +41,28 @@ public sealed class PermissionManager { | |||||||
| 		return allPermissions.Select(static permission => permission.Id).Except(existingPermissionIds).Order().ToImmutableArray(); | 		return allPermissions.Select(static permission => permission.Id).Except(existingPermissionIds).Order().ToImmutableArray(); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	private IdentityPermissions FetchPermissionsForUserId(Guid userId) { | 	private async Task<IdentityPermissions> FetchPermissionsForUserId(Guid userId) { | ||||||
| 		using var ctx = databaseProvider.Provide(); | 		await using var ctx = dbProvider.Eager(); | ||||||
| 		var userPermissions = ctx.UserPermissions.Where(up => up.UserGuid == userId).Select(static up => up.PermissionId); | 		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); | 		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)); | 		return new IdentityPermissions(await userPermissions.Union(rolePermissions).AsAsyncEnumerable().ToImmutableSetAsync()); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	private IdentityPermissions GetPermissionsForUserId(Guid userId, bool refreshCache) { | 	// private IdentityPermissions GetPermissionsForUserId(Guid userId, bool refreshCache) { | ||||||
| 		if (!refreshCache && userIdsToPermissionIds.TryGetValue(userId, out var userPermissions)) { | 	// 	if (!refreshCache && userIdsToPermissionIds.TryGetValue(userId, out var userPermissions)) { | ||||||
| 			return userPermissions; | 	// 		return userPermissions; | ||||||
| 		} | 	// 	} | ||||||
| 		else { | 	// 	else { | ||||||
| 			return userIdsToPermissionIds[userId] = FetchPermissionsForUserId(userId); | 	// 		return userIdsToPermissionIds[userId] = FetchPermissionsForUserId(userId); | ||||||
| 		} | 	// 	} | ||||||
| 	} | 	// } | ||||||
| 
 | 	// | ||||||
| 	public IdentityPermissions GetPermissions(ClaimsPrincipal user, bool refreshCache = false) { | 	// public IdentityPermissions GetPermissions(ClaimsPrincipal user, bool refreshCache = false) { | ||||||
| 		Guid? userId = UserManager.GetAuthenticatedUserId(user); | 	// 	Guid? userId = UserManager.GetAuthenticatedUserId(user); | ||||||
| 		return userId == null ? IdentityPermissions.None : GetPermissionsForUserId(userId.Value, refreshCache); | 	// 	return userId == null ? IdentityPermissions.None : GetPermissionsForUserId(userId.Value, refreshCache); | ||||||
| 	} | 	// } | ||||||
| 
 | 	// | ||||||
| 	public bool CheckPermission(ClaimsPrincipal user, Permission permission, bool refreshCache = false) { | 	// public bool CheckPermission(ClaimsPrincipal user, Permission permission, bool refreshCache = false) { | ||||||
| 		return GetPermissions(user, refreshCache).Check(permission); | 	// 	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; |  | ||||||
| } |  | ||||||
| @@ -1,136 +1,116 @@ | |||||||
| using System.Collections.Immutable; | using Phantom.Common.Data.Web.Users; | ||||||
| using System.Security.Claims; | using Phantom.Common.Data.Web.Users.CreateOrUpdateAdministratorUserResults; | ||||||
| using Microsoft.EntityFrameworkCore; |  | ||||||
| using Phantom.Common.Logging; | using Phantom.Common.Logging; | ||||||
| using Phantom.Controller.Database; | using Phantom.Controller.Database; | ||||||
| using Phantom.Controller.Database.Entities; | using Phantom.Controller.Database.Repositories; | ||||||
| using Phantom.Utils.Collections; | using Serilog; | ||||||
| using Phantom.Utils.Tasks; |  | ||||||
| using ILogger = Serilog.ILogger; |  | ||||||
|  |  | ||||||
| namespace Phantom.Controller.Services.Users; | namespace Phantom.Controller.Services.Users; | ||||||
|  |  | ||||||
| public sealed class UserManager { | sealed class UserManager { | ||||||
| 	private static readonly ILogger Logger = PhantomLogger.Create<UserManager>(); | 	private static readonly ILogger Logger = PhantomLogger.Create<UserManager>(); | ||||||
|  |  | ||||||
| 	private const int MaxUserNameLength = 40; | 	private readonly IDbContextProvider dbProvider; | ||||||
|  |  | ||||||
| 	private readonly IDatabaseProvider databaseProvider; | 	public UserManager(IDbContextProvider dbProvider) { | ||||||
|  | 		this.dbProvider = dbProvider; | ||||||
| 	public UserManager(IDatabaseProvider databaseProvider) { |  | ||||||
| 		this.databaseProvider = databaseProvider; |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	public static Guid? GetAuthenticatedUserId(ClaimsPrincipal user) { | 	// public static Guid? GetAuthenticatedUserId(ClaimsPrincipal user) { | ||||||
| 		if (user.Identity is not { IsAuthenticated: true }) { | 	// 	if (user.Identity is not { IsAuthenticated: true }) { | ||||||
| 			return null; | 	// 		return null; | ||||||
| 		} | 	// 	} | ||||||
|  | 	// | ||||||
|  | 	// 	var claim = user.FindFirst(ClaimTypes.NameIdentifier); | ||||||
|  | 	// 	if (claim == null) { | ||||||
|  | 	// 		return null; | ||||||
|  | 	// 	} | ||||||
|  | 	// | ||||||
|  | 	// 	return Guid.TryParse(claim.Value, out var guid) ? guid : null; | ||||||
|  | 	// } | ||||||
|  | 	// | ||||||
|  | 	// public async Task<UserEntity?> GetAuthenticated(string username, string password) { | ||||||
|  | 	// 	await using var ctx = dbProvider.Lazy(); | ||||||
|  | 	// 	var user = await ctx.Users.FirstOrDefaultAsync(user => user.Name == username); | ||||||
|  | 	// 	if (user == null) { | ||||||
|  | 	// 		return null; | ||||||
|  | 	// 	} | ||||||
|  | 	// | ||||||
|  | 	// 	switch (UserValidation.VerifyPassword(user, password)) { | ||||||
|  | 	// 		case PasswordVerificationResult.SuccessRehashNeeded: | ||||||
|  | 	// 			try { | ||||||
|  | 	// 				UserValidation.SetPassword(user, password); | ||||||
|  | 	// 				await ctx.SaveChangesAsync(); | ||||||
|  | 	// 			} catch (Exception e) { | ||||||
|  | 	// 				Logger.Warning(e, "Could not rehash password for \"{Username}\".", user.Name); | ||||||
|  | 	// 			} | ||||||
|  | 	// | ||||||
|  | 	// 			goto case PasswordVerificationResult.Success; | ||||||
|  | 	// | ||||||
|  | 	// 		case PasswordVerificationResult.Success: | ||||||
|  | 	// 			return user; | ||||||
|  | 	// | ||||||
|  | 	// 		case PasswordVerificationResult.Failed: | ||||||
|  | 	// 			return null; | ||||||
|  | 	// 	} | ||||||
|  | 	// | ||||||
|  | 	// 	throw new InvalidOperationException(); | ||||||
|  | 	// } | ||||||
|  |  | ||||||
| 		var claim = user.FindFirst(ClaimTypes.NameIdentifier); | 	public async Task<CreateOrUpdateAdministratorUserResult> CreateOrUpdateAdministrator(string username, string password) { | ||||||
| 		if (claim == null) { | 		await using var db = dbProvider.Lazy(); | ||||||
| 			return null; | 		var repository = new UserRepository(db); | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		return Guid.TryParse(claim.Value, out var guid) ? guid : null; |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	public async Task<ImmutableArray<UserEntity>> GetAll() { |  | ||||||
| 		await using var ctx = databaseProvider.Provide(); |  | ||||||
| 		return await ctx.Users.AsAsyncEnumerable().ToImmutableArrayAsync(); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	public async Task<Dictionary<Guid, T>> GetAllByGuid<T>(Func<UserEntity, T> valueSelector, CancellationToken cancellationToken = default) { |  | ||||||
| 		await using var ctx = databaseProvider.Provide(); |  | ||||||
| 		return await ctx.Users.ToDictionaryAsync(static user => user.UserGuid, valueSelector, cancellationToken); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	public async Task<UserEntity?> GetByName(string username) { |  | ||||||
| 		await using var ctx = databaseProvider.Provide(); |  | ||||||
| 		return await ctx.Users.FirstOrDefaultAsync(user => user.Name == username); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	public async Task<UserEntity?> GetAuthenticated(string username, string password) { |  | ||||||
| 		await using var ctx = databaseProvider.Provide(); |  | ||||||
| 		var user = await ctx.Users.FirstOrDefaultAsync(user => user.Name == username); |  | ||||||
| 		return user != null && UserPasswords.Verify(user, password) ? user : null; |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	public async Task<Result<UserEntity, AddUserError>> CreateUser(string username, string password) { |  | ||||||
| 		if (string.IsNullOrWhiteSpace(username)) { |  | ||||||
| 			return Result.Fail<UserEntity, AddUserError>(new AddUserError.NameIsEmpty()); |  | ||||||
| 		} |  | ||||||
| 		else if (username.Length > MaxUserNameLength) { |  | ||||||
| 			return Result.Fail<UserEntity, AddUserError>(new AddUserError.NameIsTooLong(MaxUserNameLength)); |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		var requirementViolations = UserPasswords.CheckRequirements(password); |  | ||||||
| 		if (!requirementViolations.IsEmpty) { |  | ||||||
| 			return Result.Fail<UserEntity, AddUserError>(new AddUserError.PasswordIsInvalid(requirementViolations)); |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		UserEntity newUser; |  | ||||||
| 		try { | 		try { | ||||||
| 			await using var ctx = databaseProvider.Provide(); | 			var user = await repository.GetByName(username); | ||||||
| 			 |  | ||||||
| 			if (await ctx.Users.AnyAsync(user => user.Name == username)) { |  | ||||||
| 				return Result.Fail<UserEntity, AddUserError>(new AddUserError.NameAlreadyExists()); |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			newUser = new UserEntity(Guid.NewGuid(), username); |  | ||||||
| 			UserPasswords.Set(newUser, password); |  | ||||||
|  |  | ||||||
| 			ctx.Users.Add(newUser); |  | ||||||
| 			await ctx.SaveChangesAsync(); |  | ||||||
| 		} catch (Exception e) { |  | ||||||
| 			Logger.Error(e, "Could not create user \"{Name}\".", username); |  | ||||||
| 			return Result.Fail<UserEntity, AddUserError>(new AddUserError.UnknownError()); |  | ||||||
| 		} |  | ||||||
| 		 |  | ||||||
| 		Logger.Information("Created user \"{Name}\" (GUID {Guid}).", username, newUser.UserGuid); |  | ||||||
| 		return Result.Ok<UserEntity, AddUserError>(newUser); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	public async Task<Result<SetUserPasswordError>> SetUserPassword(Guid guid, string password) { |  | ||||||
| 		UserEntity foundUser; |  | ||||||
| 		 |  | ||||||
| 		await using (var ctx = databaseProvider.Provide()) { |  | ||||||
| 			var user = await ctx.Users.FindAsync(guid); |  | ||||||
| 			if (user == null) { | 			if (user == null) { | ||||||
| 				return Result.Fail<SetUserPasswordError>(new SetUserPasswordError.UserNotFound()); | 				var result = await repository.CreateUser(username, password); | ||||||
| 			} | 				if (result) { | ||||||
|  | 					user = result.Value; | ||||||
| 			foundUser = user; | 				} | ||||||
| 			try { | 				else { | ||||||
| 				var requirementViolations = UserPasswords.CheckRequirements(password); | 					return new CreationFailed(result.Error); | ||||||
| 				if (!requirementViolations.IsEmpty) { | 				} | ||||||
| 					return Result.Fail<SetUserPasswordError>(new SetUserPasswordError.PasswordIsInvalid(requirementViolations)); | 			} | ||||||
|  | 			else { | ||||||
|  | 				var result = repository.SetUserPassword(user, password); | ||||||
|  | 				if (!result) { | ||||||
|  | 					return new UpdatingFailed(result.Error); | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
| 				UserPasswords.Set(user, password); |  | ||||||
| 				await ctx.SaveChangesAsync(); |  | ||||||
| 			} catch (Exception e) { |  | ||||||
| 				Logger.Error(e, "Could not change password for user \"{Name}\" (GUID {Guid}).", user.Name, user.UserGuid); |  | ||||||
| 				return Result.Fail<SetUserPasswordError>(new SetUserPasswordError.UnknownError()); |  | ||||||
| 			} | 			} | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		Logger.Information("Changed password for user \"{Name}\" (GUID {Guid}).", foundUser.Name, foundUser.UserGuid); | 			var role = await new RoleRepository(db).GetByGuid(Role.Administrator.Guid); | ||||||
| 		return Result.Ok<SetUserPasswordError>(); | 			if (role == null) { | ||||||
|  | 				return new AddingToRoleFailed(); | ||||||
|  | 			} | ||||||
|  | 			 | ||||||
|  | 			await new UserRoleRepository(db).Add(user, role); | ||||||
|  |  | ||||||
|  | 			Logger.Information("Created administrator user \"{Username}\" (GUID {Guid}).", username, user.UserGuid); | ||||||
|  | 			return new Success(user.ToUserInfo()); | ||||||
|  | 		} catch (Exception e) { | ||||||
|  | 			Logger.Error(e, "Could not create or update administrator user \"{Username}\".", username); | ||||||
|  | 			return new UnknownError(); | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	public async Task<DeleteUserResult> DeleteByGuid(Guid guid) { | 	public async Task<DeleteUserResult> DeleteByGuid(Guid guid) { | ||||||
| 		await using var ctx = databaseProvider.Provide(); | 		await using var db = dbProvider.Lazy(); | ||||||
| 		var user = await ctx.Users.FindAsync(guid); | 		var repository = new UserRepository(db); | ||||||
|  | 			 | ||||||
|  | 		var user = await repository.GetByGuid(guid); | ||||||
| 		if (user == null) { | 		if (user == null) { | ||||||
| 			return DeleteUserResult.NotFound; | 			return DeleteUserResult.NotFound; | ||||||
| 		} | 		} | ||||||
|  | 		 | ||||||
| 		try { | 		try { | ||||||
| 			ctx.Users.Remove(user); | 			repository.DeleteUser(user); | ||||||
| 			await ctx.SaveChangesAsync(); | 			await db.Ctx.SaveChangesAsync(); | ||||||
|  | 			 | ||||||
|  | 			Logger.Information("Deleted user \"{Username}\" (GUID {Guid}).", user.Name, user.UserGuid); | ||||||
| 			return DeleteUserResult.Deleted; | 			return DeleteUserResult.Deleted; | ||||||
| 		} catch (Exception e) { | 		} catch (Exception e) { | ||||||
| 			Logger.Error(e, "Could not delete user \"{Name}\" (GUID {Guid}).", user.Name, user.UserGuid); | 			Logger.Error(e, "Could not delete user \"{Username}\" (GUID {Guid}).", user.Name, user.UserGuid); | ||||||
| 			return DeleteUserResult.Failed; | 			return DeleteUserResult.Failed; | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -1,38 +0,0 @@ | |||||||
| using System.Collections.Immutable; |  | ||||||
| using Phantom.Controller.Database.Entities; |  | ||||||
|  |  | ||||||
| namespace Phantom.Controller.Services.Users; |  | ||||||
|  |  | ||||||
| internal static class UserPasswords { |  | ||||||
| 	private const int MinimumLength = 16; |  | ||||||
| 	 |  | ||||||
| 	public static ImmutableArray<PasswordRequirementViolation> CheckRequirements(string password) { |  | ||||||
| 		var violations = ImmutableArray.CreateBuilder<PasswordRequirementViolation>(); |  | ||||||
| 		 |  | ||||||
| 		if (password.Length < MinimumLength) { |  | ||||||
| 			violations.Add(new PasswordRequirementViolation.TooShort(MinimumLength)); |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if (!password.Any(char.IsLower)) { |  | ||||||
| 			violations.Add(new PasswordRequirementViolation.LowercaseLetterRequired()); |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if (!password.Any(char.IsUpper)) { |  | ||||||
| 			violations.Add(new PasswordRequirementViolation.UppercaseLetterRequired()); |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if (!password.Any(char.IsDigit)) { |  | ||||||
| 			violations.Add(new PasswordRequirementViolation.DigitRequired()); |  | ||||||
| 		} |  | ||||||
| 		 |  | ||||||
| 		return violations.ToImmutable(); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	public static void Set(UserEntity user, string password) { |  | ||||||
| 		user.PasswordHash = BCrypt.Net.BCrypt.HashPassword(password); |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	public static bool Verify(UserEntity user, string password) { |  | ||||||
| 		return BCrypt.Net.BCrypt.Verify(password, user.PasswordHash); |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| using NetMQ; | using NetMQ; | ||||||
| using Phantom.Common.Data.Agent; | using Phantom.Common.Data; | ||||||
|  |  | ||||||
| namespace Phantom.Controller; | namespace Phantom.Controller; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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; | ||||||
|   | |||||||
| @@ -26,6 +26,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Common.Data", "Comm | |||||||
| EndProject | EndProject | ||||||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Common.Data.Tests", "Common\Phantom.Common.Data.Tests\Phantom.Common.Data.Tests.csproj", "{435D7981-DFDA-46A0-8CD8-CD8C117935D7}" | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Common.Data.Tests", "Common\Phantom.Common.Data.Tests\Phantom.Common.Data.Tests.csproj", "{435D7981-DFDA-46A0-8CD8-CD8C117935D7}" | ||||||
| EndProject | EndProject | ||||||
|  | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Common.Data.Web", "Common\Phantom.Common.Data.Web\Phantom.Common.Data.Web.csproj", "{BC969D0B-0019-48E0-9FAF-F5CC906AAF09}" | ||||||
|  | EndProject | ||||||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Common.Logging", "Common\Phantom.Common.Logging\Phantom.Common.Logging.csproj", "{D7F55010-B3ED-42A5-8D83-E754FFC5F2A2}" | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Common.Logging", "Common\Phantom.Common.Logging\Phantom.Common.Logging.csproj", "{D7F55010-B3ED-42A5-8D83-E754FFC5F2A2}" | ||||||
| EndProject | EndProject | ||||||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Common.Messages.Agent", "Common\Phantom.Common.Messages.Agent\Phantom.Common.Messages.Agent.csproj", "{95B55357-F8F0-48C2-A1C2-5EA997651783}" | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Common.Messages.Agent", "Common\Phantom.Common.Messages.Agent\Phantom.Common.Messages.Agent.csproj", "{95B55357-F8F0-48C2-A1C2-5EA997651783}" | ||||||
| @@ -58,7 +60,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Web.Bootstrap", "We | |||||||
| EndProject | EndProject | ||||||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Web.Components", "Web\Phantom.Web.Components\Phantom.Web.Components.csproj", "{3F4F9059-F869-42D3-B92C-90D27ADFC42D}" | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Web.Components", "Web\Phantom.Web.Components\Phantom.Web.Components.csproj", "{3F4F9059-F869-42D3-B92C-90D27ADFC42D}" | ||||||
| EndProject | EndProject | ||||||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Web.Identity", "Web\Phantom.Web.Identity\Phantom.Web.Identity.csproj", "{A9870842-FE7A-4760-95DC-9D485DDDA31F}" | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Phantom.Web.Services", "Web\Phantom.Web.Services\Phantom.Web.Services.csproj", "{7B0EEE34-A586-4629-AC51-16757DE53261}" | ||||||
| EndProject | EndProject | ||||||
| Global | Global | ||||||
| 	GlobalSection(SolutionConfigurationPlatforms) = preSolution | 	GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||||||
| @@ -90,6 +92,10 @@ Global | |||||||
| 		{435D7981-DFDA-46A0-8CD8-CD8C117935D7}.Debug|Any CPU.Build.0 = Debug|Any CPU | 		{435D7981-DFDA-46A0-8CD8-CD8C117935D7}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||||||
| 		{435D7981-DFDA-46A0-8CD8-CD8C117935D7}.Release|Any CPU.ActiveCfg = Release|Any CPU | 		{435D7981-DFDA-46A0-8CD8-CD8C117935D7}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||||||
| 		{435D7981-DFDA-46A0-8CD8-CD8C117935D7}.Release|Any CPU.Build.0 = Release|Any CPU | 		{435D7981-DFDA-46A0-8CD8-CD8C117935D7}.Release|Any CPU.Build.0 = Release|Any CPU | ||||||
|  | 		{BC969D0B-0019-48E0-9FAF-F5CC906AAF09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||||||
|  | 		{BC969D0B-0019-48E0-9FAF-F5CC906AAF09}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||||||
|  | 		{BC969D0B-0019-48E0-9FAF-F5CC906AAF09}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||||||
|  | 		{BC969D0B-0019-48E0-9FAF-F5CC906AAF09}.Release|Any CPU.Build.0 = Release|Any CPU | ||||||
| 		{D7F55010-B3ED-42A5-8D83-E754FFC5F2A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | 		{D7F55010-B3ED-42A5-8D83-E754FFC5F2A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||||||
| 		{D7F55010-B3ED-42A5-8D83-E754FFC5F2A2}.Debug|Any CPU.Build.0 = Debug|Any CPU | 		{D7F55010-B3ED-42A5-8D83-E754FFC5F2A2}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||||||
| 		{D7F55010-B3ED-42A5-8D83-E754FFC5F2A2}.Release|Any CPU.ActiveCfg = Release|Any CPU | 		{D7F55010-B3ED-42A5-8D83-E754FFC5F2A2}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||||||
| @@ -150,10 +156,10 @@ Global | |||||||
| 		{3F4F9059-F869-42D3-B92C-90D27ADFC42D}.Debug|Any CPU.Build.0 = Debug|Any CPU | 		{3F4F9059-F869-42D3-B92C-90D27ADFC42D}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||||||
| 		{3F4F9059-F869-42D3-B92C-90D27ADFC42D}.Release|Any CPU.ActiveCfg = Release|Any CPU | 		{3F4F9059-F869-42D3-B92C-90D27ADFC42D}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||||||
| 		{3F4F9059-F869-42D3-B92C-90D27ADFC42D}.Release|Any CPU.Build.0 = Release|Any CPU | 		{3F4F9059-F869-42D3-B92C-90D27ADFC42D}.Release|Any CPU.Build.0 = Release|Any CPU | ||||||
| 		{A9870842-FE7A-4760-95DC-9D485DDDA31F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | 		{7B0EEE34-A586-4629-AC51-16757DE53261}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||||||
| 		{A9870842-FE7A-4760-95DC-9D485DDDA31F}.Debug|Any CPU.Build.0 = Debug|Any CPU | 		{7B0EEE34-A586-4629-AC51-16757DE53261}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||||||
| 		{A9870842-FE7A-4760-95DC-9D485DDDA31F}.Release|Any CPU.ActiveCfg = Release|Any CPU | 		{7B0EEE34-A586-4629-AC51-16757DE53261}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||||||
| 		{A9870842-FE7A-4760-95DC-9D485DDDA31F}.Release|Any CPU.Build.0 = Release|Any CPU | 		{7B0EEE34-A586-4629-AC51-16757DE53261}.Release|Any CPU.Build.0 = Release|Any CPU | ||||||
| 	EndGlobalSection | 	EndGlobalSection | ||||||
| 	GlobalSection(NestedProjects) = preSolution | 	GlobalSection(NestedProjects) = preSolution | ||||||
| 		{418BE1BF-9F63-4B46-B4E4-DF64C3B3DDA7} = {F5878792-64C8-4ECF-A075-66341FF97127} | 		{418BE1BF-9F63-4B46-B4E4-DF64C3B3DDA7} = {F5878792-64C8-4ECF-A075-66341FF97127} | ||||||
| @@ -165,6 +171,7 @@ Global | |||||||
| 		{95B55357-F8F0-48C2-A1C2-5EA997651783} = {01CB1A81-8950-471C-BFDF-F135FDDB2C18} | 		{95B55357-F8F0-48C2-A1C2-5EA997651783} = {01CB1A81-8950-471C-BFDF-F135FDDB2C18} | ||||||
| 		{6E798DEB-8921-41A2-8AFB-E4416A9E0704} = {01CB1A81-8950-471C-BFDF-F135FDDB2C18} | 		{6E798DEB-8921-41A2-8AFB-E4416A9E0704} = {01CB1A81-8950-471C-BFDF-F135FDDB2C18} | ||||||
| 		{435D7981-DFDA-46A0-8CD8-CD8C117935D7} = {D781E00D-8563-4102-A0CD-477A679193B5} | 		{435D7981-DFDA-46A0-8CD8-CD8C117935D7} = {D781E00D-8563-4102-A0CD-477A679193B5} | ||||||
|  | 		{BC969D0B-0019-48E0-9FAF-F5CC906AAF09} = {01CB1A81-8950-471C-BFDF-F135FDDB2C18} | ||||||
| 		{A0F1C595-96B6-4DBF-8C16-6B99223F8F35} = {0AB9471E-6228-4EB7-802E-3102B3952AAD} | 		{A0F1C595-96B6-4DBF-8C16-6B99223F8F35} = {0AB9471E-6228-4EB7-802E-3102B3952AAD} | ||||||
| 		{E3AD566F-384A-489A-A3BB-EA3BA400C18C} = {0AB9471E-6228-4EB7-802E-3102B3952AAD} | 		{E3AD566F-384A-489A-A3BB-EA3BA400C18C} = {0AB9471E-6228-4EB7-802E-3102B3952AAD} | ||||||
| 		{81625B4A-3DB6-48BD-A739-D23DA02107D1} = {0AB9471E-6228-4EB7-802E-3102B3952AAD} | 		{81625B4A-3DB6-48BD-A739-D23DA02107D1} = {0AB9471E-6228-4EB7-802E-3102B3952AAD} | ||||||
| @@ -178,6 +185,6 @@ Global | |||||||
| 		{7CA2E5FE-E507-4DC6-930C-E18711A9F856} = {92B26F48-235F-4500-BD55-800F06A0BA39} | 		{7CA2E5FE-E507-4DC6-930C-E18711A9F856} = {92B26F48-235F-4500-BD55-800F06A0BA39} | ||||||
| 		{83FA86DB-34E4-4C2C-832C-90F491CA10C7} = {92B26F48-235F-4500-BD55-800F06A0BA39} | 		{83FA86DB-34E4-4C2C-832C-90F491CA10C7} = {92B26F48-235F-4500-BD55-800F06A0BA39} | ||||||
| 		{3F4F9059-F869-42D3-B92C-90D27ADFC42D} = {92B26F48-235F-4500-BD55-800F06A0BA39} | 		{3F4F9059-F869-42D3-B92C-90D27ADFC42D} = {92B26F48-235F-4500-BD55-800F06A0BA39} | ||||||
| 		{A9870842-FE7A-4760-95DC-9D485DDDA31F} = {92B26F48-235F-4500-BD55-800F06A0BA39} | 		{7B0EEE34-A586-4629-AC51-16757DE53261} = {92B26F48-235F-4500-BD55-800F06A0BA39} | ||||||
| 	EndGlobalSection | 	EndGlobalSection | ||||||
| EndGlobal | EndGlobal | ||||||
|   | |||||||
| @@ -149,8 +149,8 @@ The repository includes a [Rider](https://www.jetbrains.com/rider/) projects wit | |||||||
|    - `Controller` starts the Controller. |    - `Controller` starts the Controller. | ||||||
|    - `Web` starts the Web server. |    - `Web` starts the Web server. | ||||||
|    - `Agent 1`, `Agent 2`, `Agent 3` start one of the Agents. |    - `Agent 1`, `Agent 2`, `Agent 3` start one of the Agents. | ||||||
|    - `Controller + Agent` starts the Controller and Agent 1. |    - `Controller + Web + Agent` starts the Controller and Agent 1. | ||||||
|    - `Controller + Agent x3` starts the Controller and Agent 1, 2, and 3. |    - `Controller + Web + Agent x3` starts the Controller and Agent 1, 2, and 3. | ||||||
|  |  | ||||||
| ## Bootstrap | ## Bootstrap | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| namespace Phantom.Utils.Rpc.Message;  | namespace Phantom.Utils.Rpc.Message; | ||||||
|  |  | ||||||
| public interface IReply { | public interface IReply { | ||||||
| 	uint SequenceId { get; } | 	uint SequenceId { get; } | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
| using NetMQ.Sockets; | using NetMQ.Sockets; | ||||||
| using Phantom.Utils.Rpc.Message; | using Phantom.Utils.Rpc.Message; | ||||||
|  |  | ||||||
| namespace Phantom.Utils.Rpc;  | namespace Phantom.Utils.Rpc; | ||||||
|  |  | ||||||
| public sealed class RpcConnectionToServer<TListener> { | public sealed class RpcConnectionToServer<TListener> { | ||||||
| 	private readonly ClientSocket socket; | 	private readonly ClientSocket socket; | ||||||
|   | |||||||
| @@ -2,17 +2,57 @@ | |||||||
|  |  | ||||||
| public abstract record Result<TValue, TError> { | public abstract record Result<TValue, TError> { | ||||||
| 	private Result() {} | 	private Result() {} | ||||||
|  |  | ||||||
|  | 	public abstract TValue Value { get; init; } | ||||||
|  | 	public abstract TError Error { get; init; } | ||||||
|  |  | ||||||
|  | 	public static implicit operator Result<TValue, TError>(TValue value) { | ||||||
|  | 		return new Ok(value); | ||||||
|  | 	} | ||||||
| 	 | 	 | ||||||
| 	public sealed record Ok(TValue Value) : Result<TValue, TError>; | 	public static implicit operator Result<TValue, TError>(TError error) { | ||||||
|  | 		return new Fail(error); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	public static implicit operator bool(Result<TValue, TError> result) { | ||||||
|  | 		return result is Ok; | ||||||
|  | 	} | ||||||
| 	 | 	 | ||||||
| 	public sealed record Fail(TError Error) : Result<TValue, TError>; | 	public sealed record Ok(TValue Value) : Result<TValue, TError> { | ||||||
|  | 		public override TError Error { | ||||||
|  | 			get => throw new InvalidOperationException("Attempted to get error from Ok result."); | ||||||
|  | 			init {} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	public sealed record Fail(TError Error) : Result<TValue, TError> { | ||||||
|  | 		public override TValue Value { | ||||||
|  | 			get => throw new InvalidOperationException("Attempted to get value from Fail result."); | ||||||
|  | 			init {} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| public abstract record Result<TError> { | public abstract record Result<TError> { | ||||||
| 	private Result() {} | 	private Result() {} | ||||||
|  | 	 | ||||||
|  | 	public abstract TError Error { get; init; } | ||||||
|  |  | ||||||
|  | 	public static implicit operator Result<TError>(TError error) { | ||||||
|  | 		return new Fail(error); | ||||||
|  | 	} | ||||||
|  | 	 | ||||||
|  | 	public static implicit operator bool(Result<TError> result) { | ||||||
|  | 		return result is Ok; | ||||||
|  | 	} | ||||||
|  | 	 | ||||||
| 	public sealed record Ok : Result<TError> { | 	public sealed record Ok : Result<TError> { | ||||||
| 		internal static Ok Instance { get; } = new (); | 		internal static Ok Instance { get; } = new (); | ||||||
|  | 		 | ||||||
|  | 		public override TError Error { | ||||||
|  | 			get => throw new InvalidOperationException("Attempted to get error from Ok result."); | ||||||
|  | 			init {} | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 	 | 	 | ||||||
| 	public sealed record Fail(TError Error) : Result<TError>; | 	public sealed record Fail(TError Error) : Result<TError>; | ||||||
|   | |||||||
| @@ -1,8 +0,0 @@ | |||||||
| using Phantom.Controller.Database.Entities; |  | ||||||
|  |  | ||||||
| namespace Phantom.Web.Identity.Interfaces; |  | ||||||
|  |  | ||||||
| public interface ILoginEvents { |  | ||||||
| 	void UserLoggedIn(UserEntity user); |  | ||||||
| 	void UserLoggedOut(Guid userGuid); |  | ||||||
| } |  | ||||||
| @@ -1,9 +1,7 @@ | |||||||
| using System.Diagnostics.CodeAnalysis; | using System.Diagnostics.CodeAnalysis; | ||||||
| using Microsoft.AspNetCore.Authentication; | using Microsoft.AspNetCore.Authentication; | ||||||
| using Phantom.Web.Identity.Authentication; |  | ||||||
| using Phantom.Web.Identity.Interfaces; |  | ||||||
| 
 | 
 | ||||||
| namespace Phantom.Web.Identity; | namespace Phantom.Web.Services.Authentication; | ||||||
| 
 | 
 | ||||||
| sealed class PhantomIdentityMiddleware { | sealed class PhantomIdentityMiddleware { | ||||||
| 	public const string LoginPath = "/login"; | 	public const string LoginPath = "/login"; | ||||||
| @@ -2,12 +2,10 @@ | |||||||
| using Microsoft.AspNetCore.Authentication; | using Microsoft.AspNetCore.Authentication; | ||||||
| using Microsoft.AspNetCore.Authentication.Cookies; | using Microsoft.AspNetCore.Authentication.Cookies; | ||||||
| using Phantom.Common.Logging; | using Phantom.Common.Logging; | ||||||
| using Phantom.Controller.Services.Users; |  | ||||||
| using Phantom.Utils.Cryptography; | using Phantom.Utils.Cryptography; | ||||||
| using Phantom.Web.Identity.Interfaces; |  | ||||||
| using ILogger = Serilog.ILogger; | using ILogger = Serilog.ILogger; | ||||||
| 
 | 
 | ||||||
| namespace Phantom.Web.Identity.Authentication; | namespace Phantom.Web.Services.Authentication; | ||||||
| 
 | 
 | ||||||
| public sealed class PhantomLoginManager { | public sealed class PhantomLoginManager { | ||||||
| 	private static readonly ILogger Logger = PhantomLogger.Create<PhantomLoginManager>(); | 	private static readonly ILogger Logger = PhantomLogger.Create<PhantomLoginManager>(); | ||||||
| @@ -4,7 +4,7 @@ using Phantom.Common.Logging; | |||||||
| using Phantom.Utils.Tasks; | using Phantom.Utils.Tasks; | ||||||
| using ILogger = Serilog.ILogger; | using ILogger = Serilog.ILogger; | ||||||
| 
 | 
 | ||||||
| namespace Phantom.Web.Identity.Authentication; | namespace Phantom.Web.Services.Authentication; | ||||||
| 
 | 
 | ||||||
| public sealed class PhantomLoginStore { | public sealed class PhantomLoginStore { | ||||||
| 	private static readonly ILogger Logger = PhantomLogger.Create<PhantomLoginStore>(); | 	private static readonly ILogger Logger = PhantomLogger.Create<PhantomLoginStore>(); | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| using Microsoft.AspNetCore.Authorization; | using Microsoft.AspNetCore.Authorization; | ||||||
| 
 | 
 | ||||||
| namespace Phantom.Web.Identity.Authorization; | namespace Phantom.Web.Services.Authorization; | ||||||
| 
 | 
 | ||||||
| sealed class PermissionBasedPolicyHandler : AuthorizationHandler<PermissionBasedPolicyRequirement> { | sealed class PermissionBasedPolicyHandler : AuthorizationHandler<PermissionBasedPolicyRequirement> { | ||||||
| 	private readonly PermissionManager permissionManager; | 	private readonly PermissionManager permissionManager; | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| using Microsoft.AspNetCore.Authorization; | using Microsoft.AspNetCore.Authorization; | ||||||
| using Phantom.Web.Identity.Data; | using Phantom.Common.Data.Web.Users; | ||||||
| 
 | 
 | ||||||
| namespace Phantom.Web.Identity.Authorization; | namespace Phantom.Web.Services.Authorization; | ||||||
| 
 | 
 | ||||||
| sealed record PermissionBasedPolicyRequirement(Permission Permission) : IAuthorizationRequirement; | sealed record PermissionBasedPolicyRequirement(Permission Permission) : IAuthorizationRequirement; | ||||||
							
								
								
									
										15
									
								
								Web/Phantom.Web.Services/Authorization/PermissionManager.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								Web/Phantom.Web.Services/Authorization/PermissionManager.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | using System.Security.Claims; | ||||||
|  | using Phantom.Common.Data.Web.Users; | ||||||
|  |  | ||||||
|  | namespace Phantom.Web.Services.Authorization; | ||||||
|  |  | ||||||
|  | // TODO | ||||||
|  | public class PermissionManager { | ||||||
|  | 	public IdentityPermissions GetPermissions(ClaimsPrincipal user, bool refreshCache = false) { | ||||||
|  | 		 | ||||||
|  | 	} | ||||||
|  | 	 | ||||||
|  | 	public bool CheckPermission(ClaimsPrincipal user, Permission permission, bool refreshCache = false) { | ||||||
|  | 		return false; | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| @using Microsoft.AspNetCore.Components.Authorization | @using Microsoft.AspNetCore.Components.Authorization | ||||||
| @using Phantom.Web.Identity.Data | @using Phantom.Common.Data.Web.Users | ||||||
| @inject PermissionManager PermissionManager | @inject PermissionManager PermissionManager | ||||||
| 
 | 
 | ||||||
| <AuthorizeView> | <AuthorizeView> | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| using System.Diagnostics.CodeAnalysis; | using System.Diagnostics.CodeAnalysis; | ||||||
| 
 | 
 | ||||||
| namespace Phantom.Web.Identity.Interfaces; | namespace Phantom.Web.Services; | ||||||
| 
 | 
 | ||||||
| public interface INavigation { | public interface INavigation { | ||||||
| 	string BasePath { get; } | 	string BasePath { get; } | ||||||
							
								
								
									
										10
									
								
								Web/Phantom.Web.Services/Instances/InstanceManager.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								Web/Phantom.Web.Services/Instances/InstanceManager.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | using Phantom.Common.Data.Replies; | ||||||
|  |  | ||||||
|  | namespace Phantom.Web.Services.Instances; | ||||||
|  |  | ||||||
|  | // TODO | ||||||
|  | public class InstanceManager { | ||||||
|  | 	public async Task<InstanceActionResult<SendCommandToInstanceResult>> SendCommand(Guid instanceGuid, string command) { | ||||||
|  | 		 | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -10,17 +10,14 @@ | |||||||
|   </PropertyGroup> |   </PropertyGroup> | ||||||
|    |    | ||||||
|   <ItemGroup> |   <ItemGroup> | ||||||
|     <SupportedPlatform Include="browser" /> |     <ProjectReference Include="..\..\Common\Phantom.Common.Data\Phantom.Common.Data.csproj" /> | ||||||
|   </ItemGroup> |     <ProjectReference Include="..\..\Common\Phantom.Common.Data.Web\Phantom.Common.Data.Web.csproj" /> | ||||||
|    |  | ||||||
|   <ItemGroup> |  | ||||||
|     <PackageReference Include="Microsoft.AspNetCore.Components.Authorization" /> |  | ||||||
|     <PackageReference Include="Microsoft.AspNetCore.Components.Web" /> |  | ||||||
|   </ItemGroup> |  | ||||||
|    |  | ||||||
|   <ItemGroup> |  | ||||||
|     <ProjectReference Include="..\..\Common\Phantom.Common.Logging\Phantom.Common.Logging.csproj" /> |     <ProjectReference Include="..\..\Common\Phantom.Common.Logging\Phantom.Common.Logging.csproj" /> | ||||||
|     <ProjectReference Include="..\..\Utils\Phantom.Utils\Phantom.Utils.csproj" /> |     <ProjectReference Include="..\..\Common\Phantom.Common.Messages.Web\Phantom.Common.Messages.Web.csproj" /> | ||||||
|  |   </ItemGroup> | ||||||
|  |    | ||||||
|  |   <ItemGroup> | ||||||
|  |     <AdditionalFiles Include="Authorization\PermissionView.razor" /> | ||||||
|   </ItemGroup> |   </ItemGroup> | ||||||
| 
 | 
 | ||||||
| </Project> | </Project> | ||||||
| @@ -1,43 +1,33 @@ | |||||||
| using Microsoft.AspNetCore.Authentication.Cookies; | using Microsoft.AspNetCore.Authorization; | ||||||
| using Microsoft.AspNetCore.Authorization; |  | ||||||
| using Microsoft.AspNetCore.Components.Authorization; | using Microsoft.AspNetCore.Components.Authorization; | ||||||
| using Microsoft.AspNetCore.Components.Server; | using Microsoft.AspNetCore.Components.Server; | ||||||
| using Phantom.Web.Identity.Authentication; | using Phantom.Common.Data.Web.Users; | ||||||
| using Phantom.Web.Identity.Authorization; | using Phantom.Web.Services.Authentication; | ||||||
|  | using Phantom.Web.Services.Authorization; | ||||||
|  | using Phantom.Web.Services.Rpc; | ||||||
| 
 | 
 | ||||||
| namespace Phantom.Web.Identity; | namespace Phantom.Web.Services; | ||||||
| 
 |  | ||||||
| public static class PhantomIdentityExtensions { |  | ||||||
| 	public static void AddPhantomIdentity(this IServiceCollection services, CancellationToken cancellationToken) { |  | ||||||
| 		services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(ConfigureIdentityCookie); |  | ||||||
| 		services.AddAuthorization(ConfigureAuthorization); |  | ||||||
| 
 | 
 | ||||||
|  | public static class PhantomWebServices { | ||||||
|  | 	public static void AddPhantomServices(this IServiceCollection services, CancellationToken cancellationToken) { | ||||||
|  | 		services.AddSingleton<MessageListener>(); | ||||||
|  | 		services.AddSingleton<ControllerCommunication>(); | ||||||
|  | 		services.AddSingleton<PermissionManager>(); | ||||||
|  | 		 | ||||||
| 		services.AddSingleton(PhantomLoginStore.Create(cancellationToken)); | 		services.AddSingleton(PhantomLoginStore.Create(cancellationToken)); | ||||||
| 		services.AddScoped<PhantomLoginManager>(); | 		services.AddScoped<PhantomLoginManager>(); | ||||||
| 		 | 		 | ||||||
|  | 		services.AddAuthorization(ConfigureAuthorization); | ||||||
| 		services.AddScoped<IAuthorizationHandler, PermissionBasedPolicyHandler>(); | 		services.AddScoped<IAuthorizationHandler, PermissionBasedPolicyHandler>(); | ||||||
| 		services.AddScoped<AuthenticationStateProvider, ServerAuthenticationStateProvider>(); | 		services.AddScoped<AuthenticationStateProvider, ServerAuthenticationStateProvider>(); | ||||||
| 	} | 	} | ||||||
| 
 | 	 | ||||||
| 	public static void UsePhantomIdentity(this IApplicationBuilder application) { | 	public static void UsePhantomServices(this IApplicationBuilder application) { | ||||||
| 		application.UseAuthentication(); | 		application.UseAuthentication(); | ||||||
| 		application.UseAuthorization(); | 		application.UseAuthorization(); | ||||||
| 		application.UseWhen(PhantomIdentityMiddleware.AcceptsPath, static app => app.UseMiddleware<PhantomIdentityMiddleware>()); | 		application.UseWhen(PhantomIdentityMiddleware.AcceptsPath, static app => app.UseMiddleware<PhantomIdentityMiddleware>()); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	private static void ConfigureIdentityCookie(CookieAuthenticationOptions o) { |  | ||||||
| 		o.Cookie.Name = "Phantom.Identity"; |  | ||||||
| 		o.Cookie.HttpOnly = true; |  | ||||||
| 		o.Cookie.SameSite = SameSiteMode.Lax; |  | ||||||
| 
 |  | ||||||
| 		o.ExpireTimeSpan = TimeSpan.FromDays(30); |  | ||||||
| 		o.SlidingExpiration = true; |  | ||||||
| 
 |  | ||||||
| 		o.LoginPath = PhantomIdentityMiddleware.LoginPath; |  | ||||||
| 		o.LogoutPath = PhantomIdentityMiddleware.LogoutPath; |  | ||||||
| 		o.AccessDeniedPath = PhantomIdentityMiddleware.LoginPath; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	private static void ConfigureAuthorization(AuthorizationOptions o) { | 	private static void ConfigureAuthorization(AuthorizationOptions o) { | ||||||
| 		foreach (var permission in Permission.All) { | 		foreach (var permission in Permission.All) { | ||||||
| 			o.AddPolicy(permission.Id, policy => policy.Requirements.Add(new PermissionBasedPolicyRequirement(permission))); | 			o.AddPolicy(permission.Id, policy => policy.Requirements.Add(new PermissionBasedPolicyRequirement(permission))); | ||||||
							
								
								
									
										20
									
								
								Web/Phantom.Web.Services/Rpc/ControllerCommunication.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								Web/Phantom.Web.Services/Rpc/ControllerCommunication.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | using Phantom.Common.Messages.Web; | ||||||
|  | using Phantom.Utils.Rpc; | ||||||
|  |  | ||||||
|  | namespace Phantom.Web.Services.Rpc; | ||||||
|  |  | ||||||
|  | public sealed class ControllerCommunication { | ||||||
|  | 	private readonly RpcConnectionToServer<IMessageToControllerListener> connection; | ||||||
|  | 	 | ||||||
|  | 	public ControllerCommunication(RpcConnectionToServer<IMessageToControllerListener> connection) { | ||||||
|  | 		this.connection = connection; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	public Task Send<TMessage>(TMessage message) where TMessage : IMessageToController { | ||||||
|  | 		return connection.Send(message); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	public Task<TReply?> Send<TMessage, TReply>(TMessage message, TimeSpan timeout) where TMessage : IMessageToController<TReply> where TReply : class { | ||||||
|  | 		return connection.Send<TMessage, TReply>(message, timeout, CancellationToken.None); | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										19
									
								
								Web/Phantom.Web.Services/Rpc/MessageListener.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								Web/Phantom.Web.Services/Rpc/MessageListener.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | using Phantom.Common.Messages.Web; | ||||||
|  | using Phantom.Common.Messages.Web.BiDirectional; | ||||||
|  | using Phantom.Utils.Rpc; | ||||||
|  | using Phantom.Utils.Rpc.Message; | ||||||
|  |  | ||||||
|  | namespace Phantom.Web.Services.Rpc;  | ||||||
|  |  | ||||||
|  | public sealed class MessageListener : IMessageToWebListener { | ||||||
|  | 	private readonly RpcConnectionToServer<IMessageToControllerListener> connection; | ||||||
|  | 	 | ||||||
|  | 	public MessageListener(RpcConnectionToServer<IMessageToControllerListener> connection) { | ||||||
|  | 		this.connection = connection; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	public Task<NoReply> HandleReply(ReplyMessage message) { | ||||||
|  | 		connection.Receive(message); | ||||||
|  | 		return Task.FromResult(NoReply.Instance); | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										14
									
								
								Web/Phantom.Web.Services/Rpc/RpcClientRuntime.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								Web/Phantom.Web.Services/Rpc/RpcClientRuntime.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | using Phantom.Common.Messages.Web; | ||||||
|  | using Phantom.Common.Messages.Web.BiDirectional; | ||||||
|  | using Phantom.Utils.Rpc; | ||||||
|  | using Phantom.Utils.Rpc.Sockets; | ||||||
|  |  | ||||||
|  | namespace Phantom.Web.Services.Rpc; | ||||||
|  |  | ||||||
|  | public sealed class RpcClientRuntime : RpcClientRuntime<IMessageToWebListener, IMessageToControllerListener, ReplyMessage> { | ||||||
|  | 	public static Task Launch(RpcClientSocket<IMessageToWebListener, IMessageToControllerListener, ReplyMessage> socket, IMessageToWebListener messageListener, SemaphoreSlim disconnectSemaphore, CancellationToken receiveCancellationToken) { | ||||||
|  | 		return new RpcClientRuntime(socket, messageListener, disconnectSemaphore, receiveCancellationToken).Launch(); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	private RpcClientRuntime(RpcClientSocket<IMessageToWebListener, IMessageToControllerListener, ReplyMessage> socket, IMessageToWebListener messageListener, SemaphoreSlim disconnectSemaphore, CancellationToken receiveCancellationToken) : base(socket, messageListener, disconnectSemaphore, receiveCancellationToken) {} | ||||||
|  | } | ||||||
							
								
								
									
										5
									
								
								Web/Phantom.Web.Services/Users/UserManager.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								Web/Phantom.Web.Services/Users/UserManager.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | namespace Phantom.Web.Services.Users; | ||||||
|  |  | ||||||
|  | public sealed class UserManager { | ||||||
|  | 	 | ||||||
|  | } | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| @using Phantom.Web.Identity.Interfaces | @using Phantom.Web.Services | ||||||
| @using Phantom.Web.Identity.Authentication | @using Phantom.Web.Services.Authentication | ||||||
| @inject INavigation Nav | @inject INavigation Nav | ||||||
| @inject NavigationManager NavigationManager | @inject NavigationManager NavigationManager | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,21 +0,0 @@ | |||||||
| using Phantom.Controller.Database.Entities; |  | ||||||
| using Phantom.Controller.Services.Audit; |  | ||||||
| using Phantom.Web.Identity.Interfaces; |  | ||||||
|  |  | ||||||
| namespace Phantom.Web.Base; |  | ||||||
|  |  | ||||||
| sealed class LoginEvents : ILoginEvents { |  | ||||||
| 	private readonly AuditLog auditLog; |  | ||||||
|  |  | ||||||
| 	public LoginEvents(AuditLog auditLog) { |  | ||||||
| 		this.auditLog = auditLog; |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	public void UserLoggedIn(UserEntity user) { |  | ||||||
| 		auditLog.AddUserLoggedInEvent(user); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	public void UserLoggedOut(Guid userGuid) { |  | ||||||
| 		auditLog.AddUserLoggedOutEvent(userGuid); |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| using System.Diagnostics.CodeAnalysis; | using System.Diagnostics.CodeAnalysis; | ||||||
| using System.Web; | using System.Web; | ||||||
| using Microsoft.AspNetCore.Components; | using Microsoft.AspNetCore.Components; | ||||||
| using Phantom.Web.Identity.Interfaces; | using Phantom.Web.Services; | ||||||
|  |  | ||||||
| namespace Phantom.Web.Base; | namespace Phantom.Web.Base; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| using Microsoft.AspNetCore.Components; | using Microsoft.AspNetCore.Components; | ||||||
| using Microsoft.AspNetCore.Components.Authorization; | using Microsoft.AspNetCore.Components.Authorization; | ||||||
|  | using Phantom.Common.Data.Web.Users; | ||||||
| using Phantom.Common.Logging; | using Phantom.Common.Logging; | ||||||
| using Phantom.Web.Identity.Authorization; | using Phantom.Web.Services.Authorization; | ||||||
| using Phantom.Web.Identity.Data; |  | ||||||
| using ILogger = Serilog.ILogger; | using ILogger = Serilog.ILogger; | ||||||
|  |  | ||||||
| namespace Phantom.Web.Base; | namespace Phantom.Web.Base; | ||||||
|   | |||||||
| @@ -2,6 +2,6 @@ | |||||||
|  |  | ||||||
| namespace Phantom.Web; | namespace Phantom.Web; | ||||||
|  |  | ||||||
| public sealed record Configuration(ILogger Logger, string Host, ushort Port, string BasePath, string KeyFolderPath, CancellationToken CancellationToken) { | sealed record Configuration(ILogger Logger, string Host, ushort Port, string BasePath, string DataProtectionKeyFolderPath, CancellationToken CancellationToken) { | ||||||
| 	public string HttpUrl => "http://" + Host + ":" + Port; | 	public string HttpUrl => "http://" + Host + ":" + Port; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| @using Phantom.Controller.Services | @using Phantom.Web.Services.Authorization | ||||||
|  | @using Phantom.Common.Data.Web.Users | ||||||
| @inject ServiceConfiguration Configuration | @inject ServiceConfiguration Configuration | ||||||
| @inject PermissionManager PermissionManager | @inject PermissionManager PermissionManager | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| @page | @page | ||||||
| @using Phantom.Web.Identity.Interfaces | @using Phantom.Web.Services | ||||||
| @model Phantom.Web.Layout.ErrorModel | @model Phantom.Web.Layout.ErrorModel | ||||||
| @inject INavigation Navigation | @inject INavigation Navigation | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| @using Phantom.Web.Identity.Interfaces | @using Phantom.Web.Services | ||||||
| @namespace Phantom.Web.Layout | @namespace Phantom.Web.Layout | ||||||
| @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers | ||||||
| @inject INavigation Navigation | @inject INavigation Navigation | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user