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

Compare commits

..

2 Commits

Author SHA1 Message Date
891a999ffd
Add event log 2023-02-24 17:22:19 +01:00
c0bfe8f403
Rename audit log classes and entities 2023-02-13 05:30:32 +01:00
44 changed files with 1711 additions and 150 deletions

View File

@ -19,6 +19,8 @@ sealed class BackupScheduler : CancellableBackgroundTask {
private readonly ServerStatusProtocol serverStatusProtocol; private readonly ServerStatusProtocol serverStatusProtocol;
private readonly ManualResetEventSlim serverOutputWhileWaitingForOnlinePlayers = new (); private readonly ManualResetEventSlim serverOutputWhileWaitingForOnlinePlayers = new ();
public event EventHandler<BackupCreationResult>? BackupCompleted;
public BackupScheduler(TaskManager taskManager, BackupManager backupManager, InstanceSession session, int serverPort, string loggerName) : base(PhantomLogger.Create<BackupScheduler>(loggerName), taskManager, "Backup scheduler for " + loggerName) { public BackupScheduler(TaskManager taskManager, BackupManager backupManager, InstanceSession session, int serverPort, string loggerName) : base(PhantomLogger.Create<BackupScheduler>(loggerName), taskManager, "Backup scheduler for " + loggerName) {
this.loggerName = loggerName; this.loggerName = loggerName;
this.backupManager = backupManager; this.backupManager = backupManager;
@ -33,6 +35,8 @@ sealed class BackupScheduler : CancellableBackgroundTask {
while (!CancellationToken.IsCancellationRequested) { while (!CancellationToken.IsCancellationRequested) {
var result = await CreateBackup(); var result = await CreateBackup();
BackupCompleted?.Invoke(this, result);
if (result.Kind.ShouldRetry()) { if (result.Kind.ShouldRetry()) {
Logger.Warning("Scheduled backup failed, retrying in {Minutes} minutes.", BackupFailureRetryDelay.TotalMinutes); Logger.Warning("Scheduled backup failed, retrying in {Minutes} minutes.", BackupFailureRetryDelay.TotalMinutes);
await Task.Delay(BackupFailureRetryDelay, CancellationToken); await Task.Delay(BackupFailureRetryDelay, CancellationToken);

View File

@ -18,9 +18,9 @@ sealed class Instance : IDisposable {
return prefix[..prefix.IndexOf('-')] + "/" + Interlocked.Increment(ref loggerSequenceId); return prefix[..prefix.IndexOf('-')] + "/" + Interlocked.Increment(ref loggerSequenceId);
} }
public static async Task<Instance> Create(InstanceConfiguration configuration, InstanceServices services, BaseLauncher launcher) { public static Instance Create(InstanceConfiguration configuration, InstanceServices services, BaseLauncher launcher) {
var instance = new Instance(configuration, services, launcher); var instance = new Instance(configuration, services, launcher);
await instance.ReportLastStatus(); instance.SetStatus(instance.currentStatus);
return instance; return instance;
} }
@ -32,6 +32,8 @@ sealed class Instance : IDisposable {
private readonly ILogger logger; private readonly ILogger logger;
private IInstanceStatus currentStatus; private IInstanceStatus currentStatus;
private int statusUpdateCounter;
private IInstanceState currentState; private IInstanceState currentState;
private readonly SemaphoreSlim stateTransitioningActionSemaphore = new (1, 1); private readonly SemaphoreSlim stateTransitioningActionSemaphore = new (1, 1);
@ -51,8 +53,20 @@ sealed class Instance : IDisposable {
this.currentStatus = InstanceStatus.NotRunning; this.currentStatus = InstanceStatus.NotRunning;
} }
private async Task ReportLastStatus() { private void SetStatus(IInstanceStatus status) {
await ServerMessaging.Send(new ReportInstanceStatusMessage(Configuration.InstanceGuid, currentStatus)); int myStatusUpdateCounter = Interlocked.Increment(ref statusUpdateCounter);
Services.TaskManager.Run("Report status of instance " + shortName + " as " + status.GetType().Name, async () => {
if (myStatusUpdateCounter == statusUpdateCounter) {
currentStatus = status;
await ServerMessaging.Send(new ReportInstanceStatusMessage(Configuration.InstanceGuid, status));
}
});
}
private void ReportEvent(IInstanceEvent instanceEvent) {
var message = new ReportInstanceEventMessage(Guid.NewGuid(), DateTime.UtcNow, Configuration.InstanceGuid, instanceEvent);
Services.TaskManager.Run("Report event for instance " + shortName, async () => await ServerMessaging.Send(message));
} }
private void TransitionState(IInstanceState newState) { private void TransitionState(IInstanceState newState) {
@ -85,7 +99,6 @@ sealed class Instance : IDisposable {
try { try {
Configuration = configuration; Configuration = configuration;
Launcher = launcher; Launcher = launcher;
await ReportLastStatus();
} finally { } finally {
stateTransitioningActionSemaphore.Release(); stateTransitioningActionSemaphore.Release();
} }
@ -134,9 +147,7 @@ sealed class Instance : IDisposable {
private readonly Instance instance; private readonly Instance instance;
private readonly CancellationToken shutdownCancellationToken; private readonly CancellationToken shutdownCancellationToken;
private int statusUpdateCounter; public InstanceContextImpl(Instance instance, CancellationToken shutdownCancellationToken) : base(instance.Configuration, instance.Launcher, instance.Services) {
public InstanceContextImpl(Instance instance, CancellationToken shutdownCancellationToken) : base(instance.Configuration, instance.Services, instance.Launcher) {
this.instance = instance; this.instance = instance;
this.shutdownCancellationToken = shutdownCancellationToken; this.shutdownCancellationToken = shutdownCancellationToken;
} }
@ -144,15 +155,12 @@ sealed class Instance : IDisposable {
public override ILogger Logger => instance.logger; public override ILogger Logger => instance.logger;
public override string ShortName => instance.shortName; public override string ShortName => instance.shortName;
public override void ReportStatus(IInstanceStatus newStatus) { public override void SetStatus(IInstanceStatus newStatus) {
int myStatusUpdateCounter = Interlocked.Increment(ref statusUpdateCounter); instance.SetStatus(newStatus);
instance.Services.TaskManager.Run("Report status of instance " + instance.shortName + " as " + newStatus.GetType().Name, async () => {
if (myStatusUpdateCounter == statusUpdateCounter) {
instance.currentStatus = newStatus;
await ServerMessaging.Send(new ReportInstanceStatusMessage(Configuration.InstanceGuid, newStatus));
} }
});
public override void ReportEvent(IInstanceEvent instanceEvent) {
instance.ReportEvent(instanceEvent);
} }
public override void TransitionState(Func<(IInstanceState, IInstanceStatus?)> newStateAndStatus) { public override void TransitionState(Func<(IInstanceState, IInstanceStatus?)> newStateAndStatus) {
@ -172,7 +180,7 @@ sealed class Instance : IDisposable {
} }
if (status != null) { if (status != null) {
ReportStatus(status); SetStatus(status);
} }
instance.TransitionState(state); instance.TransitionState(state);

View File

@ -7,19 +7,26 @@ namespace Phantom.Agent.Services.Instances;
abstract class InstanceContext { abstract class InstanceContext {
public InstanceConfiguration Configuration { get; } public InstanceConfiguration Configuration { get; }
public InstanceServices Services { get; }
public BaseLauncher Launcher { get; } public BaseLauncher Launcher { get; }
public InstanceServices Services { get; }
public abstract ILogger Logger { get; } public abstract ILogger Logger { get; }
public abstract string ShortName { get; } public abstract string ShortName { get; }
protected InstanceContext(InstanceConfiguration configuration, InstanceServices services, BaseLauncher launcher) { protected InstanceContext(InstanceConfiguration configuration, BaseLauncher launcher, InstanceServices services) {
Configuration = configuration; Configuration = configuration;
Launcher = launcher; Launcher = launcher;
Services = services; Services = services;
} }
public abstract void ReportStatus(IInstanceStatus newStatus); public abstract void SetStatus(IInstanceStatus newStatus);
public void SetLaunchFailedStatusAndReportEvent(InstanceLaunchFailReason reason) {
SetStatus(InstanceStatus.Failed(reason));
ReportEvent(new InstanceLaunchFailedEvent(reason));
}
public abstract void ReportEvent(IInstanceEvent instanceEvent);
public abstract void TransitionState(Func<(IInstanceState, IInstanceStatus?)> newStateAndStatus); public abstract void TransitionState(Func<(IInstanceState, IInstanceStatus?)> newStateAndStatus);
public void TransitionState(IInstanceState newState, IInstanceStatus? newStatus = null) { public void TransitionState(IInstanceState newState, IInstanceStatus? newStatus = null) {

View File

@ -100,7 +100,7 @@ sealed class InstanceSessionManager : IDisposable {
Logger.Information("Reconfigured instance \"{Name}\" (GUID {Guid}).", configuration.InstanceName, configuration.InstanceGuid); Logger.Information("Reconfigured instance \"{Name}\" (GUID {Guid}).", configuration.InstanceName, configuration.InstanceGuid);
} }
else { else {
instances[instanceGuid] = instance = await Instance.Create(configuration, instanceServices, launcher); instances[instanceGuid] = instance = Instance.Create(configuration, instanceServices, launcher);
instance.IsRunningChanged += OnInstanceIsRunningChanged; instance.IsRunningChanged += OnInstanceIsRunningChanged;
Logger.Information("Created instance \"{Name}\" (GUID {Guid}).", configuration.InstanceName, configuration.InstanceGuid); Logger.Information("Created instance \"{Name}\" (GUID {Guid}).", configuration.InstanceName, configuration.InstanceGuid);
} }

View File

@ -33,7 +33,7 @@ sealed class InstanceLaunchingState : IInstanceState, IDisposable {
if (lastDownloadProgress != progress) { if (lastDownloadProgress != progress) {
lastDownloadProgress = progress; lastDownloadProgress = progress;
context.ReportStatus(InstanceStatus.Downloading(progress)); context.SetStatus(InstanceStatus.Downloading(progress));
} }
} }
@ -58,7 +58,7 @@ sealed class InstanceLaunchingState : IInstanceState, IDisposable {
throw new LaunchFailureException(InstanceLaunchFailReason.UnknownError, "Session failed to launch."); throw new LaunchFailureException(InstanceLaunchFailReason.UnknownError, "Session failed to launch.");
} }
context.ReportStatus(InstanceStatus.Launching); context.SetStatus(InstanceStatus.Launching);
return launchSuccess.Session; return launchSuccess.Session;
} }
@ -69,6 +69,7 @@ sealed class InstanceLaunchingState : IInstanceState, IDisposable {
return (new InstanceNotRunningState(), InstanceStatus.NotRunning); return (new InstanceNotRunningState(), InstanceStatus.NotRunning);
} }
else { else {
context.ReportEvent(InstanceEvent.LaunchSucceded);
return (new InstanceRunningState(context, task.Result), null); return (new InstanceRunningState(context, task.Result), null);
} }
}); });
@ -77,10 +78,10 @@ sealed class InstanceLaunchingState : IInstanceState, IDisposable {
private void OnLaunchFailure(Task task) { private void OnLaunchFailure(Task task) {
if (task.Exception is { InnerException: LaunchFailureException e }) { if (task.Exception is { InnerException: LaunchFailureException e }) {
context.Logger.Error(e.LogMessage); context.Logger.Error(e.LogMessage);
context.ReportStatus(InstanceStatus.Failed(e.Reason)); context.SetLaunchFailedStatusAndReportEvent(e.Reason);
} }
else { else {
context.ReportStatus(InstanceStatus.Failed(InstanceLaunchFailReason.UnknownError)); context.SetLaunchFailedStatusAndReportEvent(InstanceLaunchFailReason.UnknownError);
} }
context.Services.PortManager.Release(context.Configuration); context.Services.PortManager.Release(context.Configuration);

View File

@ -16,12 +16,12 @@ sealed class InstanceNotRunningState : IInstanceState {
_ => null _ => null
}; };
if (failReason != null) { if (failReason is {} reason) {
context.ReportStatus(InstanceStatus.Failed(failReason.Value)); context.SetLaunchFailedStatusAndReportEvent(reason);
return (this, LaunchInstanceResult.LaunchInitiated); return (this, LaunchInstanceResult.LaunchInitiated);
} }
context.ReportStatus(InstanceStatus.Launching); context.SetStatus(InstanceStatus.Launching);
return (new InstanceLaunchingState(context), LaunchInstanceResult.LaunchInitiated); return (new InstanceLaunchingState(context), LaunchInstanceResult.LaunchInitiated);
} }

View File

@ -1,6 +1,7 @@
using Phantom.Agent.Minecraft.Command; using Phantom.Agent.Minecraft.Command;
using Phantom.Agent.Minecraft.Instance; using Phantom.Agent.Minecraft.Instance;
using Phantom.Agent.Services.Backups; using Phantom.Agent.Services.Backups;
using Phantom.Common.Data.Backups;
using Phantom.Common.Data.Instance; using Phantom.Common.Data.Instance;
using Phantom.Common.Data.Minecraft; using Phantom.Common.Data.Minecraft;
using Phantom.Common.Data.Replies; using Phantom.Common.Data.Replies;
@ -23,6 +24,7 @@ sealed class InstanceRunningState : IInstanceState {
this.session = session; this.session = session;
this.logSender = new InstanceLogSender(context.Services.TaskManager, context.Configuration.InstanceGuid, context.ShortName); this.logSender = new InstanceLogSender(context.Services.TaskManager, context.Configuration.InstanceGuid, context.ShortName);
this.backupScheduler = new BackupScheduler(context.Services.TaskManager, context.Services.BackupManager, session, context.Configuration.ServerPort, context.ShortName); this.backupScheduler = new BackupScheduler(context.Services.TaskManager, context.Services.BackupManager, session, context.Configuration.ServerPort, context.ShortName);
this.backupScheduler.BackupCompleted += OnScheduledBackupCompleted;
this.sessionObjects = new SessionObjects(this); this.sessionObjects = new SessionObjects(this);
} }
@ -33,11 +35,12 @@ sealed class InstanceRunningState : IInstanceState {
if (session.HasEnded) { if (session.HasEnded) {
if (sessionObjects.Dispose()) { if (sessionObjects.Dispose()) {
context.Logger.Warning("Session ended immediately after it was started."); context.Logger.Warning("Session ended immediately after it was started.");
context.ReportEvent(InstanceEvent.Stopped);
context.Services.TaskManager.Run("Transition state of instance " + context.ShortName + " to not running", () => context.TransitionState(new InstanceNotRunningState(), InstanceStatus.Failed(InstanceLaunchFailReason.UnknownError))); context.Services.TaskManager.Run("Transition state of instance " + context.ShortName + " to not running", () => context.TransitionState(new InstanceNotRunningState(), InstanceStatus.Failed(InstanceLaunchFailReason.UnknownError)));
} }
} }
else { else {
context.ReportStatus(InstanceStatus.Running); context.SetStatus(InstanceStatus.Running);
context.Logger.Information("Session started."); context.Logger.Information("Session started.");
} }
} }
@ -54,10 +57,12 @@ sealed class InstanceRunningState : IInstanceState {
if (isStopping) { if (isStopping) {
context.Logger.Information("Session ended."); context.Logger.Information("Session ended.");
context.ReportEvent(InstanceEvent.Stopped);
context.TransitionState(new InstanceNotRunningState(), InstanceStatus.NotRunning); context.TransitionState(new InstanceNotRunningState(), InstanceStatus.NotRunning);
} }
else { else {
context.Logger.Information("Session ended unexpectedly, restarting..."); context.Logger.Information("Session ended unexpectedly, restarting...");
context.ReportEvent(InstanceEvent.Crashed);
context.TransitionState(new InstanceLaunchingState(context), InstanceStatus.Restarting); context.TransitionState(new InstanceLaunchingState(context), InstanceStatus.Restarting);
} }
} }
@ -139,6 +144,10 @@ sealed class InstanceRunningState : IInstanceState {
} }
} }
private void OnScheduledBackupCompleted(object? sender, BackupCreationResult e) {
context.ReportEvent(new InstanceBackupCompletedEvent(e.Kind, e.Warnings));
}
public sealed class SessionObjects { public sealed class SessionObjects {
private readonly InstanceRunningState state; private readonly InstanceRunningState state;
private bool isDisposed; private bool isDisposed;

View File

@ -20,7 +20,7 @@ sealed class InstanceStoppingState : IInstanceState, IDisposable {
public void Initialize() { public void Initialize() {
context.Logger.Information("Session stopping."); context.Logger.Information("Session stopping.");
context.ReportStatus(InstanceStatus.Stopping); context.SetStatus(InstanceStatus.Stopping);
context.Services.TaskManager.Run("Stop procedure for instance " + context.ShortName, DoStop); context.Services.TaskManager.Run("Stop procedure for instance " + context.ShortName, DoStop);
} }
@ -39,6 +39,7 @@ sealed class InstanceStoppingState : IInstanceState, IDisposable {
await DoWaitForSessionToEnd(); await DoWaitForSessionToEnd();
} finally { } finally {
context.Logger.Information("Session stopped."); context.Logger.Information("Session stopped.");
context.ReportEvent(InstanceEvent.Stopped);
context.TransitionState(new InstanceNotRunningState(), InstanceStatus.NotRunning); context.TransitionState(new InstanceNotRunningState(), InstanceStatus.NotRunning);
} }
} }

View File

@ -14,4 +14,8 @@ public static class BackupCreationWarningsExtensions {
public static int Count(this BackupCreationWarnings warnings) { public static int Count(this BackupCreationWarnings warnings) {
return BitOperations.PopCount((byte) warnings); return BitOperations.PopCount((byte) warnings);
} }
public static IEnumerable<BackupCreationWarnings> ListFlags(this BackupCreationWarnings warnings) {
return Enum.GetValues<BackupCreationWarnings>().Where(warning => warning != BackupCreationWarnings.None && warnings.HasFlag(warning));
}
} }

View File

@ -0,0 +1,55 @@
using MemoryPack;
using Phantom.Common.Data.Backups;
namespace Phantom.Common.Data.Instance;
[MemoryPackable]
[MemoryPackUnion(0, typeof(InstanceLaunchSuccededEvent))]
[MemoryPackUnion(1, typeof(InstanceLaunchFailedEvent))]
[MemoryPackUnion(2, typeof(InstanceCrashedEvent))]
[MemoryPackUnion(3, typeof(InstanceStoppedEvent))]
[MemoryPackUnion(4, typeof(InstanceBackupCompletedEvent))]
public partial interface IInstanceEvent {
void Accept(IInstanceEventVisitor visitor);
}
[MemoryPackable]
public sealed partial record InstanceLaunchSuccededEvent : IInstanceEvent {
public void Accept(IInstanceEventVisitor visitor) {
visitor.OnLaunchSucceeded(this);
}
}
[MemoryPackable]
public sealed partial record InstanceLaunchFailedEvent([property: MemoryPackOrder(0)] InstanceLaunchFailReason Reason) : IInstanceEvent {
public void Accept(IInstanceEventVisitor visitor) {
visitor.OnLaunchFailed(this);
}
}
[MemoryPackable]
public sealed partial record InstanceCrashedEvent : IInstanceEvent {
public void Accept(IInstanceEventVisitor visitor) {
visitor.OnCrashed(this);
}
}
[MemoryPackable]
public sealed partial record InstanceStoppedEvent : IInstanceEvent {
public void Accept(IInstanceEventVisitor visitor) {
visitor.OnStopped(this);
}
}
[MemoryPackable]
public sealed partial record InstanceBackupCompletedEvent([property: MemoryPackOrder(0)] BackupCreationResultKind Kind, [property: MemoryPackOrder(1)] BackupCreationWarnings Warnings) : IInstanceEvent {
public void Accept(IInstanceEventVisitor visitor) {
visitor.OnBackupCompleted(this);
}
}
public static class InstanceEvent {
public static readonly IInstanceEvent LaunchSucceded = new InstanceLaunchSuccededEvent();
public static readonly IInstanceEvent Crashed = new InstanceCrashedEvent();
public static readonly IInstanceEvent Stopped = new InstanceStoppedEvent();
}

View File

@ -0,0 +1,9 @@
namespace Phantom.Common.Data.Instance;
public interface IInstanceEventVisitor {
void OnLaunchSucceeded(InstanceLaunchSuccededEvent e);
void OnLaunchFailed(InstanceLaunchFailedEvent e);
void OnCrashed(InstanceCrashedEvent e);
void OnStopped(InstanceStoppedEvent e);
void OnBackupCompleted(InstanceBackupCompletedEvent e);
}

View File

@ -12,6 +12,7 @@ public interface IMessageToServerListener {
Task<NoReply> HandleAdvertiseJavaRuntimes(AdvertiseJavaRuntimesMessage message); Task<NoReply> HandleAdvertiseJavaRuntimes(AdvertiseJavaRuntimesMessage message);
Task<NoReply> HandleReportAgentStatus(ReportAgentStatusMessage message); Task<NoReply> HandleReportAgentStatus(ReportAgentStatusMessage message);
Task<NoReply> HandleReportInstanceStatus(ReportInstanceStatusMessage message); Task<NoReply> HandleReportInstanceStatus(ReportInstanceStatusMessage message);
Task<NoReply> HandleReportInstanceEvent(ReportInstanceEventMessage message);
Task<NoReply> HandleInstanceOutput(InstanceOutputMessage message); Task<NoReply> HandleInstanceOutput(InstanceOutputMessage message);
Task<NoReply> HandleReply(ReplyMessage message); Task<NoReply> HandleReply(ReplyMessage message);
} }

View File

@ -27,6 +27,7 @@ public static class MessageRegistries {
ToServer.Add<ReportInstanceStatusMessage>(4); ToServer.Add<ReportInstanceStatusMessage>(4);
ToServer.Add<InstanceOutputMessage>(5); ToServer.Add<InstanceOutputMessage>(5);
ToServer.Add<ReportAgentStatusMessage>(6); ToServer.Add<ReportAgentStatusMessage>(6);
ToServer.Add<ReportInstanceEventMessage>(7);
ToServer.Add<ReplyMessage>(127); ToServer.Add<ReplyMessage>(127);
} }
} }

View File

@ -0,0 +1,17 @@
using MemoryPack;
using Phantom.Common.Data.Instance;
using Phantom.Utils.Rpc.Message;
namespace Phantom.Common.Messages.ToServer;
[MemoryPackable]
public sealed partial record ReportInstanceEventMessage(
[property: MemoryPackOrder(0)] Guid EventGuid,
[property: MemoryPackOrder(1)] DateTime UtcTime,
[property: MemoryPackOrder(2)] Guid InstanceGuid,
[property: MemoryPackOrder(3)] IInstanceEvent Event
) : IMessageToServer {
public Task<NoReply> Accept(IMessageToServerListener listener) {
return listener.HandleReportInstanceEvent(this);
}
}

View File

@ -0,0 +1,466 @@
// <auto-generated />
using System;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Phantom.Server.Database;
#nullable disable
namespace Phantom.Server.Database.Postgres.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20230213040522_AuditLogRename")]
partial class AuditLogRename
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("Roles", "identity");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("RoleClaims", "identity");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("Users", "identity");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("UserClaims", "identity");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("UserLogins", "identity");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("RoleId")
.HasColumnType("text");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("UserRoles", "identity");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("UserTokens", "identity");
});
modelBuilder.Entity("Phantom.Server.Database.Entities.AgentEntity", b =>
{
b.Property<Guid>("AgentGuid")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("BuildVersion")
.IsRequired()
.HasColumnType("text");
b.Property<int>("MaxInstances")
.HasColumnType("integer");
b.Property<ushort>("MaxMemory")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<int>("ProtocolVersion")
.HasColumnType("integer");
b.HasKey("AgentGuid");
b.ToTable("Agents", "agents");
});
modelBuilder.Entity("Phantom.Server.Database.Entities.AuditEventEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<JsonDocument>("Data")
.HasColumnType("jsonb");
b.Property<string>("EventType")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SubjectId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SubjectType")
.IsRequired()
.HasColumnType("text");
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<DateTime>("UtcTime")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AuditLog", "system");
});
modelBuilder.Entity("Phantom.Server.Database.Entities.InstanceEntity", b =>
{
b.Property<Guid>("InstanceGuid")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("AgentGuid")
.HasColumnType("uuid");
b.Property<string>("InstanceName")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("JavaRuntimeGuid")
.HasColumnType("uuid");
b.Property<string>("JvmArguments")
.IsRequired()
.HasColumnType("text");
b.Property<bool>("LaunchAutomatically")
.HasColumnType("boolean");
b.Property<ushort>("MemoryAllocation")
.HasColumnType("integer");
b.Property<string>("MinecraftServerKind")
.IsRequired()
.HasColumnType("text");
b.Property<string>("MinecraftVersion")
.IsRequired()
.HasColumnType("text");
b.Property<int>("RconPort")
.HasColumnType("integer");
b.Property<int>("ServerPort")
.HasColumnType("integer");
b.HasKey("InstanceGuid");
b.ToTable("Instances", "agents");
});
modelBuilder.Entity("Phantom.Server.Database.Entities.PermissionEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Permissions", "identity");
});
modelBuilder.Entity("Phantom.Server.Database.Entities.RolePermissionEntity", b =>
{
b.Property<string>("RoleId")
.HasColumnType("text");
b.Property<string>("PermissionId")
.HasColumnType("text");
b.HasKey("RoleId", "PermissionId");
b.HasIndex("PermissionId");
b.ToTable("RolePermissions", "identity");
});
modelBuilder.Entity("Phantom.Server.Database.Entities.UserPermissionEntity", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("PermissionId")
.HasColumnType("text");
b.HasKey("UserId", "PermissionId");
b.HasIndex("PermissionId");
b.ToTable("UserPermissions", "identity");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Phantom.Server.Database.Entities.AuditEventEntity", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User")
.WithMany()
.HasForeignKey("UserId");
b.Navigation("User");
});
modelBuilder.Entity("Phantom.Server.Database.Entities.RolePermissionEntity", b =>
{
b.HasOne("Phantom.Server.Database.Entities.PermissionEntity", null)
.WithMany()
.HasForeignKey("PermissionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Phantom.Server.Database.Entities.UserPermissionEntity", b =>
{
b.HasOne("Phantom.Server.Database.Entities.PermissionEntity", null)
.WithMany()
.HasForeignKey("PermissionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,92 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Phantom.Server.Database.Postgres.Migrations
{
/// <inheritdoc />
public partial class AuditLogRename : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_AuditEvents_Users_UserId",
schema: "system",
table: "AuditEvents");
migrationBuilder.DropPrimaryKey(
name: "PK_AuditEvents",
schema: "system",
table: "AuditEvents");
migrationBuilder.RenameTable(
name: "AuditEvents",
schema: "system",
newName: "AuditLog",
newSchema: "system");
migrationBuilder.RenameIndex(
name: "IX_AuditEvents_UserId",
schema: "system",
table: "AuditLog",
newName: "IX_AuditLog_UserId");
migrationBuilder.AddPrimaryKey(
name: "PK_AuditLog",
schema: "system",
table: "AuditLog",
column: "Id");
migrationBuilder.AddForeignKey(
name: "FK_AuditLog_Users_UserId",
schema: "system",
table: "AuditLog",
column: "UserId",
principalSchema: "identity",
principalTable: "Users",
principalColumn: "Id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_AuditLog_Users_UserId",
schema: "system",
table: "AuditLog");
migrationBuilder.DropPrimaryKey(
name: "PK_AuditLog",
schema: "system",
table: "AuditLog");
migrationBuilder.RenameTable(
name: "AuditLog",
schema: "system",
newName: "AuditEvents",
newSchema: "system");
migrationBuilder.RenameIndex(
name: "IX_AuditLog_UserId",
schema: "system",
table: "AuditEvents",
newName: "IX_AuditEvents_UserId");
migrationBuilder.AddPrimaryKey(
name: "PK_AuditEvents",
schema: "system",
table: "AuditEvents",
column: "Id");
migrationBuilder.AddForeignKey(
name: "FK_AuditEvents_Users_UserId",
schema: "system",
table: "AuditEvents",
column: "UserId",
principalSchema: "identity",
principalTable: "Users",
principalColumn: "Id");
}
}
}

View File

@ -0,0 +1,498 @@
// <auto-generated />
using System;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Phantom.Server.Database;
#nullable disable
namespace Phantom.Server.Database.Postgres.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20230215101444_EventLog")]
partial class EventLog
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("Roles", "identity");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("RoleClaims", "identity");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("text");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("Users", "identity");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("UserClaims", "identity");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("text");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("UserLogins", "identity");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("RoleId")
.HasColumnType("text");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("UserRoles", "identity");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("UserTokens", "identity");
});
modelBuilder.Entity("Phantom.Server.Database.Entities.AgentEntity", b =>
{
b.Property<Guid>("AgentGuid")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("BuildVersion")
.IsRequired()
.HasColumnType("text");
b.Property<int>("MaxInstances")
.HasColumnType("integer");
b.Property<ushort>("MaxMemory")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<int>("ProtocolVersion")
.HasColumnType("integer");
b.HasKey("AgentGuid");
b.ToTable("Agents", "agents");
});
modelBuilder.Entity("Phantom.Server.Database.Entities.AuditLogEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<JsonDocument>("Data")
.HasColumnType("jsonb");
b.Property<string>("EventType")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SubjectId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SubjectType")
.IsRequired()
.HasColumnType("text");
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<DateTime>("UtcTime")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AuditLog", "system");
});
modelBuilder.Entity("Phantom.Server.Database.Entities.EventLogEntity", b =>
{
b.Property<Guid>("EventGuid")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid?>("AgentGuid")
.HasColumnType("uuid");
b.Property<JsonDocument>("Data")
.HasColumnType("jsonb");
b.Property<string>("EventType")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SubjectId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SubjectType")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("UtcTime")
.HasColumnType("timestamp with time zone");
b.HasKey("EventGuid");
b.ToTable("EventLog", "system");
});
modelBuilder.Entity("Phantom.Server.Database.Entities.InstanceEntity", b =>
{
b.Property<Guid>("InstanceGuid")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("AgentGuid")
.HasColumnType("uuid");
b.Property<string>("InstanceName")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("JavaRuntimeGuid")
.HasColumnType("uuid");
b.Property<string>("JvmArguments")
.IsRequired()
.HasColumnType("text");
b.Property<bool>("LaunchAutomatically")
.HasColumnType("boolean");
b.Property<ushort>("MemoryAllocation")
.HasColumnType("integer");
b.Property<string>("MinecraftServerKind")
.IsRequired()
.HasColumnType("text");
b.Property<string>("MinecraftVersion")
.IsRequired()
.HasColumnType("text");
b.Property<int>("RconPort")
.HasColumnType("integer");
b.Property<int>("ServerPort")
.HasColumnType("integer");
b.HasKey("InstanceGuid");
b.ToTable("Instances", "agents");
});
modelBuilder.Entity("Phantom.Server.Database.Entities.PermissionEntity", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Permissions", "identity");
});
modelBuilder.Entity("Phantom.Server.Database.Entities.RolePermissionEntity", b =>
{
b.Property<string>("RoleId")
.HasColumnType("text");
b.Property<string>("PermissionId")
.HasColumnType("text");
b.HasKey("RoleId", "PermissionId");
b.HasIndex("PermissionId");
b.ToTable("RolePermissions", "identity");
});
modelBuilder.Entity("Phantom.Server.Database.Entities.UserPermissionEntity", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("PermissionId")
.HasColumnType("text");
b.HasKey("UserId", "PermissionId");
b.HasIndex("PermissionId");
b.ToTable("UserPermissions", "identity");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Phantom.Server.Database.Entities.AuditLogEntity", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User")
.WithMany()
.HasForeignKey("UserId");
b.Navigation("User");
});
modelBuilder.Entity("Phantom.Server.Database.Entities.RolePermissionEntity", b =>
{
b.HasOne("Phantom.Server.Database.Entities.PermissionEntity", null)
.WithMany()
.HasForeignKey("PermissionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Phantom.Server.Database.Entities.UserPermissionEntity", b =>
{
b.HasOne("Phantom.Server.Database.Entities.PermissionEntity", null)
.WithMany()
.HasForeignKey("PermissionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,42 @@
using System;
using System.Text.Json;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Phantom.Server.Database.Postgres.Migrations
{
/// <inheritdoc />
public partial class EventLog : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "EventLog",
schema: "system",
columns: table => new
{
EventGuid = table.Column<Guid>(type: "uuid", nullable: false),
UtcTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
AgentGuid = table.Column<Guid>(type: "uuid", nullable: true),
EventType = table.Column<string>(type: "text", nullable: false),
SubjectType = table.Column<string>(type: "text", nullable: false),
SubjectId = table.Column<string>(type: "text", nullable: false),
Data = table.Column<JsonDocument>(type: "jsonb", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_EventLog", x => x.EventGuid);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "EventLog",
schema: "system");
}
}
}

View File

@ -18,7 +18,7 @@ namespace Phantom.Server.Database.Postgres.Migrations
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "7.0.0-rc.1.22426.7") .HasAnnotation("ProductVersion", "7.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@ -247,7 +247,7 @@ namespace Phantom.Server.Database.Postgres.Migrations
b.ToTable("Agents", "agents"); b.ToTable("Agents", "agents");
}); });
modelBuilder.Entity("Phantom.Server.Database.Entities.AuditEventEntity", b => modelBuilder.Entity("Phantom.Server.Database.Entities.AuditLogEntity", b =>
{ {
b.Property<long>("Id") b.Property<long>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@ -280,7 +280,39 @@ namespace Phantom.Server.Database.Postgres.Migrations
b.HasIndex("UserId"); b.HasIndex("UserId");
b.ToTable("AuditEvents", "system"); b.ToTable("AuditLog", "system");
});
modelBuilder.Entity("Phantom.Server.Database.Entities.EventLogEntity", b =>
{
b.Property<Guid>("EventGuid")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid?>("AgentGuid")
.HasColumnType("uuid");
b.Property<JsonDocument>("Data")
.HasColumnType("jsonb");
b.Property<string>("EventType")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SubjectId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SubjectType")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("UtcTime")
.HasColumnType("timestamp with time zone");
b.HasKey("EventGuid");
b.ToTable("EventLog", "system");
}); });
modelBuilder.Entity("Phantom.Server.Database.Entities.InstanceEntity", b => modelBuilder.Entity("Phantom.Server.Database.Entities.InstanceEntity", b =>
@ -419,7 +451,7 @@ namespace Phantom.Server.Database.Postgres.Migrations
.IsRequired(); .IsRequired();
}); });
modelBuilder.Entity("Phantom.Server.Database.Entities.AuditEventEntity", b => modelBuilder.Entity("Phantom.Server.Database.Entities.AuditLogEntity", b =>
{ {
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User") b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User")
.WithMany() .WithMany()

View File

@ -20,7 +20,8 @@ public class ApplicationDbContext : IdentityDbContext {
public DbSet<AgentEntity> Agents { get; set; } = null!; public DbSet<AgentEntity> Agents { get; set; } = null!;
public DbSet<InstanceEntity> Instances { get; set; } = null!; public DbSet<InstanceEntity> Instances { get; set; } = null!;
public DbSet<AuditEventEntity> AuditEvents { get; set; } = null!; public DbSet<AuditLogEntity> AuditLog { get; set; } = null!;
public DbSet<EventLogEntity> EventLog { get; set; } = null!;
public AgentEntityUpsert AgentUpsert { get; } public AgentEntityUpsert AgentUpsert { get; }
public InstanceEntityUpsert InstanceUpsert { get; } public InstanceEntityUpsert InstanceUpsert { get; }
@ -60,8 +61,10 @@ public class ApplicationDbContext : IdentityDbContext {
protected override void ConfigureConventions(ModelConfigurationBuilder builder) { protected override void ConfigureConventions(ModelConfigurationBuilder builder) {
base.ConfigureConventions(builder); base.ConfigureConventions(builder);
builder.Properties<AuditEventType>().HaveConversion<EnumToStringConverter<AuditEventType>>(); builder.Properties<AuditLogEventType>().HaveConversion<EnumToStringConverter<AuditLogEventType>>();
builder.Properties<AuditSubjectType>().HaveConversion<EnumToStringConverter<AuditSubjectType>>(); builder.Properties<AuditLogSubjectType>().HaveConversion<EnumToStringConverter<AuditLogSubjectType>>();
builder.Properties<EventLogEventType>().HaveConversion<EnumToStringConverter<EventLogEventType>>();
builder.Properties<EventLogSubjectType>().HaveConversion<EnumToStringConverter<EventLogSubjectType>>();
builder.Properties<MinecraftServerKind>().HaveConversion<EnumToStringConverter<MinecraftServerKind>>(); builder.Properties<MinecraftServerKind>().HaveConversion<EnumToStringConverter<MinecraftServerKind>>();
builder.Properties<RamAllocationUnits>().HaveConversion<RamAllocationUnitsConverter>(); builder.Properties<RamAllocationUnits>().HaveConversion<RamAllocationUnitsConverter>();
} }

View File

@ -7,10 +7,10 @@ using Phantom.Server.Database.Enums;
namespace Phantom.Server.Database.Entities; namespace Phantom.Server.Database.Entities;
[Table("AuditEvents", Schema = "system")] [Table("AuditLog", Schema = "system")]
[SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Global")] [SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Global")]
[SuppressMessage("ReSharper", "ClassWithVirtualMembersNeverInherited.Global")] [SuppressMessage("ReSharper", "ClassWithVirtualMembersNeverInherited.Global")]
public class AuditEventEntity : IDisposable { public class AuditLogEntity : IDisposable {
[Key] [Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)] [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[SuppressMessage("ReSharper", "UnusedMember.Global")] [SuppressMessage("ReSharper", "UnusedMember.Global")]
@ -18,19 +18,19 @@ public class AuditEventEntity : IDisposable {
public string? UserId { get; set; } public string? UserId { get; set; }
public DateTime UtcTime { get; set; } // Note: Converting to UTC is not best practice, but for historical records it's good enough. public DateTime UtcTime { get; set; } // Note: Converting to UTC is not best practice, but for historical records it's good enough.
public AuditEventType EventType { get; set; } public AuditLogEventType EventType { get; set; }
public AuditSubjectType SubjectType { get; set; } public AuditLogSubjectType SubjectType { get; set; }
public string SubjectId { get; set; } public string SubjectId { get; set; }
public JsonDocument? Data { get; set; } public JsonDocument? Data { get; set; }
public virtual IdentityUser? User { get; set; } public virtual IdentityUser? User { get; set; }
[SuppressMessage("ReSharper", "UnusedMember.Global")] [SuppressMessage("ReSharper", "UnusedMember.Global")]
internal AuditEventEntity() { internal AuditLogEntity() {
SubjectId = string.Empty; SubjectId = string.Empty;
} }
public AuditEventEntity(string? userId, AuditEventType eventType, string subjectId, Dictionary<string, object?>? data) { public AuditLogEntity(string? userId, AuditLogEventType eventType, string subjectId, Dictionary<string, object?>? data) {
UserId = userId; UserId = userId;
UtcTime = DateTime.UtcNow; UtcTime = DateTime.UtcNow;
EventType = eventType; EventType = eventType;

View File

@ -0,0 +1,41 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using Phantom.Server.Database.Enums;
namespace Phantom.Server.Database.Entities;
[Table("EventLog", Schema = "system")]
[SuppressMessage("ReSharper", "AutoPropertyCanBeMadeGetOnly.Global")]
[SuppressMessage("ReSharper", "ClassWithVirtualMembersNeverInherited.Global")]
public sealed class EventLogEntity : IDisposable {
[Key]
public Guid EventGuid { get; set; }
public DateTime UtcTime { get; set; } // Note: Converting to UTC is not best practice, but for historical records it's good enough.
public Guid? AgentGuid { get; set; }
public EventLogEventType EventType { get; set; }
public EventLogSubjectType SubjectType { get; set; }
public string SubjectId { get; set; }
public JsonDocument? Data { get; set; }
[SuppressMessage("ReSharper", "UnusedMember.Global")]
internal EventLogEntity() {
SubjectId = string.Empty;
}
public EventLogEntity(Guid eventGuid, DateTime utcTime, Guid? agentGuid, EventLogEventType eventType, string subjectId, Dictionary<string, object?>? data) {
EventGuid = eventGuid;
UtcTime = utcTime;
AgentGuid = agentGuid;
EventType = eventType;
SubjectType = eventType.GetSubjectType();
SubjectId = subjectId;
Data = data == null ? null : JsonSerializer.SerializeToDocument(data);
}
public void Dispose() {
Data?.Dispose();
}
}

View File

@ -1,54 +0,0 @@
using System.Text.RegularExpressions;
namespace Phantom.Server.Database.Enums;
public enum AuditEventType {
AdministratorUserCreated,
AdministratorUserModified,
UserLoggedIn,
UserLoggedOut,
UserCreated,
UserRolesChanged,
UserDeleted,
InstanceCreated,
InstanceEdited,
InstanceLaunched,
InstanceStopped,
InstanceCommandExecuted
}
public static partial class AuditEventCategoryExtensions {
private static readonly Dictionary<AuditEventType, AuditSubjectType> SubjectTypes = new () {
{ AuditEventType.AdministratorUserCreated, AuditSubjectType.User },
{ AuditEventType.AdministratorUserModified, AuditSubjectType.User },
{ AuditEventType.UserLoggedIn, AuditSubjectType.User },
{ AuditEventType.UserLoggedOut, AuditSubjectType.User },
{ AuditEventType.UserCreated, AuditSubjectType.User },
{ AuditEventType.UserRolesChanged, AuditSubjectType.User },
{ AuditEventType.UserDeleted, AuditSubjectType.User },
{ AuditEventType.InstanceCreated, AuditSubjectType.Instance },
{ AuditEventType.InstanceEdited, AuditSubjectType.Instance },
{ AuditEventType.InstanceLaunched, AuditSubjectType.Instance },
{ AuditEventType.InstanceStopped, AuditSubjectType.Instance },
{ AuditEventType.InstanceCommandExecuted, AuditSubjectType.Instance }
};
static AuditEventCategoryExtensions() {
foreach (var eventType in Enum.GetValues<AuditEventType>()) {
if (!SubjectTypes.ContainsKey(eventType)) {
throw new Exception("Missing mapping from " + eventType + " to a subject type.");
}
}
}
internal static AuditSubjectType GetSubjectType(this AuditEventType type) {
return SubjectTypes[type];
}
[GeneratedRegex(@"\B([A-Z])", RegexOptions.NonBacktracking)]
private static partial Regex FindCapitalLettersRegex();
public static string ToNiceString(this AuditEventType type) {
return FindCapitalLettersRegex().Replace(type.ToString(), static match => " " + match.Groups[1].Value.ToLowerInvariant());
}
}

View File

@ -0,0 +1,45 @@
namespace Phantom.Server.Database.Enums;
public enum AuditLogEventType {
AdministratorUserCreated,
AdministratorUserModified,
UserLoggedIn,
UserLoggedOut,
UserCreated,
UserRolesChanged,
UserDeleted,
InstanceCreated,
InstanceEdited,
InstanceLaunched,
InstanceStopped,
InstanceCommandExecuted
}
public static class AuditLogEventTypeExtensions {
private static readonly Dictionary<AuditLogEventType, AuditLogSubjectType> SubjectTypes = new () {
{ AuditLogEventType.AdministratorUserCreated, AuditLogSubjectType.User },
{ AuditLogEventType.AdministratorUserModified, AuditLogSubjectType.User },
{ AuditLogEventType.UserLoggedIn, AuditLogSubjectType.User },
{ AuditLogEventType.UserLoggedOut, AuditLogSubjectType.User },
{ AuditLogEventType.UserCreated, AuditLogSubjectType.User },
{ AuditLogEventType.UserRolesChanged, AuditLogSubjectType.User },
{ AuditLogEventType.UserDeleted, AuditLogSubjectType.User },
{ AuditLogEventType.InstanceCreated, AuditLogSubjectType.Instance },
{ AuditLogEventType.InstanceEdited, AuditLogSubjectType.Instance },
{ AuditLogEventType.InstanceLaunched, AuditLogSubjectType.Instance },
{ AuditLogEventType.InstanceStopped, AuditLogSubjectType.Instance },
{ AuditLogEventType.InstanceCommandExecuted, AuditLogSubjectType.Instance }
};
static AuditLogEventTypeExtensions() {
foreach (var eventType in Enum.GetValues<AuditLogEventType>()) {
if (!SubjectTypes.ContainsKey(eventType)) {
throw new Exception("Missing mapping from " + eventType + " to a subject type.");
}
}
}
internal static AuditLogSubjectType GetSubjectType(this AuditLogEventType type) {
return SubjectTypes[type];
}
}

View File

@ -1,6 +1,6 @@
namespace Phantom.Server.Database.Enums; namespace Phantom.Server.Database.Enums;
public enum AuditSubjectType { public enum AuditLogSubjectType {
User, User,
Instance Instance
} }

View File

@ -0,0 +1,35 @@
namespace Phantom.Server.Database.Enums;
public enum EventLogEventType {
InstanceLaunchSucceded,
InstanceLaunchFailed,
InstanceCrashed,
InstanceStopped,
InstanceBackupSucceeded,
InstanceBackupSucceededWithWarnings,
InstanceBackupFailed,
}
internal static class EventLogEventTypeExtensions {
private static readonly Dictionary<EventLogEventType, EventLogSubjectType> SubjectTypes = new () {
{ EventLogEventType.InstanceLaunchSucceded, EventLogSubjectType.Instance },
{ EventLogEventType.InstanceLaunchFailed, EventLogSubjectType.Instance },
{ EventLogEventType.InstanceCrashed, EventLogSubjectType.Instance },
{ EventLogEventType.InstanceStopped, EventLogSubjectType.Instance },
{ EventLogEventType.InstanceBackupSucceeded, EventLogSubjectType.Instance },
{ EventLogEventType.InstanceBackupSucceededWithWarnings, EventLogSubjectType.Instance },
{ EventLogEventType.InstanceBackupFailed, EventLogSubjectType.Instance },
};
static EventLogEventTypeExtensions() {
foreach (var eventType in Enum.GetValues<EventLogEventType>()) {
if (!SubjectTypes.ContainsKey(eventType)) {
throw new Exception("Missing mapping from " + eventType + " to a subject type.");
}
}
}
public static EventLogSubjectType GetSubjectType(this EventLogEventType type) {
return SubjectTypes[type];
}
}

View File

@ -0,0 +1,5 @@
namespace Phantom.Server.Database.Enums;
public enum EventLogSubjectType {
Instance
}

View File

@ -1,6 +0,0 @@
using System.Text.Json;
using Phantom.Server.Database.Enums;
namespace Phantom.Server.Services.Audit;
public sealed record AuditEvent(DateTime UtcTime, string? UserId, string? UserName, AuditEventType EventType, AuditSubjectType SubjectType, string? SubjectId, JsonDocument? Data);

View File

@ -5,23 +5,23 @@ namespace Phantom.Server.Services.Audit;
public sealed partial class AuditLog { public sealed partial class AuditLog {
public Task AddAdministratorUserCreatedEvent(IdentityUser administratorUser) { public Task AddAdministratorUserCreatedEvent(IdentityUser administratorUser) {
return AddEvent(AuditEventType.AdministratorUserCreated, administratorUser.Id); return AddItem(AuditLogEventType.AdministratorUserCreated, administratorUser.Id);
} }
public Task AddAdministratorUserModifiedEvent(IdentityUser administratorUser) { public Task AddAdministratorUserModifiedEvent(IdentityUser administratorUser) {
return AddEvent(AuditEventType.AdministratorUserModified, administratorUser.Id); return AddItem(AuditLogEventType.AdministratorUserModified, administratorUser.Id);
} }
public void AddUserLoggedInEvent(string userId) { public void AddUserLoggedInEvent(string userId) {
AddEvent(userId, AuditEventType.UserLoggedIn, userId); AddItem(userId, AuditLogEventType.UserLoggedIn, userId);
} }
public void AddUserLoggedOutEvent(string userId) { public void AddUserLoggedOutEvent(string userId) {
AddEvent(userId, AuditEventType.UserLoggedOut, userId); AddItem(userId, AuditLogEventType.UserLoggedOut, userId);
} }
public Task AddUserCreatedEvent(IdentityUser user) { public Task AddUserCreatedEvent(IdentityUser user) {
return AddEvent(AuditEventType.UserCreated, user.Id); return AddItem(AuditLogEventType.UserCreated, user.Id);
} }
public Task AddUserRolesChangedEvent(IdentityUser user, List<string> addedToRoles, List<string> removedFromRoles) { public Task AddUserRolesChangedEvent(IdentityUser user, List<string> addedToRoles, List<string> removedFromRoles) {
@ -37,35 +37,35 @@ public sealed partial class AuditLog {
extra["removedFromRoles"] = removedFromRoles; extra["removedFromRoles"] = removedFromRoles;
} }
return AddEvent(AuditEventType.UserDeleted, user.Id, extra); return AddItem(AuditLogEventType.UserDeleted, user.Id, extra);
} }
public Task AddUserDeletedEvent(IdentityUser user) { public Task AddUserDeletedEvent(IdentityUser user) {
return AddEvent(AuditEventType.UserDeleted, user.Id, new Dictionary<string, object?> { return AddItem(AuditLogEventType.UserDeleted, user.Id, new Dictionary<string, object?> {
{ "username", user.UserName } { "username", user.UserName }
}); });
} }
public Task AddInstanceCreatedEvent(Guid instanceGuid) { public Task AddInstanceCreatedEvent(Guid instanceGuid) {
return AddEvent(AuditEventType.InstanceCreated, instanceGuid.ToString()); return AddItem(AuditLogEventType.InstanceCreated, instanceGuid.ToString());
} }
public Task AddInstanceEditedEvent(Guid instanceGuid) { public Task AddInstanceEditedEvent(Guid instanceGuid) {
return AddEvent(AuditEventType.InstanceEdited, instanceGuid.ToString()); return AddItem(AuditLogEventType.InstanceEdited, instanceGuid.ToString());
} }
public Task AddInstanceLaunchedEvent(Guid instanceGuid) { public Task AddInstanceLaunchedEvent(Guid instanceGuid) {
return AddEvent(AuditEventType.InstanceLaunched, instanceGuid.ToString()); return AddItem(AuditLogEventType.InstanceLaunched, instanceGuid.ToString());
} }
public Task AddInstanceCommandExecutedEvent(Guid instanceGuid, string command) { public Task AddInstanceCommandExecutedEvent(Guid instanceGuid, string command) {
return AddEvent(AuditEventType.InstanceCommandExecuted, instanceGuid.ToString(), new Dictionary<string, object?> { return AddItem(AuditLogEventType.InstanceCommandExecuted, instanceGuid.ToString(), new Dictionary<string, object?> {
{ "command", command } { "command", command }
}); });
} }
public Task AddInstanceStoppedEvent(Guid instanceGuid, int stopInSeconds) { public Task AddInstanceStoppedEvent(Guid instanceGuid, int stopInSeconds) {
return AddEvent(AuditEventType.InstanceStopped, instanceGuid.ToString(), new Dictionary<string, object?> { return AddItem(AuditLogEventType.InstanceStopped, instanceGuid.ToString(), new Dictionary<string, object?> {
{ "stop_in_seconds", stopInSeconds.ToString() } { "stop_in_seconds", stopInSeconds.ToString() }
}); });
} }

View File

@ -28,29 +28,29 @@ public sealed partial class AuditLog {
return identityLookup.GetAuthenticatedUserId(authenticationState.User); return identityLookup.GetAuthenticatedUserId(authenticationState.User);
} }
private async Task AddEventToDatabase(AuditEventEntity eventEntity) { private async Task AddEntityToDatabase(AuditLogEntity logEntity) {
using var scope = databaseProvider.CreateScope(); using var scope = databaseProvider.CreateScope();
scope.Ctx.AuditEvents.Add(eventEntity); scope.Ctx.AuditLog.Add(logEntity);
await scope.Ctx.SaveChangesAsync(cancellationToken); await scope.Ctx.SaveChangesAsync(cancellationToken);
} }
private void AddEvent(string? userId, AuditEventType eventType, string subjectId, Dictionary<string, object?>? extra = null) { private void AddItem(string? userId, AuditLogEventType eventType, string subjectId, Dictionary<string, object?>? extra = null) {
var eventEntity = new AuditEventEntity(userId, eventType, subjectId, extra); var logEntity = new AuditLogEntity(userId, eventType, subjectId, extra);
taskManager.Run("Store audit log event", () => AddEventToDatabase(eventEntity)); taskManager.Run("Store audit log item to database", () => AddEntityToDatabase(logEntity));
} }
private async Task AddEvent(AuditEventType eventType, string subjectId, Dictionary<string, object?>? extra = null) { private async Task AddItem(AuditLogEventType eventType, string subjectId, Dictionary<string, object?>? extra = null) {
AddEvent(await GetCurrentAuthenticatedUserId(), eventType, subjectId, extra); AddItem(await GetCurrentAuthenticatedUserId(), eventType, subjectId, extra);
} }
public async Task<AuditEvent[]> GetEvents(int count, CancellationToken cancellationToken) { public async Task<AuditLogItem[]> GetItems(int count, CancellationToken cancellationToken) {
using var scope = databaseProvider.CreateScope(); using var scope = databaseProvider.CreateScope();
return await scope.Ctx.AuditEvents return await scope.Ctx.AuditLog
.Include(static entity => entity.User) .Include(static entity => entity.User)
.AsQueryable() .AsQueryable()
.OrderByDescending(static entity => entity.UtcTime) .OrderByDescending(static entity => entity.UtcTime)
.Take(count) .Take(count)
.Select(static entity => new AuditEvent(entity.UtcTime, entity.UserId, entity.User == null ? null : entity.User.UserName, entity.EventType, entity.SubjectType, entity.SubjectId, entity.Data)) .Select(static entity => new AuditLogItem(entity.UtcTime, entity.UserId, entity.User == null ? null : entity.User.UserName, entity.EventType, entity.SubjectType, entity.SubjectId, entity.Data))
.ToArrayAsync(cancellationToken); .ToArrayAsync(cancellationToken);
} }
} }

View File

@ -0,0 +1,6 @@
using System.Text.Json;
using Phantom.Server.Database.Enums;
namespace Phantom.Server.Services.Audit;
public sealed record AuditLogItem(DateTime UtcTime, string? UserId, string? UserName, AuditLogEventType EventType, AuditLogSubjectType SubjectType, string? SubjectId, JsonDocument? Data);

View File

@ -0,0 +1,65 @@
using Phantom.Common.Data.Backups;
using Phantom.Common.Data.Instance;
using Phantom.Server.Database.Enums;
namespace Phantom.Server.Services.Events;
public sealed partial class EventLog {
internal IInstanceEventVisitor CreateInstanceEventVisitor(Guid eventGuid, DateTime utcTime, Guid agentGuid, Guid instanceGuid) {
return new InstanceEventVisitor(this, utcTime, eventGuid, agentGuid, instanceGuid);
}
private sealed class InstanceEventVisitor : IInstanceEventVisitor {
private readonly EventLog eventLog;
private readonly Guid eventGuid;
private readonly DateTime utcTime;
private readonly Guid agentGuid;
private readonly Guid instanceGuid;
public InstanceEventVisitor(EventLog eventLog, DateTime utcTime, Guid eventGuid, Guid agentGuid, Guid instanceGuid) {
this.eventLog = eventLog;
this.eventGuid = eventGuid;
this.utcTime = utcTime;
this.agentGuid = agentGuid;
this.instanceGuid = instanceGuid;
}
public void OnLaunchSucceeded(InstanceLaunchSuccededEvent e) {
eventLog.AddItem(eventGuid, utcTime, agentGuid, EventLogEventType.InstanceLaunchSucceded, instanceGuid.ToString());
}
public void OnLaunchFailed(InstanceLaunchFailedEvent e) {
eventLog.AddItem(eventGuid, utcTime, agentGuid, EventLogEventType.InstanceLaunchFailed, instanceGuid.ToString(), new Dictionary<string, object?> {
{ "reason", e.Reason.ToString() }
});
}
public void OnCrashed(InstanceCrashedEvent e) {
eventLog.AddItem(eventGuid, utcTime, agentGuid, EventLogEventType.InstanceCrashed, instanceGuid.ToString());
}
public void OnStopped(InstanceStoppedEvent e) {
eventLog.AddItem(eventGuid, utcTime, agentGuid, EventLogEventType.InstanceStopped, instanceGuid.ToString());
}
public void OnBackupCompleted(InstanceBackupCompletedEvent e) {
var eventType = e.Kind switch {
BackupCreationResultKind.Success when e.Warnings != BackupCreationWarnings.None => EventLogEventType.InstanceBackupSucceededWithWarnings,
BackupCreationResultKind.Success => EventLogEventType.InstanceBackupSucceeded,
_ => EventLogEventType.InstanceBackupFailed
};
var dictionary = new Dictionary<string, object?>();
if (eventType == EventLogEventType.InstanceBackupFailed) {
dictionary["reason"] = e.Kind.ToString();
}
if (e.Warnings != BackupCreationWarnings.None) {
dictionary["warnings"] = e.Warnings.ListFlags().Select(static warning => warning.ToString()).ToArray();
}
eventLog.AddItem(eventGuid, utcTime, agentGuid, eventType, instanceGuid.ToString(), dictionary.Count == 0 ? null : dictionary);
}
}
}

View File

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore;
using Phantom.Server.Database;
using Phantom.Server.Database.Entities;
using Phantom.Server.Database.Enums;
using Phantom.Utils.Runtime;
namespace Phantom.Server.Services.Events;
public sealed partial class EventLog {
private readonly CancellationToken cancellationToken;
private readonly DatabaseProvider databaseProvider;
private readonly TaskManager taskManager;
public EventLog(ServiceConfiguration serviceConfiguration, DatabaseProvider databaseProvider, TaskManager taskManager) {
this.cancellationToken = serviceConfiguration.CancellationToken;
this.databaseProvider = databaseProvider;
this.taskManager = taskManager;
}
private async Task AddEntityToDatabase(EventLogEntity logEntity) {
using var scope = databaseProvider.CreateScope();
scope.Ctx.EventLog.Add(logEntity);
await scope.Ctx.SaveChangesAsync(cancellationToken);
}
private void AddItem(Guid eventGuid, DateTime utcTime, Guid? agentGuid, EventLogEventType eventType, string subjectId, Dictionary<string, object?>? extra = null) {
var logEntity = new EventLogEntity(eventGuid, utcTime, agentGuid, eventType, subjectId, extra);
taskManager.Run("Store event log item to database", () => AddEntityToDatabase(logEntity));
}
public async Task<EventLogItem[]> GetItems(int count, CancellationToken cancellationToken) {
using var scope = databaseProvider.CreateScope();
return await scope.Ctx.EventLog
.AsQueryable()
.OrderByDescending(static entity => entity.UtcTime)
.Take(count)
.Select(static entity => new EventLogItem(entity.UtcTime, entity.AgentGuid, entity.EventType, entity.SubjectType, entity.SubjectId, entity.Data))
.ToArrayAsync(cancellationToken);
}
}

View File

@ -0,0 +1,6 @@
using System.Text.Json;
using Phantom.Server.Database.Enums;
namespace Phantom.Server.Services.Events;
public sealed record EventLogItem(DateTime UtcTime, Guid? AgentGuid, EventLogEventType EventType, EventLogSubjectType SubjectType, string SubjectId, JsonDocument? Data);

View File

@ -6,6 +6,7 @@ using Phantom.Common.Messages.ToAgent;
using Phantom.Common.Messages.ToServer; using Phantom.Common.Messages.ToServer;
using Phantom.Server.Rpc; using Phantom.Server.Rpc;
using Phantom.Server.Services.Agents; using Phantom.Server.Services.Agents;
using Phantom.Server.Services.Events;
using Phantom.Server.Services.Instances; using Phantom.Server.Services.Instances;
using Phantom.Utils.Rpc.Message; using Phantom.Utils.Rpc.Message;
@ -18,28 +19,28 @@ public sealed class MessageToServerListener : IMessageToServerListener {
private readonly AgentJavaRuntimesManager agentJavaRuntimesManager; private readonly AgentJavaRuntimesManager agentJavaRuntimesManager;
private readonly InstanceManager instanceManager; private readonly InstanceManager instanceManager;
private readonly InstanceLogManager instanceLogManager; private readonly InstanceLogManager instanceLogManager;
private readonly EventLog eventLog;
private Guid? agentGuid;
private readonly TaskCompletionSource<Guid> agentGuidWaiter = new (); private readonly TaskCompletionSource<Guid> agentGuidWaiter = new ();
public bool IsDisposed { get; private set; } public bool IsDisposed { get; private set; }
internal MessageToServerListener(RpcClientConnection connection, ServiceConfiguration configuration, AgentManager agentManager, AgentJavaRuntimesManager agentJavaRuntimesManager, InstanceManager instanceManager, InstanceLogManager instanceLogManager) { internal MessageToServerListener(RpcClientConnection connection, ServiceConfiguration configuration, AgentManager agentManager, AgentJavaRuntimesManager agentJavaRuntimesManager, InstanceManager instanceManager, InstanceLogManager instanceLogManager, EventLog eventLog) {
this.connection = connection; this.connection = connection;
this.cancellationToken = configuration.CancellationToken; this.cancellationToken = configuration.CancellationToken;
this.agentManager = agentManager; this.agentManager = agentManager;
this.agentJavaRuntimesManager = agentJavaRuntimesManager; this.agentJavaRuntimesManager = agentJavaRuntimesManager;
this.instanceManager = instanceManager; this.instanceManager = instanceManager;
this.instanceLogManager = instanceLogManager; this.instanceLogManager = instanceLogManager;
this.eventLog = eventLog;
} }
public async Task<NoReply> HandleRegisterAgent(RegisterAgentMessage message) { public async Task<NoReply> HandleRegisterAgent(RegisterAgentMessage message) {
if (agentGuid != null && agentGuid != message.AgentInfo.Guid) { if (agentGuidWaiter.Task.IsCompleted && agentGuidWaiter.Task.Result != message.AgentInfo.Guid) {
await connection.Send(new RegisterAgentFailureMessage(RegisterAgentFailure.ConnectionAlreadyHasAnAgent)); await connection.Send(new RegisterAgentFailureMessage(RegisterAgentFailure.ConnectionAlreadyHasAnAgent));
} }
else if (await agentManager.RegisterAgent(message.AuthToken, message.AgentInfo, instanceManager, connection)) { else if (await agentManager.RegisterAgent(message.AuthToken, message.AgentInfo, instanceManager, connection)) {
var guid = message.AgentInfo.Guid; var guid = message.AgentInfo.Guid;
agentGuid = guid;
agentGuidWaiter.SetResult(guid); agentGuidWaiter.SetResult(guid);
} }
@ -80,6 +81,11 @@ public sealed class MessageToServerListener : IMessageToServerListener {
return Task.FromResult(NoReply.Instance); return Task.FromResult(NoReply.Instance);
} }
public async Task<NoReply> HandleReportInstanceEvent(ReportInstanceEventMessage message) {
message.Event.Accept(eventLog.CreateInstanceEventVisitor(message.EventGuid, message.UtcTime, await WaitForAgentGuid(), message.InstanceGuid));
return NoReply.Instance;
}
public Task<NoReply> HandleInstanceOutput(InstanceOutputMessage message) { public Task<NoReply> HandleInstanceOutput(InstanceOutputMessage message) {
instanceLogManager.AddLines(message.InstanceGuid, message.Lines); instanceLogManager.AddLines(message.InstanceGuid, message.Lines);
return Task.FromResult(NoReply.Instance); return Task.FromResult(NoReply.Instance);

View File

@ -1,5 +1,6 @@
using Phantom.Server.Rpc; using Phantom.Server.Rpc;
using Phantom.Server.Services.Agents; using Phantom.Server.Services.Agents;
using Phantom.Server.Services.Events;
using Phantom.Server.Services.Instances; using Phantom.Server.Services.Instances;
namespace Phantom.Server.Services.Rpc; namespace Phantom.Server.Services.Rpc;
@ -10,16 +11,18 @@ public sealed class MessageToServerListenerFactory {
private readonly AgentJavaRuntimesManager agentJavaRuntimesManager; private readonly AgentJavaRuntimesManager agentJavaRuntimesManager;
private readonly InstanceManager instanceManager; private readonly InstanceManager instanceManager;
private readonly InstanceLogManager instanceLogManager; private readonly InstanceLogManager instanceLogManager;
private readonly EventLog eventLog;
public MessageToServerListenerFactory(ServiceConfiguration configuration, AgentManager agentManager, AgentJavaRuntimesManager agentJavaRuntimesManager, InstanceManager instanceManager, InstanceLogManager instanceLogManager) { public MessageToServerListenerFactory(ServiceConfiguration configuration, AgentManager agentManager, AgentJavaRuntimesManager agentJavaRuntimesManager, InstanceManager instanceManager, InstanceLogManager instanceLogManager, EventLog eventLog) {
this.configuration = configuration; this.configuration = configuration;
this.agentManager = agentManager; this.agentManager = agentManager;
this.agentJavaRuntimesManager = agentJavaRuntimesManager; this.agentJavaRuntimesManager = agentJavaRuntimesManager;
this.instanceManager = instanceManager; this.instanceManager = instanceManager;
this.instanceLogManager = instanceLogManager; this.instanceLogManager = instanceLogManager;
this.eventLog = eventLog;
} }
public MessageToServerListener CreateListener(RpcClientConnection connection) { public MessageToServerListener CreateListener(RpcClientConnection connection) {
return new MessageToServerListener(connection, configuration, agentManager, agentJavaRuntimesManager, instanceManager, instanceLogManager); return new MessageToServerListener(connection, configuration, agentManager, agentJavaRuntimesManager, instanceManager, instanceLogManager, eventLog);
} }
} }

View File

@ -34,4 +34,7 @@ public sealed record Permission(string Id, Permission? Parent) {
public const string ViewAuditPolicy = "Audit.View"; public const string ViewAuditPolicy = "Audit.View";
public static readonly Permission ViewAudit = Register(ViewAuditPolicy); public static readonly Permission ViewAudit = Register(ViewAuditPolicy);
public const string ViewEventsPolicy = "Events.View";
public static readonly Permission ViewEvents = Register(ViewEventsPolicy);
} }

View File

@ -13,5 +13,5 @@ public sealed record Role(string Name, ImmutableArray<Permission> Permissions) {
} }
public static readonly Role Administrator = Register("Administrator", Permission.All.ToImmutableArray()); public static readonly Role Administrator = Register("Administrator", Permission.All.ToImmutableArray());
public static readonly Role InstanceManager = Register("Instance Manager", ImmutableArray.Create(Permission.ViewInstances, Permission.ViewInstanceLogs, Permission.CreateInstances, Permission.ControlInstances)); public static readonly Role InstanceManager = Register("Instance Manager", ImmutableArray.Create(Permission.ViewInstances, Permission.ViewInstanceLogs, Permission.CreateInstances, Permission.ControlInstances, Permission.ViewEvents));
} }

View File

@ -35,6 +35,10 @@
<NavMenuItem Label="Audit Log" Icon="clipboard" Href="audit" /> <NavMenuItem Label="Audit Log" Icon="clipboard" Href="audit" />
} }
@if (permissions.Check(Permission.ViewEvents)) {
<NavMenuItem Label="Event Log" Icon="project" Href="events" />
}
<NavMenuItem Label="Logout" Icon="account-logout" Href="logout" /> <NavMenuItem Label="Logout" Icon="account-logout" Href="logout" />
</Authorized> </Authorized>
</AuthorizeView> </AuthorizeView>

View File

@ -24,27 +24,27 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach (var eventEntity in events) { @foreach (var logItem in logItems) {
DateTimeOffset time = eventEntity.UtcTime.ToLocalTime(); DateTimeOffset time = logItem.UtcTime.ToLocalTime();
<tr> <tr>
<td class="text-end"> <td class="text-end">
<time datetime="@time.ToString("o")">@time.ToString()</time> <time datetime="@time.ToString("o")">@time.ToString()</time>
</td> </td>
<td> <td>
@(eventEntity.UserName ?? "-") @(logItem.UserName ?? "-")
<br> <br>
<code class="text-uppercase">@eventEntity.UserId</code> <code class="text-uppercase">@logItem.UserId</code>
</td> </td>
<td>@eventEntity.EventType.ToNiceString()</td> <td>@logItem.EventType.ToNiceString()</td>
<td> <td>
@if (eventEntity.SubjectId is {} subjectId && GetSubjectName(eventEntity.SubjectType, subjectId) is {} subjectName) { @if (logItem.SubjectId is {} subjectId && GetSubjectName(logItem.SubjectType, subjectId) is {} subjectName) {
@subjectName @subjectName
<br> <br>
} }
<code class="text-uppercase">@(eventEntity.SubjectId ?? "-")</code> <code class="text-uppercase">@(logItem.SubjectId ?? "-")</code>
</td> </td>
<td> <td>
<code>@eventEntity.Data?.RootElement.ToString()</code> <code>@logItem.Data?.RootElement.ToString()</code>
</td> </td>
</tr> </tr>
} }
@ -54,7 +54,7 @@
@code { @code {
private CancellationTokenSource? initializationCancellationTokenSource; private CancellationTokenSource? initializationCancellationTokenSource;
private AuditEvent[] events = Array.Empty<AuditEvent>(); private AuditLogItem[] logItems = Array.Empty<AuditLogItem>();
private Dictionary<string, string>? userNamesById; private Dictionary<string, string>? userNamesById;
private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty; private ImmutableDictionary<Guid, string> instanceNamesByGuid = ImmutableDictionary<Guid, string>.Empty;
@ -63,7 +63,7 @@
var cancellationToken = initializationCancellationTokenSource.Token; var cancellationToken = initializationCancellationTokenSource.Token;
try { try {
events = await AuditLog.GetEvents(50, cancellationToken); logItems = await AuditLog.GetItems(50, cancellationToken);
userNamesById = await UserManager.Users.ToDictionaryAsync(static user => user.Id, static user => user.UserName ?? user.Id, cancellationToken); userNamesById = await UserManager.Users.ToDictionaryAsync(static user => user.Id, static user => user.UserName ?? user.Id, cancellationToken);
instanceNamesByGuid = InstanceManager.GetInstanceNames(); instanceNamesByGuid = InstanceManager.GetInstanceNames();
} finally { } finally {
@ -71,10 +71,10 @@
} }
} }
private string? GetSubjectName(AuditSubjectType type, string id) { private string? GetSubjectName(AuditLogSubjectType type, string id) {
return type switch { return type switch {
AuditSubjectType.Instance => instanceNamesByGuid.TryGetValue(Guid.Parse(id), out var name) ? name : null, AuditLogSubjectType.Instance => instanceNamesByGuid.TryGetValue(Guid.Parse(id), out var name) ? name : null,
AuditSubjectType.User => userNamesById != null && userNamesById.TryGetValue(id, out var name) ? name : null, AuditLogSubjectType.User => userNamesById != null && userNamesById.TryGetValue(id, out var name) ? name : null,
_ => null _ => null
}; };
} }

View File

@ -0,0 +1,97 @@
@page "/events"
@attribute [Authorize(Permission.ViewEventsPolicy)]
@using System.Collections.Immutable
@using Phantom.Server.Services.Agents
@using Phantom.Server.Services.Events
@using Phantom.Server.Services.Instances
@using Microsoft.AspNetCore.Identity
@using Phantom.Server.Database.Enums
@implements IDisposable
@inject AgentManager AgentManager
@inject EventLog EventLog
@inject InstanceManager InstanceManager
@inject UserManager<IdentityUser> UserManager
<h1>Audit 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) {}
}
}

View File

@ -0,0 +1,12 @@
using System.Text.RegularExpressions;
namespace Phantom.Server.Web.Utils;
static partial class EnumNameConverter {
[GeneratedRegex(@"\B([A-Z])", RegexOptions.NonBacktracking)]
private static partial Regex FindCapitalLettersRegex();
public static string ToNiceString<T>(this T type) where T : Enum {
return FindCapitalLettersRegex().Replace(type.ToString(), static match => " " + match.Groups[1].Value.ToLowerInvariant());
}
}

View File

@ -17,4 +17,5 @@
@using Phantom.Server.Web.Identity.Data @using Phantom.Server.Web.Identity.Data
@using Phantom.Server.Web.Layout @using Phantom.Server.Web.Layout
@using Phantom.Server.Web.Shared @using Phantom.Server.Web.Shared
@using Phantom.Server.Web.Utils
@attribute [Authorize] @attribute [Authorize]

View File

@ -4,6 +4,7 @@ using Phantom.Common.Minecraft;
using Phantom.Server.Services; using Phantom.Server.Services;
using Phantom.Server.Services.Agents; using Phantom.Server.Services.Agents;
using Phantom.Server.Services.Audit; using Phantom.Server.Services.Audit;
using Phantom.Server.Services.Events;
using Phantom.Server.Services.Instances; using Phantom.Server.Services.Instances;
using Phantom.Server.Services.Rpc; using Phantom.Server.Services.Rpc;
using Phantom.Server.Services.Users; using Phantom.Server.Services.Users;
@ -29,6 +30,7 @@ sealed class WebConfigurator : WebLauncher.IConfigurator {
services.AddSingleton(agentToken); services.AddSingleton(agentToken);
services.AddSingleton<AgentManager>(); services.AddSingleton<AgentManager>();
services.AddSingleton<AgentJavaRuntimesManager>(); services.AddSingleton<AgentJavaRuntimesManager>();
services.AddSingleton<EventLog>();
services.AddSingleton<InstanceManager>(); services.AddSingleton<InstanceManager>();
services.AddSingleton<InstanceLogManager>(); services.AddSingleton<InstanceLogManager>();
services.AddSingleton<MinecraftVersions>(); services.AddSingleton<MinecraftVersions>();