1
0
mirror of https://github.com/chylex/Minecraft-Phantom-Panel.git synced 2024-10-18 15:42:50 +02:00

Compare commits

..

2 Commits

Author SHA1 Message Date
149bb6e0f1
Reimplement Web service 2023-10-31 22:10:09 +01:00
f14d7f5590
WIP 2023-10-31 22:10:09 +01:00
44 changed files with 1657 additions and 1552 deletions

View File

@ -18,8 +18,4 @@ public sealed class ControllerConnection {
public Task Send<TMessage>(TMessage message) where TMessage : IMessageToController { public Task Send<TMessage>(TMessage message) where TMessage : IMessageToController {
return connection.Send(message); return connection.Send(message);
} }
public Task<TReply?> Send<TMessage, TReply>(TMessage message, TimeSpan waitForReplyTime, CancellationToken waitForReplyCancellationToken) where TMessage : IMessageToController<TReply> where TReply : class {
return connection.Send<TMessage, TReply>(message, waitForReplyTime, waitForReplyCancellationToken);
}
} }

View File

@ -0,0 +1,11 @@
using System.Collections.Immutable;
using MemoryPack;
namespace Phantom.Common.Data.Web.Users;
[MemoryPackable(GenerateType.VersionTolerant)]
public sealed partial record LogInSuccess (
[property: MemoryPackOrder(0)] Guid UserGuid,
[property: MemoryPackOrder(1)] PermissionSet Permissions,
[property: MemoryPackOrder(2)] ImmutableArray<byte> Token
);

View File

@ -1,14 +1,18 @@
using System.Collections.Immutable; using System.Collections.Immutable;
using MemoryPack;
namespace Phantom.Common.Data.Web.Users; namespace Phantom.Common.Data.Web.Users;
public sealed class IdentityPermissions { [MemoryPackable(GenerateType.VersionTolerant)]
public static IdentityPermissions None { get; } = new (ImmutableHashSet<string>.Empty); public sealed partial class PermissionSet {
public static PermissionSet None { get; } = new (ImmutableHashSet<string>.Empty);
[MemoryPackOrder(0)]
[MemoryPackInclude]
private readonly ImmutableHashSet<string> permissionIds; private readonly ImmutableHashSet<string> permissionIds;
public IdentityPermissions(ImmutableHashSet<string> permissionIdsQuery) { public PermissionSet(ImmutableHashSet<string> permissionIds) {
this.permissionIds = permissionIdsQuery; this.permissionIds = permissionIds;
} }
public bool Check(Permission? permission) { public bool Check(Permission? permission) {

View File

@ -8,6 +8,6 @@ namespace Phantom.Common.Messages.Web;
public interface IMessageToControllerListener { public interface IMessageToControllerListener {
Task<NoReply> HandleRegisterWeb(RegisterWebMessage message); Task<NoReply> HandleRegisterWeb(RegisterWebMessage message);
Task<CreateOrUpdateAdministratorUserResult> CreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUser message); Task<CreateOrUpdateAdministratorUserResult> CreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUser message);
Task<byte[]?> HandleLogIn(LogIn message); Task<LogInSuccess?> HandleLogIn(LogIn message);
Task<NoReply> HandleReply(ReplyMessage message); Task<NoReply> HandleReply(ReplyMessage message);
} }

View File

@ -1,4 +1,5 @@
using MemoryPack; using MemoryPack;
using Phantom.Common.Data.Web.Users;
namespace Phantom.Common.Messages.Web.ToController; namespace Phantom.Common.Messages.Web.ToController;
@ -6,8 +7,8 @@ namespace Phantom.Common.Messages.Web.ToController;
public sealed partial record LogIn( public sealed partial record LogIn(
[property: MemoryPackOrder(0)] string Username, [property: MemoryPackOrder(0)] string Username,
[property: MemoryPackOrder(1)] string Password [property: MemoryPackOrder(1)] string Password
) : IMessageToController<byte[]?> { ) : IMessageToController<LogInSuccess?> {
public Task<byte[]?> Accept(IMessageToControllerListener listener) { public Task<LogInSuccess?> Accept(IMessageToControllerListener listener) {
return listener.HandleLogIn(this); return listener.HandleLogIn(this);
} }
}; };

View File

@ -16,6 +16,7 @@ public static class WebMessageRegistries {
static WebMessageRegistries() { static WebMessageRegistries() {
ToController.Add<RegisterWebMessage>(0); ToController.Add<RegisterWebMessage>(0);
ToController.Add<CreateOrUpdateAdministratorUser, CreateOrUpdateAdministratorUserResult>(1); ToController.Add<CreateOrUpdateAdministratorUser, CreateOrUpdateAdministratorUserResult>(1);
ToController.Add<LogIn, LogInSuccess?>(2);
ToController.Add<ReplyMessage>(127); ToController.Add<ReplyMessage>(127);
ToWeb.Add<RegisterWebResultMessage>(0); ToWeb.Add<RegisterWebResultMessage>(0);

View File

@ -66,7 +66,7 @@ public sealed class RpcConnectionToClient<TListener> {
} }
await socket.SendAsync(routingId, bytes); await socket.SendAsync(routingId, bytes);
return await messageReplyTracker.WaitForReply<TReply>(sequenceId, waitForReplyTime, waitForReplyCancellationToken); return await messageReplyTracker.TryWaitForReply<TReply>(sequenceId, waitForReplyTime, waitForReplyCancellationToken);
} }
public void Receive(IReply message) { public void Receive(IReply message) {

View File

@ -48,7 +48,7 @@ public sealed class ControllerServices {
this.RoleManager = new RoleManager(dbProvider); this.RoleManager = new RoleManager(dbProvider);
this.PermissionManager = new PermissionManager(dbProvider); this.PermissionManager = new PermissionManager(dbProvider);
this.UserLoginManager = new UserLoginManager(UserManager); this.UserLoginManager = new UserLoginManager(UserManager, PermissionManager);
this.dbProvider = dbProvider; this.dbProvider = dbProvider;
this.webAuthToken = webAuthToken; this.webAuthToken = webAuthToken;

View File

@ -45,7 +45,7 @@ public sealed class WebMessageListener : IMessageToControllerListener {
return userManager.CreateOrUpdateAdministrator(message.Username, message.Password); return userManager.CreateOrUpdateAdministrator(message.Username, message.Password);
} }
public Task<byte[]?> HandleLogIn(LogIn message) { public Task<LogInSuccess?> HandleLogIn(LogIn message) {
return userLoginManager.LogIn(message.Username, message.Password); return userLoginManager.LogIn(message.Username, message.Password);
} }

View File

@ -9,17 +9,16 @@ using Serilog;
namespace Phantom.Controller.Services.Users; namespace Phantom.Controller.Services.Users;
public sealed class PermissionManager { sealed class PermissionManager {
private static readonly ILogger Logger = PhantomLogger.Create<PermissionManager>(); private static readonly ILogger Logger = PhantomLogger.Create<PermissionManager>();
private readonly IDbContextProvider dbProvider; private readonly IDbContextProvider dbProvider;
private readonly Dictionary<Guid, IdentityPermissions> userIdsToPermissionIds = new ();
public PermissionManager(IDbContextProvider dbProvider) { public PermissionManager(IDbContextProvider dbProvider) {
this.dbProvider = dbProvider; this.dbProvider = dbProvider;
} }
internal async Task Initialize() { public async Task Initialize() {
Logger.Information("Adding default permissions to database."); Logger.Information("Adding default permissions to database.");
await using var ctx = dbProvider.Eager(); await using var ctx = dbProvider.Eager();
@ -37,32 +36,21 @@ public sealed class PermissionManager {
} }
} }
internal static ImmutableArray<string> GetMissingPermissionsOrdered(IEnumerable<Permission> allPermissions, ImmutableHashSet<string> existingPermissionIds) { 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(); return allPermissions.Select(static permission => permission.Id).Except(existingPermissionIds).Order().ToImmutableArray();
} }
private async Task<IdentityPermissions> 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 IdentityPermissions(await userPermissions.Union(rolePermissions).AsAsyncEnumerable().ToImmutableSetAsync());
}
// private IdentityPermissions GetPermissionsForUserId(Guid userId, bool refreshCache) {
// if (!refreshCache && userIdsToPermissionIds.TryGetValue(userId, out var userPermissions)) {
// return userPermissions;
// }
// else {
// return userIdsToPermissionIds[userId] = FetchPermissionsForUserId(userId);
// }
// }
//
// public IdentityPermissions GetPermissions(ClaimsPrincipal user, bool refreshCache = false) {
// Guid? userId = UserManager.GetAuthenticatedUserId(user);
// return userId == null ? IdentityPermissions.None : GetPermissionsForUserId(userId.Value, refreshCache);
// }
//
// public bool CheckPermission(ClaimsPrincipal user, Permission permission, bool refreshCache = false) {
// return GetPermissions(user, refreshCache).Check(permission);
// }
} }

View File

@ -1,30 +1,34 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Security.Cryptography; using System.Security.Cryptography;
using Phantom.Common.Data.Web.Users;
namespace Phantom.Controller.Services.Users; namespace Phantom.Controller.Services.Users;
sealed class UserLoginManager { sealed class UserLoginManager {
private const int SessionIdBytes = 20; private const int SessionIdBytes = 20;
private readonly ConcurrentDictionary<string, List<byte[]>> sessionTokensByUsername = new (); private readonly ConcurrentDictionary<string, List<ImmutableArray<byte>>> sessionTokensByUsername = new ();
private readonly UserManager userManager; private readonly UserManager userManager;
private readonly PermissionManager permissionManager;
public UserLoginManager(UserManager userManager) { public UserLoginManager(UserManager userManager, PermissionManager permissionManager) {
this.userManager = userManager; this.userManager = userManager;
this.permissionManager = permissionManager;
} }
public async Task<byte[]?> LogIn(string username, string password) { public async Task<LogInSuccess?> LogIn(string username, string password) {
var user = await userManager.GetAuthenticated(username, password); var user = await userManager.GetAuthenticated(username, password);
if (user == null) { if (user == null) {
return null; return null;
} }
var token = RandomNumberGenerator.GetBytes(SessionIdBytes); var token = ImmutableArray.Create(RandomNumberGenerator.GetBytes(SessionIdBytes));
var sessionTokens = sessionTokensByUsername.GetOrAdd(username, static _ => new List<byte[]>()); var sessionTokens = sessionTokensByUsername.GetOrAdd(username, static _ => new List<ImmutableArray<byte>>());
lock (sessionTokens) { lock (sessionTokens) {
sessionTokens.Add(token); sessionTokens.Add(token);
} }
return token; return new LogInSuccess(user.UserGuid, await permissionManager.FetchPermissionsForUserId(user.UserGuid), token);
} }
} }

View File

@ -20,27 +20,37 @@ public sealed class MessageReplyTracker {
return sequenceId; return sequenceId;
} }
public async Task<TReply?> WaitForReply<TReply>(uint sequenceId, TimeSpan waitForReplyTime, CancellationToken cancellationToken) where TReply : class { public async Task<TReply> WaitForReply<TReply>(uint sequenceId, TimeSpan waitForReplyTime, CancellationToken cancellationToken) {
if (!replyTasks.TryGetValue(sequenceId, out var completionSource)) { if (!replyTasks.TryGetValue(sequenceId, out var completionSource)) {
logger.Warning("No reply callback for id {SequenceId}.", sequenceId); logger.Warning("No reply callback for id {SequenceId}.", sequenceId);
return null; throw new ArgumentException("No reply callback for id: " + sequenceId, nameof(sequenceId));
} }
try { try {
byte[] replyBytes = await completionSource.Task.WaitAsync(waitForReplyTime, cancellationToken); byte[] replyBytes = await completionSource.Task.WaitAsync(waitForReplyTime, cancellationToken);
return MessageSerializer.Deserialize<TReply>(replyBytes); return MessageSerializer.Deserialize<TReply>(replyBytes);
} catch (TimeoutException) { } catch (TimeoutException) {
return null; logger.Debug("Timed out waiting for reply with id {SequenceId}.", sequenceId);
throw;
} catch (OperationCanceledException) { } catch (OperationCanceledException) {
return null; logger.Debug("Cancelled waiting for reply with id {SequenceId}.", sequenceId);
throw;
} catch (Exception e) { } catch (Exception e) {
logger.Warning(e, "Error processing reply with id {SequenceId}.", sequenceId); logger.Warning(e, "Error processing reply with id {SequenceId}.", sequenceId);
return null; throw;
} finally { } finally {
ForgetReply(sequenceId); ForgetReply(sequenceId);
} }
} }
public async Task<TReply?> TryWaitForReply<TReply>(uint sequenceId, TimeSpan waitForReplyTime, CancellationToken cancellationToken) where TReply : class {
try {
return await WaitForReply<TReply>(sequenceId, waitForReplyTime, cancellationToken);
} catch (Exception) {
return null;
}
}
public void ForgetReply(uint sequenceId) { public void ForgetReply(uint sequenceId) {
if (replyTasks.TryRemove(sequenceId, out var task)) { if (replyTasks.TryRemove(sequenceId, out var task)) {
task.SetCanceled(); task.SetCanceled();

View File

@ -22,7 +22,7 @@ public sealed class RpcConnectionToServer<TListener> {
} }
} }
public async Task<TReply?> Send<TMessage, TReply>(TMessage message, TimeSpan waitForReplyTime, CancellationToken waitForReplyCancellationToken) where TMessage : IMessage<TListener, TReply> where TReply : class { public async Task<TReply?> TrySend<TMessage, TReply>(TMessage message, TimeSpan waitForReplyTime, CancellationToken waitForReplyCancellationToken) where TMessage : IMessage<TListener, TReply> where TReply : class {
var sequenceId = replyTracker.RegisterReply(); var sequenceId = replyTracker.RegisterReply();
var bytes = messageRegistry.Write<TMessage, TReply>(sequenceId, message).ToArray(); var bytes = messageRegistry.Write<TMessage, TReply>(sequenceId, message).ToArray();
@ -31,6 +31,19 @@ public sealed class RpcConnectionToServer<TListener> {
return null; return null;
} }
await socket.SendAsync(bytes);
return await replyTracker.TryWaitForReply<TReply>(sequenceId, waitForReplyTime, waitForReplyCancellationToken);
}
public async Task<TReply> Send<TMessage, TReply>(TMessage message, TimeSpan waitForReplyTime, CancellationToken waitForReplyCancellationToken) where TMessage : IMessage<TListener, TReply> {
var sequenceId = replyTracker.RegisterReply();
var bytes = messageRegistry.Write<TMessage, TReply>(sequenceId, message).ToArray();
if (bytes.Length == 0) {
replyTracker.ForgetReply(sequenceId);
throw new ArgumentException("Could not write message.", nameof(message));
}
await socket.SendAsync(bytes); await socket.SendAsync(bytes);
return await replyTracker.WaitForReply<TReply>(sequenceId, waitForReplyTime, waitForReplyCancellationToken); return await replyTracker.WaitForReply<TReply>(sequenceId, waitForReplyTime, waitForReplyCancellationToken);
} }

View File

@ -0,0 +1,90 @@
using System.Collections.Immutable;
using System.Security.Claims;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
using Phantom.Common.Logging;
using ILogger = Serilog.ILogger;
namespace Phantom.Web.Services.Authentication;
public sealed class PhantomAuthenticationStateProvider : ServerAuthenticationStateProvider {
private const string SessionTokenKey = "PhantomSession";
private static readonly ILogger Logger = PhantomLogger.Create<PhantomAuthenticationStateProvider>();
private readonly PhantomLoginSessions loginSessions;
private readonly ProtectedLocalStorage localStorage;
private bool isLoaded;
public PhantomAuthenticationStateProvider(PhantomLoginSessions loginSessions, ProtectedLocalStorage localStorage) {
this.loginSessions = loginSessions;
this.localStorage = localStorage;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync() {
if (isLoaded) {
return await base.GetAuthenticationStateAsync();
}
LocalStorageEntry? stored;
try {
stored = await GetLocalStorageEntry();
} catch (InvalidOperationException) {
stored = null;
}
if (stored != null) {
var session = loginSessions.Find(stored.UserGuid, stored.Token);
if (session != null) {
SetLoadedSession(session);
}
}
return await base.GetAuthenticationStateAsync();
}
private sealed record LocalStorageEntry(Guid UserGuid, ImmutableArray<byte> Token);
private async Task<LocalStorageEntry?> GetLocalStorageEntry() {
try {
var result = await localStorage.GetAsync<LocalStorageEntry>(SessionTokenKey);
return result.Success ? result.Value : null;
} catch (InvalidOperationException) {
return null;
} catch (CryptographicException) {
return null;
} catch (Exception e) {
Logger.Error(e, "Could not read local storage entry.");
return null;
}
}
private void SetLoadedSession(UserSession session) {
isLoaded = true;
SetAuthenticationState(Task.FromResult(new AuthenticationState(session.AsClaimsPrincipal)));
}
internal async Task HandleLogin(UserSession session) {
await localStorage.SetAsync(SessionTokenKey, new LocalStorageEntry(session.UserGuid, session.Token));
loginSessions.Add(session);
SetLoadedSession(session);
}
internal async Task HandleLogout() {
if (!isLoaded) {
return;
}
await localStorage.DeleteAsync(SessionTokenKey);
var stored = await GetLocalStorageEntry();
if (stored != null) {
loginSessions.Remove(stored.UserGuid, stored.Token);
}
isLoaded = false;
SetAuthenticationState(Task.FromResult(new AuthenticationState(new ClaimsPrincipal())));
}
}

View File

@ -1,37 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Authentication;
namespace Phantom.Web.Services.Authentication;
sealed class PhantomIdentityMiddleware {
public const string LoginPath = "/login";
public const string LogoutPath = "/logout";
public static bool AcceptsPath(HttpContext context) {
var path = context.Request.Path;
return path == LoginPath || path == LogoutPath;
}
private readonly RequestDelegate next;
public PhantomIdentityMiddleware(RequestDelegate next) {
this.next = next;
}
[SuppressMessage("ReSharper", "UnusedMember.Global")]
public async Task InvokeAsync(HttpContext context, INavigation navigation, PhantomLoginManager loginManager) {
var path = context.Request.Path;
if (path == LoginPath && context.Request.Query.TryGetValue("token", out var tokens) && tokens[0] is {} token && await loginManager.ProcessToken(token) is {} result) {
await context.SignInAsync(result.ClaimsPrincipal, result.AuthenticationProperties);
context.Response.Redirect(navigation.BasePath + result.ReturnUrl);
}
else if (path == LogoutPath) {
loginManager.OnSignedOut(context.User);
await context.SignOutAsync();
context.Response.Redirect(navigation.BasePath);
}
else {
await next.Invoke(context);
}
}
}

View File

@ -1,8 +1,8 @@
using System.Security.Claims; using System.Security.Claims;
using Microsoft.AspNetCore.Authentication; using Phantom.Common.Data.Web.Users;
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
using Phantom.Common.Logging; using Phantom.Common.Logging;
using Phantom.Utils.Cryptography; using Phantom.Common.Messages.Web.ToController;
using Phantom.Web.Services.Rpc;
using ILogger = Serilog.ILogger; using ILogger = Serilog.ILogger;
namespace Phantom.Web.Services.Authentication; namespace Phantom.Web.Services.Authentication;
@ -10,66 +10,44 @@ namespace Phantom.Web.Services.Authentication;
public sealed class PhantomLoginManager { public sealed class PhantomLoginManager {
private static readonly ILogger Logger = PhantomLogger.Create<PhantomLoginManager>(); private static readonly ILogger Logger = PhantomLogger.Create<PhantomLoginManager>();
public static bool IsAuthenticated(ClaimsPrincipal user) { public const string AuthenticationType = "Phantom";
return user.Identity is { IsAuthenticated: true };
}
private readonly INavigation navigation; private readonly INavigation navigation;
private readonly PhantomLoginStore loginStore; private readonly PhantomAuthenticationStateProvider authenticationStateProvider;
private readonly ProtectedSessionStorage sessionStorage; private readonly ControllerConnection controllerConnection;
public PhantomLoginManager(INavigation navigation, PhantomLoginStore loginStore, ProtectedSessionStorage sessionStorage) { public PhantomLoginManager(INavigation navigation, PhantomAuthenticationStateProvider authenticationStateProvider, ControllerConnection controllerConnection) {
this.navigation = navigation; this.navigation = navigation;
this.loginStore = loginStore; this.authenticationStateProvider = authenticationStateProvider;
this.sessionStorage = sessionStorage; this.controllerConnection = controllerConnection;
} }
public async Task<bool> SignIn(string username, string password, string? returnUrl = null) { public async Task<bool> SignIn(string username, string password, string? returnUrl = null) {
LogInSuccess? success;
try {
success = await controllerConnection.Send<LogIn, LogInSuccess?>(new LogIn(username, password), TimeSpan.FromSeconds(30));
} catch (Exception e) {
Logger.Error(e, "Could not log in {Username}.", username);
return false; return false;
// if (await userManager.GetAuthenticated(username, password) == null) { }
// return false;
// }
Logger.Debug("Created login token for {Username}.", username); if (success == null) {
return false;
}
string token = TokenGenerator.Create(60); Logger.Information("Successfully logged in {Username}.", username);
loginStore.Add(token, username, password, returnUrl ?? string.Empty);
navigation.NavigateTo("login" + QueryString.Create("token", token), forceLoad: true);
var identity = new ClaimsIdentity(AuthenticationType);
identity.AddClaim(new Claim(ClaimTypes.Name, username));
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, success.UserGuid.ToString()));
await authenticationStateProvider.HandleLogin(new UserSession(success.UserGuid, username, success.Token));
await navigation.NavigateTo(returnUrl ?? string.Empty);
return true; return true;
} }
internal async Task<SignInResult?> ProcessToken(string token) { public async Task SignOut() {
return null; await navigation.NavigateTo(string.Empty);
// var entry = loginStore.Pop(token); await authenticationStateProvider.HandleLogout();
// if (entry == null) {
// return null;
// }
//
// var user = await userManager.GetAuthenticated(entry.Username, entry.Password);
// if (user == null) {
// return null;
// }
//
// Logger.Information("Successful login for {Username}.", user.Name);
// loginEvents.UserLoggedIn(user);
//
// var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
// identity.AddClaim(new Claim(ClaimTypes.Name, user.Name));
// identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.UserGuid.ToString()));
//
// var authenticationProperties = new AuthenticationProperties {
// IsPersistent = true
// };
//
// return new SignInResult(new ClaimsPrincipal(identity), authenticationProperties, entry.ReturnUrl);
}
internal sealed record SignInResult(ClaimsPrincipal ClaimsPrincipal, AuthenticationProperties AuthenticationProperties, string ReturnUrl);
internal void OnSignedOut(ClaimsPrincipal user) {
// if (UserManager.GetAuthenticatedUserId(user) is {} userGuid) {
// loginEvents.UserLoggedOut(userGuid);
// }
} }
} }

View File

@ -0,0 +1,49 @@
using System.Collections.Concurrent;
using System.Collections.Immutable;
namespace Phantom.Web.Services.Authentication;
public sealed class PhantomLoginSessions {
private readonly ConcurrentDictionary<Guid, List<UserSession>> userSessions = new ();
internal void Add(UserSession session) {
var sessions = userSessions.GetOrAdd(session.UserGuid, static _ => new List<UserSession>());
lock (sessions) {
RemoveSessionInternal(sessions, session.Token);
sessions.Add(session);
}
}
internal UserSession? Find(Guid userGuid, ImmutableArray<byte> token) {
if (!userSessions.TryGetValue(userGuid, out var sessions)) {
return null;
}
lock (sessions) {
int index = FindSessionInternal(sessions, token);
return index == -1 ? null : sessions[index];
}
}
internal void Remove(Guid userGuid, ImmutableArray<byte> token) {
if (!userSessions.TryGetValue(userGuid, out var sessions)) {
return;
}
lock (sessions) {
RemoveSessionInternal(sessions, token);
}
}
private static int FindSessionInternal(List<UserSession> sessions, ImmutableArray<byte> token) {
return sessions.FindIndex(s => s.TokenEquals(token));
}
private static void RemoveSessionInternal(List<UserSession> sessions, ImmutableArray<byte> token) {
int index = FindSessionInternal(sessions, token);
if (index != -1) {
sessions.RemoveAt(index);
}
}
}

View File

@ -1,62 +0,0 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using Phantom.Common.Logging;
using Phantom.Utils.Tasks;
using ILogger = Serilog.ILogger;
namespace Phantom.Web.Services.Authentication;
public sealed class PhantomLoginStore {
private static readonly ILogger Logger = PhantomLogger.Create<PhantomLoginStore>();
private static readonly TimeSpan ExpirationTime = TimeSpan.FromMinutes(1);
internal static Func<IServiceProvider, PhantomLoginStore> Create(CancellationToken cancellationToken) {
return provider => new PhantomLoginStore(provider.GetRequiredService<TaskManager>(), cancellationToken);
}
private readonly ConcurrentDictionary<string, LoginEntry> loginEntries = new ();
private readonly CancellationToken cancellationToken;
private PhantomLoginStore(TaskManager taskManager, CancellationToken cancellationToken) {
this.cancellationToken = cancellationToken;
taskManager.Run("Web login entry expiration loop", RunExpirationLoop);
}
private async Task RunExpirationLoop() {
try {
while (true) {
await Task.Delay(ExpirationTime, cancellationToken);
foreach (var (token, entry) in loginEntries) {
if (entry.IsExpired) {
Logger.Debug("Expired login entry for {Username}.", entry.Username);
loginEntries.TryRemove(token, out _);
}
}
}
} finally {
Logger.Information("Expiration loop stopped.");
}
}
internal void Add(string token, string username, string password, string returnUrl) {
loginEntries[token] = new LoginEntry(username, password, returnUrl, Stopwatch.StartNew());
}
internal LoginEntry? Pop(string token) {
if (!loginEntries.TryRemove(token, out var entry)) {
return null;
}
if (entry.IsExpired) {
Logger.Debug("Expired login entry for {Username}.", entry.Username);
return null;
}
return entry;
}
internal sealed record LoginEntry(string Username, string Password, string ReturnUrl, Stopwatch AddedTime) {
public bool IsExpired => AddedTime.Elapsed >= ExpirationTime;
}
}

View File

@ -0,0 +1,32 @@
using System.Collections.Immutable;
using System.Security.Claims;
using System.Security.Cryptography;
namespace Phantom.Web.Services.Authentication;
sealed class UserSession {
public Guid UserGuid { get; }
public string Username { get; }
public ImmutableArray<byte> Token { get; }
public ClaimsPrincipal AsClaimsPrincipal {
get {
var identity = new ClaimsIdentity(PhantomLoginManager.AuthenticationType);
identity.AddClaim(new Claim(ClaimTypes.Name, Username));
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, UserGuid.ToString()));
return new ClaimsPrincipal(identity);
}
}
public UserSession(Guid userGuid, string username, ImmutableArray<byte> token) {
UserGuid = userGuid;
Username = username;
Token = token;
}
public bool TokenEquals(ImmutableArray<byte> other) {
return CryptographicOperations.FixedTimeEquals(Token.AsSpan(), other.AsSpan());
}
}

View File

@ -6,7 +6,7 @@ namespace Phantom.Web.Services.Authorization;
// TODO // TODO
public class PermissionManager { public class PermissionManager {
public IdentityPermissions GetPermissions(ClaimsPrincipal user, bool refreshCache = false) { public IdentityPermissions GetPermissions(ClaimsPrincipal user, bool refreshCache = false) {
return IdentityPermissions.None;
} }
public bool CheckPermission(ClaimsPrincipal user, Permission permission, bool refreshCache = false) { public bool CheckPermission(ClaimsPrincipal user, Permission permission, bool refreshCache = false) {

View File

@ -5,5 +5,5 @@ namespace Phantom.Web.Services;
public interface INavigation { public interface INavigation {
string BasePath { get; } string BasePath { get; }
bool GetQueryParameter(string key, [MaybeNullWhen(false)] out string value); bool GetQueryParameter(string key, [MaybeNullWhen(false)] out string value);
void NavigateTo(string url, bool forceLoad = false); Task NavigateTo(string url, bool forceLoad = false);
} }

View File

@ -1,8 +1,10 @@
namespace Phantom.Web.Services.Instances; using Phantom.Common.Data.Replies;
namespace Phantom.Web.Services.Instances;
// TODO // TODO
public class InstanceManager { public class InstanceManager {
// public async Task<InstanceActionResult<SendCommandToInstanceResult>> SendCommand(Guid instanceGuid, string command) { public async Task<InstanceActionResult<SendCommandToInstanceResult>> SendCommand(Guid instanceGuid, string command) {
//
// } }
} }

View File

@ -1,6 +1,5 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
using Phantom.Common.Data.Web.Users; using Phantom.Common.Data.Web.Users;
using Phantom.Web.Services.Authentication; using Phantom.Web.Services.Authentication;
using Phantom.Web.Services.Authorization; using Phantom.Web.Services.Authorization;
@ -9,23 +8,23 @@ using Phantom.Web.Services.Rpc;
namespace Phantom.Web.Services; namespace Phantom.Web.Services;
public static class PhantomWebServices { public static class PhantomWebServices {
public static void AddPhantomServices(this IServiceCollection services, CancellationToken cancellationToken) { public static void AddPhantomServices(this IServiceCollection services) {
services.AddSingleton<MessageListener>(); services.AddSingleton<MessageListener>();
services.AddSingleton<ControllerCommunication>(); services.AddSingleton<ControllerConnection>();
services.AddSingleton<PermissionManager>(); services.AddSingleton<PermissionManager>();
services.AddSingleton(PhantomLoginStore.Create(cancellationToken)); services.AddSingleton<PhantomLoginSessions>();
services.AddScoped<PhantomLoginManager>(); services.AddScoped<PhantomLoginManager>();
services.AddScoped<PhantomAuthenticationStateProvider>();
services.AddScoped<AuthenticationStateProvider>(static services => services.GetRequiredService<PhantomAuthenticationStateProvider>());
services.AddScoped<IHostEnvironmentAuthenticationStateProvider>(static services => services.GetRequiredService<PhantomAuthenticationStateProvider>());
services.AddAuthorization(ConfigureAuthorization); services.AddAuthorization(ConfigureAuthorization);
services.AddScoped<IAuthorizationHandler, PermissionBasedPolicyHandler>(); services.AddScoped<IAuthorizationHandler, PermissionBasedPolicyHandler>();
services.AddScoped<AuthenticationStateProvider, ServerAuthenticationStateProvider>();
} }
public static void UsePhantomServices(this IApplicationBuilder application) { public static void UsePhantomServices(this IApplicationBuilder application) {
application.UseAuthentication();
application.UseAuthorization(); application.UseAuthorization();
application.UseWhen(PhantomIdentityMiddleware.AcceptsPath, static app => app.UseMiddleware<PhantomIdentityMiddleware>());
} }
private static void ConfigureAuthorization(AuthorizationOptions o) { private static void ConfigureAuthorization(AuthorizationOptions o) {

View File

@ -3,10 +3,10 @@ using Phantom.Utils.Rpc;
namespace Phantom.Web.Services.Rpc; namespace Phantom.Web.Services.Rpc;
public sealed class ControllerCommunication { public sealed class ControllerConnection {
private readonly RpcConnectionToServer<IMessageToControllerListener> connection; private readonly RpcConnectionToServer<IMessageToControllerListener> connection;
public ControllerCommunication(RpcConnectionToServer<IMessageToControllerListener> connection) { public ControllerConnection(RpcConnectionToServer<IMessageToControllerListener> connection) {
this.connection = connection; this.connection = connection;
} }
@ -14,7 +14,7 @@ public sealed class ControllerCommunication {
return connection.Send(message); return connection.Send(message);
} }
public Task<TReply?> Send<TMessage, TReply>(TMessage message, TimeSpan timeout) where TMessage : IMessageToController<TReply> where TReply : class { public Task<TReply> Send<TMessage, TReply>(TMessage message, TimeSpan timeout) where TMessage : IMessageToController<TReply> {
return connection.Send<TMessage, TReply>(message, timeout, CancellationToken.None); return connection.Send<TMessage, TReply>(message, timeout, CancellationToken.None);
} }
} }

View File

@ -1,5 +1,4 @@
@using Phantom.Web.Services @using Phantom.Web.Services
@using Phantom.Web.Services.Authentication
@inject INavigation Nav @inject INavigation Nav
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@ -8,14 +7,14 @@
<Found Context="routeData"> <Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"> <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<NotAuthorized> <NotAuthorized>
@if (!PhantomLoginManager.IsAuthenticated(context.User)) { @if (context.User.Identity is { IsAuthenticated: true }) {
var returnUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri).TrimEnd('/');
Nav.NavigateTo("login" + QueryString.Create("return", returnUrl), forceLoad: true);
}
else {
<h1>Forbidden</h1> <h1>Forbidden</h1>
<p role="alert">You do not have permission to visit this page.</p> <p role="alert">You do not have permission to visit this page.</p>
} }
else {
var returnUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri).TrimEnd('/');
Nav.NavigateTo("login" + QueryString.Create("return", returnUrl), forceLoad: true);
}
</NotAuthorized> </NotAuthorized>
</AuthorizeRouteView> </AuthorizeRouteView>
<FocusOnNavigate RouteData="@routeData" Selector="h1" /> <FocusOnNavigate RouteData="@routeData" Selector="h1" />

View File

@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Web; using System.Web;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using Phantom.Web.Services; using Phantom.Web.Services;
namespace Phantom.Web.Base; namespace Phantom.Web.Base;
@ -27,7 +28,24 @@ sealed class Navigation : INavigation {
return value != null; return value != null;
} }
public void NavigateTo(string url, bool forceLoad = false) { public async Task NavigateTo(string url, bool forceLoad = false) {
navigationManager.NavigateTo(BasePath + url, forceLoad); var newPath = BasePath + url;
var navigationTaskSource = new TaskCompletionSource();
navigationManager.LocationChanged += NavigationManagerOnLocationChanged;
try {
navigationManager.NavigateTo(newPath, forceLoad);
await navigationTaskSource.Task.WaitAsync(TimeSpan.FromSeconds(10));
} finally {
navigationManager.LocationChanged -= NavigationManagerOnLocationChanged;
}
return;
void NavigationManagerOnLocationChanged(object? sender, LocationChangedEventArgs e) {
if (Uri.TryCreate(e.Location, UriKind.Absolute, out var uri) && uri.AbsolutePath == newPath) {
navigationTaskSource.SetResult();
}
}
} }
} }

View File

@ -1,90 +1,90 @@
@* @page "/agents" *@ @page "/agents"
@* @using Phantom.Utils.Collections *@ @using Phantom.Utils.Collections
@* @implements IDisposable *@ @implements IDisposable
@* @inject AgentManager AgentManager *@ @inject AgentManager AgentManager
@* *@
@* <h1>Agents</h1> *@ <h1>Agents</h1>
@* *@
@* <table class="table align-middle"> *@ <table class="table align-middle">
@* <thead> *@ <thead>
@* <tr> *@ <tr>
@* <Column Width="200px; 44%">Name</Column> *@ <Column Width="200px; 44%">Name</Column>
@* <Column Width=" 90px; 19%" Class="text-end">Instances</Column> *@ <Column Width=" 90px; 19%" Class="text-end">Instances</Column>
@* <Column Width="145px; 21%" Class="text-end">Memory</Column> *@ <Column Width="145px; 21%" Class="text-end">Memory</Column>
@* <Column Width="180px; 8%">Version</Column> *@ <Column Width="180px; 8%">Version</Column>
@* <Column Width="320px">Identifier</Column> *@ <Column Width="320px">Identifier</Column>
@* <Column Width="100px; 8%" Class="text-center">Status</Column> *@ <Column Width="100px; 8%" Class="text-center">Status</Column>
@* <Column Width="215px" Class="text-end">Last Ping</Column> *@ <Column Width="215px" Class="text-end">Last Ping</Column>
@* </tr> *@ </tr>
@* </thead> *@ </thead>
@* @if (!agentTable.IsEmpty) { *@ @if (!agentTable.IsEmpty) {
@* <tbody> *@ <tbody>
@* @foreach (var agent in agentTable) { *@ @foreach (var agent in agentTable) {
@* var usedInstances = agent.Stats?.RunningInstanceCount; *@ var usedInstances = agent.Stats?.RunningInstanceCount;
@* var usedMemory = agent.Stats?.RunningInstanceMemory.InMegabytes; *@ var usedMemory = agent.Stats?.RunningInstanceMemory.InMegabytes;
@* *@
@* <tr> *@ <tr>
@* <td>@agent.Name</td> *@ <td>@agent.Name</td>
@* <td class="text-end"> *@ <td class="text-end">
@* <ProgressBar Value="@(usedInstances ?? 0)" Maximum="@agent.MaxInstances"> *@ <ProgressBar Value="@(usedInstances ?? 0)" Maximum="@agent.MaxInstances">
@* @(usedInstances?.ToString() ?? "?") / @agent.MaxInstances *@ @(usedInstances?.ToString() ?? "?") / @agent.MaxInstances
@* </ProgressBar> *@ </ProgressBar>
@* </td> *@ </td>
@* <td class="text-end"> *@ <td class="text-end">
@* <ProgressBar Value="@(usedMemory ?? 0)" Maximum="@agent.MaxMemory.InMegabytes"> *@ <ProgressBar Value="@(usedMemory ?? 0)" Maximum="@agent.MaxMemory.InMegabytes">
@* @(usedMemory?.ToString() ?? "?") / @agent.MaxMemory.InMegabytes MB *@ @(usedMemory?.ToString() ?? "?") / @agent.MaxMemory.InMegabytes MB
@* </ProgressBar> *@ </ProgressBar>
@* </td> *@ </td>
@* <td class="text-condensed"> *@ <td class="text-condensed">
@* Build: <code>@agent.BuildVersion</code> *@ Build: <code>@agent.BuildVersion</code>
@* <br> *@ <br>
@* Protocol: <code>v@(agent.ProtocolVersion)</code> *@ Protocol: <code>v@(agent.ProtocolVersion)</code>
@* </td> *@ </td>
@* <td> *@ <td>
@* <code class="text-uppercase">@agent.Guid.ToString()</code> *@ <code class="text-uppercase">@agent.Guid.ToString()</code>
@* </td> *@ </td>
@* @if (agent.IsOnline) { *@ @if (agent.IsOnline) {
@* <td class="text-center text-success">Online</td> *@ <td class="text-center text-success">Online</td>
@* <td class="text-end"></td> *@ <td class="text-end"></td>
@* } *@ }
@* else { *@ else {
@* <td class="text-center text-danger">Offline</td> *@ <td class="text-center text-danger">Offline</td>
@* @if (agent.LastPing is {} lastPing) { *@ @if (agent.LastPing is {} lastPing) {
@* <td class="text-end"> *@ <td class="text-end">
@* <time datetime="@lastPing.ToString("o")" data-time-type="relative">@lastPing.ToString()</time> *@ <time datetime="@lastPing.ToString("o")" data-time-type="relative">@lastPing.ToString()</time>
@* </td> *@ </td>
@* } *@ }
@* else { *@ else {
@* <td class="text-end">-</td> *@ <td class="text-end">-</td>
@* } *@ }
@* } *@ }
@* </tr> *@ </tr>
@* } *@ }
@* </tbody> *@ </tbody>
@* } *@ }
@* else { *@ else {
@* <tfoot> *@ <tfoot>
@* <tr> *@ <tr>
@* <td colspan="7">No agents registered.</td> *@ <td colspan="7">No agents registered.</td>
@* </tr> *@ </tr>
@* </tfoot> *@ </tfoot>
@* } *@ }
@* </table> *@ </table>
@* *@
@* @code { *@ @code {
@* *@
@* private readonly Table<Agent, Guid> agentTable = new(); *@ private readonly Table<Agent, Guid> agentTable = new();
@* *@
@* protected override void OnInitialized() { *@ protected override void OnInitialized() {
@* AgentManager.AgentsChanged.Subscribe(this, agents => { *@ AgentManager.AgentsChanged.Subscribe(this, agents => {
@* var sortedAgents = agents.Sort(static (a1, a2) => a1.Name.CompareTo(a2.Name)); *@ var sortedAgents = agents.Sort(static (a1, a2) => a1.Name.CompareTo(a2.Name));
@* agentTable.UpdateFrom(sortedAgents, static agent => agent.Guid, static agent => agent, static (agent, _) => agent); *@ agentTable.UpdateFrom(sortedAgents, static agent => agent.Guid, static agent => agent, static (agent, _) => agent);
@* InvokeAsync(StateHasChanged); *@ InvokeAsync(StateHasChanged);
@* }); *@ });
@* } *@ }
@* *@
@* void IDisposable.Dispose() { *@ void IDisposable.Dispose() {
@* AgentManager.AgentsChanged.Unsubscribe(this); *@ AgentManager.AgentsChanged.Unsubscribe(this);
@* } *@ }
@* *@
@* } *@ }

View File

@ -1,86 +1,86 @@
@* @page "/audit" *@ @page "/audit"
@* @attribute [Authorize(Permission.ViewAuditPolicy)] *@ @attribute [Authorize(Permission.ViewAuditPolicy)]
@* @using Phantom.Common.Data.Web.AuditLog *@ @using Phantom.Common.Data.Web.AuditLog
@* @using Phantom.Common.Data.Web.Users *@ @using Phantom.Common.Data.Web.Users
@* @using System.Collections.Immutable *@ @using System.Collections.Immutable
@* @using Phantom.Web.Services.Instances *@ @using Phantom.Web.Services.Instances
@* @implements IDisposable *@ @implements IDisposable
@* @inject AuditLog AuditLog *@ @inject AuditLog AuditLog
@* @inject InstanceManager InstanceManager *@ @inject InstanceManager InstanceManager
@* @inject UserManager UserManager *@ @inject UserManager UserManager
@* *@
@* <h1>Audit Log</h1> *@ <h1>Audit Log</h1>
@* *@
@* <table class="table"> *@ <table class="table">
@* <thead> *@ <thead>
@* <tr> *@ <tr>
@* <Column Width="165px" Class="text-end">Time</Column> *@ <Column Width="165px" Class="text-end">Time</Column>
@* <Column Width="320px; 20%">User</Column> *@ <Column Width="320px; 20%">User</Column>
@* <Column Width="160px">Event Type</Column> *@ <Column Width="160px">Event Type</Column>
@* <Column Width="320px; 20%">Subject</Column> *@ <Column Width="320px; 20%">Subject</Column>
@* <Column Width="100px; 60%">Data</Column> *@ <Column Width="100px; 60%">Data</Column>
@* </tr> *@ </tr>
@* </thead> *@ </thead>
@* <tbody> *@ <tbody>
@* @foreach (var logItem in logItems) { *@ @foreach (var logItem in logItems) {
@* DateTimeOffset time = logItem.UtcTime.ToLocalTime(); *@ DateTimeOffset time = logItem.UtcTime.ToLocalTime();
@* <tr> *@ <tr>
@* <td class="text-end"> *@ <td class="text-end">
@* <time datetime="@time.ToString("o")">@time.ToString()</time> *@ <time datetime="@time.ToString("o")">@time.ToString()</time>
@* </td> *@ </td>
@* <td> *@ <td>
@* @(logItem.UserName ?? "-") *@ @(logItem.UserName ?? "-")
@* <br> *@ <br>
@* <code class="text-uppercase">@logItem.UserGuid</code> *@ <code class="text-uppercase">@logItem.UserGuid</code>
@* </td> *@ </td>
@* <td>@logItem.EventType.ToNiceString()</td> *@ <td>@logItem.EventType.ToNiceString()</td>
@* <td> *@ <td>
@* @if (logItem.SubjectId is {} subjectId && GetSubjectName(logItem.SubjectType, subjectId) is {} subjectName) { *@ @if (logItem.SubjectId is {} subjectId && GetSubjectName(logItem.SubjectType, subjectId) is {} subjectName) {
@* @subjectName *@ @subjectName
@* <br> *@ <br>
@* } *@ }
@* <code class="text-uppercase">@(logItem.SubjectId ?? "-")</code> *@ <code class="text-uppercase">@(logItem.SubjectId ?? "-")</code>
@* </td> *@ </td>
@* <td> *@ <td>
@* <code>@logItem.Data?.RootElement.ToString()</code> *@ <code>@logItem.Data?.RootElement.ToString()</code>
@* </td> *@ </td>
@* </tr> *@ </tr>
@* } *@ }
@* </tbody> *@ </tbody>
@* </table> *@ </table>
@* *@
@* @code { *@ @code {
@* *@
@* private CancellationTokenSource? initializationCancellationTokenSource; *@ private CancellationTokenSource? initializationCancellationTokenSource;
@* private AuditLogItem[] logItems = Array.Empty<AuditLogItem>(); *@ private AuditLogItem[] logItems = Array.Empty<AuditLogItem>();
@* private Dictionary<Guid, string>? userNamesByGuid; *@ private Dictionary<Guid, string>? userNamesByGuid;
@* private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty; *@ private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
@* *@
@* protected override async Task OnInitializedAsync() { *@ protected override async Task OnInitializedAsync() {
@* initializationCancellationTokenSource = new CancellationTokenSource(); *@ initializationCancellationTokenSource = new CancellationTokenSource();
@* var cancellationToken = initializationCancellationTokenSource.Token; *@ var cancellationToken = initializationCancellationTokenSource.Token;
@* *@
@* try { *@ try {
@* logItems = await AuditLog.GetItems(50, cancellationToken); *@ logItems = await AuditLog.GetItems(50, cancellationToken);
@* userNamesByGuid = await UserManager.GetAllByGuid(static user => user.Name, cancellationToken); *@ userNamesByGuid = await UserManager.GetAllByGuid(static user => user.Name, cancellationToken);
@* instanceNamesByGuid = InstanceManager.GetInstanceNames(); *@ instanceNamesByGuid = InstanceManager.GetInstanceNames();
@* } finally { *@ } finally {
@* initializationCancellationTokenSource.Dispose(); *@ initializationCancellationTokenSource.Dispose();
@* } *@ }
@* } *@ }
@* *@
@* private string? GetSubjectName(AuditLogSubjectType type, string id) { *@ private string? GetSubjectName(AuditLogSubjectType type, string id) {
@* return type switch { *@ return type switch {
@* AuditLogSubjectType.Instance => instanceNamesByGuid.TryGetValue(Guid.Parse(id), out var name) ? name : null, *@ AuditLogSubjectType.Instance => instanceNamesByGuid.TryGetValue(Guid.Parse(id), out var name) ? name : null,
@* AuditLogSubjectType.User => userNamesByGuid != null && userNamesByGuid.TryGetValue(Guid.Parse(id), out var name) ? name : null, *@ AuditLogSubjectType.User => userNamesByGuid != null && userNamesByGuid.TryGetValue(Guid.Parse(id), out var name) ? name : null,
@* _ => null *@ _ => null
@* }; *@ };
@* } *@ }
@* *@
@* public void Dispose() { *@ public void Dispose() {
@* try { *@ try {
@* initializationCancellationTokenSource?.Cancel(); *@ initializationCancellationTokenSource?.Cancel();
@* } catch (ObjectDisposedException) {} *@ } catch (ObjectDisposedException) {}
@* } *@ }
@* *@
@* } *@ }

View File

@ -1,95 +1,95 @@
@* @page "/events" *@ @page "/events"
@* @attribute [Authorize(Permission.ViewEventsPolicy)] *@ @attribute [Authorize(Permission.ViewEventsPolicy)]
@* @using Phantom.Common.Data.Web.EventLog *@ @using Phantom.Common.Data.Web.EventLog
@* @using Phantom.Common.Data.Web.Users *@ @using Phantom.Common.Data.Web.Users
@* @using System.Collections.Immutable *@ @using System.Collections.Immutable
@* @using System.Diagnostics *@ @using System.Diagnostics
@* @using Phantom.Web.Services.Instances *@ @using Phantom.Web.Services.Instances
@* @implements IDisposable *@ @implements IDisposable
@* @inject AgentManager AgentManager *@ @inject AgentManager AgentManager
@* @inject EventLog EventLog *@ @inject EventLog EventLog
@* @inject InstanceManager InstanceManager *@ @inject InstanceManager InstanceManager
@* *@
@* <h1>Event Log</h1> *@ <h1>Event Log</h1>
@* *@
@* <table class="table"> *@ <table class="table">
@* <thead> *@ <thead>
@* <tr> *@ <tr>
@* <Column Width="165px" Class="text-end">Time</Column> *@ <Column Width="165px" Class="text-end">Time</Column>
@* <Column Width="320px; 20%">Agent</Column> *@ <Column Width="320px; 20%">Agent</Column>
@* <Column Width="160px">Event Type</Column> *@ <Column Width="160px">Event Type</Column>
@* <Column Width="320px; 20%">Subject</Column> *@ <Column Width="320px; 20%">Subject</Column>
@* <Column Width="100px; 60%">Data</Column> *@ <Column Width="100px; 60%">Data</Column>
@* </tr> *@ </tr>
@* </thead> *@ </thead>
@* <tbody> *@ <tbody>
@* @foreach (var logItem in logItems) { *@ @foreach (var logItem in logItems) {
@* DateTimeOffset time = logItem.UtcTime.ToLocalTime(); *@ DateTimeOffset time = logItem.UtcTime.ToLocalTime();
@* <tr> *@ <tr>
@* <td class="text-end"> *@ <td class="text-end">
@* <time datetime="@time.ToString("o")">@time.ToString()</time> *@ <time datetime="@time.ToString("o")">@time.ToString()</time>
@* </td> *@ </td>
@* <td> *@ <td>
@* @if (logItem.AgentGuid is {} agentGuid) { *@ @if (logItem.AgentGuid is {} agentGuid) {
@* @(GetAgentName(agentGuid)) *@ @(GetAgentName(agentGuid))
@* <br> *@ <br>
@* <code class="text-uppercase">@agentGuid</code> *@ <code class="text-uppercase">@agentGuid</code>
@* } *@ }
@* else { *@ else {
@* <text>-</text> *@ <text>-</text>
@* } *@ }
@* </td> *@ </td>
@* <td>@logItem.EventType.ToNiceString()</td> *@ <td>@logItem.EventType.ToNiceString()</td>
@* <td> *@ <td>
@* @if (GetSubjectName(logItem.SubjectType, logItem.SubjectId) is {} subjectName) { *@ @if (GetSubjectName(logItem.SubjectType, logItem.SubjectId) is {} subjectName) {
@* @subjectName *@ @subjectName
@* <br> *@ <br>
@* } *@ }
@* <code class="text-uppercase">@logItem.SubjectId</code> *@ <code class="text-uppercase">@logItem.SubjectId</code>
@* </td> *@ </td>
@* <td> *@ <td>
@* <code>@logItem.Data?.RootElement.ToString()</code> *@ <code>@logItem.Data?.RootElement.ToString()</code>
@* </td> *@ </td>
@* </tr> *@ </tr>
@* } *@ }
@* </tbody> *@ </tbody>
@* </table> *@ </table>
@* *@
@* @code { *@ @code {
@* *@
@* private CancellationTokenSource? initializationCancellationTokenSource; *@ private CancellationTokenSource? initializationCancellationTokenSource;
@* private EventLogItem[] logItems = Array.Empty<EventLogItem>(); *@ private EventLogItem[] logItems = Array.Empty<EventLogItem>();
@* private ImmutableDictionary<Guid, string> agentNamesByGuid = ImmutableDictionary<Guid, string>.Empty; *@ private ImmutableDictionary<Guid, string> agentNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
@* private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty; *@ private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
@* *@
@* protected override async Task OnInitializedAsync() { *@ protected override async Task OnInitializedAsync() {
@* initializationCancellationTokenSource = new CancellationTokenSource(); *@ initializationCancellationTokenSource = new CancellationTokenSource();
@* var cancellationToken = initializationCancellationTokenSource.Token; *@ var cancellationToken = initializationCancellationTokenSource.Token;
@* *@
@* try { *@ try {
@* logItems = await EventLog.GetItems(50, cancellationToken); *@ logItems = await EventLog.GetItems(50, cancellationToken);
@* agentNamesByGuid = AgentManager.GetAgents().ToImmutableDictionary(static kvp => kvp.Key, static kvp => kvp.Value.Name); *@ agentNamesByGuid = AgentManager.GetAgents().ToImmutableDictionary(static kvp => kvp.Key, static kvp => kvp.Value.Name);
@* instanceNamesByGuid = InstanceManager.GetInstanceNames(); *@ instanceNamesByGuid = InstanceManager.GetInstanceNames();
@* } finally { *@ } finally {
@* initializationCancellationTokenSource.Dispose(); *@ initializationCancellationTokenSource.Dispose();
@* } *@ }
@* } *@ }
@* *@
@* private string GetAgentName(Guid agentGuid) { *@ private string GetAgentName(Guid agentGuid) {
@* return agentNamesByGuid.TryGetValue(agentGuid, out var name) ? name : "?"; *@ return agentNamesByGuid.TryGetValue(agentGuid, out var name) ? name : "?";
@* } *@ }
@* *@
@* private string? GetSubjectName(EventLogSubjectType type, string id) { *@ private string? GetSubjectName(EventLogSubjectType type, string id) {
@* return type switch { *@ return type switch {
@* EventLogSubjectType.Instance => instanceNamesByGuid.TryGetValue(Guid.Parse(id), out var name) ? name : null, *@ EventLogSubjectType.Instance => instanceNamesByGuid.TryGetValue(Guid.Parse(id), out var name) ? name : null,
@* _ => null *@ _ => null
@* }; *@ };
@* } *@ }
@* *@
@* public void Dispose() { *@ public void Dispose() {
@* try { *@ try {
@* initializationCancellationTokenSource?.Cancel(); *@ initializationCancellationTokenSource?.Cancel();
@* } catch (ObjectDisposedException) {} *@ } catch (ObjectDisposedException) {}
@* } *@ }
@* *@
@* } *@ }

View File

@ -1,91 +1,91 @@
@* @page "/instances/{InstanceGuid:guid}" *@ @page "/instances/{InstanceGuid:guid}"
@* @attribute [Authorize(Permission.ViewInstancesPolicy)] *@ @attribute [Authorize(Permission.ViewInstancesPolicy)]
@* @inherits PhantomComponent *@ @inherits PhantomComponent
@* @using Phantom.Web.Services.Instances *@ @using Phantom.Web.Services.Instances
@* @using Phantom.Common.Data.Web.Users *@ @using Phantom.Common.Data.Web.Users
@* @using Phantom.Common.Data.Replies *@ @using Phantom.Common.Data.Replies
@* @implements IDisposable *@ @implements IDisposable
@* @inject InstanceManager InstanceManager *@ @inject InstanceManager InstanceManager
@* @inject AuditLog AuditLog *@ @inject AuditLog AuditLog
@* *@
@* @if (Instance == null) { *@ @if (Instance == null) {
@* <h1>Instance Not Found</h1> *@ <h1>Instance Not Found</h1>
@* <p>Return to <a href="instances">all instances</a>.</p> *@ <p>Return to <a href="instances">all instances</a>.</p>
@* } *@ }
@* else { *@ else {
@* <h1>Instance: @Instance.Configuration.InstanceName</h1> *@ <h1>Instance: @Instance.Configuration.InstanceName</h1>
@* <div class="d-flex flex-row align-items-center gap-2"> *@ <div class="d-flex flex-row align-items-center gap-2">
@* <PermissionView Permission="Permission.ControlInstances"> *@ <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-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> *@ <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> *@ <span><!-- extra spacing --></span>
@* </PermissionView> *@ </PermissionView>
@* <InstanceStatusText Status="Instance.Status" /> *@ <InstanceStatusText Status="Instance.Status" />
@* <PermissionView Permission="Permission.CreateInstances"> *@ <PermissionView Permission="Permission.CreateInstances">
@* <a href="instances/@InstanceGuid/edit" class="btn btn-warning ms-auto">Edit Configuration</a> *@ <a href="instances/@InstanceGuid/edit" class="btn btn-warning ms-auto">Edit Configuration</a>
@* </PermissionView> *@ </PermissionView>
@* </div> *@ </div>
@* @if (lastError != null) { *@ @if (lastError != null) {
@* <p class="text-danger mt-2">@lastError</p> *@ <p class="text-danger mt-2">@lastError</p>
@* } *@ }
@* *@
@* <PermissionView Permission="Permission.ViewInstanceLogs"> *@ <PermissionView Permission="Permission.ViewInstanceLogs">
@* <InstanceLog InstanceGuid="InstanceGuid" /> *@ <InstanceLog InstanceGuid="InstanceGuid" />
@* </PermissionView> *@ </PermissionView>
@* *@
@* <PermissionView Permission="Permission.ControlInstances"> *@ <PermissionView Permission="Permission.ControlInstances">
@* <div class="mb-3"> *@ <div class="mb-3">
@* <InstanceCommandInput InstanceGuid="InstanceGuid" Disabled="@(!Instance.Status.CanSendCommand())" /> *@ <InstanceCommandInput InstanceGuid="InstanceGuid" Disabled="@(!Instance.Status.CanSendCommand())" />
@* </div> *@ </div>
@* *@
@* <InstanceStopDialog InstanceGuid="InstanceGuid" ModalId="stop-instance" Disabled="@(!Instance.Status.CanStop())" /> *@ <InstanceStopDialog InstanceGuid="InstanceGuid" ModalId="stop-instance" Disabled="@(!Instance.Status.CanStop())" />
@* </PermissionView> *@ </PermissionView>
@* } *@ }
@* *@
@* @code { *@ @code {
@* *@
@* [Parameter] *@ [Parameter]
@* public Guid InstanceGuid { get; set; } *@ public Guid InstanceGuid { get; set; }
@* *@
@* private string? lastError = null; *@ private string? lastError = null;
@* private bool isLaunchingInstance = false; *@ private bool isLaunchingInstance = false;
@* *@
@* private Instance? Instance { get; set; } *@ private Instance? Instance { get; set; }
@* *@
@* protected override void OnInitialized() { *@ protected override void OnInitialized() {
@* InstanceManager.InstancesChanged.Subscribe(this, instances => { *@ InstanceManager.InstancesChanged.Subscribe(this, instances => {
@* var newInstance = instances.TryGetValue(InstanceGuid, out var instance) ? instance : null; *@ var newInstance = instances.TryGetValue(InstanceGuid, out var instance) ? instance : null;
@* if (newInstance != Instance) { *@ if (newInstance != Instance) {
@* Instance = newInstance; *@ Instance = newInstance;
@* InvokeAsync(StateHasChanged); *@ InvokeAsync(StateHasChanged);
@* } *@ }
@* }); *@ });
@* } *@ }
@* *@
@* private async Task LaunchInstance() { *@ private async Task LaunchInstance() {
@* isLaunchingInstance = true; *@ isLaunchingInstance = true;
@* lastError = null; *@ lastError = null;
@* *@
@* try { *@ try {
@* if (!await CheckPermission(Permission.ControlInstances)) { *@ if (!await CheckPermission(Permission.ControlInstances)) {
@* lastError = "You do not have permission to launch instances."; *@ lastError = "You do not have permission to launch instances.";
@* return; *@ return;
@* } *@ }
@* *@
@* var result = await InstanceManager.LaunchInstance(InstanceGuid); *@ var result = await InstanceManager.LaunchInstance(InstanceGuid);
@* if (result.Is(LaunchInstanceResult.LaunchInitiated)) { *@ if (result.Is(LaunchInstanceResult.LaunchInitiated)) {
@* await AuditLog.AddInstanceLaunchedEvent(InstanceGuid); *@ await AuditLog.AddInstanceLaunchedEvent(InstanceGuid);
@* } *@ }
@* else { *@ else {
@* lastError = result.ToSentence(Messages.ToSentence); *@ lastError = result.ToSentence(Messages.ToSentence);
@* } *@ }
@* } finally { *@ } finally {
@* isLaunchingInstance = false; *@ isLaunchingInstance = false;
@* } *@ }
@* } *@ }
@* *@
@* public void Dispose() { *@ public void Dispose() {
@* InstanceManager.InstancesChanged.Unsubscribe(this); *@ InstanceManager.InstancesChanged.Unsubscribe(this);
@* } *@ }
@* *@
@* } *@ }

View File

@ -1,29 +1,29 @@
@* @page "/instances/{InstanceGuid:guid}/edit" *@ @page "/instances/{InstanceGuid:guid}/edit"
@* @attribute [Authorize(Permission.CreateInstancesPolicy)] *@ @attribute [Authorize(Permission.CreateInstancesPolicy)]
@* @using Phantom.Common.Data.Instance *@ @using Phantom.Common.Data.Instance
@* @using Phantom.Common.Data.Web.Users *@ @using Phantom.Common.Data.Web.Users
@* @using Phantom.Web.Services.Instances *@ @using Phantom.Web.Services.Instances
@* @inherits PhantomComponent *@ @inherits PhantomComponent
@* @inject InstanceManager InstanceManager *@ @inject InstanceManager InstanceManager
@* *@
@* @if (InstanceConfiguration == null) { *@ @if (InstanceConfiguration == null) {
@* <h1>Instance Not Found</h1> *@ <h1>Instance Not Found</h1>
@* <p>Return to <a href="instances">all instances</a>.</p> *@ <p>Return to <a href="instances">all instances</a>.</p>
@* } *@ }
@* else { *@ else {
@* <h1>Edit Instance: @InstanceConfiguration.InstanceName</h1> *@ <h1>Edit Instance: @InstanceConfiguration.InstanceName</h1>
@* <InstanceAddOrEditForm EditedInstanceConfiguration="InstanceConfiguration" /> *@ <InstanceAddOrEditForm EditedInstanceConfiguration="InstanceConfiguration" />
@* } *@ }
@* *@
@* @code { *@ @code {
@* *@
@* [Parameter] *@ [Parameter]
@* public Guid InstanceGuid { get; set; } *@ public Guid InstanceGuid { get; set; }
@* *@
@* private InstanceConfiguration? InstanceConfiguration { get; set; } *@ private InstanceConfiguration? InstanceConfiguration { get; set; }
@* *@
@* protected override void OnInitialized() { *@ protected override void OnInitialized() {
@* InstanceConfiguration = InstanceManager.GetInstanceConfiguration(InstanceGuid); *@ InstanceConfiguration = InstanceManager.GetInstanceConfiguration(InstanceGuid);
@* } *@ }
@* *@
@* } *@ }

View File

@ -1,92 +1,92 @@
@* @page "/instances" *@ @page "/instances"
@* @attribute [Authorize(Permission.ViewInstancesPolicy)] *@ @attribute [Authorize(Permission.ViewInstancesPolicy)]
@* @using System.Collections.Immutable *@ @using System.Collections.Immutable
@* @implements IDisposable *@ @implements IDisposable
@* @inject AgentManager AgentManager *@ @inject AgentManager AgentManager
@* @inject InstanceManager InstanceManager *@ @inject InstanceManager InstanceManager
@* *@
@* <h1>Instances</h1> *@ <h1>Instances</h1>
@* *@
@* <PermissionView Permission="Permission.CreateInstances"> *@ <PermissionView Permission="Permission.CreateInstances">
@* <a href="instances/create" class="btn btn-primary" role="button">New Instance</a> *@ <a href="instances/create" class="btn btn-primary" role="button">New Instance</a>
@* </PermissionView> *@ </PermissionView>
@* *@
@* <table class="table align-middle"> *@ <table class="table align-middle">
@* <thead> *@ <thead>
@* <tr> *@ <tr>
@* <Column Width="200px; 28%">Agent</Column> *@ <Column Width="200px; 28%">Agent</Column>
@* <Column Width="200px; 28%">Name</Column> *@ <Column Width="200px; 28%">Name</Column>
@* <Column Width="130px; 11%">Version</Column> *@ <Column Width="130px; 11%">Version</Column>
@* <Column Width="110px; 8%" Class="text-center">Server Port</Column> *@ <Column Width="110px; 8%" Class="text-center">Server Port</Column>
@* <Column Width="110px; 8%" Class="text-center">Rcon Port</Column> *@ <Column Width="110px; 8%" Class="text-center">Rcon Port</Column>
@* <Column Width=" 90px; 8%" Class="text-end">Memory</Column> *@ <Column Width=" 90px; 8%" Class="text-end">Memory</Column>
@* <Column Width="320px">Identifier</Column> *@ <Column Width="320px">Identifier</Column>
@* <Column Width="200px; 9%">Status</Column> *@ <Column Width="200px; 9%">Status</Column>
@* <Column Width=" 75px">Actions</Column> *@ <Column Width=" 75px">Actions</Column>
@* </tr> *@ </tr>
@* </thead> *@ </thead>
@* @if (!instances.IsEmpty) { *@ @if (!instances.IsEmpty) {
@* <tbody> *@ <tbody>
@* @foreach (var (configuration, status, _) in instances) { *@ @foreach (var (configuration, status, _) in instances) {
@* var agentName = agentNames.TryGetValue(configuration.AgentGuid, out var name) ? name : string.Empty; *@ var agentName = agentNames.TryGetValue(configuration.AgentGuid, out var name) ? name : string.Empty;
@* var instanceGuid = configuration.InstanceGuid.ToString(); *@ var instanceGuid = configuration.InstanceGuid.ToString();
@* <tr> *@ <tr>
@* <td>@agentName</td> *@ <td>@agentName</td>
@* <td>@configuration.InstanceName</td> *@ <td>@configuration.InstanceName</td>
@* <td>@configuration.MinecraftServerKind @configuration.MinecraftVersion</td> *@ <td>@configuration.MinecraftServerKind @configuration.MinecraftVersion</td>
@* <td class="text-center"> *@ <td class="text-center">
@* <code>@configuration.ServerPort</code> *@ <code>@configuration.ServerPort</code>
@* </td> *@ </td>
@* <td class="text-center"> *@ <td class="text-center">
@* <code>@configuration.RconPort</code> *@ <code>@configuration.RconPort</code>
@* </td> *@ </td>
@* <td class="text-end"> *@ <td class="text-end">
@* <code>@configuration.MemoryAllocation.InMegabytes MB</code> *@ <code>@configuration.MemoryAllocation.InMegabytes MB</code>
@* </td> *@ </td>
@* <td> *@ <td>
@* <code class="text-uppercase">@instanceGuid</code> *@ <code class="text-uppercase">@instanceGuid</code>
@* </td> *@ </td>
@* <td> *@ <td>
@* <InstanceStatusText Status="status" /> *@ <InstanceStatusText Status="status" />
@* </td> *@ </td>
@* <td> *@ <td>
@* <a href="instances/@instanceGuid" class="btn btn-info btn-sm">Detail</a> *@ <a href="instances/@instanceGuid" class="btn btn-info btn-sm">Detail</a>
@* </td> *@ </td>
@* </tr> *@ </tr>
@* } *@ }
@* </tbody> *@ </tbody>
@* } *@ }
@* @if (instances.IsEmpty) { *@ @if (instances.IsEmpty) {
@* <tfoot> *@ <tfoot>
@* <tr> *@ <tr>
@* <td colspan="9"> *@ <td colspan="9">
@* No instances. *@ No instances.
@* </td> *@ </td>
@* </tr> *@ </tr>
@* </tfoot> *@ </tfoot>
@* } *@ }
@* </table> *@ </table>
@* *@
@* @code { *@ @code {
@* *@
@* private ImmutableDictionary<Guid, string> agentNames = ImmutableDictionary<Guid, string>.Empty; *@ private ImmutableDictionary<Guid, string> agentNames = ImmutableDictionary<Guid, string>.Empty;
@* private ImmutableArray<Instance> instances = ImmutableArray<Instance>.Empty; *@ private ImmutableArray<Instance> instances = ImmutableArray<Instance>.Empty;
@* *@
@* protected override void OnInitialized() { *@ protected override void OnInitialized() {
@* AgentManager.AgentsChanged.Subscribe(this, agents => { *@ AgentManager.AgentsChanged.Subscribe(this, agents => {
@* this.agentNames = agents.ToImmutableDictionary(static agent => agent.Guid, static agent => agent.Name); *@ this.agentNames = agents.ToImmutableDictionary(static agent => agent.Guid, static agent => agent.Name);
@* InvokeAsync(StateHasChanged); *@ InvokeAsync(StateHasChanged);
@* }); *@ });
@* *@
@* InstanceManager.InstancesChanged.Subscribe(this, instances => { *@ InstanceManager.InstancesChanged.Subscribe(this, instances => {
@* this.instances = instances.Values.OrderBy(instance => agentNames.TryGetValue(instance.Configuration.AgentGuid, out var agentName) ? agentName : string.Empty).ThenBy(static instance => instance.Configuration.InstanceName).ToImmutableArray(); *@ this.instances = instances.Values.OrderBy(instance => agentNames.TryGetValue(instance.Configuration.AgentGuid, out var agentName) ? agentName : string.Empty).ThenBy(static instance => instance.Configuration.InstanceName).ToImmutableArray();
@* InvokeAsync(StateHasChanged); *@ InvokeAsync(StateHasChanged);
@* }); *@ });
@* } *@ }
@* *@
@* void IDisposable.Dispose() { *@ void IDisposable.Dispose() {
@* AgentManager.AgentsChanged.Unsubscribe(this); *@ AgentManager.AgentsChanged.Unsubscribe(this);
@* InstanceManager.InstancesChanged.Unsubscribe(this); *@ InstanceManager.InstancesChanged.Unsubscribe(this);
@* } *@ }
@* *@
@* } *@ }

View File

@ -1,12 +1,10 @@
@page "/login" @page "/login"
@using Phantom.Web.Services @using Phantom.Web.Services
@using Phantom.Web.Services.Authentication @using Phantom.Web.Services.Authentication
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@using System.ComponentModel.DataAnnotations @using System.ComponentModel.DataAnnotations
@attribute [AllowAnonymous] @attribute [AllowAnonymous]
@inject INavigation Navigation @inject INavigation Navigation
@inject PhantomLoginManager LoginManager @inject PhantomLoginManager LoginManager
@inject ProtectedSessionStorage ProtectedSessionStore
<h1>Login</h1> <h1>Login</h1>

View File

@ -0,0 +1,11 @@
@page "/logout"
@using Phantom.Web.Services.Authentication
@inject PhantomLoginManager LoginManager
@code {
protected override Task OnInitializedAsync() {
return LoginManager.SignOut();
}
}

View File

@ -11,7 +11,7 @@
@attribute [AllowAnonymous] @attribute [AllowAnonymous]
@inject ServiceConfiguration ServiceConfiguration @inject ServiceConfiguration ServiceConfiguration
@inject PhantomLoginManager LoginManager @inject PhantomLoginManager LoginManager
@inject ControllerCommunication ControllerCommunication @inject ControllerConnection ControllerConnection
<h1>Administrator Setup</h1> <h1>Administrator Setup</h1>
@ -87,7 +87,7 @@
} }
private async Task<Result<string>> CreateOrUpdateAdministrator() { private async Task<Result<string>> CreateOrUpdateAdministrator() {
var reply = await ControllerCommunication.Send<CreateOrUpdateAdministratorUser, CreateOrUpdateAdministratorUserResult>(new CreateOrUpdateAdministratorUser(form.Username, form.Password), Timeout.InfiniteTimeSpan); var reply = await ControllerConnection.Send<CreateOrUpdateAdministratorUser, CreateOrUpdateAdministratorUserResult>(new CreateOrUpdateAdministratorUser(form.Username, form.Password), Timeout.InfiniteTimeSpan);
return reply switch { return reply switch {
Success => Result.Ok<string>(), Success => Result.Ok<string>(),
CreationFailed fail => fail.Error.ToSentences("\n"), CreationFailed fail => fail.Error.ToSentences("\n"),

View File

@ -1,110 +1,110 @@
@* @page "/users" *@ @page "/users"
@* @using Phantom.Common.Data.Web.Users *@ @using Phantom.Common.Data.Web.Users
@* @using System.Collections.Immutable *@ @using System.Collections.Immutable
@* @using Phantom.Web.Services.Authorization *@ @using Phantom.Web.Services.Authorization
@* @attribute [Authorize(Permission.ViewUsersPolicy)] *@ @attribute [Authorize(Permission.ViewUsersPolicy)]
@* @inject UserManager UserManager *@ @inject UserManager UserManager
@* @inject UserRoleManager UserRoleManager *@ @inject UserRoleManager UserRoleManager
@* @inject PermissionManager PermissionManager *@ @inject PermissionManager PermissionManager
@* *@
@* <h1>Users</h1> *@ <h1>Users</h1>
@* *@
@* <PermissionView Permission="Permission.EditUsers"> *@ <PermissionView Permission="Permission.EditUsers">
@* <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-user">Add User...</button> *@ <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-user">Add User...</button>
@* </PermissionView> *@ </PermissionView>
@* *@
@* <AuthorizeView> *@ <AuthorizeView>
@* <Authorized> *@ <Authorized>
@* @{ var canEdit = PermissionManager.CheckPermission(context.User, Permission.EditUsers); } *@ @{ var canEdit = PermissionManager.CheckPermission(context.User, Permission.EditUsers); }
@* <table class="table align-middle"> *@ <table class="table align-middle">
@* <thead> *@ <thead>
@* <tr> *@ <tr>
@* <Column Width="320px">Identifier</Column> *@ <Column Width="320px">Identifier</Column>
@* <Column Width="125px; 40%">Username</Column> *@ <Column Width="125px; 40%">Username</Column>
@* <Column Width="125px; 60%">Roles</Column> *@ <Column Width="125px; 60%">Roles</Column>
@* @if (canEdit) { *@ @if (canEdit) {
@* <Column Width="175px">Actions</Column> *@ <Column Width="175px">Actions</Column>
@* } *@ }
@* </tr> *@ </tr>
@* </thead> *@ </thead>
@* <tbody> *@ <tbody>
@* @{ var myUserId = UserManager.GetAuthenticatedUserId(context.User); } *@ @{ var myUserId = UserManager.GetAuthenticatedUserId(context.User); }
@* @foreach (var user in allUsers) { *@ @foreach (var user in allUsers) {
@* var isMe = myUserId == user.Guid; *@ var isMe = myUserId == user.Guid;
@* <tr> *@ <tr>
@* <td> *@ <td>
@* <code class="text-uppercase">@user.Guid</code> *@ <code class="text-uppercase">@user.Guid</code>
@* </td> *@ </td>
@* @if (isMe) { *@ @if (isMe) {
@* <td class="fw-semibold">@user.Name</td> *@ <td class="fw-semibold">@user.Name</td>
@* } *@ }
@* else { *@ else {
@* <td>@user.Name</td> *@ <td>@user.Name</td>
@* } *@ }
@* <td>@(userGuidToRoleDescription.TryGetValue(user.Guid, out var roles) ? roles : "?")</td> *@ <td>@(userGuidToRoleDescription.TryGetValue(user.Guid, out var roles) ? roles : "?")</td>
@* @if (canEdit) { *@ @if (canEdit) {
@* <td> *@ <td>
@* @if (!isMe) { *@ @if (!isMe) {
@* <button class="btn btn-primary btn-sm" @onclick="() => userRolesDialog.Show(user)">Edit Roles</button> *@ <button class="btn btn-primary btn-sm" @onclick="() => userRolesDialog.Show(user)">Edit Roles</button>
@* <button class="btn btn-danger btn-sm" @onclick="() => userDeleteDialog.Show(user)">Delete...</button> *@ <button class="btn btn-danger btn-sm" @onclick="() => userDeleteDialog.Show(user)">Delete...</button>
@* } *@ }
@* </td> *@ </td>
@* } *@ }
@* </tr> *@ </tr>
@* } *@ }
@* </tbody> *@ </tbody>
@* </table> *@ </table>
@* </Authorized> *@ </Authorized>
@* </AuthorizeView> *@ </AuthorizeView>
@* *@
@* <PermissionView Permission="Permission.EditUsers"> *@ <PermissionView Permission="Permission.EditUsers">
@* <UserAddDialog ModalId="add-user" UserAdded="OnUserAdded" /> *@ <UserAddDialog ModalId="add-user" UserAdded="OnUserAdded" />
@* <UserRolesDialog @ref="userRolesDialog" ModalId="manage-user-roles" UserModified="OnUserRolesChanged" /> *@ <UserRolesDialog @ref="userRolesDialog" ModalId="manage-user-roles" UserModified="OnUserRolesChanged" />
@* <UserDeleteDialog @ref="userDeleteDialog" ModalId="delete-user" UserModified="OnUserDeleted" /> *@ <UserDeleteDialog @ref="userDeleteDialog" ModalId="delete-user" UserModified="OnUserDeleted" />
@* </PermissionView> *@ </PermissionView>
@* *@
@* @code { *@ @code {
@* *@
@* private ImmutableArray<UserInfo> allUsers = ImmutableArray<UserInfo>.Empty; *@ private ImmutableArray<UserInfo> allUsers = ImmutableArray<UserInfo>.Empty;
@* private readonly Dictionary<Guid, string> userGuidToRoleDescription = new(); *@ private readonly Dictionary<Guid, string> userGuidToRoleDescription = new();
@* *@
@* private UserRolesDialog userRolesDialog = null!; *@ private UserRolesDialog userRolesDialog = null!;
@* private UserDeleteDialog userDeleteDialog = null!; *@ private UserDeleteDialog userDeleteDialog = null!;
@* *@
@* protected override async Task OnInitializedAsync() { *@ protected override async Task OnInitializedAsync() {
@* var unsortedUsers = await UserManager.GetAll(); *@ var unsortedUsers = await UserManager.GetAll();
@* allUsers = unsortedUsers.Sort(static (a, b) => a.Name.CompareTo(b.Name)); *@ allUsers = unsortedUsers.Sort(static (a, b) => a.Name.CompareTo(b.Name));
@* *@
@* foreach (var (userGuid, roles) in await UserRoleManager.GetAllByUserGuid()) { *@ foreach (var (userGuid, roles) in await UserRoleManager.GetAllByUserGuid()) {
@* userGuidToRoleDescription[userGuid] = StringifyRoles(roles); *@ userGuidToRoleDescription[userGuid] = StringifyRoles(roles);
@* } *@ }
@* *@
@* foreach (var user in allUsers) { *@ foreach (var user in allUsers) {
@* await RefreshUserRoles(user); *@ await RefreshUserRoles(user);
@* } *@ }
@* } *@ }
@* *@
@* private async Task RefreshUserRoles(UserInfo user) { *@ private async Task RefreshUserRoles(UserInfo user) {
@* var roles = await UserRoleManager.GetUserRoles(user); *@ var roles = await UserRoleManager.GetUserRoles(user);
@* userGuidToRoleDescription[user.Guid] = StringifyRoles(roles); *@ userGuidToRoleDescription[user.Guid] = StringifyRoles(roles);
@* } *@ }
@* *@
@* private static string StringifyRoles(ImmutableArray<RoleInfo> roles) { *@ private static string StringifyRoles(ImmutableArray<RoleInfo> roles) {
@* return roles.IsEmpty ? "-" : string.Join(", ", roles.Select(static role => role.Name)); *@ return roles.IsEmpty ? "-" : string.Join(", ", roles.Select(static role => role.Name));
@* } *@ }
@* *@
@* private Task OnUserAdded(UserInfo user) { *@ private Task OnUserAdded(UserInfo user) {
@* allUsers = allUsers.Add(user); *@ allUsers = allUsers.Add(user);
@* return RefreshUserRoles(user); *@ return RefreshUserRoles(user);
@* } *@ }
@* *@
@* private Task OnUserRolesChanged(UserInfo user) { *@ private Task OnUserRolesChanged(UserInfo user) {
@* return RefreshUserRoles(user); *@ return RefreshUserRoles(user);
@* } *@ }
@* *@
@* private void OnUserDeleted(UserInfo user) { *@ private void OnUserDeleted(UserInfo user) {
@* allUsers = allUsers.Remove(user); *@ allUsers = allUsers.Remove(user);
@* userGuidToRoleDescription.Remove(user.Guid); *@ userGuidToRoleDescription.Remove(user.Guid);
@* } *@ }
@* *@
@* } *@ }

View File

@ -1,338 +1,338 @@
@* @using Phantom.Web.Components.Utils *@ @using Phantom.Web.Components.Utils
@* @using Phantom.Common.Data.Minecraft *@ @using Phantom.Common.Data.Minecraft
@* @using Phantom.Common.Data.Web.Minecraft *@ @using Phantom.Common.Data.Web.Minecraft
@* @using Phantom.Common.Data.Instance *@ @using Phantom.Common.Data.Instance
@* @using Phantom.Common.Data.Java *@ @using Phantom.Common.Data.Java
@* @using System.Collections.Immutable *@ @using System.Collections.Immutable
@* @using System.ComponentModel.DataAnnotations *@ @using System.ComponentModel.DataAnnotations
@* @using System.Diagnostics.CodeAnalysis *@ @using System.Diagnostics.CodeAnalysis
@* @using Phantom.Common.Data *@ @using Phantom.Common.Data
@* @using Phantom.Web.Services *@ @using Phantom.Web.Services
@* @using Phantom.Web.Services.Instances *@ @using Phantom.Web.Services.Instances
@* @inject INavigation Nav *@ @inject INavigation Nav
@* @inject MinecraftVersions MinecraftVersions *@ @inject MinecraftVersions MinecraftVersions
@* @inject AgentManager AgentManager *@ @inject AgentManager AgentManager
@* @inject AgentJavaRuntimesManager AgentJavaRuntimesManager *@ @inject AgentJavaRuntimesManager AgentJavaRuntimesManager
@* @inject InstanceManager InstanceManager *@ @inject InstanceManager InstanceManager
@* @inject AuditLog AuditLog *@ @inject AuditLog AuditLog
@* *@
@* <Form Model="form" OnSubmit="AddOrEditInstance"> *@ <Form Model="form" OnSubmit="AddOrEditInstance">
@* @{ var selectedAgent = form.SelectedAgent; } *@ @{ var selectedAgent = form.SelectedAgent; }
@* <div class="row"> *@ <div class="row">
@* <div class="col-xl-7 mb-3"> *@ <div class="col-xl-7 mb-3">
@* @{ *@ @{
@* static RenderFragment GetAgentOption(Agent agent) { *@ static RenderFragment GetAgentOption(Agent agent) {
@* return @<option value="@agent.Guid"> *@ return @<option value="@agent.Guid">
@* @agent.Name *@ @agent.Name
@* &bullet; *@ &bullet;
@* @(agent.Stats?.RunningInstanceCount.ToString() ?? "?")/@(agent.MaxInstances) @(agent.MaxInstances == 1 ? "Instance" : "Instances") *@ @(agent.Stats?.RunningInstanceCount.ToString() ?? "?")/@(agent.MaxInstances) @(agent.MaxInstances == 1 ? "Instance" : "Instances")
@* &bullet; *@ &bullet;
@* @(agent.Stats?.RunningInstanceMemory.InMegabytes.ToString() ?? "?")/@(agent.MaxMemory.InMegabytes) MB RAM *@ @(agent.Stats?.RunningInstanceMemory.InMegabytes.ToString() ?? "?")/@(agent.MaxMemory.InMegabytes) MB RAM
@* </option>; *@ </option>;
@* } *@ }
@* } *@ }
@* @if (EditedInstanceConfiguration == null) { *@ @if (EditedInstanceConfiguration == null) {
@* <FormSelectInput Id="instance-agent" Label="Agent" @bind-Value="form.SelectedAgentGuid"> *@ <FormSelectInput Id="instance-agent" Label="Agent" @bind-Value="form.SelectedAgentGuid">
@* <option value="" selected>Select which agent will run the instance...</option> *@ <option value="" selected>Select which agent will run the instance...</option>
@* @foreach (var agent in form.AgentsByGuid.Values.Where(static agent => agent.IsOnline).OrderBy(static agent => agent.Name)) { *@ @foreach (var agent in form.AgentsByGuid.Values.Where(static agent => agent.IsOnline).OrderBy(static agent => agent.Name)) {
@* @GetAgentOption(agent) *@ @GetAgentOption(agent)
@* } *@ }
@* </FormSelectInput> *@ </FormSelectInput>
@* } *@ }
@* else { *@ else {
@* <FormSelectInput Id="instance-agent" Label="Agent" @bind-Value="form.SelectedAgentGuid" disabled="true"> *@ <FormSelectInput Id="instance-agent" Label="Agent" @bind-Value="form.SelectedAgentGuid" disabled="true">
@* @if (form.SelectedAgentGuid is {} guid && form.AgentsByGuid.TryGetValue(guid, out var agent)) { *@ @if (form.SelectedAgentGuid is {} guid && form.AgentsByGuid.TryGetValue(guid, out var agent)) {
@* @GetAgentOption(agent) *@ @GetAgentOption(agent)
@* } *@ }
@* </FormSelectInput> *@ </FormSelectInput>
@* } *@ }
@* </div> *@ </div>
@* *@
@* <div class="col-xl-5 mb-3"> *@ <div class="col-xl-5 mb-3">
@* <FormTextInput Id="instance-name" Label="Instance Name" @bind-Value="form.InstanceName" /> *@ <FormTextInput Id="instance-name" Label="Instance Name" @bind-Value="form.InstanceName" />
@* </div> *@ </div>
@* </div> *@ </div>
@* *@
@* <div class="row"> *@ <div class="row">
@* <div class="col-sm-6 col-xl-2 mb-3"> *@ <div class="col-sm-6 col-xl-2 mb-3">
@* <FormSelectInput Id="instance-server-kind" Label="Server Software" @bind-Value="form.MinecraftServerKind"> *@ <FormSelectInput Id="instance-server-kind" Label="Server Software" @bind-Value="form.MinecraftServerKind">
@* @foreach (var kind in Enum.GetValues<MinecraftServerKind>()) { *@ @foreach (var kind in Enum.GetValues<MinecraftServerKind>()) {
@* <option value="@kind">@kind</option> *@ <option value="@kind">@kind</option>
@* } *@ }
@* </FormSelectInput> *@ </FormSelectInput>
@* </div> *@ </div>
@* *@
@* <div class="col-sm-6 col-xl-3 mb-3"> *@ <div class="col-sm-6 col-xl-3 mb-3">
@* <FormSelectInput Id="instance-minecraft-version" Label="Minecraft Version" @bind-Value="form.MinecraftVersion"> *@ <FormSelectInput Id="instance-minecraft-version" Label="Minecraft Version" @bind-Value="form.MinecraftVersion">
@* <ChildContent> *@ <ChildContent>
@* @foreach (var version in availableMinecraftVersions) { *@ @foreach (var version in availableMinecraftVersions) {
@* <option value="@version.Id">@version.Id</option> *@ <option value="@version.Id">@version.Id</option>
@* } *@ }
@* </ChildContent> *@ </ChildContent>
@* <GroupContent> *@ <GroupContent>
@* <button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">@minecraftVersionType.ToNiceNamePlural()</button> *@ <button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">@minecraftVersionType.ToNiceNamePlural()</button>
@* <ul class="dropdown-menu dropdown-menu-end"> *@ <ul class="dropdown-menu dropdown-menu-end">
@* @foreach (var versionType in MinecraftVersionTypes.WithServerJars) { *@ @foreach (var versionType in MinecraftVersionTypes.WithServerJars) {
@* <li> *@ <li>
@* <button type="button" class="dropdown-item" @onclick="() => SetMinecraftVersionType(versionType)">@versionType.ToNiceNamePlural()</button> *@ <button type="button" class="dropdown-item" @onclick="() => SetMinecraftVersionType(versionType)">@versionType.ToNiceNamePlural()</button>
@* </li> *@ </li>
@* } *@ }
@* </ul> *@ </ul>
@* </GroupContent> *@ </GroupContent>
@* </FormSelectInput> *@ </FormSelectInput>
@* </div> *@ </div>
@* *@
@* <div class="col-xl-3 mb-3"> *@ <div class="col-xl-3 mb-3">
@* <FormSelectInput Id="instance-java-runtime" Label="Java Runtime" @bind-Value="form.JavaRuntimeGuid" disabled="@(form.JavaRuntimesForSelectedAgent.IsEmpty)"> *@ <FormSelectInput Id="instance-java-runtime" Label="Java Runtime" @bind-Value="form.JavaRuntimeGuid" disabled="@(form.JavaRuntimesForSelectedAgent.IsEmpty)">
@* <option value="" selected>Select Java runtime...</option> *@ <option value="" selected>Select Java runtime...</option>
@* @foreach (var (guid, runtime) in form.JavaRuntimesForSelectedAgent) { *@ @foreach (var (guid, runtime) in form.JavaRuntimesForSelectedAgent) {
@* <option value="@guid">@runtime.DisplayName</option> *@ <option value="@guid">@runtime.DisplayName</option>
@* } *@ }
@* </FormSelectInput> *@ </FormSelectInput>
@* </div> *@ </div>
@* *@
@* @{ *@ @{
@* string? allowedServerPorts = selectedAgent?.AllowedServerPorts?.ToString(); *@ string? allowedServerPorts = selectedAgent?.AllowedServerPorts?.ToString();
@* string? allowedRconPorts = selectedAgent?.AllowedRconPorts?.ToString(); *@ string? allowedRconPorts = selectedAgent?.AllowedRconPorts?.ToString();
@* } *@ }
@* <div class="col-sm-6 col-xl-2 mb-3"> *@ <div class="col-sm-6 col-xl-2 mb-3">
@* <FormNumberInput Id="instance-server-port" @bind-Value="form.ServerPort" min="0" max="65535"> *@ <FormNumberInput Id="instance-server-port" @bind-Value="form.ServerPort" min="0" max="65535">
@* <LabelFragment> *@ <LabelFragment>
@* @if (string.IsNullOrEmpty(allowedServerPorts)) { *@ @if (string.IsNullOrEmpty(allowedServerPorts)) {
@* <text>Server Port</text> *@ <text>Server Port</text>
@* } *@ }
@* else { *@ else {
@* <text>Server Port <sup title="Allowed: @allowedServerPorts">[?]</sup></text> *@ <text>Server Port <sup title="Allowed: @allowedServerPorts">[?]</sup></text>
@* } *@ }
@* </LabelFragment> *@ </LabelFragment>
@* </FormNumberInput> *@ </FormNumberInput>
@* </div> *@ </div>
@* *@
@* <div class="col-sm-6 col-xl-2 mb-3"> *@ <div class="col-sm-6 col-xl-2 mb-3">
@* <FormNumberInput Id="instance-rcon-port" @bind-Value="form.RconPort" min="0" max="65535"> *@ <FormNumberInput Id="instance-rcon-port" @bind-Value="form.RconPort" min="0" max="65535">
@* <LabelFragment> *@ <LabelFragment>
@* @if (string.IsNullOrEmpty(allowedRconPorts)) { *@ @if (string.IsNullOrEmpty(allowedRconPorts)) {
@* <text>Rcon Port</text> *@ <text>Rcon Port</text>
@* } *@ }
@* else { *@ else {
@* <text>Rcon Port <sup title="Allowed: @allowedRconPorts">[?]</sup></text> *@ <text>Rcon Port <sup title="Allowed: @allowedRconPorts">[?]</sup></text>
@* } *@ }
@* </LabelFragment> *@ </LabelFragment>
@* </FormNumberInput> *@ </FormNumberInput>
@* </div> *@ </div>
@* </div> *@ </div>
@* *@
@* <div class="row"> *@ <div class="row">
@* <div class="col-xl-12 mb-3"> *@ <div class="col-xl-12 mb-3">
@* @{ *@ @{
@* const ushort MinimumMemoryUnits = 2; *@ const ushort MinimumMemoryUnits = 2;
@* ushort maximumMemoryUnits = form.MaximumMemoryUnits; *@ ushort maximumMemoryUnits = form.MaximumMemoryUnits;
@* double availableMemoryRatio = maximumMemoryUnits <= MinimumMemoryUnits ? 100.0 : 100.0 * (form.AvailableMemoryUnits - MinimumMemoryUnits) / (maximumMemoryUnits - MinimumMemoryUnits); *@ double availableMemoryRatio = maximumMemoryUnits <= MinimumMemoryUnits ? 100.0 : 100.0 * (form.AvailableMemoryUnits - MinimumMemoryUnits) / (maximumMemoryUnits - MinimumMemoryUnits);
@* string memoryInputSplitVar = FormattableString.Invariant($"--range-split: {Math.Round(availableMemoryRatio, 2)}%"); *@ string memoryInputSplitVar = FormattableString.Invariant($"--range-split: {Math.Round(availableMemoryRatio, 2)}%");
@* } *@ }
@* <FormNumberInput Id="instance-memory" Type="FormNumberInputType.Range" DebounceMillis="0" DisableTwoWayBinding="true" @bind-Value="form.MemoryUnits" min="@MinimumMemoryUnits" max="@maximumMemoryUnits" disabled="@(maximumMemoryUnits == 0)" class="form-range split-danger" style="@memoryInputSplitVar"> *@ <FormNumberInput Id="instance-memory" Type="FormNumberInputType.Range" DebounceMillis="0" DisableTwoWayBinding="true" @bind-Value="form.MemoryUnits" min="@MinimumMemoryUnits" max="@maximumMemoryUnits" disabled="@(maximumMemoryUnits == 0)" class="form-range split-danger" style="@memoryInputSplitVar">
@* <LabelFragment> *@ <LabelFragment>
@* @if (maximumMemoryUnits == 0) { *@ @if (maximumMemoryUnits == 0) {
@* <text>RAM</text> *@ <text>RAM</text>
@* } *@ }
@* else { *@ else {
@* <text>RAM &bullet; <code>@(form.MemoryAllocation?.InMegabytes ?? 0) / @(selectedAgent?.MaxMemory.InMegabytes) MB</code></text> *@ <text>RAM &bullet; <code>@(form.MemoryAllocation?.InMegabytes ?? 0) / @(selectedAgent?.MaxMemory.InMegabytes) MB</code></text>
@* } *@ }
@* </LabelFragment> *@ </LabelFragment>
@* </FormNumberInput> *@ </FormNumberInput>
@* </div> *@ </div>
@* </div> *@ </div>
@* *@
@* <div class="row"> *@ <div class="row">
@* <div class="mb-3"> *@ <div class="mb-3">
@* <FormTextInput Id="instance-jvm-arguments" Type="FormTextInputType.Textarea" @bind-Value="form.JvmArguments" rows="4"> *@ <FormTextInput Id="instance-jvm-arguments" Type="FormTextInputType.Textarea" @bind-Value="form.JvmArguments" rows="4">
@* <LabelFragment> *@ <LabelFragment>
@* JVM Arguments <span class="text-black-50">(one per line)</span> *@ JVM Arguments <span class="text-black-50">(one per line)</span>
@* </LabelFragment> *@ </LabelFragment>
@* </FormTextInput> *@ </FormTextInput>
@* </div> *@ </div>
@* </div> *@ </div>
@* *@
@* <FormButtonSubmit Label="@(EditedInstanceConfiguration == null ? "Create Instance" : "Edit Instance")" class="btn btn-primary" disabled="@(!IsSubmittable)" /> *@ <FormButtonSubmit Label="@(EditedInstanceConfiguration == null ? "Create Instance" : "Edit Instance")" class="btn btn-primary" disabled="@(!IsSubmittable)" />
@* <FormSubmitError /> *@ <FormSubmitError />
@* </Form> *@ </Form>
@* *@
@* @code { *@ @code {
@* *@
@* [Parameter, EditorRequired] *@ [Parameter, EditorRequired]
@* public InstanceConfiguration? EditedInstanceConfiguration { get; set; } *@ public InstanceConfiguration? EditedInstanceConfiguration { get; set; }
@* *@
@* private ConfigureInstanceFormModel form = null!; *@ private ConfigureInstanceFormModel form = null!;
@* *@
@* private MinecraftVersionType minecraftVersionType = MinecraftVersionType.Release; *@ private MinecraftVersionType minecraftVersionType = MinecraftVersionType.Release;
@* private ImmutableArray<MinecraftVersion> availableMinecraftVersions = ImmutableArray<MinecraftVersion>.Empty; *@ private ImmutableArray<MinecraftVersion> availableMinecraftVersions = ImmutableArray<MinecraftVersion>.Empty;
@* *@
@* private bool IsSubmittable => form.SelectedAgentGuid != null && !form.EditContext.GetValidationMessages(form.EditContext.Field(nameof(ConfigureInstanceFormModel.SelectedAgentGuid))).Any(); *@ private bool IsSubmittable => form.SelectedAgentGuid != null && !form.EditContext.GetValidationMessages(form.EditContext.Field(nameof(ConfigureInstanceFormModel.SelectedAgentGuid))).Any();
@* *@
@* private sealed class ConfigureInstanceFormModel : FormModel { *@ private sealed class ConfigureInstanceFormModel : FormModel {
@* public ImmutableDictionary<Guid, Agent> AgentsByGuid { get; } *@ public ImmutableDictionary<Guid, Agent> AgentsByGuid { get; }
@* private readonly ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>> javaRuntimesByAgentGuid; *@ private readonly ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>> javaRuntimesByAgentGuid;
@* private readonly RamAllocationUnits? editedInstanceRamAllocation; *@ private readonly RamAllocationUnits? editedInstanceRamAllocation;
@* *@
@* public ConfigureInstanceFormModel(AgentManager agentManager, AgentJavaRuntimesManager agentJavaRuntimesManager, RamAllocationUnits? editedInstanceRamAllocation) { *@ public ConfigureInstanceFormModel(AgentManager agentManager, AgentJavaRuntimesManager agentJavaRuntimesManager, RamAllocationUnits? editedInstanceRamAllocation) {
@* this.AgentsByGuid = agentManager.GetAgents().ToImmutableDictionary(); *@ this.AgentsByGuid = agentManager.GetAgents().ToImmutableDictionary();
@* this.javaRuntimesByAgentGuid = agentJavaRuntimesManager.All; *@ this.javaRuntimesByAgentGuid = agentJavaRuntimesManager.All;
@* this.editedInstanceRamAllocation = editedInstanceRamAllocation; *@ this.editedInstanceRamAllocation = editedInstanceRamAllocation;
@* } *@ }
@* *@
@* private bool TryGet<TValue>(ImmutableDictionary<Guid, TValue> dictionary, Guid? agentGuid, [MaybeNullWhen(false)] out TValue value) { *@ private bool TryGet<TValue>(ImmutableDictionary<Guid, TValue> dictionary, Guid? agentGuid, [MaybeNullWhen(false)] out TValue value) {
@* if (agentGuid == null) { *@ if (agentGuid == null) {
@* value = default; *@ value = default;
@* return false; *@ return false;
@* } *@ }
@* else { *@ else {
@* return dictionary.TryGetValue(agentGuid.Value, out value); *@ return dictionary.TryGetValue(agentGuid.Value, out value);
@* } *@ }
@* } *@ }
@* *@
@* private bool TryGetAgent(Guid? agentGuid, [NotNullWhen(true)] out Agent? agent) { *@ private bool TryGetAgent(Guid? agentGuid, [NotNullWhen(true)] out Agent? agent) {
@* return TryGet(AgentsByGuid, agentGuid, out agent); *@ return TryGet(AgentsByGuid, agentGuid, out agent);
@* } *@ }
@* *@
@* public Agent? SelectedAgent => TryGetAgent(SelectedAgentGuid, out var agent) ? agent : null; *@ public Agent? SelectedAgent => TryGetAgent(SelectedAgentGuid, out var agent) ? agent : null;
@* *@
@* public ImmutableArray<TaggedJavaRuntime> JavaRuntimesForSelectedAgent => TryGet(javaRuntimesByAgentGuid, SelectedAgentGuid, out var javaRuntimes) ? javaRuntimes : ImmutableArray<TaggedJavaRuntime>.Empty; *@ public ImmutableArray<TaggedJavaRuntime> JavaRuntimesForSelectedAgent => TryGet(javaRuntimesByAgentGuid, SelectedAgentGuid, out var javaRuntimes) ? javaRuntimes : ImmutableArray<TaggedJavaRuntime>.Empty;
@* *@
@* public ushort MaximumMemoryUnits => SelectedAgent?.MaxMemory.RawValue ?? 0; *@ public ushort MaximumMemoryUnits => SelectedAgent?.MaxMemory.RawValue ?? 0;
@* public ushort AvailableMemoryUnits => Math.Min((SelectedAgent?.AvailableMemory + editedInstanceRamAllocation)?.RawValue ?? MaximumMemoryUnits, MaximumMemoryUnits); *@ public ushort AvailableMemoryUnits => Math.Min((SelectedAgent?.AvailableMemory + editedInstanceRamAllocation)?.RawValue ?? MaximumMemoryUnits, MaximumMemoryUnits);
@* private ushort selectedMemoryUnits = 4; *@ private ushort selectedMemoryUnits = 4;
@* *@
@* [Required(ErrorMessage = "You must select an agent.")] *@ [Required(ErrorMessage = "You must select an agent.")]
@* public Guid? SelectedAgentGuid { get; set; } = null; *@ public Guid? SelectedAgentGuid { get; set; } = null;
@* *@
@* [Required(ErrorMessage = "Instance name is required.")] *@ [Required(ErrorMessage = "Instance name is required.")]
@* [StringLength(100, ErrorMessage = "Instance name must be at most 100 characters.")] *@ [StringLength(100, ErrorMessage = "Instance name must be at most 100 characters.")]
@* public string InstanceName { get; set; } = string.Empty; *@ public string InstanceName { get; set; } = string.Empty;
@* *@
@* [Range(minimum: 0, maximum: 65535, ErrorMessage = "Server port must be between 0 and 65535.")] *@ [Range(minimum: 0, maximum: 65535, ErrorMessage = "Server port must be between 0 and 65535.")]
@* [ServerPortMustBeAllowed(ErrorMessage = "Server port is not allowed.")] *@ [ServerPortMustBeAllowed(ErrorMessage = "Server port is not allowed.")]
@* public int ServerPort { get; set; } = 25565; *@ public int ServerPort { get; set; } = 25565;
@* *@
@* [Range(minimum: 0, maximum: 65535, ErrorMessage = "Rcon port must be between 0 and 65535.")] *@ [Range(minimum: 0, maximum: 65535, ErrorMessage = "Rcon port must be between 0 and 65535.")]
@* [RconPortMustBeAllowed(ErrorMessage = "Rcon port is not allowed.")] *@ [RconPortMustBeAllowed(ErrorMessage = "Rcon port is not allowed.")]
@* [RconPortMustDifferFromServerPort(ErrorMessage = "Rcon port must not be the same as Server port.")] *@ [RconPortMustDifferFromServerPort(ErrorMessage = "Rcon port must not be the same as Server port.")]
@* public int RconPort { get; set; } = 25575; *@ public int RconPort { get; set; } = 25575;
@* *@
@* public MinecraftServerKind MinecraftServerKind { get; set; } = MinecraftServerKind.Vanilla; *@ public MinecraftServerKind MinecraftServerKind { get; set; } = MinecraftServerKind.Vanilla;
@* *@
@* [Required(ErrorMessage = "You must select a Java runtime.")] *@ [Required(ErrorMessage = "You must select a Java runtime.")]
@* public Guid? JavaRuntimeGuid { get; set; } *@ public Guid? JavaRuntimeGuid { get; set; }
@* *@
@* [Required(ErrorMessage = "You must select a Minecraft version.")] *@ [Required(ErrorMessage = "You must select a Minecraft version.")]
@* public string MinecraftVersion { get; set; } = string.Empty; *@ public string MinecraftVersion { get; set; } = string.Empty;
@* *@
@* [Range(minimum: 0, maximum: RamAllocationUnits.MaximumUnits, ErrorMessage = "Memory is out of range.")] *@ [Range(minimum: 0, maximum: RamAllocationUnits.MaximumUnits, ErrorMessage = "Memory is out of range.")]
@* public ushort MemoryUnits { *@ public ushort MemoryUnits {
@* get => Math.Min(selectedMemoryUnits, MaximumMemoryUnits); *@ get => Math.Min(selectedMemoryUnits, MaximumMemoryUnits);
@* set => selectedMemoryUnits = value; *@ set => selectedMemoryUnits = value;
@* } *@ }
@* *@
@* public RamAllocationUnits? MemoryAllocation => new RamAllocationUnits(MemoryUnits); *@ public RamAllocationUnits? MemoryAllocation => new RamAllocationUnits(MemoryUnits);
@* *@
@* [JvmArgumentsMustBeValid(ErrorMessage = "JVM arguments are not valid.")] *@ [JvmArgumentsMustBeValid(ErrorMessage = "JVM arguments are not valid.")]
@* public string JvmArguments { get; set; } = string.Empty; *@ public string JvmArguments { get; set; } = string.Empty;
@* *@
@* public sealed class ServerPortMustBeAllowedAttribute : FormValidationAttribute<ConfigureInstanceFormModel, int> { *@ public sealed class ServerPortMustBeAllowedAttribute : FormValidationAttribute<ConfigureInstanceFormModel, int> {
@* protected override string FieldName => nameof(ServerPort); *@ protected override string FieldName => nameof(ServerPort);
@* protected override bool IsValid(ConfigureInstanceFormModel model, int value) => model.SelectedAgent is not {} agent || agent.AllowedServerPorts?.Contains((ushort) value) == true; *@ protected override bool IsValid(ConfigureInstanceFormModel model, int value) => model.SelectedAgent is not {} agent || agent.AllowedServerPorts?.Contains((ushort) value) == true;
@* } *@ }
@* *@
@* public sealed class RconPortMustBeAllowedAttribute : FormValidationAttribute<ConfigureInstanceFormModel, int> { *@ public sealed class RconPortMustBeAllowedAttribute : FormValidationAttribute<ConfigureInstanceFormModel, int> {
@* protected override string FieldName => nameof(RconPort); *@ protected override string FieldName => nameof(RconPort);
@* protected override bool IsValid(ConfigureInstanceFormModel model, int value) => model.SelectedAgent is not {} agent || agent.AllowedRconPorts?.Contains((ushort) value) == true; *@ protected override bool IsValid(ConfigureInstanceFormModel model, int value) => model.SelectedAgent is not {} agent || agent.AllowedRconPorts?.Contains((ushort) value) == true;
@* } *@ }
@* *@
@* public sealed class RconPortMustDifferFromServerPortAttribute : FormValidationAttribute<ConfigureInstanceFormModel, int?> { *@ public sealed class RconPortMustDifferFromServerPortAttribute : FormValidationAttribute<ConfigureInstanceFormModel, int?> {
@* protected override string FieldName => nameof(RconPort); *@ protected override string FieldName => nameof(RconPort);
@* protected override bool IsValid(ConfigureInstanceFormModel model, int? value) => value != model.ServerPort; *@ protected override bool IsValid(ConfigureInstanceFormModel model, int? value) => value != model.ServerPort;
@* } *@ }
@* *@
@* public sealed class JvmArgumentsMustBeValidAttribute : FormCustomValidationAttribute<ConfigureInstanceFormModel, string> { *@ public sealed class JvmArgumentsMustBeValidAttribute : FormCustomValidationAttribute<ConfigureInstanceFormModel, string> {
@* protected override string FieldName => nameof(JvmArguments); *@ protected override string FieldName => nameof(JvmArguments);
@* *@
@* protected override ValidationResult? Validate(ConfigureInstanceFormModel model, string value) { *@ protected override ValidationResult? Validate(ConfigureInstanceFormModel model, string value) {
@* var error = JvmArgumentsHelper.Validate(value); *@ var error = JvmArgumentsHelper.Validate(value);
@* return error == null ? ValidationResult.Success : new ValidationResult(error.ToSentence()); *@ return error == null ? ValidationResult.Success : new ValidationResult(error.ToSentence());
@* } *@ }
@* } *@ }
@* } *@ }
@* *@
@* protected override void OnInitialized() { *@ protected override void OnInitialized() {
@* form = new ConfigureInstanceFormModel(AgentManager, AgentJavaRuntimesManager, EditedInstanceConfiguration?.MemoryAllocation); *@ form = new ConfigureInstanceFormModel(AgentManager, AgentJavaRuntimesManager, EditedInstanceConfiguration?.MemoryAllocation);
@* *@
@* if (EditedInstanceConfiguration != null) { *@ if (EditedInstanceConfiguration != null) {
@* form.SelectedAgentGuid = EditedInstanceConfiguration.AgentGuid; *@ form.SelectedAgentGuid = EditedInstanceConfiguration.AgentGuid;
@* form.InstanceName = EditedInstanceConfiguration.InstanceName; *@ form.InstanceName = EditedInstanceConfiguration.InstanceName;
@* form.ServerPort = EditedInstanceConfiguration.ServerPort; *@ form.ServerPort = EditedInstanceConfiguration.ServerPort;
@* form.RconPort = EditedInstanceConfiguration.RconPort; *@ form.RconPort = EditedInstanceConfiguration.RconPort;
@* form.MinecraftVersion = EditedInstanceConfiguration.MinecraftVersion; *@ form.MinecraftVersion = EditedInstanceConfiguration.MinecraftVersion;
@* form.MinecraftServerKind = EditedInstanceConfiguration.MinecraftServerKind; *@ form.MinecraftServerKind = EditedInstanceConfiguration.MinecraftServerKind;
@* form.MemoryUnits = EditedInstanceConfiguration.MemoryAllocation.RawValue; *@ form.MemoryUnits = EditedInstanceConfiguration.MemoryAllocation.RawValue;
@* form.JavaRuntimeGuid = EditedInstanceConfiguration.JavaRuntimeGuid; *@ form.JavaRuntimeGuid = EditedInstanceConfiguration.JavaRuntimeGuid;
@* form.JvmArguments = JvmArgumentsHelper.Join(EditedInstanceConfiguration.JvmArguments); *@ form.JvmArguments = JvmArgumentsHelper.Join(EditedInstanceConfiguration.JvmArguments);
@* } *@ }
@* *@
@* form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.SelectedAgentGuid), revalidated: nameof(ConfigureInstanceFormModel.MemoryUnits)); *@ form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.SelectedAgentGuid), revalidated: nameof(ConfigureInstanceFormModel.MemoryUnits));
@* form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.SelectedAgentGuid), revalidated: nameof(ConfigureInstanceFormModel.JavaRuntimeGuid)); *@ form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.SelectedAgentGuid), revalidated: nameof(ConfigureInstanceFormModel.JavaRuntimeGuid));
@* form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.SelectedAgentGuid), revalidated: nameof(ConfigureInstanceFormModel.ServerPort)); *@ 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.SelectedAgentGuid), revalidated: nameof(ConfigureInstanceFormModel.RconPort));
@* form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.ServerPort), revalidated: nameof(ConfigureInstanceFormModel.RconPort)); *@ form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.ServerPort), revalidated: nameof(ConfigureInstanceFormModel.RconPort));
@* } *@ }
@* *@
@* protected override async Task OnInitializedAsync() { *@ protected override async Task OnInitializedAsync() {
@* if (EditedInstanceConfiguration != null) { *@ if (EditedInstanceConfiguration != null) {
@* var allMinecraftVersions = await MinecraftVersions.GetVersions(CancellationToken.None); *@ var allMinecraftVersions = await MinecraftVersions.GetVersions(CancellationToken.None);
@* minecraftVersionType = allMinecraftVersions.FirstOrDefault(version => version.Id == EditedInstanceConfiguration.MinecraftVersion)?.Type ?? minecraftVersionType; *@ minecraftVersionType = allMinecraftVersions.FirstOrDefault(version => version.Id == EditedInstanceConfiguration.MinecraftVersion)?.Type ?? minecraftVersionType;
@* } *@ }
@* *@
@* await SetMinecraftVersionType(minecraftVersionType); *@ await SetMinecraftVersionType(minecraftVersionType);
@* } *@ }
@* *@
@* private async Task SetMinecraftVersionType(MinecraftVersionType type) { *@ private async Task SetMinecraftVersionType(MinecraftVersionType type) {
@* minecraftVersionType = type; *@ minecraftVersionType = type;
@* *@
@* var allMinecraftVersions = await MinecraftVersions.GetVersions(CancellationToken.None); *@ var allMinecraftVersions = await MinecraftVersions.GetVersions(CancellationToken.None);
@* availableMinecraftVersions = allMinecraftVersions.Where(version => version.Type == type).ToImmutableArray(); *@ availableMinecraftVersions = allMinecraftVersions.Where(version => version.Type == type).ToImmutableArray();
@* *@
@* if (!availableMinecraftVersions.IsEmpty && !availableMinecraftVersions.Any(version => version.Id == form.MinecraftVersion)) { *@ if (!availableMinecraftVersions.IsEmpty && !availableMinecraftVersions.Any(version => version.Id == form.MinecraftVersion)) {
@* form.MinecraftVersion = availableMinecraftVersions[0].Id; *@ form.MinecraftVersion = availableMinecraftVersions[0].Id;
@* } *@ }
@* } *@ }
@* *@
@* private async Task AddOrEditInstance(EditContext context) { *@ private async Task AddOrEditInstance(EditContext context) {
@* var selectedAgent = form.SelectedAgent; *@ var selectedAgent = form.SelectedAgent;
@* if (selectedAgent == null) { *@ if (selectedAgent == null) {
@* return; *@ return;
@* } *@ }
@* *@
@* await form.SubmitModel.StartSubmitting(); *@ await form.SubmitModel.StartSubmitting();
@* *@
@* var instance = new InstanceConfiguration( *@ var instance = new InstanceConfiguration(
@* EditedInstanceConfiguration?.AgentGuid ?? selectedAgent.Guid, *@ EditedInstanceConfiguration?.AgentGuid ?? selectedAgent.Guid,
@* EditedInstanceConfiguration?.InstanceGuid ?? Guid.NewGuid(), *@ EditedInstanceConfiguration?.InstanceGuid ?? Guid.NewGuid(),
@* form.InstanceName, *@ form.InstanceName,
@* (ushort) form.ServerPort, *@ (ushort) form.ServerPort,
@* (ushort) form.RconPort, *@ (ushort) form.RconPort,
@* form.MinecraftVersion, *@ form.MinecraftVersion,
@* form.MinecraftServerKind, *@ form.MinecraftServerKind,
@* form.MemoryAllocation ?? RamAllocationUnits.Zero, *@ form.MemoryAllocation ?? RamAllocationUnits.Zero,
@* form.JavaRuntimeGuid.GetValueOrDefault(), *@ form.JavaRuntimeGuid.GetValueOrDefault(),
@* JvmArgumentsHelper.Split(form.JvmArguments) *@ JvmArgumentsHelper.Split(form.JvmArguments)
@* ); *@ );
@* *@
@* var result = await InstanceManager.AddOrEditInstance(instance); *@ var result = await InstanceManager.AddOrEditInstance(instance);
@* if (result.Is(AddOrEditInstanceResult.Success)) { *@ if (result.Is(AddOrEditInstanceResult.Success)) {
@* await (EditedInstanceConfiguration == null ? AuditLog.AddInstanceCreatedEvent(instance.InstanceGuid) : AuditLog.AddInstanceEditedEvent(instance.InstanceGuid)); *@ await (EditedInstanceConfiguration == null ? AuditLog.AddInstanceCreatedEvent(instance.InstanceGuid) : AuditLog.AddInstanceEditedEvent(instance.InstanceGuid));
@* Nav.NavigateTo("instances/" + instance.InstanceGuid); *@ Nav.NavigateTo("instances/" + instance.InstanceGuid);
@* } *@ }
@* else { *@ else {
@* form.SubmitModel.StopSubmitting(result.ToSentence(AddOrEditInstanceResultExtensions.ToSentence)); *@ form.SubmitModel.StopSubmitting(result.ToSentence(AddOrEditInstanceResultExtensions.ToSentence));
@* } *@ }
@* } *@ }
@* *@
@* } *@ }

View File

@ -1,53 +1,53 @@
@* @using Phantom.Web.Services.Instances *@ @using Phantom.Web.Services.Instances
@* @using Phantom.Common.Data.Web.Users *@ @using Phantom.Common.Data.Web.Users
@* @using Phantom.Common.Data.Replies *@ @using Phantom.Common.Data.Replies
@* @inherits PhantomComponent *@ @inherits PhantomComponent
@* @inject InstanceManager InstanceManager *@ @inject InstanceManager InstanceManager
@* *@
@* <Form Model="form" OnSubmit="ExecuteCommand"> *@ <Form Model="form" OnSubmit="ExecuteCommand">
@* <label for="command-input" class="form-label">Instance Name</label> *@ <label for="command-input" class="form-label">Instance Name</label>
@* <div class="input-group flex-nowrap"> *@ <div class="input-group flex-nowrap">
@* <span class="input-group-text" style="padding-top: 0.3rem;">/</span> *@ <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" /> *@ <input id="command-input" class="form-control" type="text" placeholder="command" @bind="form.Command" @bind:event="oninput" disabled="@(Disabled || form.SubmitModel.IsSubmitting)" @ref="commandInputElement" />
@* <FormButtonSubmit Label="Execute" class="btn btn-primary" disabled="@(Disabled || string.IsNullOrWhiteSpace(form.Command))" /> *@ <FormButtonSubmit Label="Execute" class="btn btn-primary" disabled="@(Disabled || string.IsNullOrWhiteSpace(form.Command))" />
@* </div> *@ </div>
@* <FormSubmitError /> *@ <FormSubmitError />
@* </Form> *@ </Form>
@* *@
@* @code { *@ @code {
@* *@
@* [Parameter] *@ [Parameter]
@* public Guid InstanceGuid { get; set; } *@ public Guid InstanceGuid { get; set; }
@* *@
@* [Parameter] *@ [Parameter]
@* public bool Disabled { get; set; } *@ public bool Disabled { get; set; }
@* *@
@* private readonly SendCommandFormModel form = new (); *@ private readonly SendCommandFormModel form = new ();
@* *@
@* private sealed class SendCommandFormModel : FormModel { *@ private sealed class SendCommandFormModel : FormModel {
@* public string Command { get; set; } = string.Empty; *@ public string Command { get; set; } = string.Empty;
@* } *@ }
@* *@
@* private ElementReference commandInputElement; *@ private ElementReference commandInputElement;
@* *@
@* private async Task ExecuteCommand(EditContext context) { *@ private async Task ExecuteCommand(EditContext context) {
@* await form.SubmitModel.StartSubmitting(); *@ await form.SubmitModel.StartSubmitting();
@* *@
@* if (!await CheckPermission(Permission.ControlInstances)) { *@ if (!await CheckPermission(Permission.ControlInstances)) {
@* form.SubmitModel.StopSubmitting("You do not have permission to execute commands."); *@ form.SubmitModel.StopSubmitting("You do not have permission to execute commands.");
@* return; *@ return;
@* } *@ }
@* *@
@* var result = await InstanceManager.SendCommand(InstanceGuid, form.Command); *@ var result = await InstanceManager.SendCommand(InstanceGuid, form.Command);
@* if (result.Is(SendCommandToInstanceResult.Success)) { *@ if (result.Is(SendCommandToInstanceResult.Success)) {
@* form.Command = string.Empty; *@ form.Command = string.Empty;
@* form.SubmitModel.StopSubmitting(); *@ form.SubmitModel.StopSubmitting();
@* } *@ }
@* else { *@ else {
@* form.SubmitModel.StopSubmitting(result.ToSentence(Messages.ToSentence)); *@ form.SubmitModel.StopSubmitting(result.ToSentence(Messages.ToSentence));
@* } *@ }
@* *@
@* await commandInputElement.FocusAsync(preventScroll: true); *@ await commandInputElement.FocusAsync(preventScroll: true);
@* } *@ }
@* *@
@* } *@ }

View File

@ -1,71 +1,71 @@
@* @inherits PhantomComponent *@ @inherits PhantomComponent
@* @using Phantom.Utils.Collections *@ @using Phantom.Utils.Collections
@* @using System.Diagnostics *@ @using System.Diagnostics
@* @using Phantom.Common.Data.Web.Users *@ @using Phantom.Common.Data.Web.Users
@* @implements IDisposable *@ @implements IDisposable
@* @inject IJSRuntime Js; *@ @inject IJSRuntime Js;
@* @inject InstanceLogManager InstanceLogManager *@ @inject InstanceLogManager InstanceLogManager
@* *@
@* <div id="log" class="font-monospace mb-3"> *@ <div id="log" class="font-monospace mb-3">
@* @foreach (var line in instanceLogs.EnumerateLast(uint.MaxValue)) { *@ @foreach (var line in instanceLogs.EnumerateLast(uint.MaxValue)) {
@* <p>@(new MarkupString(line))</p> *@ <p>@(new MarkupString(line))</p>
@* } *@ }
@* </div> *@ </div>
@* *@
@* @code { *@ @code {
@* *@
@* [Parameter, EditorRequired] *@ [Parameter, EditorRequired]
@* public Guid InstanceGuid { get; set; } *@ public Guid InstanceGuid { get; set; }
@* *@
@* private IJSObjectReference? PageJs { get; set; } *@ private IJSObjectReference? PageJs { get; set; }
@* *@
@* private EventSubscribers<RingBuffer<string>> instanceLogsSubs = null!; *@ private EventSubscribers<RingBuffer<string>> instanceLogsSubs = null!;
@* private RingBuffer<string> instanceLogs = null!; *@ private RingBuffer<string> instanceLogs = null!;
@* *@
@* private readonly Stopwatch recheckPermissionsStopwatch = Stopwatch.StartNew(); *@ private readonly Stopwatch recheckPermissionsStopwatch = Stopwatch.StartNew();
@* *@
@* protected override void OnInitialized() { *@ protected override void OnInitialized() {
@* instanceLogsSubs = InstanceLogManager.GetSubs(InstanceGuid); *@ instanceLogsSubs = InstanceLogManager.GetSubs(InstanceGuid);
@* instanceLogsSubs.Subscribe(this, buffer => { *@ instanceLogsSubs.Subscribe(this, buffer => {
@* instanceLogs = buffer; *@ instanceLogs = buffer;
@* InvokeAsyncChecked(RefreshLog); *@ InvokeAsyncChecked(RefreshLog);
@* }); *@ });
@* } *@ }
@* *@
@* protected override async Task OnAfterRenderAsync(bool firstRender) { *@ protected override async Task OnAfterRenderAsync(bool firstRender) {
@* if (firstRender) { *@ if (firstRender) {
@* PageJs = await Js.InvokeAsync<IJSObjectReference>("import", "./Shared/InstanceLog.razor.js"); *@ PageJs = await Js.InvokeAsync<IJSObjectReference>("import", "./Shared/InstanceLog.razor.js");
@* await RecheckPermissions(); *@ await RecheckPermissions();
@* StateHasChanged(); *@ StateHasChanged();
@* *@
@* await PageJs.InvokeVoidAsync("initLog"); *@ await PageJs.InvokeVoidAsync("initLog");
@* } *@ }
@* } *@ }
@* *@
@* private async Task RefreshLog() { *@ private async Task RefreshLog() {
@* if (recheckPermissionsStopwatch.Elapsed > TimeSpan.FromSeconds(2)) { *@ if (recheckPermissionsStopwatch.Elapsed > TimeSpan.FromSeconds(2)) {
@* await RecheckPermissions(); *@ await RecheckPermissions();
@* } *@ }
@* *@
@* StateHasChanged(); *@ StateHasChanged();
@* *@
@* if (PageJs != null) { *@ if (PageJs != null) {
@* await PageJs.InvokeVoidAsync("scrollLog"); *@ await PageJs.InvokeVoidAsync("scrollLog");
@* } *@ }
@* } *@ }
@* *@
@* private async Task RecheckPermissions() { *@ private async Task RecheckPermissions() {
@* recheckPermissionsStopwatch.Restart(); *@ recheckPermissionsStopwatch.Restart();
@* *@
@* if (!await CheckPermission(Permission.ViewInstanceLogs)) { *@ if (!await CheckPermission(Permission.ViewInstanceLogs)) {
@* await Task.Yield(); *@ await Task.Yield();
@* Dispose(); *@ Dispose();
@* instanceLogs = new RingBuffer<string>(0); *@ instanceLogs = new RingBuffer<string>(0);
@* } *@ }
@* } *@ }
@* *@
@* public void Dispose() { *@ public void Dispose() {
@* instanceLogsSubs.Unsubscribe(this); *@ instanceLogsSubs.Unsubscribe(this);
@* } *@ }
@* *@
@* } *@ }

View File

@ -1,70 +1,70 @@
@* @using Phantom.Web.Services.Instances *@ @using Phantom.Web.Services.Instances
@* @using System.ComponentModel.DataAnnotations *@ @using System.ComponentModel.DataAnnotations
@* @using Phantom.Common.Data.Web.Users *@ @using Phantom.Common.Data.Web.Users
@* @using Phantom.Common.Data.Minecraft *@ @using Phantom.Common.Data.Minecraft
@* @using Phantom.Common.Data.Replies *@ @using Phantom.Common.Data.Replies
@* @inherits PhantomComponent *@ @inherits PhantomComponent
@* @inject IJSRuntime Js; *@ @inject IJSRuntime Js;
@* @inject InstanceManager InstanceManager; *@ @inject InstanceManager InstanceManager;
@* @inject AuditLog AuditLog *@ @inject AuditLog AuditLog
@* *@
@* <Form Model="form" OnSubmit="StopInstance"> *@ <Form Model="form" OnSubmit="StopInstance">
@* <Modal Id="@ModalId" TitleText="Stop Instance"> *@ <Modal Id="@ModalId" TitleText="Stop Instance">
@* <Body> *@ <Body>
@* <FormSelectInput Id="stop-in-seconds" Label="Stop In..." @bind-Value="form.StopInSeconds"> *@ <FormSelectInput Id="stop-in-seconds" Label="Stop In..." @bind-Value="form.StopInSeconds">
@* <option value="0">Immediately</option> *@ <option value="0">Immediately</option>
@* <option value="10">10 Seconds</option> *@ <option value="10">10 Seconds</option>
@* <option value="30">30 Seconds</option> *@ <option value="30">30 Seconds</option>
@* <option value="60">1 Minute</option> *@ <option value="60">1 Minute</option>
@* <option value="120">2 Minutes</option> *@ <option value="120">2 Minutes</option>
@* <option value="180">3 Minutes</option> *@ <option value="180">3 Minutes</option>
@* <option value="240">4 Minutes</option> *@ <option value="240">4 Minutes</option>
@* <option value="300">5 Minutes</option> *@ <option value="300">5 Minutes</option>
@* </FormSelectInput> *@ </FormSelectInput>
@* </Body> *@ </Body>
@* <Footer> *@ <Footer>
@* <FormSubmitError /> *@ <FormSubmitError />
@* <FormButtonSubmit Label="Stop Instance" class="btn btn-danger" disabled="@Disabled" /> *@ <FormButtonSubmit Label="Stop Instance" class="btn btn-danger" disabled="@Disabled" />
@* <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> *@ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
@* </Footer> *@ </Footer>
@* </Modal> *@ </Modal>
@* </Form> *@ </Form>
@* *@
@* @code { *@ @code {
@* *@
@* [Parameter, EditorRequired] *@ [Parameter, EditorRequired]
@* public Guid InstanceGuid { get; set; } *@ public Guid InstanceGuid { get; set; }
@* *@
@* [Parameter, EditorRequired] *@ [Parameter, EditorRequired]
@* public string ModalId { get; set; } = string.Empty; *@ public string ModalId { get; set; } = string.Empty;
@* *@
@* [Parameter] *@ [Parameter]
@* public bool Disabled { get; set; } *@ public bool Disabled { get; set; }
@* *@
@* private readonly StopInstanceFormModel form = new (); *@ private readonly StopInstanceFormModel form = new ();
@* *@
@* private sealed class StopInstanceFormModel : FormModel { *@ private sealed class StopInstanceFormModel : FormModel {
@* [Range(minimum: 0, maximum: 300, ErrorMessage = "Stop delay must be between 0 and 300 seconds.")] *@ [Range(minimum: 0, maximum: 300, ErrorMessage = "Stop delay must be between 0 and 300 seconds.")]
@* public ushort StopInSeconds { get; set; } = 0; *@ public ushort StopInSeconds { get; set; } = 0;
@* } *@ }
@* *@
@* private async Task StopInstance(EditContext context) { *@ private async Task StopInstance(EditContext context) {
@* await form.SubmitModel.StartSubmitting(); *@ await form.SubmitModel.StartSubmitting();
@* *@
@* if (!await CheckPermission(Permission.ControlInstances)) { *@ if (!await CheckPermission(Permission.ControlInstances)) {
@* form.SubmitModel.StopSubmitting("You do not have permission to stop instances."); *@ form.SubmitModel.StopSubmitting("You do not have permission to stop instances.");
@* return; *@ return;
@* } *@ }
@* *@
@* var result = await InstanceManager.StopInstance(InstanceGuid, new MinecraftStopStrategy(form.StopInSeconds)); *@ var result = await InstanceManager.StopInstance(InstanceGuid, new MinecraftStopStrategy(form.StopInSeconds));
@* if (result.Is(StopInstanceResult.StopInitiated)) { *@ if (result.Is(StopInstanceResult.StopInitiated)) {
@* await AuditLog.AddInstanceStoppedEvent(InstanceGuid, form.StopInSeconds); *@ await AuditLog.AddInstanceStoppedEvent(InstanceGuid, form.StopInSeconds);
@* await Js.InvokeVoidAsync("closeModal", ModalId); *@ await Js.InvokeVoidAsync("closeModal", ModalId);
@* form.SubmitModel.StopSubmitting(); *@ form.SubmitModel.StopSubmitting();
@* } *@ }
@* else { *@ else {
@* form.SubmitModel.StopSubmitting(result.ToSentence(Messages.ToSentence)); *@ form.SubmitModel.StopSubmitting(result.ToSentence(Messages.ToSentence));
@* } *@ }
@* } *@ }
@* *@
@* } *@ }

View File

@ -1,72 +1,72 @@
@* @using Phantom.Common.Data.Web.Users *@ @using Phantom.Common.Data.Web.Users
@* @using Phantom.Utils.Tasks *@ @using Phantom.Utils.Tasks
@* @using System.ComponentModel.DataAnnotations *@ @using System.ComponentModel.DataAnnotations
@* @inherits PhantomComponent *@ @inherits PhantomComponent
@* @inject IJSRuntime Js; *@ @inject IJSRuntime Js;
@* *@
@* <Form Model="form" OnSubmit="AddUser"> *@ <Form Model="form" OnSubmit="AddUser">
@* <Modal Id="@ModalId" TitleText="Add User"> *@ <Modal Id="@ModalId" TitleText="Add User">
@* <Body> *@ <Body>
@* *@
@* <div class="row"> *@ <div class="row">
@* <div class="mb-3"> *@ <div class="mb-3">
@* <FormTextInput Id="account-username" Label="Username" @bind-Value="form.Username" autocomplete="off" /> *@ <FormTextInput Id="account-username" Label="Username" @bind-Value="form.Username" autocomplete="off" />
@* </div> *@ </div>
@* </div> *@ </div>
@* *@
@* <div class="row"> *@ <div class="row">
@* <div class="mb-3"> *@ <div class="mb-3">
@* <FormTextInput Id="account-password" Label="Password" Type="FormTextInputType.Password" autocomplete="new-password" @bind-Value="form.Password" /> *@ <FormTextInput Id="account-password" Label="Password" Type="FormTextInputType.Password" autocomplete="new-password" @bind-Value="form.Password" />
@* </div> *@ </div>
@* </div> *@ </div>
@* *@
@* </Body> *@ </Body>
@* <Footer> *@ <Footer>
@* <FormSubmitError /> *@ <FormSubmitError />
@* <FormButtonSubmit Label="Add User" class="btn btn-primary" /> *@ <FormButtonSubmit Label="Add User" class="btn btn-primary" />
@* <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> *@ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
@* </Footer> *@ </Footer>
@* </Modal> *@ </Modal>
@* </Form> *@ </Form>
@* *@
@* @code { *@ @code {
@* *@
@* [Parameter, EditorRequired] *@ [Parameter, EditorRequired]
@* public string ModalId { get; set; } = string.Empty; *@ public string ModalId { get; set; } = string.Empty;
@* *@
@* [Parameter] *@ [Parameter]
@* public EventCallback<UserInfo> UserAdded { get; set; } *@ public EventCallback<UserInfo> UserAdded { get; set; }
@* *@
@* private readonly AddUserFormModel form = new(); *@ private readonly AddUserFormModel form = new();
@* *@
@* private sealed class AddUserFormModel : FormModel { *@ private sealed class AddUserFormModel : FormModel {
@* [Required] *@ [Required]
@* public string Username { get; set; } = string.Empty; *@ public string Username { get; set; } = string.Empty;
@* *@
@* [Required] *@ [Required]
@* public string Password { get; set; } = string.Empty; *@ public string Password { get; set; } = string.Empty;
@* } *@ }
@* *@
@* private async Task AddUser(EditContext context) { *@ private async Task AddUser(EditContext context) {
@* await form.SubmitModel.StartSubmitting(); *@ await form.SubmitModel.StartSubmitting();
@* *@
@* if (!await CheckPermission(Permission.EditUsers)) { *@ if (!await CheckPermission(Permission.EditUsers)) {
@* form.SubmitModel.StopSubmitting("You do not have permission to add users."); *@ form.SubmitModel.StopSubmitting("You do not have permission to add users.");
@* return; *@ return;
@* } *@ }
@* *@
@* switch (await UserManager.CreateUser(form.Username, form.Password)) { *@ switch (await UserManager.CreateUser(form.Username, form.Password)) {
@* case Result<UserInfo, AddUserError>.Ok ok: *@ case Result<UserInfo, AddUserError>.Ok ok:
@* await AuditLog.AddUserCreatedEvent(ok.Value); *@ await AuditLog.AddUserCreatedEvent(ok.Value);
@* await UserAdded.InvokeAsync(ok.Value); *@ await UserAdded.InvokeAsync(ok.Value);
@* await Js.InvokeVoidAsync("closeModal", ModalId); *@ await Js.InvokeVoidAsync("closeModal", ModalId);
@* form.SubmitModel.StopSubmitting(); *@ form.SubmitModel.StopSubmitting();
@* break; *@ break;
@* *@
@* case Result<UserInfo, AddUserError>.Fail fail: *@ case Result<UserInfo, AddUserError>.Fail fail:
@* form.SubmitModel.StopSubmitting(fail.Error.ToSentences("\n")); *@ form.SubmitModel.StopSubmitting(fail.Error.ToSentences("\n"));
@* break; *@ break;
@* } *@ }
@* } *@ }
@* *@
@* } *@ }

View File

@ -1,37 +1,37 @@
@* @using Phantom.Common.Data.Web.Users *@ @using Phantom.Common.Data.Web.Users
@* @inherits UserEditDialogBase *@ @inherits UserEditDialogBase
@* @inject UserManager UserManager *@ @inject UserManager UserManager
@* @inject AuditLog AuditLog *@ @inject AuditLog AuditLog
@* *@
@* <Modal Id="@ModalId" TitleText="Delete User"> *@ <Modal Id="@ModalId" TitleText="Delete User">
@* <Body> *@ <Body>
@* You are about to delete the user <strong class="fw-semibold">@EditedUserName</strong>.<br> *@ You are about to delete the user <strong class="fw-semibold">@EditedUserName</strong>.<br>
@* This action cannot be undone. *@ This action cannot be undone.
@* </Body> *@ </Body>
@* <Footer> *@ <Footer>
@* <FormSubmitError Model="SubmitModel" /> *@ <FormSubmitError Model="SubmitModel" />
@* <FormButtonSubmit Model="SubmitModel" Label="Delete User" type="button" class="btn btn-danger" @onclick="Submit" /> *@ <FormButtonSubmit Model="SubmitModel" Label="Delete User" type="button" class="btn btn-danger" @onclick="Submit" />
@* <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="OnClosed">Cancel</button> *@ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="OnClosed">Cancel</button>
@* </Footer> *@ </Footer>
@* </Modal> *@ </Modal>
@* *@
@* @code { *@ @code {
@* *@
@* protected override async Task DoEdit(UserInfo user) { *@ protected override async Task DoEdit(UserInfo user) {
@* switch (await UserManager.DeleteByGuid(user.Guid)) { *@ switch (await UserManager.DeleteByGuid(user.Guid)) {
@* case DeleteUserResult.Deleted: *@ case DeleteUserResult.Deleted:
@* await AuditLog.AddUserDeletedEvent(user); *@ await AuditLog.AddUserDeletedEvent(user);
@* await OnEditSuccess(); *@ await OnEditSuccess();
@* break; *@ break;
@* *@
@* case DeleteUserResult.NotFound: *@ case DeleteUserResult.NotFound:
@* await OnEditSuccess(); *@ await OnEditSuccess();
@* break; *@ break;
@* *@
@* case DeleteUserResult.Failed: *@ case DeleteUserResult.Failed:
@* OnEditFailure("Could not delete user."); *@ OnEditFailure("Could not delete user.");
@* break; *@ break;
@* } *@ }
@* } *@ }
@* *@
@* } *@ }

View File

@ -1,76 +1,76 @@
@* @using Phantom.Common.Data.Web.Users *@ @using Phantom.Common.Data.Web.Users
@* @inherits UserEditDialogBase *@ @inherits UserEditDialogBase
@* *@
@* <Modal Id="@ModalId" TitleText="Manage User Roles"> *@ <Modal Id="@ModalId" TitleText="Manage User Roles">
@* <Body> *@ <Body>
@* Roles for user: <strong class="fw-semibold">@EditedUserName</strong><br> *@ Roles for user: <strong class="fw-semibold">@EditedUserName</strong><br>
@* @for (var index = 0; index < items.Count; index++) { *@ @for (var index = 0; index < items.Count; index++) {
@* var item = items[index]; *@ var item = items[index];
@* <div class="mt-1"> *@ <div class="mt-1">
@* <input id="role-@index" type="checkbox" class="form-check-input" @bind="@item.Checked" /> *@ <input id="role-@index" type="checkbox" class="form-check-input" @bind="@item.Checked" />
@* <label for="role-@index" class="form-check-label">@item.Role.Name</label> *@ <label for="role-@index" class="form-check-label">@item.Role.Name</label>
@* </div> *@ </div>
@* } *@ }
@* </Body> *@ </Body>
@* <Footer> *@ <Footer>
@* <FormSubmitError Model="SubmitModel" /> *@ <FormSubmitError Model="SubmitModel" />
@* <FormButtonSubmit Model="SubmitModel" Label="Save Roles" type="button" class="btn btn-success" @onclick="Submit" /> *@ <FormButtonSubmit Model="SubmitModel" Label="Save Roles" type="button" class="btn btn-success" @onclick="Submit" />
@* <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="OnClosed">Cancel</button> *@ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @onclick="OnClosed">Cancel</button>
@* </Footer> *@ </Footer>
@* </Modal> *@ </Modal>
@* *@
@* @code { *@ @code {
@* *@
@* private List<RoleItem> items = new(); *@ private List<RoleItem> items = new();
@* *@
@* protected override async Task BeforeShown(UserInfo user) { *@ protected override async Task BeforeShown(UserInfo user) {
@* var userRoles = await UserRoleManager.GetUserRoleGuids(user); *@ var userRoles = await UserRoleManager.GetUserRoleGuids(user);
@* var allRoles = await RoleManager.GetAll(); *@ var allRoles = await RoleManager.GetAll();
@* this.items = allRoles.Select(role => new RoleItem(role, userRoles.Contains(role.RoleGuid))).ToList(); *@ this.items = allRoles.Select(role => new RoleItem(role, userRoles.Contains(role.RoleGuid))).ToList();
@* } *@ }
@* *@
@* protected override async Task DoEdit(UserInfo user) { *@ protected override async Task DoEdit(UserInfo user) {
@* var userRoles = await UserRoleManager.GetUserRoleGuids(user); *@ var userRoles = await UserRoleManager.GetUserRoleGuids(user);
@* var addedToRoles = new List<string>(); *@ var addedToRoles = new List<string>();
@* var removedFromRoles = new List<string>(); *@ var removedFromRoles = new List<string>();
@* var errors = new List<string>(); *@ var errors = new List<string>();
@* *@
@* foreach (var item in items) { *@ foreach (var item in items) {
@* var shouldHaveRole = item.Checked; *@ var shouldHaveRole = item.Checked;
@* if (shouldHaveRole == userRoles.Contains(item.Role.Guid)) { *@ if (shouldHaveRole == userRoles.Contains(item.Role.Guid)) {
@* continue; *@ continue;
@* } *@ }
@* *@
@* bool success = shouldHaveRole ? await UserRoleManager.Add(user, item.Role) : await UserRoleManager.Remove(user, item.Role); *@ bool success = shouldHaveRole ? await UserRoleManager.Add(user, item.Role) : await UserRoleManager.Remove(user, item.Role);
@* if (success) { *@ if (success) {
@* var modifiedList = shouldHaveRole ? addedToRoles : removedFromRoles; *@ var modifiedList = shouldHaveRole ? addedToRoles : removedFromRoles;
@* modifiedList.Add(item.Role.Name); *@ modifiedList.Add(item.Role.Name);
@* } *@ }
@* else if (shouldHaveRole) { *@ else if (shouldHaveRole) {
@* errors.Add("Could not add role " + item.Role.Name + " to user."); *@ errors.Add("Could not add role " + item.Role.Name + " to user.");
@* } *@ }
@* else { *@ else {
@* errors.Add("Could not remove role " + item.Role.Name + " from user."); *@ errors.Add("Could not remove role " + item.Role.Name + " from user.");
@* } *@ }
@* } *@ }
@* *@
@* if (errors.Count == 0) { *@ if (errors.Count == 0) {
@* await AuditLog.AddUserRolesChangedEvent(user, addedToRoles, removedFromRoles); *@ await AuditLog.AddUserRolesChangedEvent(user, addedToRoles, removedFromRoles);
@* await OnEditSuccess(); *@ await OnEditSuccess();
@* } *@ }
@* else { *@ else {
@* OnEditFailure(string.Join("\n", errors)); *@ OnEditFailure(string.Join("\n", errors));
@* } *@ }
@* } *@ }
@* *@
@* private sealed class RoleItem { *@ private sealed class RoleItem {
@* public RoleInfo Role { get; } *@ public RoleInfo Role { get; }
@* public bool Checked { get; set; } *@ public bool Checked { get; set; }
@* *@
@* public RoleItem(RoleInfo role, bool @checked) { *@ public RoleItem(RoleInfo role, bool @checked) {
@* this.Role = role; *@ this.Role = role;
@* this.Checked = @checked; *@ this.Checked = @checked;
@* } *@ }
@* } *@ }
@* *@
@* } *@ }

View File

@ -28,7 +28,7 @@ static class WebLauncher {
builder.Services.AddSingleton(taskManager); builder.Services.AddSingleton(taskManager);
builder.Services.AddSingleton(serviceConfiguration); builder.Services.AddSingleton(serviceConfiguration);
builder.Services.AddSingleton(controllerConnection); builder.Services.AddSingleton(controllerConnection);
builder.Services.AddPhantomServices(config.CancellationToken); builder.Services.AddPhantomServices();
builder.Services.AddSingleton<IHostLifetime>(new NullLifetime()); builder.Services.AddSingleton<IHostLifetime>(new NullLifetime());
builder.Services.AddScoped<INavigation>(Navigation.Create(config.BasePath)); builder.Services.AddScoped<INavigation>(Navigation.Create(config.BasePath));