diff --git a/Moonlight/App/Database/Entities/User.cs b/Moonlight/App/Database/Entities/User.cs
index a0277788..854cdd63 100644
--- a/Moonlight/App/Database/Entities/User.cs
+++ b/Moonlight/App/Database/Entities/User.cs
@@ -43,6 +43,7 @@ public class User
// Date stuff
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
+ public DateTime LastVisitedAt { get; set; } = DateTime.UtcNow;
// Subscriptions
diff --git a/Moonlight/App/Database/Migrations/20230611152138_AddLastVisitedTimestamp.Designer.cs b/Moonlight/App/Database/Migrations/20230611152138_AddLastVisitedTimestamp.Designer.cs
new file mode 100644
index 00000000..d8440197
--- /dev/null
+++ b/Moonlight/App/Database/Migrations/20230611152138_AddLastVisitedTimestamp.Designer.cs
@@ -0,0 +1,1059 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Moonlight.App.Database;
+
+#nullable disable
+
+namespace Moonlight.App.Database.Migrations
+{
+ [DbContext(typeof(DataContext))]
+ [Migration("20230611152138_AddLastVisitedTimestamp")]
+ partial class AddLastVisitedTimestamp
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "7.0.3")
+ .HasAnnotation("Relational:MaxIdentifierLength", 64);
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.CloudPanel", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ b.Property("ApiKey")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("ApiUrl")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("Host")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.HasKey("Id");
+
+ b.ToTable("CloudPanels");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.DdosAttack", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Data")
+ .HasColumnType("bigint");
+
+ b.Property("Ip")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("NodeId")
+ .HasColumnType("int");
+
+ b.Property("Ongoing")
+ .HasColumnType("tinyint(1)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NodeId");
+
+ b.ToTable("DdosAttacks");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.DockerImage", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ b.Property("Default")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("ImageId")
+ .HasColumnType("int");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ImageId");
+
+ b.ToTable("DockerImages");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.Domain", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("OwnerId")
+ .HasColumnType("int");
+
+ b.Property("SharedDomainId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerId");
+
+ b.HasIndex("SharedDomainId");
+
+ b.ToTable("Domains");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.Image", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ b.Property("Allocations")
+ .HasColumnType("int");
+
+ b.Property("BackgroundImageUrl")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("ConfigFiles")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("InstallDockerImage")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("InstallEntrypoint")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("InstallScript")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("Startup")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("StartupDetection")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("StopCommand")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("TagsJson")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("Uuid")
+ .HasColumnType("char(36)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Images");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.ImageVariable", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ b.Property("DefaultValue")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("ImageId")
+ .HasColumnType("int");
+
+ b.Property("Key")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ImageId");
+
+ b.ToTable("ImageVariables");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.IpBan", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Ip")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.HasKey("Id");
+
+ b.ToTable("IpBans");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.LoadingMessage", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.HasKey("Id");
+
+ b.ToTable("LoadingMessages");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.LogsEntries.AuditLogEntry", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Ip")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("JsonData")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("System")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("Type")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.ToTable("AuditLog");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.LogsEntries.ErrorLogEntry", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ b.Property("Class")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Ip")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("JsonData")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("Stacktrace")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("System")
+ .HasColumnType("tinyint(1)");
+
+ b.HasKey("Id");
+
+ b.ToTable("ErrorLog");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.LogsEntries.SecurityLogEntry", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Ip")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("JsonData")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("System")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("Type")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.ToTable("SecurityLog");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.MySqlDatabase", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ b.Property("Password")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("UserName")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("WebSpaceId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("WebSpaceId");
+
+ b.ToTable("Databases");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.NewsEntry", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ b.Property("Date")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Markdown")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.HasKey("Id");
+
+ b.ToTable("NewsEntries");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.Node", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ b.Property("Fqdn")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("HttpPort")
+ .HasColumnType("int");
+
+ b.Property("MoonlightDaemonPort")
+ .HasColumnType("int");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("SftpPort")
+ .HasColumnType("int");
+
+ b.Property("Ssl")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("Token")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("TokenId")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.HasKey("Id");
+
+ b.ToTable("Nodes");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.NodeAllocation", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ b.Property("NodeId")
+ .HasColumnType("int");
+
+ b.Property("Port")
+ .HasColumnType("int");
+
+ b.Property("ServerId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NodeId");
+
+ b.HasIndex("ServerId");
+
+ b.ToTable("NodeAllocations");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.Notification.NotificationAction", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ b.Property("Action")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("NotificationClientId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("NotificationClientId");
+
+ b.ToTable("NotificationActions");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.Notification.NotificationClient", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ b.Property("UserId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("NotificationClients");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.Revoke", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ b.Property("Identifier")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.HasKey("Id");
+
+ b.ToTable("Revokes");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.Server", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ b.Property("Cpu")
+ .HasColumnType("int");
+
+ b.Property("Disk")
+ .HasColumnType("bigint");
+
+ b.Property("DockerImageIndex")
+ .HasColumnType("int");
+
+ b.Property("ImageId")
+ .HasColumnType("int");
+
+ b.Property("Installing")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("IsCleanupException")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("MainAllocationId")
+ .HasColumnType("int");
+
+ b.Property("Memory")
+ .HasColumnType("bigint");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("NodeId")
+ .HasColumnType("int");
+
+ b.Property("OverrideStartup")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("OwnerId")
+ .HasColumnType("int");
+
+ b.Property("Suspended")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("Uuid")
+ .HasColumnType("char(36)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ImageId");
+
+ b.HasIndex("MainAllocationId");
+
+ b.HasIndex("NodeId");
+
+ b.HasIndex("OwnerId");
+
+ b.ToTable("Servers");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.ServerBackup", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ b.Property("Bytes")
+ .HasColumnType("bigint");
+
+ b.Property("Created")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("ServerId")
+ .HasColumnType("int");
+
+ b.Property("Uuid")
+ .HasColumnType("char(36)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ServerId");
+
+ b.ToTable("ServerBackups");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.ServerVariable", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ b.Property("Key")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("ServerId")
+ .HasColumnType("int");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ServerId");
+
+ b.ToTable("ServerVariables");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.SharedDomain", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ b.Property("CloudflareId")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.HasKey("Id");
+
+ b.ToTable("SharedDomains");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.StatisticsData", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ b.Property("Chart")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("Date")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Value")
+ .HasColumnType("double");
+
+ b.HasKey("Id");
+
+ b.ToTable("Statistics");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.Subscription", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("LimitsJson")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.HasKey("Id");
+
+ b.ToTable("Subscriptions");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.SupportChatMessage", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ b.Property("Answer")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("Attachment")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("Content")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("IsQuestion")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("QuestionType")
+ .HasColumnType("int");
+
+ b.Property("RecipientId")
+ .HasColumnType("int");
+
+ b.Property("SenderId")
+ .HasColumnType("int");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RecipientId");
+
+ b.HasIndex("SenderId");
+
+ b.ToTable("SupportChatMessages");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ b.Property("Address")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("Admin")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("City")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("Country")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("CurrentSubscriptionId")
+ .HasColumnType("int");
+
+ b.Property("DiscordId")
+ .HasColumnType("bigint unsigned");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("FirstName")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("HasRated")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("LastName")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("LastVisitedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Password")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("Rating")
+ .HasColumnType("int");
+
+ b.Property("State")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("Status")
+ .HasColumnType("int");
+
+ b.Property("SubscriptionDuration")
+ .HasColumnType("int");
+
+ b.Property("SubscriptionSince")
+ .HasColumnType("datetime(6)");
+
+ b.Property("SupportPending")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("TokenValidTime")
+ .HasColumnType("datetime(6)");
+
+ b.Property("TotpEnabled")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("TotpSecret")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CurrentSubscriptionId");
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.WebSpace", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ b.Property("CloudPanelId")
+ .HasColumnType("int");
+
+ b.Property("Domain")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("OwnerId")
+ .HasColumnType("int");
+
+ b.Property("Password")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("UserName")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("VHostTemplate")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CloudPanelId");
+
+ b.HasIndex("OwnerId");
+
+ b.ToTable("WebSpaces");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.DdosAttack", b =>
+ {
+ b.HasOne("Moonlight.App.Database.Entities.Node", "Node")
+ .WithMany()
+ .HasForeignKey("NodeId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Node");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.DockerImage", b =>
+ {
+ b.HasOne("Moonlight.App.Database.Entities.Image", null)
+ .WithMany("DockerImages")
+ .HasForeignKey("ImageId");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.Domain", b =>
+ {
+ b.HasOne("Moonlight.App.Database.Entities.User", "Owner")
+ .WithMany()
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Moonlight.App.Database.Entities.SharedDomain", "SharedDomain")
+ .WithMany()
+ .HasForeignKey("SharedDomainId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Owner");
+
+ b.Navigation("SharedDomain");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.ImageVariable", b =>
+ {
+ b.HasOne("Moonlight.App.Database.Entities.Image", null)
+ .WithMany("Variables")
+ .HasForeignKey("ImageId");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.MySqlDatabase", b =>
+ {
+ b.HasOne("Moonlight.App.Database.Entities.WebSpace", "WebSpace")
+ .WithMany("Databases")
+ .HasForeignKey("WebSpaceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("WebSpace");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.NodeAllocation", b =>
+ {
+ b.HasOne("Moonlight.App.Database.Entities.Node", null)
+ .WithMany("Allocations")
+ .HasForeignKey("NodeId");
+
+ b.HasOne("Moonlight.App.Database.Entities.Server", null)
+ .WithMany("Allocations")
+ .HasForeignKey("ServerId");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.Notification.NotificationAction", b =>
+ {
+ b.HasOne("Moonlight.App.Database.Entities.Notification.NotificationClient", "NotificationClient")
+ .WithMany()
+ .HasForeignKey("NotificationClientId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("NotificationClient");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.Notification.NotificationClient", b =>
+ {
+ b.HasOne("Moonlight.App.Database.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.Server", b =>
+ {
+ b.HasOne("Moonlight.App.Database.Entities.Image", "Image")
+ .WithMany()
+ .HasForeignKey("ImageId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Moonlight.App.Database.Entities.NodeAllocation", "MainAllocation")
+ .WithMany()
+ .HasForeignKey("MainAllocationId");
+
+ b.HasOne("Moonlight.App.Database.Entities.Node", "Node")
+ .WithMany()
+ .HasForeignKey("NodeId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Moonlight.App.Database.Entities.User", "Owner")
+ .WithMany()
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Image");
+
+ b.Navigation("MainAllocation");
+
+ b.Navigation("Node");
+
+ b.Navigation("Owner");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.ServerBackup", b =>
+ {
+ b.HasOne("Moonlight.App.Database.Entities.Server", null)
+ .WithMany("Backups")
+ .HasForeignKey("ServerId");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.ServerVariable", b =>
+ {
+ b.HasOne("Moonlight.App.Database.Entities.Server", null)
+ .WithMany("Variables")
+ .HasForeignKey("ServerId");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.SupportChatMessage", b =>
+ {
+ b.HasOne("Moonlight.App.Database.Entities.User", "Recipient")
+ .WithMany()
+ .HasForeignKey("RecipientId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Moonlight.App.Database.Entities.User", "Sender")
+ .WithMany()
+ .HasForeignKey("SenderId");
+
+ b.Navigation("Recipient");
+
+ b.Navigation("Sender");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
+ {
+ b.HasOne("Moonlight.App.Database.Entities.Subscription", "CurrentSubscription")
+ .WithMany()
+ .HasForeignKey("CurrentSubscriptionId");
+
+ b.Navigation("CurrentSubscription");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.WebSpace", b =>
+ {
+ b.HasOne("Moonlight.App.Database.Entities.CloudPanel", "CloudPanel")
+ .WithMany()
+ .HasForeignKey("CloudPanelId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Moonlight.App.Database.Entities.User", "Owner")
+ .WithMany()
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("CloudPanel");
+
+ b.Navigation("Owner");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.Image", b =>
+ {
+ b.Navigation("DockerImages");
+
+ b.Navigation("Variables");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.Node", b =>
+ {
+ b.Navigation("Allocations");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.Server", b =>
+ {
+ b.Navigation("Allocations");
+
+ b.Navigation("Backups");
+
+ b.Navigation("Variables");
+ });
+
+ modelBuilder.Entity("Moonlight.App.Database.Entities.WebSpace", b =>
+ {
+ b.Navigation("Databases");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Moonlight/App/Database/Migrations/20230611152138_AddLastVisitedTimestamp.cs b/Moonlight/App/Database/Migrations/20230611152138_AddLastVisitedTimestamp.cs
new file mode 100644
index 00000000..763ef815
--- /dev/null
+++ b/Moonlight/App/Database/Migrations/20230611152138_AddLastVisitedTimestamp.cs
@@ -0,0 +1,30 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Moonlight.App.Database.Migrations
+{
+ ///
+ public partial class AddLastVisitedTimestamp : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "LastVisitedAt",
+ table: "Users",
+ type: "datetime(6)",
+ nullable: false,
+ defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "LastVisitedAt",
+ table: "Users");
+ }
+ }
+}
diff --git a/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs b/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs
index 8b8aeb29..18d134db 100644
--- a/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs
+++ b/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs
@@ -762,6 +762,9 @@ namespace Moonlight.App.Database.Migrations
.IsRequired()
.HasColumnType("longtext");
+ b.Property("LastVisitedAt")
+ .HasColumnType("datetime(6)");
+
b.Property("Password")
.IsRequired()
.HasColumnType("longtext");
diff --git a/Moonlight/App/Helpers/AvgHelper.cs b/Moonlight/App/Helpers/AvgHelper.cs
new file mode 100644
index 00000000..808aa81a
--- /dev/null
+++ b/Moonlight/App/Helpers/AvgHelper.cs
@@ -0,0 +1,39 @@
+using Moonlight.App.Database.Entities;
+
+namespace Moonlight.App.Helpers;
+
+public static class AvgHelper
+{
+ public static StatisticsData[] Calculate(StatisticsData[] data, int splitSize = 40)
+ {
+ if (data.Length <= splitSize)
+ return data;
+
+ var result = new List();
+
+ var i = data.Length / (float)splitSize;
+ var pc = (int)Math.Round(i);
+
+ foreach (var part in data.Chunk(pc))
+ {
+ double d = 0;
+ var res = new StatisticsData();
+
+ foreach (var entry in part)
+ {
+ d += entry.Value;
+ }
+
+ res.Chart = part.First().Chart;
+ res.Date = part.First().Date;
+
+ if (d == 0)
+ res.Value = 0;
+
+ res.Value = d / part.Length;
+ result.Add(res);
+ }
+
+ return result.ToArray();
+ }
+}
\ No newline at end of file
diff --git a/Moonlight/App/Services/Sessions/SessionService.cs b/Moonlight/App/Services/Sessions/SessionService.cs
index f7b97cef..dad4fa8f 100644
--- a/Moonlight/App/Services/Sessions/SessionService.cs
+++ b/Moonlight/App/Services/Sessions/SessionService.cs
@@ -9,6 +9,7 @@ namespace Moonlight.App.Services.Sessions;
public class SessionService
{
private readonly SessionRepository SessionRepository;
+ private Repository UserRepository;
private readonly IdentityService IdentityService;
private readonly NavigationManager NavigationManager;
private readonly AlertService AlertService;
@@ -21,13 +22,15 @@ public class SessionService
IdentityService identityService,
NavigationManager navigationManager,
AlertService alertService,
- DateTimeService dateTimeService)
+ DateTimeService dateTimeService,
+ Repository userRepository)
{
SessionRepository = sessionRepository;
IdentityService = identityService;
NavigationManager = navigationManager;
AlertService = alertService;
DateTimeService = dateTimeService;
+ UserRepository = userRepository;
}
public async Task Register()
@@ -46,6 +49,12 @@ public class SessionService
};
SessionRepository.Add(OwnSession);
+
+ if (user != null) // Track last session init of user as last visited timestamp
+ {
+ user.LastVisitedAt = DateTimeService.GetCurrent();
+ UserRepository.Update(user);
+ }
}
public void Refresh()
diff --git a/Moonlight/App/Services/Statistics/StatisticsViewService.cs b/Moonlight/App/Services/Statistics/StatisticsViewService.cs
index dfb68f01..2a46fa0a 100644
--- a/Moonlight/App/Services/Statistics/StatisticsViewService.cs
+++ b/Moonlight/App/Services/Statistics/StatisticsViewService.cs
@@ -7,20 +7,33 @@ namespace Moonlight.App.Services.Statistics;
public class StatisticsViewService
{
private readonly StatisticsRepository StatisticsRepository;
+ private readonly Repository UserRepository;
private readonly DateTimeService DateTimeService;
- public StatisticsViewService(StatisticsRepository statisticsRepository, DateTimeService dateTimeService)
+ public StatisticsViewService(StatisticsRepository statisticsRepository, DateTimeService dateTimeService, Repository userRepository)
{
StatisticsRepository = statisticsRepository;
DateTimeService = dateTimeService;
+ UserRepository = userRepository;
}
public StatisticsData[] GetData(string chart, StatisticsTimeSpan timeSpan)
{
var startDate = DateTimeService.GetCurrent() - TimeSpan.FromHours((int)timeSpan);
- var objs = StatisticsRepository.Get().Where(x => x.Date > startDate && x.Chart == chart);
+ var objs = StatisticsRepository
+ .Get()
+ .Where(x => x.Date > startDate && x.Chart == chart);
return objs.ToArray();
}
+
+ public int GetActiveUsers(StatisticsTimeSpan timeSpan)
+ {
+ var startDate = DateTimeService.GetCurrent() - TimeSpan.FromHours((int)timeSpan);
+
+ return UserRepository
+ .Get()
+ .Count(x => x.LastVisitedAt > startDate);
+ }
}
\ No newline at end of file
diff --git a/Moonlight/Shared/Views/Admin/Statistics.razor b/Moonlight/Shared/Views/Admin/Statistics.razor
index d2645e70..14481fba 100644
--- a/Moonlight/Shared/Views/Admin/Statistics.razor
+++ b/Moonlight/Shared/Views/Admin/Statistics.razor
@@ -66,6 +66,20 @@
}
}
+
@@ -73,7 +87,9 @@
{
private StatisticsTimeSpan StatisticsTimeSpan = StatisticsTimeSpan.Day;
private LazyLoader Loader;
+
private Dictionary Charts = new();
+ private int ActiveUsers = 0;
private int TimeSpanBind
{
@@ -91,34 +107,48 @@
Charts.Add(
SmartTranslateService.Translate("Servers"),
- StatisticsViewService.GetData("serversCount", StatisticsTimeSpan)
+ AvgHelper.Calculate(
+ StatisticsViewService.GetData("serversCount", StatisticsTimeSpan)
+ )
);
Charts.Add(
SmartTranslateService.Translate("Users"),
- StatisticsViewService.GetData("usersCount", StatisticsTimeSpan)
+ AvgHelper.Calculate(
+ StatisticsViewService.GetData("usersCount", StatisticsTimeSpan)
+ )
);
Charts.Add(
SmartTranslateService.Translate("Domains"),
- StatisticsViewService.GetData("domainsCount", StatisticsTimeSpan)
+ AvgHelper.Calculate(
+ StatisticsViewService.GetData("domainsCount", StatisticsTimeSpan)
+ )
);
Charts.Add(
SmartTranslateService.Translate("Databases"),
- StatisticsViewService.GetData("databasesCount", StatisticsTimeSpan)
+ AvgHelper.Calculate(
+ StatisticsViewService.GetData("databasesCount", StatisticsTimeSpan)
+ )
);
Charts.Add(
SmartTranslateService.Translate("Webspaces"),
- StatisticsViewService.GetData("webspacesCount", StatisticsTimeSpan)
+ AvgHelper.Calculate(
+ StatisticsViewService.GetData("webspacesCount", StatisticsTimeSpan)
+ )
);
Charts.Add(
SmartTranslateService.Translate("Sessions"),
- StatisticsViewService.GetData("sessionsCount", StatisticsTimeSpan)
+ AvgHelper.Calculate(
+ StatisticsViewService.GetData("sessionsCount", StatisticsTimeSpan)
+ )
);
+ ActiveUsers = StatisticsViewService.GetActiveUsers(StatisticsTimeSpan);
+
return Task.CompletedTask;
}