mirror of
				https://github.com/chylex/Minecraft-Phantom-Panel.git
				synced 2025-10-31 11:17:15 +01:00 
			
		
		
		
	Compare commits
	
		
			5 Commits
		
	
	
		
			4c3b81c54a
			...
			8c623171f3
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8c623171f3 | |||
| 62f8c685f0 | |||
| 4a110db078 | |||
| f683a1f700 | |||
| 3ffb37529c | 
| @@ -6,5 +6,5 @@ namespace Phantom.Common.Data.Web.Users; | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record LogInSuccess( | ||||
| 	[property: MemoryPackOrder(0)] AuthenticatedUserInfo UserInfo, | ||||
| 	[property: MemoryPackOrder(1)] ImmutableArray<byte> Token | ||||
| 	[property: MemoryPackOrder(1)] ImmutableArray<byte> AuthToken | ||||
| ); | ||||
|   | ||||
| @@ -0,0 +1,5 @@ | ||||
| namespace Phantom.Common.Data.Web.Users; | ||||
|  | ||||
| public enum UserActionFailure { | ||||
| 	NotAuthorized | ||||
| } | ||||
| @@ -0,0 +1,25 @@ | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data.Replies; | ||||
|  | ||||
| namespace Phantom.Common.Data.Web.Users; | ||||
|  | ||||
| [MemoryPackable] | ||||
| [MemoryPackUnion(0, typeof(OfUserActionFailure))] | ||||
| [MemoryPackUnion(1, typeof(OfInstanceActionFailure))] | ||||
| public abstract partial record UserInstanceActionFailure { | ||||
| 	internal UserInstanceActionFailure() {} | ||||
| 	 | ||||
| 	public static implicit operator UserInstanceActionFailure(UserActionFailure failure) { | ||||
| 		return new OfUserActionFailure(failure); | ||||
| 	} | ||||
| 	 | ||||
| 	public static implicit operator UserInstanceActionFailure(InstanceActionFailure failure) { | ||||
| 		return new OfInstanceActionFailure(failure); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record OfUserActionFailure([property: MemoryPackOrder(0)] UserActionFailure Failure) : UserInstanceActionFailure; | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record OfInstanceActionFailure([property: MemoryPackOrder(0)] InstanceActionFailure Failure) : UserInstanceActionFailure; | ||||
| @@ -1,5 +1,6 @@ | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using MemoryPack; | ||||
| using Phantom.Utils.Result; | ||||
|  | ||||
| namespace Phantom.Common.Data; | ||||
|  | ||||
| @@ -33,10 +34,18 @@ public sealed partial class Result<TValue, TError> { | ||||
| 		return hasValue && EqualityComparer<TValue>.Default.Equals(value, expectedValue); | ||||
| 	} | ||||
|  | ||||
| 	public TOutput Map<TOutput>(Func<TValue, TOutput> valueConverter, Func<TError, TOutput> errorConverter) { | ||||
| 	public TOutput Into<TOutput>(Func<TValue, TOutput> valueConverter, Func<TError, TOutput> errorConverter) { | ||||
| 		return hasValue ? valueConverter(value!) : errorConverter(error!); | ||||
| 	} | ||||
|  | ||||
| 	public Result<TValue, TNewError> MapError<TNewError>(Func<TError, TNewError> errorConverter) { | ||||
| 		return hasValue ? value! : errorConverter(error!); | ||||
| 	} | ||||
|  | ||||
| 	public Utils.Result.Result Variant() { | ||||
| 		return hasValue ? new Ok<TValue>(Value) : new Err<TError>(Error); | ||||
| 	} | ||||
|  | ||||
| 	public static implicit operator Result<TValue, TError>(TValue value) { | ||||
| 		return new Result<TValue, TError>(hasValue: true, value, default); | ||||
| 	} | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| using System.Collections.Immutable; | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Utils.Actor; | ||||
|  | ||||
| @@ -7,8 +8,8 @@ namespace Phantom.Common.Messages.Web.ToController; | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record ChangeUserRolesMessage( | ||||
| 	[property: MemoryPackOrder(0)] Guid LoggedInUserGuid, | ||||
| 	[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken, | ||||
| 	[property: MemoryPackOrder(1)] Guid SubjectUserGuid, | ||||
| 	[property: MemoryPackOrder(2)] ImmutableHashSet<Guid> AddToRoleGuids, | ||||
| 	[property: MemoryPackOrder(3)] ImmutableHashSet<Guid> RemoveFromRoleGuids | ||||
| ) : IMessageToController, ICanReply<ChangeUserRolesResult>; | ||||
| ) : IMessageToController, ICanReply<Result<ChangeUserRolesResult, UserActionFailure>>; | ||||
|   | ||||
| @@ -1,15 +1,16 @@ | ||||
| using MemoryPack; | ||||
| using System.Collections.Immutable; | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Instance; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Common.Data.Web.Instance; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Utils.Actor; | ||||
|  | ||||
| namespace Phantom.Common.Messages.Web.ToController;  | ||||
| namespace Phantom.Common.Messages.Web.ToController; | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record CreateOrUpdateInstanceMessage( | ||||
| 	[property: MemoryPackOrder(0)] Guid LoggedInUserGuid, | ||||
| 	[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken, | ||||
| 	[property: MemoryPackOrder(1)] Guid InstanceGuid, | ||||
| 	[property: MemoryPackOrder(2)] InstanceConfiguration Configuration | ||||
| ) : IMessageToController, ICanReply<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>; | ||||
| ) : IMessageToController, ICanReply<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>; | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| using MemoryPack; | ||||
| using System.Collections.Immutable; | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Utils.Actor; | ||||
|  | ||||
| @@ -6,7 +8,7 @@ namespace Phantom.Common.Messages.Web.ToController; | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record CreateUserMessage( | ||||
| 	[property: MemoryPackOrder(0)] Guid LoggedInUserGuid, | ||||
| 	[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken, | ||||
| 	[property: MemoryPackOrder(1)] string Username, | ||||
| 	[property: MemoryPackOrder(2)] string Password | ||||
| ) : IMessageToController, ICanReply<CreateUserResult>; | ||||
| ) : IMessageToController, ICanReply<Result<CreateUserResult, UserActionFailure>>; | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| using MemoryPack; | ||||
| using System.Collections.Immutable; | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Utils.Actor; | ||||
|  | ||||
| @@ -6,6 +8,6 @@ namespace Phantom.Common.Messages.Web.ToController; | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record DeleteUserMessage( | ||||
| 	[property: MemoryPackOrder(0)] Guid LoggedInUserGuid, | ||||
| 	[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken, | ||||
| 	[property: MemoryPackOrder(1)] Guid SubjectUserGuid | ||||
| ) : IMessageToController, ICanReply<DeleteUserResult>; | ||||
| ) : IMessageToController, ICanReply<Result<DeleteUserResult, UserActionFailure>>; | ||||
|   | ||||
| @@ -1,11 +1,14 @@ | ||||
| using System.Collections.Immutable; | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Web.AuditLog; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Utils.Actor; | ||||
|  | ||||
| namespace Phantom.Common.Messages.Web.ToController; | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record GetAuditLogMessage( | ||||
| 	[property: MemoryPackOrder(0)] int Count | ||||
| ) : IMessageToController, ICanReply<ImmutableArray<AuditLogItem>>; | ||||
| 	[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken, | ||||
| 	[property: MemoryPackOrder(1)] int Count | ||||
| ) : IMessageToController, ICanReply<Result<ImmutableArray<AuditLogItem>, UserActionFailure>>; | ||||
|   | ||||
| @@ -9,5 +9,5 @@ namespace Phantom.Common.Messages.Web.ToController; | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record GetAuthenticatedUser( | ||||
| 	[property: MemoryPackOrder(0)] Guid UserGuid, | ||||
| 	[property: MemoryPackOrder(1)] ImmutableArray<byte> SessionToken | ||||
| 	[property: MemoryPackOrder(1)] ImmutableArray<byte> AuthToken | ||||
| ) : IMessageToController, ICanReply<Optional<AuthenticatedUserInfo>>; | ||||
|   | ||||
| @@ -1,11 +1,14 @@ | ||||
| using System.Collections.Immutable; | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Web.EventLog; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Utils.Actor; | ||||
|  | ||||
| namespace Phantom.Common.Messages.Web.ToController; | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record GetEventLogMessage( | ||||
| 	[property: MemoryPackOrder(0)] int Count | ||||
| ) : IMessageToController, ICanReply<ImmutableArray<EventLogItem>>; | ||||
| 	[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken, | ||||
| 	[property: MemoryPackOrder(1)] int Count | ||||
| ) : IMessageToController, ICanReply<Result<ImmutableArray<EventLogItem>, UserActionFailure>>; | ||||
|   | ||||
| @@ -1,13 +1,15 @@ | ||||
| using MemoryPack; | ||||
| using System.Collections.Immutable; | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Utils.Actor; | ||||
|  | ||||
| namespace Phantom.Common.Messages.Web.ToController;  | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record LaunchInstanceMessage( | ||||
| 	[property: MemoryPackOrder(0)] Guid LoggedInUserGuid, | ||||
| 	[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken, | ||||
| 	[property: MemoryPackOrder(1)] Guid AgentGuid, | ||||
| 	[property: MemoryPackOrder(2)] Guid InstanceGuid | ||||
| ) : IMessageToController, ICanReply<Result<LaunchInstanceResult, InstanceActionFailure>>; | ||||
| ) : IMessageToController, ICanReply<Result<LaunchInstanceResult, UserInstanceActionFailure>>; | ||||
|   | ||||
| @@ -1,14 +1,16 @@ | ||||
| using MemoryPack; | ||||
| using System.Collections.Immutable; | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Utils.Actor; | ||||
|  | ||||
| namespace Phantom.Common.Messages.Web.ToController;  | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record SendCommandToInstanceMessage( | ||||
| 	[property: MemoryPackOrder(0)] Guid LoggedInUserGuid, | ||||
| 	[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken, | ||||
| 	[property: MemoryPackOrder(1)] Guid AgentGuid, | ||||
| 	[property: MemoryPackOrder(2)] Guid InstanceGuid, | ||||
| 	[property: MemoryPackOrder(3)] string Command | ||||
| ) : IMessageToController, ICanReply<Result<SendCommandToInstanceResult, InstanceActionFailure>>; | ||||
| ) : IMessageToController, ICanReply<Result<SendCommandToInstanceResult, UserInstanceActionFailure>>; | ||||
|   | ||||
| @@ -1,15 +1,17 @@ | ||||
| using MemoryPack; | ||||
| using System.Collections.Immutable; | ||||
| using MemoryPack; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Minecraft; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Utils.Actor; | ||||
|  | ||||
| namespace Phantom.Common.Messages.Web.ToController;  | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record StopInstanceMessage( | ||||
| 	[property: MemoryPackOrder(0)] Guid LoggedInUserGuid, | ||||
| 	[property: MemoryPackOrder(0)] ImmutableArray<byte> AuthToken, | ||||
| 	[property: MemoryPackOrder(1)] Guid AgentGuid, | ||||
| 	[property: MemoryPackOrder(2)] Guid InstanceGuid, | ||||
| 	[property: MemoryPackOrder(3)] MinecraftStopStrategy StopStrategy | ||||
| ) : IMessageToController, ICanReply<Result<StopInstanceResult, InstanceActionFailure>>; | ||||
| ) : IMessageToController, ICanReply<Result<StopInstanceResult, UserInstanceActionFailure>>; | ||||
|   | ||||
| @@ -0,0 +1,8 @@ | ||||
| using MemoryPack; | ||||
|  | ||||
| namespace Phantom.Common.Messages.Web.ToWeb;  | ||||
|  | ||||
| [MemoryPackable(GenerateType.VersionTolerant)] | ||||
| public sealed partial record RefreshUserSessionMessage( | ||||
| 	[property: MemoryPackOrder(0)] Guid UserGuid | ||||
| ) : IMessageToWeb; | ||||
| @@ -28,26 +28,27 @@ public static class WebMessageRegistries { | ||||
| 		ToController.Add<LogOutMessage>(3); | ||||
| 		ToController.Add<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(4); | ||||
| 		ToController.Add<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(5); | ||||
| 		ToController.Add<CreateUserMessage, CreateUserResult>(6); | ||||
| 		ToController.Add<DeleteUserMessage, DeleteUserResult>(7); | ||||
| 		ToController.Add<CreateUserMessage, Result<CreateUserResult, UserActionFailure>>(6); | ||||
| 		ToController.Add<DeleteUserMessage, Result<DeleteUserResult, UserActionFailure>>(7); | ||||
| 		ToController.Add<GetUsersMessage, ImmutableArray<UserInfo>>(8); | ||||
| 		ToController.Add<GetRolesMessage, ImmutableArray<RoleInfo>>(9); | ||||
| 		ToController.Add<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(10); | ||||
| 		ToController.Add<ChangeUserRolesMessage, ChangeUserRolesResult>(11); | ||||
| 		ToController.Add<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(12); | ||||
| 		ToController.Add<LaunchInstanceMessage, Result<LaunchInstanceResult, InstanceActionFailure>>(13); | ||||
| 		ToController.Add<StopInstanceMessage, Result<StopInstanceResult, InstanceActionFailure>>(14); | ||||
| 		ToController.Add<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, InstanceActionFailure>>(15); | ||||
| 		ToController.Add<ChangeUserRolesMessage, Result<ChangeUserRolesResult, UserActionFailure>>(11); | ||||
| 		ToController.Add<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(12); | ||||
| 		ToController.Add<LaunchInstanceMessage, Result<LaunchInstanceResult, UserInstanceActionFailure>>(13); | ||||
| 		ToController.Add<StopInstanceMessage, Result<StopInstanceResult, UserInstanceActionFailure>>(14); | ||||
| 		ToController.Add<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, UserInstanceActionFailure>>(15); | ||||
| 		ToController.Add<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(16); | ||||
| 		ToController.Add<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(17); | ||||
| 		ToController.Add<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(18); | ||||
| 		ToController.Add<GetEventLogMessage, ImmutableArray<EventLogItem>>(19); | ||||
| 		ToController.Add<GetAuditLogMessage, Result<ImmutableArray<AuditLogItem>, UserActionFailure>>(18); | ||||
| 		ToController.Add<GetEventLogMessage, Result<ImmutableArray<EventLogItem>, UserActionFailure>>(19); | ||||
| 		ToController.Add<ReplyMessage>(127); | ||||
| 		 | ||||
| 		ToWeb.Add<RegisterWebResultMessage>(0); | ||||
| 		ToWeb.Add<RefreshAgentsMessage>(1); | ||||
| 		ToWeb.Add<RefreshInstancesMessage>(2); | ||||
| 		ToWeb.Add<InstanceOutputMessage>(3); | ||||
| 		ToWeb.Add<RefreshUserSessionMessage>(4); | ||||
| 		ToWeb.Add<ReplyMessage>(127); | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,26 @@ | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Controller.Database.Entities; | ||||
| using Phantom.Utils.Collections; | ||||
|  | ||||
| namespace Phantom.Controller.Database.Repositories; | ||||
|  | ||||
| public sealed class PermissionRepository { | ||||
| 	private readonly ILazyDbContext db; | ||||
|  | ||||
| 	public PermissionRepository(ILazyDbContext db) { | ||||
| 		this.db = db; | ||||
| 	} | ||||
|  | ||||
| 	public async Task<PermissionSet> GetAllUserPermissions(UserEntity user) { | ||||
| 		var userPermissions = db.Ctx.UserPermissions | ||||
| 		                        .Where(up => up.UserGuid == user.UserGuid) | ||||
| 		                        .Select(static up => up.PermissionId); | ||||
|  | ||||
| 		var rolePermissions = db.Ctx.UserRoles | ||||
| 		                        .Where(ur => ur.UserGuid == user.UserGuid) | ||||
| 		                        .Join(db.Ctx.RolePermissions, static ur => ur.RoleGuid, static rp => rp.RoleGuid, static (ur, rp) => rp.PermissionId); | ||||
|  | ||||
| 		return new PermissionSet(await userPermissions.Union(rolePermissions).AsAsyncEnumerable().ToImmutableSetAsync()); | ||||
| 	} | ||||
| } | ||||
| @@ -10,12 +10,14 @@ using Phantom.Common.Data.Replies; | ||||
| using Phantom.Common.Data.Web.Agent; | ||||
| using Phantom.Common.Data.Web.Instance; | ||||
| using Phantom.Common.Data.Web.Minecraft; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Common.Messages.Agent; | ||||
| using Phantom.Common.Messages.Agent.ToAgent; | ||||
| using Phantom.Controller.Database; | ||||
| using Phantom.Controller.Database.Entities; | ||||
| using Phantom.Controller.Minecraft; | ||||
| using Phantom.Controller.Services.Instances; | ||||
| using Phantom.Controller.Services.Users.Sessions; | ||||
| using Phantom.Utils.Actor; | ||||
| using Phantom.Utils.Actor.Mailbox; | ||||
| using Phantom.Utils.Actor.Tasks; | ||||
| @@ -32,7 +34,7 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> { | ||||
| 	private static readonly TimeSpan DisconnectionRecheckInterval = TimeSpan.FromSeconds(5); | ||||
| 	private static readonly TimeSpan DisconnectionThreshold = TimeSpan.FromSeconds(12); | ||||
|  | ||||
| 	public readonly record struct Init(Guid AgentGuid, AgentConfiguration AgentConfiguration, ControllerState ControllerState, MinecraftVersions MinecraftVersions, IDbContextProvider DbProvider, CancellationToken CancellationToken); | ||||
| 	public readonly record struct Init(Guid AgentGuid, AgentConfiguration AgentConfiguration, ControllerState ControllerState, MinecraftVersions MinecraftVersions, UserLoginManager UserLoginManager, IDbContextProvider DbProvider, CancellationToken CancellationToken); | ||||
| 	 | ||||
| 	public static Props<ICommand> Factory(Init init) { | ||||
| 		return Props<ICommand>.Create(() => new AgentActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume, MailboxType = UnboundedJumpAheadMailbox.Name }); | ||||
| @@ -40,6 +42,7 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> { | ||||
|  | ||||
| 	private readonly ControllerState controllerState; | ||||
| 	private readonly MinecraftVersions minecraftVersions; | ||||
| 	private readonly UserLoginManager userLoginManager; | ||||
| 	private readonly IDbContextProvider dbProvider; | ||||
| 	private readonly CancellationToken cancellationToken; | ||||
| 	 | ||||
| @@ -76,6 +79,7 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> { | ||||
| 	private AgentActor(Init init) { | ||||
| 		this.controllerState = init.ControllerState; | ||||
| 		this.minecraftVersions = init.MinecraftVersions; | ||||
| 		this.userLoginManager = init.UserLoginManager; | ||||
| 		this.dbProvider = init.DbProvider; | ||||
| 		this.cancellationToken = init.CancellationToken; | ||||
| 		 | ||||
| @@ -94,11 +98,11 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> { | ||||
| 		Receive<NotifyIsAliveCommand>(NotifyIsAlive); | ||||
| 		Receive<UpdateStatsCommand>(UpdateStats); | ||||
| 		Receive<UpdateJavaRuntimesCommand>(UpdateJavaRuntimes); | ||||
| 		ReceiveAndReplyLater<CreateOrUpdateInstanceCommand, Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(CreateOrUpdateInstance); | ||||
| 		ReceiveAndReplyLater<CreateOrUpdateInstanceCommand, Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(CreateOrUpdateInstance); | ||||
| 		Receive<UpdateInstanceStatusCommand>(UpdateInstanceStatus); | ||||
| 		ReceiveAndReplyLater<LaunchInstanceCommand, Result<LaunchInstanceResult, InstanceActionFailure>>(LaunchInstance); | ||||
| 		ReceiveAndReplyLater<StopInstanceCommand, Result<StopInstanceResult, InstanceActionFailure>>(StopInstance); | ||||
| 		ReceiveAndReplyLater<SendCommandToInstanceCommand, Result<SendCommandToInstanceResult, InstanceActionFailure>>(SendMinecraftCommand); | ||||
| 		ReceiveAndReplyLater<LaunchInstanceCommand, Result<LaunchInstanceResult, UserInstanceActionFailure>>(LaunchInstance); | ||||
| 		ReceiveAndReplyLater<StopInstanceCommand, Result<StopInstanceResult, UserInstanceActionFailure>>(StopInstance); | ||||
| 		ReceiveAndReplyLater<SendCommandToInstanceCommand, Result<SendCommandToInstanceResult, UserInstanceActionFailure>>(SendMinecraftCommand); | ||||
| 		Receive<ReceiveInstanceDataCommand>(ReceiveInstanceData); | ||||
| 	} | ||||
|  | ||||
| @@ -146,13 +150,21 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private Task<Result<TReply, InstanceActionFailure>> RequestInstance<TCommand, TReply>(Guid instanceGuid, TCommand command) where TCommand : InstanceActor.ICommand, ICanReply<Result<TReply, InstanceActionFailure>> { | ||||
| 	private async Task<Result<TReply, UserInstanceActionFailure>> RequestInstance<TCommand, TReply>(ImmutableArray<byte> authToken, Guid instanceGuid, Func<Guid, TCommand> commandFactoryFromLoggedInUserGuid) where TCommand : InstanceActor.ICommand, ICanReply<Result<TReply, InstanceActionFailure>> { | ||||
| 		var loggedInUser = userLoginManager.GetLoggedInUser(authToken); | ||||
| 		if (!loggedInUser.CheckPermission(Permission.ControlInstances)) { | ||||
| 			return (UserInstanceActionFailure) UserActionFailure.NotAuthorized; | ||||
| 		} | ||||
| 		 | ||||
| 		var command = commandFactoryFromLoggedInUserGuid(loggedInUser.Guid!.Value); | ||||
| 		 | ||||
| 		if (instanceActorByGuid.TryGetValue(instanceGuid, out var instance)) { | ||||
| 			return instance.Request(command, cancellationToken); | ||||
| 			var result = await instance.Request(command, cancellationToken); | ||||
| 			return result.MapError(static error => (UserInstanceActionFailure) error); | ||||
| 		} | ||||
| 		else { | ||||
| 			Logger.Warning("Could not deliver command {CommandType} to instance {InstanceGuid}, instance not found.", command.GetType().Name, instanceGuid); | ||||
| 			return Task.FromResult<Result<TReply, InstanceActionFailure>>(InstanceActionFailure.InstanceDoesNotExist); | ||||
| 			return (UserInstanceActionFailure) InstanceActionFailure.InstanceDoesNotExist; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @@ -183,15 +195,15 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> { | ||||
| 	 | ||||
| 	public sealed record UpdateJavaRuntimesCommand(ImmutableArray<TaggedJavaRuntime> JavaRuntimes) : ICommand; | ||||
| 	 | ||||
| 	public sealed record CreateOrUpdateInstanceCommand(Guid AuditLogUserGuid, Guid InstanceGuid, InstanceConfiguration Configuration) : ICommand, ICanReply<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>; | ||||
| 	public sealed record CreateOrUpdateInstanceCommand(ImmutableArray<byte> AuthToken, Guid InstanceGuid, InstanceConfiguration Configuration) : ICommand, ICanReply<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>; | ||||
| 	 | ||||
| 	public sealed record UpdateInstanceStatusCommand(Guid InstanceGuid, IInstanceStatus Status) : ICommand; | ||||
|  | ||||
| 	public sealed record LaunchInstanceCommand(Guid InstanceGuid, Guid AuditLogUserGuid) : ICommand, ICanReply<Result<LaunchInstanceResult, InstanceActionFailure>>; | ||||
| 	public sealed record LaunchInstanceCommand(ImmutableArray<byte> AuthToken, Guid InstanceGuid) : ICommand, ICanReply<Result<LaunchInstanceResult, UserInstanceActionFailure>>; | ||||
| 	 | ||||
| 	public sealed record StopInstanceCommand(Guid InstanceGuid, Guid AuditLogUserGuid, MinecraftStopStrategy StopStrategy) : ICommand, ICanReply<Result<StopInstanceResult, InstanceActionFailure>>; | ||||
| 	public sealed record StopInstanceCommand(ImmutableArray<byte> AuthToken, Guid InstanceGuid, MinecraftStopStrategy StopStrategy) : ICommand, ICanReply<Result<StopInstanceResult, UserInstanceActionFailure>>; | ||||
| 	 | ||||
| 	public sealed record SendCommandToInstanceCommand(Guid InstanceGuid, Guid AuditLogUserGuid, string Command) : ICommand, ICanReply<Result<SendCommandToInstanceResult, InstanceActionFailure>>; | ||||
| 	public sealed record SendCommandToInstanceCommand(ImmutableArray<byte> AuthToken, Guid InstanceGuid, string Command) : ICommand, ICanReply<Result<SendCommandToInstanceResult, UserInstanceActionFailure>>; | ||||
| 	 | ||||
| 	public sealed record ReceiveInstanceDataCommand(Instance Instance) : ICommand, IJumpAhead; | ||||
|  | ||||
| @@ -280,25 +292,30 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> { | ||||
| 		controllerState.UpdateAgentJavaRuntimes(agentGuid, javaRuntimes); | ||||
| 	} | ||||
| 	 | ||||
| 	private Task<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>> CreateOrUpdateInstance(CreateOrUpdateInstanceCommand command) { | ||||
| 	private Task<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>> CreateOrUpdateInstance(CreateOrUpdateInstanceCommand command) { | ||||
| 		var loggedInUser = userLoginManager.GetLoggedInUser(command.AuthToken); | ||||
| 		if (!loggedInUser.CheckPermission(Permission.CreateInstances)) { | ||||
| 			return Task.FromResult<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>((UserInstanceActionFailure) UserActionFailure.NotAuthorized); | ||||
| 		} | ||||
| 		 | ||||
| 		var instanceConfiguration = command.Configuration; | ||||
|  | ||||
| 		if (string.IsNullOrWhiteSpace(instanceConfiguration.InstanceName)) { | ||||
| 			return Task.FromResult<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(CreateOrUpdateInstanceResult.InstanceNameMustNotBeEmpty); | ||||
| 			return Task.FromResult<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(CreateOrUpdateInstanceResult.InstanceNameMustNotBeEmpty); | ||||
| 		} | ||||
| 		 | ||||
| 		if (instanceConfiguration.MemoryAllocation <= RamAllocationUnits.Zero) { | ||||
| 			return Task.FromResult<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(CreateOrUpdateInstanceResult.InstanceMemoryMustNotBeZero); | ||||
| 			return Task.FromResult<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(CreateOrUpdateInstanceResult.InstanceMemoryMustNotBeZero); | ||||
| 		} | ||||
| 		 | ||||
| 		return minecraftVersions.GetServerExecutableInfo(instanceConfiguration.MinecraftVersion, cancellationToken) | ||||
| 		                        .ContinueOnActor(CreateOrUpdateInstance1, command) | ||||
| 		                        .ContinueOnActor(CreateOrUpdateInstance1, loggedInUser.Guid!.Value, command) | ||||
| 		                        .Unwrap(); | ||||
| 	} | ||||
|  | ||||
| 	private Task<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>> CreateOrUpdateInstance1(FileDownloadInfo? serverExecutableInfo, CreateOrUpdateInstanceCommand command) { | ||||
| 	private Task<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>> CreateOrUpdateInstance1(FileDownloadInfo? serverExecutableInfo, Guid loggedInUserGuid, CreateOrUpdateInstanceCommand command) { | ||||
| 		if (serverExecutableInfo == null) { | ||||
| 			return Task.FromResult<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(CreateOrUpdateInstanceResult.MinecraftVersionDownloadInfoNotFound); | ||||
| 			return Task.FromResult<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(CreateOrUpdateInstanceResult.MinecraftVersionDownloadInfoNotFound); | ||||
| 		} | ||||
| 		 | ||||
| 		var instanceConfiguration = command.Configuration; | ||||
| @@ -308,13 +325,13 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> { | ||||
| 			instanceActorRef = CreateNewInstance(Instance.Offline(command.InstanceGuid, instanceConfiguration)); | ||||
| 		} | ||||
| 		 | ||||
| 		var configureInstanceCommand = new InstanceActor.ConfigureInstanceCommand(command.AuditLogUserGuid, command.InstanceGuid, instanceConfiguration, new InstanceLaunchProperties(serverExecutableInfo), isCreatingInstance); | ||||
| 		var configureInstanceCommand = new InstanceActor.ConfigureInstanceCommand(loggedInUserGuid, command.InstanceGuid, instanceConfiguration, new InstanceLaunchProperties(serverExecutableInfo), isCreatingInstance); | ||||
| 		 | ||||
| 		return instanceActorRef.Request(configureInstanceCommand, cancellationToken) | ||||
| 		                       .ContinueOnActor(CreateOrUpdateInstance2, configureInstanceCommand); | ||||
| 	} | ||||
| 	 | ||||
| 	private Result<CreateOrUpdateInstanceResult, InstanceActionFailure> CreateOrUpdateInstance2(Result<ConfigureInstanceResult, InstanceActionFailure> result, InstanceActor.ConfigureInstanceCommand command) { | ||||
| 	private Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure> CreateOrUpdateInstance2(Result<ConfigureInstanceResult, InstanceActionFailure> result, InstanceActor.ConfigureInstanceCommand command) { | ||||
| 		var instanceGuid = command.InstanceGuid; | ||||
| 		var instanceName = command.Configuration.InstanceName; | ||||
| 		var isCreating = command.IsCreatingInstance; | ||||
| @@ -330,7 +347,7 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> { | ||||
| 		else { | ||||
| 			string action = isCreating ? "adding" : "editing"; | ||||
| 			string relation = isCreating ? "to agent" : "in agent"; | ||||
| 			string reason = result.Map(ConfigureInstanceResultExtensions.ToSentence, InstanceActionFailureExtensions.ToSentence); | ||||
| 			string reason = result.Into(ConfigureInstanceResultExtensions.ToSentence, InstanceActionFailureExtensions.ToSentence); | ||||
| 			 | ||||
| 			Logger.Information("Failed " + action + " instance \"{InstanceName}\" (GUID {InstanceGuid}) " + relation + " \"{AgentName}\". {ErrorMessage}", instanceName, instanceGuid, configuration.AgentName, reason); | ||||
| 			 | ||||
| @@ -342,16 +359,16 @@ sealed class AgentActor : ReceiveActor<AgentActor.ICommand> { | ||||
| 		TellInstance(command.InstanceGuid, new InstanceActor.SetStatusCommand(command.Status)); | ||||
| 	} | ||||
|  | ||||
| 	private Task<Result<LaunchInstanceResult, InstanceActionFailure>> LaunchInstance(LaunchInstanceCommand command) { | ||||
| 		return RequestInstance<InstanceActor.LaunchInstanceCommand, LaunchInstanceResult>(command.InstanceGuid, new InstanceActor.LaunchInstanceCommand(command.AuditLogUserGuid)); | ||||
| 	private Task<Result<LaunchInstanceResult, UserInstanceActionFailure>> LaunchInstance(LaunchInstanceCommand command) { | ||||
| 		return RequestInstance<InstanceActor.LaunchInstanceCommand, LaunchInstanceResult>(command.AuthToken, command.InstanceGuid, static loggedInUserGuid => new InstanceActor.LaunchInstanceCommand(loggedInUserGuid)); | ||||
| 	} | ||||
|  | ||||
| 	private Task<Result<StopInstanceResult, InstanceActionFailure>> StopInstance(StopInstanceCommand command) { | ||||
| 		return RequestInstance<InstanceActor.StopInstanceCommand, StopInstanceResult>(command.InstanceGuid, new InstanceActor.StopInstanceCommand(command.AuditLogUserGuid, command.StopStrategy)); | ||||
| 	private Task<Result<StopInstanceResult, UserInstanceActionFailure>> StopInstance(StopInstanceCommand command) { | ||||
| 		return RequestInstance<InstanceActor.StopInstanceCommand, StopInstanceResult>(command.AuthToken, command.InstanceGuid, loggedInUserGuid => new InstanceActor.StopInstanceCommand(loggedInUserGuid, command.StopStrategy)); | ||||
| 	} | ||||
|  | ||||
| 	private Task<Result<SendCommandToInstanceResult, InstanceActionFailure>> SendMinecraftCommand(SendCommandToInstanceCommand command) { | ||||
| 		return RequestInstance<InstanceActor.SendCommandToInstanceCommand, SendCommandToInstanceResult>(command.InstanceGuid, new InstanceActor.SendCommandToInstanceCommand(command.AuditLogUserGuid, command.Command)); | ||||
| 	private Task<Result<SendCommandToInstanceResult, UserInstanceActionFailure>> SendMinecraftCommand(SendCommandToInstanceCommand command) { | ||||
| 		return RequestInstance<InstanceActor.SendCommandToInstanceCommand, SendCommandToInstanceResult>(command.AuthToken, command.InstanceGuid, loggedInUserGuid => new InstanceActor.SendCommandToInstanceCommand(loggedInUserGuid, command.Command)); | ||||
| 	} | ||||
|  | ||||
| 	private void ReceiveInstanceData(ReceiveInstanceDataCommand command) { | ||||
|   | ||||
| @@ -4,10 +4,12 @@ using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Agent; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Common.Data.Web.Agent; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Common.Messages.Agent; | ||||
| using Phantom.Common.Messages.Agent.ToAgent; | ||||
| using Phantom.Controller.Database; | ||||
| using Phantom.Controller.Minecraft; | ||||
| using Phantom.Controller.Services.Users.Sessions; | ||||
| using Phantom.Utils.Actor; | ||||
| using Phantom.Utils.Logging; | ||||
| using Phantom.Utils.Rpc.Runtime; | ||||
| @@ -22,17 +24,19 @@ sealed class AgentManager { | ||||
| 	private readonly AuthToken authToken; | ||||
| 	private readonly ControllerState controllerState; | ||||
| 	private readonly MinecraftVersions minecraftVersions; | ||||
| 	private readonly UserLoginManager userLoginManager; | ||||
| 	private readonly IDbContextProvider dbProvider; | ||||
| 	private readonly CancellationToken cancellationToken; | ||||
| 	 | ||||
| 	private readonly ConcurrentDictionary<Guid, ActorRef<AgentActor.ICommand>> agentsByGuid = new (); | ||||
| 	private readonly Func<Guid, AgentConfiguration, ActorRef<AgentActor.ICommand>> addAgentActorFactory; | ||||
| 	 | ||||
| 	public AgentManager(IActorRefFactory actorSystem, AuthToken authToken, ControllerState controllerState, MinecraftVersions minecraftVersions, IDbContextProvider dbProvider, CancellationToken cancellationToken) { | ||||
| 	public AgentManager(IActorRefFactory actorSystem, AuthToken authToken, ControllerState controllerState, MinecraftVersions minecraftVersions, UserLoginManager userLoginManager, IDbContextProvider dbProvider, CancellationToken cancellationToken) { | ||||
| 		this.actorSystem = actorSystem; | ||||
| 		this.authToken = authToken; | ||||
| 		this.controllerState = controllerState; | ||||
| 		this.minecraftVersions = minecraftVersions; | ||||
| 		this.userLoginManager = userLoginManager; | ||||
| 		this.dbProvider = dbProvider; | ||||
| 		this.cancellationToken = cancellationToken; | ||||
| 		 | ||||
| @@ -40,7 +44,7 @@ sealed class AgentManager { | ||||
| 	} | ||||
|  | ||||
| 	private ActorRef<AgentActor.ICommand> CreateAgentActor(Guid agentGuid, AgentConfiguration agentConfiguration) { | ||||
| 		var init = new AgentActor.Init(agentGuid, agentConfiguration, controllerState, minecraftVersions, dbProvider, cancellationToken); | ||||
| 		var init = new AgentActor.Init(agentGuid, agentConfiguration, controllerState, minecraftVersions, userLoginManager, dbProvider, cancellationToken); | ||||
| 		var name = "Agent:" + agentGuid; | ||||
| 		return actorSystem.ActorOf(AgentActor.Factory(init), name); | ||||
| 	} | ||||
| @@ -83,7 +87,7 @@ sealed class AgentManager { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public async Task<Result<TReply, InstanceActionFailure>> DoInstanceAction<TCommand, TReply>(Guid agentGuid, TCommand command) where TCommand : class, AgentActor.ICommand, ICanReply<Result<TReply, InstanceActionFailure>> { | ||||
| 		return agentsByGuid.TryGetValue(agentGuid, out var agent) ? await agent.Request(command, cancellationToken) : InstanceActionFailure.AgentDoesNotExist; | ||||
| 	public async Task<Result<TReply, UserInstanceActionFailure>> DoInstanceAction<TCommand, TReply>(Guid agentGuid, TCommand command) where TCommand : class, AgentActor.ICommand, ICanReply<Result<TReply, UserInstanceActionFailure>> { | ||||
| 		return agentsByGuid.TryGetValue(agentGuid, out var agent) ? await agent.Request(command, cancellationToken) : (UserInstanceActionFailure) InstanceActionFailure.AgentDoesNotExist; | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -11,6 +11,7 @@ using Phantom.Controller.Services.Events; | ||||
| using Phantom.Controller.Services.Instances; | ||||
| using Phantom.Controller.Services.Rpc; | ||||
| using Phantom.Controller.Services.Users; | ||||
| using Phantom.Controller.Services.Users.Sessions; | ||||
| using Phantom.Utils.Actor; | ||||
| using Phantom.Utils.Rpc.Runtime; | ||||
| using IMessageFromAgentToController = Phantom.Common.Messages.Agent.IMessageToController; | ||||
| @@ -24,17 +25,18 @@ public sealed class ControllerServices : IDisposable { | ||||
| 	private ControllerState ControllerState { get; } | ||||
| 	private MinecraftVersions MinecraftVersions { get; } | ||||
|  | ||||
| 	private AgentManager AgentManager { get; } | ||||
| 	private InstanceLogManager InstanceLogManager { get; } | ||||
| 	private EventLogManager EventLogManager { get; } | ||||
|  | ||||
| 	private AuthenticatedUserCache AuthenticatedUserCache { get; } | ||||
| 	private UserManager UserManager { get; } | ||||
| 	private RoleManager RoleManager { get; } | ||||
| 	private PermissionManager PermissionManager { get; } | ||||
|  | ||||
| 	private UserRoleManager UserRoleManager { get; } | ||||
| 	private UserLoginManager UserLoginManager { get; } | ||||
| 	private PermissionManager PermissionManager { get; } | ||||
|  | ||||
| 	private AgentManager AgentManager { get; } | ||||
| 	private InstanceLogManager InstanceLogManager { get; } | ||||
| 	 | ||||
| 	private AuditLogManager AuditLogManager { get; } | ||||
| 	private EventLogManager EventLogManager { get; } | ||||
|  | ||||
| 	public IRegistrationHandler<IMessageToAgent, IMessageFromAgentToController, RegisterAgentMessage> AgentRegistrationHandler { get; } | ||||
| 	public IRegistrationHandler<IMessageToWeb, IMessageFromWebToController, RegisterWebMessage> WebRegistrationHandler { get; } | ||||
| @@ -51,15 +53,16 @@ public sealed class ControllerServices : IDisposable { | ||||
| 		this.ControllerState = new ControllerState(); | ||||
| 		this.MinecraftVersions = new MinecraftVersions(); | ||||
| 		 | ||||
| 		this.AgentManager = new AgentManager(ActorSystem, agentAuthToken, ControllerState, MinecraftVersions, dbProvider, cancellationToken); | ||||
| 		this.AuthenticatedUserCache = new AuthenticatedUserCache(); | ||||
| 		this.UserManager = new UserManager(AuthenticatedUserCache, ControllerState, dbProvider); | ||||
| 		this.RoleManager = new RoleManager(dbProvider); | ||||
| 		this.UserRoleManager = new UserRoleManager(AuthenticatedUserCache, ControllerState, dbProvider); | ||||
| 		this.UserLoginManager = new UserLoginManager(AuthenticatedUserCache, UserManager, dbProvider); | ||||
| 		this.PermissionManager = new PermissionManager(dbProvider); | ||||
| 		 | ||||
| 		this.AgentManager = new AgentManager(ActorSystem, agentAuthToken, ControllerState, MinecraftVersions, UserLoginManager, dbProvider, cancellationToken); | ||||
| 		this.InstanceLogManager = new InstanceLogManager(); | ||||
| 		 | ||||
| 		this.UserManager = new UserManager(dbProvider); | ||||
| 		this.RoleManager = new RoleManager(dbProvider); | ||||
| 		this.PermissionManager = new PermissionManager(dbProvider); | ||||
|  | ||||
| 		this.UserRoleManager = new UserRoleManager(dbProvider); | ||||
| 		this.UserLoginManager = new UserLoginManager(UserManager, PermissionManager, dbProvider); | ||||
| 		this.AuditLogManager = new AuditLogManager(dbProvider); | ||||
| 		this.EventLogManager = new EventLogManager(ActorSystem, dbProvider, shutdownCancellationToken); | ||||
| 		 | ||||
|   | ||||
| @@ -19,6 +19,8 @@ sealed class ControllerState { | ||||
| 	public ObservableState<ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>.Receiver AgentJavaRuntimesByGuidReceiver => agentJavaRuntimesByGuid.ReceiverSide; | ||||
| 	public ObservableState<ImmutableDictionary<Guid, Instance>>.Receiver InstancesByGuidReceiver => instancesByGuid.ReceiverSide; | ||||
| 	 | ||||
| 	public event EventHandler<Guid>? UserUpdatedOrDeleted; | ||||
|  | ||||
| 	public void UpdateAgent(Agent agent) { | ||||
| 		agentsByGuid.PublisherSide.Publish(static (agentsByGuid, agent) => agentsByGuid.SetItem(agent.AgentGuid, agent), agent); | ||||
| 	} | ||||
| @@ -30,4 +32,8 @@ sealed class ControllerState { | ||||
| 	public void UpdateInstance(Instance instance) { | ||||
| 		instancesByGuid.PublisherSide.Publish(static (instancesByGuid, instance) => instancesByGuid.SetItem(instance.InstanceGuid, instance), instance); | ||||
| 	} | ||||
|  | ||||
| 	public void UpdateOrDeleteUser(Guid userGuid) { | ||||
| 		UserUpdatedOrDeleted?.Invoke(null, userGuid); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,8 +1,11 @@ | ||||
| using System.Collections.Immutable; | ||||
| using Akka.Actor; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Web.EventLog; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Controller.Database; | ||||
| using Phantom.Controller.Database.Repositories; | ||||
| using Phantom.Controller.Services.Users.Sessions; | ||||
| using Phantom.Utils.Actor; | ||||
|  | ||||
| namespace Phantom.Controller.Services.Events;  | ||||
| @@ -22,7 +25,11 @@ sealed partial class EventLogManager { | ||||
| 		databaseStorageActor.Tell(new EventLogDatabaseStorageActor.StoreEventCommand(eventGuid, utcTime, agentGuid, eventType, subjectId, extra)); | ||||
| 	} | ||||
| 	 | ||||
| 	public async Task<ImmutableArray<EventLogItem>> GetMostRecentItems(int count) { | ||||
| 	public async Task<Result<ImmutableArray<EventLogItem>, UserActionFailure>> GetMostRecentItems(LoggedInUser loggedInUser, int count) { | ||||
| 		if (!loggedInUser.CheckPermission(Permission.ViewEvents)) { | ||||
| 			return UserActionFailure.NotAuthorized; | ||||
| 		} | ||||
| 		 | ||||
| 		await using var db = dbProvider.Lazy(); | ||||
| 		return await new EventLogRepository(db).GetMostRecentItems(count, cancellationToken); | ||||
| 	} | ||||
|   | ||||
| @@ -30,22 +30,31 @@ sealed class WebMessageDataUpdateSenderActor : ReceiveActor<WebMessageDataUpdate | ||||
| 		ReceiveAsync<RefreshAgentsCommand>(RefreshAgents); | ||||
| 		ReceiveAsync<RefreshInstancesCommand>(RefreshInstances); | ||||
| 		ReceiveAsync<ReceiveInstanceLogsCommand>(ReceiveInstanceLogs); | ||||
| 		ReceiveAsync<RefreshUserSessionCommand>(RefreshUserSession); | ||||
| 	} | ||||
|  | ||||
| 	protected override void PreStart() { | ||||
| 		controllerState.AgentsByGuidReceiver.Register(SelfTyped, static state => new RefreshAgentsCommand(state)); | ||||
| 		controllerState.InstancesByGuidReceiver.Register(SelfTyped, static state => new RefreshInstancesCommand(state)); | ||||
|  | ||||
| 		 | ||||
| 		controllerState.UserUpdatedOrDeleted += OnUserUpdatedOrDeleted; | ||||
| 		 | ||||
| 		instanceLogManager.LogsReceived += OnInstanceLogsReceived; | ||||
| 	} | ||||
|  | ||||
| 	protected override void PostStop() { | ||||
| 		instanceLogManager.LogsReceived -= OnInstanceLogsReceived; | ||||
| 		 | ||||
| 		controllerState.UserUpdatedOrDeleted -= OnUserUpdatedOrDeleted; | ||||
|  | ||||
| 		controllerState.AgentsByGuidReceiver.Unregister(SelfTyped); | ||||
| 		controllerState.InstancesByGuidReceiver.Unregister(SelfTyped); | ||||
| 	} | ||||
|  | ||||
| 	private void OnUserUpdatedOrDeleted(object? sender, Guid userGuid) { | ||||
| 		selfCached.Tell(new RefreshUserSessionCommand(userGuid)); | ||||
| 	} | ||||
| 	 | ||||
| 	private void OnInstanceLogsReceived(object? sender, InstanceLogManager.Event e) { | ||||
| 		selfCached.Tell(new ReceiveInstanceLogsCommand(e.InstanceGuid, e.Lines)); | ||||
| 	} | ||||
| @@ -57,6 +66,8 @@ sealed class WebMessageDataUpdateSenderActor : ReceiveActor<WebMessageDataUpdate | ||||
| 	private sealed record RefreshInstancesCommand(ImmutableDictionary<Guid, Instance> Instances) : ICommand; | ||||
| 	 | ||||
| 	private sealed record ReceiveInstanceLogsCommand(Guid InstanceGuid, ImmutableArray<string> Lines) : ICommand; | ||||
| 	 | ||||
| 	private sealed record RefreshUserSessionCommand(Guid UserGuid) : ICommand; | ||||
|  | ||||
| 	private Task RefreshAgents(RefreshAgentsCommand command) { | ||||
| 		return connection.Send(new RefreshAgentsMessage(command.Agents.Values.ToImmutableArray())); | ||||
| @@ -69,4 +80,8 @@ sealed class WebMessageDataUpdateSenderActor : ReceiveActor<WebMessageDataUpdate | ||||
| 	private Task ReceiveInstanceLogs(ReceiveInstanceLogsCommand command) { | ||||
| 		return connection.Send(new InstanceOutputMessage(command.InstanceGuid, command.Lines)); | ||||
| 	} | ||||
| 	 | ||||
| 	private Task RefreshUserSession(RefreshUserSessionCommand command) { | ||||
| 		return connection.Send(new RefreshUserSessionMessage(command.UserGuid)); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -15,6 +15,7 @@ using Phantom.Controller.Services.Agents; | ||||
| using Phantom.Controller.Services.Events; | ||||
| using Phantom.Controller.Services.Instances; | ||||
| using Phantom.Controller.Services.Users; | ||||
| using Phantom.Controller.Services.Users.Sessions; | ||||
| using Phantom.Utils.Actor; | ||||
| using Phantom.Utils.Rpc.Runtime; | ||||
|  | ||||
| @@ -67,27 +68,27 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> { | ||||
|  | ||||
| 		var senderActorInit = new WebMessageDataUpdateSenderActor.Init(connection, controllerState, init.InstanceLogManager); | ||||
| 		Context.ActorOf(WebMessageDataUpdateSenderActor.Factory(senderActorInit), "DataUpdateSender"); | ||||
| 		 | ||||
|  | ||||
| 		ReceiveAsync<RegisterWebMessage>(HandleRegisterWeb); | ||||
| 		Receive<UnregisterWebMessage>(HandleUnregisterWeb); | ||||
| 		ReceiveAndReplyLater<LogInMessage, LogInSuccess?>(HandleLogIn); | ||||
| 		Receive<LogOutMessage>(HandleLogOut); | ||||
| 		ReceiveAndReply<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(GetAuthenticatedUser); | ||||
| 		ReceiveAndReplyLater<CreateOrUpdateAdministratorUserMessage, CreateOrUpdateAdministratorUserResult>(HandleCreateOrUpdateAdministratorUser); | ||||
| 		ReceiveAndReplyLater<CreateUserMessage, CreateUserResult>(HandleCreateUser); | ||||
| 		ReceiveAndReplyLater<CreateUserMessage, Result<CreateUserResult, UserActionFailure>>(HandleCreateUser); | ||||
| 		ReceiveAndReplyLater<GetUsersMessage, ImmutableArray<UserInfo>>(HandleGetUsers); | ||||
| 		ReceiveAndReplyLater<GetRolesMessage, ImmutableArray<RoleInfo>>(HandleGetRoles); | ||||
| 		ReceiveAndReplyLater<GetUserRolesMessage, ImmutableDictionary<Guid, ImmutableArray<Guid>>>(HandleGetUserRoles); | ||||
| 		ReceiveAndReplyLater<ChangeUserRolesMessage, ChangeUserRolesResult>(HandleChangeUserRoles); | ||||
| 		ReceiveAndReplyLater<DeleteUserMessage, DeleteUserResult>(HandleDeleteUser); | ||||
| 		ReceiveAndReplyLater<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(HandleCreateOrUpdateInstance); | ||||
| 		ReceiveAndReplyLater<LaunchInstanceMessage, Result<LaunchInstanceResult, InstanceActionFailure>>(HandleLaunchInstance); | ||||
| 		ReceiveAndReplyLater<StopInstanceMessage, Result<StopInstanceResult, InstanceActionFailure>>(HandleStopInstance); | ||||
| 		ReceiveAndReplyLater<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, InstanceActionFailure>>(HandleSendCommandToInstance); | ||||
| 		ReceiveAndReplyLater<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(HandleGetMinecraftVersions);  | ||||
| 		ReceiveAndReplyLater<ChangeUserRolesMessage, Result<ChangeUserRolesResult, UserActionFailure>>(HandleChangeUserRoles); | ||||
| 		ReceiveAndReplyLater<DeleteUserMessage, Result<DeleteUserResult, UserActionFailure>>(HandleDeleteUser); | ||||
| 		ReceiveAndReplyLater<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(HandleCreateOrUpdateInstance); | ||||
| 		ReceiveAndReplyLater<LaunchInstanceMessage, Result<LaunchInstanceResult, UserInstanceActionFailure>>(HandleLaunchInstance); | ||||
| 		ReceiveAndReplyLater<StopInstanceMessage, Result<StopInstanceResult, UserInstanceActionFailure>>(HandleStopInstance); | ||||
| 		ReceiveAndReplyLater<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, UserInstanceActionFailure>>(HandleSendCommandToInstance); | ||||
| 		ReceiveAndReplyLater<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(HandleGetMinecraftVersions); | ||||
| 		ReceiveAndReply<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(HandleGetAgentJavaRuntimes); | ||||
| 		ReceiveAndReplyLater<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(HandleGetAuditLog); | ||||
| 		ReceiveAndReplyLater<GetEventLogMessage, ImmutableArray<EventLogItem>>(HandleGetEventLog); | ||||
| 		ReceiveAndReplyLater<GetAuditLogMessage, Result<ImmutableArray<AuditLogItem>, UserActionFailure>>(HandleGetAuditLog); | ||||
| 		ReceiveAndReplyLater<GetEventLogMessage, Result<ImmutableArray<EventLogItem>, UserActionFailure>>(HandleGetEventLog); | ||||
| 		Receive<ReplyMessage>(HandleReply); | ||||
| 	} | ||||
|  | ||||
| @@ -108,15 +109,15 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> { | ||||
| 	} | ||||
|  | ||||
| 	private Optional<AuthenticatedUserInfo> GetAuthenticatedUser(GetAuthenticatedUser message) { | ||||
| 		return userLoginManager.GetAuthenticatedUser(message.UserGuid, message.SessionToken); | ||||
| 		return userLoginManager.GetAuthenticatedUser(message.UserGuid, message.AuthToken); | ||||
| 	} | ||||
|      | ||||
|  | ||||
| 	private Task<CreateOrUpdateAdministratorUserResult> HandleCreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUserMessage message) { | ||||
| 		return userManager.CreateOrUpdateAdministrator(message.Username, message.Password); | ||||
| 	} | ||||
|  | ||||
| 	private Task<CreateUserResult> HandleCreateUser(CreateUserMessage message) { | ||||
| 		return userManager.Create(message.LoggedInUserGuid, message.Username, message.Password); | ||||
| 	private Task<Result<CreateUserResult, UserActionFailure>> HandleCreateUser(CreateUserMessage message) { | ||||
| 		return userManager.Create(userLoginManager.GetLoggedInUser(message.AuthToken), message.Username, message.Password); | ||||
| 	} | ||||
|  | ||||
| 	private Task<ImmutableArray<UserInfo>> HandleGetUsers(GetUsersMessage message) { | ||||
| @@ -131,28 +132,28 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> { | ||||
| 		return userRoleManager.GetUserRoles(message.UserGuids); | ||||
| 	} | ||||
|  | ||||
| 	private Task<ChangeUserRolesResult> HandleChangeUserRoles(ChangeUserRolesMessage message) { | ||||
| 		return userRoleManager.ChangeUserRoles(message.LoggedInUserGuid, message.SubjectUserGuid, message.AddToRoleGuids, message.RemoveFromRoleGuids); | ||||
| 	private Task<Result<ChangeUserRolesResult, UserActionFailure>> HandleChangeUserRoles(ChangeUserRolesMessage message) { | ||||
| 		return userRoleManager.ChangeUserRoles(userLoginManager.GetLoggedInUser(message.AuthToken), message.SubjectUserGuid, message.AddToRoleGuids, message.RemoveFromRoleGuids); | ||||
| 	} | ||||
|  | ||||
| 	private Task<DeleteUserResult> HandleDeleteUser(DeleteUserMessage message) { | ||||
| 		return userManager.DeleteByGuid(message.LoggedInUserGuid, message.SubjectUserGuid); | ||||
| 	private Task<Result<DeleteUserResult, UserActionFailure>> HandleDeleteUser(DeleteUserMessage message) { | ||||
| 		return userManager.DeleteByGuid(userLoginManager.GetLoggedInUser(message.AuthToken), message.SubjectUserGuid); | ||||
| 	} | ||||
|  | ||||
| 	private Task<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>> HandleCreateOrUpdateInstance(CreateOrUpdateInstanceMessage message) { | ||||
| 		return agentManager.DoInstanceAction<AgentActor.CreateOrUpdateInstanceCommand, CreateOrUpdateInstanceResult>(message.Configuration.AgentGuid, new AgentActor.CreateOrUpdateInstanceCommand(message.LoggedInUserGuid, message.InstanceGuid, message.Configuration)); | ||||
| 	private Task<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>> HandleCreateOrUpdateInstance(CreateOrUpdateInstanceMessage message) { | ||||
| 		return agentManager.DoInstanceAction<AgentActor.CreateOrUpdateInstanceCommand, CreateOrUpdateInstanceResult>(message.Configuration.AgentGuid, new AgentActor.CreateOrUpdateInstanceCommand(message.AuthToken, message.InstanceGuid, message.Configuration)); | ||||
| 	} | ||||
|  | ||||
| 	private Task<Result<LaunchInstanceResult, InstanceActionFailure>> HandleLaunchInstance(LaunchInstanceMessage message) { | ||||
| 		return agentManager.DoInstanceAction<AgentActor.LaunchInstanceCommand, LaunchInstanceResult>(message.AgentGuid, new AgentActor.LaunchInstanceCommand(message.InstanceGuid, message.LoggedInUserGuid)); | ||||
| 	private Task<Result<LaunchInstanceResult, UserInstanceActionFailure>> HandleLaunchInstance(LaunchInstanceMessage message) { | ||||
| 		return agentManager.DoInstanceAction<AgentActor.LaunchInstanceCommand, LaunchInstanceResult>(message.AgentGuid, new AgentActor.LaunchInstanceCommand(message.AuthToken, message.InstanceGuid)); | ||||
| 	} | ||||
|  | ||||
| 	private Task<Result<StopInstanceResult, InstanceActionFailure>> HandleStopInstance(StopInstanceMessage message) { | ||||
| 		return agentManager.DoInstanceAction<AgentActor.StopInstanceCommand, StopInstanceResult>(message.AgentGuid, new AgentActor.StopInstanceCommand(message.InstanceGuid, message.LoggedInUserGuid, message.StopStrategy)); | ||||
| 	private Task<Result<StopInstanceResult, UserInstanceActionFailure>> HandleStopInstance(StopInstanceMessage message) { | ||||
| 		return agentManager.DoInstanceAction<AgentActor.StopInstanceCommand, StopInstanceResult>(message.AgentGuid, new AgentActor.StopInstanceCommand(message.AuthToken, message.InstanceGuid, message.StopStrategy)); | ||||
| 	} | ||||
|  | ||||
| 	private Task<Result<SendCommandToInstanceResult, InstanceActionFailure>> HandleSendCommandToInstance(SendCommandToInstanceMessage message) { | ||||
| 		return agentManager.DoInstanceAction<AgentActor.SendCommandToInstanceCommand, SendCommandToInstanceResult>(message.AgentGuid, new AgentActor.SendCommandToInstanceCommand(message.InstanceGuid, message.LoggedInUserGuid, message.Command)); | ||||
| 	private Task<Result<SendCommandToInstanceResult, UserInstanceActionFailure>> HandleSendCommandToInstance(SendCommandToInstanceMessage message) { | ||||
| 		return agentManager.DoInstanceAction<AgentActor.SendCommandToInstanceCommand, SendCommandToInstanceResult>(message.AgentGuid, new AgentActor.SendCommandToInstanceCommand(message.AuthToken, message.InstanceGuid, message.Command)); | ||||
| 	} | ||||
|  | ||||
| 	private Task<ImmutableArray<MinecraftVersion>> HandleGetMinecraftVersions(GetMinecraftVersionsMessage message) { | ||||
| @@ -163,12 +164,12 @@ sealed class WebMessageHandlerActor : ReceiveActor<IMessageToController> { | ||||
| 		return controllerState.AgentJavaRuntimesByGuid; | ||||
| 	} | ||||
|  | ||||
| 	private Task<ImmutableArray<AuditLogItem>> HandleGetAuditLog(GetAuditLogMessage message) { | ||||
| 		return auditLogManager.GetMostRecentItems(message.Count); | ||||
| 	private Task<Result<ImmutableArray<AuditLogItem>, UserActionFailure>> HandleGetAuditLog(GetAuditLogMessage message) { | ||||
| 		return auditLogManager.GetMostRecentItems(userLoginManager.GetLoggedInUser(message.AuthToken), message.Count); | ||||
| 	} | ||||
|  | ||||
| 	private Task<ImmutableArray<EventLogItem>> HandleGetEventLog(GetEventLogMessage message) { | ||||
| 		return eventLogManager.GetMostRecentItems(message.Count); | ||||
| 	private Task<Result<ImmutableArray<EventLogItem>, UserActionFailure>> HandleGetEventLog(GetEventLogMessage message) { | ||||
| 		return eventLogManager.GetMostRecentItems(userLoginManager.GetLoggedInUser(message.AuthToken), message.Count); | ||||
| 	} | ||||
|  | ||||
| 	private void HandleReply(ReplyMessage message) { | ||||
|   | ||||
| @@ -7,6 +7,7 @@ using Phantom.Controller.Services.Agents; | ||||
| using Phantom.Controller.Services.Events; | ||||
| using Phantom.Controller.Services.Instances; | ||||
| using Phantom.Controller.Services.Users; | ||||
| using Phantom.Controller.Services.Users.Sessions; | ||||
| using Phantom.Utils.Actor; | ||||
| using Phantom.Utils.Logging; | ||||
| using Phantom.Utils.Rpc.Runtime; | ||||
|   | ||||
| @@ -1,7 +1,10 @@ | ||||
| using System.Collections.Immutable; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Web.AuditLog; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Controller.Database; | ||||
| using Phantom.Controller.Database.Repositories; | ||||
| using Phantom.Controller.Services.Users.Sessions; | ||||
|  | ||||
| namespace Phantom.Controller.Services.Users;  | ||||
|  | ||||
| @@ -12,7 +15,11 @@ sealed class AuditLogManager { | ||||
| 		this.dbProvider = dbProvider; | ||||
| 	} | ||||
|  | ||||
| 	public async Task<ImmutableArray<AuditLogItem>> GetMostRecentItems(int count) { | ||||
| 	public async Task<Result<ImmutableArray<AuditLogItem>, UserActionFailure>> GetMostRecentItems(LoggedInUser loggedInUser, int count) { | ||||
| 		if (!loggedInUser.CheckPermission(Permission.ViewAudit)) { | ||||
| 			return UserActionFailure.NotAuthorized; | ||||
| 		} | ||||
| 		 | ||||
| 		await using var db = dbProvider.Lazy(); | ||||
| 		return await new AuditLogRepository(db).GetMostRecentItems(count, CancellationToken.None); | ||||
| 	} | ||||
|   | ||||
| @@ -36,34 +36,6 @@ sealed class PermissionManager { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public async Task<PermissionSet> FetchPermissionsForAllUsers(Guid userId) { | ||||
| 		await using var ctx = dbProvider.Eager(); | ||||
| 		 | ||||
| 		var userPermissions = ctx.UserPermissions | ||||
| 		                         .Where(up => up.UserGuid == userId) | ||||
| 		                         .Select(static up => up.PermissionId); | ||||
| 		 | ||||
| 		var rolePermissions = ctx.UserRoles | ||||
| 		                         .Where(ur => ur.UserGuid == userId) | ||||
| 		                         .Join(ctx.RolePermissions, static ur => ur.RoleGuid, static rp => rp.RoleGuid, static (ur, rp) => rp.PermissionId); | ||||
| 		 | ||||
| 		return new PermissionSet(await userPermissions.Union(rolePermissions).AsAsyncEnumerable().ToImmutableSetAsync()); | ||||
| 	} | ||||
| 	 | ||||
| 	public async Task<PermissionSet> FetchPermissionsForUserId(Guid userId) { | ||||
| 		await using var ctx = dbProvider.Eager(); | ||||
| 		 | ||||
| 		var userPermissions = ctx.UserPermissions | ||||
| 		                         .Where(up => up.UserGuid == userId) | ||||
| 		                         .Select(static up => up.PermissionId); | ||||
| 		 | ||||
| 		var rolePermissions = ctx.UserRoles | ||||
| 		                         .Where(ur => ur.UserGuid == userId) | ||||
| 		                         .Join(ctx.RolePermissions, static ur => ur.RoleGuid, static rp => rp.RoleGuid, static (ur, rp) => rp.PermissionId); | ||||
| 		 | ||||
| 		return new PermissionSet(await userPermissions.Union(rolePermissions).AsAsyncEnumerable().ToImmutableSetAsync()); | ||||
| 	} | ||||
|  | ||||
| 	public static ImmutableArray<string> GetMissingPermissionsOrdered(IEnumerable<Permission> allPermissions, ImmutableHashSet<string> existingPermissionIds) { | ||||
| 		return allPermissions.Select(static permission => permission.Id).Except(existingPermissionIds).Order().ToImmutableArray(); | ||||
| 	} | ||||
|   | ||||
| @@ -0,0 +1,26 @@ | ||||
| using System.Collections.Concurrent; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Controller.Database; | ||||
| using Phantom.Controller.Database.Entities; | ||||
| using Phantom.Controller.Database.Repositories; | ||||
|  | ||||
| namespace Phantom.Controller.Services.Users.Sessions; | ||||
|  | ||||
| sealed class AuthenticatedUserCache { | ||||
| 	private readonly ConcurrentDictionary<Guid, AuthenticatedUserInfo> authenticatedUsersByGuid = new (); | ||||
|  | ||||
| 	public bool TryGet(Guid userGuid, out AuthenticatedUserInfo? userInfo) { | ||||
| 		return authenticatedUsersByGuid.TryGetValue(userGuid, out userInfo); | ||||
| 	} | ||||
|  | ||||
| 	public async Task<AuthenticatedUserInfo?> Update(UserEntity user, ILazyDbContext db) { | ||||
| 		var userGuid = user.UserGuid; | ||||
| 		var userPermissions = await new PermissionRepository(db).GetAllUserPermissions(user); | ||||
| 		var userInfo = new AuthenticatedUserInfo(userGuid, user.Name, userPermissions); | ||||
| 		return authenticatedUsersByGuid[userGuid] = userInfo; | ||||
| 	} | ||||
| 	 | ||||
| 	public void Remove(Guid userGuid) { | ||||
| 		authenticatedUsersByGuid.Remove(userGuid, out _); | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,11 @@ | ||||
| using Phantom.Common.Data.Web.Users; | ||||
|  | ||||
| namespace Phantom.Controller.Services.Users.Sessions; | ||||
|  | ||||
| readonly record struct LoggedInUser(AuthenticatedUserInfo? AuthenticatedUserInfo) { | ||||
| 	public Guid? Guid => AuthenticatedUserInfo?.Guid; | ||||
| 	 | ||||
| 	public bool CheckPermission(Permission permission) { | ||||
| 		return AuthenticatedUserInfo != null && AuthenticatedUserInfo.Permissions.Check(permission); | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,139 @@ | ||||
| using System.Collections.Immutable; | ||||
| using System.Security.Cryptography; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Controller.Database; | ||||
| using Phantom.Controller.Database.Repositories; | ||||
|  | ||||
| namespace Phantom.Controller.Services.Users.Sessions; | ||||
|  | ||||
| sealed class UserLoginManager { | ||||
| 	private const int SessionIdBytes = 20; | ||||
|  | ||||
| 	private readonly AuthenticatedUserCache authenticatedUserCache; | ||||
| 	private readonly UserManager userManager; | ||||
| 	private readonly IDbContextProvider dbProvider; | ||||
| 	 | ||||
| 	private readonly UserSessionBucket[] sessionBuckets = new UserSessionBucket[256]; | ||||
|  | ||||
| 	public UserLoginManager(AuthenticatedUserCache authenticatedUserCache, UserManager userManager, IDbContextProvider dbProvider) { | ||||
| 		this.authenticatedUserCache = authenticatedUserCache; | ||||
| 		this.userManager = userManager; | ||||
| 		this.dbProvider = dbProvider; | ||||
|  | ||||
| 		for (int i = 0; i < sessionBuckets.GetLength(0); i++) { | ||||
| 			sessionBuckets[i] = new UserSessionBucket(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private UserSessionBucket GetSessionBucket(ImmutableArray<byte> token) { | ||||
| 		return sessionBuckets[token[0]]; | ||||
| 	} | ||||
|  | ||||
| 	public async Task<LogInSuccess?> LogIn(string username, string password) { | ||||
| 		Guid userGuid; | ||||
| 		AuthenticatedUserInfo? authenticatedUserInfo; | ||||
| 		 | ||||
| 		await using (var db = dbProvider.Lazy()) { | ||||
| 			var userRepository = new UserRepository(db); | ||||
|  | ||||
| 			var user = await userRepository.GetByName(username); | ||||
| 			if (user == null || !UserPasswords.Verify(password, user.PasswordHash)) { | ||||
| 				return null; | ||||
| 			} | ||||
|  | ||||
| 			authenticatedUserInfo = await authenticatedUserCache.Update(user, db); | ||||
| 			if (authenticatedUserInfo == null) { | ||||
| 				return null; | ||||
| 			} | ||||
|  | ||||
| 			userGuid = user.UserGuid; | ||||
|  | ||||
| 			var auditLogWriter = new AuditLogRepository(db).Writer(userGuid); | ||||
| 			auditLogWriter.UserLoggedIn(user); | ||||
|  | ||||
| 			await db.Ctx.SaveChangesAsync(); | ||||
| 		} | ||||
|  | ||||
| 		var authToken = ImmutableArray.Create(RandomNumberGenerator.GetBytes(SessionIdBytes)); | ||||
| 		GetSessionBucket(authToken).Add(userGuid, authToken); | ||||
| 		 | ||||
| 		return new LogInSuccess(authenticatedUserInfo, authToken); | ||||
| 	} | ||||
|  | ||||
| 	public async Task LogOut(Guid userGuid, ImmutableArray<byte> authToken) { | ||||
| 		if (!GetSessionBucket(authToken).Remove(userGuid, authToken)) { | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		await using var db = dbProvider.Lazy(); | ||||
|  | ||||
| 		var auditLogWriter = new AuditLogRepository(db).Writer(userGuid); | ||||
| 		auditLogWriter.UserLoggedOut(userGuid); | ||||
|  | ||||
| 		await db.Ctx.SaveChangesAsync(); | ||||
| 	} | ||||
|  | ||||
| 	public LoggedInUser GetLoggedInUser(ImmutableArray<byte> authToken) { | ||||
| 		var userGuid = GetSessionBucket(authToken).FindUserGuid(authToken); | ||||
| 		return userGuid != null && authenticatedUserCache.TryGet(userGuid.Value, out var userInfo) ? new LoggedInUser(userInfo) : default; | ||||
| 	} | ||||
| 	 | ||||
| 	public AuthenticatedUserInfo? GetAuthenticatedUser(Guid userGuid, ImmutableArray<byte> authToken) { | ||||
| 		return authenticatedUserCache.TryGet(userGuid, out var userInfo) && GetSessionBucket(authToken).Contains(userGuid, authToken) ? userInfo : null; | ||||
| 	} | ||||
|  | ||||
| 	private sealed class UserSessionBucket { | ||||
| 		private ImmutableList<UserSession> sessions = ImmutableList<UserSession>.Empty; | ||||
|  | ||||
| 		public void Add(Guid userGuid, ImmutableArray<byte> authToken) { | ||||
| 			lock (this) { | ||||
| 				var session = new UserSession(userGuid, authToken); | ||||
| 				if (!sessions.Contains(session)) { | ||||
| 					sessions = sessions.Add(session); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		public bool Contains(Guid userGuid, ImmutableArray<byte> authToken) { | ||||
| 			lock (this) { | ||||
| 				return sessions.Contains(new UserSession(userGuid, authToken)); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		public Guid? FindUserGuid(ImmutableArray<byte> authToken) { | ||||
| 			lock (this) { | ||||
| 				return sessions.Find(session => session.AuthTokenEquals(authToken))?.UserGuid; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		public bool Remove(Guid userGuid, ImmutableArray<byte> authToken) { | ||||
| 			lock (this) { | ||||
| 				int index = sessions.IndexOf(new UserSession(userGuid, authToken)); | ||||
| 				if (index == -1) { | ||||
| 					return false; | ||||
| 				} | ||||
|  | ||||
| 				sessions = sessions.RemoveAt(index); | ||||
| 				return true; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private sealed record UserSession(Guid UserGuid, ImmutableArray<byte> AuthToken) { | ||||
| 		public bool AuthTokenEquals(ImmutableArray<byte> other) { | ||||
| 			return CryptographicOperations.FixedTimeEquals(AuthToken.AsSpan(), other.AsSpan()); | ||||
| 		} | ||||
|  | ||||
| 		public bool Equals(UserSession? other) { | ||||
| 			if (ReferenceEquals(null, other)) { | ||||
| 				return false; | ||||
| 			} | ||||
| 			 | ||||
| 			return UserGuid.Equals(other.UserGuid) && AuthTokenEquals(other.AuthToken); | ||||
| 		} | ||||
|  | ||||
| 		public override int GetHashCode() { | ||||
| 			throw new NotImplementedException(); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -1,107 +0,0 @@ | ||||
| using System.Collections.Concurrent; | ||||
| using System.Collections.Immutable; | ||||
| using System.Runtime.CompilerServices; | ||||
| using System.Security.Cryptography; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Controller.Database; | ||||
| using Phantom.Controller.Database.Repositories; | ||||
|  | ||||
| namespace Phantom.Controller.Services.Users;  | ||||
|  | ||||
| sealed class UserLoginManager { | ||||
| 	private const int SessionIdBytes = 20; | ||||
| 	private readonly ConcurrentDictionary<Guid, UserSession> sessionsByUserGuid = new (); | ||||
| 	 | ||||
| 	private readonly UserManager userManager; | ||||
| 	private readonly PermissionManager permissionManager; | ||||
| 	private readonly IDbContextProvider dbProvider; | ||||
| 	 | ||||
| 	public UserLoginManager(UserManager userManager, PermissionManager permissionManager, IDbContextProvider dbProvider) { | ||||
| 		this.userManager = userManager; | ||||
| 		this.permissionManager = permissionManager; | ||||
| 		this.dbProvider = dbProvider; | ||||
| 	} | ||||
|  | ||||
| 	public async Task<LogInSuccess?> LogIn(string username, string password) { | ||||
| 		var user = await userManager.GetAuthenticated(username, password); | ||||
| 		if (user == null) { | ||||
| 			return null; | ||||
| 		} | ||||
|  | ||||
| 		var permissions = await permissionManager.FetchPermissionsForUserId(user.UserGuid); | ||||
| 		var userInfo = new AuthenticatedUserInfo(user.UserGuid, user.Name, permissions); | ||||
| 		var token = ImmutableArray.Create(RandomNumberGenerator.GetBytes(SessionIdBytes)); | ||||
| 		 | ||||
| 		sessionsByUserGuid.AddOrUpdate(user.UserGuid, UserSession.Create, UserSession.Add, new NewUserSession(userInfo, token)); | ||||
|  | ||||
| 		await using (var db = dbProvider.Lazy()) { | ||||
| 			var auditLogWriter = new AuditLogRepository(db).Writer(user.UserGuid); | ||||
| 			auditLogWriter.UserLoggedIn(user); | ||||
| 			 | ||||
| 			await db.Ctx.SaveChangesAsync(); | ||||
| 		} | ||||
|  | ||||
| 		return new LogInSuccess(userInfo, token); | ||||
| 	} | ||||
|  | ||||
| 	public async Task LogOut(Guid userGuid, ImmutableArray<byte> token) { | ||||
| 		while (true) { | ||||
| 			if (!sessionsByUserGuid.TryGetValue(userGuid, out var oldSession)) { | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			if (sessionsByUserGuid.TryUpdate(userGuid, oldSession.RemoveToken(token), oldSession)) { | ||||
| 				break; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		await using var db = dbProvider.Lazy(); | ||||
| 		 | ||||
| 		var auditLogWriter = new AuditLogRepository(db).Writer(userGuid); | ||||
| 		auditLogWriter.UserLoggedOut(userGuid); | ||||
| 			 | ||||
| 		await db.Ctx.SaveChangesAsync(); | ||||
| 	} | ||||
|  | ||||
| 	public AuthenticatedUserInfo? GetAuthenticatedUser(Guid userGuid, ImmutableArray<byte> token) { | ||||
| 		return sessionsByUserGuid.TryGetValue(userGuid, out var session) && session.Tokens.Contains(token, TokenEqualityComparer.Instance) ? session.UserInfo : null; | ||||
| 	} | ||||
|  | ||||
| 	private readonly record struct NewUserSession(AuthenticatedUserInfo UserInfo, ImmutableArray<byte> Token); | ||||
| 	 | ||||
| 	private sealed record UserSession(AuthenticatedUserInfo UserInfo, ImmutableList<ImmutableArray<byte>> Tokens) { | ||||
| 		public static UserSession Create(Guid userGuid, NewUserSession newSession) { | ||||
| 			return new UserSession(newSession.UserInfo, ImmutableList.Create(newSession.Token)); | ||||
| 		} | ||||
| 		 | ||||
| 		public static UserSession Add(Guid userGuid, UserSession oldSession, NewUserSession newSession) { | ||||
| 			return new UserSession(newSession.UserInfo, oldSession.Tokens.Add(newSession.Token)); | ||||
| 		} | ||||
|  | ||||
| 		public UserSession RemoveToken(ImmutableArray<byte> token) { | ||||
| 			return this with { Tokens = Tokens.Remove(token, TokenEqualityComparer.Instance) }; | ||||
| 		} | ||||
|  | ||||
| 		public bool Equals(UserSession? other) { | ||||
| 			return ReferenceEquals(this, other); | ||||
| 		} | ||||
|  | ||||
| 		public override int GetHashCode() { | ||||
| 			return RuntimeHelpers.GetHashCode(this); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private sealed class TokenEqualityComparer : IEqualityComparer<ImmutableArray<byte>> { | ||||
| 		public static TokenEqualityComparer Instance { get; } = new (); | ||||
| 		 | ||||
| 		private TokenEqualityComparer() {} | ||||
|  | ||||
| 		public bool Equals(ImmutableArray<byte> x, ImmutableArray<byte> y) { | ||||
| 			return x.SequenceEqual(y); | ||||
| 		} | ||||
|  | ||||
| 		public int GetHashCode(ImmutableArray<byte> obj) { | ||||
| 			throw new NotImplementedException(); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -1,8 +1,10 @@ | ||||
| using System.Collections.Immutable; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Controller.Database; | ||||
| using Phantom.Controller.Database.Entities; | ||||
| using Phantom.Controller.Database.Repositories; | ||||
| using Phantom.Controller.Services.Users.Sessions; | ||||
| using Phantom.Utils.Logging; | ||||
| using Serilog; | ||||
|  | ||||
| @@ -11,9 +13,13 @@ namespace Phantom.Controller.Services.Users; | ||||
| sealed class UserManager { | ||||
| 	private static readonly ILogger Logger = PhantomLogger.Create<UserManager>(); | ||||
|  | ||||
| 	private readonly AuthenticatedUserCache authenticatedUserCache; | ||||
| 	private readonly ControllerState controllerState; | ||||
| 	private readonly IDbContextProvider dbProvider; | ||||
|  | ||||
| 	public UserManager(IDbContextProvider dbProvider) { | ||||
| 	public UserManager(AuthenticatedUserCache authenticatedUserCache, ControllerState controllerState, IDbContextProvider dbProvider) { | ||||
| 		this.authenticatedUserCache = authenticatedUserCache; | ||||
| 		this.controllerState = controllerState; | ||||
| 		this.dbProvider = dbProvider; | ||||
| 	} | ||||
|  | ||||
| @@ -85,10 +91,14 @@ sealed class UserManager { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public async Task<CreateUserResult> Create(Guid loggedInUserGuid, string username, string password) { | ||||
| 	public async Task<Result<CreateUserResult, UserActionFailure>> Create(LoggedInUser loggedInUser, string username, string password) { | ||||
| 		if (!loggedInUser.CheckPermission(Permission.EditUsers)) { | ||||
| 			return UserActionFailure.NotAuthorized; | ||||
| 		} | ||||
| 		 | ||||
| 		await using var db = dbProvider.Lazy(); | ||||
| 		var userRepository = new UserRepository(db); | ||||
| 		var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUserGuid); | ||||
| 		var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUser.Guid); | ||||
|  | ||||
| 		try { | ||||
| 			var result = await userRepository.CreateUser(username, password); | ||||
| @@ -109,7 +119,11 @@ sealed class UserManager { | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	public async Task<DeleteUserResult> DeleteByGuid(Guid loggedInUserGuid, Guid userGuid) { | ||||
| 	public async Task<Result<DeleteUserResult, UserActionFailure>> DeleteByGuid(LoggedInUser loggedInUser, Guid userGuid) { | ||||
| 		if (!loggedInUser.CheckPermission(Permission.EditUsers)) { | ||||
| 			return UserActionFailure.NotAuthorized; | ||||
| 		} | ||||
| 		 | ||||
| 		await using var db = dbProvider.Lazy(); | ||||
| 		var userRepository = new UserRepository(db); | ||||
|  | ||||
| @@ -118,11 +132,17 @@ sealed class UserManager { | ||||
| 			return DeleteUserResult.NotFound; | ||||
| 		} | ||||
|  | ||||
| 		var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUserGuid); | ||||
| 		authenticatedUserCache.Remove(userGuid); | ||||
| 		 | ||||
| 		var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUser.Guid); | ||||
| 		try { | ||||
| 			userRepository.DeleteUser(user); | ||||
| 			auditLogWriter.UserDeleted(user); | ||||
| 			await db.Ctx.SaveChangesAsync(); | ||||
| 			 | ||||
| 			// In case the user logged in during deletion. | ||||
| 			authenticatedUserCache.Remove(userGuid); | ||||
| 			controllerState.UpdateOrDeleteUser(userGuid); | ||||
|  | ||||
| 			Logger.Information("Deleted user \"{Username}\" (GUID {Guid}).", user.Name, user.UserGuid); | ||||
| 			return DeleteUserResult.Deleted; | ||||
|   | ||||
| @@ -1,7 +1,9 @@ | ||||
| using System.Collections.Immutable; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Controller.Database; | ||||
| using Phantom.Controller.Database.Repositories; | ||||
| using Phantom.Controller.Services.Users.Sessions; | ||||
| using Phantom.Utils.Logging; | ||||
| using Serilog; | ||||
|  | ||||
| @@ -9,10 +11,14 @@ namespace Phantom.Controller.Services.Users; | ||||
|  | ||||
| sealed class UserRoleManager { | ||||
| 	private static readonly ILogger Logger = PhantomLogger.Create<UserRoleManager>(); | ||||
| 	 | ||||
|  | ||||
| 	private readonly AuthenticatedUserCache authenticatedUserCache; | ||||
| 	private readonly ControllerState controllerState; | ||||
| 	private readonly IDbContextProvider dbProvider; | ||||
| 	 | ||||
| 	public UserRoleManager(IDbContextProvider dbProvider) { | ||||
| 	public UserRoleManager(AuthenticatedUserCache authenticatedUserCache, ControllerState controllerState, IDbContextProvider dbProvider) { | ||||
| 		this.authenticatedUserCache = authenticatedUserCache; | ||||
| 		this.controllerState = controllerState; | ||||
| 		this.dbProvider = dbProvider; | ||||
| 	} | ||||
|  | ||||
| @@ -21,7 +27,11 @@ sealed class UserRoleManager { | ||||
| 		return await new UserRoleRepository(db).GetRoleGuidsByUserGuid(userGuids); | ||||
| 	} | ||||
|  | ||||
| 	public async Task<ChangeUserRolesResult> ChangeUserRoles(Guid loggedInUserGuid, Guid subjectUserGuid, ImmutableHashSet<Guid> addToRoleGuids, ImmutableHashSet<Guid> removeFromRoleGuids) { | ||||
| 	public async Task<Result<ChangeUserRolesResult, UserActionFailure>> ChangeUserRoles(LoggedInUser loggedInUser, Guid subjectUserGuid, ImmutableHashSet<Guid> addToRoleGuids, ImmutableHashSet<Guid> removeFromRoleGuids) { | ||||
| 		if (!loggedInUser.CheckPermission(Permission.EditUsers)) { | ||||
| 			return UserActionFailure.NotAuthorized; | ||||
| 		} | ||||
| 		 | ||||
| 		await using var db = dbProvider.Lazy(); | ||||
| 		var userRepository = new UserRepository(db); | ||||
| 		 | ||||
| @@ -32,7 +42,7 @@ sealed class UserRoleManager { | ||||
|  | ||||
| 		var roleRepository = new RoleRepository(db); | ||||
| 		var userRoleRepository = new UserRoleRepository(db); | ||||
| 		var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUserGuid); | ||||
| 		var auditLogWriter = new AuditLogRepository(db).Writer(loggedInUser.Guid); | ||||
| 		 | ||||
| 		var rolesByGuid = await roleRepository.GetByGuids(addToRoleGuids.Union(removeFromRoleGuids)); | ||||
| 		 | ||||
| @@ -41,7 +51,7 @@ sealed class UserRoleManager { | ||||
| 		 | ||||
| 		var removedFromRoleGuids = ImmutableHashSet.CreateBuilder<Guid>(); | ||||
| 		var removedFromRoleNames = new List<string>(); | ||||
|          | ||||
|  | ||||
| 		try { | ||||
| 			foreach (var roleGuid in addToRoleGuids) { | ||||
| 				if (rolesByGuid.TryGetValue(roleGuid, out var role)) { | ||||
| @@ -62,6 +72,9 @@ sealed class UserRoleManager { | ||||
| 			auditLogWriter.UserRolesChanged(user, addedToRoleNames, removedFromRoleNames); | ||||
| 			await db.Ctx.SaveChangesAsync(); | ||||
| 			 | ||||
| 			await authenticatedUserCache.Update(user, db); | ||||
| 			controllerState.UpdateOrDeleteUser(user.UserGuid); | ||||
| 			 | ||||
| 			Logger.Information("Changed roles for user \"{Username}\" (GUID {Guid}).", user.Name, user.UserGuid); | ||||
| 			return new ChangeUserRolesResult(addedToRoleGuids.ToImmutable(), removedFromRoleGuids.ToImmutable()); | ||||
| 		} catch (Exception e) { | ||||
|   | ||||
| @@ -17,6 +17,10 @@ public static class TaskExtensions { | ||||
| 		return task.ContinueOnActor(result => mapper(result, arg)); | ||||
| 	} | ||||
| 	 | ||||
| 	public static Task<TResult> ContinueOnActor<TSource, TArg1, TArg2, TResult>(this Task<TSource> task, Func<TSource, TArg1, TArg2, TResult> mapper, TArg1 arg1, TArg2 arg2) { | ||||
| 		return task.ContinueOnActor(result => mapper(result, arg1, arg2)); | ||||
| 	} | ||||
| 	 | ||||
| 	private static Task<TResult> MapResult<TSource, TResult>(Task<TSource> task, Func<TSource, TResult> mapper, TaskCompletionSource<TResult> completionSource) { | ||||
| 		if (task.IsFaulted) { | ||||
| 			completionSource.SetException(task.Exception.InnerExceptions); | ||||
|   | ||||
							
								
								
									
										3
									
								
								Utils/Phantom.Utils/Result/Err.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								Utils/Phantom.Utils/Result/Err.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| namespace Phantom.Utils.Result; | ||||
|  | ||||
| public sealed record Err<T>(T Error) : Result; | ||||
							
								
								
									
										3
									
								
								Utils/Phantom.Utils/Result/Ok.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								Utils/Phantom.Utils/Result/Ok.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| namespace Phantom.Utils.Result; | ||||
|  | ||||
| public sealed record Ok<T>(T Value) : Result; | ||||
							
								
								
									
										5
									
								
								Utils/Phantom.Utils/Result/Result.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								Utils/Phantom.Utils/Result/Result.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| namespace Phantom.Utils.Result; | ||||
|  | ||||
| public abstract record Result { | ||||
| 	private protected Result() {} | ||||
| } | ||||
| @@ -1,6 +1,6 @@ | ||||
| @using Phantom.Web.Components.Utils | ||||
| @if (messageLines.Length > 0) { | ||||
|   <div class="form-submit-errors text-danger"> | ||||
|   <div class="form-submit-errors text-danger" role="alert"> | ||||
|     @for (int i = 0; i < messageLines.Length; i++) { | ||||
|       @messageLines[i] | ||||
|       if (i < messageLines.Length - 1) { | ||||
|   | ||||
| @@ -17,11 +17,11 @@ public abstract class PhantomComponent : ComponentBase, IDisposable { | ||||
|  | ||||
| 	protected CancellationToken CancellationToken => cancellationTokenSource.Token; | ||||
|  | ||||
| 	protected async Task<Guid?> GetUserGuid() { | ||||
| 	protected async Task<AuthenticatedUser?> GetAuthenticatedUser() { | ||||
| 		var authenticationState = await AuthenticationStateTask; | ||||
| 		return authenticationState.TryGetGuid(); | ||||
| 		return authenticationState.GetAuthenticatedUser(); | ||||
| 	} | ||||
|  | ||||
| 	 | ||||
| 	protected async Task<bool> CheckPermission(Permission permission) { | ||||
| 		var authenticationState = await AuthenticationStateTask; | ||||
| 		return authenticationState.CheckPermission(permission); | ||||
|   | ||||
							
								
								
									
										10
									
								
								Web/Phantom.Web.Services/Authentication/AuthenticatedUser.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								Web/Phantom.Web.Services/Authentication/AuthenticatedUser.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| using System.Collections.Immutable; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
|  | ||||
| namespace Phantom.Web.Services.Authentication; | ||||
|  | ||||
| public sealed record AuthenticatedUser(AuthenticatedUserInfo Info, ImmutableArray<byte> Token) { | ||||
| 	public bool CheckPermission(Permission permission) { | ||||
| 		return Info.Permissions.Check(permission); | ||||
| 	} | ||||
| } | ||||
| @@ -5,23 +5,27 @@ using Phantom.Common.Data.Web.Users; | ||||
| namespace Phantom.Web.Services.Authentication; | ||||
|  | ||||
| public static class AuthenticationStateExtensions { | ||||
| 	public static Guid? TryGetGuid(this AuthenticationState authenticationState) { | ||||
| 		return authenticationState.User is CustomClaimsPrincipal customUser ? customUser.UserInfo.Guid : null; | ||||
| 	public static AuthenticatedUser? GetAuthenticatedUser(this AuthenticationState authenticationState) { | ||||
| 		return authenticationState.User.GetAuthenticatedUser(); | ||||
| 	} | ||||
|  | ||||
| 	public static PermissionSet GetPermissions(this ClaimsPrincipal user) { | ||||
| 		return user is CustomClaimsPrincipal customUser ? customUser.UserInfo.Permissions : PermissionSet.None; | ||||
| 	} | ||||
|  | ||||
| 	public static bool CheckPermission(this ClaimsPrincipal user, Permission permission) { | ||||
| 		return user.GetPermissions().Check(permission); | ||||
| 	public static AuthenticatedUser? GetAuthenticatedUser(this ClaimsPrincipal claimsPrincipal) { | ||||
| 		return claimsPrincipal is CustomClaimsPrincipal principal ? principal.User : null; | ||||
| 	} | ||||
|  | ||||
| 	public static PermissionSet GetPermissions(this AuthenticationState authenticationState) { | ||||
| 		return authenticationState.User.GetPermissions(); | ||||
| 	} | ||||
| 	 | ||||
| 	public static PermissionSet GetPermissions(this ClaimsPrincipal claimsPrincipal) { | ||||
| 		return claimsPrincipal.GetAuthenticatedUser() is {} user ? user.Info.Permissions : PermissionSet.None; | ||||
| 	} | ||||
|  | ||||
| 	public static bool CheckPermission(this AuthenticationState authenticationState, Permission permission) { | ||||
| 		return authenticationState.User.CheckPermission(permission); | ||||
| 	} | ||||
| 	 | ||||
| 	public static bool CheckPermission(this ClaimsPrincipal claimsPrincipal, Permission permission) { | ||||
| 		return claimsPrincipal.GetPermissions().Check(permission); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -4,41 +4,118 @@ using Microsoft.AspNetCore.Components.Server; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Common.Messages.Web.ToController; | ||||
| using Phantom.Utils.Logging; | ||||
| using Phantom.Web.Services.Rpc; | ||||
| using ILogger = Serilog.ILogger; | ||||
|  | ||||
| namespace Phantom.Web.Services.Authentication; | ||||
|  | ||||
| public sealed class CustomAuthenticationStateProvider : ServerAuthenticationStateProvider { | ||||
| public sealed class CustomAuthenticationStateProvider : ServerAuthenticationStateProvider, IAsyncDisposable { | ||||
| 	private static readonly ILogger Logger = PhantomLogger.Create<CustomAuthenticationStateProvider>(); | ||||
|  | ||||
| 	private readonly UserSessionRefreshManager sessionRefreshManager; | ||||
| 	private readonly UserSessionBrowserStorage sessionBrowserStorage; | ||||
| 	private readonly ControllerConnection controllerConnection; | ||||
| 	private bool isLoaded; | ||||
|  | ||||
| 	public CustomAuthenticationStateProvider(UserSessionBrowserStorage sessionBrowserStorage, ControllerConnection controllerConnection) { | ||||
| 	private readonly SemaphoreSlim loadSemaphore = new (1); | ||||
| 	private bool isLoaded = false; | ||||
| 	private CancellationTokenSource? loadCancellationTokenSource; | ||||
| 	private UserSessionRefreshManager.EventHolder? userRefreshEventHolder; | ||||
|  | ||||
| 	public CustomAuthenticationStateProvider(UserSessionRefreshManager sessionRefreshManager, UserSessionBrowserStorage sessionBrowserStorage, ControllerConnection controllerConnection) { | ||||
| 		this.sessionRefreshManager = sessionRefreshManager; | ||||
| 		this.sessionBrowserStorage = sessionBrowserStorage; | ||||
| 		this.controllerConnection = controllerConnection; | ||||
| 	} | ||||
|  | ||||
| 	public override async Task<AuthenticationState> GetAuthenticationStateAsync() { | ||||
| 		if (!isLoaded) { | ||||
| 			var stored = await sessionBrowserStorage.Get(); | ||||
| 			if (stored != null) { | ||||
| 				var session = await controllerConnection.Send<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(new GetAuthenticatedUser(stored.UserGuid, stored.Token), TimeSpan.FromSeconds(30)); | ||||
| 				if (session.Value is {} userInfo) { | ||||
| 					SetLoadedSession(userInfo); | ||||
| 				} | ||||
| 			} | ||||
| 			await LoadSession(); | ||||
| 		} | ||||
|  | ||||
| 		return await base.GetAuthenticationStateAsync(); | ||||
| 	} | ||||
|  | ||||
| 	internal void SetLoadedSession(AuthenticatedUserInfo user) { | ||||
| 	private async Task LoadSession() { | ||||
| 		await CancelCurrentLoad(); | ||||
| 		await loadSemaphore.WaitAsync(CancellationToken.None); | ||||
|  | ||||
| 		loadCancellationTokenSource = new CancellationTokenSource(); | ||||
| 		CancellationToken cancellationToken = loadCancellationTokenSource.Token; | ||||
|  | ||||
| 		try { | ||||
| 			var authenticatedUser = await TryGetSession(cancellationToken); | ||||
| 			if (authenticatedUser != null) { | ||||
| 				SetLoadedSession(authenticatedUser); | ||||
| 			} | ||||
| 			else { | ||||
| 				SetUnloadedSession(); | ||||
| 			} | ||||
| 		} catch (OperationCanceledException) { | ||||
| 			SetUnloadedSession(); | ||||
| 		} catch (Exception e) { | ||||
| 			SetUnloadedSession(); | ||||
| 			Logger.Error(e, "Could not load user session."); | ||||
| 		} finally { | ||||
| 			loadCancellationTokenSource.Dispose(); | ||||
| 			loadCancellationTokenSource = null; | ||||
| 			loadSemaphore.Release(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private async Task CancelCurrentLoad() { | ||||
| 		var cancellationTokenSource = loadCancellationTokenSource; | ||||
| 		if (cancellationTokenSource != null) { | ||||
| 			await cancellationTokenSource.CancelAsync(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private async Task<AuthenticatedUser?> TryGetSession(CancellationToken cancellationToken) { | ||||
| 		var stored = await sessionBrowserStorage.Get(); | ||||
| 		if (stored == null) { | ||||
| 			return null; | ||||
| 		} | ||||
|  | ||||
| 		cancellationToken.ThrowIfCancellationRequested(); | ||||
|  | ||||
| 		var userGuid = stored.UserGuid; | ||||
| 		var authToken = stored.Token; | ||||
|  | ||||
| 		if (userRefreshEventHolder == null) { | ||||
| 			userRefreshEventHolder = sessionRefreshManager.GetEventHolder(userGuid); | ||||
| 			userRefreshEventHolder.UserNeedsRefresh += OnUserNeedsRefresh; | ||||
| 		} | ||||
|  | ||||
| 		var session = await controllerConnection.Send<GetAuthenticatedUser, Optional<AuthenticatedUserInfo>>(new GetAuthenticatedUser(userGuid, authToken), TimeSpan.FromSeconds(30), cancellationToken); | ||||
| 		if (session.Value is {} userInfo) { | ||||
| 			return new AuthenticatedUser(userInfo, authToken); | ||||
| 		} | ||||
| 		else { | ||||
| 			return null; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private void SetLoadedSession(AuthenticatedUser authenticatedUser) { | ||||
| 		SetAuthenticationState(Task.FromResult(new AuthenticationState(new CustomClaimsPrincipal(authenticatedUser)))); | ||||
| 		isLoaded = true; | ||||
| 		SetAuthenticationState(Task.FromResult(new AuthenticationState(new CustomClaimsPrincipal(user)))); | ||||
| 	} | ||||
|  | ||||
| 	internal void SetUnloadedSession() { | ||||
| 		isLoaded = false; | ||||
| 		SetAuthenticationState(Task.FromResult(new AuthenticationState(new ClaimsPrincipal()))); | ||||
| 		isLoaded = false; | ||||
| 	} | ||||
|  | ||||
| 	private void OnUserNeedsRefresh(object? sender, EventArgs args) { | ||||
| 		_ = LoadSession(); | ||||
| 	} | ||||
|  | ||||
| 	public async ValueTask DisposeAsync() { | ||||
| 		if (userRefreshEventHolder != null) { | ||||
| 			userRefreshEventHolder.UserNeedsRefresh -= OnUserNeedsRefresh; | ||||
| 			userRefreshEventHolder = null; | ||||
| 		} | ||||
|  | ||||
| 		await CancelCurrentLoad(); | ||||
| 		loadSemaphore.Dispose(); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -4,10 +4,10 @@ using Phantom.Common.Data.Web.Users; | ||||
| namespace Phantom.Web.Services.Authentication; | ||||
|  | ||||
| sealed class CustomClaimsPrincipal : ClaimsPrincipal { | ||||
| 	internal AuthenticatedUserInfo UserInfo { get; } | ||||
| 	internal AuthenticatedUser User { get; } | ||||
|  | ||||
| 	internal CustomClaimsPrincipal(AuthenticatedUserInfo userInfo) : base(GetIdentity(userInfo)) { | ||||
| 		UserInfo = userInfo; | ||||
| 	internal CustomClaimsPrincipal(AuthenticatedUser user) : base(GetIdentity(user.Info)) { | ||||
| 		User = user; | ||||
| 	} | ||||
|  | ||||
| 	private static ClaimsIdentity GetIdentity(AuthenticatedUserInfo userInfo) { | ||||
|   | ||||
| @@ -37,9 +37,11 @@ public sealed class UserLoginManager { | ||||
| 		Logger.Information("Successfully logged in {Username}.", username); | ||||
|  | ||||
| 		var userInfo = success.UserInfo; | ||||
|  | ||||
| 		await sessionBrowserStorage.Store(userInfo.Guid, success.Token); | ||||
| 		authenticationStateProvider.SetLoadedSession(userInfo); | ||||
| 		var authToken = success.AuthToken; | ||||
| 		 | ||||
| 		authenticationStateProvider.SetUnloadedSession(); | ||||
| 		await sessionBrowserStorage.Store(userInfo.Guid, authToken); | ||||
| 		await authenticationStateProvider.GetAuthenticationStateAsync(); | ||||
| 		await navigation.NavigateTo(returnUrl ?? string.Empty); | ||||
| 		 | ||||
| 		return true; | ||||
|   | ||||
| @@ -0,0 +1,25 @@ | ||||
| using System.Collections.Concurrent; | ||||
|  | ||||
| namespace Phantom.Web.Services.Authentication; | ||||
|  | ||||
| public sealed class UserSessionRefreshManager { | ||||
| 	private readonly ConcurrentDictionary<Guid, EventHolder> userUpdateEventHoldersByUserGuid = new (); | ||||
|  | ||||
| 	internal EventHolder GetEventHolder(Guid userGuid) { | ||||
| 		return userUpdateEventHoldersByUserGuid.GetOrAdd(userGuid, static _ => new EventHolder()); | ||||
| 	} | ||||
| 	 | ||||
| 	internal void RefreshUser(Guid userGuid) { | ||||
| 		if (userUpdateEventHoldersByUserGuid.TryGetValue(userGuid, out var eventHolder)) { | ||||
| 			eventHolder.Notify(); | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	internal sealed class EventHolder { | ||||
| 		public event EventHandler? UserNeedsRefresh; | ||||
|  | ||||
| 		internal void Notify() { | ||||
| 			UserNeedsRefresh?.Invoke(null, EventArgs.Empty); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -1,6 +1,9 @@ | ||||
| using System.Collections.Immutable; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Web.EventLog; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Common.Messages.Web.ToController; | ||||
| using Phantom.Web.Services.Authentication; | ||||
| using Phantom.Web.Services.Rpc; | ||||
|  | ||||
| namespace Phantom.Web.Services.Events;  | ||||
| @@ -12,8 +15,13 @@ public sealed class EventLogManager { | ||||
| 		this.controllerConnection = controllerConnection; | ||||
| 	} | ||||
|  | ||||
| 	public Task<ImmutableArray<EventLogItem>> GetMostRecentItems(int count, CancellationToken cancellationToken) { | ||||
| 		var message = new GetEventLogMessage(count); | ||||
| 		return controllerConnection.Send<GetEventLogMessage, ImmutableArray<EventLogItem>>(message, cancellationToken); | ||||
| 	public async Task<Result<ImmutableArray<EventLogItem>, UserActionFailure>> GetMostRecentItems(AuthenticatedUser? authenticatedUser, int count, CancellationToken cancellationToken) { | ||||
| 		if (authenticatedUser != null && authenticatedUser.CheckPermission(Permission.ViewEvents)) { | ||||
| 			var message = new GetEventLogMessage(authenticatedUser.Token, count); | ||||
| 			return await controllerConnection.Send<GetEventLogMessage, Result<ImmutableArray<EventLogItem>, UserActionFailure>>(message, cancellationToken); | ||||
| 		} | ||||
| 		else { | ||||
| 			return UserActionFailure.NotAuthorized; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -4,9 +4,11 @@ using Phantom.Common.Data.Instance; | ||||
| using Phantom.Common.Data.Minecraft; | ||||
| using Phantom.Common.Data.Replies; | ||||
| using Phantom.Common.Data.Web.Instance; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Common.Messages.Web.ToController; | ||||
| using Phantom.Utils.Events; | ||||
| using Phantom.Utils.Logging; | ||||
| using Phantom.Web.Services.Authentication; | ||||
| using Phantom.Web.Services.Rpc; | ||||
|  | ||||
| namespace Phantom.Web.Services.Instances; | ||||
| @@ -35,23 +37,43 @@ public sealed class InstanceManager { | ||||
| 		return instances.Value.GetValueOrDefault(instanceGuid); | ||||
| 	} | ||||
|  | ||||
| 	public Task<Result<CreateOrUpdateInstanceResult, InstanceActionFailure>> CreateOrUpdateInstance(Guid loggedInUserGuid, Guid instanceGuid, InstanceConfiguration configuration, CancellationToken cancellationToken) { | ||||
| 		var message = new CreateOrUpdateInstanceMessage(loggedInUserGuid, instanceGuid, configuration); | ||||
| 		return controllerConnection.Send<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, InstanceActionFailure>>(message, cancellationToken); | ||||
| 	public async Task<Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>> CreateOrUpdateInstance(AuthenticatedUser? authenticatedUser, Guid instanceGuid, InstanceConfiguration configuration, CancellationToken cancellationToken) { | ||||
| 		if (authenticatedUser != null && authenticatedUser.CheckPermission(Permission.CreateInstances)) { | ||||
| 			var message = new CreateOrUpdateInstanceMessage(authenticatedUser.Token, instanceGuid, configuration); | ||||
| 			return await controllerConnection.Send<CreateOrUpdateInstanceMessage, Result<CreateOrUpdateInstanceResult, UserInstanceActionFailure>>(message, cancellationToken); | ||||
| 		} | ||||
| 		else { | ||||
| 			return (UserInstanceActionFailure) UserActionFailure.NotAuthorized; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public Task<Result<LaunchInstanceResult, InstanceActionFailure>> LaunchInstance(Guid loggedInUserGuid, Guid agentGuid, Guid instanceGuid, CancellationToken cancellationToken) { | ||||
| 		var message = new LaunchInstanceMessage(loggedInUserGuid, agentGuid, instanceGuid); | ||||
| 		return controllerConnection.Send<LaunchInstanceMessage, Result<LaunchInstanceResult, InstanceActionFailure>>(message, cancellationToken); | ||||
| 	public async Task<Result<LaunchInstanceResult, UserInstanceActionFailure>> LaunchInstance(AuthenticatedUser? authenticatedUser, Guid agentGuid, Guid instanceGuid, CancellationToken cancellationToken) { | ||||
| 		if (authenticatedUser != null && authenticatedUser.CheckPermission(Permission.ControlInstances)) { | ||||
| 			var message = new LaunchInstanceMessage(authenticatedUser.Token, agentGuid, instanceGuid); | ||||
| 			return await controllerConnection.Send<LaunchInstanceMessage, Result<LaunchInstanceResult, UserInstanceActionFailure>>(message, cancellationToken); | ||||
| 		} | ||||
| 		else { | ||||
| 			return (UserInstanceActionFailure) UserActionFailure.NotAuthorized; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public Task<Result<StopInstanceResult, InstanceActionFailure>> StopInstance(Guid loggedInUserGuid, Guid agentGuid, Guid instanceGuid, MinecraftStopStrategy stopStrategy, CancellationToken cancellationToken) { | ||||
| 		var message = new StopInstanceMessage(loggedInUserGuid, agentGuid, instanceGuid, stopStrategy); | ||||
| 		return controllerConnection.Send<StopInstanceMessage, Result<StopInstanceResult, InstanceActionFailure>>(message, cancellationToken); | ||||
| 	public async Task<Result<StopInstanceResult, UserInstanceActionFailure>> StopInstance(AuthenticatedUser? authenticatedUser, Guid agentGuid, Guid instanceGuid, MinecraftStopStrategy stopStrategy, CancellationToken cancellationToken) { | ||||
| 		if (authenticatedUser != null && authenticatedUser.CheckPermission(Permission.ControlInstances)) { | ||||
| 			var message = new StopInstanceMessage(authenticatedUser.Token, agentGuid, instanceGuid, stopStrategy); | ||||
| 			return await controllerConnection.Send<StopInstanceMessage, Result<StopInstanceResult, UserInstanceActionFailure>>(message, cancellationToken); | ||||
| 		} | ||||
| 		else { | ||||
| 			return (UserInstanceActionFailure) UserActionFailure.NotAuthorized; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	public Task<Result<SendCommandToInstanceResult, InstanceActionFailure>> SendCommandToInstance(Guid loggedInUserGuid, Guid agentGuid, Guid instanceGuid, string command, CancellationToken cancellationToken) { | ||||
| 		var message = new SendCommandToInstanceMessage(loggedInUserGuid, agentGuid, instanceGuid, command); | ||||
| 		return controllerConnection.Send<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, InstanceActionFailure>>(message, cancellationToken); | ||||
| 	public async Task<Result<SendCommandToInstanceResult, UserInstanceActionFailure>> SendCommandToInstance(AuthenticatedUser? authenticatedUser, Guid agentGuid, Guid instanceGuid, string command, CancellationToken cancellationToken) { | ||||
| 		if (authenticatedUser != null && authenticatedUser.CheckPermission(Permission.ControlInstances)) { | ||||
| 			var message = new SendCommandToInstanceMessage(authenticatedUser.Token, agentGuid, instanceGuid, command); | ||||
| 			return await controllerConnection.Send<SendCommandToInstanceMessage, Result<SendCommandToInstanceResult, UserInstanceActionFailure>>(message, cancellationToken); | ||||
| 		} | ||||
| 		else { | ||||
| 			return (UserInstanceActionFailure) UserActionFailure.NotAuthorized; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -23,6 +23,7 @@ public static class PhantomWebServices { | ||||
| 		 | ||||
| 		services.AddSingleton<UserManager>(); | ||||
| 		services.AddSingleton<AuditLogManager>(); | ||||
| 		services.AddSingleton<UserSessionRefreshManager>(); | ||||
| 		services.AddScoped<UserLoginManager>(); | ||||
| 		services.AddScoped<UserSessionBrowserStorage>(); | ||||
| 		 | ||||
|   | ||||
| @@ -4,12 +4,20 @@ using Phantom.Common.Messages.Web.ToWeb; | ||||
| using Phantom.Utils.Actor; | ||||
| using Phantom.Utils.Rpc.Runtime; | ||||
| using Phantom.Web.Services.Agents; | ||||
| using Phantom.Web.Services.Authentication; | ||||
| using Phantom.Web.Services.Instances; | ||||
|  | ||||
| namespace Phantom.Web.Services.Rpc;  | ||||
|  | ||||
| sealed class ControllerMessageHandlerActor : ReceiveActor<IMessageToWeb> { | ||||
| 	public readonly record struct Init(RpcConnectionToServer<IMessageToController> Connection, AgentManager AgentManager, InstanceManager InstanceManager, InstanceLogManager InstanceLogManager, TaskCompletionSource<bool> RegisterSuccessWaiter); | ||||
| 	public readonly record struct Init( | ||||
| 		RpcConnectionToServer<IMessageToController> Connection, | ||||
| 		AgentManager AgentManager, | ||||
| 		InstanceManager InstanceManager, | ||||
| 		InstanceLogManager InstanceLogManager, | ||||
| 		UserSessionRefreshManager UserSessionRefreshManager, | ||||
| 		TaskCompletionSource<bool> RegisterSuccessWaiter | ||||
| 	); | ||||
| 	 | ||||
| 	public static Props<IMessageToWeb> Factory(Init init) { | ||||
| 		return Props<IMessageToWeb>.Create(() => new ControllerMessageHandlerActor(init), new ActorConfiguration { SupervisorStrategy = SupervisorStrategies.Resume }); | ||||
| @@ -19,6 +27,7 @@ sealed class ControllerMessageHandlerActor : ReceiveActor<IMessageToWeb> { | ||||
| 	private readonly AgentManager agentManager; | ||||
| 	private readonly InstanceManager instanceManager; | ||||
| 	private readonly InstanceLogManager instanceLogManager; | ||||
| 	private readonly UserSessionRefreshManager userSessionRefreshManager; | ||||
| 	private readonly TaskCompletionSource<bool> registerSuccessWaiter; | ||||
| 	 | ||||
| 	private ControllerMessageHandlerActor(Init init) { | ||||
| @@ -26,12 +35,14 @@ sealed class ControllerMessageHandlerActor : ReceiveActor<IMessageToWeb> { | ||||
| 		this.agentManager = init.AgentManager; | ||||
| 		this.instanceManager = init.InstanceManager; | ||||
| 		this.instanceLogManager = init.InstanceLogManager; | ||||
| 		this.userSessionRefreshManager = init.UserSessionRefreshManager; | ||||
| 		this.registerSuccessWaiter = init.RegisterSuccessWaiter; | ||||
| 		 | ||||
| 		Receive<RegisterWebResultMessage>(HandleRegisterWebResult); | ||||
| 		Receive<RefreshAgentsMessage>(HandleRefreshAgents); | ||||
| 		Receive<RefreshInstancesMessage>(HandleRefreshInstances); | ||||
| 		Receive<InstanceOutputMessage>(HandleInstanceOutput); | ||||
| 		Receive<RefreshUserSessionMessage>(HandleRefreshUserSession); | ||||
| 		Receive<ReplyMessage>(HandleReply); | ||||
| 	} | ||||
|  | ||||
| @@ -51,6 +62,10 @@ sealed class ControllerMessageHandlerActor : ReceiveActor<IMessageToWeb> { | ||||
| 		instanceLogManager.AddLines(message.InstanceGuid, message.Lines); | ||||
| 	} | ||||
|  | ||||
| 	private void HandleRefreshUserSession(RefreshUserSessionMessage message) { | ||||
| 		userSessionRefreshManager.RefreshUser(message.UserGuid); | ||||
| 	} | ||||
|  | ||||
| 	private void HandleReply(ReplyMessage message) { | ||||
| 		connection.Receive(message); | ||||
| 	} | ||||
|   | ||||
| @@ -4,6 +4,7 @@ using Phantom.Utils.Actor; | ||||
| using Phantom.Utils.Rpc.Runtime; | ||||
| using Phantom.Utils.Tasks; | ||||
| using Phantom.Web.Services.Agents; | ||||
| using Phantom.Web.Services.Authentication; | ||||
| using Phantom.Web.Services.Instances; | ||||
|  | ||||
| namespace Phantom.Web.Services.Rpc; | ||||
| @@ -13,6 +14,7 @@ public sealed class ControllerMessageHandlerFactory { | ||||
| 	private readonly AgentManager agentManager; | ||||
| 	private readonly InstanceManager instanceManager; | ||||
| 	private readonly InstanceLogManager instanceLogManager; | ||||
| 	private readonly UserSessionRefreshManager userSessionRefreshManager; | ||||
| 	 | ||||
| 	private readonly TaskCompletionSource<bool> registerSuccessWaiter = AsyncTasks.CreateCompletionSource<bool>(); | ||||
| 	 | ||||
| @@ -20,15 +22,17 @@ public sealed class ControllerMessageHandlerFactory { | ||||
| 	 | ||||
| 	private int messageHandlerId = 0; | ||||
| 	 | ||||
| 	public ControllerMessageHandlerFactory(RpcConnectionToServer<IMessageToController> connection, AgentManager agentManager, InstanceManager instanceManager, InstanceLogManager instanceLogManager) { | ||||
| 	public ControllerMessageHandlerFactory(RpcConnectionToServer<IMessageToController> connection, AgentManager agentManager, InstanceManager instanceManager, InstanceLogManager instanceLogManager, UserSessionRefreshManager userSessionRefreshManager) { | ||||
| 		this.connection = connection; | ||||
| 		this.agentManager = agentManager; | ||||
| 		this.instanceManager = instanceManager; | ||||
| 		this.instanceLogManager = instanceLogManager; | ||||
| 		this.userSessionRefreshManager = userSessionRefreshManager; | ||||
| 	} | ||||
| 	 | ||||
| 	public ActorRef<IMessageToWeb> Create(IActorRefFactory actorSystem) { | ||||
| 		int id = Interlocked.Increment(ref messageHandlerId); | ||||
| 		return actorSystem.ActorOf(ControllerMessageHandlerActor.Factory(new ControllerMessageHandlerActor.Init(connection, agentManager, instanceManager, instanceLogManager, registerSuccessWaiter)), "ControllerMessageHandler-" + id); | ||||
| 		var init = new ControllerMessageHandlerActor.Init(connection, agentManager, instanceManager, instanceLogManager, userSessionRefreshManager, registerSuccessWaiter); | ||||
| 		var name = "ControllerMessageHandler-" + Interlocked.Increment(ref messageHandlerId); | ||||
| 		return actorSystem.ActorOf(ControllerMessageHandlerActor.Factory(init), name); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,9 @@ | ||||
| using System.Collections.Immutable; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Web.AuditLog; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Common.Messages.Web.ToController; | ||||
| using Phantom.Web.Services.Authentication; | ||||
| using Phantom.Web.Services.Rpc; | ||||
|  | ||||
| namespace Phantom.Web.Services.Users;  | ||||
| @@ -12,8 +15,13 @@ public sealed class AuditLogManager { | ||||
| 		this.controllerConnection = controllerConnection; | ||||
| 	} | ||||
|  | ||||
| 	public Task<ImmutableArray<AuditLogItem>> GetMostRecentItems(int count, CancellationToken cancellationToken) { | ||||
| 		var message = new GetAuditLogMessage(count); | ||||
| 		return controllerConnection.Send<GetAuditLogMessage, ImmutableArray<AuditLogItem>>(message, cancellationToken); | ||||
| 	public async Task<Result<ImmutableArray<AuditLogItem>, UserActionFailure>> GetMostRecentItems(AuthenticatedUser? authenticatedUser, int count, CancellationToken cancellationToken) { | ||||
| 		if (authenticatedUser != null && authenticatedUser.CheckPermission(Permission.ViewAudit)) { | ||||
| 			var message = new GetAuditLogMessage(authenticatedUser.Token, count); | ||||
| 			return await controllerConnection.Send<GetAuditLogMessage, Result<ImmutableArray<AuditLogItem>, UserActionFailure>>(message, cancellationToken); | ||||
| 		} | ||||
| 		else { | ||||
| 			return UserActionFailure.NotAuthorized; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| using System.Collections.Immutable; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Common.Messages.Web.ToController; | ||||
| using Phantom.Web.Services.Authentication; | ||||
| using Phantom.Web.Services.Rpc; | ||||
|  | ||||
| namespace Phantom.Web.Services.Users; | ||||
| @@ -16,11 +18,21 @@ public sealed class UserManager { | ||||
| 		return controllerConnection.Send<GetUsersMessage, ImmutableArray<UserInfo>>(new GetUsersMessage(), cancellationToken); | ||||
| 	} | ||||
|  | ||||
| 	public Task<CreateUserResult> Create(Guid loggedInUserGuid, string username, string password, CancellationToken cancellationToken) { | ||||
| 		return controllerConnection.Send<CreateUserMessage, CreateUserResult>(new CreateUserMessage(loggedInUserGuid, username, password), cancellationToken); | ||||
| 	public async Task<Result<CreateUserResult, UserActionFailure>> Create(AuthenticatedUser? authenticatedUser, string username, string password, CancellationToken cancellationToken) { | ||||
| 		if (authenticatedUser != null && authenticatedUser.CheckPermission(Permission.EditUsers)) { | ||||
| 			return await controllerConnection.Send<CreateUserMessage, Result<CreateUserResult, UserActionFailure>>(new CreateUserMessage(authenticatedUser.Token, username, password), cancellationToken); | ||||
| 		} | ||||
| 		else { | ||||
| 			return UserActionFailure.NotAuthorized; | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	public Task<DeleteUserResult> DeleteByGuid(Guid loggedInUserGuid, Guid userGuid, CancellationToken cancellationToken) { | ||||
| 		return controllerConnection.Send<DeleteUserMessage, DeleteUserResult>(new DeleteUserMessage(loggedInUserGuid, userGuid), cancellationToken); | ||||
| 	public async Task<Result<DeleteUserResult, UserActionFailure>> DeleteByGuid(AuthenticatedUser? authenticatedUser, Guid userGuid, CancellationToken cancellationToken) { | ||||
| 		if (authenticatedUser != null && authenticatedUser.CheckPermission(Permission.EditUsers)) { | ||||
| 			return await controllerConnection.Send<DeleteUserMessage, Result<DeleteUserResult, UserActionFailure>>(new DeleteUserMessage(authenticatedUser.Token, userGuid), cancellationToken); | ||||
| 		} | ||||
| 		else { | ||||
| 			return UserActionFailure.NotAuthorized; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| using System.Collections.Immutable; | ||||
| using Phantom.Common.Data; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Common.Messages.Web.ToController; | ||||
| using Phantom.Web.Services.Authentication; | ||||
| using Phantom.Web.Services.Rpc; | ||||
|  | ||||
| namespace Phantom.Web.Services.Users; | ||||
| @@ -20,7 +22,12 @@ public sealed class UserRoleManager { | ||||
| 		return (await GetUserRoles(ImmutableHashSet.Create(userGuid), cancellationToken)).GetValueOrDefault(userGuid, ImmutableArray<Guid>.Empty); | ||||
| 	} | ||||
|  | ||||
| 	public Task<ChangeUserRolesResult> ChangeUserRoles(Guid loggedInUserGuid, Guid subjectUserGuid, ImmutableHashSet<Guid> addToRoleGuids, ImmutableHashSet<Guid> removeFromRoleGuids, CancellationToken cancellationToken) { | ||||
| 		return controllerConnection.Send<ChangeUserRolesMessage, ChangeUserRolesResult>(new ChangeUserRolesMessage(loggedInUserGuid, subjectUserGuid, addToRoleGuids, removeFromRoleGuids), cancellationToken); | ||||
| 	public async Task<Result<ChangeUserRolesResult, UserActionFailure>> ChangeUserRoles(AuthenticatedUser? authenticatedUser, Guid subjectUserGuid, ImmutableHashSet<Guid> addToRoleGuids, ImmutableHashSet<Guid> removeFromRoleGuids, CancellationToken cancellationToken) { | ||||
| 		if (authenticatedUser != null && authenticatedUser.CheckPermission(Permission.EditUsers)) { | ||||
| 			return await controllerConnection.Send<ChangeUserRolesMessage, Result<ChangeUserRolesResult, UserActionFailure>>(new ChangeUserRolesMessage(authenticatedUser.Token, subjectUserGuid, addToRoleGuids, removeFromRoleGuids), cancellationToken); | ||||
| 		} | ||||
| 		else { | ||||
| 			return UserActionFailure.NotAuthorized; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -11,7 +11,7 @@ | ||||
|             <p role="alert">You do not have permission to visit this page.</p> | ||||
|           } | ||||
|           else { | ||||
|             Navigation.NavigateTo("login" + QueryString.Create("return", Navigation.CreateReturnUrl()), forceLoad: true); | ||||
|             _ = Navigation.NavigateTo("login" + QueryString.Create("return", Navigation.CreateReturnUrl()), forceLoad: true); | ||||
|           } | ||||
|         </NotAuthorized> | ||||
|       </AuthorizeRouteView> | ||||
|   | ||||
| @@ -17,7 +17,6 @@ | ||||
|  | ||||
| <div class="navbar-menu @NavMenuCssClass" @onclick="ToggleNavMenu"> | ||||
|   <nav> | ||||
|     <NavMenuItem Label="Home" Icon="home" Match="NavLinkMatch.All" /> | ||||
|     <AuthorizeView> | ||||
|       <NotAuthorized> | ||||
|         <NavMenuItem Label="Login" Icon="account-login" Href="login" /> | ||||
| @@ -25,6 +24,8 @@ | ||||
|       <Authorized> | ||||
|         @{ var permissions = context.GetPermissions(); } | ||||
|          | ||||
|         <NavMenuItem Label="Home" Icon="home" Match="NavLinkMatch.All" /> | ||||
|          | ||||
|         @if (permissions.Check(Permission.ViewInstances)) { | ||||
|           <NavMenuItem Label="Instances" Icon="folder" Href="instances" /> | ||||
|         } | ||||
|   | ||||
| @@ -5,13 +5,18 @@ | ||||
| @using Phantom.Common.Data.Web.Users | ||||
| @using Phantom.Web.Services.Users | ||||
| @using Phantom.Web.Services.Instances | ||||
| @inherits Phantom.Web.Components.PhantomComponent | ||||
| @inherits PhantomComponent | ||||
| @inject AuditLogManager AuditLogManager | ||||
| @inject InstanceManager InstanceManager | ||||
| @inject UserManager UserManager | ||||
|  | ||||
| <h1>Audit Log</h1> | ||||
|  | ||||
| @if (loadError is {} error) { | ||||
|   <p role="alert">@error</p> | ||||
|   return; | ||||
| } | ||||
|  | ||||
| <Table TItem="AuditLogItem" Items="logItems"> | ||||
|   <HeaderRow> | ||||
|     <Column Class="text-end" MinWidth="200px">Time</Column> | ||||
| @@ -46,21 +51,25 @@ | ||||
|  | ||||
| @code { | ||||
|  | ||||
|   private CancellationTokenSource? initializationCancellationTokenSource; | ||||
|   private ImmutableArray<AuditLogItem>? logItems; | ||||
|   private string? loadError; | ||||
|    | ||||
|   private ImmutableDictionary<Guid, string>? userNamesByGuid; | ||||
|   private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty; | ||||
|  | ||||
|   protected override async Task OnInitializedAsync() { | ||||
|     initializationCancellationTokenSource = new CancellationTokenSource(); | ||||
|     var cancellationToken = initializationCancellationTokenSource.Token; | ||||
|  | ||||
|     try { | ||||
|       logItems = await AuditLogManager.GetMostRecentItems(50, cancellationToken); | ||||
|       userNamesByGuid = (await UserManager.GetAll(cancellationToken)).ToImmutableDictionary(static user => user.Guid, static user => user.Name); | ||||
|     var result = await AuditLogManager.GetMostRecentItems(await GetAuthenticatedUser(), 50, CancellationToken); | ||||
|     if (result) { | ||||
|       logItems = result.Value; | ||||
|       userNamesByGuid = (await UserManager.GetAll(CancellationToken)).ToImmutableDictionary(static user => user.Guid, static user => user.Name); | ||||
|       instanceNamesByGuid = InstanceManager.GetAll().Values.ToImmutableDictionary(static instance => instance.InstanceGuid, static instance => instance.Configuration.InstanceName); | ||||
|     } finally { | ||||
|       initializationCancellationTokenSource.Dispose(); | ||||
|     } | ||||
|     else { | ||||
|       logItems = ImmutableArray<AuditLogItem>.Empty; | ||||
|       loadError = result.Error switch { | ||||
|         UserActionFailure.NotAuthorized => "You do not have permission to view the audit log.", | ||||
|         _                               => "Unknown error." | ||||
|       }; | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -72,10 +81,4 @@ | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   protected override void OnDisposed() { | ||||
|     try { | ||||
|       initializationCancellationTokenSource?.Cancel(); | ||||
|     } catch (ObjectDisposedException) {} | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -13,6 +13,11 @@ | ||||
|  | ||||
| <h1>Event Log</h1> | ||||
|  | ||||
| @if (loadError is {} error) { | ||||
|   <p role="alert">@error</p> | ||||
|   return; | ||||
| } | ||||
|  | ||||
| <Table TItem="EventLogItem" Items="logItems"> | ||||
|   <HeaderRow> | ||||
|     <Column Class="text-end" MinWidth="200px">Time</Column> | ||||
| @@ -50,21 +55,25 @@ | ||||
|  | ||||
| @code { | ||||
|  | ||||
|   private CancellationTokenSource? initializationCancellationTokenSource; | ||||
|   private ImmutableArray<EventLogItem>? logItems; | ||||
|   private string? loadError; | ||||
|    | ||||
|   private ImmutableDictionary<Guid, string> agentNamesByGuid = ImmutableDictionary<Guid, string>.Empty; | ||||
|   private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty; | ||||
|  | ||||
|   protected override async Task OnInitializedAsync() { | ||||
|     initializationCancellationTokenSource = new CancellationTokenSource(); | ||||
|     var cancellationToken = initializationCancellationTokenSource.Token; | ||||
|  | ||||
|     try { | ||||
|       logItems = await EventLogManager.GetMostRecentItems(50, cancellationToken); | ||||
|     var result = await EventLogManager.GetMostRecentItems(await GetAuthenticatedUser(), 50, CancellationToken); | ||||
|     if (result) { | ||||
|       logItems = result.Value; | ||||
|       agentNamesByGuid = AgentManager.GetAll().ToImmutableDictionary(static kvp => kvp.AgentGuid, static kvp => kvp.Configuration.AgentName); | ||||
|       instanceNamesByGuid = InstanceManager.GetAll().Values.ToImmutableDictionary(static instance => instance.InstanceGuid, static instance => instance.Configuration.InstanceName); | ||||
|     } finally { | ||||
|       initializationCancellationTokenSource.Dispose(); | ||||
|     } | ||||
|     else { | ||||
|       logItems = ImmutableArray<EventLogItem>.Empty; | ||||
|       loadError = result.Error switch { | ||||
|         UserActionFailure.NotAuthorized => "You do not have permission to view the event log.", | ||||
|         _                               => "Unknown error." | ||||
|       }; | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -79,10 +88,4 @@ | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   protected override void OnDisposed() { | ||||
|     try { | ||||
|       initializationCancellationTokenSource?.Cancel(); | ||||
|     } catch (ObjectDisposedException) {} | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,12 +1,18 @@ | ||||
| @page "/" | ||||
| @attribute [AllowAnonymous] | ||||
| @inherits Phantom.Web.Components.PhantomComponent | ||||
|  | ||||
| <h1>Hello, world!</h1> | ||||
| <h1>Home</h1> | ||||
|  | ||||
| Welcome to your new app. | ||||
| @if (username != null) { | ||||
|    <p>Welcome back, @username!</p> | ||||
| } | ||||
|  | ||||
| <AuthorizeView> | ||||
|   <Authorized> | ||||
|     You are logged in as @context.User.Identity!.Name. | ||||
|   </Authorized> | ||||
| </AuthorizeView> | ||||
| @code { | ||||
|  | ||||
|   private string? username = null; | ||||
|    | ||||
|   protected override async Task OnInitializedAsync() { | ||||
|     username = (await GetAuthenticatedUser())?.Info.Name; | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| @page "/instances/{InstanceGuid:guid}" | ||||
| @attribute [Authorize(Permission.ViewInstancesPolicy)] | ||||
| @using Phantom.Common.Data.Instance | ||||
| @using Phantom.Common.Data.Replies | ||||
| @using Phantom.Common.Data.Web.Instance | ||||
| @using Phantom.Common.Data.Web.Users | ||||
| @using Phantom.Utils.Result | ||||
| @using Phantom.Common.Data.Instance | ||||
| @using Phantom.Web.Services.Instances | ||||
| @using Phantom.Web.Services.Authorization | ||||
| @inherits Phantom.Web.Components.PhantomComponent | ||||
| @@ -12,42 +13,42 @@ | ||||
| @if (Instance == null) { | ||||
|   <h1>Instance Not Found</h1> | ||||
|   <p>Return to <a href="instances">all instances</a>.</p> | ||||
|   return; | ||||
| } | ||||
| else { | ||||
|   <div class="d-flex flex-row align-items-center gap-3 mb-3"> | ||||
|     <h1 class="mb-0">Instance: @Instance.Configuration.InstanceName</h1> | ||||
|     <span class="fs-4 text-muted">//</span> | ||||
|     <div class="mt-2"> | ||||
|       <InstanceStatusText Status="Instance.Status" /> | ||||
|     </div> | ||||
|   </div> | ||||
|   <div class="d-flex flex-row align-items-center gap-2"> | ||||
|     <PermissionView Permission="Permission.ControlInstances"> | ||||
|       <button type="button" class="btn btn-success" @onclick="LaunchInstance" disabled="@(isLaunchingInstance || !Instance.Status.CanLaunch())">Launch</button> | ||||
|       <button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#stop-instance" disabled="@(!Instance.Status.CanStop())">Stop...</button> | ||||
|       <span><!-- extra spacing --></span> | ||||
|     </PermissionView> | ||||
|     <PermissionView Permission="Permission.CreateInstances"> | ||||
|       <a href="instances/@InstanceGuid/edit" class="btn btn-warning ms-auto">Edit Configuration</a> | ||||
|     </PermissionView> | ||||
|   </div> | ||||
|   @if (lastError != null) { | ||||
|     <p class="text-danger mt-2">@lastError</p> | ||||
|   } | ||||
|  | ||||
|   <PermissionView Permission="Permission.ViewInstanceLogs"> | ||||
|     <InstanceLog InstanceGuid="InstanceGuid" /> | ||||
|   </PermissionView> | ||||
|  | ||||
| <div class="d-flex flex-row align-items-center gap-3 mb-3"> | ||||
|   <h1 class="mb-0">Instance: @Instance.Configuration.InstanceName</h1> | ||||
|   <span class="fs-4 text-muted">//</span> | ||||
|   <div class="mt-2"> | ||||
|     <InstanceStatusText Status="Instance.Status" /> | ||||
|   </div> | ||||
| </div> | ||||
| <div class="d-flex flex-row align-items-center gap-2"> | ||||
|   <PermissionView Permission="Permission.ControlInstances"> | ||||
|     <div class="mb-3"> | ||||
|       <InstanceCommandInput AgentGuid="Instance.Configuration.AgentGuid" InstanceGuid="InstanceGuid" Disabled="@(!Instance.Status.CanSendCommand())" /> | ||||
|     </div> | ||||
|  | ||||
|     <InstanceStopDialog AgentGuid="Instance.Configuration.AgentGuid" InstanceGuid="InstanceGuid" ModalId="stop-instance" Disabled="@(!Instance.Status.CanStop())" /> | ||||
|     <button type="button" class="btn btn-success" @onclick="LaunchInstance" disabled="@(isLaunchingInstance || !Instance.Status.CanLaunch())">Launch</button> | ||||
|     <button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#stop-instance" disabled="@(!Instance.Status.CanStop())">Stop...</button> | ||||
|     <span><!-- extra spacing --></span> | ||||
|   </PermissionView> | ||||
|   <PermissionView Permission="Permission.CreateInstances"> | ||||
|     <a href="instances/@InstanceGuid/edit" class="btn btn-warning ms-auto">Edit Configuration</a> | ||||
|   </PermissionView> | ||||
| </div> | ||||
| @if (lastError != null) { | ||||
|   <p class="text-danger mt-2" role="alert">@lastError</p> | ||||
| } | ||||
|  | ||||
| <PermissionView Permission="Permission.ViewInstanceLogs"> | ||||
|   <InstanceLog InstanceGuid="InstanceGuid" /> | ||||
| </PermissionView> | ||||
|  | ||||
| <PermissionView Permission="Permission.ControlInstances"> | ||||
|   <div class="my-3"> | ||||
|     <InstanceCommandInput AgentGuid="Instance.Configuration.AgentGuid" InstanceGuid="InstanceGuid" Disabled="@(!Instance.Status.CanSendCommand())" /> | ||||
|   </div> | ||||
|  | ||||
|   <InstanceStopDialog AgentGuid="Instance.Configuration.AgentGuid" InstanceGuid="InstanceGuid" ModalId="stop-instance" Disabled="@(!Instance.Status.CanStop())" /> | ||||
| </PermissionView> | ||||
|  | ||||
| @code { | ||||
|  | ||||
|   [Parameter] | ||||
| @@ -73,20 +74,32 @@ else { | ||||
|     lastError = null; | ||||
|  | ||||
|     try { | ||||
|       var loggedInUserGuid = await GetUserGuid(); | ||||
|       if (loggedInUserGuid == null || !await CheckPermission(Permission.ControlInstances)) { | ||||
|         lastError = "You do not have permission to launch instances."; | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       if (Instance == null) { | ||||
|         lastError = "Instance not found."; | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       var result = await InstanceManager.LaunchInstance(loggedInUserGuid.Value, Instance.Configuration.AgentGuid, InstanceGuid, CancellationToken); | ||||
|       if (!result.Is(LaunchInstanceResult.LaunchInitiated)) { | ||||
|         lastError = result.Map(Messages.ToSentence, InstanceActionFailureExtensions.ToSentence); | ||||
|       var result = await InstanceManager.LaunchInstance(await GetAuthenticatedUser(), Instance.Configuration.AgentGuid, InstanceGuid, CancellationToken); | ||||
|  | ||||
|       switch (result.Variant()) { | ||||
|         case Ok<LaunchInstanceResult>(LaunchInstanceResult.LaunchInitiated): | ||||
|           break; | ||||
|  | ||||
|         case Ok<LaunchInstanceResult>(var launchInstanceResult): | ||||
|           lastError = launchInstanceResult.ToSentence(); | ||||
|           break; | ||||
|  | ||||
|         case Err<UserInstanceActionFailure>(OfInstanceActionFailure(var failure)): | ||||
|           lastError = failure.ToSentence(); | ||||
|           break; | ||||
|  | ||||
|         case Err<UserInstanceActionFailure>(OfUserActionFailure(UserActionFailure.NotAuthorized)): | ||||
|           lastError = "You do not have permission to launch this instance."; | ||||
|           break; | ||||
|  | ||||
|         default: | ||||
|           lastError = "Unknown error."; | ||||
|           break; | ||||
|       } | ||||
|     } finally { | ||||
|       isLaunchingInstance = false; | ||||
|   | ||||
| @@ -60,7 +60,7 @@ | ||||
|  | ||||
| @code { | ||||
|  | ||||
|   private Guid? me = Guid.Empty; | ||||
|   private Guid? me = null; | ||||
|   private ImmutableArray<UserInfo>? allUsers; | ||||
|   private ImmutableDictionary<Guid, RoleInfo> allRolesByGuid = ImmutableDictionary<Guid, RoleInfo>.Empty; | ||||
|   private readonly Dictionary<Guid, string> userGuidToRoleDescription = new (); | ||||
| @@ -71,7 +71,7 @@ | ||||
|   private UserDeleteDialog userDeleteDialog = null!; | ||||
|  | ||||
|   protected override async Task OnInitializedAsync() { | ||||
|     me = await GetUserGuid(); | ||||
|     me = (await GetAuthenticatedUser())?.Info.Guid; | ||||
|  | ||||
|     allUsers = (await UserManager.GetAll(CancellationToken)).Sort(static (a, b) => a.Name.CompareTo(b.Name)); | ||||
|     allRolesByGuid = (await RoleManager.GetAll(CancellationToken)).ToImmutableDictionary(static role => role.Guid, static role => role); | ||||
|   | ||||
| @@ -2,13 +2,14 @@ | ||||
| @using System.Collections.Immutable | ||||
| @using System.ComponentModel.DataAnnotations | ||||
| @using System.Diagnostics.CodeAnalysis | ||||
| @using Phantom.Common.Data.Minecraft | ||||
| @using Phantom.Common.Data.Replies | ||||
| @using Phantom.Common.Data.Web.Agent | ||||
| @using Phantom.Common.Data.Web.Instance | ||||
| @using Phantom.Common.Data.Web.Minecraft | ||||
| @using Phantom.Common.Data.Web.Users | ||||
| @using Phantom.Common.Messages.Web.ToController | ||||
| @using Phantom.Utils.Result | ||||
| @using Phantom.Common.Data.Replies | ||||
| @using Phantom.Common.Data.Web.Agent | ||||
| @using Phantom.Common.Data.Minecraft | ||||
| @using Phantom.Common.Data.Java | ||||
| @using Phantom.Common.Data | ||||
| @using Phantom.Common.Data.Instance | ||||
| @@ -29,13 +30,14 @@ | ||||
|       @{ | ||||
|         static RenderFragment GetAgentOption(Agent agent) { | ||||
|           var configuration = agent.Configuration; | ||||
|           return @<option value="@agent.AgentGuid"> | ||||
|                    @configuration.AgentName | ||||
|                    • | ||||
|                    @(agent.Stats?.RunningInstanceCount.ToString() ?? "?")/@(configuration.MaxInstances) @(configuration.MaxInstances == 1 ? "Instance" : "Instances") | ||||
|                    • | ||||
|                    @(agent.Stats?.RunningInstanceMemory.InMegabytes.ToString() ?? "?")/@(configuration.MaxMemory.InMegabytes) MB RAM | ||||
|                  </option>; | ||||
|           return | ||||
|             @<option value="@agent.AgentGuid"> | ||||
|               @configuration.AgentName | ||||
|               • | ||||
|               @(agent.Stats?.RunningInstanceCount.ToString() ?? "?")/@(configuration.MaxInstances) @(configuration.MaxInstances == 1 ? "Instance" : "Instances") | ||||
|               • | ||||
|               @(agent.Stats?.RunningInstanceMemory.InMegabytes.ToString() ?? "?")/@(configuration.MaxMemory.InMegabytes) MB RAM | ||||
|             </option>; | ||||
|         } | ||||
|       } | ||||
|       @if (EditedInstance == null) { | ||||
| @@ -168,12 +170,12 @@ | ||||
|  | ||||
|   [Parameter, EditorRequired] | ||||
|   public Instance? EditedInstance { get; init; } | ||||
|    | ||||
|  | ||||
|   private ConfigureInstanceFormModel form = null!; | ||||
|  | ||||
|   private ImmutableDictionary<Guid, Agent> allAgentsByGuid = ImmutableDictionary<Guid, Agent>.Empty; | ||||
|   private ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>> allAgentJavaRuntimes = ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>.Empty; | ||||
|    | ||||
|  | ||||
|   private MinecraftVersionType minecraftVersionType = MinecraftVersionType.Release; | ||||
|   private ImmutableArray<MinecraftVersion> allMinecraftVersions = ImmutableArray<MinecraftVersion>.Empty; | ||||
|   private ImmutableArray<MinecraftVersion> availableMinecraftVersions = ImmutableArray<MinecraftVersion>.Empty; | ||||
| @@ -278,7 +280,7 @@ | ||||
|   protected override async Task OnInitializedAsync() { | ||||
|     var agentJavaRuntimesTask = ControllerConnection.Send<GetAgentJavaRuntimesMessage, ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>>>(new GetAgentJavaRuntimesMessage(), TimeSpan.FromSeconds(30)); | ||||
|     var minecraftVersionsTask = ControllerConnection.Send<GetMinecraftVersionsMessage, ImmutableArray<MinecraftVersion>>(new GetMinecraftVersionsMessage(), TimeSpan.FromSeconds(30)); | ||||
|      | ||||
|  | ||||
|     allAgentsByGuid = AgentManager.ToDictionaryByGuid(); | ||||
|     allAgentJavaRuntimes = await agentJavaRuntimesTask; | ||||
|     allMinecraftVersions = await minecraftVersionsTask; | ||||
| @@ -294,7 +296,7 @@ | ||||
|       form.MemoryUnits = configuration.MemoryAllocation.RawValue; | ||||
|       form.JavaRuntimeGuid = configuration.JavaRuntimeGuid; | ||||
|       form.JvmArguments = JvmArgumentsHelper.Join(configuration.JvmArguments); | ||||
|        | ||||
|  | ||||
|       minecraftVersionType = allMinecraftVersions.FirstOrDefault(version => version.Id == configuration.MinecraftVersion)?.Type ?? minecraftVersionType; | ||||
|     } | ||||
|  | ||||
| @@ -303,7 +305,7 @@ | ||||
|     form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.SelectedAgentGuid), revalidated: nameof(ConfigureInstanceFormModel.ServerPort)); | ||||
|     form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.SelectedAgentGuid), revalidated: nameof(ConfigureInstanceFormModel.RconPort)); | ||||
|     form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.ServerPort), revalidated: nameof(ConfigureInstanceFormModel.RconPort)); | ||||
|      | ||||
|  | ||||
|     SetMinecraftVersionType(minecraftVersionType); | ||||
|   } | ||||
|  | ||||
| @@ -324,12 +326,6 @@ | ||||
|  | ||||
|     await form.SubmitModel.StartSubmitting(); | ||||
|  | ||||
|     var loggedInUserGuid = await GetUserGuid(); | ||||
|     if (loggedInUserGuid == null || !await CheckPermission(Permission.CreateInstances)) { | ||||
|       form.SubmitModel.StopSubmitting("You do not have permission to edit instances."); | ||||
|       return; | ||||
|     } | ||||
|      | ||||
|     var instanceGuid = EditedInstance?.InstanceGuid ?? Guid.NewGuid(); | ||||
|     var instanceConfiguration = new InstanceConfiguration( | ||||
|       EditedInstance?.Configuration.AgentGuid ?? selectedAgent.AgentGuid, | ||||
| @@ -343,12 +339,28 @@ | ||||
|       JvmArgumentsHelper.Split(form.JvmArguments) | ||||
|     ); | ||||
|  | ||||
|     var result = await InstanceManager.CreateOrUpdateInstance(loggedInUserGuid.Value, instanceGuid, instanceConfiguration, CancellationToken); | ||||
|     if (result.Is(CreateOrUpdateInstanceResult.Success)) { | ||||
|       await Navigation.NavigateTo("instances/" + instanceGuid); | ||||
|     } | ||||
|     else { | ||||
|       form.SubmitModel.StopSubmitting(result.Map(CreateOrUpdateInstanceResultExtensions.ToSentence, InstanceActionFailureExtensions.ToSentence)); | ||||
|     var result = await InstanceManager.CreateOrUpdateInstance(await GetAuthenticatedUser(), instanceGuid, instanceConfiguration, CancellationToken); | ||||
|  | ||||
|     switch (result.Variant()) { | ||||
|       case Ok<CreateOrUpdateInstanceResult>(CreateOrUpdateInstanceResult.Success): | ||||
|         await Navigation.NavigateTo("instances/" + instanceGuid); | ||||
|         break; | ||||
|  | ||||
|       case Ok<CreateOrUpdateInstanceResult>(var createOrUpdateInstanceResult): | ||||
|         form.SubmitModel.StopSubmitting(createOrUpdateInstanceResult.ToSentence()); | ||||
|         break; | ||||
|  | ||||
|       case Err<UserInstanceActionFailure>(OfInstanceActionFailure(var failure)): | ||||
|         form.SubmitModel.StopSubmitting(failure.ToSentence()); | ||||
|         break; | ||||
|  | ||||
|       case Err<UserInstanceActionFailure>(OfUserActionFailure(UserActionFailure.NotAuthorized)): | ||||
|         form.SubmitModel.StopSubmitting("You do not have permission to create or edit instances."); | ||||
|         break; | ||||
|  | ||||
|       default: | ||||
|         form.SubmitModel.StopSubmitting("Unknown error."); | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -1,11 +1,12 @@ | ||||
| @using Phantom.Web.Services.Instances | ||||
| @using Phantom.Common.Data.Replies | ||||
| @using Phantom.Common.Data.Web.Users | ||||
| @using Phantom.Common.Data.Replies | ||||
| @using Phantom.Utils.Result | ||||
| @using Phantom.Web.Services.Instances | ||||
| @inherits Phantom.Web.Components.PhantomComponent | ||||
| @inject InstanceManager InstanceManager | ||||
|  | ||||
| <Form Model="form" OnSubmit="ExecuteCommand"> | ||||
|   <label for="command-input" class="form-label">Instance Name</label> | ||||
|   <label for="command-input" class="form-label">Execute Command</label> | ||||
|   <div class="input-group flex-nowrap"> | ||||
|     <span class="input-group-text" style="padding-top: 0.3rem;">/</span> | ||||
|     <input id="command-input" class="form-control" type="text" placeholder="command" @bind="form.Command" @bind:event="oninput" disabled="@(Disabled || form.SubmitModel.IsSubmitting)" @ref="commandInputElement" /> | ||||
| @@ -18,7 +19,7 @@ | ||||
|  | ||||
|   [Parameter, EditorRequired] | ||||
|   public Guid AgentGuid { get; set; } | ||||
|    | ||||
|  | ||||
|   [Parameter, EditorRequired] | ||||
|   public Guid InstanceGuid { get; set; } | ||||
|  | ||||
| @@ -36,19 +37,29 @@ | ||||
|   private async Task ExecuteCommand(EditContext context) { | ||||
|     await form.SubmitModel.StartSubmitting(); | ||||
|  | ||||
|     var loggedInUserGuid = await GetUserGuid(); | ||||
|     if (loggedInUserGuid == null || !await CheckPermission(Permission.ControlInstances)) { | ||||
|       form.SubmitModel.StopSubmitting("You do not have permission to execute commands."); | ||||
|       return; | ||||
|     } | ||||
|     var result = await InstanceManager.SendCommandToInstance(await GetAuthenticatedUser(), AgentGuid, InstanceGuid, form.Command, CancellationToken); | ||||
|  | ||||
|     var result = await InstanceManager.SendCommandToInstance(loggedInUserGuid.Value, AgentGuid, InstanceGuid, form.Command, CancellationToken); | ||||
|     if (result.Is(SendCommandToInstanceResult.Success)) { | ||||
|       form.Command = string.Empty; | ||||
|       form.SubmitModel.StopSubmitting(); | ||||
|     } | ||||
|     else { | ||||
|       form.SubmitModel.StopSubmitting(result.Map(Messages.ToSentence, InstanceActionFailureExtensions.ToSentence)); | ||||
|     switch (result.Variant()) { | ||||
|       case Ok<SendCommandToInstanceResult>(SendCommandToInstanceResult.Success): | ||||
|         form.Command = string.Empty; | ||||
|         form.SubmitModel.StopSubmitting(); | ||||
|         break; | ||||
|  | ||||
|       case Ok<SendCommandToInstanceResult>(var sendCommandToInstanceResult): | ||||
|         form.SubmitModel.StopSubmitting(sendCommandToInstanceResult.ToSentence()); | ||||
|         break; | ||||
|  | ||||
|       case Err<UserInstanceActionFailure>(OfInstanceActionFailure(var failure)): | ||||
|         form.SubmitModel.StopSubmitting(failure.ToSentence()); | ||||
|         break; | ||||
|  | ||||
|       case Err<UserInstanceActionFailure>(OfUserActionFailure(UserActionFailure.NotAuthorized)): | ||||
|         form.SubmitModel.StopSubmitting("You do not have permission to send commands to this instance."); | ||||
|         break; | ||||
|  | ||||
|       default: | ||||
|         form.SubmitModel.StopSubmitting("Unknown error."); | ||||
|         break; | ||||
|     } | ||||
|  | ||||
|     StateHasChanged(); | ||||
|   | ||||
| @@ -1,8 +1,9 @@ | ||||
| @using Phantom.Web.Services.Instances | ||||
| @using System.ComponentModel.DataAnnotations | ||||
| @using Phantom.Common.Data.Replies | ||||
| @using Phantom.Common.Data.Web.Users | ||||
| @using Phantom.Utils.Result | ||||
| @using Phantom.Web.Services.Instances | ||||
| @using System.ComponentModel.DataAnnotations | ||||
| @using Phantom.Common.Data.Minecraft | ||||
| @using Phantom.Common.Data.Replies | ||||
| @inherits Phantom.Web.Components.PhantomComponent | ||||
| @inject IJSRuntime Js; | ||||
| @inject InstanceManager InstanceManager; | ||||
| @@ -33,7 +34,7 @@ | ||||
|  | ||||
|   [Parameter, EditorRequired] | ||||
|   public Guid AgentGuid { get; init; } | ||||
|    | ||||
|  | ||||
|   [Parameter, EditorRequired] | ||||
|   public Guid InstanceGuid { get; init; } | ||||
|  | ||||
| @@ -53,19 +54,29 @@ | ||||
|   private async Task StopInstance(EditContext context) { | ||||
|     await form.SubmitModel.StartSubmitting(); | ||||
|  | ||||
|     var loggedInUserGuid = await GetUserGuid(); | ||||
|     if (loggedInUserGuid == null || !await CheckPermission(Permission.ControlInstances)) { | ||||
|       form.SubmitModel.StopSubmitting("You do not have permission to stop instances."); | ||||
|       return; | ||||
|     } | ||||
|     var result = await InstanceManager.StopInstance(await GetAuthenticatedUser(), AgentGuid, InstanceGuid, new MinecraftStopStrategy(form.StopInSeconds), CancellationToken); | ||||
|  | ||||
|     var result = await InstanceManager.StopInstance(loggedInUserGuid.Value, AgentGuid, InstanceGuid, new MinecraftStopStrategy(form.StopInSeconds), CancellationToken); | ||||
|     if (result.Is(StopInstanceResult.StopInitiated)) { | ||||
|       await Js.InvokeVoidAsync("closeModal", ModalId); | ||||
|       form.SubmitModel.StopSubmitting(); | ||||
|     } | ||||
|     else { | ||||
|       form.SubmitModel.StopSubmitting(result.Map(Messages.ToSentence, InstanceActionFailureExtensions.ToSentence)); | ||||
|     switch (result.Variant()) { | ||||
|       case Ok<StopInstanceResult>(StopInstanceResult.StopInitiated): | ||||
|         await Js.InvokeVoidAsync("closeModal", ModalId); | ||||
|         form.SubmitModel.StopSubmitting(); | ||||
|         break; | ||||
|  | ||||
|       case Ok<StopInstanceResult>(var stopInstanceResult): | ||||
|         form.SubmitModel.StopSubmitting(stopInstanceResult.ToSentence()); | ||||
|         break; | ||||
|  | ||||
|       case Err<UserInstanceActionFailure>(OfInstanceActionFailure(var failure)): | ||||
|         form.SubmitModel.StopSubmitting(failure.ToSentence()); | ||||
|         break; | ||||
|  | ||||
|       case Err<UserInstanceActionFailure>(OfUserActionFailure(UserActionFailure.NotAuthorized)): | ||||
|         form.SubmitModel.StopSubmitting("You do not have permission to stop this instance."); | ||||
|         break; | ||||
|  | ||||
|       default: | ||||
|         form.SubmitModel.StopSubmitting("Unknown error."); | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| @using Phantom.Common.Data.Web.Users | ||||
| @using Phantom.Common.Data.Web.Users.CreateUserResults | ||||
| @using Phantom.Utils.Result | ||||
| @using Phantom.Web.Services.Users | ||||
| @using System.ComponentModel.DataAnnotations | ||||
| @inherits Phantom.Web.Components.PhantomComponent | ||||
| @@ -39,7 +40,7 @@ | ||||
|   [Parameter] | ||||
|   public EventCallback<UserInfo> UserAdded { get; set; } | ||||
|  | ||||
|   private readonly AddUserFormModel form = new(); | ||||
|   private readonly AddUserFormModel form = new (); | ||||
|  | ||||
|   private sealed class AddUserFormModel : FormModel { | ||||
|     [Required] | ||||
| @@ -52,23 +53,23 @@ | ||||
|   private async Task AddUser(EditContext context) { | ||||
|     await form.SubmitModel.StartSubmitting(); | ||||
|  | ||||
|     var loggedInUserGuid = await GetUserGuid(); | ||||
|     if (loggedInUserGuid == null || !await CheckPermission(Permission.EditUsers)) { | ||||
|       form.SubmitModel.StopSubmitting("You do not have permission to add users."); | ||||
|       return; | ||||
|     } | ||||
|     var result = await UserManager.Create(await GetAuthenticatedUser(), form.Username, form.Password, CancellationToken); | ||||
|  | ||||
|     switch (await UserManager.Create(loggedInUserGuid.Value, form.Username, form.Password, CancellationToken)) { | ||||
|       case Success success: | ||||
|     switch (result.Variant()) { | ||||
|       case Ok<CreateUserResult>(Success success): | ||||
|         await UserAdded.InvokeAsync(success.User); | ||||
|         await Js.InvokeVoidAsync("closeModal", ModalId); | ||||
|         form.SubmitModel.StopSubmitting(); | ||||
|         break; | ||||
|  | ||||
|       case CreationFailed fail: | ||||
|       case Ok<CreateUserResult>(CreationFailed fail): | ||||
|         form.SubmitModel.StopSubmitting(fail.Error.ToSentences("\n")); | ||||
|         break; | ||||
|  | ||||
|       case Err<UserActionFailure>(UserActionFailure.NotAuthorized): | ||||
|         form.SubmitModel.StopSubmitting("You do not have permission to add users."); | ||||
|         break; | ||||
|  | ||||
|       default: | ||||
|         form.SubmitModel.StopSubmitting("Unknown error."); | ||||
|         break; | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| @using Phantom.Common.Data.Web.Users | ||||
| @using Phantom.Web.Services.Authentication | ||||
| @using Phantom.Web.Services.Users | ||||
| @inherits UserEditDialogBase | ||||
| @inject UserManager UserManager | ||||
| @@ -17,8 +18,13 @@ | ||||
|  | ||||
| @code { | ||||
|  | ||||
|   protected override async Task DoEdit(Guid loggedInUserGuid, UserInfo user) { | ||||
|     switch (await UserManager.DeleteByGuid(loggedInUserGuid, user.Guid, CancellationToken)) { | ||||
|   protected override async Task<UserActionFailure?> DoEdit(AuthenticatedUser? authenticatedUser, UserInfo editedUser) { | ||||
|     var result = await UserManager.DeleteByGuid(authenticatedUser, editedUser.Guid, CancellationToken); | ||||
|     if (!result) { | ||||
|       return result.Error; | ||||
|     } | ||||
|      | ||||
|     switch (result.Value) { | ||||
|       case DeleteUserResult.Deleted: | ||||
|       case DeleteUserResult.NotFound: | ||||
|         await OnEditSuccess(); | ||||
| @@ -28,6 +34,8 @@ | ||||
|         OnEditFailure("Could not delete user."); | ||||
|         break; | ||||
|     } | ||||
|      | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -3,6 +3,7 @@ using Microsoft.JSInterop; | ||||
| using Phantom.Common.Data.Web.Users; | ||||
| using Phantom.Web.Components; | ||||
| using Phantom.Web.Components.Forms; | ||||
| using Phantom.Web.Services.Authentication; | ||||
|  | ||||
| namespace Phantom.Web.Shared; | ||||
|  | ||||
| @@ -16,7 +17,7 @@ public abstract class UserEditDialogBase : PhantomComponent { | ||||
| 	[Parameter] | ||||
| 	public EventCallback<UserInfo> UserModified { get; set; } | ||||
|  | ||||
| 	protected readonly FormButtonSubmit.SubmitModel SubmitModel = new(); | ||||
| 	protected readonly FormButtonSubmit.SubmitModel SubmitModel = new (); | ||||
|  | ||||
| 	private UserInfo? EditedUser { get; set; } = null; | ||||
| 	protected string EditedUserName { get; private set; } = string.Empty; | ||||
| @@ -41,19 +42,26 @@ public abstract class UserEditDialogBase : PhantomComponent { | ||||
| 	protected async Task Submit() { | ||||
| 		await SubmitModel.StartSubmitting(); | ||||
|  | ||||
| 		var loggedInUserGuid = await GetUserGuid(); | ||||
| 		if (loggedInUserGuid == null || !await CheckPermission(Permission.EditUsers)) { | ||||
| 			SubmitModel.StopSubmitting("You do not have permission to edit users."); | ||||
| 		} | ||||
| 		else if (EditedUser == null) { | ||||
| 		if (EditedUser == null) { | ||||
| 			SubmitModel.StopSubmitting("Invalid user."); | ||||
| 			return; | ||||
| 		} | ||||
| 		else { | ||||
| 			await DoEdit(loggedInUserGuid.Value, EditedUser); | ||||
|  | ||||
| 		switch (await DoEdit(await GetAuthenticatedUser(), EditedUser)) { | ||||
| 			case null: | ||||
| 				break; | ||||
|  | ||||
| 			case UserActionFailure.NotAuthorized: | ||||
| 				SubmitModel.StopSubmitting("You do not have permission to edit users."); | ||||
| 				break; | ||||
|  | ||||
| 			default: | ||||
| 				SubmitModel.StopSubmitting("Unknown error."); | ||||
| 				break; | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	protected abstract Task DoEdit(Guid loggedInUserGuid, UserInfo user); | ||||
|  | ||||
| 	protected abstract Task<UserActionFailure?> DoEdit(AuthenticatedUser? authenticatedUser, UserInfo editedUser); | ||||
|  | ||||
| 	protected async Task OnEditSuccess() { | ||||
| 		await UserModified.InvokeAsync(EditedUser); | ||||
| @@ -61,7 +69,7 @@ public abstract class UserEditDialogBase : PhantomComponent { | ||||
| 		SubmitModel.StopSubmitting(); | ||||
| 		OnClosed(); | ||||
| 	} | ||||
| 	 | ||||
|  | ||||
| 	protected void OnEditFailure(string message) { | ||||
| 		SubmitModel.StopSubmitting(message); | ||||
| 	} | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| @using System.Collections.Immutable | ||||
| @using Phantom.Common.Data.Web.Users | ||||
| @using Phantom.Web.Services.Authentication | ||||
| @using Phantom.Web.Services.Users | ||||
| @inherits UserEditDialogBase | ||||
| @inject RoleManager RoleManager | ||||
| @@ -36,8 +37,8 @@ | ||||
|     this.items = allRoles.Select(role => new RoleItem(role, currentRoleGuids.Contains(role.Guid))).ToList(); | ||||
|   } | ||||
|  | ||||
|   protected override async Task DoEdit(Guid loggedInUserGuid, UserInfo user) { | ||||
|     var currentRoleGuids = await UserRoleManager.GetUserRoles(user.Guid, CancellationToken); | ||||
|   protected override async Task<UserActionFailure?> DoEdit(AuthenticatedUser? authenticatedUser, UserInfo editedUser) { | ||||
|     var currentRoleGuids = await UserRoleManager.GetUserRoles(editedUser.Guid, CancellationToken); | ||||
|     var addToRoleGuids = ImmutableHashSet.CreateBuilder<Guid>(); | ||||
|     var removeFromRoleGuids = ImmutableHashSet.CreateBuilder<Guid>(); | ||||
|  | ||||
| @@ -56,18 +57,21 @@ | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     await DoChangeUserRoles(user, loggedInUserGuid, addToRoleGuids.ToImmutable(), removeFromRoleGuids.ToImmutable()); | ||||
|     return await DoChangeUserRoles(authenticatedUser, editedUser, addToRoleGuids.ToImmutable(), removeFromRoleGuids.ToImmutable()); | ||||
|   } | ||||
|  | ||||
|   private async Task DoChangeUserRoles(UserInfo user, Guid loggedInUserGuid, ImmutableHashSet<Guid> addToRoleGuids, ImmutableHashSet<Guid> removeFromRoleGuids) { | ||||
|     var result = await UserRoleManager.ChangeUserRoles(loggedInUserGuid, user.Guid, addToRoleGuids, removeFromRoleGuids, CancellationToken); | ||||
|   private async Task<UserActionFailure?> DoChangeUserRoles(AuthenticatedUser? authenticatedUser, UserInfo editedUser, ImmutableHashSet<Guid> addToRoleGuids, ImmutableHashSet<Guid> removeFromRoleGuids) { | ||||
|     var result = await UserRoleManager.ChangeUserRoles(authenticatedUser, editedUser.Guid, addToRoleGuids, removeFromRoleGuids, CancellationToken); | ||||
|     if (!result) { | ||||
|       return result.Error; | ||||
|     } | ||||
|      | ||||
|     var failedToAdd = addToRoleGuids.Except(result.AddedToRoleGuids); | ||||
|     var failedToRemove = removeFromRoleGuids.Except(result.RemovedFromRoleGuids); | ||||
|     var failedToAdd = addToRoleGuids.Except(result.Value.AddedToRoleGuids); | ||||
|     var failedToRemove = removeFromRoleGuids.Except(result.Value.RemovedFromRoleGuids); | ||||
|      | ||||
|     if (failedToAdd.IsEmpty && failedToRemove.IsEmpty) { | ||||
|       await OnEditSuccess(); | ||||
|       return; | ||||
|       return null; | ||||
|     } | ||||
|      | ||||
|     var errors = new List<string>(); | ||||
| @@ -81,6 +85,7 @@ | ||||
|     } | ||||
|      | ||||
|     OnEditFailure(string.Join("\n", errors)); | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   private string GetRoleName(Guid roleGuid) { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user