mirror of
https://github.com/chylex/Minecraft-Phantom-Panel.git
synced 2024-11-25 16:42:54 +01:00
Compare commits
2 Commits
149bb6e0f1
...
0df1546fb6
Author | SHA1 | Date | |
---|---|---|---|
0df1546fb6 | |||
d752a482b2 |
@ -18,4 +18,8 @@ public sealed class ControllerConnection {
|
||||
public Task Send<TMessage>(TMessage message) where TMessage : IMessageToController {
|
||||
return connection.Send(message);
|
||||
}
|
||||
|
||||
public Task<TReply?> Send<TMessage, TReply>(TMessage message, TimeSpan waitForReplyTime, CancellationToken waitForReplyCancellationToken) where TMessage : IMessageToController<TReply> where TReply : class {
|
||||
return connection.Send<TMessage, TReply>(message, waitForReplyTime, waitForReplyCancellationToken);
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,14 @@
|
||||
using System.Collections.Immutable;
|
||||
using MemoryPack;
|
||||
|
||||
namespace Phantom.Common.Data.Web.Users;
|
||||
|
||||
[MemoryPackable(GenerateType.VersionTolerant)]
|
||||
public sealed partial class PermissionSet {
|
||||
public static PermissionSet None { get; } = new (ImmutableHashSet<string>.Empty);
|
||||
public sealed class IdentityPermissions {
|
||||
public static IdentityPermissions None { get; } = new (ImmutableHashSet<string>.Empty);
|
||||
|
||||
[MemoryPackOrder(0)]
|
||||
[MemoryPackInclude]
|
||||
private readonly ImmutableHashSet<string> permissionIds;
|
||||
|
||||
public PermissionSet(ImmutableHashSet<string> permissionIds) {
|
||||
this.permissionIds = permissionIds;
|
||||
public IdentityPermissions(ImmutableHashSet<string> permissionIdsQuery) {
|
||||
this.permissionIds = permissionIdsQuery;
|
||||
}
|
||||
|
||||
public bool Check(Permission? permission) {
|
@ -1,11 +0,0 @@
|
||||
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
|
||||
);
|
@ -8,6 +8,6 @@ namespace Phantom.Common.Messages.Web;
|
||||
public interface IMessageToControllerListener {
|
||||
Task<NoReply> HandleRegisterWeb(RegisterWebMessage message);
|
||||
Task<CreateOrUpdateAdministratorUserResult> CreateOrUpdateAdministratorUser(CreateOrUpdateAdministratorUser message);
|
||||
Task<LogInSuccess?> HandleLogIn(LogIn message);
|
||||
Task<byte[]?> HandleLogIn(LogIn message);
|
||||
Task<NoReply> HandleReply(ReplyMessage message);
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
using MemoryPack;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
|
||||
namespace Phantom.Common.Messages.Web.ToController;
|
||||
|
||||
@ -7,8 +6,8 @@ namespace Phantom.Common.Messages.Web.ToController;
|
||||
public sealed partial record LogIn(
|
||||
[property: MemoryPackOrder(0)] string Username,
|
||||
[property: MemoryPackOrder(1)] string Password
|
||||
) : IMessageToController<LogInSuccess?> {
|
||||
public Task<LogInSuccess?> Accept(IMessageToControllerListener listener) {
|
||||
) : IMessageToController<byte[]?> {
|
||||
public Task<byte[]?> Accept(IMessageToControllerListener listener) {
|
||||
return listener.HandleLogIn(this);
|
||||
}
|
||||
};
|
||||
|
@ -16,7 +16,6 @@ public static class WebMessageRegistries {
|
||||
static WebMessageRegistries() {
|
||||
ToController.Add<RegisterWebMessage>(0);
|
||||
ToController.Add<CreateOrUpdateAdministratorUser, CreateOrUpdateAdministratorUserResult>(1);
|
||||
ToController.Add<LogIn, LogInSuccess?>(2);
|
||||
ToController.Add<ReplyMessage>(127);
|
||||
|
||||
ToWeb.Add<RegisterWebResultMessage>(0);
|
||||
|
@ -66,7 +66,7 @@ public sealed class RpcConnectionToClient<TListener> {
|
||||
}
|
||||
|
||||
await socket.SendAsync(routingId, bytes);
|
||||
return await messageReplyTracker.TryWaitForReply<TReply>(sequenceId, waitForReplyTime, waitForReplyCancellationToken);
|
||||
return await messageReplyTracker.WaitForReply<TReply>(sequenceId, waitForReplyTime, waitForReplyCancellationToken);
|
||||
}
|
||||
|
||||
public void Receive(IReply message) {
|
||||
|
@ -48,7 +48,7 @@ public sealed class ControllerServices {
|
||||
this.RoleManager = new RoleManager(dbProvider);
|
||||
this.PermissionManager = new PermissionManager(dbProvider);
|
||||
|
||||
this.UserLoginManager = new UserLoginManager(UserManager, PermissionManager);
|
||||
this.UserLoginManager = new UserLoginManager(UserManager);
|
||||
|
||||
this.dbProvider = dbProvider;
|
||||
this.webAuthToken = webAuthToken;
|
||||
|
@ -45,7 +45,7 @@ public sealed class WebMessageListener : IMessageToControllerListener {
|
||||
return userManager.CreateOrUpdateAdministrator(message.Username, message.Password);
|
||||
}
|
||||
|
||||
public Task<LogInSuccess?> HandleLogIn(LogIn message) {
|
||||
public Task<byte[]?> HandleLogIn(LogIn message) {
|
||||
return userLoginManager.LogIn(message.Username, message.Password);
|
||||
}
|
||||
|
||||
|
@ -9,48 +9,60 @@ using Serilog;
|
||||
|
||||
namespace Phantom.Controller.Services.Users;
|
||||
|
||||
sealed class PermissionManager {
|
||||
public sealed class PermissionManager {
|
||||
private static readonly ILogger Logger = PhantomLogger.Create<PermissionManager>();
|
||||
|
||||
|
||||
private readonly IDbContextProvider dbProvider;
|
||||
private readonly Dictionary<Guid, IdentityPermissions> userIdsToPermissionIds = new ();
|
||||
|
||||
public PermissionManager(IDbContextProvider dbProvider) {
|
||||
this.dbProvider = dbProvider;
|
||||
}
|
||||
|
||||
public async Task Initialize() {
|
||||
internal async Task Initialize() {
|
||||
Logger.Information("Adding default permissions to database.");
|
||||
|
||||
|
||||
await using var ctx = dbProvider.Eager();
|
||||
|
||||
|
||||
var existingPermissionIds = await ctx.Permissions.Select(static p => p.Id).AsAsyncEnumerable().ToImmutableSetAsync();
|
||||
var missingPermissionIds = GetMissingPermissionsOrdered(Permission.All, existingPermissionIds);
|
||||
if (!missingPermissionIds.IsEmpty) {
|
||||
Logger.Information("Adding default permissions: {Permissions}", string.Join(", ", missingPermissionIds));
|
||||
|
||||
|
||||
foreach (var permissionId in missingPermissionIds) {
|
||||
ctx.Permissions.Add(new PermissionEntity(permissionId));
|
||||
}
|
||||
|
||||
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<PermissionSet> 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) {
|
||||
|
||||
internal static ImmutableArray<string> GetMissingPermissionsOrdered(IEnumerable<Permission> allPermissions, ImmutableHashSet<string> existingPermissionIds) {
|
||||
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);
|
||||
// }
|
||||
}
|
||||
|
@ -1,34 +1,30 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.Security.Cryptography;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
|
||||
namespace Phantom.Controller.Services.Users;
|
||||
|
||||
sealed class UserLoginManager {
|
||||
private const int SessionIdBytes = 20;
|
||||
private readonly ConcurrentDictionary<string, List<ImmutableArray<byte>>> sessionTokensByUsername = new ();
|
||||
private readonly ConcurrentDictionary<string, List<byte[]>> sessionTokensByUsername = new ();
|
||||
|
||||
private readonly UserManager userManager;
|
||||
private readonly PermissionManager permissionManager;
|
||||
|
||||
public UserLoginManager(UserManager userManager, PermissionManager permissionManager) {
|
||||
public UserLoginManager(UserManager userManager) {
|
||||
this.userManager = userManager;
|
||||
this.permissionManager = permissionManager;
|
||||
}
|
||||
|
||||
public async Task<LogInSuccess?> LogIn(string username, string password) {
|
||||
public async Task<byte[]?> LogIn(string username, string password) {
|
||||
var user = await userManager.GetAuthenticated(username, password);
|
||||
if (user == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var token = ImmutableArray.Create(RandomNumberGenerator.GetBytes(SessionIdBytes));
|
||||
var sessionTokens = sessionTokensByUsername.GetOrAdd(username, static _ => new List<ImmutableArray<byte>>());
|
||||
var token = RandomNumberGenerator.GetBytes(SessionIdBytes);
|
||||
var sessionTokens = sessionTokensByUsername.GetOrAdd(username, static _ => new List<byte[]>());
|
||||
lock (sessionTokens) {
|
||||
sessionTokens.Add(token);
|
||||
}
|
||||
|
||||
return new LogInSuccess(user.UserGuid, await permissionManager.FetchPermissionsForUserId(user.UserGuid), token);
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
@ -20,36 +20,26 @@ public sealed class MessageReplyTracker {
|
||||
return sequenceId;
|
||||
}
|
||||
|
||||
public async Task<TReply> WaitForReply<TReply>(uint sequenceId, TimeSpan waitForReplyTime, CancellationToken cancellationToken) {
|
||||
public async Task<TReply?> WaitForReply<TReply>(uint sequenceId, TimeSpan waitForReplyTime, CancellationToken cancellationToken) where TReply : class {
|
||||
if (!replyTasks.TryGetValue(sequenceId, out var completionSource)) {
|
||||
logger.Warning("No reply callback for id {SequenceId}.", sequenceId);
|
||||
throw new ArgumentException("No reply callback for id: " + sequenceId, nameof(sequenceId));
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] replyBytes = await completionSource.Task.WaitAsync(waitForReplyTime, cancellationToken);
|
||||
return MessageSerializer.Deserialize<TReply>(replyBytes);
|
||||
} catch (TimeoutException) {
|
||||
logger.Debug("Timed out waiting for reply with id {SequenceId}.", sequenceId);
|
||||
throw;
|
||||
return null;
|
||||
} catch (OperationCanceledException) {
|
||||
logger.Debug("Cancelled waiting for reply with id {SequenceId}.", sequenceId);
|
||||
throw;
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
logger.Warning(e, "Error processing reply with id {SequenceId}.", sequenceId);
|
||||
throw;
|
||||
return null;
|
||||
} finally {
|
||||
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) {
|
||||
if (replyTasks.TryRemove(sequenceId, out var task)) {
|
||||
|
@ -22,7 +22,7 @@ public sealed class RpcConnectionToServer<TListener> {
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<TReply?> TrySend<TMessage, TReply>(TMessage message, TimeSpan waitForReplyTime, CancellationToken waitForReplyCancellationToken) where TMessage : IMessage<TListener, TReply> where TReply : class {
|
||||
public async Task<TReply?> Send<TMessage, TReply>(TMessage message, TimeSpan waitForReplyTime, CancellationToken waitForReplyCancellationToken) where TMessage : IMessage<TListener, TReply> where TReply : class {
|
||||
var sequenceId = replyTracker.RegisterReply();
|
||||
|
||||
var bytes = messageRegistry.Write<TMessage, TReply>(sequenceId, message).ToArray();
|
||||
@ -31,19 +31,6 @@ public sealed class RpcConnectionToServer<TListener> {
|
||||
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);
|
||||
return await replyTracker.WaitForReply<TReply>(sequenceId, waitForReplyTime, waitForReplyCancellationToken);
|
||||
}
|
||||
|
@ -1,90 +0,0 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Server;
|
||||
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
|
||||
using Phantom.Common.Logging;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace Phantom.Web.Services.Authentication;
|
||||
|
||||
public sealed class PhantomAuthenticationStateProvider : ServerAuthenticationStateProvider {
|
||||
private const string SessionTokenKey = "PhantomSession";
|
||||
|
||||
private static readonly ILogger Logger = PhantomLogger.Create<PhantomAuthenticationStateProvider>();
|
||||
|
||||
private readonly PhantomLoginSessions loginSessions;
|
||||
private readonly ProtectedLocalStorage localStorage;
|
||||
private bool isLoaded;
|
||||
|
||||
public PhantomAuthenticationStateProvider(PhantomLoginSessions loginSessions, ProtectedLocalStorage localStorage) {
|
||||
this.loginSessions = loginSessions;
|
||||
this.localStorage = localStorage;
|
||||
}
|
||||
|
||||
public override async Task<AuthenticationState> GetAuthenticationStateAsync() {
|
||||
if (isLoaded) {
|
||||
return await base.GetAuthenticationStateAsync();
|
||||
}
|
||||
|
||||
LocalStorageEntry? stored;
|
||||
try {
|
||||
stored = await GetLocalStorageEntry();
|
||||
} catch (InvalidOperationException) {
|
||||
stored = null;
|
||||
}
|
||||
|
||||
if (stored != null) {
|
||||
var session = loginSessions.Find(stored.UserGuid, stored.Token);
|
||||
if (session != null) {
|
||||
SetLoadedSession(session);
|
||||
}
|
||||
}
|
||||
|
||||
return await base.GetAuthenticationStateAsync();
|
||||
}
|
||||
|
||||
private sealed record LocalStorageEntry(Guid UserGuid, ImmutableArray<byte> Token);
|
||||
|
||||
private async Task<LocalStorageEntry?> GetLocalStorageEntry() {
|
||||
try {
|
||||
var result = await localStorage.GetAsync<LocalStorageEntry>(SessionTokenKey);
|
||||
return result.Success ? result.Value : null;
|
||||
} catch (InvalidOperationException) {
|
||||
return null;
|
||||
} catch (CryptographicException) {
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
Logger.Error(e, "Could not read local storage entry.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetLoadedSession(UserSession session) {
|
||||
isLoaded = true;
|
||||
SetAuthenticationState(Task.FromResult(new AuthenticationState(session.AsClaimsPrincipal)));
|
||||
}
|
||||
|
||||
internal async Task HandleLogin(UserSession session) {
|
||||
await localStorage.SetAsync(SessionTokenKey, new LocalStorageEntry(session.UserGuid, session.Token));
|
||||
loginSessions.Add(session);
|
||||
SetLoadedSession(session);
|
||||
}
|
||||
|
||||
internal async Task HandleLogout() {
|
||||
if (!isLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
await localStorage.DeleteAsync(SessionTokenKey);
|
||||
|
||||
var stored = await GetLocalStorageEntry();
|
||||
if (stored != null) {
|
||||
loginSessions.Remove(stored.UserGuid, stored.Token);
|
||||
}
|
||||
|
||||
isLoaded = false;
|
||||
SetAuthenticationState(Task.FromResult(new AuthenticationState(new ClaimsPrincipal())));
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
using System.Security.Claims;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
|
||||
using Phantom.Common.Logging;
|
||||
using Phantom.Common.Messages.Web.ToController;
|
||||
using Phantom.Web.Services.Rpc;
|
||||
using Phantom.Utils.Cryptography;
|
||||
using ILogger = Serilog.ILogger;
|
||||
|
||||
namespace Phantom.Web.Services.Authentication;
|
||||
@ -10,44 +10,66 @@ namespace Phantom.Web.Services.Authentication;
|
||||
public sealed class PhantomLoginManager {
|
||||
private static readonly ILogger Logger = PhantomLogger.Create<PhantomLoginManager>();
|
||||
|
||||
public const string AuthenticationType = "Phantom";
|
||||
public static bool IsAuthenticated(ClaimsPrincipal user) {
|
||||
return user.Identity is { IsAuthenticated: true };
|
||||
}
|
||||
|
||||
private readonly INavigation navigation;
|
||||
private readonly PhantomAuthenticationStateProvider authenticationStateProvider;
|
||||
private readonly ControllerConnection controllerConnection;
|
||||
private readonly PhantomLoginStore loginStore;
|
||||
private readonly ProtectedSessionStorage sessionStorage;
|
||||
|
||||
public PhantomLoginManager(INavigation navigation, PhantomAuthenticationStateProvider authenticationStateProvider, ControllerConnection controllerConnection) {
|
||||
public PhantomLoginManager(INavigation navigation, PhantomLoginStore loginStore, ProtectedSessionStorage sessionStorage) {
|
||||
this.navigation = navigation;
|
||||
this.authenticationStateProvider = authenticationStateProvider;
|
||||
this.controllerConnection = controllerConnection;
|
||||
this.loginStore = loginStore;
|
||||
this.sessionStorage = sessionStorage;
|
||||
}
|
||||
|
||||
public async Task<bool> SignIn(string username, string password, string? returnUrl = null) {
|
||||
LogInSuccess? success;
|
||||
try {
|
||||
success = await controllerConnection.Send<LogIn, LogInSuccess?>(new LogIn(username, password), TimeSpan.FromSeconds(30));
|
||||
} catch (Exception e) {
|
||||
Logger.Error(e, "Could not log in {Username}.", username);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (success == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger.Information("Successfully logged in {Username}.", username);
|
||||
return false;
|
||||
// if (await userManager.GetAuthenticated(username, password) == null) {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
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);
|
||||
Logger.Debug("Created login token for {Username}.", username);
|
||||
|
||||
string token = TokenGenerator.Create(60);
|
||||
loginStore.Add(token, username, password, returnUrl ?? string.Empty);
|
||||
navigation.NavigateTo("login" + QueryString.Create("token", token), forceLoad: true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task SignOut() {
|
||||
await navigation.NavigateTo(string.Empty);
|
||||
await authenticationStateProvider.HandleLogout();
|
||||
internal async Task<SignInResult?> ProcessToken(string token) {
|
||||
return null;
|
||||
// var entry = loginStore.Pop(token);
|
||||
// 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);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
@ -1,49 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
62
Web/Phantom.Web.Services/Authentication/PhantomLoginStore.cs
Normal file
62
Web/Phantom.Web.Services/Authentication/PhantomLoginStore.cs
Normal file
@ -0,0 +1,62 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
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());
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ namespace Phantom.Web.Services.Authorization;
|
||||
// TODO
|
||||
public class PermissionManager {
|
||||
public IdentityPermissions GetPermissions(ClaimsPrincipal user, bool refreshCache = false) {
|
||||
|
||||
return IdentityPermissions.None;
|
||||
}
|
||||
|
||||
public bool CheckPermission(ClaimsPrincipal user, Permission permission, bool refreshCache = false) {
|
||||
|
@ -5,5 +5,5 @@ namespace Phantom.Web.Services;
|
||||
public interface INavigation {
|
||||
string BasePath { get; }
|
||||
bool GetQueryParameter(string key, [MaybeNullWhen(false)] out string value);
|
||||
Task NavigateTo(string url, bool forceLoad = false);
|
||||
void NavigateTo(string url, bool forceLoad = false);
|
||||
}
|
||||
|
@ -1,10 +1,8 @@
|
||||
using Phantom.Common.Data.Replies;
|
||||
|
||||
namespace Phantom.Web.Services.Instances;
|
||||
namespace Phantom.Web.Services.Instances;
|
||||
|
||||
// TODO
|
||||
public class InstanceManager {
|
||||
public async Task<InstanceActionResult<SendCommandToInstanceResult>> SendCommand(Guid instanceGuid, string command) {
|
||||
|
||||
}
|
||||
// public async Task<InstanceActionResult<SendCommandToInstanceResult>> SendCommand(Guid instanceGuid, string command) {
|
||||
//
|
||||
// }
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Server;
|
||||
using Phantom.Common.Data.Web.Users;
|
||||
using Phantom.Web.Services.Authentication;
|
||||
using Phantom.Web.Services.Authorization;
|
||||
@ -8,23 +9,23 @@ using Phantom.Web.Services.Rpc;
|
||||
namespace Phantom.Web.Services;
|
||||
|
||||
public static class PhantomWebServices {
|
||||
public static void AddPhantomServices(this IServiceCollection services) {
|
||||
public static void AddPhantomServices(this IServiceCollection services, CancellationToken cancellationToken) {
|
||||
services.AddSingleton<MessageListener>();
|
||||
services.AddSingleton<ControllerConnection>();
|
||||
services.AddSingleton<ControllerCommunication>();
|
||||
services.AddSingleton<PermissionManager>();
|
||||
|
||||
services.AddSingleton<PhantomLoginSessions>();
|
||||
services.AddSingleton(PhantomLoginStore.Create(cancellationToken));
|
||||
services.AddScoped<PhantomLoginManager>();
|
||||
services.AddScoped<PhantomAuthenticationStateProvider>();
|
||||
services.AddScoped<AuthenticationStateProvider>(static services => services.GetRequiredService<PhantomAuthenticationStateProvider>());
|
||||
services.AddScoped<IHostEnvironmentAuthenticationStateProvider>(static services => services.GetRequiredService<PhantomAuthenticationStateProvider>());
|
||||
|
||||
services.AddAuthorization(ConfigureAuthorization);
|
||||
services.AddScoped<IAuthorizationHandler, PermissionBasedPolicyHandler>();
|
||||
services.AddScoped<AuthenticationStateProvider, ServerAuthenticationStateProvider>();
|
||||
}
|
||||
|
||||
public static void UsePhantomServices(this IApplicationBuilder application) {
|
||||
application.UseAuthentication();
|
||||
application.UseAuthorization();
|
||||
application.UseWhen(PhantomIdentityMiddleware.AcceptsPath, static app => app.UseMiddleware<PhantomIdentityMiddleware>());
|
||||
}
|
||||
|
||||
private static void ConfigureAuthorization(AuthorizationOptions o) {
|
||||
|
@ -3,10 +3,10 @@ using Phantom.Utils.Rpc;
|
||||
|
||||
namespace Phantom.Web.Services.Rpc;
|
||||
|
||||
public sealed class ControllerConnection {
|
||||
public sealed class ControllerCommunication {
|
||||
private readonly RpcConnectionToServer<IMessageToControllerListener> connection;
|
||||
|
||||
public ControllerConnection(RpcConnectionToServer<IMessageToControllerListener> connection) {
|
||||
public ControllerCommunication(RpcConnectionToServer<IMessageToControllerListener> connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ public sealed class ControllerConnection {
|
||||
return connection.Send(message);
|
||||
}
|
||||
|
||||
public Task<TReply> Send<TMessage, TReply>(TMessage message, TimeSpan timeout) where TMessage : IMessageToController<TReply> {
|
||||
public Task<TReply?> Send<TMessage, TReply>(TMessage message, TimeSpan timeout) where TMessage : IMessageToController<TReply> where TReply : class {
|
||||
return connection.Send<TMessage, TReply>(message, timeout, CancellationToken.None);
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
@using Phantom.Web.Services
|
||||
@using Phantom.Web.Services.Authentication
|
||||
@inject INavigation Nav
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
@ -7,14 +8,14 @@
|
||||
<Found Context="routeData">
|
||||
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
|
||||
<NotAuthorized>
|
||||
@if (context.User.Identity is { IsAuthenticated: true }) {
|
||||
<h1>Forbidden</h1>
|
||||
<p role="alert">You do not have permission to visit this page.</p>
|
||||
}
|
||||
else {
|
||||
@if (!PhantomLoginManager.IsAuthenticated(context.User)) {
|
||||
var returnUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri).TrimEnd('/');
|
||||
Nav.NavigateTo("login" + QueryString.Create("return", returnUrl), forceLoad: true);
|
||||
}
|
||||
else {
|
||||
<h1>Forbidden</h1>
|
||||
<p role="alert">You do not have permission to visit this page.</p>
|
||||
}
|
||||
</NotAuthorized>
|
||||
</AuthorizeRouteView>
|
||||
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
||||
|
@ -1,7 +1,6 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Web;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Routing;
|
||||
using Phantom.Web.Services;
|
||||
|
||||
namespace Phantom.Web.Base;
|
||||
@ -28,24 +27,7 @@ sealed class Navigation : INavigation {
|
||||
return value != null;
|
||||
}
|
||||
|
||||
public async Task NavigateTo(string url, bool forceLoad = false) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
public void NavigateTo(string url, bool forceLoad = false) {
|
||||
navigationManager.NavigateTo(BasePath + url, forceLoad);
|
||||
}
|
||||
}
|
||||
|
@ -1,90 +1,90 @@
|
||||
@page "/agents"
|
||||
@using Phantom.Utils.Collections
|
||||
@implements IDisposable
|
||||
@inject AgentManager AgentManager
|
||||
|
||||
<h1>Agents</h1>
|
||||
|
||||
<table class="table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<Column Width="200px; 44%">Name</Column>
|
||||
<Column Width=" 90px; 19%" Class="text-end">Instances</Column>
|
||||
<Column Width="145px; 21%" Class="text-end">Memory</Column>
|
||||
<Column Width="180px; 8%">Version</Column>
|
||||
<Column Width="320px">Identifier</Column>
|
||||
<Column Width="100px; 8%" Class="text-center">Status</Column>
|
||||
<Column Width="215px" Class="text-end">Last Ping</Column>
|
||||
</tr>
|
||||
</thead>
|
||||
@if (!agentTable.IsEmpty) {
|
||||
<tbody>
|
||||
@foreach (var agent in agentTable) {
|
||||
var usedInstances = agent.Stats?.RunningInstanceCount;
|
||||
var usedMemory = agent.Stats?.RunningInstanceMemory.InMegabytes;
|
||||
|
||||
<tr>
|
||||
<td>@agent.Name</td>
|
||||
<td class="text-end">
|
||||
<ProgressBar Value="@(usedInstances ?? 0)" Maximum="@agent.MaxInstances">
|
||||
@(usedInstances?.ToString() ?? "?") / @agent.MaxInstances
|
||||
</ProgressBar>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<ProgressBar Value="@(usedMemory ?? 0)" Maximum="@agent.MaxMemory.InMegabytes">
|
||||
@(usedMemory?.ToString() ?? "?") / @agent.MaxMemory.InMegabytes MB
|
||||
</ProgressBar>
|
||||
</td>
|
||||
<td class="text-condensed">
|
||||
Build: <code>@agent.BuildVersion</code>
|
||||
<br>
|
||||
Protocol: <code>v@(agent.ProtocolVersion)</code>
|
||||
</td>
|
||||
<td>
|
||||
<code class="text-uppercase">@agent.Guid.ToString()</code>
|
||||
</td>
|
||||
@if (agent.IsOnline) {
|
||||
<td class="text-center text-success">Online</td>
|
||||
<td class="text-end"></td>
|
||||
}
|
||||
else {
|
||||
<td class="text-center text-danger">Offline</td>
|
||||
@if (agent.LastPing is {} lastPing) {
|
||||
<td class="text-end">
|
||||
<time datetime="@lastPing.ToString("o")" data-time-type="relative">@lastPing.ToString()</time>
|
||||
</td>
|
||||
}
|
||||
else {
|
||||
<td class="text-end">-</td>
|
||||
}
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
}
|
||||
else {
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="7">No agents registered.</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
}
|
||||
</table>
|
||||
|
||||
@code {
|
||||
|
||||
private readonly Table<Agent, Guid> agentTable = new();
|
||||
|
||||
protected override void OnInitialized() {
|
||||
AgentManager.AgentsChanged.Subscribe(this, agents => {
|
||||
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);
|
||||
InvokeAsync(StateHasChanged);
|
||||
});
|
||||
}
|
||||
|
||||
void IDisposable.Dispose() {
|
||||
AgentManager.AgentsChanged.Unsubscribe(this);
|
||||
}
|
||||
|
||||
}
|
||||
@* @page "/agents" *@
|
||||
@* @using Phantom.Utils.Collections *@
|
||||
@* @implements IDisposable *@
|
||||
@* @inject AgentManager AgentManager *@
|
||||
@* *@
|
||||
@* <h1>Agents</h1> *@
|
||||
@* *@
|
||||
@* <table class="table align-middle"> *@
|
||||
@* <thead> *@
|
||||
@* <tr> *@
|
||||
@* <Column Width="200px; 44%">Name</Column> *@
|
||||
@* <Column Width=" 90px; 19%" Class="text-end">Instances</Column> *@
|
||||
@* <Column Width="145px; 21%" Class="text-end">Memory</Column> *@
|
||||
@* <Column Width="180px; 8%">Version</Column> *@
|
||||
@* <Column Width="320px">Identifier</Column> *@
|
||||
@* <Column Width="100px; 8%" Class="text-center">Status</Column> *@
|
||||
@* <Column Width="215px" Class="text-end">Last Ping</Column> *@
|
||||
@* </tr> *@
|
||||
@* </thead> *@
|
||||
@* @if (!agentTable.IsEmpty) { *@
|
||||
@* <tbody> *@
|
||||
@* @foreach (var agent in agentTable) { *@
|
||||
@* var usedInstances = agent.Stats?.RunningInstanceCount; *@
|
||||
@* var usedMemory = agent.Stats?.RunningInstanceMemory.InMegabytes; *@
|
||||
@* *@
|
||||
@* <tr> *@
|
||||
@* <td>@agent.Name</td> *@
|
||||
@* <td class="text-end"> *@
|
||||
@* <ProgressBar Value="@(usedInstances ?? 0)" Maximum="@agent.MaxInstances"> *@
|
||||
@* @(usedInstances?.ToString() ?? "?") / @agent.MaxInstances *@
|
||||
@* </ProgressBar> *@
|
||||
@* </td> *@
|
||||
@* <td class="text-end"> *@
|
||||
@* <ProgressBar Value="@(usedMemory ?? 0)" Maximum="@agent.MaxMemory.InMegabytes"> *@
|
||||
@* @(usedMemory?.ToString() ?? "?") / @agent.MaxMemory.InMegabytes MB *@
|
||||
@* </ProgressBar> *@
|
||||
@* </td> *@
|
||||
@* <td class="text-condensed"> *@
|
||||
@* Build: <code>@agent.BuildVersion</code> *@
|
||||
@* <br> *@
|
||||
@* Protocol: <code>v@(agent.ProtocolVersion)</code> *@
|
||||
@* </td> *@
|
||||
@* <td> *@
|
||||
@* <code class="text-uppercase">@agent.Guid.ToString()</code> *@
|
||||
@* </td> *@
|
||||
@* @if (agent.IsOnline) { *@
|
||||
@* <td class="text-center text-success">Online</td> *@
|
||||
@* <td class="text-end"></td> *@
|
||||
@* } *@
|
||||
@* else { *@
|
||||
@* <td class="text-center text-danger">Offline</td> *@
|
||||
@* @if (agent.LastPing is {} lastPing) { *@
|
||||
@* <td class="text-end"> *@
|
||||
@* <time datetime="@lastPing.ToString("o")" data-time-type="relative">@lastPing.ToString()</time> *@
|
||||
@* </td> *@
|
||||
@* } *@
|
||||
@* else { *@
|
||||
@* <td class="text-end">-</td> *@
|
||||
@* } *@
|
||||
@* } *@
|
||||
@* </tr> *@
|
||||
@* } *@
|
||||
@* </tbody> *@
|
||||
@* } *@
|
||||
@* else { *@
|
||||
@* <tfoot> *@
|
||||
@* <tr> *@
|
||||
@* <td colspan="7">No agents registered.</td> *@
|
||||
@* </tr> *@
|
||||
@* </tfoot> *@
|
||||
@* } *@
|
||||
@* </table> *@
|
||||
@* *@
|
||||
@* @code { *@
|
||||
@* *@
|
||||
@* private readonly Table<Agent, Guid> agentTable = new(); *@
|
||||
@* *@
|
||||
@* protected override void OnInitialized() { *@
|
||||
@* AgentManager.AgentsChanged.Subscribe(this, agents => { *@
|
||||
@* 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); *@
|
||||
@* InvokeAsync(StateHasChanged); *@
|
||||
@* }); *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* void IDisposable.Dispose() { *@
|
||||
@* AgentManager.AgentsChanged.Unsubscribe(this); *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* } *@
|
||||
|
@ -1,86 +1,86 @@
|
||||
@page "/audit"
|
||||
@attribute [Authorize(Permission.ViewAuditPolicy)]
|
||||
@using Phantom.Common.Data.Web.AuditLog
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@using System.Collections.Immutable
|
||||
@using Phantom.Web.Services.Instances
|
||||
@implements IDisposable
|
||||
@inject AuditLog AuditLog
|
||||
@inject InstanceManager InstanceManager
|
||||
@inject UserManager UserManager
|
||||
|
||||
<h1>Audit Log</h1>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<Column Width="165px" Class="text-end">Time</Column>
|
||||
<Column Width="320px; 20%">User</Column>
|
||||
<Column Width="160px">Event Type</Column>
|
||||
<Column Width="320px; 20%">Subject</Column>
|
||||
<Column Width="100px; 60%">Data</Column>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var logItem in logItems) {
|
||||
DateTimeOffset time = logItem.UtcTime.ToLocalTime();
|
||||
<tr>
|
||||
<td class="text-end">
|
||||
<time datetime="@time.ToString("o")">@time.ToString()</time>
|
||||
</td>
|
||||
<td>
|
||||
@(logItem.UserName ?? "-")
|
||||
<br>
|
||||
<code class="text-uppercase">@logItem.UserGuid</code>
|
||||
</td>
|
||||
<td>@logItem.EventType.ToNiceString()</td>
|
||||
<td>
|
||||
@if (logItem.SubjectId is {} subjectId && GetSubjectName(logItem.SubjectType, subjectId) is {} subjectName) {
|
||||
@subjectName
|
||||
<br>
|
||||
}
|
||||
<code class="text-uppercase">@(logItem.SubjectId ?? "-")</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>@logItem.Data?.RootElement.ToString()</code>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@code {
|
||||
|
||||
private CancellationTokenSource? initializationCancellationTokenSource;
|
||||
private AuditLogItem[] logItems = Array.Empty<AuditLogItem>();
|
||||
private Dictionary<Guid, string>? userNamesByGuid;
|
||||
private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
initializationCancellationTokenSource = new CancellationTokenSource();
|
||||
var cancellationToken = initializationCancellationTokenSource.Token;
|
||||
|
||||
try {
|
||||
logItems = await AuditLog.GetItems(50, cancellationToken);
|
||||
userNamesByGuid = await UserManager.GetAllByGuid(static user => user.Name, cancellationToken);
|
||||
instanceNamesByGuid = InstanceManager.GetInstanceNames();
|
||||
} finally {
|
||||
initializationCancellationTokenSource.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private string? GetSubjectName(AuditLogSubjectType type, string id) {
|
||||
return type switch {
|
||||
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,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
try {
|
||||
initializationCancellationTokenSource?.Cancel();
|
||||
} catch (ObjectDisposedException) {}
|
||||
}
|
||||
|
||||
}
|
||||
@* @page "/audit" *@
|
||||
@* @attribute [Authorize(Permission.ViewAuditPolicy)] *@
|
||||
@* @using Phantom.Common.Data.Web.AuditLog *@
|
||||
@* @using Phantom.Common.Data.Web.Users *@
|
||||
@* @using System.Collections.Immutable *@
|
||||
@* @using Phantom.Web.Services.Instances *@
|
||||
@* @implements IDisposable *@
|
||||
@* @inject AuditLog AuditLog *@
|
||||
@* @inject InstanceManager InstanceManager *@
|
||||
@* @inject UserManager UserManager *@
|
||||
@* *@
|
||||
@* <h1>Audit Log</h1> *@
|
||||
@* *@
|
||||
@* <table class="table"> *@
|
||||
@* <thead> *@
|
||||
@* <tr> *@
|
||||
@* <Column Width="165px" Class="text-end">Time</Column> *@
|
||||
@* <Column Width="320px; 20%">User</Column> *@
|
||||
@* <Column Width="160px">Event Type</Column> *@
|
||||
@* <Column Width="320px; 20%">Subject</Column> *@
|
||||
@* <Column Width="100px; 60%">Data</Column> *@
|
||||
@* </tr> *@
|
||||
@* </thead> *@
|
||||
@* <tbody> *@
|
||||
@* @foreach (var logItem in logItems) { *@
|
||||
@* DateTimeOffset time = logItem.UtcTime.ToLocalTime(); *@
|
||||
@* <tr> *@
|
||||
@* <td class="text-end"> *@
|
||||
@* <time datetime="@time.ToString("o")">@time.ToString()</time> *@
|
||||
@* </td> *@
|
||||
@* <td> *@
|
||||
@* @(logItem.UserName ?? "-") *@
|
||||
@* <br> *@
|
||||
@* <code class="text-uppercase">@logItem.UserGuid</code> *@
|
||||
@* </td> *@
|
||||
@* <td>@logItem.EventType.ToNiceString()</td> *@
|
||||
@* <td> *@
|
||||
@* @if (logItem.SubjectId is {} subjectId && GetSubjectName(logItem.SubjectType, subjectId) is {} subjectName) { *@
|
||||
@* @subjectName *@
|
||||
@* <br> *@
|
||||
@* } *@
|
||||
@* <code class="text-uppercase">@(logItem.SubjectId ?? "-")</code> *@
|
||||
@* </td> *@
|
||||
@* <td> *@
|
||||
@* <code>@logItem.Data?.RootElement.ToString()</code> *@
|
||||
@* </td> *@
|
||||
@* </tr> *@
|
||||
@* } *@
|
||||
@* </tbody> *@
|
||||
@* </table> *@
|
||||
@* *@
|
||||
@* @code { *@
|
||||
@* *@
|
||||
@* private CancellationTokenSource? initializationCancellationTokenSource; *@
|
||||
@* private AuditLogItem[] logItems = Array.Empty<AuditLogItem>(); *@
|
||||
@* private Dictionary<Guid, string>? userNamesByGuid; *@
|
||||
@* private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty; *@
|
||||
@* *@
|
||||
@* protected override async Task OnInitializedAsync() { *@
|
||||
@* initializationCancellationTokenSource = new CancellationTokenSource(); *@
|
||||
@* var cancellationToken = initializationCancellationTokenSource.Token; *@
|
||||
@* *@
|
||||
@* try { *@
|
||||
@* logItems = await AuditLog.GetItems(50, cancellationToken); *@
|
||||
@* userNamesByGuid = await UserManager.GetAllByGuid(static user => user.Name, cancellationToken); *@
|
||||
@* instanceNamesByGuid = InstanceManager.GetInstanceNames(); *@
|
||||
@* } finally { *@
|
||||
@* initializationCancellationTokenSource.Dispose(); *@
|
||||
@* } *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* private string? GetSubjectName(AuditLogSubjectType type, string id) { *@
|
||||
@* return type switch { *@
|
||||
@* 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, *@
|
||||
@* _ => null *@
|
||||
@* }; *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* public void Dispose() { *@
|
||||
@* try { *@
|
||||
@* initializationCancellationTokenSource?.Cancel(); *@
|
||||
@* } catch (ObjectDisposedException) {} *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* } *@
|
||||
|
@ -1,95 +1,95 @@
|
||||
@page "/events"
|
||||
@attribute [Authorize(Permission.ViewEventsPolicy)]
|
||||
@using Phantom.Common.Data.Web.EventLog
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@using System.Collections.Immutable
|
||||
@using System.Diagnostics
|
||||
@using Phantom.Web.Services.Instances
|
||||
@implements IDisposable
|
||||
@inject AgentManager AgentManager
|
||||
@inject EventLog EventLog
|
||||
@inject InstanceManager InstanceManager
|
||||
|
||||
<h1>Event Log</h1>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<Column Width="165px" Class="text-end">Time</Column>
|
||||
<Column Width="320px; 20%">Agent</Column>
|
||||
<Column Width="160px">Event Type</Column>
|
||||
<Column Width="320px; 20%">Subject</Column>
|
||||
<Column Width="100px; 60%">Data</Column>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var logItem in logItems) {
|
||||
DateTimeOffset time = logItem.UtcTime.ToLocalTime();
|
||||
<tr>
|
||||
<td class="text-end">
|
||||
<time datetime="@time.ToString("o")">@time.ToString()</time>
|
||||
</td>
|
||||
<td>
|
||||
@if (logItem.AgentGuid is {} agentGuid) {
|
||||
@(GetAgentName(agentGuid))
|
||||
<br>
|
||||
<code class="text-uppercase">@agentGuid</code>
|
||||
}
|
||||
else {
|
||||
<text>-</text>
|
||||
}
|
||||
</td>
|
||||
<td>@logItem.EventType.ToNiceString()</td>
|
||||
<td>
|
||||
@if (GetSubjectName(logItem.SubjectType, logItem.SubjectId) is {} subjectName) {
|
||||
@subjectName
|
||||
<br>
|
||||
}
|
||||
<code class="text-uppercase">@logItem.SubjectId</code>
|
||||
</td>
|
||||
<td>
|
||||
<code>@logItem.Data?.RootElement.ToString()</code>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@code {
|
||||
|
||||
private CancellationTokenSource? initializationCancellationTokenSource;
|
||||
private EventLogItem[] logItems = Array.Empty<EventLogItem>();
|
||||
private ImmutableDictionary<Guid, string> agentNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
|
||||
private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
initializationCancellationTokenSource = new CancellationTokenSource();
|
||||
var cancellationToken = initializationCancellationTokenSource.Token;
|
||||
|
||||
try {
|
||||
logItems = await EventLog.GetItems(50, cancellationToken);
|
||||
agentNamesByGuid = AgentManager.GetAgents().ToImmutableDictionary(static kvp => kvp.Key, static kvp => kvp.Value.Name);
|
||||
instanceNamesByGuid = InstanceManager.GetInstanceNames();
|
||||
} finally {
|
||||
initializationCancellationTokenSource.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private string GetAgentName(Guid agentGuid) {
|
||||
return agentNamesByGuid.TryGetValue(agentGuid, out var name) ? name : "?";
|
||||
}
|
||||
|
||||
private string? GetSubjectName(EventLogSubjectType type, string id) {
|
||||
return type switch {
|
||||
EventLogSubjectType.Instance => instanceNamesByGuid.TryGetValue(Guid.Parse(id), out var name) ? name : null,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
try {
|
||||
initializationCancellationTokenSource?.Cancel();
|
||||
} catch (ObjectDisposedException) {}
|
||||
}
|
||||
|
||||
}
|
||||
@* @page "/events" *@
|
||||
@* @attribute [Authorize(Permission.ViewEventsPolicy)] *@
|
||||
@* @using Phantom.Common.Data.Web.EventLog *@
|
||||
@* @using Phantom.Common.Data.Web.Users *@
|
||||
@* @using System.Collections.Immutable *@
|
||||
@* @using System.Diagnostics *@
|
||||
@* @using Phantom.Web.Services.Instances *@
|
||||
@* @implements IDisposable *@
|
||||
@* @inject AgentManager AgentManager *@
|
||||
@* @inject EventLog EventLog *@
|
||||
@* @inject InstanceManager InstanceManager *@
|
||||
@* *@
|
||||
@* <h1>Event Log</h1> *@
|
||||
@* *@
|
||||
@* <table class="table"> *@
|
||||
@* <thead> *@
|
||||
@* <tr> *@
|
||||
@* <Column Width="165px" Class="text-end">Time</Column> *@
|
||||
@* <Column Width="320px; 20%">Agent</Column> *@
|
||||
@* <Column Width="160px">Event Type</Column> *@
|
||||
@* <Column Width="320px; 20%">Subject</Column> *@
|
||||
@* <Column Width="100px; 60%">Data</Column> *@
|
||||
@* </tr> *@
|
||||
@* </thead> *@
|
||||
@* <tbody> *@
|
||||
@* @foreach (var logItem in logItems) { *@
|
||||
@* DateTimeOffset time = logItem.UtcTime.ToLocalTime(); *@
|
||||
@* <tr> *@
|
||||
@* <td class="text-end"> *@
|
||||
@* <time datetime="@time.ToString("o")">@time.ToString()</time> *@
|
||||
@* </td> *@
|
||||
@* <td> *@
|
||||
@* @if (logItem.AgentGuid is {} agentGuid) { *@
|
||||
@* @(GetAgentName(agentGuid)) *@
|
||||
@* <br> *@
|
||||
@* <code class="text-uppercase">@agentGuid</code> *@
|
||||
@* } *@
|
||||
@* else { *@
|
||||
@* <text>-</text> *@
|
||||
@* } *@
|
||||
@* </td> *@
|
||||
@* <td>@logItem.EventType.ToNiceString()</td> *@
|
||||
@* <td> *@
|
||||
@* @if (GetSubjectName(logItem.SubjectType, logItem.SubjectId) is {} subjectName) { *@
|
||||
@* @subjectName *@
|
||||
@* <br> *@
|
||||
@* } *@
|
||||
@* <code class="text-uppercase">@logItem.SubjectId</code> *@
|
||||
@* </td> *@
|
||||
@* <td> *@
|
||||
@* <code>@logItem.Data?.RootElement.ToString()</code> *@
|
||||
@* </td> *@
|
||||
@* </tr> *@
|
||||
@* } *@
|
||||
@* </tbody> *@
|
||||
@* </table> *@
|
||||
@* *@
|
||||
@* @code { *@
|
||||
@* *@
|
||||
@* private CancellationTokenSource? initializationCancellationTokenSource; *@
|
||||
@* private EventLogItem[] logItems = Array.Empty<EventLogItem>(); *@
|
||||
@* private ImmutableDictionary<Guid, string> agentNamesByGuid = ImmutableDictionary<Guid, string>.Empty; *@
|
||||
@* private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty; *@
|
||||
@* *@
|
||||
@* protected override async Task OnInitializedAsync() { *@
|
||||
@* initializationCancellationTokenSource = new CancellationTokenSource(); *@
|
||||
@* var cancellationToken = initializationCancellationTokenSource.Token; *@
|
||||
@* *@
|
||||
@* try { *@
|
||||
@* logItems = await EventLog.GetItems(50, cancellationToken); *@
|
||||
@* agentNamesByGuid = AgentManager.GetAgents().ToImmutableDictionary(static kvp => kvp.Key, static kvp => kvp.Value.Name); *@
|
||||
@* instanceNamesByGuid = InstanceManager.GetInstanceNames(); *@
|
||||
@* } finally { *@
|
||||
@* initializationCancellationTokenSource.Dispose(); *@
|
||||
@* } *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* private string GetAgentName(Guid agentGuid) { *@
|
||||
@* return agentNamesByGuid.TryGetValue(agentGuid, out var name) ? name : "?"; *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* private string? GetSubjectName(EventLogSubjectType type, string id) { *@
|
||||
@* return type switch { *@
|
||||
@* EventLogSubjectType.Instance => instanceNamesByGuid.TryGetValue(Guid.Parse(id), out var name) ? name : null, *@
|
||||
@* _ => null *@
|
||||
@* }; *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* public void Dispose() { *@
|
||||
@* try { *@
|
||||
@* initializationCancellationTokenSource?.Cancel(); *@
|
||||
@* } catch (ObjectDisposedException) {} *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* } *@
|
||||
|
@ -1,91 +1,91 @@
|
||||
@page "/instances/{InstanceGuid:guid}"
|
||||
@attribute [Authorize(Permission.ViewInstancesPolicy)]
|
||||
@inherits PhantomComponent
|
||||
@using Phantom.Web.Services.Instances
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@using Phantom.Common.Data.Replies
|
||||
@implements IDisposable
|
||||
@inject InstanceManager InstanceManager
|
||||
@inject AuditLog AuditLog
|
||||
|
||||
@if (Instance == null) {
|
||||
<h1>Instance Not Found</h1>
|
||||
<p>Return to <a href="instances">all instances</a>.</p>
|
||||
}
|
||||
else {
|
||||
<h1>Instance: @Instance.Configuration.InstanceName</h1>
|
||||
<div class="d-flex flex-row align-items-center gap-2">
|
||||
<PermissionView Permission="Permission.ControlInstances">
|
||||
<button type="button" class="btn btn-success" @onclick="LaunchInstance" disabled="@(isLaunchingInstance || !Instance.Status.CanLaunch())">Launch</button>
|
||||
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#stop-instance" disabled="@(!Instance.Status.CanStop())">Stop...</button>
|
||||
<span><!-- extra spacing --></span>
|
||||
</PermissionView>
|
||||
<InstanceStatusText Status="Instance.Status" />
|
||||
<PermissionView Permission="Permission.CreateInstances">
|
||||
<a href="instances/@InstanceGuid/edit" class="btn btn-warning ms-auto">Edit Configuration</a>
|
||||
</PermissionView>
|
||||
</div>
|
||||
@if (lastError != null) {
|
||||
<p class="text-danger mt-2">@lastError</p>
|
||||
}
|
||||
|
||||
<PermissionView Permission="Permission.ViewInstanceLogs">
|
||||
<InstanceLog InstanceGuid="InstanceGuid" />
|
||||
</PermissionView>
|
||||
|
||||
<PermissionView Permission="Permission.ControlInstances">
|
||||
<div class="mb-3">
|
||||
<InstanceCommandInput InstanceGuid="InstanceGuid" Disabled="@(!Instance.Status.CanSendCommand())" />
|
||||
</div>
|
||||
|
||||
<InstanceStopDialog InstanceGuid="InstanceGuid" ModalId="stop-instance" Disabled="@(!Instance.Status.CanStop())" />
|
||||
</PermissionView>
|
||||
}
|
||||
|
||||
@code {
|
||||
|
||||
[Parameter]
|
||||
public Guid InstanceGuid { get; set; }
|
||||
|
||||
private string? lastError = null;
|
||||
private bool isLaunchingInstance = false;
|
||||
|
||||
private Instance? Instance { get; set; }
|
||||
|
||||
protected override void OnInitialized() {
|
||||
InstanceManager.InstancesChanged.Subscribe(this, instances => {
|
||||
var newInstance = instances.TryGetValue(InstanceGuid, out var instance) ? instance : null;
|
||||
if (newInstance != Instance) {
|
||||
Instance = newInstance;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async Task LaunchInstance() {
|
||||
isLaunchingInstance = true;
|
||||
lastError = null;
|
||||
|
||||
try {
|
||||
if (!await CheckPermission(Permission.ControlInstances)) {
|
||||
lastError = "You do not have permission to launch instances.";
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await InstanceManager.LaunchInstance(InstanceGuid);
|
||||
if (result.Is(LaunchInstanceResult.LaunchInitiated)) {
|
||||
await AuditLog.AddInstanceLaunchedEvent(InstanceGuid);
|
||||
}
|
||||
else {
|
||||
lastError = result.ToSentence(Messages.ToSentence);
|
||||
}
|
||||
} finally {
|
||||
isLaunchingInstance = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
InstanceManager.InstancesChanged.Unsubscribe(this);
|
||||
}
|
||||
|
||||
}
|
||||
@* @page "/instances/{InstanceGuid:guid}" *@
|
||||
@* @attribute [Authorize(Permission.ViewInstancesPolicy)] *@
|
||||
@* @inherits PhantomComponent *@
|
||||
@* @using Phantom.Web.Services.Instances *@
|
||||
@* @using Phantom.Common.Data.Web.Users *@
|
||||
@* @using Phantom.Common.Data.Replies *@
|
||||
@* @implements IDisposable *@
|
||||
@* @inject InstanceManager InstanceManager *@
|
||||
@* @inject AuditLog AuditLog *@
|
||||
@* *@
|
||||
@* @if (Instance == null) { *@
|
||||
@* <h1>Instance Not Found</h1> *@
|
||||
@* <p>Return to <a href="instances">all instances</a>.</p> *@
|
||||
@* } *@
|
||||
@* else { *@
|
||||
@* <h1>Instance: @Instance.Configuration.InstanceName</h1> *@
|
||||
@* <div class="d-flex flex-row align-items-center gap-2"> *@
|
||||
@* <PermissionView Permission="Permission.ControlInstances"> *@
|
||||
@* <button type="button" class="btn btn-success" @onclick="LaunchInstance" disabled="@(isLaunchingInstance || !Instance.Status.CanLaunch())">Launch</button> *@
|
||||
@* <button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#stop-instance" disabled="@(!Instance.Status.CanStop())">Stop...</button> *@
|
||||
@* <span><!-- extra spacing --></span> *@
|
||||
@* </PermissionView> *@
|
||||
@* <InstanceStatusText Status="Instance.Status" /> *@
|
||||
@* <PermissionView Permission="Permission.CreateInstances"> *@
|
||||
@* <a href="instances/@InstanceGuid/edit" class="btn btn-warning ms-auto">Edit Configuration</a> *@
|
||||
@* </PermissionView> *@
|
||||
@* </div> *@
|
||||
@* @if (lastError != null) { *@
|
||||
@* <p class="text-danger mt-2">@lastError</p> *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* <PermissionView Permission="Permission.ViewInstanceLogs"> *@
|
||||
@* <InstanceLog InstanceGuid="InstanceGuid" /> *@
|
||||
@* </PermissionView> *@
|
||||
@* *@
|
||||
@* <PermissionView Permission="Permission.ControlInstances"> *@
|
||||
@* <div class="mb-3"> *@
|
||||
@* <InstanceCommandInput InstanceGuid="InstanceGuid" Disabled="@(!Instance.Status.CanSendCommand())" /> *@
|
||||
@* </div> *@
|
||||
@* *@
|
||||
@* <InstanceStopDialog InstanceGuid="InstanceGuid" ModalId="stop-instance" Disabled="@(!Instance.Status.CanStop())" /> *@
|
||||
@* </PermissionView> *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* @code { *@
|
||||
@* *@
|
||||
@* [Parameter] *@
|
||||
@* public Guid InstanceGuid { get; set; } *@
|
||||
@* *@
|
||||
@* private string? lastError = null; *@
|
||||
@* private bool isLaunchingInstance = false; *@
|
||||
@* *@
|
||||
@* private Instance? Instance { get; set; } *@
|
||||
@* *@
|
||||
@* protected override void OnInitialized() { *@
|
||||
@* InstanceManager.InstancesChanged.Subscribe(this, instances => { *@
|
||||
@* var newInstance = instances.TryGetValue(InstanceGuid, out var instance) ? instance : null; *@
|
||||
@* if (newInstance != Instance) { *@
|
||||
@* Instance = newInstance; *@
|
||||
@* InvokeAsync(StateHasChanged); *@
|
||||
@* } *@
|
||||
@* }); *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* private async Task LaunchInstance() { *@
|
||||
@* isLaunchingInstance = true; *@
|
||||
@* lastError = null; *@
|
||||
@* *@
|
||||
@* try { *@
|
||||
@* if (!await CheckPermission(Permission.ControlInstances)) { *@
|
||||
@* lastError = "You do not have permission to launch instances."; *@
|
||||
@* return; *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* var result = await InstanceManager.LaunchInstance(InstanceGuid); *@
|
||||
@* if (result.Is(LaunchInstanceResult.LaunchInitiated)) { *@
|
||||
@* await AuditLog.AddInstanceLaunchedEvent(InstanceGuid); *@
|
||||
@* } *@
|
||||
@* else { *@
|
||||
@* lastError = result.ToSentence(Messages.ToSentence); *@
|
||||
@* } *@
|
||||
@* } finally { *@
|
||||
@* isLaunchingInstance = false; *@
|
||||
@* } *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* public void Dispose() { *@
|
||||
@* InstanceManager.InstancesChanged.Unsubscribe(this); *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* } *@
|
||||
|
@ -1,29 +1,29 @@
|
||||
@page "/instances/{InstanceGuid:guid}/edit"
|
||||
@attribute [Authorize(Permission.CreateInstancesPolicy)]
|
||||
@using Phantom.Common.Data.Instance
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@using Phantom.Web.Services.Instances
|
||||
@inherits PhantomComponent
|
||||
@inject InstanceManager InstanceManager
|
||||
|
||||
@if (InstanceConfiguration == null) {
|
||||
<h1>Instance Not Found</h1>
|
||||
<p>Return to <a href="instances">all instances</a>.</p>
|
||||
}
|
||||
else {
|
||||
<h1>Edit Instance: @InstanceConfiguration.InstanceName</h1>
|
||||
<InstanceAddOrEditForm EditedInstanceConfiguration="InstanceConfiguration" />
|
||||
}
|
||||
|
||||
@code {
|
||||
|
||||
[Parameter]
|
||||
public Guid InstanceGuid { get; set; }
|
||||
|
||||
private InstanceConfiguration? InstanceConfiguration { get; set; }
|
||||
|
||||
protected override void OnInitialized() {
|
||||
InstanceConfiguration = InstanceManager.GetInstanceConfiguration(InstanceGuid);
|
||||
}
|
||||
|
||||
}
|
||||
@* @page "/instances/{InstanceGuid:guid}/edit" *@
|
||||
@* @attribute [Authorize(Permission.CreateInstancesPolicy)] *@
|
||||
@* @using Phantom.Common.Data.Instance *@
|
||||
@* @using Phantom.Common.Data.Web.Users *@
|
||||
@* @using Phantom.Web.Services.Instances *@
|
||||
@* @inherits PhantomComponent *@
|
||||
@* @inject InstanceManager InstanceManager *@
|
||||
@* *@
|
||||
@* @if (InstanceConfiguration == null) { *@
|
||||
@* <h1>Instance Not Found</h1> *@
|
||||
@* <p>Return to <a href="instances">all instances</a>.</p> *@
|
||||
@* } *@
|
||||
@* else { *@
|
||||
@* <h1>Edit Instance: @InstanceConfiguration.InstanceName</h1> *@
|
||||
@* <InstanceAddOrEditForm EditedInstanceConfiguration="InstanceConfiguration" /> *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* @code { *@
|
||||
@* *@
|
||||
@* [Parameter] *@
|
||||
@* public Guid InstanceGuid { get; set; } *@
|
||||
@* *@
|
||||
@* private InstanceConfiguration? InstanceConfiguration { get; set; } *@
|
||||
@* *@
|
||||
@* protected override void OnInitialized() { *@
|
||||
@* InstanceConfiguration = InstanceManager.GetInstanceConfiguration(InstanceGuid); *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* } *@
|
||||
|
@ -1,92 +1,92 @@
|
||||
@page "/instances"
|
||||
@attribute [Authorize(Permission.ViewInstancesPolicy)]
|
||||
@using System.Collections.Immutable
|
||||
@implements IDisposable
|
||||
@inject AgentManager AgentManager
|
||||
@inject InstanceManager InstanceManager
|
||||
|
||||
<h1>Instances</h1>
|
||||
|
||||
<PermissionView Permission="Permission.CreateInstances">
|
||||
<a href="instances/create" class="btn btn-primary" role="button">New Instance</a>
|
||||
</PermissionView>
|
||||
|
||||
<table class="table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<Column Width="200px; 28%">Agent</Column>
|
||||
<Column Width="200px; 28%">Name</Column>
|
||||
<Column Width="130px; 11%">Version</Column>
|
||||
<Column Width="110px; 8%" Class="text-center">Server Port</Column>
|
||||
<Column Width="110px; 8%" Class="text-center">Rcon Port</Column>
|
||||
<Column Width=" 90px; 8%" Class="text-end">Memory</Column>
|
||||
<Column Width="320px">Identifier</Column>
|
||||
<Column Width="200px; 9%">Status</Column>
|
||||
<Column Width=" 75px">Actions</Column>
|
||||
</tr>
|
||||
</thead>
|
||||
@if (!instances.IsEmpty) {
|
||||
<tbody>
|
||||
@foreach (var (configuration, status, _) in instances) {
|
||||
var agentName = agentNames.TryGetValue(configuration.AgentGuid, out var name) ? name : string.Empty;
|
||||
var instanceGuid = configuration.InstanceGuid.ToString();
|
||||
<tr>
|
||||
<td>@agentName</td>
|
||||
<td>@configuration.InstanceName</td>
|
||||
<td>@configuration.MinecraftServerKind @configuration.MinecraftVersion</td>
|
||||
<td class="text-center">
|
||||
<code>@configuration.ServerPort</code>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<code>@configuration.RconPort</code>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<code>@configuration.MemoryAllocation.InMegabytes MB</code>
|
||||
</td>
|
||||
<td>
|
||||
<code class="text-uppercase">@instanceGuid</code>
|
||||
</td>
|
||||
<td>
|
||||
<InstanceStatusText Status="status" />
|
||||
</td>
|
||||
<td>
|
||||
<a href="instances/@instanceGuid" class="btn btn-info btn-sm">Detail</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
}
|
||||
@if (instances.IsEmpty) {
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="9">
|
||||
No instances.
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
}
|
||||
</table>
|
||||
|
||||
@code {
|
||||
|
||||
private ImmutableDictionary<Guid, string> agentNames = ImmutableDictionary<Guid, string>.Empty;
|
||||
private ImmutableArray<Instance> instances = ImmutableArray<Instance>.Empty;
|
||||
|
||||
protected override void OnInitialized() {
|
||||
AgentManager.AgentsChanged.Subscribe(this, agents => {
|
||||
this.agentNames = agents.ToImmutableDictionary(static agent => agent.Guid, static agent => agent.Name);
|
||||
InvokeAsync(StateHasChanged);
|
||||
});
|
||||
|
||||
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();
|
||||
InvokeAsync(StateHasChanged);
|
||||
});
|
||||
}
|
||||
|
||||
void IDisposable.Dispose() {
|
||||
AgentManager.AgentsChanged.Unsubscribe(this);
|
||||
InstanceManager.InstancesChanged.Unsubscribe(this);
|
||||
}
|
||||
|
||||
}
|
||||
@* @page "/instances" *@
|
||||
@* @attribute [Authorize(Permission.ViewInstancesPolicy)] *@
|
||||
@* @using System.Collections.Immutable *@
|
||||
@* @implements IDisposable *@
|
||||
@* @inject AgentManager AgentManager *@
|
||||
@* @inject InstanceManager InstanceManager *@
|
||||
@* *@
|
||||
@* <h1>Instances</h1> *@
|
||||
@* *@
|
||||
@* <PermissionView Permission="Permission.CreateInstances"> *@
|
||||
@* <a href="instances/create" class="btn btn-primary" role="button">New Instance</a> *@
|
||||
@* </PermissionView> *@
|
||||
@* *@
|
||||
@* <table class="table align-middle"> *@
|
||||
@* <thead> *@
|
||||
@* <tr> *@
|
||||
@* <Column Width="200px; 28%">Agent</Column> *@
|
||||
@* <Column Width="200px; 28%">Name</Column> *@
|
||||
@* <Column Width="130px; 11%">Version</Column> *@
|
||||
@* <Column Width="110px; 8%" Class="text-center">Server Port</Column> *@
|
||||
@* <Column Width="110px; 8%" Class="text-center">Rcon Port</Column> *@
|
||||
@* <Column Width=" 90px; 8%" Class="text-end">Memory</Column> *@
|
||||
@* <Column Width="320px">Identifier</Column> *@
|
||||
@* <Column Width="200px; 9%">Status</Column> *@
|
||||
@* <Column Width=" 75px">Actions</Column> *@
|
||||
@* </tr> *@
|
||||
@* </thead> *@
|
||||
@* @if (!instances.IsEmpty) { *@
|
||||
@* <tbody> *@
|
||||
@* @foreach (var (configuration, status, _) in instances) { *@
|
||||
@* var agentName = agentNames.TryGetValue(configuration.AgentGuid, out var name) ? name : string.Empty; *@
|
||||
@* var instanceGuid = configuration.InstanceGuid.ToString(); *@
|
||||
@* <tr> *@
|
||||
@* <td>@agentName</td> *@
|
||||
@* <td>@configuration.InstanceName</td> *@
|
||||
@* <td>@configuration.MinecraftServerKind @configuration.MinecraftVersion</td> *@
|
||||
@* <td class="text-center"> *@
|
||||
@* <code>@configuration.ServerPort</code> *@
|
||||
@* </td> *@
|
||||
@* <td class="text-center"> *@
|
||||
@* <code>@configuration.RconPort</code> *@
|
||||
@* </td> *@
|
||||
@* <td class="text-end"> *@
|
||||
@* <code>@configuration.MemoryAllocation.InMegabytes MB</code> *@
|
||||
@* </td> *@
|
||||
@* <td> *@
|
||||
@* <code class="text-uppercase">@instanceGuid</code> *@
|
||||
@* </td> *@
|
||||
@* <td> *@
|
||||
@* <InstanceStatusText Status="status" /> *@
|
||||
@* </td> *@
|
||||
@* <td> *@
|
||||
@* <a href="instances/@instanceGuid" class="btn btn-info btn-sm">Detail</a> *@
|
||||
@* </td> *@
|
||||
@* </tr> *@
|
||||
@* } *@
|
||||
@* </tbody> *@
|
||||
@* } *@
|
||||
@* @if (instances.IsEmpty) { *@
|
||||
@* <tfoot> *@
|
||||
@* <tr> *@
|
||||
@* <td colspan="9"> *@
|
||||
@* No instances. *@
|
||||
@* </td> *@
|
||||
@* </tr> *@
|
||||
@* </tfoot> *@
|
||||
@* } *@
|
||||
@* </table> *@
|
||||
@* *@
|
||||
@* @code { *@
|
||||
@* *@
|
||||
@* private ImmutableDictionary<Guid, string> agentNames = ImmutableDictionary<Guid, string>.Empty; *@
|
||||
@* private ImmutableArray<Instance> instances = ImmutableArray<Instance>.Empty; *@
|
||||
@* *@
|
||||
@* protected override void OnInitialized() { *@
|
||||
@* AgentManager.AgentsChanged.Subscribe(this, agents => { *@
|
||||
@* this.agentNames = agents.ToImmutableDictionary(static agent => agent.Guid, static agent => agent.Name); *@
|
||||
@* InvokeAsync(StateHasChanged); *@
|
||||
@* }); *@
|
||||
@* *@
|
||||
@* 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(); *@
|
||||
@* InvokeAsync(StateHasChanged); *@
|
||||
@* }); *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* void IDisposable.Dispose() { *@
|
||||
@* AgentManager.AgentsChanged.Unsubscribe(this); *@
|
||||
@* InstanceManager.InstancesChanged.Unsubscribe(this); *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* } *@
|
||||
|
@ -1,10 +1,12 @@
|
||||
@page "/login"
|
||||
@using Phantom.Web.Services
|
||||
@using Phantom.Web.Services.Authentication
|
||||
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@attribute [AllowAnonymous]
|
||||
@inject INavigation Navigation
|
||||
@inject PhantomLoginManager LoginManager
|
||||
@inject ProtectedSessionStorage ProtectedSessionStore
|
||||
|
||||
<h1>Login</h1>
|
||||
|
||||
|
@ -1,11 +0,0 @@
|
||||
@page "/logout"
|
||||
@using Phantom.Web.Services.Authentication
|
||||
@inject PhantomLoginManager LoginManager
|
||||
|
||||
@code {
|
||||
|
||||
protected override Task OnInitializedAsync() {
|
||||
return LoginManager.SignOut();
|
||||
}
|
||||
|
||||
}
|
@ -11,7 +11,7 @@
|
||||
@attribute [AllowAnonymous]
|
||||
@inject ServiceConfiguration ServiceConfiguration
|
||||
@inject PhantomLoginManager LoginManager
|
||||
@inject ControllerConnection ControllerConnection
|
||||
@inject ControllerCommunication ControllerCommunication
|
||||
|
||||
<h1>Administrator Setup</h1>
|
||||
|
||||
@ -87,7 +87,7 @@
|
||||
}
|
||||
|
||||
private async Task<Result<string>> CreateOrUpdateAdministrator() {
|
||||
var reply = await ControllerConnection.Send<CreateOrUpdateAdministratorUser, CreateOrUpdateAdministratorUserResult>(new CreateOrUpdateAdministratorUser(form.Username, form.Password), Timeout.InfiniteTimeSpan);
|
||||
var reply = await ControllerCommunication.Send<CreateOrUpdateAdministratorUser, CreateOrUpdateAdministratorUserResult>(new CreateOrUpdateAdministratorUser(form.Username, form.Password), Timeout.InfiniteTimeSpan);
|
||||
return reply switch {
|
||||
Success => Result.Ok<string>(),
|
||||
CreationFailed fail => fail.Error.ToSentences("\n"),
|
||||
|
@ -1,110 +1,110 @@
|
||||
@page "/users"
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@using System.Collections.Immutable
|
||||
@using Phantom.Web.Services.Authorization
|
||||
@attribute [Authorize(Permission.ViewUsersPolicy)]
|
||||
@inject UserManager UserManager
|
||||
@inject UserRoleManager UserRoleManager
|
||||
@inject PermissionManager PermissionManager
|
||||
|
||||
<h1>Users</h1>
|
||||
|
||||
<PermissionView Permission="Permission.EditUsers">
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-user">Add User...</button>
|
||||
</PermissionView>
|
||||
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
@{ var canEdit = PermissionManager.CheckPermission(context.User, Permission.EditUsers); }
|
||||
<table class="table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<Column Width="320px">Identifier</Column>
|
||||
<Column Width="125px; 40%">Username</Column>
|
||||
<Column Width="125px; 60%">Roles</Column>
|
||||
@if (canEdit) {
|
||||
<Column Width="175px">Actions</Column>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@{ var myUserId = UserManager.GetAuthenticatedUserId(context.User); }
|
||||
@foreach (var user in allUsers) {
|
||||
var isMe = myUserId == user.Guid;
|
||||
<tr>
|
||||
<td>
|
||||
<code class="text-uppercase">@user.Guid</code>
|
||||
</td>
|
||||
@if (isMe) {
|
||||
<td class="fw-semibold">@user.Name</td>
|
||||
}
|
||||
else {
|
||||
<td>@user.Name</td>
|
||||
}
|
||||
<td>@(userGuidToRoleDescription.TryGetValue(user.Guid, out var roles) ? roles : "?")</td>
|
||||
@if (canEdit) {
|
||||
<td>
|
||||
@if (!isMe) {
|
||||
<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>
|
||||
}
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</Authorized>
|
||||
</AuthorizeView>
|
||||
|
||||
<PermissionView Permission="Permission.EditUsers">
|
||||
<UserAddDialog ModalId="add-user" UserAdded="OnUserAdded" />
|
||||
<UserRolesDialog @ref="userRolesDialog" ModalId="manage-user-roles" UserModified="OnUserRolesChanged" />
|
||||
<UserDeleteDialog @ref="userDeleteDialog" ModalId="delete-user" UserModified="OnUserDeleted" />
|
||||
</PermissionView>
|
||||
|
||||
@code {
|
||||
|
||||
private ImmutableArray<UserInfo> allUsers = ImmutableArray<UserInfo>.Empty;
|
||||
private readonly Dictionary<Guid, string> userGuidToRoleDescription = new();
|
||||
|
||||
private UserRolesDialog userRolesDialog = null!;
|
||||
private UserDeleteDialog userDeleteDialog = null!;
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
var unsortedUsers = await UserManager.GetAll();
|
||||
allUsers = unsortedUsers.Sort(static (a, b) => a.Name.CompareTo(b.Name));
|
||||
|
||||
foreach (var (userGuid, roles) in await UserRoleManager.GetAllByUserGuid()) {
|
||||
userGuidToRoleDescription[userGuid] = StringifyRoles(roles);
|
||||
}
|
||||
|
||||
foreach (var user in allUsers) {
|
||||
await RefreshUserRoles(user);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshUserRoles(UserInfo user) {
|
||||
var roles = await UserRoleManager.GetUserRoles(user);
|
||||
userGuidToRoleDescription[user.Guid] = StringifyRoles(roles);
|
||||
}
|
||||
|
||||
private static string StringifyRoles(ImmutableArray<RoleInfo> roles) {
|
||||
return roles.IsEmpty ? "-" : string.Join(", ", roles.Select(static role => role.Name));
|
||||
}
|
||||
|
||||
private Task OnUserAdded(UserInfo user) {
|
||||
allUsers = allUsers.Add(user);
|
||||
return RefreshUserRoles(user);
|
||||
}
|
||||
|
||||
private Task OnUserRolesChanged(UserInfo user) {
|
||||
return RefreshUserRoles(user);
|
||||
}
|
||||
|
||||
private void OnUserDeleted(UserInfo user) {
|
||||
allUsers = allUsers.Remove(user);
|
||||
userGuidToRoleDescription.Remove(user.Guid);
|
||||
}
|
||||
|
||||
}
|
||||
@* @page "/users" *@
|
||||
@* @using Phantom.Common.Data.Web.Users *@
|
||||
@* @using System.Collections.Immutable *@
|
||||
@* @using Phantom.Web.Services.Authorization *@
|
||||
@* @attribute [Authorize(Permission.ViewUsersPolicy)] *@
|
||||
@* @inject UserManager UserManager *@
|
||||
@* @inject UserRoleManager UserRoleManager *@
|
||||
@* @inject PermissionManager PermissionManager *@
|
||||
@* *@
|
||||
@* <h1>Users</h1> *@
|
||||
@* *@
|
||||
@* <PermissionView Permission="Permission.EditUsers"> *@
|
||||
@* <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-user">Add User...</button> *@
|
||||
@* </PermissionView> *@
|
||||
@* *@
|
||||
@* <AuthorizeView> *@
|
||||
@* <Authorized> *@
|
||||
@* @{ var canEdit = PermissionManager.CheckPermission(context.User, Permission.EditUsers); } *@
|
||||
@* <table class="table align-middle"> *@
|
||||
@* <thead> *@
|
||||
@* <tr> *@
|
||||
@* <Column Width="320px">Identifier</Column> *@
|
||||
@* <Column Width="125px; 40%">Username</Column> *@
|
||||
@* <Column Width="125px; 60%">Roles</Column> *@
|
||||
@* @if (canEdit) { *@
|
||||
@* <Column Width="175px">Actions</Column> *@
|
||||
@* } *@
|
||||
@* </tr> *@
|
||||
@* </thead> *@
|
||||
@* <tbody> *@
|
||||
@* @{ var myUserId = UserManager.GetAuthenticatedUserId(context.User); } *@
|
||||
@* @foreach (var user in allUsers) { *@
|
||||
@* var isMe = myUserId == user.Guid; *@
|
||||
@* <tr> *@
|
||||
@* <td> *@
|
||||
@* <code class="text-uppercase">@user.Guid</code> *@
|
||||
@* </td> *@
|
||||
@* @if (isMe) { *@
|
||||
@* <td class="fw-semibold">@user.Name</td> *@
|
||||
@* } *@
|
||||
@* else { *@
|
||||
@* <td>@user.Name</td> *@
|
||||
@* } *@
|
||||
@* <td>@(userGuidToRoleDescription.TryGetValue(user.Guid, out var roles) ? roles : "?")</td> *@
|
||||
@* @if (canEdit) { *@
|
||||
@* <td> *@
|
||||
@* @if (!isMe) { *@
|
||||
@* <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> *@
|
||||
@* } *@
|
||||
@* </td> *@
|
||||
@* } *@
|
||||
@* </tr> *@
|
||||
@* } *@
|
||||
@* </tbody> *@
|
||||
@* </table> *@
|
||||
@* </Authorized> *@
|
||||
@* </AuthorizeView> *@
|
||||
@* *@
|
||||
@* <PermissionView Permission="Permission.EditUsers"> *@
|
||||
@* <UserAddDialog ModalId="add-user" UserAdded="OnUserAdded" /> *@
|
||||
@* <UserRolesDialog @ref="userRolesDialog" ModalId="manage-user-roles" UserModified="OnUserRolesChanged" /> *@
|
||||
@* <UserDeleteDialog @ref="userDeleteDialog" ModalId="delete-user" UserModified="OnUserDeleted" /> *@
|
||||
@* </PermissionView> *@
|
||||
@* *@
|
||||
@* @code { *@
|
||||
@* *@
|
||||
@* private ImmutableArray<UserInfo> allUsers = ImmutableArray<UserInfo>.Empty; *@
|
||||
@* private readonly Dictionary<Guid, string> userGuidToRoleDescription = new(); *@
|
||||
@* *@
|
||||
@* private UserRolesDialog userRolesDialog = null!; *@
|
||||
@* private UserDeleteDialog userDeleteDialog = null!; *@
|
||||
@* *@
|
||||
@* protected override async Task OnInitializedAsync() { *@
|
||||
@* var unsortedUsers = await UserManager.GetAll(); *@
|
||||
@* allUsers = unsortedUsers.Sort(static (a, b) => a.Name.CompareTo(b.Name)); *@
|
||||
@* *@
|
||||
@* foreach (var (userGuid, roles) in await UserRoleManager.GetAllByUserGuid()) { *@
|
||||
@* userGuidToRoleDescription[userGuid] = StringifyRoles(roles); *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* foreach (var user in allUsers) { *@
|
||||
@* await RefreshUserRoles(user); *@
|
||||
@* } *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* private async Task RefreshUserRoles(UserInfo user) { *@
|
||||
@* var roles = await UserRoleManager.GetUserRoles(user); *@
|
||||
@* userGuidToRoleDescription[user.Guid] = StringifyRoles(roles); *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* private static string StringifyRoles(ImmutableArray<RoleInfo> roles) { *@
|
||||
@* return roles.IsEmpty ? "-" : string.Join(", ", roles.Select(static role => role.Name)); *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* private Task OnUserAdded(UserInfo user) { *@
|
||||
@* allUsers = allUsers.Add(user); *@
|
||||
@* return RefreshUserRoles(user); *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* private Task OnUserRolesChanged(UserInfo user) { *@
|
||||
@* return RefreshUserRoles(user); *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* private void OnUserDeleted(UserInfo user) { *@
|
||||
@* allUsers = allUsers.Remove(user); *@
|
||||
@* userGuidToRoleDescription.Remove(user.Guid); *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* } *@
|
||||
|
@ -1,338 +1,338 @@
|
||||
@using Phantom.Web.Components.Utils
|
||||
@using Phantom.Common.Data.Minecraft
|
||||
@using Phantom.Common.Data.Web.Minecraft
|
||||
@using Phantom.Common.Data.Instance
|
||||
@using Phantom.Common.Data.Java
|
||||
@using System.Collections.Immutable
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using System.Diagnostics.CodeAnalysis
|
||||
@using Phantom.Common.Data
|
||||
@using Phantom.Web.Services
|
||||
@using Phantom.Web.Services.Instances
|
||||
@inject INavigation Nav
|
||||
@inject MinecraftVersions MinecraftVersions
|
||||
@inject AgentManager AgentManager
|
||||
@inject AgentJavaRuntimesManager AgentJavaRuntimesManager
|
||||
@inject InstanceManager InstanceManager
|
||||
@inject AuditLog AuditLog
|
||||
|
||||
<Form Model="form" OnSubmit="AddOrEditInstance">
|
||||
@{ var selectedAgent = form.SelectedAgent; }
|
||||
<div class="row">
|
||||
<div class="col-xl-7 mb-3">
|
||||
@{
|
||||
static RenderFragment GetAgentOption(Agent agent) {
|
||||
return @<option value="@agent.Guid">
|
||||
@agent.Name
|
||||
•
|
||||
@(agent.Stats?.RunningInstanceCount.ToString() ?? "?")/@(agent.MaxInstances) @(agent.MaxInstances == 1 ? "Instance" : "Instances")
|
||||
•
|
||||
@(agent.Stats?.RunningInstanceMemory.InMegabytes.ToString() ?? "?")/@(agent.MaxMemory.InMegabytes) MB RAM
|
||||
</option>;
|
||||
}
|
||||
}
|
||||
@if (EditedInstanceConfiguration == null) {
|
||||
<FormSelectInput Id="instance-agent" Label="Agent" @bind-Value="form.SelectedAgentGuid">
|
||||
<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)) {
|
||||
@GetAgentOption(agent)
|
||||
}
|
||||
</FormSelectInput>
|
||||
}
|
||||
else {
|
||||
<FormSelectInput Id="instance-agent" Label="Agent" @bind-Value="form.SelectedAgentGuid" disabled="true">
|
||||
@if (form.SelectedAgentGuid is {} guid && form.AgentsByGuid.TryGetValue(guid, out var agent)) {
|
||||
@GetAgentOption(agent)
|
||||
}
|
||||
</FormSelectInput>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="col-xl-5 mb-3">
|
||||
<FormTextInput Id="instance-name" Label="Instance Name" @bind-Value="form.InstanceName" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-6 col-xl-2 mb-3">
|
||||
<FormSelectInput Id="instance-server-kind" Label="Server Software" @bind-Value="form.MinecraftServerKind">
|
||||
@foreach (var kind in Enum.GetValues<MinecraftServerKind>()) {
|
||||
<option value="@kind">@kind</option>
|
||||
}
|
||||
</FormSelectInput>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6 col-xl-3 mb-3">
|
||||
<FormSelectInput Id="instance-minecraft-version" Label="Minecraft Version" @bind-Value="form.MinecraftVersion">
|
||||
<ChildContent>
|
||||
@foreach (var version in availableMinecraftVersions) {
|
||||
<option value="@version.Id">@version.Id</option>
|
||||
}
|
||||
</ChildContent>
|
||||
<GroupContent>
|
||||
<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">
|
||||
@foreach (var versionType in MinecraftVersionTypes.WithServerJars) {
|
||||
<li>
|
||||
<button type="button" class="dropdown-item" @onclick="() => SetMinecraftVersionType(versionType)">@versionType.ToNiceNamePlural()</button>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</GroupContent>
|
||||
</FormSelectInput>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-3 mb-3">
|
||||
<FormSelectInput Id="instance-java-runtime" Label="Java Runtime" @bind-Value="form.JavaRuntimeGuid" disabled="@(form.JavaRuntimesForSelectedAgent.IsEmpty)">
|
||||
<option value="" selected>Select Java runtime...</option>
|
||||
@foreach (var (guid, runtime) in form.JavaRuntimesForSelectedAgent) {
|
||||
<option value="@guid">@runtime.DisplayName</option>
|
||||
}
|
||||
</FormSelectInput>
|
||||
</div>
|
||||
|
||||
@{
|
||||
string? allowedServerPorts = selectedAgent?.AllowedServerPorts?.ToString();
|
||||
string? allowedRconPorts = selectedAgent?.AllowedRconPorts?.ToString();
|
||||
}
|
||||
<div class="col-sm-6 col-xl-2 mb-3">
|
||||
<FormNumberInput Id="instance-server-port" @bind-Value="form.ServerPort" min="0" max="65535">
|
||||
<LabelFragment>
|
||||
@if (string.IsNullOrEmpty(allowedServerPorts)) {
|
||||
<text>Server Port</text>
|
||||
}
|
||||
else {
|
||||
<text>Server Port <sup title="Allowed: @allowedServerPorts">[?]</sup></text>
|
||||
}
|
||||
</LabelFragment>
|
||||
</FormNumberInput>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6 col-xl-2 mb-3">
|
||||
<FormNumberInput Id="instance-rcon-port" @bind-Value="form.RconPort" min="0" max="65535">
|
||||
<LabelFragment>
|
||||
@if (string.IsNullOrEmpty(allowedRconPorts)) {
|
||||
<text>Rcon Port</text>
|
||||
}
|
||||
else {
|
||||
<text>Rcon Port <sup title="Allowed: @allowedRconPorts">[?]</sup></text>
|
||||
}
|
||||
</LabelFragment>
|
||||
</FormNumberInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xl-12 mb-3">
|
||||
@{
|
||||
const ushort MinimumMemoryUnits = 2;
|
||||
ushort maximumMemoryUnits = form.MaximumMemoryUnits;
|
||||
double availableMemoryRatio = maximumMemoryUnits <= MinimumMemoryUnits ? 100.0 : 100.0 * (form.AvailableMemoryUnits - MinimumMemoryUnits) / (maximumMemoryUnits - MinimumMemoryUnits);
|
||||
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">
|
||||
<LabelFragment>
|
||||
@if (maximumMemoryUnits == 0) {
|
||||
<text>RAM</text>
|
||||
}
|
||||
else {
|
||||
<text>RAM • <code>@(form.MemoryAllocation?.InMegabytes ?? 0) / @(selectedAgent?.MaxMemory.InMegabytes) MB</code></text>
|
||||
}
|
||||
</LabelFragment>
|
||||
</FormNumberInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="mb-3">
|
||||
<FormTextInput Id="instance-jvm-arguments" Type="FormTextInputType.Textarea" @bind-Value="form.JvmArguments" rows="4">
|
||||
<LabelFragment>
|
||||
JVM Arguments <span class="text-black-50">(one per line)</span>
|
||||
</LabelFragment>
|
||||
</FormTextInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormButtonSubmit Label="@(EditedInstanceConfiguration == null ? "Create Instance" : "Edit Instance")" class="btn btn-primary" disabled="@(!IsSubmittable)" />
|
||||
<FormSubmitError />
|
||||
</Form>
|
||||
|
||||
@code {
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public InstanceConfiguration? EditedInstanceConfiguration { get; set; }
|
||||
|
||||
private ConfigureInstanceFormModel form = null!;
|
||||
|
||||
private MinecraftVersionType minecraftVersionType = MinecraftVersionType.Release;
|
||||
private ImmutableArray<MinecraftVersion> availableMinecraftVersions = ImmutableArray<MinecraftVersion>.Empty;
|
||||
|
||||
private bool IsSubmittable => form.SelectedAgentGuid != null && !form.EditContext.GetValidationMessages(form.EditContext.Field(nameof(ConfigureInstanceFormModel.SelectedAgentGuid))).Any();
|
||||
|
||||
private sealed class ConfigureInstanceFormModel : FormModel {
|
||||
public ImmutableDictionary<Guid, Agent> AgentsByGuid { get; }
|
||||
private readonly ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>> javaRuntimesByAgentGuid;
|
||||
private readonly RamAllocationUnits? editedInstanceRamAllocation;
|
||||
|
||||
public ConfigureInstanceFormModel(AgentManager agentManager, AgentJavaRuntimesManager agentJavaRuntimesManager, RamAllocationUnits? editedInstanceRamAllocation) {
|
||||
this.AgentsByGuid = agentManager.GetAgents().ToImmutableDictionary();
|
||||
this.javaRuntimesByAgentGuid = agentJavaRuntimesManager.All;
|
||||
this.editedInstanceRamAllocation = editedInstanceRamAllocation;
|
||||
}
|
||||
|
||||
private bool TryGet<TValue>(ImmutableDictionary<Guid, TValue> dictionary, Guid? agentGuid, [MaybeNullWhen(false)] out TValue value) {
|
||||
if (agentGuid == null) {
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
return dictionary.TryGetValue(agentGuid.Value, out value);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetAgent(Guid? agentGuid, [NotNullWhen(true)] out Agent? agent) {
|
||||
return TryGet(AgentsByGuid, agentGuid, out agent);
|
||||
}
|
||||
|
||||
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 ushort MaximumMemoryUnits => SelectedAgent?.MaxMemory.RawValue ?? 0;
|
||||
public ushort AvailableMemoryUnits => Math.Min((SelectedAgent?.AvailableMemory + editedInstanceRamAllocation)?.RawValue ?? MaximumMemoryUnits, MaximumMemoryUnits);
|
||||
private ushort selectedMemoryUnits = 4;
|
||||
|
||||
[Required(ErrorMessage = "You must select an agent.")]
|
||||
public Guid? SelectedAgentGuid { get; set; } = null;
|
||||
|
||||
[Required(ErrorMessage = "Instance name is required.")]
|
||||
[StringLength(100, ErrorMessage = "Instance name must be at most 100 characters.")]
|
||||
public string InstanceName { get; set; } = string.Empty;
|
||||
|
||||
[Range(minimum: 0, maximum: 65535, ErrorMessage = "Server port must be between 0 and 65535.")]
|
||||
[ServerPortMustBeAllowed(ErrorMessage = "Server port is not allowed.")]
|
||||
public int ServerPort { get; set; } = 25565;
|
||||
|
||||
[Range(minimum: 0, maximum: 65535, ErrorMessage = "Rcon port must be between 0 and 65535.")]
|
||||
[RconPortMustBeAllowed(ErrorMessage = "Rcon port is not allowed.")]
|
||||
[RconPortMustDifferFromServerPort(ErrorMessage = "Rcon port must not be the same as Server port.")]
|
||||
public int RconPort { get; set; } = 25575;
|
||||
|
||||
public MinecraftServerKind MinecraftServerKind { get; set; } = MinecraftServerKind.Vanilla;
|
||||
|
||||
[Required(ErrorMessage = "You must select a Java runtime.")]
|
||||
public Guid? JavaRuntimeGuid { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "You must select a Minecraft version.")]
|
||||
public string MinecraftVersion { get; set; } = string.Empty;
|
||||
|
||||
[Range(minimum: 0, maximum: RamAllocationUnits.MaximumUnits, ErrorMessage = "Memory is out of range.")]
|
||||
public ushort MemoryUnits {
|
||||
get => Math.Min(selectedMemoryUnits, MaximumMemoryUnits);
|
||||
set => selectedMemoryUnits = value;
|
||||
}
|
||||
|
||||
public RamAllocationUnits? MemoryAllocation => new RamAllocationUnits(MemoryUnits);
|
||||
|
||||
[JvmArgumentsMustBeValid(ErrorMessage = "JVM arguments are not valid.")]
|
||||
public string JvmArguments { get; set; } = string.Empty;
|
||||
|
||||
public sealed class ServerPortMustBeAllowedAttribute : FormValidationAttribute<ConfigureInstanceFormModel, int> {
|
||||
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;
|
||||
}
|
||||
|
||||
public sealed class RconPortMustBeAllowedAttribute : FormValidationAttribute<ConfigureInstanceFormModel, int> {
|
||||
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;
|
||||
}
|
||||
|
||||
public sealed class RconPortMustDifferFromServerPortAttribute : FormValidationAttribute<ConfigureInstanceFormModel, int?> {
|
||||
protected override string FieldName => nameof(RconPort);
|
||||
protected override bool IsValid(ConfigureInstanceFormModel model, int? value) => value != model.ServerPort;
|
||||
}
|
||||
|
||||
public sealed class JvmArgumentsMustBeValidAttribute : FormCustomValidationAttribute<ConfigureInstanceFormModel, string> {
|
||||
protected override string FieldName => nameof(JvmArguments);
|
||||
|
||||
protected override ValidationResult? Validate(ConfigureInstanceFormModel model, string value) {
|
||||
var error = JvmArgumentsHelper.Validate(value);
|
||||
return error == null ? ValidationResult.Success : new ValidationResult(error.ToSentence());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnInitialized() {
|
||||
form = new ConfigureInstanceFormModel(AgentManager, AgentJavaRuntimesManager, EditedInstanceConfiguration?.MemoryAllocation);
|
||||
|
||||
if (EditedInstanceConfiguration != null) {
|
||||
form.SelectedAgentGuid = EditedInstanceConfiguration.AgentGuid;
|
||||
form.InstanceName = EditedInstanceConfiguration.InstanceName;
|
||||
form.ServerPort = EditedInstanceConfiguration.ServerPort;
|
||||
form.RconPort = EditedInstanceConfiguration.RconPort;
|
||||
form.MinecraftVersion = EditedInstanceConfiguration.MinecraftVersion;
|
||||
form.MinecraftServerKind = EditedInstanceConfiguration.MinecraftServerKind;
|
||||
form.MemoryUnits = EditedInstanceConfiguration.MemoryAllocation.RawValue;
|
||||
form.JavaRuntimeGuid = EditedInstanceConfiguration.JavaRuntimeGuid;
|
||||
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.JavaRuntimeGuid));
|
||||
form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.SelectedAgentGuid), revalidated: nameof(ConfigureInstanceFormModel.ServerPort));
|
||||
form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.SelectedAgentGuid), revalidated: nameof(ConfigureInstanceFormModel.RconPort));
|
||||
form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.ServerPort), revalidated: nameof(ConfigureInstanceFormModel.RconPort));
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
if (EditedInstanceConfiguration != null) {
|
||||
var allMinecraftVersions = await MinecraftVersions.GetVersions(CancellationToken.None);
|
||||
minecraftVersionType = allMinecraftVersions.FirstOrDefault(version => version.Id == EditedInstanceConfiguration.MinecraftVersion)?.Type ?? minecraftVersionType;
|
||||
}
|
||||
|
||||
await SetMinecraftVersionType(minecraftVersionType);
|
||||
}
|
||||
|
||||
private async Task SetMinecraftVersionType(MinecraftVersionType type) {
|
||||
minecraftVersionType = type;
|
||||
|
||||
var allMinecraftVersions = await MinecraftVersions.GetVersions(CancellationToken.None);
|
||||
availableMinecraftVersions = allMinecraftVersions.Where(version => version.Type == type).ToImmutableArray();
|
||||
|
||||
if (!availableMinecraftVersions.IsEmpty && !availableMinecraftVersions.Any(version => version.Id == form.MinecraftVersion)) {
|
||||
form.MinecraftVersion = availableMinecraftVersions[0].Id;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AddOrEditInstance(EditContext context) {
|
||||
var selectedAgent = form.SelectedAgent;
|
||||
if (selectedAgent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await form.SubmitModel.StartSubmitting();
|
||||
|
||||
var instance = new InstanceConfiguration(
|
||||
EditedInstanceConfiguration?.AgentGuid ?? selectedAgent.Guid,
|
||||
EditedInstanceConfiguration?.InstanceGuid ?? Guid.NewGuid(),
|
||||
form.InstanceName,
|
||||
(ushort) form.ServerPort,
|
||||
(ushort) form.RconPort,
|
||||
form.MinecraftVersion,
|
||||
form.MinecraftServerKind,
|
||||
form.MemoryAllocation ?? RamAllocationUnits.Zero,
|
||||
form.JavaRuntimeGuid.GetValueOrDefault(),
|
||||
JvmArgumentsHelper.Split(form.JvmArguments)
|
||||
);
|
||||
|
||||
var result = await InstanceManager.AddOrEditInstance(instance);
|
||||
if (result.Is(AddOrEditInstanceResult.Success)) {
|
||||
await (EditedInstanceConfiguration == null ? AuditLog.AddInstanceCreatedEvent(instance.InstanceGuid) : AuditLog.AddInstanceEditedEvent(instance.InstanceGuid));
|
||||
Nav.NavigateTo("instances/" + instance.InstanceGuid);
|
||||
}
|
||||
else {
|
||||
form.SubmitModel.StopSubmitting(result.ToSentence(AddOrEditInstanceResultExtensions.ToSentence));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@* @using Phantom.Web.Components.Utils *@
|
||||
@* @using Phantom.Common.Data.Minecraft *@
|
||||
@* @using Phantom.Common.Data.Web.Minecraft *@
|
||||
@* @using Phantom.Common.Data.Instance *@
|
||||
@* @using Phantom.Common.Data.Java *@
|
||||
@* @using System.Collections.Immutable *@
|
||||
@* @using System.ComponentModel.DataAnnotations *@
|
||||
@* @using System.Diagnostics.CodeAnalysis *@
|
||||
@* @using Phantom.Common.Data *@
|
||||
@* @using Phantom.Web.Services *@
|
||||
@* @using Phantom.Web.Services.Instances *@
|
||||
@* @inject INavigation Nav *@
|
||||
@* @inject MinecraftVersions MinecraftVersions *@
|
||||
@* @inject AgentManager AgentManager *@
|
||||
@* @inject AgentJavaRuntimesManager AgentJavaRuntimesManager *@
|
||||
@* @inject InstanceManager InstanceManager *@
|
||||
@* @inject AuditLog AuditLog *@
|
||||
@* *@
|
||||
@* <Form Model="form" OnSubmit="AddOrEditInstance"> *@
|
||||
@* @{ var selectedAgent = form.SelectedAgent; } *@
|
||||
@* <div class="row"> *@
|
||||
@* <div class="col-xl-7 mb-3"> *@
|
||||
@* @{ *@
|
||||
@* static RenderFragment GetAgentOption(Agent agent) { *@
|
||||
@* return @<option value="@agent.Guid"> *@
|
||||
@* @agent.Name *@
|
||||
@* • *@
|
||||
@* @(agent.Stats?.RunningInstanceCount.ToString() ?? "?")/@(agent.MaxInstances) @(agent.MaxInstances == 1 ? "Instance" : "Instances") *@
|
||||
@* • *@
|
||||
@* @(agent.Stats?.RunningInstanceMemory.InMegabytes.ToString() ?? "?")/@(agent.MaxMemory.InMegabytes) MB RAM *@
|
||||
@* </option>; *@
|
||||
@* } *@
|
||||
@* } *@
|
||||
@* @if (EditedInstanceConfiguration == null) { *@
|
||||
@* <FormSelectInput Id="instance-agent" Label="Agent" @bind-Value="form.SelectedAgentGuid"> *@
|
||||
@* <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)) { *@
|
||||
@* @GetAgentOption(agent) *@
|
||||
@* } *@
|
||||
@* </FormSelectInput> *@
|
||||
@* } *@
|
||||
@* else { *@
|
||||
@* <FormSelectInput Id="instance-agent" Label="Agent" @bind-Value="form.SelectedAgentGuid" disabled="true"> *@
|
||||
@* @if (form.SelectedAgentGuid is {} guid && form.AgentsByGuid.TryGetValue(guid, out var agent)) { *@
|
||||
@* @GetAgentOption(agent) *@
|
||||
@* } *@
|
||||
@* </FormSelectInput> *@
|
||||
@* } *@
|
||||
@* </div> *@
|
||||
@* *@
|
||||
@* <div class="col-xl-5 mb-3"> *@
|
||||
@* <FormTextInput Id="instance-name" Label="Instance Name" @bind-Value="form.InstanceName" /> *@
|
||||
@* </div> *@
|
||||
@* </div> *@
|
||||
@* *@
|
||||
@* <div class="row"> *@
|
||||
@* <div class="col-sm-6 col-xl-2 mb-3"> *@
|
||||
@* <FormSelectInput Id="instance-server-kind" Label="Server Software" @bind-Value="form.MinecraftServerKind"> *@
|
||||
@* @foreach (var kind in Enum.GetValues<MinecraftServerKind>()) { *@
|
||||
@* <option value="@kind">@kind</option> *@
|
||||
@* } *@
|
||||
@* </FormSelectInput> *@
|
||||
@* </div> *@
|
||||
@* *@
|
||||
@* <div class="col-sm-6 col-xl-3 mb-3"> *@
|
||||
@* <FormSelectInput Id="instance-minecraft-version" Label="Minecraft Version" @bind-Value="form.MinecraftVersion"> *@
|
||||
@* <ChildContent> *@
|
||||
@* @foreach (var version in availableMinecraftVersions) { *@
|
||||
@* <option value="@version.Id">@version.Id</option> *@
|
||||
@* } *@
|
||||
@* </ChildContent> *@
|
||||
@* <GroupContent> *@
|
||||
@* <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"> *@
|
||||
@* @foreach (var versionType in MinecraftVersionTypes.WithServerJars) { *@
|
||||
@* <li> *@
|
||||
@* <button type="button" class="dropdown-item" @onclick="() => SetMinecraftVersionType(versionType)">@versionType.ToNiceNamePlural()</button> *@
|
||||
@* </li> *@
|
||||
@* } *@
|
||||
@* </ul> *@
|
||||
@* </GroupContent> *@
|
||||
@* </FormSelectInput> *@
|
||||
@* </div> *@
|
||||
@* *@
|
||||
@* <div class="col-xl-3 mb-3"> *@
|
||||
@* <FormSelectInput Id="instance-java-runtime" Label="Java Runtime" @bind-Value="form.JavaRuntimeGuid" disabled="@(form.JavaRuntimesForSelectedAgent.IsEmpty)"> *@
|
||||
@* <option value="" selected>Select Java runtime...</option> *@
|
||||
@* @foreach (var (guid, runtime) in form.JavaRuntimesForSelectedAgent) { *@
|
||||
@* <option value="@guid">@runtime.DisplayName</option> *@
|
||||
@* } *@
|
||||
@* </FormSelectInput> *@
|
||||
@* </div> *@
|
||||
@* *@
|
||||
@* @{ *@
|
||||
@* string? allowedServerPorts = selectedAgent?.AllowedServerPorts?.ToString(); *@
|
||||
@* string? allowedRconPorts = selectedAgent?.AllowedRconPorts?.ToString(); *@
|
||||
@* } *@
|
||||
@* <div class="col-sm-6 col-xl-2 mb-3"> *@
|
||||
@* <FormNumberInput Id="instance-server-port" @bind-Value="form.ServerPort" min="0" max="65535"> *@
|
||||
@* <LabelFragment> *@
|
||||
@* @if (string.IsNullOrEmpty(allowedServerPorts)) { *@
|
||||
@* <text>Server Port</text> *@
|
||||
@* } *@
|
||||
@* else { *@
|
||||
@* <text>Server Port <sup title="Allowed: @allowedServerPorts">[?]</sup></text> *@
|
||||
@* } *@
|
||||
@* </LabelFragment> *@
|
||||
@* </FormNumberInput> *@
|
||||
@* </div> *@
|
||||
@* *@
|
||||
@* <div class="col-sm-6 col-xl-2 mb-3"> *@
|
||||
@* <FormNumberInput Id="instance-rcon-port" @bind-Value="form.RconPort" min="0" max="65535"> *@
|
||||
@* <LabelFragment> *@
|
||||
@* @if (string.IsNullOrEmpty(allowedRconPorts)) { *@
|
||||
@* <text>Rcon Port</text> *@
|
||||
@* } *@
|
||||
@* else { *@
|
||||
@* <text>Rcon Port <sup title="Allowed: @allowedRconPorts">[?]</sup></text> *@
|
||||
@* } *@
|
||||
@* </LabelFragment> *@
|
||||
@* </FormNumberInput> *@
|
||||
@* </div> *@
|
||||
@* </div> *@
|
||||
@* *@
|
||||
@* <div class="row"> *@
|
||||
@* <div class="col-xl-12 mb-3"> *@
|
||||
@* @{ *@
|
||||
@* const ushort MinimumMemoryUnits = 2; *@
|
||||
@* ushort maximumMemoryUnits = form.MaximumMemoryUnits; *@
|
||||
@* double availableMemoryRatio = maximumMemoryUnits <= MinimumMemoryUnits ? 100.0 : 100.0 * (form.AvailableMemoryUnits - MinimumMemoryUnits) / (maximumMemoryUnits - MinimumMemoryUnits); *@
|
||||
@* 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"> *@
|
||||
@* <LabelFragment> *@
|
||||
@* @if (maximumMemoryUnits == 0) { *@
|
||||
@* <text>RAM</text> *@
|
||||
@* } *@
|
||||
@* else { *@
|
||||
@* <text>RAM • <code>@(form.MemoryAllocation?.InMegabytes ?? 0) / @(selectedAgent?.MaxMemory.InMegabytes) MB</code></text> *@
|
||||
@* } *@
|
||||
@* </LabelFragment> *@
|
||||
@* </FormNumberInput> *@
|
||||
@* </div> *@
|
||||
@* </div> *@
|
||||
@* *@
|
||||
@* <div class="row"> *@
|
||||
@* <div class="mb-3"> *@
|
||||
@* <FormTextInput Id="instance-jvm-arguments" Type="FormTextInputType.Textarea" @bind-Value="form.JvmArguments" rows="4"> *@
|
||||
@* <LabelFragment> *@
|
||||
@* JVM Arguments <span class="text-black-50">(one per line)</span> *@
|
||||
@* </LabelFragment> *@
|
||||
@* </FormTextInput> *@
|
||||
@* </div> *@
|
||||
@* </div> *@
|
||||
@* *@
|
||||
@* <FormButtonSubmit Label="@(EditedInstanceConfiguration == null ? "Create Instance" : "Edit Instance")" class="btn btn-primary" disabled="@(!IsSubmittable)" /> *@
|
||||
@* <FormSubmitError /> *@
|
||||
@* </Form> *@
|
||||
@* *@
|
||||
@* @code { *@
|
||||
@* *@
|
||||
@* [Parameter, EditorRequired] *@
|
||||
@* public InstanceConfiguration? EditedInstanceConfiguration { get; set; } *@
|
||||
@* *@
|
||||
@* private ConfigureInstanceFormModel form = null!; *@
|
||||
@* *@
|
||||
@* private MinecraftVersionType minecraftVersionType = MinecraftVersionType.Release; *@
|
||||
@* private ImmutableArray<MinecraftVersion> availableMinecraftVersions = ImmutableArray<MinecraftVersion>.Empty; *@
|
||||
@* *@
|
||||
@* private bool IsSubmittable => form.SelectedAgentGuid != null && !form.EditContext.GetValidationMessages(form.EditContext.Field(nameof(ConfigureInstanceFormModel.SelectedAgentGuid))).Any(); *@
|
||||
@* *@
|
||||
@* private sealed class ConfigureInstanceFormModel : FormModel { *@
|
||||
@* public ImmutableDictionary<Guid, Agent> AgentsByGuid { get; } *@
|
||||
@* private readonly ImmutableDictionary<Guid, ImmutableArray<TaggedJavaRuntime>> javaRuntimesByAgentGuid; *@
|
||||
@* private readonly RamAllocationUnits? editedInstanceRamAllocation; *@
|
||||
@* *@
|
||||
@* public ConfigureInstanceFormModel(AgentManager agentManager, AgentJavaRuntimesManager agentJavaRuntimesManager, RamAllocationUnits? editedInstanceRamAllocation) { *@
|
||||
@* this.AgentsByGuid = agentManager.GetAgents().ToImmutableDictionary(); *@
|
||||
@* this.javaRuntimesByAgentGuid = agentJavaRuntimesManager.All; *@
|
||||
@* this.editedInstanceRamAllocation = editedInstanceRamAllocation; *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* private bool TryGet<TValue>(ImmutableDictionary<Guid, TValue> dictionary, Guid? agentGuid, [MaybeNullWhen(false)] out TValue value) { *@
|
||||
@* if (agentGuid == null) { *@
|
||||
@* value = default; *@
|
||||
@* return false; *@
|
||||
@* } *@
|
||||
@* else { *@
|
||||
@* return dictionary.TryGetValue(agentGuid.Value, out value); *@
|
||||
@* } *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* private bool TryGetAgent(Guid? agentGuid, [NotNullWhen(true)] out Agent? agent) { *@
|
||||
@* return TryGet(AgentsByGuid, agentGuid, out agent); *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* 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 ushort MaximumMemoryUnits => SelectedAgent?.MaxMemory.RawValue ?? 0; *@
|
||||
@* public ushort AvailableMemoryUnits => Math.Min((SelectedAgent?.AvailableMemory + editedInstanceRamAllocation)?.RawValue ?? MaximumMemoryUnits, MaximumMemoryUnits); *@
|
||||
@* private ushort selectedMemoryUnits = 4; *@
|
||||
@* *@
|
||||
@* [Required(ErrorMessage = "You must select an agent.")] *@
|
||||
@* public Guid? SelectedAgentGuid { get; set; } = null; *@
|
||||
@* *@
|
||||
@* [Required(ErrorMessage = "Instance name is required.")] *@
|
||||
@* [StringLength(100, ErrorMessage = "Instance name must be at most 100 characters.")] *@
|
||||
@* public string InstanceName { get; set; } = string.Empty; *@
|
||||
@* *@
|
||||
@* [Range(minimum: 0, maximum: 65535, ErrorMessage = "Server port must be between 0 and 65535.")] *@
|
||||
@* [ServerPortMustBeAllowed(ErrorMessage = "Server port is not allowed.")] *@
|
||||
@* public int ServerPort { get; set; } = 25565; *@
|
||||
@* *@
|
||||
@* [Range(minimum: 0, maximum: 65535, ErrorMessage = "Rcon port must be between 0 and 65535.")] *@
|
||||
@* [RconPortMustBeAllowed(ErrorMessage = "Rcon port is not allowed.")] *@
|
||||
@* [RconPortMustDifferFromServerPort(ErrorMessage = "Rcon port must not be the same as Server port.")] *@
|
||||
@* public int RconPort { get; set; } = 25575; *@
|
||||
@* *@
|
||||
@* public MinecraftServerKind MinecraftServerKind { get; set; } = MinecraftServerKind.Vanilla; *@
|
||||
@* *@
|
||||
@* [Required(ErrorMessage = "You must select a Java runtime.")] *@
|
||||
@* public Guid? JavaRuntimeGuid { get; set; } *@
|
||||
@* *@
|
||||
@* [Required(ErrorMessage = "You must select a Minecraft version.")] *@
|
||||
@* public string MinecraftVersion { get; set; } = string.Empty; *@
|
||||
@* *@
|
||||
@* [Range(minimum: 0, maximum: RamAllocationUnits.MaximumUnits, ErrorMessage = "Memory is out of range.")] *@
|
||||
@* public ushort MemoryUnits { *@
|
||||
@* get => Math.Min(selectedMemoryUnits, MaximumMemoryUnits); *@
|
||||
@* set => selectedMemoryUnits = value; *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* public RamAllocationUnits? MemoryAllocation => new RamAllocationUnits(MemoryUnits); *@
|
||||
@* *@
|
||||
@* [JvmArgumentsMustBeValid(ErrorMessage = "JVM arguments are not valid.")] *@
|
||||
@* public string JvmArguments { get; set; } = string.Empty; *@
|
||||
@* *@
|
||||
@* public sealed class ServerPortMustBeAllowedAttribute : FormValidationAttribute<ConfigureInstanceFormModel, int> { *@
|
||||
@* 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; *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* public sealed class RconPortMustBeAllowedAttribute : FormValidationAttribute<ConfigureInstanceFormModel, int> { *@
|
||||
@* 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; *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* public sealed class RconPortMustDifferFromServerPortAttribute : FormValidationAttribute<ConfigureInstanceFormModel, int?> { *@
|
||||
@* protected override string FieldName => nameof(RconPort); *@
|
||||
@* protected override bool IsValid(ConfigureInstanceFormModel model, int? value) => value != model.ServerPort; *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* public sealed class JvmArgumentsMustBeValidAttribute : FormCustomValidationAttribute<ConfigureInstanceFormModel, string> { *@
|
||||
@* protected override string FieldName => nameof(JvmArguments); *@
|
||||
@* *@
|
||||
@* protected override ValidationResult? Validate(ConfigureInstanceFormModel model, string value) { *@
|
||||
@* var error = JvmArgumentsHelper.Validate(value); *@
|
||||
@* return error == null ? ValidationResult.Success : new ValidationResult(error.ToSentence()); *@
|
||||
@* } *@
|
||||
@* } *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* protected override void OnInitialized() { *@
|
||||
@* form = new ConfigureInstanceFormModel(AgentManager, AgentJavaRuntimesManager, EditedInstanceConfiguration?.MemoryAllocation); *@
|
||||
@* *@
|
||||
@* if (EditedInstanceConfiguration != null) { *@
|
||||
@* form.SelectedAgentGuid = EditedInstanceConfiguration.AgentGuid; *@
|
||||
@* form.InstanceName = EditedInstanceConfiguration.InstanceName; *@
|
||||
@* form.ServerPort = EditedInstanceConfiguration.ServerPort; *@
|
||||
@* form.RconPort = EditedInstanceConfiguration.RconPort; *@
|
||||
@* form.MinecraftVersion = EditedInstanceConfiguration.MinecraftVersion; *@
|
||||
@* form.MinecraftServerKind = EditedInstanceConfiguration.MinecraftServerKind; *@
|
||||
@* form.MemoryUnits = EditedInstanceConfiguration.MemoryAllocation.RawValue; *@
|
||||
@* form.JavaRuntimeGuid = EditedInstanceConfiguration.JavaRuntimeGuid; *@
|
||||
@* 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.JavaRuntimeGuid)); *@
|
||||
@* form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.SelectedAgentGuid), revalidated: nameof(ConfigureInstanceFormModel.ServerPort)); *@
|
||||
@* form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.SelectedAgentGuid), revalidated: nameof(ConfigureInstanceFormModel.RconPort)); *@
|
||||
@* form.EditContext.RevalidateWhenFieldChanges(tracked: nameof(ConfigureInstanceFormModel.ServerPort), revalidated: nameof(ConfigureInstanceFormModel.RconPort)); *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* protected override async Task OnInitializedAsync() { *@
|
||||
@* if (EditedInstanceConfiguration != null) { *@
|
||||
@* var allMinecraftVersions = await MinecraftVersions.GetVersions(CancellationToken.None); *@
|
||||
@* minecraftVersionType = allMinecraftVersions.FirstOrDefault(version => version.Id == EditedInstanceConfiguration.MinecraftVersion)?.Type ?? minecraftVersionType; *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* await SetMinecraftVersionType(minecraftVersionType); *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* private async Task SetMinecraftVersionType(MinecraftVersionType type) { *@
|
||||
@* minecraftVersionType = type; *@
|
||||
@* *@
|
||||
@* var allMinecraftVersions = await MinecraftVersions.GetVersions(CancellationToken.None); *@
|
||||
@* availableMinecraftVersions = allMinecraftVersions.Where(version => version.Type == type).ToImmutableArray(); *@
|
||||
@* *@
|
||||
@* if (!availableMinecraftVersions.IsEmpty && !availableMinecraftVersions.Any(version => version.Id == form.MinecraftVersion)) { *@
|
||||
@* form.MinecraftVersion = availableMinecraftVersions[0].Id; *@
|
||||
@* } *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* private async Task AddOrEditInstance(EditContext context) { *@
|
||||
@* var selectedAgent = form.SelectedAgent; *@
|
||||
@* if (selectedAgent == null) { *@
|
||||
@* return; *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* await form.SubmitModel.StartSubmitting(); *@
|
||||
@* *@
|
||||
@* var instance = new InstanceConfiguration( *@
|
||||
@* EditedInstanceConfiguration?.AgentGuid ?? selectedAgent.Guid, *@
|
||||
@* EditedInstanceConfiguration?.InstanceGuid ?? Guid.NewGuid(), *@
|
||||
@* form.InstanceName, *@
|
||||
@* (ushort) form.ServerPort, *@
|
||||
@* (ushort) form.RconPort, *@
|
||||
@* form.MinecraftVersion, *@
|
||||
@* form.MinecraftServerKind, *@
|
||||
@* form.MemoryAllocation ?? RamAllocationUnits.Zero, *@
|
||||
@* form.JavaRuntimeGuid.GetValueOrDefault(), *@
|
||||
@* JvmArgumentsHelper.Split(form.JvmArguments) *@
|
||||
@* ); *@
|
||||
@* *@
|
||||
@* var result = await InstanceManager.AddOrEditInstance(instance); *@
|
||||
@* if (result.Is(AddOrEditInstanceResult.Success)) { *@
|
||||
@* await (EditedInstanceConfiguration == null ? AuditLog.AddInstanceCreatedEvent(instance.InstanceGuid) : AuditLog.AddInstanceEditedEvent(instance.InstanceGuid)); *@
|
||||
@* Nav.NavigateTo("instances/" + instance.InstanceGuid); *@
|
||||
@* } *@
|
||||
@* else { *@
|
||||
@* form.SubmitModel.StopSubmitting(result.ToSentence(AddOrEditInstanceResultExtensions.ToSentence)); *@
|
||||
@* } *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* } *@
|
||||
|
@ -1,53 +1,53 @@
|
||||
@using Phantom.Web.Services.Instances
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@using Phantom.Common.Data.Replies
|
||||
@inherits PhantomComponent
|
||||
@inject InstanceManager InstanceManager
|
||||
|
||||
<Form Model="form" OnSubmit="ExecuteCommand">
|
||||
<label for="command-input" class="form-label">Instance Name</label>
|
||||
<div class="input-group flex-nowrap">
|
||||
<span class="input-group-text" style="padding-top: 0.3rem;">/</span>
|
||||
<input id="command-input" class="form-control" type="text" placeholder="command" @bind="form.Command" @bind:event="oninput" disabled="@(Disabled || form.SubmitModel.IsSubmitting)" @ref="commandInputElement" />
|
||||
<FormButtonSubmit Label="Execute" class="btn btn-primary" disabled="@(Disabled || string.IsNullOrWhiteSpace(form.Command))" />
|
||||
</div>
|
||||
<FormSubmitError />
|
||||
</Form>
|
||||
|
||||
@code {
|
||||
|
||||
[Parameter]
|
||||
public Guid InstanceGuid { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
private readonly SendCommandFormModel form = new ();
|
||||
|
||||
private sealed class SendCommandFormModel : FormModel {
|
||||
public string Command { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private ElementReference commandInputElement;
|
||||
|
||||
private async Task ExecuteCommand(EditContext context) {
|
||||
await form.SubmitModel.StartSubmitting();
|
||||
|
||||
if (!await CheckPermission(Permission.ControlInstances)) {
|
||||
form.SubmitModel.StopSubmitting("You do not have permission to execute commands.");
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await InstanceManager.SendCommand(InstanceGuid, form.Command);
|
||||
if (result.Is(SendCommandToInstanceResult.Success)) {
|
||||
form.Command = string.Empty;
|
||||
form.SubmitModel.StopSubmitting();
|
||||
}
|
||||
else {
|
||||
form.SubmitModel.StopSubmitting(result.ToSentence(Messages.ToSentence));
|
||||
}
|
||||
|
||||
await commandInputElement.FocusAsync(preventScroll: true);
|
||||
}
|
||||
|
||||
}
|
||||
@* @using Phantom.Web.Services.Instances *@
|
||||
@* @using Phantom.Common.Data.Web.Users *@
|
||||
@* @using Phantom.Common.Data.Replies *@
|
||||
@* @inherits PhantomComponent *@
|
||||
@* @inject InstanceManager InstanceManager *@
|
||||
@* *@
|
||||
@* <Form Model="form" OnSubmit="ExecuteCommand"> *@
|
||||
@* <label for="command-input" class="form-label">Instance Name</label> *@
|
||||
@* <div class="input-group flex-nowrap"> *@
|
||||
@* <span class="input-group-text" style="padding-top: 0.3rem;">/</span> *@
|
||||
@* <input id="command-input" class="form-control" type="text" placeholder="command" @bind="form.Command" @bind:event="oninput" disabled="@(Disabled || form.SubmitModel.IsSubmitting)" @ref="commandInputElement" /> *@
|
||||
@* <FormButtonSubmit Label="Execute" class="btn btn-primary" disabled="@(Disabled || string.IsNullOrWhiteSpace(form.Command))" /> *@
|
||||
@* </div> *@
|
||||
@* <FormSubmitError /> *@
|
||||
@* </Form> *@
|
||||
@* *@
|
||||
@* @code { *@
|
||||
@* *@
|
||||
@* [Parameter] *@
|
||||
@* public Guid InstanceGuid { get; set; } *@
|
||||
@* *@
|
||||
@* [Parameter] *@
|
||||
@* public bool Disabled { get; set; } *@
|
||||
@* *@
|
||||
@* private readonly SendCommandFormModel form = new (); *@
|
||||
@* *@
|
||||
@* private sealed class SendCommandFormModel : FormModel { *@
|
||||
@* public string Command { get; set; } = string.Empty; *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* private ElementReference commandInputElement; *@
|
||||
@* *@
|
||||
@* private async Task ExecuteCommand(EditContext context) { *@
|
||||
@* await form.SubmitModel.StartSubmitting(); *@
|
||||
@* *@
|
||||
@* if (!await CheckPermission(Permission.ControlInstances)) { *@
|
||||
@* form.SubmitModel.StopSubmitting("You do not have permission to execute commands."); *@
|
||||
@* return; *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* var result = await InstanceManager.SendCommand(InstanceGuid, form.Command); *@
|
||||
@* if (result.Is(SendCommandToInstanceResult.Success)) { *@
|
||||
@* form.Command = string.Empty; *@
|
||||
@* form.SubmitModel.StopSubmitting(); *@
|
||||
@* } *@
|
||||
@* else { *@
|
||||
@* form.SubmitModel.StopSubmitting(result.ToSentence(Messages.ToSentence)); *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* await commandInputElement.FocusAsync(preventScroll: true); *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* } *@
|
||||
|
@ -1,71 +1,71 @@
|
||||
@inherits PhantomComponent
|
||||
@using Phantom.Utils.Collections
|
||||
@using System.Diagnostics
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@implements IDisposable
|
||||
@inject IJSRuntime Js;
|
||||
@inject InstanceLogManager InstanceLogManager
|
||||
|
||||
<div id="log" class="font-monospace mb-3">
|
||||
@foreach (var line in instanceLogs.EnumerateLast(uint.MaxValue)) {
|
||||
<p>@(new MarkupString(line))</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public Guid InstanceGuid { get; set; }
|
||||
|
||||
private IJSObjectReference? PageJs { get; set; }
|
||||
|
||||
private EventSubscribers<RingBuffer<string>> instanceLogsSubs = null!;
|
||||
private RingBuffer<string> instanceLogs = null!;
|
||||
|
||||
private readonly Stopwatch recheckPermissionsStopwatch = Stopwatch.StartNew();
|
||||
|
||||
protected override void OnInitialized() {
|
||||
instanceLogsSubs = InstanceLogManager.GetSubs(InstanceGuid);
|
||||
instanceLogsSubs.Subscribe(this, buffer => {
|
||||
instanceLogs = buffer;
|
||||
InvokeAsyncChecked(RefreshLog);
|
||||
});
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender) {
|
||||
if (firstRender) {
|
||||
PageJs = await Js.InvokeAsync<IJSObjectReference>("import", "./Shared/InstanceLog.razor.js");
|
||||
await RecheckPermissions();
|
||||
StateHasChanged();
|
||||
|
||||
await PageJs.InvokeVoidAsync("initLog");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshLog() {
|
||||
if (recheckPermissionsStopwatch.Elapsed > TimeSpan.FromSeconds(2)) {
|
||||
await RecheckPermissions();
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
|
||||
if (PageJs != null) {
|
||||
await PageJs.InvokeVoidAsync("scrollLog");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RecheckPermissions() {
|
||||
recheckPermissionsStopwatch.Restart();
|
||||
|
||||
if (!await CheckPermission(Permission.ViewInstanceLogs)) {
|
||||
await Task.Yield();
|
||||
Dispose();
|
||||
instanceLogs = new RingBuffer<string>(0);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
instanceLogsSubs.Unsubscribe(this);
|
||||
}
|
||||
|
||||
}
|
||||
@* @inherits PhantomComponent *@
|
||||
@* @using Phantom.Utils.Collections *@
|
||||
@* @using System.Diagnostics *@
|
||||
@* @using Phantom.Common.Data.Web.Users *@
|
||||
@* @implements IDisposable *@
|
||||
@* @inject IJSRuntime Js; *@
|
||||
@* @inject InstanceLogManager InstanceLogManager *@
|
||||
@* *@
|
||||
@* <div id="log" class="font-monospace mb-3"> *@
|
||||
@* @foreach (var line in instanceLogs.EnumerateLast(uint.MaxValue)) { *@
|
||||
@* <p>@(new MarkupString(line))</p> *@
|
||||
@* } *@
|
||||
@* </div> *@
|
||||
@* *@
|
||||
@* @code { *@
|
||||
@* *@
|
||||
@* [Parameter, EditorRequired] *@
|
||||
@* public Guid InstanceGuid { get; set; } *@
|
||||
@* *@
|
||||
@* private IJSObjectReference? PageJs { get; set; } *@
|
||||
@* *@
|
||||
@* private EventSubscribers<RingBuffer<string>> instanceLogsSubs = null!; *@
|
||||
@* private RingBuffer<string> instanceLogs = null!; *@
|
||||
@* *@
|
||||
@* private readonly Stopwatch recheckPermissionsStopwatch = Stopwatch.StartNew(); *@
|
||||
@* *@
|
||||
@* protected override void OnInitialized() { *@
|
||||
@* instanceLogsSubs = InstanceLogManager.GetSubs(InstanceGuid); *@
|
||||
@* instanceLogsSubs.Subscribe(this, buffer => { *@
|
||||
@* instanceLogs = buffer; *@
|
||||
@* InvokeAsyncChecked(RefreshLog); *@
|
||||
@* }); *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* protected override async Task OnAfterRenderAsync(bool firstRender) { *@
|
||||
@* if (firstRender) { *@
|
||||
@* PageJs = await Js.InvokeAsync<IJSObjectReference>("import", "./Shared/InstanceLog.razor.js"); *@
|
||||
@* await RecheckPermissions(); *@
|
||||
@* StateHasChanged(); *@
|
||||
@* *@
|
||||
@* await PageJs.InvokeVoidAsync("initLog"); *@
|
||||
@* } *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* private async Task RefreshLog() { *@
|
||||
@* if (recheckPermissionsStopwatch.Elapsed > TimeSpan.FromSeconds(2)) { *@
|
||||
@* await RecheckPermissions(); *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* StateHasChanged(); *@
|
||||
@* *@
|
||||
@* if (PageJs != null) { *@
|
||||
@* await PageJs.InvokeVoidAsync("scrollLog"); *@
|
||||
@* } *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* private async Task RecheckPermissions() { *@
|
||||
@* recheckPermissionsStopwatch.Restart(); *@
|
||||
@* *@
|
||||
@* if (!await CheckPermission(Permission.ViewInstanceLogs)) { *@
|
||||
@* await Task.Yield(); *@
|
||||
@* Dispose(); *@
|
||||
@* instanceLogs = new RingBuffer<string>(0); *@
|
||||
@* } *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* public void Dispose() { *@
|
||||
@* instanceLogsSubs.Unsubscribe(this); *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* } *@
|
||||
|
@ -1,70 +1,70 @@
|
||||
@using Phantom.Web.Services.Instances
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@using Phantom.Common.Data.Minecraft
|
||||
@using Phantom.Common.Data.Replies
|
||||
@inherits PhantomComponent
|
||||
@inject IJSRuntime Js;
|
||||
@inject InstanceManager InstanceManager;
|
||||
@inject AuditLog AuditLog
|
||||
|
||||
<Form Model="form" OnSubmit="StopInstance">
|
||||
<Modal Id="@ModalId" TitleText="Stop Instance">
|
||||
<Body>
|
||||
<FormSelectInput Id="stop-in-seconds" Label="Stop In..." @bind-Value="form.StopInSeconds">
|
||||
<option value="0">Immediately</option>
|
||||
<option value="10">10 Seconds</option>
|
||||
<option value="30">30 Seconds</option>
|
||||
<option value="60">1 Minute</option>
|
||||
<option value="120">2 Minutes</option>
|
||||
<option value="180">3 Minutes</option>
|
||||
<option value="240">4 Minutes</option>
|
||||
<option value="300">5 Minutes</option>
|
||||
</FormSelectInput>
|
||||
</Body>
|
||||
<Footer>
|
||||
<FormSubmitError />
|
||||
<FormButtonSubmit Label="Stop Instance" class="btn btn-danger" disabled="@Disabled" />
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
</Footer>
|
||||
</Modal>
|
||||
</Form>
|
||||
|
||||
@code {
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public Guid InstanceGuid { get; set; }
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public string ModalId { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
private readonly StopInstanceFormModel form = new ();
|
||||
|
||||
private sealed class StopInstanceFormModel : FormModel {
|
||||
[Range(minimum: 0, maximum: 300, ErrorMessage = "Stop delay must be between 0 and 300 seconds.")]
|
||||
public ushort StopInSeconds { get; set; } = 0;
|
||||
}
|
||||
|
||||
private async Task StopInstance(EditContext context) {
|
||||
await form.SubmitModel.StartSubmitting();
|
||||
|
||||
if (!await CheckPermission(Permission.ControlInstances)) {
|
||||
form.SubmitModel.StopSubmitting("You do not have permission to stop instances.");
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await InstanceManager.StopInstance(InstanceGuid, new MinecraftStopStrategy(form.StopInSeconds));
|
||||
if (result.Is(StopInstanceResult.StopInitiated)) {
|
||||
await AuditLog.AddInstanceStoppedEvent(InstanceGuid, form.StopInSeconds);
|
||||
await Js.InvokeVoidAsync("closeModal", ModalId);
|
||||
form.SubmitModel.StopSubmitting();
|
||||
}
|
||||
else {
|
||||
form.SubmitModel.StopSubmitting(result.ToSentence(Messages.ToSentence));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@* @using Phantom.Web.Services.Instances *@
|
||||
@* @using System.ComponentModel.DataAnnotations *@
|
||||
@* @using Phantom.Common.Data.Web.Users *@
|
||||
@* @using Phantom.Common.Data.Minecraft *@
|
||||
@* @using Phantom.Common.Data.Replies *@
|
||||
@* @inherits PhantomComponent *@
|
||||
@* @inject IJSRuntime Js; *@
|
||||
@* @inject InstanceManager InstanceManager; *@
|
||||
@* @inject AuditLog AuditLog *@
|
||||
@* *@
|
||||
@* <Form Model="form" OnSubmit="StopInstance"> *@
|
||||
@* <Modal Id="@ModalId" TitleText="Stop Instance"> *@
|
||||
@* <Body> *@
|
||||
@* <FormSelectInput Id="stop-in-seconds" Label="Stop In..." @bind-Value="form.StopInSeconds"> *@
|
||||
@* <option value="0">Immediately</option> *@
|
||||
@* <option value="10">10 Seconds</option> *@
|
||||
@* <option value="30">30 Seconds</option> *@
|
||||
@* <option value="60">1 Minute</option> *@
|
||||
@* <option value="120">2 Minutes</option> *@
|
||||
@* <option value="180">3 Minutes</option> *@
|
||||
@* <option value="240">4 Minutes</option> *@
|
||||
@* <option value="300">5 Minutes</option> *@
|
||||
@* </FormSelectInput> *@
|
||||
@* </Body> *@
|
||||
@* <Footer> *@
|
||||
@* <FormSubmitError /> *@
|
||||
@* <FormButtonSubmit Label="Stop Instance" class="btn btn-danger" disabled="@Disabled" /> *@
|
||||
@* <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> *@
|
||||
@* </Footer> *@
|
||||
@* </Modal> *@
|
||||
@* </Form> *@
|
||||
@* *@
|
||||
@* @code { *@
|
||||
@* *@
|
||||
@* [Parameter, EditorRequired] *@
|
||||
@* public Guid InstanceGuid { get; set; } *@
|
||||
@* *@
|
||||
@* [Parameter, EditorRequired] *@
|
||||
@* public string ModalId { get; set; } = string.Empty; *@
|
||||
@* *@
|
||||
@* [Parameter] *@
|
||||
@* public bool Disabled { get; set; } *@
|
||||
@* *@
|
||||
@* private readonly StopInstanceFormModel form = new (); *@
|
||||
@* *@
|
||||
@* private sealed class StopInstanceFormModel : FormModel { *@
|
||||
@* [Range(minimum: 0, maximum: 300, ErrorMessage = "Stop delay must be between 0 and 300 seconds.")] *@
|
||||
@* public ushort StopInSeconds { get; set; } = 0; *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* private async Task StopInstance(EditContext context) { *@
|
||||
@* await form.SubmitModel.StartSubmitting(); *@
|
||||
@* *@
|
||||
@* if (!await CheckPermission(Permission.ControlInstances)) { *@
|
||||
@* form.SubmitModel.StopSubmitting("You do not have permission to stop instances."); *@
|
||||
@* return; *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* var result = await InstanceManager.StopInstance(InstanceGuid, new MinecraftStopStrategy(form.StopInSeconds)); *@
|
||||
@* if (result.Is(StopInstanceResult.StopInitiated)) { *@
|
||||
@* await AuditLog.AddInstanceStoppedEvent(InstanceGuid, form.StopInSeconds); *@
|
||||
@* await Js.InvokeVoidAsync("closeModal", ModalId); *@
|
||||
@* form.SubmitModel.StopSubmitting(); *@
|
||||
@* } *@
|
||||
@* else { *@
|
||||
@* form.SubmitModel.StopSubmitting(result.ToSentence(Messages.ToSentence)); *@
|
||||
@* } *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* } *@
|
||||
|
@ -1,72 +1,72 @@
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@using Phantom.Utils.Tasks
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@inherits PhantomComponent
|
||||
@inject IJSRuntime Js;
|
||||
|
||||
<Form Model="form" OnSubmit="AddUser">
|
||||
<Modal Id="@ModalId" TitleText="Add User">
|
||||
<Body>
|
||||
|
||||
<div class="row">
|
||||
<div class="mb-3">
|
||||
<FormTextInput Id="account-username" Label="Username" @bind-Value="form.Username" autocomplete="off" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="mb-3">
|
||||
<FormTextInput Id="account-password" Label="Password" Type="FormTextInputType.Password" autocomplete="new-password" @bind-Value="form.Password" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</Body>
|
||||
<Footer>
|
||||
<FormSubmitError />
|
||||
<FormButtonSubmit Label="Add User" class="btn btn-primary" />
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
</Footer>
|
||||
</Modal>
|
||||
</Form>
|
||||
|
||||
@code {
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public string ModalId { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<UserInfo> UserAdded { get; set; }
|
||||
|
||||
private readonly AddUserFormModel form = new();
|
||||
|
||||
private sealed class AddUserFormModel : FormModel {
|
||||
[Required]
|
||||
public string Username { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string Password { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private async Task AddUser(EditContext context) {
|
||||
await form.SubmitModel.StartSubmitting();
|
||||
|
||||
if (!await CheckPermission(Permission.EditUsers)) {
|
||||
form.SubmitModel.StopSubmitting("You do not have permission to add users.");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (await UserManager.CreateUser(form.Username, form.Password)) {
|
||||
case Result<UserInfo, AddUserError>.Ok ok:
|
||||
await AuditLog.AddUserCreatedEvent(ok.Value);
|
||||
await UserAdded.InvokeAsync(ok.Value);
|
||||
await Js.InvokeVoidAsync("closeModal", ModalId);
|
||||
form.SubmitModel.StopSubmitting();
|
||||
break;
|
||||
|
||||
case Result<UserInfo, AddUserError>.Fail fail:
|
||||
form.SubmitModel.StopSubmitting(fail.Error.ToSentences("\n"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@* @using Phantom.Common.Data.Web.Users *@
|
||||
@* @using Phantom.Utils.Tasks *@
|
||||
@* @using System.ComponentModel.DataAnnotations *@
|
||||
@* @inherits PhantomComponent *@
|
||||
@* @inject IJSRuntime Js; *@
|
||||
@* *@
|
||||
@* <Form Model="form" OnSubmit="AddUser"> *@
|
||||
@* <Modal Id="@ModalId" TitleText="Add User"> *@
|
||||
@* <Body> *@
|
||||
@* *@
|
||||
@* <div class="row"> *@
|
||||
@* <div class="mb-3"> *@
|
||||
@* <FormTextInput Id="account-username" Label="Username" @bind-Value="form.Username" autocomplete="off" /> *@
|
||||
@* </div> *@
|
||||
@* </div> *@
|
||||
@* *@
|
||||
@* <div class="row"> *@
|
||||
@* <div class="mb-3"> *@
|
||||
@* <FormTextInput Id="account-password" Label="Password" Type="FormTextInputType.Password" autocomplete="new-password" @bind-Value="form.Password" /> *@
|
||||
@* </div> *@
|
||||
@* </div> *@
|
||||
@* *@
|
||||
@* </Body> *@
|
||||
@* <Footer> *@
|
||||
@* <FormSubmitError /> *@
|
||||
@* <FormButtonSubmit Label="Add User" class="btn btn-primary" /> *@
|
||||
@* <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> *@
|
||||
@* </Footer> *@
|
||||
@* </Modal> *@
|
||||
@* </Form> *@
|
||||
@* *@
|
||||
@* @code { *@
|
||||
@* *@
|
||||
@* [Parameter, EditorRequired] *@
|
||||
@* public string ModalId { get; set; } = string.Empty; *@
|
||||
@* *@
|
||||
@* [Parameter] *@
|
||||
@* public EventCallback<UserInfo> UserAdded { get; set; } *@
|
||||
@* *@
|
||||
@* private readonly AddUserFormModel form = new(); *@
|
||||
@* *@
|
||||
@* private sealed class AddUserFormModel : FormModel { *@
|
||||
@* [Required] *@
|
||||
@* public string Username { get; set; } = string.Empty; *@
|
||||
@* *@
|
||||
@* [Required] *@
|
||||
@* public string Password { get; set; } = string.Empty; *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* private async Task AddUser(EditContext context) { *@
|
||||
@* await form.SubmitModel.StartSubmitting(); *@
|
||||
@* *@
|
||||
@* if (!await CheckPermission(Permission.EditUsers)) { *@
|
||||
@* form.SubmitModel.StopSubmitting("You do not have permission to add users."); *@
|
||||
@* return; *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* switch (await UserManager.CreateUser(form.Username, form.Password)) { *@
|
||||
@* case Result<UserInfo, AddUserError>.Ok ok: *@
|
||||
@* await AuditLog.AddUserCreatedEvent(ok.Value); *@
|
||||
@* await UserAdded.InvokeAsync(ok.Value); *@
|
||||
@* await Js.InvokeVoidAsync("closeModal", ModalId); *@
|
||||
@* form.SubmitModel.StopSubmitting(); *@
|
||||
@* break; *@
|
||||
@* *@
|
||||
@* case Result<UserInfo, AddUserError>.Fail fail: *@
|
||||
@* form.SubmitModel.StopSubmitting(fail.Error.ToSentences("\n")); *@
|
||||
@* break; *@
|
||||
@* } *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* } *@
|
||||
|
@ -1,37 +1,37 @@
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@inherits UserEditDialogBase
|
||||
@inject UserManager UserManager
|
||||
@inject AuditLog AuditLog
|
||||
|
||||
<Modal Id="@ModalId" TitleText="Delete User">
|
||||
<Body>
|
||||
You are about to delete the user <strong class="fw-semibold">@EditedUserName</strong>.<br>
|
||||
This action cannot be undone.
|
||||
</Body>
|
||||
<Footer>
|
||||
<FormSubmitError Model="SubmitModel" />
|
||||
<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>
|
||||
</Footer>
|
||||
</Modal>
|
||||
|
||||
@code {
|
||||
|
||||
protected override async Task DoEdit(UserInfo user) {
|
||||
switch (await UserManager.DeleteByGuid(user.Guid)) {
|
||||
case DeleteUserResult.Deleted:
|
||||
await AuditLog.AddUserDeletedEvent(user);
|
||||
await OnEditSuccess();
|
||||
break;
|
||||
|
||||
case DeleteUserResult.NotFound:
|
||||
await OnEditSuccess();
|
||||
break;
|
||||
|
||||
case DeleteUserResult.Failed:
|
||||
OnEditFailure("Could not delete user.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@* @using Phantom.Common.Data.Web.Users *@
|
||||
@* @inherits UserEditDialogBase *@
|
||||
@* @inject UserManager UserManager *@
|
||||
@* @inject AuditLog AuditLog *@
|
||||
@* *@
|
||||
@* <Modal Id="@ModalId" TitleText="Delete User"> *@
|
||||
@* <Body> *@
|
||||
@* You are about to delete the user <strong class="fw-semibold">@EditedUserName</strong>.<br> *@
|
||||
@* This action cannot be undone. *@
|
||||
@* </Body> *@
|
||||
@* <Footer> *@
|
||||
@* <FormSubmitError Model="SubmitModel" /> *@
|
||||
@* <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> *@
|
||||
@* </Footer> *@
|
||||
@* </Modal> *@
|
||||
@* *@
|
||||
@* @code { *@
|
||||
@* *@
|
||||
@* protected override async Task DoEdit(UserInfo user) { *@
|
||||
@* switch (await UserManager.DeleteByGuid(user.Guid)) { *@
|
||||
@* case DeleteUserResult.Deleted: *@
|
||||
@* await AuditLog.AddUserDeletedEvent(user); *@
|
||||
@* await OnEditSuccess(); *@
|
||||
@* break; *@
|
||||
@* *@
|
||||
@* case DeleteUserResult.NotFound: *@
|
||||
@* await OnEditSuccess(); *@
|
||||
@* break; *@
|
||||
@* *@
|
||||
@* case DeleteUserResult.Failed: *@
|
||||
@* OnEditFailure("Could not delete user."); *@
|
||||
@* break; *@
|
||||
@* } *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* } *@
|
||||
|
@ -1,76 +1,76 @@
|
||||
@using Phantom.Common.Data.Web.Users
|
||||
@inherits UserEditDialogBase
|
||||
|
||||
<Modal Id="@ModalId" TitleText="Manage User Roles">
|
||||
<Body>
|
||||
Roles for user: <strong class="fw-semibold">@EditedUserName</strong><br>
|
||||
@for (var index = 0; index < items.Count; index++) {
|
||||
var item = items[index];
|
||||
<div class="mt-1">
|
||||
<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>
|
||||
</div>
|
||||
}
|
||||
</Body>
|
||||
<Footer>
|
||||
<FormSubmitError Model="SubmitModel" />
|
||||
<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>
|
||||
</Footer>
|
||||
</Modal>
|
||||
|
||||
@code {
|
||||
|
||||
private List<RoleItem> items = new();
|
||||
|
||||
protected override async Task BeforeShown(UserInfo user) {
|
||||
var userRoles = await UserRoleManager.GetUserRoleGuids(user);
|
||||
var allRoles = await RoleManager.GetAll();
|
||||
this.items = allRoles.Select(role => new RoleItem(role, userRoles.Contains(role.RoleGuid))).ToList();
|
||||
}
|
||||
|
||||
protected override async Task DoEdit(UserInfo user) {
|
||||
var userRoles = await UserRoleManager.GetUserRoleGuids(user);
|
||||
var addedToRoles = new List<string>();
|
||||
var removedFromRoles = new List<string>();
|
||||
var errors = new List<string>();
|
||||
|
||||
foreach (var item in items) {
|
||||
var shouldHaveRole = item.Checked;
|
||||
if (shouldHaveRole == userRoles.Contains(item.Role.Guid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool success = shouldHaveRole ? await UserRoleManager.Add(user, item.Role) : await UserRoleManager.Remove(user, item.Role);
|
||||
if (success) {
|
||||
var modifiedList = shouldHaveRole ? addedToRoles : removedFromRoles;
|
||||
modifiedList.Add(item.Role.Name);
|
||||
}
|
||||
else if (shouldHaveRole) {
|
||||
errors.Add("Could not add role " + item.Role.Name + " to user.");
|
||||
}
|
||||
else {
|
||||
errors.Add("Could not remove role " + item.Role.Name + " from user.");
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.Count == 0) {
|
||||
await AuditLog.AddUserRolesChangedEvent(user, addedToRoles, removedFromRoles);
|
||||
await OnEditSuccess();
|
||||
}
|
||||
else {
|
||||
OnEditFailure(string.Join("\n", errors));
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RoleItem {
|
||||
public RoleInfo Role { get; }
|
||||
public bool Checked { get; set; }
|
||||
|
||||
public RoleItem(RoleInfo role, bool @checked) {
|
||||
this.Role = role;
|
||||
this.Checked = @checked;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@* @using Phantom.Common.Data.Web.Users *@
|
||||
@* @inherits UserEditDialogBase *@
|
||||
@* *@
|
||||
@* <Modal Id="@ModalId" TitleText="Manage User Roles"> *@
|
||||
@* <Body> *@
|
||||
@* Roles for user: <strong class="fw-semibold">@EditedUserName</strong><br> *@
|
||||
@* @for (var index = 0; index < items.Count; index++) { *@
|
||||
@* var item = items[index]; *@
|
||||
@* <div class="mt-1"> *@
|
||||
@* <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> *@
|
||||
@* </div> *@
|
||||
@* } *@
|
||||
@* </Body> *@
|
||||
@* <Footer> *@
|
||||
@* <FormSubmitError Model="SubmitModel" /> *@
|
||||
@* <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> *@
|
||||
@* </Footer> *@
|
||||
@* </Modal> *@
|
||||
@* *@
|
||||
@* @code { *@
|
||||
@* *@
|
||||
@* private List<RoleItem> items = new(); *@
|
||||
@* *@
|
||||
@* protected override async Task BeforeShown(UserInfo user) { *@
|
||||
@* var userRoles = await UserRoleManager.GetUserRoleGuids(user); *@
|
||||
@* var allRoles = await RoleManager.GetAll(); *@
|
||||
@* this.items = allRoles.Select(role => new RoleItem(role, userRoles.Contains(role.RoleGuid))).ToList(); *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* protected override async Task DoEdit(UserInfo user) { *@
|
||||
@* var userRoles = await UserRoleManager.GetUserRoleGuids(user); *@
|
||||
@* var addedToRoles = new List<string>(); *@
|
||||
@* var removedFromRoles = new List<string>(); *@
|
||||
@* var errors = new List<string>(); *@
|
||||
@* *@
|
||||
@* foreach (var item in items) { *@
|
||||
@* var shouldHaveRole = item.Checked; *@
|
||||
@* if (shouldHaveRole == userRoles.Contains(item.Role.Guid)) { *@
|
||||
@* continue; *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* bool success = shouldHaveRole ? await UserRoleManager.Add(user, item.Role) : await UserRoleManager.Remove(user, item.Role); *@
|
||||
@* if (success) { *@
|
||||
@* var modifiedList = shouldHaveRole ? addedToRoles : removedFromRoles; *@
|
||||
@* modifiedList.Add(item.Role.Name); *@
|
||||
@* } *@
|
||||
@* else if (shouldHaveRole) { *@
|
||||
@* errors.Add("Could not add role " + item.Role.Name + " to user."); *@
|
||||
@* } *@
|
||||
@* else { *@
|
||||
@* errors.Add("Could not remove role " + item.Role.Name + " from user."); *@
|
||||
@* } *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* if (errors.Count == 0) { *@
|
||||
@* await AuditLog.AddUserRolesChangedEvent(user, addedToRoles, removedFromRoles); *@
|
||||
@* await OnEditSuccess(); *@
|
||||
@* } *@
|
||||
@* else { *@
|
||||
@* OnEditFailure(string.Join("\n", errors)); *@
|
||||
@* } *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* private sealed class RoleItem { *@
|
||||
@* public RoleInfo Role { get; } *@
|
||||
@* public bool Checked { get; set; } *@
|
||||
@* *@
|
||||
@* public RoleItem(RoleInfo role, bool @checked) { *@
|
||||
@* this.Role = role; *@
|
||||
@* this.Checked = @checked; *@
|
||||
@* } *@
|
||||
@* } *@
|
||||
@* *@
|
||||
@* } *@
|
||||
|
@ -28,7 +28,7 @@ static class WebLauncher {
|
||||
builder.Services.AddSingleton(taskManager);
|
||||
builder.Services.AddSingleton(serviceConfiguration);
|
||||
builder.Services.AddSingleton(controllerConnection);
|
||||
builder.Services.AddPhantomServices();
|
||||
builder.Services.AddPhantomServices(config.CancellationToken);
|
||||
|
||||
builder.Services.AddSingleton<IHostLifetime>(new NullLifetime());
|
||||
builder.Services.AddScoped<INavigation>(Navigation.Create(config.BasePath));
|
||||
|
Loading…
Reference in New Issue
Block a user