Jelajahi Sumber

Merge pull request #46 from Moonlight-Panel/PleskIntegration

Plesk integration
Marcel Baumgartner 2 tahun lalu
induk
melakukan
9188323594
56 mengubah file dengan 4290 tambahan dan 74 penghapusan
  1. 2 0
      Moonlight/App/Database/DataContext.cs
  2. 9 0
      Moonlight/App/Database/Entities/PleskServer.cs
  3. 12 0
      Moonlight/App/Database/Entities/Website.cs
  4. 946 0
      Moonlight/App/Database/Migrations/20230404181522_AddedPleskAndWebsiteModels.Designer.cs
  5. 84 0
      Moonlight/App/Database/Migrations/20230404181522_AddedPleskAndWebsiteModels.cs
  6. 954 0
      Moonlight/App/Database/Migrations/20230405162507_UpdatedWebsiteModel.Designer.cs
  7. 40 0
      Moonlight/App/Database/Migrations/20230405162507_UpdatedWebsiteModel.cs
  8. 78 0
      Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs
  9. 32 0
      Moonlight/App/Exceptions/DaemonException.cs
  10. 32 0
      Moonlight/App/Exceptions/PleskException.cs
  11. 1 1
      Moonlight/App/Helpers/DaemonApiHelper.cs
  12. 15 17
      Moonlight/App/Helpers/Files/FtpFileAccess.cs
  13. 220 0
      Moonlight/App/Helpers/PleskApiHelper.cs
  14. 17 0
      Moonlight/App/Models/Forms/DatabaseDataModel.cs
  15. 16 0
      Moonlight/App/Models/Forms/PleskServerDataModel.cs
  16. 14 0
      Moonlight/App/Models/Forms/WebsiteAdminDataModel.cs
  17. 10 0
      Moonlight/App/Models/Forms/WebsiteDataModel.cs
  18. 10 0
      Moonlight/App/Models/Plesk/Requests/CliCall.cs
  19. 23 0
      Moonlight/App/Models/Plesk/Requests/CreateDatabase.cs
  20. 15 0
      Moonlight/App/Models/Plesk/Requests/CreateDatabaseUser.cs
  21. 45 0
      Moonlight/App/Models/Plesk/Requests/CreateDomain.cs
  22. 13 0
      Moonlight/App/Models/Plesk/Resources/CliResult.cs
  23. 45 0
      Moonlight/App/Models/Plesk/Resources/Client.cs
  24. 12 0
      Moonlight/App/Models/Plesk/Resources/CreateResult.cs
  25. 15 0
      Moonlight/App/Models/Plesk/Resources/Database.cs
  26. 30 0
      Moonlight/App/Models/Plesk/Resources/DatabaseServer.cs
  27. 15 0
      Moonlight/App/Models/Plesk/Resources/DatabaseUser.cs
  28. 18 0
      Moonlight/App/Models/Plesk/Resources/ServerStatus.cs
  29. 44 0
      Moonlight/App/Repositories/PleskServerRepository.cs
  30. 44 0
      Moonlight/App/Repositories/WebsiteRepository.cs
  31. 18 0
      Moonlight/App/Services/NodeService.cs
  32. 13 5
      Moonlight/App/Services/ServerService.cs
  33. 383 0
      Moonlight/App/Services/WebsiteService.cs
  34. 5 1
      Moonlight/Program.cs
  35. 11 0
      Moonlight/Shared/Components/ErrorBoundaries/SoftErrorBoundary.razor
  36. 2 2
      Moonlight/Shared/Components/FileManagerPartials/FileManager.razor
  37. 1 6
      Moonlight/Shared/Components/Forms/DeleteButton.razor
  38. 22 0
      Moonlight/Shared/Components/Navigations/AdminWebsitesNavigation.razor
  39. 5 24
      Moonlight/Shared/Components/Partials/SidebarMenu.razor
  40. 111 0
      Moonlight/Shared/Components/WebsiteControl/WebsiteDashboard.razor
  41. 135 0
      Moonlight/Shared/Components/WebsiteControl/WebsiteDatabases.razor
  42. 24 0
      Moonlight/Shared/Components/WebsiteControl/WebsiteFiles.razor
  43. 64 0
      Moonlight/Shared/Components/WebsiteControl/WebsiteFtp.razor
  44. 57 0
      Moonlight/Shared/Components/WebsiteControl/WebsiteNavigation.razor
  45. 0 2
      Moonlight/Shared/Views/Admin/Servers/New.razor
  46. 93 0
      Moonlight/Shared/Views/Admin/Websites/Index.razor
  47. 79 0
      Moonlight/Shared/Views/Admin/Websites/New.razor
  48. 98 0
      Moonlight/Shared/Views/Admin/Websites/Servers/Edit.razor
  49. 115 0
      Moonlight/Shared/Views/Admin/Websites/Servers/Index.razor
  50. 55 0
      Moonlight/Shared/Views/Admin/Websites/Servers/New.razor
  51. 8 16
      Moonlight/Shared/Views/Server/Index.razor
  52. 132 0
      Moonlight/Shared/Views/Website/Index.razor
  53. 1 0
      Moonlight/Shared/Views/Websites/Index.razor
  54. 1 0
      Moonlight/Shared/Views/Websites/New.razor
  55. 25 0
      Moonlight/resources/lang/de_de.lang
  56. 21 0
      Moonlight/resources/lang/en_us.lang

+ 2 - 0
Moonlight/App/Database/DataContext.cs

@@ -39,6 +39,8 @@ public class DataContext : DbContext
     public DbSet<NotificationAction> NotificationActions { get; set; }
     public DbSet<DdosAttack> DdosAttacks { get; set; }
     public DbSet<Subscription> Subscriptions { get; set; }
+    public DbSet<PleskServer> PleskServers { get; set; }
+    public DbSet<Website> Websites { get; set; }
 
     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
     {

+ 9 - 0
Moonlight/App/Database/Entities/PleskServer.cs

@@ -0,0 +1,9 @@
+namespace Moonlight.App.Database.Entities;
+
+public class PleskServer
+{
+    public int Id { get; set; }
+    public string Name { get; set; } = "";
+    public string ApiUrl { get; set; } = "";
+    public string ApiKey { get; set; } = "";
+}

+ 12 - 0
Moonlight/App/Database/Entities/Website.cs

@@ -0,0 +1,12 @@
+namespace Moonlight.App.Database.Entities;
+
+public class Website
+{
+    public int Id { get; set; }
+    public string BaseDomain { get; set; } = "";
+    public int PleskId { get; set; }
+    public PleskServer PleskServer { get; set; }
+    public User Owner { get; set; }
+    public string FtpLogin { get; set; } = "";
+    public string FtpPassword { get; set; } = "";
+}

+ 946 - 0
Moonlight/App/Database/Migrations/20230404181522_AddedPleskAndWebsiteModels.Designer.cs

@@ -0,0 +1,946 @@
+// <auto-generated />
+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("20230404181522_AddedPleskAndWebsiteModels")]
+    partial class AddedPleskAndWebsiteModels
+    {
+        /// <inheritdoc />
+        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.DdosAttack", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime(6)");
+
+                    b.Property<long>("Data")
+                        .HasColumnType("bigint");
+
+                    b.Property<string>("Ip")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<int>("NodeId")
+                        .HasColumnType("int");
+
+                    b.Property<bool>("Ongoing")
+                        .HasColumnType("tinyint(1)");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("NodeId");
+
+                    b.ToTable("DdosAttacks");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.DockerImage", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<bool>("Default")
+                        .HasColumnType("tinyint(1)");
+
+                    b.Property<int?>("ImageId")
+                        .HasColumnType("int");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("ImageId");
+
+                    b.ToTable("DockerImages");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.Domain", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<int>("OwnerId")
+                        .HasColumnType("int");
+
+                    b.Property<int>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<int>("Allocations")
+                        .HasColumnType("int");
+
+                    b.Property<string>("ConfigFiles")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("Description")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("InstallDockerImage")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("InstallEntrypoint")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("InstallScript")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("Startup")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("StartupDetection")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("StopCommand")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("TagsJson")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<Guid>("Uuid")
+                        .HasColumnType("char(36)");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("Images");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.ImageTag", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("ImageTags");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.ImageVariable", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("DefaultValue")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<int?>("ImageId")
+                        .HasColumnType("int");
+
+                    b.Property<string>("Key")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("ImageId");
+
+                    b.ToTable("ImageVariables");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.LoadingMessage", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("LoadingMessages");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.LogsEntries.AuditLogEntry", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime(6)");
+
+                    b.Property<string>("Ip")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("JsonData")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<bool>("System")
+                        .HasColumnType("tinyint(1)");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("int");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("AuditLog");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.LogsEntries.ErrorLogEntry", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("Class")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime(6)");
+
+                    b.Property<string>("Ip")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("JsonData")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("Stacktrace")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<bool>("System")
+                        .HasColumnType("tinyint(1)");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("ErrorLog");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.LogsEntries.SecurityLogEntry", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime(6)");
+
+                    b.Property<string>("Ip")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("JsonData")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<bool>("System")
+                        .HasColumnType("tinyint(1)");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("int");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("SecurityLog");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.Node", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("Fqdn")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<int>("HttpPort")
+                        .HasColumnType("int");
+
+                    b.Property<int>("MoonlightDaemonPort")
+                        .HasColumnType("int");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<int>("SftpPort")
+                        .HasColumnType("int");
+
+                    b.Property<bool>("Ssl")
+                        .HasColumnType("tinyint(1)");
+
+                    b.Property<string>("Token")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("TokenId")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("Nodes");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.NodeAllocation", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<int?>("NodeId")
+                        .HasColumnType("int");
+
+                    b.Property<int>("Port")
+                        .HasColumnType("int");
+
+                    b.Property<int?>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("Action")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<int>("NotificationClientId")
+                        .HasColumnType("int");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("NotificationClientId");
+
+                    b.ToTable("NotificationActions");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.Notification.NotificationClient", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<int>("UserId")
+                        .HasColumnType("int");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("NotificationClients");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.PleskServer", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("ApiKey")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("ApiUrl")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("PleskServers");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.Revoke", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("Identifier")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("Revokes");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.Server", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<int>("Cpu")
+                        .HasColumnType("int");
+
+                    b.Property<long>("Disk")
+                        .HasColumnType("bigint");
+
+                    b.Property<int>("DockerImageIndex")
+                        .HasColumnType("int");
+
+                    b.Property<int>("ImageId")
+                        .HasColumnType("int");
+
+                    b.Property<bool>("Installing")
+                        .HasColumnType("tinyint(1)");
+
+                    b.Property<bool>("IsCleanupException")
+                        .HasColumnType("tinyint(1)");
+
+                    b.Property<int>("MainAllocationId")
+                        .HasColumnType("int");
+
+                    b.Property<long>("Memory")
+                        .HasColumnType("bigint");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<int>("NodeId")
+                        .HasColumnType("int");
+
+                    b.Property<string>("OverrideStartup")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<int>("OwnerId")
+                        .HasColumnType("int");
+
+                    b.Property<bool>("Suspended")
+                        .HasColumnType("tinyint(1)");
+
+                    b.Property<Guid>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<long>("Bytes")
+                        .HasColumnType("bigint");
+
+                    b.Property<bool>("Created")
+                        .HasColumnType("tinyint(1)");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime(6)");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<int?>("ServerId")
+                        .HasColumnType("int");
+
+                    b.Property<Guid>("Uuid")
+                        .HasColumnType("char(36)");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("ServerId");
+
+                    b.ToTable("ServerBackups");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.ServerVariable", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("Key")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<int?>("ServerId")
+                        .HasColumnType("int");
+
+                    b.Property<string>("Value")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("ServerId");
+
+                    b.ToTable("ServerVariables");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.SharedDomain", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("CloudflareId")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("SharedDomains");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.Subscription", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("Description")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("LimitsJson")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("Subscriptions");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.SupportMessage", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("Answer")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime(6)");
+
+                    b.Property<bool>("IsQuestion")
+                        .HasColumnType("tinyint(1)");
+
+                    b.Property<bool>("IsSupport")
+                        .HasColumnType("tinyint(1)");
+
+                    b.Property<bool>("IsSystem")
+                        .HasColumnType("tinyint(1)");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<int?>("RecipientId")
+                        .HasColumnType("int");
+
+                    b.Property<int?>("SenderId")
+                        .HasColumnType("int");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("int");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("RecipientId");
+
+                    b.HasIndex("SenderId");
+
+                    b.ToTable("SupportMessages");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("Address")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<bool>("Admin")
+                        .HasColumnType("tinyint(1)");
+
+                    b.Property<string>("City")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("Country")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime(6)");
+
+                    b.Property<int?>("CurrentSubscriptionId")
+                        .HasColumnType("int");
+
+                    b.Property<long>("DiscordId")
+                        .HasColumnType("bigint");
+
+                    b.Property<string>("Email")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("FirstName")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("LastName")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("Password")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("State")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<int>("Status")
+                        .HasColumnType("int");
+
+                    b.Property<int>("SubscriptionDuration")
+                        .HasColumnType("int");
+
+                    b.Property<DateTime>("SubscriptionSince")
+                        .HasColumnType("datetime(6)");
+
+                    b.Property<bool>("SupportPending")
+                        .HasColumnType("tinyint(1)");
+
+                    b.Property<DateTime>("TokenValidTime")
+                        .HasColumnType("datetime(6)");
+
+                    b.Property<bool>("TotpEnabled")
+                        .HasColumnType("tinyint(1)");
+
+                    b.Property<string>("TotpSecret")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<DateTime>("UpdatedAt")
+                        .HasColumnType("datetime(6)");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("CurrentSubscriptionId");
+
+                    b.ToTable("Users");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.Website", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("BaseDomain")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<int>("OwnerId")
+                        .HasColumnType("int");
+
+                    b.Property<int>("PleskId")
+                        .HasColumnType("int");
+
+                    b.Property<int>("PleskServerId")
+                        .HasColumnType("int");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("OwnerId");
+
+                    b.HasIndex("PleskServerId");
+
+                    b.ToTable("Websites");
+                });
+
+            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.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")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    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.SupportMessage", b =>
+                {
+                    b.HasOne("Moonlight.App.Database.Entities.User", "Recipient")
+                        .WithMany()
+                        .HasForeignKey("RecipientId");
+
+                    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.Website", b =>
+                {
+                    b.HasOne("Moonlight.App.Database.Entities.User", "Owner")
+                        .WithMany()
+                        .HasForeignKey("OwnerId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Moonlight.App.Database.Entities.PleskServer", "PleskServer")
+                        .WithMany()
+                        .HasForeignKey("PleskServerId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Owner");
+
+                    b.Navigation("PleskServer");
+                });
+
+            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");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}

+ 84 - 0
Moonlight/App/Database/Migrations/20230404181522_AddedPleskAndWebsiteModels.cs

@@ -0,0 +1,84 @@
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Moonlight.App.Database.Migrations
+{
+    /// <inheritdoc />
+    public partial class AddedPleskAndWebsiteModels : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.CreateTable(
+                name: "PleskServers",
+                columns: table => new
+                {
+                    Id = table.Column<int>(type: "int", nullable: false)
+                        .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
+                    Name = table.Column<string>(type: "longtext", nullable: false)
+                        .Annotation("MySql:CharSet", "utf8mb4"),
+                    ApiUrl = table.Column<string>(type: "longtext", nullable: false)
+                        .Annotation("MySql:CharSet", "utf8mb4"),
+                    ApiKey = table.Column<string>(type: "longtext", nullable: false)
+                        .Annotation("MySql:CharSet", "utf8mb4")
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_PleskServers", x => x.Id);
+                })
+                .Annotation("MySql:CharSet", "utf8mb4");
+
+            migrationBuilder.CreateTable(
+                name: "Websites",
+                columns: table => new
+                {
+                    Id = table.Column<int>(type: "int", nullable: false)
+                        .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
+                    BaseDomain = table.Column<string>(type: "longtext", nullable: false)
+                        .Annotation("MySql:CharSet", "utf8mb4"),
+                    PleskId = table.Column<int>(type: "int", nullable: false),
+                    PleskServerId = table.Column<int>(type: "int", nullable: false),
+                    OwnerId = table.Column<int>(type: "int", nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_Websites", x => x.Id);
+                    table.ForeignKey(
+                        name: "FK_Websites_PleskServers_PleskServerId",
+                        column: x => x.PleskServerId,
+                        principalTable: "PleskServers",
+                        principalColumn: "Id",
+                        onDelete: ReferentialAction.Cascade);
+                    table.ForeignKey(
+                        name: "FK_Websites_Users_OwnerId",
+                        column: x => x.OwnerId,
+                        principalTable: "Users",
+                        principalColumn: "Id",
+                        onDelete: ReferentialAction.Cascade);
+                })
+                .Annotation("MySql:CharSet", "utf8mb4");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Websites_OwnerId",
+                table: "Websites",
+                column: "OwnerId");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Websites_PleskServerId",
+                table: "Websites",
+                column: "PleskServerId");
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropTable(
+                name: "Websites");
+
+            migrationBuilder.DropTable(
+                name: "PleskServers");
+        }
+    }
+}

+ 954 - 0
Moonlight/App/Database/Migrations/20230405162507_UpdatedWebsiteModel.Designer.cs

@@ -0,0 +1,954 @@
+// <auto-generated />
+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("20230405162507_UpdatedWebsiteModel")]
+    partial class UpdatedWebsiteModel
+    {
+        /// <inheritdoc />
+        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.DdosAttack", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime(6)");
+
+                    b.Property<long>("Data")
+                        .HasColumnType("bigint");
+
+                    b.Property<string>("Ip")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<int>("NodeId")
+                        .HasColumnType("int");
+
+                    b.Property<bool>("Ongoing")
+                        .HasColumnType("tinyint(1)");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("NodeId");
+
+                    b.ToTable("DdosAttacks");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.DockerImage", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<bool>("Default")
+                        .HasColumnType("tinyint(1)");
+
+                    b.Property<int?>("ImageId")
+                        .HasColumnType("int");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("ImageId");
+
+                    b.ToTable("DockerImages");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.Domain", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<int>("OwnerId")
+                        .HasColumnType("int");
+
+                    b.Property<int>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<int>("Allocations")
+                        .HasColumnType("int");
+
+                    b.Property<string>("ConfigFiles")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("Description")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("InstallDockerImage")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("InstallEntrypoint")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("InstallScript")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("Startup")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("StartupDetection")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("StopCommand")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("TagsJson")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<Guid>("Uuid")
+                        .HasColumnType("char(36)");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("Images");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.ImageTag", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("ImageTags");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.ImageVariable", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("DefaultValue")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<int?>("ImageId")
+                        .HasColumnType("int");
+
+                    b.Property<string>("Key")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("ImageId");
+
+                    b.ToTable("ImageVariables");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.LoadingMessage", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("LoadingMessages");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.LogsEntries.AuditLogEntry", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime(6)");
+
+                    b.Property<string>("Ip")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("JsonData")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<bool>("System")
+                        .HasColumnType("tinyint(1)");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("int");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("AuditLog");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.LogsEntries.ErrorLogEntry", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("Class")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime(6)");
+
+                    b.Property<string>("Ip")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("JsonData")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("Stacktrace")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<bool>("System")
+                        .HasColumnType("tinyint(1)");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("ErrorLog");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.LogsEntries.SecurityLogEntry", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime(6)");
+
+                    b.Property<string>("Ip")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("JsonData")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<bool>("System")
+                        .HasColumnType("tinyint(1)");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("int");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("SecurityLog");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.Node", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("Fqdn")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<int>("HttpPort")
+                        .HasColumnType("int");
+
+                    b.Property<int>("MoonlightDaemonPort")
+                        .HasColumnType("int");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<int>("SftpPort")
+                        .HasColumnType("int");
+
+                    b.Property<bool>("Ssl")
+                        .HasColumnType("tinyint(1)");
+
+                    b.Property<string>("Token")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("TokenId")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("Nodes");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.NodeAllocation", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<int?>("NodeId")
+                        .HasColumnType("int");
+
+                    b.Property<int>("Port")
+                        .HasColumnType("int");
+
+                    b.Property<int?>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("Action")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<int>("NotificationClientId")
+                        .HasColumnType("int");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("NotificationClientId");
+
+                    b.ToTable("NotificationActions");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.Notification.NotificationClient", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<int>("UserId")
+                        .HasColumnType("int");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("NotificationClients");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.PleskServer", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("ApiKey")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("ApiUrl")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("PleskServers");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.Revoke", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("Identifier")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("Revokes");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.Server", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<int>("Cpu")
+                        .HasColumnType("int");
+
+                    b.Property<long>("Disk")
+                        .HasColumnType("bigint");
+
+                    b.Property<int>("DockerImageIndex")
+                        .HasColumnType("int");
+
+                    b.Property<int>("ImageId")
+                        .HasColumnType("int");
+
+                    b.Property<bool>("Installing")
+                        .HasColumnType("tinyint(1)");
+
+                    b.Property<bool>("IsCleanupException")
+                        .HasColumnType("tinyint(1)");
+
+                    b.Property<int>("MainAllocationId")
+                        .HasColumnType("int");
+
+                    b.Property<long>("Memory")
+                        .HasColumnType("bigint");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<int>("NodeId")
+                        .HasColumnType("int");
+
+                    b.Property<string>("OverrideStartup")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<int>("OwnerId")
+                        .HasColumnType("int");
+
+                    b.Property<bool>("Suspended")
+                        .HasColumnType("tinyint(1)");
+
+                    b.Property<Guid>("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<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<long>("Bytes")
+                        .HasColumnType("bigint");
+
+                    b.Property<bool>("Created")
+                        .HasColumnType("tinyint(1)");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime(6)");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<int?>("ServerId")
+                        .HasColumnType("int");
+
+                    b.Property<Guid>("Uuid")
+                        .HasColumnType("char(36)");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("ServerId");
+
+                    b.ToTable("ServerBackups");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.ServerVariable", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("Key")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<int?>("ServerId")
+                        .HasColumnType("int");
+
+                    b.Property<string>("Value")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("ServerId");
+
+                    b.ToTable("ServerVariables");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.SharedDomain", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("CloudflareId")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("SharedDomains");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.Subscription", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("Description")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("LimitsJson")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("Subscriptions");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.SupportMessage", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("Answer")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime(6)");
+
+                    b.Property<bool>("IsQuestion")
+                        .HasColumnType("tinyint(1)");
+
+                    b.Property<bool>("IsSupport")
+                        .HasColumnType("tinyint(1)");
+
+                    b.Property<bool>("IsSystem")
+                        .HasColumnType("tinyint(1)");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<int?>("RecipientId")
+                        .HasColumnType("int");
+
+                    b.Property<int?>("SenderId")
+                        .HasColumnType("int");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("int");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("RecipientId");
+
+                    b.HasIndex("SenderId");
+
+                    b.ToTable("SupportMessages");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("Address")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<bool>("Admin")
+                        .HasColumnType("tinyint(1)");
+
+                    b.Property<string>("City")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("Country")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime(6)");
+
+                    b.Property<int?>("CurrentSubscriptionId")
+                        .HasColumnType("int");
+
+                    b.Property<long>("DiscordId")
+                        .HasColumnType("bigint");
+
+                    b.Property<string>("Email")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("FirstName")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("LastName")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("Password")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("State")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<int>("Status")
+                        .HasColumnType("int");
+
+                    b.Property<int>("SubscriptionDuration")
+                        .HasColumnType("int");
+
+                    b.Property<DateTime>("SubscriptionSince")
+                        .HasColumnType("datetime(6)");
+
+                    b.Property<bool>("SupportPending")
+                        .HasColumnType("tinyint(1)");
+
+                    b.Property<DateTime>("TokenValidTime")
+                        .HasColumnType("datetime(6)");
+
+                    b.Property<bool>("TotpEnabled")
+                        .HasColumnType("tinyint(1)");
+
+                    b.Property<string>("TotpSecret")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<DateTime>("UpdatedAt")
+                        .HasColumnType("datetime(6)");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("CurrentSubscriptionId");
+
+                    b.ToTable("Users");
+                });
+
+            modelBuilder.Entity("Moonlight.App.Database.Entities.Website", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("BaseDomain")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("FtpLogin")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("FtpPassword")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<int>("OwnerId")
+                        .HasColumnType("int");
+
+                    b.Property<int>("PleskId")
+                        .HasColumnType("int");
+
+                    b.Property<int>("PleskServerId")
+                        .HasColumnType("int");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("OwnerId");
+
+                    b.HasIndex("PleskServerId");
+
+                    b.ToTable("Websites");
+                });
+
+            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.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")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    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.SupportMessage", b =>
+                {
+                    b.HasOne("Moonlight.App.Database.Entities.User", "Recipient")
+                        .WithMany()
+                        .HasForeignKey("RecipientId");
+
+                    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.Website", b =>
+                {
+                    b.HasOne("Moonlight.App.Database.Entities.User", "Owner")
+                        .WithMany()
+                        .HasForeignKey("OwnerId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Moonlight.App.Database.Entities.PleskServer", "PleskServer")
+                        .WithMany()
+                        .HasForeignKey("PleskServerId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Owner");
+
+                    b.Navigation("PleskServer");
+                });
+
+            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");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}

+ 40 - 0
Moonlight/App/Database/Migrations/20230405162507_UpdatedWebsiteModel.cs

@@ -0,0 +1,40 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Moonlight.App.Database.Migrations
+{
+    /// <inheritdoc />
+    public partial class UpdatedWebsiteModel : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.AddColumn<string>(
+                name: "FtpLogin",
+                table: "Websites",
+                type: "longtext",
+                nullable: false)
+                .Annotation("MySql:CharSet", "utf8mb4");
+
+            migrationBuilder.AddColumn<string>(
+                name: "FtpPassword",
+                table: "Websites",
+                type: "longtext",
+                nullable: false)
+                .Annotation("MySql:CharSet", "utf8mb4");
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropColumn(
+                name: "FtpLogin",
+                table: "Websites");
+
+            migrationBuilder.DropColumn(
+                name: "FtpPassword",
+                table: "Websites");
+        }
+    }
+}

+ 78 - 0
Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs

@@ -395,6 +395,29 @@ namespace Moonlight.App.Database.Migrations
                     b.ToTable("NotificationClients");
                 });
 
+            modelBuilder.Entity("Moonlight.App.Database.Entities.PleskServer", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("ApiKey")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("ApiUrl")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("PleskServers");
+                });
+
             modelBuilder.Entity("Moonlight.App.Database.Entities.Revoke", b =>
                 {
                     b.Property<int>("Id")
@@ -697,6 +720,42 @@ namespace Moonlight.App.Database.Migrations
                     b.ToTable("Users");
                 });
 
+            modelBuilder.Entity("Moonlight.App.Database.Entities.Website", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    b.Property<string>("BaseDomain")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("FtpLogin")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<string>("FtpPassword")
+                        .IsRequired()
+                        .HasColumnType("longtext");
+
+                    b.Property<int>("OwnerId")
+                        .HasColumnType("int");
+
+                    b.Property<int>("PleskId")
+                        .HasColumnType("int");
+
+                    b.Property<int>("PleskServerId")
+                        .HasColumnType("int");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("OwnerId");
+
+                    b.HasIndex("PleskServerId");
+
+                    b.ToTable("Websites");
+                });
+
             modelBuilder.Entity("Moonlight.App.Database.Entities.DdosAttack", b =>
                 {
                     b.HasOne("Moonlight.App.Database.Entities.Node", "Node")
@@ -847,6 +906,25 @@ namespace Moonlight.App.Database.Migrations
                     b.Navigation("CurrentSubscription");
                 });
 
+            modelBuilder.Entity("Moonlight.App.Database.Entities.Website", b =>
+                {
+                    b.HasOne("Moonlight.App.Database.Entities.User", "Owner")
+                        .WithMany()
+                        .HasForeignKey("OwnerId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Moonlight.App.Database.Entities.PleskServer", "PleskServer")
+                        .WithMany()
+                        .HasForeignKey("PleskServerId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Owner");
+
+                    b.Navigation("PleskServer");
+                });
+
             modelBuilder.Entity("Moonlight.App.Database.Entities.Image", b =>
                 {
                     b.Navigation("DockerImages");

+ 32 - 0
Moonlight/App/Exceptions/DaemonException.cs

@@ -0,0 +1,32 @@
+using System.Runtime.Serialization;
+
+namespace Moonlight.App.Exceptions;
+
+[Serializable]
+public class DaemonException : Exception
+{
+    public int StatusCode { private get; set; }
+
+    public DaemonException()
+    {
+    }
+
+    public DaemonException(string message, int statusCode) : base(message)
+    {
+        StatusCode = statusCode;
+    }
+
+    public DaemonException(string message) : base(message)
+    {
+    }
+
+    public DaemonException(string message, Exception inner) : base(message, inner)
+    {
+    }
+
+    protected DaemonException(
+        SerializationInfo info,
+        StreamingContext context) : base(info, context)
+    {
+    }
+}

+ 32 - 0
Moonlight/App/Exceptions/PleskException.cs

@@ -0,0 +1,32 @@
+using System.Runtime.Serialization;
+
+namespace Moonlight.App.Exceptions;
+
+[Serializable]
+public class PleskException : Exception
+{
+    public int StatusCode { private get; set; }
+
+    public PleskException()
+    {
+    }
+
+    public PleskException(string message, int statusCode) : base(message)
+    {
+        StatusCode = statusCode;
+    }
+
+    public PleskException(string message) : base(message)
+    {
+    }
+
+    public PleskException(string message, Exception inner) : base(message, inner)
+    {
+    }
+
+    protected PleskException(
+        SerializationInfo info,
+        StreamingContext context) : base(info, context)
+    {
+    }
+}

+ 1 - 1
Moonlight/App/Helpers/DaemonApiHelper.cs

@@ -37,7 +37,7 @@ public class DaemonApiHelper
         {
             if (response.StatusCode != 0)
             {
-                throw new WingsException(
+                throw new DaemonException(
                     $"An error occured: ({response.StatusCode}) {response.Content}",
                     (int)response.StatusCode
                 );

+ 15 - 17
Moonlight/App/Helpers/Files/FtpFileAccess.cs

@@ -1,6 +1,7 @@
 using System.Net;
 using System.Text;
 using FluentFTP;
+using Moonlight.App.Exceptions;
 
 namespace Moonlight.App.Helpers.Files;
 
@@ -78,10 +79,10 @@ public class FtpFileAccess : FileAccess
     {
         await EnsureConnect();
 
-        var s = new MemoryStream();
+        var s = new MemoryStream(8 * 1024 * 1204); //TODO: Add config option
         await Client.DownloadStream(s, CurrentPath.TrimEnd('/') + "/" + fileData.Name);
         var data = s.ToArray();
-        s.Dispose();
+        await s.DisposeAsync();
         var str = Encoding.UTF8.GetString(data);
         return str;
     }
@@ -90,11 +91,11 @@ public class FtpFileAccess : FileAccess
     {
         await EnsureConnect();
 
-        var s = new MemoryStream();
+        var s = new MemoryStream(8 * 1024 * 1204); //TODO: Add config option
         s.Write(Encoding.UTF8.GetBytes(content));
         s.Position = 0;
         await Client.UploadStream(s, CurrentPath.TrimEnd('/') + "/" + fileData.Name, FtpRemoteExists.Overwrite);
-        s.Dispose();
+        await s.DisposeAsync();
     }
 
     public override async Task Upload(string name, Stream dataStream, Action<int>? progressUpdated = null)
@@ -103,10 +104,9 @@ public class FtpFileAccess : FileAccess
 
         IProgress<FtpProgress> progress = new Progress<FtpProgress>(x =>
         {
-            progressUpdated((int) x.Progress);
+            progressUpdated?.Invoke((int)x.Progress);
         });
         await Client.UploadStream(dataStream, CurrentPath.TrimEnd('/') + "/" + name, FtpRemoteExists.Overwrite, false, progress);
-        dataStream.Dispose();
     }
 
     public override async Task MkDir(string name)
@@ -121,10 +121,8 @@ public class FtpFileAccess : FileAccess
         return Task.FromResult(CurrentPath);
     }
 
-    public override async Task<string> DownloadUrl(FileData fileData)
+    public override Task<string> DownloadUrl(FileData fileData)
     {
-        await EnsureConnect();
-
         throw new NotImplementedException();
     }
 
@@ -132,8 +130,12 @@ public class FtpFileAccess : FileAccess
     {
         await EnsureConnect();
 
-        var s = new MemoryStream();
-        await Client.DownloadStream(s, CurrentPath.TrimEnd('/') + "/" + fileData.Name);
+        var s = new MemoryStream(8 * 1024 * 1204); //TODO: Add config option
+        var downloaded = await Client.DownloadStream(s, CurrentPath.TrimEnd('/') + "/" + fileData.Name);
+
+        if (!downloaded)
+            throw new DisplayException("Unable to download file");
+        
         return s;
     }
 
@@ -157,17 +159,13 @@ public class FtpFileAccess : FileAccess
             await Client.MoveDirectory(CurrentPath.TrimEnd('/') + "/" + fileData.Name, newPath);
     }
 
-    public override async Task Compress(params FileData[] files)
+    public override Task Compress(params FileData[] files)
     {
-        await EnsureConnect();
-
         throw new NotImplementedException();
     }
 
-    public override async Task Decompress(FileData fileData)
+    public override Task Decompress(FileData fileData)
     {
-        await EnsureConnect();
-
         throw new NotImplementedException();
     }
 

+ 220 - 0
Moonlight/App/Helpers/PleskApiHelper.cs

@@ -0,0 +1,220 @@
+using System.Text;
+using Moonlight.App.Database.Entities;
+using Moonlight.App.Exceptions;
+using Newtonsoft.Json;
+using RestSharp;
+
+namespace Moonlight.App.Helpers;
+
+public class PleskApiHelper
+{
+    private readonly RestClient Client;
+
+    public PleskApiHelper()
+    {
+        Client = new();
+    }
+
+    public async Task<T> Get<T>(PleskServer server, string resource)
+    {
+        var request = CreateRequest(server, resource);
+
+        request.Method = Method.Get;
+
+        var response = await Client.ExecuteAsync(request);
+
+        if (!response.IsSuccessful)
+        {
+            if (response.StatusCode != 0)
+            {
+                throw new PleskException(
+                    $"An error occured: ({response.StatusCode}) {response.Content}",
+                    (int)response.StatusCode
+                );
+            }
+            else
+            {
+                throw new Exception($"An internal error occured: {response.ErrorMessage}");
+            }
+        }
+
+        return JsonConvert.DeserializeObject<T>(response.Content!)!;
+    }
+
+    public async Task<string> GetRaw(PleskServer server, string resource)
+    {
+        var request = CreateRequest(server, resource);
+
+        request.Method = Method.Get;
+
+        var response = await Client.ExecuteAsync(request);
+
+        if (!response.IsSuccessful)
+        {
+            if (response.StatusCode != 0)
+            {
+                throw new PleskException(
+                    $"An error occured: ({response.StatusCode}) {response.Content}",
+                    (int)response.StatusCode
+                );
+            }
+            else
+            {
+                throw new Exception($"An internal error occured: {response.ErrorMessage}");
+            }
+        }
+
+        return response.Content!;
+    }
+
+    public async Task<T> Post<T>(PleskServer server, string resource, object? body)
+    {
+        var request = CreateRequest(server, resource);
+
+        request.Method = Method.Post;
+
+        request.AddParameter("text/plain",
+            JsonConvert.SerializeObject(body),
+            ParameterType.RequestBody
+        );
+
+        var response = await Client.ExecuteAsync(request);
+
+        if (!response.IsSuccessful)
+        {
+            if (response.StatusCode != 0)
+            {
+                throw new PleskException(
+                    $"An error occured: ({response.StatusCode}) {response.Content}",
+                    (int)response.StatusCode
+                );
+            }
+            else
+            {
+                throw new Exception($"An internal error occured: {response.ErrorMessage}");
+            }
+        }
+
+        return JsonConvert.DeserializeObject<T>(response.Content!)!;
+    }
+
+    public async Task Post(PleskServer server, string resource, object? body)
+    {
+        var request = CreateRequest(server, resource);
+
+        request.Method = Method.Post;
+
+        if(body != null)
+           request.AddParameter("text/plain", JsonConvert.SerializeObject(body), ParameterType.RequestBody);
+
+        var response = await Client.ExecuteAsync(request);
+
+        if (!response.IsSuccessful)
+        {
+            if (response.StatusCode != 0)
+            {
+                throw new PleskException(
+                    $"An error occured: ({response.StatusCode}) {response.Content}",
+                    (int)response.StatusCode
+                );
+            }
+            else
+            {
+                throw new Exception($"An internal error occured: {response.ErrorMessage}");
+            }
+        }
+    }
+
+    public async Task PostRaw(PleskServer server, string resource, object body)
+    {
+        var request = CreateRequest(server, resource);
+
+        request.Method = Method.Post;
+
+        request.AddParameter("text/plain", body, ParameterType.RequestBody);
+
+        var response = await Client.ExecuteAsync(request);
+
+        if (!response.IsSuccessful)
+        {
+            if (response.StatusCode != 0)
+            {
+                throw new PleskException(
+                    $"An error occured: ({response.StatusCode}) {response.Content}",
+                    (int)response.StatusCode
+                );
+            }
+            else
+            {
+                throw new Exception($"An internal error occured: {response.ErrorMessage}");
+            }
+        }
+    }
+
+    public async Task Delete(PleskServer server, string resource, object? body)
+    {
+        var request = CreateRequest(server, resource);
+
+        request.Method = Method.Delete;
+
+        if(body != null)
+           request.AddParameter("text/plain", JsonConvert.SerializeObject(body), ParameterType.RequestBody);
+
+        var response = await Client.ExecuteAsync(request);
+
+        if (!response.IsSuccessful)
+        {
+            if (response.StatusCode != 0)
+            {
+                throw new PleskException(
+                    $"An error occured: ({response.StatusCode}) {response.Content}",
+                    (int)response.StatusCode
+                );
+            }
+            else
+            {
+                throw new Exception($"An internal error occured: {response.ErrorMessage}");
+            }
+        }
+    }
+
+    public async Task Put(PleskServer server, string resource, object? body)
+    {
+        var request = CreateRequest(server, resource);
+
+        request.Method = Method.Put;
+
+        request.AddParameter("text/plain", JsonConvert.SerializeObject(body), ParameterType.RequestBody);
+
+        var response = await Client.ExecuteAsync(request);
+
+        if (!response.IsSuccessful)
+        {
+            if (response.StatusCode != 0)
+            {
+                throw new PleskException(
+                    $"An error occured: ({response.StatusCode}) {response.Content}",
+                    (int)response.StatusCode
+                );
+            }
+            else
+            {
+                throw new Exception($"An internal error occured: {response.ErrorMessage}");
+            }
+        }
+    }
+
+    private RestRequest CreateRequest(PleskServer pleskServer, string resource)
+    {
+        var url = $"{pleskServer.ApiUrl}/" + resource;
+        
+        var request = new RestRequest(url);
+        var ba = Convert.ToBase64String(Encoding.UTF8.GetBytes(pleskServer.ApiKey));
+        
+        request.AddHeader("Content-Type", "application/json");
+        request.AddHeader("Accept", "application/json");
+        request.AddHeader("Authorization", "Basic " + ba);
+
+        return request;
+    }
+}

+ 17 - 0
Moonlight/App/Models/Forms/DatabaseDataModel.cs

@@ -0,0 +1,17 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Moonlight.App.Models.Forms;
+
+public class DatabaseDataModel
+{
+    [Required(ErrorMessage = "You need to enter a name")]
+    [MinLength(8, ErrorMessage = "The name should be at least 8 characters long")]
+    [MaxLength(32, ErrorMessage = "The database name should be maximal 32 characters")]
+    [RegularExpression(@"^[a-z0-9]+$", ErrorMessage = "The name should only contain of lower case characters and numbers")]
+    public string Name { get; set; } = "";
+
+    [Required(ErrorMessage = "You need to enter a password")]
+    [MinLength(8, ErrorMessage = "The password should be at least 8 characters long")]
+    [MaxLength(32, ErrorMessage = "The password name should be maximal 32 characters")]
+    public string Password { get; set; } = "";
+}

+ 16 - 0
Moonlight/App/Models/Forms/PleskServerDataModel.cs

@@ -0,0 +1,16 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Moonlight.App.Models.Forms;
+
+public class PleskServerDataModel
+{
+    [Required(ErrorMessage = "You have to enter a name")]
+    [MaxLength(32, ErrorMessage = "The name should not be longer than 32 characters")]
+    public string Name { get; set; }
+    
+    [Required(ErrorMessage = "You need to enter an api url")]
+    public string ApiUrl { get; set; }
+    
+    [Required(ErrorMessage = "You need to enter an api key")]
+    public string ApiKey { get; set; }
+}

+ 14 - 0
Moonlight/App/Models/Forms/WebsiteAdminDataModel.cs

@@ -0,0 +1,14 @@
+using System.ComponentModel.DataAnnotations;
+using Moonlight.App.Database.Entities;
+
+namespace Moonlight.App.Models.Forms;
+
+public class WebsiteAdminDataModel
+{
+    [Required(ErrorMessage = "You need a domain")]
+    [RegularExpression(@"([a-z0-9|-]+\.)*[a-z0-9|-]+\.[a-z]+", ErrorMessage = "You need to enter a valid domain")]
+    public string BaseDomain { get; set; } = "";
+    
+    [Required(ErrorMessage = "You need to specify a owner")]
+    public User User { get; set; }
+}

+ 10 - 0
Moonlight/App/Models/Forms/WebsiteDataModel.cs

@@ -0,0 +1,10 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Moonlight.App.Models.Forms;
+
+public class WebsiteDataModel
+{
+    [Required(ErrorMessage = "You need a domain")]
+    [RegularExpression(@"([a-z0-9|-]+\.)*[a-z0-9|-]+\.[a-z]+", ErrorMessage = "You need to enter a valid domain")]
+    public string BaseDomain { get; set; } = "";
+}

+ 10 - 0
Moonlight/App/Models/Plesk/Requests/CliCall.cs

@@ -0,0 +1,10 @@
+using Newtonsoft.Json;
+
+namespace Moonlight.App.Models.Plesk.Requests;
+
+public class CliCall
+{
+    [JsonProperty("params")] public List<string> Params { get; set; } = new();
+
+    [JsonProperty("env")] public Dictionary<string, string> Env { get; set; } = new();
+}

+ 23 - 0
Moonlight/App/Models/Plesk/Requests/CreateDatabase.cs

@@ -0,0 +1,23 @@
+using Newtonsoft.Json;
+
+namespace Moonlight.App.Models.Plesk.Requests;
+
+public class CreateDatabase
+{
+    [JsonProperty("name")]
+    public string Name { get; set; }
+
+    [JsonProperty("type")]
+    public string Type { get; set; }
+
+    [JsonProperty("parent_domain")] public ParentDomainModel ParentDomain { get; set; } = new();
+
+    [JsonProperty("server_id")]
+    public int ServerId { get; set; }
+    
+    public class ParentDomainModel
+    {
+        [JsonProperty("name")]
+        public string Name { get; set; }
+    }
+}

+ 15 - 0
Moonlight/App/Models/Plesk/Requests/CreateDatabaseUser.cs

@@ -0,0 +1,15 @@
+using Newtonsoft.Json;
+
+namespace Moonlight.App.Models.Plesk.Requests;
+
+public class CreateDatabaseUser
+{
+    [JsonProperty("login")]
+    public string Login { get; set; }
+
+    [JsonProperty("password")]
+    public string Password { get; set; }
+
+    [JsonProperty("database_id")]
+    public int DatabaseId { get; set; }
+}

+ 45 - 0
Moonlight/App/Models/Plesk/Requests/CreateDomain.cs

@@ -0,0 +1,45 @@
+using Newtonsoft.Json;
+
+namespace Moonlight.App.Models.Plesk.Requests;
+
+public class CreateDomain
+{
+    [JsonProperty("name")]
+    public string Name { get; set; }
+
+    [JsonProperty("description")]
+    public string Description { get; set; }
+
+    [JsonProperty("hosting_type")]
+    public string HostingType { get; set; }
+
+    [JsonProperty("hosting_settings")]
+    public HostingSettingsModel HostingSettings { get; set; }
+
+    [JsonProperty("owner_client")]
+    public OwnerClientModel OwnerClient { get; set; }
+
+    [JsonProperty("plan")]
+    public PlanModel Plan { get; set; }
+    
+    public partial class HostingSettingsModel
+    {
+        [JsonProperty("ftp_login")]
+        public string FtpLogin { get; set; }
+
+        [JsonProperty("ftp_password")]
+        public string FtpPassword { get; set; }
+    }
+
+    public partial class OwnerClientModel
+    {
+        [JsonProperty("id")]
+        public long Id { get; set; }
+    }
+
+    public partial class PlanModel
+    {
+        [JsonProperty("name")]
+        public string Name { get; set; }
+    }
+}

+ 13 - 0
Moonlight/App/Models/Plesk/Resources/CliResult.cs

@@ -0,0 +1,13 @@
+using Newtonsoft.Json;
+
+namespace Moonlight.App.Models.Plesk.Resources;
+
+public class CliResult
+{
+    [JsonProperty("code")]
+    public int Code { get; set; }
+
+    [JsonProperty("stdout")] public string Stdout { get; set; } = "";
+
+    [JsonProperty("stderr")] public string Stderr { get; set; } = "";
+}

+ 45 - 0
Moonlight/App/Models/Plesk/Resources/Client.cs

@@ -0,0 +1,45 @@
+using Newtonsoft.Json;
+
+namespace Moonlight.App.Models.Plesk.Resources;
+
+public class Client
+{
+    [JsonProperty("id")]
+    public int Id { get; set; }
+
+    [JsonProperty("created")]
+    public DateTimeOffset Created { get; set; }
+
+    [JsonProperty("name")]
+    public string Name { get; set; }
+
+    [JsonProperty("company")]
+    public string Company { get; set; }
+
+    [JsonProperty("login")]
+    public string Login { get; set; }
+
+    [JsonProperty("status")]
+    public long Status { get; set; }
+
+    [JsonProperty("email")]
+    public string Email { get; set; }
+
+    [JsonProperty("locale")]
+    public string Locale { get; set; }
+
+    [JsonProperty("guid")]
+    public Guid Guid { get; set; }
+
+    [JsonProperty("owner_login")]
+    public string OwnerLogin { get; set; }
+
+    [JsonProperty("external_id")]
+    public string ExternalId { get; set; }
+
+    [JsonProperty("description")]
+    public string Description { get; set; }
+
+    [JsonProperty("type")]
+    public string Type { get; set; }
+}

+ 12 - 0
Moonlight/App/Models/Plesk/Resources/CreateResult.cs

@@ -0,0 +1,12 @@
+using Newtonsoft.Json;
+
+namespace Moonlight.App.Models.Plesk.Resources;
+
+public class CreateResult
+{
+    [JsonProperty("id")]
+    public int Id { get; set; }
+
+    [JsonProperty("guid")]
+    public Guid Guid { get; set; }
+}

+ 15 - 0
Moonlight/App/Models/Plesk/Resources/Database.cs

@@ -0,0 +1,15 @@
+using Newtonsoft.Json;
+
+namespace Moonlight.App.Models.Plesk.Resources;
+
+public class Database
+{
+    [JsonProperty("id")]
+    public int Id { get; set; }
+
+    [JsonProperty("name")]
+    public string Name { get; set; }
+
+    [JsonProperty("type")]
+    public string Type { get; set; }
+}

+ 30 - 0
Moonlight/App/Models/Plesk/Resources/DatabaseServer.cs

@@ -0,0 +1,30 @@
+using Newtonsoft.Json;
+
+namespace Moonlight.App.Models.Plesk.Resources;
+
+public class DatabaseServer
+{
+    [JsonProperty("id")]
+    public int Id { get; set; }
+
+    [JsonProperty("host")]
+    public string Host { get; set; }
+
+    [JsonProperty("port")]
+    public int Port { get; set; }
+
+    [JsonProperty("type")]
+    public string Type { get; set; }
+
+    [JsonProperty("status")]
+    public string Status { get; set; }
+
+    [JsonProperty("db_count")]
+    public int DbCount { get; set; }
+
+    [JsonProperty("is_default")]
+    public bool IsDefault { get; set; }
+
+    [JsonProperty("is_local")]
+    public bool IsLocal { get; set; }
+}

+ 15 - 0
Moonlight/App/Models/Plesk/Resources/DatabaseUser.cs

@@ -0,0 +1,15 @@
+using Newtonsoft.Json;
+
+namespace Moonlight.App.Models.Plesk.Resources;
+
+public class DatabaseUser
+{
+    [JsonProperty("id")]
+    public int Id { get; set; }
+
+    [JsonProperty("login")]
+    public string Login { get; set; }
+
+    [JsonProperty("database_id")]
+    public int DatabaseId { get; set; }
+}

+ 18 - 0
Moonlight/App/Models/Plesk/Resources/ServerStatus.cs

@@ -0,0 +1,18 @@
+using Newtonsoft.Json;
+
+namespace Moonlight.App.Models.Plesk.Resources;
+
+public class ServerStatus
+{
+    [JsonProperty("platform")]
+    public string Platform { get; set; }
+
+    [JsonProperty("hostname")]
+    public string Hostname { get; set; }
+
+    [JsonProperty("guid")]
+    public Guid Guid { get; set; }
+
+    [JsonProperty("panel_version")]
+    public string PanelVersion { get; set; }
+}

+ 44 - 0
Moonlight/App/Repositories/PleskServerRepository.cs

@@ -0,0 +1,44 @@
+using Microsoft.EntityFrameworkCore;
+using Moonlight.App.Database;
+using Moonlight.App.Database.Entities;
+
+namespace Moonlight.App.Repositories;
+
+public class PleskServerRepository : IDisposable
+{
+    private readonly DataContext DataContext;
+
+    public PleskServerRepository(DataContext dataContext)
+    {
+        DataContext = dataContext;
+    }
+
+    public DbSet<PleskServer> Get()
+    {
+        return DataContext.PleskServers;
+    }
+
+    public PleskServer Add(PleskServer pleskServer)
+    {
+        var x = DataContext.PleskServers.Add(pleskServer);
+        DataContext.SaveChanges();
+        return x.Entity;
+    }
+
+    public void Update(PleskServer pleskServer)
+    {
+        DataContext.PleskServers.Update(pleskServer);
+        DataContext.SaveChanges();
+    }
+    
+    public void Delete(PleskServer pleskServer)
+    {
+        DataContext.PleskServers.Remove(pleskServer);
+        DataContext.SaveChanges();
+    }
+
+    public void Dispose()
+    {
+        DataContext.Dispose();
+    }
+}

+ 44 - 0
Moonlight/App/Repositories/WebsiteRepository.cs

@@ -0,0 +1,44 @@
+using Microsoft.EntityFrameworkCore;
+using Moonlight.App.Database;
+using Moonlight.App.Database.Entities;
+
+namespace Moonlight.App.Repositories;
+
+public class WebsiteRepository : IDisposable
+{
+    private readonly DataContext DataContext;
+
+    public WebsiteRepository(DataContext dataContext)
+    {
+        DataContext = dataContext;
+    }
+
+    public DbSet<Website> Get()
+    {
+        return DataContext.Websites;
+    }
+
+    public Website Add(Website website)
+    {
+        var x = DataContext.Websites.Add(website);
+        DataContext.SaveChanges();
+        return x.Entity;
+    }
+
+    public void Update(Website website)
+    {
+        DataContext.Websites.Update(website);
+        DataContext.SaveChanges();
+    }
+    
+    public void Delete(Website website)
+    {
+        DataContext.Websites.Remove(website);
+        DataContext.SaveChanges();
+    }
+
+    public void Dispose()
+    {
+        DataContext.Dispose();
+    }
+}

+ 18 - 0
Moonlight/App/Services/NodeService.cs

@@ -41,4 +41,22 @@ public class NodeService
     {
         return await DaemonApiHelper.Get<ContainerStats>(node, "stats/container");
     }
+
+    public async Task<bool> IsHostUp(Node node)
+    {
+        try
+        {
+            //TODO: Implement status caching
+            var data = await GetStatus(node);
+
+            if (data != null)
+                return true;
+        }
+        catch (Exception)
+        {
+            // ignored
+        }
+
+        return false;
+    }
 }

+ 13 - 5
Moonlight/App/Services/ServerService.cs

@@ -29,6 +29,7 @@ public class ServerService
     private readonly SecurityLogService SecurityLogService;
     private readonly AuditLogService AuditLogService;
     private readonly ErrorLogService ErrorLogService;
+    private readonly NodeService NodeService;
 
     public ServerService(
         ServerRepository serverRepository,
@@ -42,7 +43,8 @@ public class ServerService
         WingsJwtHelper wingsJwtHelper,
         SecurityLogService securityLogService,
         AuditLogService auditLogService,
-        ErrorLogService errorLogService)
+        ErrorLogService errorLogService,
+        NodeService nodeService)
     {
         ServerRepository = serverRepository;
         WingsApiHelper = wingsApiHelper;
@@ -56,6 +58,7 @@ public class ServerService
         SecurityLogService = securityLogService;
         AuditLogService = auditLogService;
         ErrorLogService = errorLogService;
+        NodeService = nodeService;
     }
 
     private Server EnsureNodeData(Server s)
@@ -252,16 +255,14 @@ public class ServerService
         Node node;
 
         if (n == null)
-        {
-            node = NodeRepository.Get().Include(x => x.Allocations).First(); //TODO: Smart deploy
-        }
-        else
         {
             node = NodeRepository
                 .Get()
                 .Include(x => x.Allocations)
                 .First(x => x.Id == n.Id);
         }
+        else
+            node = n;
 
         NodeAllocation freeAllo;
 
@@ -395,4 +396,11 @@ public class ServerService
         
         ServerRepository.Delete(s);
     }
+
+    public async Task<bool> IsHostUp(Server s)
+    {
+        var server = EnsureNodeData(s);
+
+        return await NodeService.IsHostUp(server.Node);
+    }
 }

+ 383 - 0
Moonlight/App/Services/WebsiteService.cs

@@ -0,0 +1,383 @@
+using Logging.Net;
+using Microsoft.EntityFrameworkCore;
+using Moonlight.App.Database.Entities;
+using Moonlight.App.Exceptions;
+using Moonlight.App.Helpers;
+using Moonlight.App.Helpers.Files;
+using Moonlight.App.Models.Plesk.Requests;
+using Moonlight.App.Models.Plesk.Resources;
+using Moonlight.App.Repositories;
+using FileAccess = Moonlight.App.Helpers.Files.FileAccess;
+
+namespace Moonlight.App.Services;
+
+public class WebsiteService
+{
+    private readonly WebsiteRepository WebsiteRepository;
+    private readonly PleskServerRepository PleskServerRepository;
+    private readonly PleskApiHelper PleskApiHelper;
+    private readonly UserRepository UserRepository;
+
+    public WebsiteService(WebsiteRepository websiteRepository, PleskApiHelper pleskApiHelper, PleskServerRepository pleskServerRepository, UserRepository userRepository)
+    {
+        WebsiteRepository = websiteRepository;
+        PleskApiHelper = pleskApiHelper;
+        PleskServerRepository = pleskServerRepository;
+        UserRepository = userRepository;
+    }
+
+    public async Task<Website> Create(string baseDomain, User owner, PleskServer? ps = null)
+    {
+        if (WebsiteRepository.Get().Any(x => x.BaseDomain == baseDomain))
+            throw new DisplayException("A website with this domain does already exist");
+
+        var pleskServer = ps ?? PleskServerRepository.Get().First();
+
+        var ftpLogin = baseDomain;
+        var ftpPassword = StringHelper.GenerateString(16);
+        
+        var w = new Website()
+        {
+            PleskServer = pleskServer,
+            Owner = owner,
+            BaseDomain = baseDomain,
+            PleskId = 0,
+            FtpPassword = ftpPassword,
+            FtpLogin = ftpLogin
+        };
+
+        var website = WebsiteRepository.Add(w);
+
+        try
+        {
+            var id = await GetAdminAccount(pleskServer);
+
+            var result = await PleskApiHelper.Post<CreateResult>(pleskServer, "domains", new CreateDomain()
+            {
+                Description = $"moonlight website {website.Id}",
+                Name = baseDomain,
+                HostingType = "virtual",
+                Plan = new()
+                {
+                    Name = "Unlimited"
+                },
+                HostingSettings = new()
+                {
+                    FtpLogin = ftpLogin,
+                    FtpPassword = ftpPassword
+                },
+                OwnerClient = new()
+                {
+                    Id = id
+                }
+            });
+
+            website.PleskId = result.Id;
+            
+            WebsiteRepository.Update(website);
+        }
+        catch (Exception e)
+        {
+            WebsiteRepository.Delete(website);
+            throw;
+        }
+
+        return website;
+    }
+
+    public async Task Delete(Website w)
+    {
+        var website = EnsureData(w);
+        
+        await PleskApiHelper.Delete(website.PleskServer, $"domains/{w.PleskId}", null);
+        
+        WebsiteRepository.Delete(website);
+    }
+
+    public async Task<bool> IsHostUp(PleskServer pleskServer)
+    {
+        try
+        {
+            var res = await PleskApiHelper.Get<ServerStatus>(pleskServer, "server");
+
+            if (res != null)
+                return true;
+        }
+        catch (Exception e)
+        {
+            // ignored
+        }
+
+        return false;
+    }
+    
+    public async Task<bool> IsHostUp(Website w)
+    {
+        var website = EnsureData(w);
+        
+        try
+        {
+            var res = await PleskApiHelper.Get<ServerStatus>(website.PleskServer, "server");
+
+            if (res != null)
+                return true;
+        }
+        catch (Exception)
+        {
+            // ignored
+        }
+
+        return false;
+    }
+    
+    #region Get host
+    
+    public async Task<string> GetHost(PleskServer pleskServer)
+    {
+        return (await PleskApiHelper.Get<ServerStatus>(pleskServer, "server")).Hostname;
+    }
+    
+    public async Task<string> GetHost(Website w)
+    {
+        var website = EnsureData(w);
+
+        return await GetHost(website.PleskServer);
+    }
+    
+    #endregion
+
+    private async Task<int> GetAdminAccount(PleskServer pleskServer)
+    {
+        var users = await PleskApiHelper.Get<Client[]>(pleskServer, "clients");
+
+        var user = users.FirstOrDefault(x => x.Type == "admin");
+
+        if (user == null)
+            throw new DisplayException("No admin account in plesk found");
+
+        return user.Id;
+    }
+
+    #region SSL
+    public async Task<string[]> GetSslCertificates(Website w)
+    {
+        var website = EnsureData(w);
+        var certs = new List<string>();
+
+        var data = await ExecuteCli(website.PleskServer, "certificate", p =>
+        {
+            p.Add("-l");
+            p.Add("-domain");
+            p.Add(w.BaseDomain);
+        });
+
+        string[] lines = data.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries);
+        
+        foreach (string line in lines)
+        {
+            if (line.Contains("Lets Encrypt"))
+            {
+                string[] parts = line.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
+
+                if(parts.Length > 6)
+                    certs.Add($"{parts[4]} {parts[5]} {parts[6]}");
+            }
+            else if (line.Contains("Listing of SSL/TLS certificates repository was successful"))
+            {
+                // This line indicates the end of the certificate listing, so we can stop parsing
+                break;
+            }
+        }
+
+        return certs.ToArray();
+    }
+
+    public async Task CreateSslCertificate(Website w)
+    {
+        var website = EnsureData(w);
+
+        await ExecuteCli(website.PleskServer, "extension", p =>
+        {
+            p.Add("--exec");
+            p.Add("letsencrypt");
+            p.Add("cli.php");
+            p.Add("-d");
+            p.Add(website.BaseDomain);
+            p.Add("-m");
+            p.Add(website.Owner.Email);
+        });
+    }
+
+    public async Task DeleteSslCertificate(Website w, string name)
+    {
+        var website = EnsureData(w);
+
+        try
+        {
+            await ExecuteCli(website.PleskServer, "site", p =>
+            {
+                p.Add("-u");
+                p.Add(website.BaseDomain);
+                p.Add("-ssl");
+                p.Add("false");
+            });
+
+            try
+            {
+                await ExecuteCli(website.PleskServer, "certificate", p =>
+                {
+                    p.Add("--remove");
+                    p.Add(name);
+                    p.Add("-domain");
+                    p.Add(website.BaseDomain);
+                });
+            }
+            catch (Exception e)
+            {
+                Logger.Warn("Error removing ssl certificate");
+                Logger.Warn(e);
+
+                throw new DisplayException("An unknown error occured while removing ssl certificate");
+            }
+        }
+        catch (DisplayException)
+        {
+            // Redirect all display exception to soft error handler
+            throw;
+        }
+        catch (Exception e)
+        {
+            Logger.Warn("Error disabling ssl certificate");
+            Logger.Warn(e);
+
+            throw new DisplayException("An unknown error occured while disabling ssl certificate");
+        }
+    }
+    
+    #endregion
+
+    #region Databases
+
+    public async Task<Models.Plesk.Resources.Database[]> GetDatabases(Website w)
+    {
+        var website = EnsureData(w);
+
+        var dbs = await PleskApiHelper.Get<Models.Plesk.Resources.Database[]>(
+            website.PleskServer, 
+            $"databases?domain={w.BaseDomain}"
+        );
+
+        return dbs;
+    }
+
+    public async Task CreateDatabase(Website w, string name, string password)
+    {
+        var website = EnsureData(w);
+
+        var server = await GetDefaultDatabaseServer(website);
+
+        if (server == null)
+            throw new DisplayException("No database server marked as default found");
+
+        var dbReq = new CreateDatabase()
+        {
+            Name = name,
+            Type = "mysql",
+            ParentDomain = new()
+            {
+                Name = website.BaseDomain
+            },
+            ServerId = server.Id
+        };
+
+        var db = await PleskApiHelper.Post<Models.Plesk.Resources.Database>(website.PleskServer, "databases", dbReq);
+
+        if (db == null)
+            throw new DisplayException("Unable to create database via api");
+
+        var dbUserReq = new CreateDatabaseUser()
+        {
+            DatabaseId = db.Id,
+            Login = name,
+            Password = password
+        };
+
+        await PleskApiHelper.Post(website.PleskServer, "dbusers", dbUserReq);
+    }
+
+    public async Task DeleteDatabase(Website w, Models.Plesk.Resources.Database database)
+    {
+        var website = EnsureData(w);
+
+        var dbUsers = await PleskApiHelper.Get<DatabaseUser[]>(
+            website.PleskServer,
+            $"dbusers?dbId={database.Id}"
+        );
+
+        foreach (var dbUser in dbUsers)
+        {
+            await PleskApiHelper.Delete(website.PleskServer, $"dbusers/{dbUser.Id}", null);
+        }
+
+        await PleskApiHelper.Delete(website.PleskServer, $"databases/{database.Id}", null);
+    }
+
+    public async Task<DatabaseServer?> GetDefaultDatabaseServer(PleskServer pleskServer)
+    {
+        var dbServers = await PleskApiHelper.Get<DatabaseServer[]>(pleskServer, "dbservers");
+
+        return dbServers.FirstOrDefault(x => x.IsDefault);
+    }
+
+    public async Task<DatabaseServer?> GetDefaultDatabaseServer(Website w)
+    {
+        var website = EnsureData(w);
+
+        return await GetDefaultDatabaseServer(website.PleskServer);
+    }
+
+    #endregion
+
+    public async Task<FileAccess> CreateFileAccess(Website w)
+    {
+        var website = EnsureData(w);
+        var host = await GetHost(website.PleskServer);
+
+        return new FtpFileAccess(host, 21, website.FtpLogin, website.FtpPassword);
+    }
+
+    private async Task<string> ExecuteCli(
+        PleskServer server,
+        string cli, Action<List<string>>? parameters = null,
+        Action<Dictionary<string, string>>? variables = null
+    )
+    {
+        var p = new List<string>();
+        var v = new Dictionary<string, string>();
+        
+        parameters?.Invoke(p);
+        variables?.Invoke(v);
+
+        var req = new CliCall()
+        {
+            Env = v,
+            Params = p
+        };
+
+        var res = await PleskApiHelper.Post<CliResult>(server, $"cli/{cli}/call", req);
+
+        return res.Stdout;
+    }
+
+    private Website EnsureData(Website website)
+    {
+        if (website.PleskServer == null || website.Owner == null)
+            return WebsiteRepository
+                .Get()
+                .Include(x => x.PleskServer)
+                .Include(x => x.Owner)
+                .First(x => x.Id == website.Id);
+        
+        return website;
+    }
+}

+ 5 - 1
Moonlight/Program.cs

@@ -60,8 +60,10 @@ namespace Moonlight
             builder.Services.AddScoped<NotificationRepository>();
             builder.Services.AddScoped<DdosAttackRepository>();
             builder.Services.AddScoped<SubscriptionRepository>();
+            builder.Services.AddScoped<PleskServerRepository>();
+            builder.Services.AddScoped<WebsiteRepository>();
             builder.Services.AddScoped<LoadingMessageRepository>();
-            
+
             builder.Services.AddScoped<AuditLogEntryRepository>();
             builder.Services.AddScoped<ErrorLogEntryRepository>();
             builder.Services.AddScoped<SecurityLogEntryRepository>();
@@ -90,6 +92,7 @@ namespace Moonlight
             builder.Services.AddScoped<NotificationClientService>();
             builder.Services.AddScoped<ModalService>();
             builder.Services.AddScoped<SmartDeployService>();
+            builder.Services.AddScoped<WebsiteService>();
             
             builder.Services.AddScoped<GoogleOAuth2Service>();
             builder.Services.AddScoped<DiscordOAuth2Service>();
@@ -121,6 +124,7 @@ namespace Moonlight
             builder.Services.AddSingleton<PaperApiHelper>();
             builder.Services.AddSingleton<HostSystemHelper>();
             builder.Services.AddScoped<DaemonApiHelper>();
+            builder.Services.AddScoped<PleskApiHelper>();
             
             // Background services
             builder.Services.AddSingleton<DiscordBotService>();

+ 11 - 0
Moonlight/Shared/Components/ErrorBoundaries/SoftErrorBoundary.razor

@@ -36,6 +36,17 @@
                 wingsException.Message
                 );
         }
+        else if (exception is PleskException pleskException)
+        {
+            await AlertService.Error(
+                SmartTranslateService.Translate("Error from plesk"),
+                pleskException.Message
+                );
+        }
+        else if (exception is NotImplementedException)
+        {
+            await AlertService.Error(SmartTranslateService.Translate("This function is not implemented"));
+        }
         else
         {
             throw exception;

+ 2 - 2
Moonlight/Shared/Components/FileManagerPartials/FileManager.razor

@@ -166,8 +166,8 @@ else
                     {
                         var stream = await Access.DownloadStream(x);
                         await ToastService.Info(SmartTranslateService.Translate("Starting download"));
-                        await FileService.AddBuffer(stream);
-                        await FileService.DownloadBinaryBuffers(x.Name);
+                        stream.Position = 0;
+                        await FileService.DownloadFile(fileName: x.Name, stream: stream, contentType: "application/octet-stream");
                     }
                     catch (NotImplementedException)
                     {

+ 1 - 6
Moonlight/Shared/Components/Forms/DeleteButton.razor

@@ -35,12 +35,7 @@ else
         {
             if (Confirm)
             {
-                var b = await AlertService.YesNo(
-                    SmartTranslateService.Translate("Are you sure?"),
-                    SmartTranslateService.Translate("Do you really want to delete it?"),
-                    SmartTranslateService.Translate("Yes"),
-                    SmartTranslateService.Translate("No")
-                    );
+                var b = await AlertService.ConfirmMath();
 
                 if (b)
                 {

+ 22 - 0
Moonlight/Shared/Components/Navigations/AdminWebsitesNavigation.razor

@@ -0,0 +1,22 @@
+<div class="card mb-5 mb-xl-10">
+    <div class="card-body pt-0 pb-0">
+        <ul class="nav nav-stretch nav-line-tabs nav-line-tabs-2x border-transparent fs-5 fw-bold">
+            <li class="nav-item mt-2">
+                <a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 0 ? "active" : "")" href="/admin/websites">
+                    <TL>Websites</TL>
+                </a>
+            </li>
+            <li class="nav-item mt-2">
+                <a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 1 ? "active" : "")" href="/admin/websites/servers">
+                    <TL>Plesk servers</TL>
+                </a>
+            </li>
+        </ul>
+    </div>
+</div>
+
+@code
+{
+    [Parameter]
+    public int Index { get; set; } = 0;
+}

+ 5 - 24
Moonlight/Shared/Components/Partials/SidebarMenu.razor

@@ -155,32 +155,13 @@ else
                 </div>
             </div>
         </div>
-        <div data-kt-menu-trigger="click" class="menu-item menu-accordion">
-            <span class="menu-link">
+        <div class="menu-item">
+            <a class="menu-link" href="/admin/websites">
                 <span class="menu-icon">
-                    <i class="bx bx-cube"></i>
+                    <i class="bx bx-globe"></i>
                 </span>
-                <span class="menu-title"><TL>aaPanel</TL></span>
-                <span class="menu-arrow"></span>
-            </span>
-            <div class="menu-sub menu-sub-accordion">
-                <div class="menu-item">
-                    <a class="menu-link" href="/admin/aapanel/">
-                        <span class="menu-bullet">
-                            <span class="bullet bullet-dot"></span>
-                        </span>
-                        <span class="menu-title"><TL>Overview</TL></span>
-                    </a>
-                </div>
-                <div class="menu-item">
-                    <a class="menu-link" href="/admin/aapanel/databases">
-                        <span class="menu-bullet">
-                            <span class="bullet bullet-dot"></span>
-                        </span>
-                        <span class="menu-title"><TL>Databases</TL></span>
-                    </a>
-                </div>
-            </div>
+                <span class="menu-title"><TL>Websites</TL></span>
+            </a>
         </div>
         <div class="menu-item">
             <a class="menu-link" href="/admin/users">

+ 111 - 0
Moonlight/Shared/Components/WebsiteControl/WebsiteDashboard.razor

@@ -0,0 +1,111 @@
+@using Moonlight.App.Database.Entities
+@using Moonlight.App.Services
+
+@inject WebsiteService WebsiteService
+@inject SmartTranslateService SmartTranslateService
+
+<div class="row gy-5 g-xl-10">
+    <div class="col-xl-4 mb-xl-10">
+        <div class="card h-md-100">
+            <div class="card-body d-flex flex-column flex-center">
+                <img class="img-fluid" src="https://image.thum.io/get/http://@(CurrentWebsite.BaseDomain)" alt="Website screenshot"/>
+            </div>
+        </div>
+    </div>
+    <div class="col-xl-8 mb-5 mb-xl-10">
+        <div class="card card-flush h-xl-100">
+            <div class="card-body pt-2">
+                <LazyLoader @ref="LazyLoader" Load="Load">
+                    <div class="row mt-5">
+                        <div class="card border">
+                            <div class="card-header">
+                                <span class="card-title">
+                                    <TL>SSL certificates</TL>
+                                </span>
+                                <div class="card-toolbar">
+                                    <WButton Text="@(SmartTranslateService.Translate("Issue certificate"))"
+                                             WorkingText="@(SmartTranslateService.Translate("Working"))"
+                                             CssClasses="btn-success"
+                                             OnClick="CreateCertificate">
+                                    </WButton>
+                                </div>
+                            </div>
+                            <div class="card-body">
+                                @if (Certs.Any())
+                                {
+                                    <table class="table align-middle gs-0 gy-3">
+                                        <thead>
+                                        <tr>
+                                            <th class="p-0 w-50px"></th>
+                                            <th class="p-0 min-w-150px"></th>
+                                            <th class="p-0 min-w-120px"></th>
+                                        </tr>
+                                        </thead>
+                                        <tbody>
+                                        @foreach (var cert in Certs)
+                                        {
+                                            <tr>
+                                                <td>
+                                                    <div class="symbol symbol-50px me-2">
+                                                        <span class="symbol-label">
+                                                            <i class="bx bx-md bx-receipt text-dark"></i>
+                                                        </span>
+                                                    </div>
+                                                </td>
+                                                <td>
+                                                    <span class="text-dark fw-bold fs-6">@(cert)</span>
+                                                </td>
+                                                <td class="text-end">
+                                                    <WButton Text="@(SmartTranslateService.Translate("Delete"))"
+                                                             WorkingText="@(SmartTranslateService.Translate("Working"))"
+                                                             CssClasses="btn btn-danger"
+                                                             OnClick="() => DeleteCertificate(cert)">
+                                                    </WButton>
+                                                </td>
+                                            </tr>
+                                        }
+                                        </tbody>
+                                    </table>
+                                }
+                                else
+                                {
+                                    <div class="alert alert-warning">
+                                        <TL>No SSL certificates found</TL>
+                                    </div>
+                                }
+                            </div>
+                        </div>
+                    </div>
+                </LazyLoader>
+            </div>
+        </div>
+    </div>
+</div>
+
+@code
+{
+    [CascadingParameter]
+    public Website CurrentWebsite { get; set; }
+
+    private string[] Certs;
+
+    private LazyLoader LazyLoader;
+
+    private async Task Load(LazyLoader lazyLoader)
+    {
+        await lazyLoader.SetText("Loading certificates");
+        Certs = await WebsiteService.GetSslCertificates(CurrentWebsite);
+    }
+
+    private async Task CreateCertificate()
+    {
+        await WebsiteService.CreateSslCertificate(CurrentWebsite);
+        await LazyLoader.Reload();
+    }
+
+    private async Task DeleteCertificate(string name)
+    {
+        await WebsiteService.DeleteSslCertificate(CurrentWebsite, name);
+        await LazyLoader.Reload();
+    }
+}

+ 135 - 0
Moonlight/Shared/Components/WebsiteControl/WebsiteDatabases.razor

@@ -0,0 +1,135 @@
+@using Moonlight.App.Database.Entities
+@using Moonlight.App.Models.Forms
+@using Moonlight.App.Models.Plesk.Resources
+@using Moonlight.App.Services
+
+@inject SmartTranslateService SmartTranslateService
+@inject WebsiteService WebsiteService
+
+<div class="card card-flush h-xl-100">
+    <LazyLoader @ref="LazyLoader" Load="Load">
+        <div class="card-header">
+            <span class="card-toolbar">
+                <div class="mt-4">
+                    <SmartForm Model="Model" OnValidSubmit="OnValidSubmit">
+                        <div class="input-group">
+                            <InputText @bind-Value="Model.Name" type="text" class="form-control" placeholder="@(SmartTranslateService.Translate("Name"))"></InputText>
+                            <InputText @bind-Value="Model.Password" type="password" class="form-control" placeholder="@(SmartTranslateService.Translate("Password"))"></InputText>
+                            <button class="btn btn-primary" type="submit">
+                                <TL>Create</TL>
+                            </button>
+                        </div>
+                    </SmartForm>
+                </div>
+            </span>
+        </div>
+        <div class="card-body pt-2">
+            @if (Databases.Any())
+            {
+                <div class="accordion" id="databases">
+                    @foreach (var database in Databases)
+                    {
+                        <div class="accordion-item">
+                            <h2 class="accordion-header" id="databases_header_@(database.Id)">
+                                <button class="accordion-button fs-4 fw-semibold" type="button" data-bs-toggle="collapse" data-bs-target="#databases_body_@(database.Id)">
+                                    @(database.Name) - @(database.Type.ToUpper())
+                                </button>
+                            </h2>
+                            <div id="databases_body_@(database.Id)" class="accordion-collapse collapse" data-bs-parent="#databases">
+                                <div class="accordion-body">
+                                    <div class="mt-7 row fv-row mb-7">
+                                        <div class="col-md-3 text-md-start">
+                                            <label class="fs-6 fw-semibold form-label mt-3">
+                                                <TL>Host</TL>
+                                            </label>
+                                        </div>
+                                        <div class="col-md-9">
+                                            <input type="text" class="form-control form-control-solid disabled" disabled="disabled" value="@(Host)">
+                                        </div>
+                                    </div>
+                                    <div class="mt-7 row fv-row mb-7">
+                                        <div class="col-md-3 text-md-start">
+                                            <label class="fs-6 fw-semibold form-label mt-3">
+                                                <TL>Port</TL>
+                                            </label>
+                                        </div>
+                                        <div class="col-md-9">
+                                            <input type="text" class="form-control form-control-solid disabled" disabled="disabled" value="@(DatabaseServer.Port)">
+                                        </div>
+                                    </div>
+                                    <div class="mt-7 row fv-row mb-7">
+                                        <div class="col-md-3 text-md-start">
+                                            <label class="fs-6 fw-semibold form-label mt-3">
+                                                <TL>Username</TL>
+                                            </label>
+                                        </div>
+                                        <div class="col-md-9">
+                                            <input type="text" class="form-control form-control-solid disabled" disabled="disabled" value="@(database.Name)">
+                                        </div>
+                                    </div>
+                                    <div class="mt-7 row fv-row mb-7">
+                                        <div class="col-md-3 text-md-start">
+                                            <label class="fs-6 fw-semibold form-label mt-3">
+                                                <TL>Database</TL>
+                                            </label>
+                                        </div>
+                                        <div class="col-md-9">
+                                            <input type="text" class="form-control form-control-solid disabled" disabled="disabled" value="@(database.Name)">
+                                        </div>
+                                    </div>
+                                    <div class="text-end">
+                                        <DeleteButton Confirm="true" OnClick="() => DeleteDatabase(database)" />
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    }
+                </div>
+            }
+            else
+            {
+                <div class="alert alert-warning">
+                    <TL>No databases found for this website</TL>
+                </div>
+            }
+        </div>
+    </LazyLoader>
+
+</div>
+
+@code
+{
+    [CascadingParameter]
+    public Website CurrentWebsite { get; set; }
+
+    private LazyLoader LazyLoader;
+    private Database[] Databases;
+    private DatabaseServer DatabaseServer;
+    private string Host;
+
+    private DatabaseDataModel Model = new();
+
+    private async Task Load(LazyLoader arg)
+    {
+        Databases = await WebsiteService.GetDatabases(CurrentWebsite);
+
+        if (Databases.Any())
+        {
+            DatabaseServer = (await WebsiteService.GetDefaultDatabaseServer(CurrentWebsite))!;
+            Host = await WebsiteService.GetHost(CurrentWebsite);
+        }
+    }
+
+    private async Task OnValidSubmit()
+    {
+        await WebsiteService.CreateDatabase(CurrentWebsite, Model.Name, Model.Password);
+        Model = new();
+        await LazyLoader.Reload();
+    }
+
+    private async Task DeleteDatabase(Database database)
+    {
+        await WebsiteService.DeleteDatabase(CurrentWebsite, database);
+        await LazyLoader.Reload();
+    }
+}

+ 24 - 0
Moonlight/Shared/Components/WebsiteControl/WebsiteFiles.razor

@@ -0,0 +1,24 @@
+@using Moonlight.App.Database.Entities
+@using Moonlight.App.Helpers.Files
+@using Moonlight.App.Services
+@using Moonlight.Shared.Components.FileManagerPartials
+
+@inject WebsiteService WebsiteService
+
+<LazyLoader Load="Load">
+    <FileManager Access="Access">
+    </FileManager>
+</LazyLoader>
+
+@code
+{
+    [CascadingParameter]
+    public Website CurrentWebsite { get; set; }
+
+    private FileAccess Access;
+
+    private async Task Load(LazyLoader arg)
+    {
+        Access = await WebsiteService.CreateFileAccess(CurrentWebsite);
+    }
+}

+ 64 - 0
Moonlight/Shared/Components/WebsiteControl/WebsiteFtp.razor

@@ -0,0 +1,64 @@
+@using Moonlight.App.Database.Entities
+@using Moonlight.App.Services
+
+@inject WebsiteService WebsiteService
+
+<div class="card card-flush h-xl-100">
+    <div class="card-body pt-2">
+        <LazyLoader Load="Load">
+            <div class="mt-7 row fv-row mb-7">
+                <div class="col-md-3 text-md-start">
+                    <label class="fs-6 fw-semibold form-label mt-3">
+                        <TL>Ftp Host</TL>
+                    </label>
+                </div>
+                <div class="col-md-9">
+                    <input type="text" class="form-control form-control-solid disabled" disabled="disabled" value="@(FtpHost)">
+                </div>
+            </div>
+            <div class="row fv-row mb-7">
+                <div class="col-md-3 text-md-start">
+                    <label class="fs-6 fw-semibold form-label mt-3">
+                        <TL>Ftp Port</TL>
+                    </label>
+                </div>
+                <div class="col-md-9">
+                    <input type="text" class="form-control form-control-solid disabled" disabled="disabled" value="21">
+                </div>
+            </div>
+            <div class="row fv-row mb-7">
+                <div class="col-md-3 text-md-start">
+                    <label class="fs-6 fw-semibold form-label mt-3">
+                        <TL>Ftp Username</TL>
+                    </label>
+                </div>
+                <div class="col-md-9">
+                    <input type="text" class="form-control form-control-solid disabled" disabled="disabled" value="@(Website.FtpLogin)">
+                </div>
+            </div>
+            <div class="row fv-row mb-7">
+                <div class="col-md-3 text-md-start">
+                    <label class="fs-6 fw-semibold form-label mt-3">
+                        <TL>Ftp Password</TL>
+                    </label>
+                </div>
+                <div class="col-md-9">
+                    <input type="text" class="form-control form-control-solid disabled blur-unless-hover" disabled="disabled" value="@(Website.FtpPassword)">
+                </div>
+            </div>
+        </LazyLoader>
+    </div>
+</div>
+
+@code
+{
+    [CascadingParameter]
+    public Website Website { get; set; }
+
+    private string FtpHost = "N/A";
+
+    private async Task Load(LazyLoader arg)
+    {
+        FtpHost = await WebsiteService.GetHost(Website.PleskServer);
+    }
+}

+ 57 - 0
Moonlight/Shared/Components/WebsiteControl/WebsiteNavigation.razor

@@ -0,0 +1,57 @@
+@using Moonlight.App.Database.Entities
+
+<div class="card card-body">
+    <div class="row">
+        <div class="col-8">
+            <div class="d-flex align-items-center">
+                <div class="symbol symbol-circle me-5">
+                    <div class="symbol-label bg-transparent text-primary border border-secondary border-dashed">
+                        <i class="bx bx-globe bx-md"></i>
+                    </div>
+                </div>
+                <div class="d-flex flex-column">
+                    <div class="mb-1 fs-4">@(Website.BaseDomain)</div>
+                    <div class="text-muted fs-5">@(Website.PleskServer.Name)</div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<div class="row">
+    <div class="separator my-5"></div>
+</div>
+<div class="card mb-5 mb-xl-10">
+    <div class="card-body pt-0 pb-0">
+        <ul class="nav nav-stretch nav-line-tabs nav-line-tabs-2x border-transparent fs-5 fw-bold">
+            <li class="nav-item mt-2">
+                <a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 0 ? "active" : "")" href="/website/@(Website.Id)">
+                    <TL>Dashboard</TL>
+                </a>
+            </li>
+            <li class="nav-item mt-2">
+                <a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 1 ? "active" : "")" href="/website/@(Website.Id)/files">
+                    <TL>Files</TL>
+                </a>
+            </li>
+            <li class="nav-item mt-2">
+                <a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 2 ? "active" : "")" href="/website/@(Website.Id)/ftp">
+                    <TL>Ftp</TL>
+                </a>
+            </li>
+            <li class="nav-item mt-2">
+                <a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 3 ? "active" : "")" href="/website/@(Website.Id)/databases">
+                    <TL>Databases</TL>
+                </a>
+            </li>
+        </ul>
+    </div>
+</div>
+
+@code
+{
+    [Parameter]
+    public int Index { get; set; }
+
+    [Parameter]
+    public Website Website { get; set; }
+}

+ 0 - 2
Moonlight/Shared/Views/Admin/Servers/New.razor

@@ -1,12 +1,10 @@
 @page "/admin/servers/new"
 @using Moonlight.App.Repositories
-@using Moonlight.App.Repositories.Servers
 @using Moonlight.App.Database.Entities
 @using Moonlight.App.Services
 @using Microsoft.EntityFrameworkCore
 @using Moonlight.App.Exceptions
 @using Moonlight.App.Services.Interop
-@using Moonlight.App.Services.Sessions
 @using Logging.Net
 @using Blazored.Typeahead
 

+ 93 - 0
Moonlight/Shared/Views/Admin/Websites/Index.razor

@@ -0,0 +1,93 @@
+@page "/admin/websites/"
+
+@using Moonlight.Shared.Components.Navigations
+@using Moonlight.App.Services
+@using Moonlight.App.Database.Entities
+@using Moonlight.App.Repositories
+@using Microsoft.EntityFrameworkCore
+@using BlazorTable
+
+@inject SmartTranslateService SmartTranslateService
+@inject WebsiteRepository WebsiteRepository
+@inject WebsiteService WebsiteService
+
+<OnlyAdmin>
+    <AdminWebsitesNavigation Index="0"/>
+
+    <div class="card">
+        <div class="card-header border-0 pt-5">
+            <h3 class="card-title align-items-start flex-column">
+                <span class="card-label fw-bold fs-3 mb-1">
+                    <TL>Websites</TL>
+                </span>
+            </h3>
+            <div class="card-toolbar">
+                <a href="/admin/websites/new" class="btn btn-sm btn-light-success">
+                    <i class="bx bx-user-plus"></i>
+                    <TL>New website</TL>
+                </a>
+            </div>
+        </div>
+        <div class="card-body">
+            <LazyLoader @ref="LazyLoader" Load="Load">
+                <div class="table-responsive">
+                    <Table TableItem="Website" Items="Websites" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
+                        <Column TableItem="Website" Title="@(SmartTranslateService.Translate("Id"))" Field="@(x => x.Id)" Sortable="true" Filterable="true"/>
+                        <Column TableItem="Website" Title="@(SmartTranslateService.Translate("Base domain"))" Field="@(x => x.BaseDomain)" Sortable="true" Filterable="true">
+                            <Template>
+                                <a href="/website/@(context.Id)/">
+                                    @(context.BaseDomain)
+                                </a>
+                            </Template>
+                        </Column>
+                        <Column TableItem="Website" Title="@(SmartTranslateService.Translate("Owner"))" Field="@(x => x.Id)" Sortable="false" Filterable="false">
+                            <Template>
+                                <a href="/admin/users/view/@(context.Owner.Id)">
+                                    @(context.Owner.Email)
+                                </a>
+                            </Template>
+                        </Column>
+                        <Column TableItem="Website" Title="@(SmartTranslateService.Translate("Plesk server"))" Field="@(x => x.PleskServer.Id)" Sortable="true" Filterable="true">
+                            <Template>
+                                <a href="/admin/websites/servers/edit/@(context.PleskServer.Id)/">
+                                    @(context.PleskServer.Name)
+                                </a>
+                            </Template>
+                        </Column>
+                        <Column TableItem="Website" Title="@(SmartTranslateService.Translate("Manage"))" Field="@(x => x.Id)" Sortable="false" Filterable="false">
+                            <Template>
+                                <DeleteButton Confirm="true" OnClick="() => Delete(context)">
+                                </DeleteButton>
+                            </Template>
+                        </Column>
+                        <Pager ShowPageNumber="true" ShowTotalCount="true"/>
+                    </Table>
+                </div>
+            </LazyLoader>
+        </div>
+    </div>
+</OnlyAdmin>
+
+@code
+{
+    private LazyLoader LazyLoader;
+
+    private Website[] Websites;
+
+    private Task Load(LazyLoader lazyLoader)
+    {
+        Websites = WebsiteRepository
+            .Get()
+            .Include(x => x.Owner)
+            .Include(x => x.PleskServer)
+            .ToArray();
+
+        return Task.CompletedTask;
+    }
+
+    private async Task Delete(Website website)
+    {
+        await WebsiteService.Delete(website);
+        await LazyLoader.Reload();
+    }
+}

+ 79 - 0
Moonlight/Shared/Views/Admin/Websites/New.razor

@@ -0,0 +1,79 @@
+@page "/admin/websites/new"
+
+@using Moonlight.App.Models.Forms
+@using Moonlight.App.Services
+@using Blazored.Typeahead
+@using Moonlight.App.Database.Entities
+@using Moonlight.App.Repositories
+
+@inject WebsiteService WebsiteService
+@inject UserRepository UserRepository
+@inject NavigationManager NavigationManager
+
+<OnlyAdmin>
+    <div class="card card-body p-10">
+        <LazyLoader Load="Load">
+            <SmartForm Model="Model" OnValidSubmit="OnValidSubmit">
+                <label class="form-label">
+                    <TL>Base domain</TL>
+                </label>
+                <div class="input-group mb-5">
+                    <InputText @bind-Value="Model.BaseDomain" class="form-control"></InputText>
+                </div>
+                <label class="form-label">
+                    <TL>Owner</TL>
+                </label>
+                <div class="input-group mb-5">
+                    <BlazoredTypeahead SearchMethod="SearchUsers"
+                                       @bind-Value="Model.User">
+                        <SelectedTemplate>
+                            @(context.Email)
+                        </SelectedTemplate>
+                        <ResultTemplate>
+                            @(context.Email)
+                        </ResultTemplate>
+                    </BlazoredTypeahead>
+                </div>
+                <div>
+                    <button type="submit" class="btn btn-primary float-end">
+                        <TL>Create</TL>
+                    </button>
+                </div>
+            </SmartForm>
+        </LazyLoader>
+    </div>
+</OnlyAdmin>
+
+@code
+{
+    private WebsiteAdminDataModel Model = new();
+    private User[] Users;
+
+    private async Task OnValidSubmit()
+    {
+        await WebsiteService.Create(Model.BaseDomain, Model.User);
+        
+        NavigationManager.NavigateTo("/admin/websites");
+    }
+
+    private Task Load(LazyLoader arg)
+    {
+        Users = UserRepository
+            .Get()
+            .ToArray();
+
+        return Task.CompletedTask;
+    }
+
+    private Task<IEnumerable<User>> SearchUsers(string input)
+    {
+        if (string.IsNullOrEmpty(input))
+        {
+            return Task.FromResult(Array.Empty<User>().Cast<User>());
+        }
+        else
+        {
+            return Task.FromResult(Users.Where(x => x.Email.ToLower().StartsWith(input)));
+        }
+    }
+}

+ 98 - 0
Moonlight/Shared/Views/Admin/Websites/Servers/Edit.razor

@@ -0,0 +1,98 @@
+@page "/admin/websites/servers/edit/{Id:int}"
+
+@using Moonlight.App.Models.Forms
+@using Moonlight.App.Repositories
+@using Moonlight.App.Database.Entities
+
+@inject PleskServerRepository PleskServerRepository
+@inject NavigationManager NavigationManager
+
+<OnlyAdmin>
+    <LazyLoader Load="Load">
+        @if (PleskServer == null)
+            {
+                <div class="d-flex justify-content-center flex-center">
+                    <div class="card">
+                        <img src="/assets/media/svg/nodata.svg" class="card-img-top w-50 mx-auto pt-5" alt="Not found image"/>
+                        <div class="card-body text-center">
+                            <h1 class="card-title">
+                                <TL>Plesk server not found</TL>
+                            </h1>
+                            <p class="card-text fs-4">
+                                <TL>A plesk server with that id cannot be found</TL>
+                            </p>
+                        </div>
+                    </div>
+                </div>
+            }
+            else
+            {
+                <div class="card card-body p-10">
+                    <SmartForm Model="Model" OnValidSubmit="OnValidSubmit">
+                        <label class="form-label">
+                            <TL>Name</TL>
+                        </label>
+                        <div class="input-group mb-5">
+                            <InputText @bind-Value="Model.Name" class="form-control"></InputText>
+                        </div>
+                        <label class="form-label">
+                            <TL>Api Url</TL>
+                        </label>
+                        <div class="input-group mb-5">
+                            <InputText @bind-Value="Model.ApiUrl" class="form-control"></InputText>
+                        </div>
+                        <label class="form-label">
+                            <TL>Api Key</TL>
+                        </label>
+                        <div class="input-group mb-5">
+                            <InputText @bind-Value="Model.ApiKey" class="blur-unless-hover form-control"></InputText>
+                        </div>
+                        <div>
+                            <button type="submit" class="btn btn-primary float-end">
+                                <TL>Save</TL>
+                            </button>
+                        </div>
+                    </SmartForm>
+                </div>
+            }
+    </LazyLoader>
+</OnlyAdmin>
+
+@code
+{
+    [Parameter]
+    public int Id { get; set; }
+
+    private PleskServer? PleskServer;
+
+    private PleskServerDataModel Model = new();
+
+    private Task OnValidSubmit()
+    {
+        PleskServer!.Name = Model.Name;
+        PleskServer.ApiUrl = Model.ApiUrl;
+        PleskServer.ApiKey = Model.ApiKey;
+        
+        PleskServerRepository.Update(PleskServer);
+        
+        NavigationManager.NavigateTo("/admin/websites/servers");
+        
+        return Task.CompletedTask;
+    }
+
+    private Task Load(LazyLoader arg)
+    {
+        PleskServer = PleskServerRepository
+            .Get()
+            .FirstOrDefault(x => x.Id == Id);
+
+        if (PleskServer != null)
+        {
+            Model.Name = PleskServer.Name;
+            Model.ApiUrl = PleskServer.ApiUrl;
+            Model.ApiKey = PleskServer.ApiKey;
+        }
+        
+        return Task.CompletedTask;
+    }
+}

+ 115 - 0
Moonlight/Shared/Views/Admin/Websites/Servers/Index.razor

@@ -0,0 +1,115 @@
+@page "/admin/websites/servers"
+
+@using Moonlight.Shared.Components.Navigations
+@using Moonlight.App.Services
+@using Moonlight.App.Repositories
+@using Moonlight.App.Database.Entities
+@using BlazorTable
+
+@inject SmartTranslateService SmartTranslateService
+@inject PleskServerRepository PleskServerRepository
+@inject WebsiteService WebsiteService
+
+<OnlyAdmin>
+    <AdminWebsitesNavigation Index="1"/>
+
+    <div class="card">
+        <div class="card-header border-0 pt-5">
+            <h3 class="card-title align-items-start flex-column">
+                <span class="card-label fw-bold fs-3 mb-1">
+                    <TL>Plesk servers</TL>
+                </span>
+            </h3>
+            <div class="card-toolbar">
+                <a href="/admin/websites/servers/new" class="btn btn-sm btn-light-success">
+                    <i class="bx bx-user-plus"></i>
+                    <TL>New plesk server</TL>
+                </a>
+            </div>
+        </div>
+        <div class="card-body">
+            <LazyLoader @ref="LazyLoader" Load="Load">
+                <div class="table-responsive">
+                    <Table TableItem="PleskServer" Items="PleskServers" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
+                        <Column TableItem="PleskServer" Title="@(SmartTranslateService.Translate("Id"))" Field="@(x => x.Id)" Sortable="true" Filterable="true"/>
+                        <Column TableItem="PleskServer" Title="@(SmartTranslateService.Translate("Name"))" Field="@(x => x.Name)" Sortable="true" Filterable="true"/>
+                        <Column TableItem="PleskServer" Title="@(SmartTranslateService.Translate("Api url"))" Field="@(x => x.ApiUrl)" Sortable="true" Filterable="true"/>
+                        <Column TableItem="PleskServer" Title="@(SmartTranslateService.Translate("Status"))" Field="@(x => x.Id)" Sortable="false" Filterable="false">
+                            <Template>
+                                @if (OnlineCache.ContainsKey(context))
+                                {
+                                    if (OnlineCache[context])
+                                    {
+                                        <span class="text-success">
+                                            <TL>Online</TL>
+                                        </span>
+                                    }
+                                    else
+                                    {
+                                        <span class="text-danger">
+                                            <TL>Offline</TL>
+                                        </span>
+                                    }
+                                }
+                                else
+                                {
+                                    <span class="text-muted">
+                                        <TL>Loading</TL>
+                                    </span>
+                                }
+                            </Template>
+                        </Column>
+                        <Column TableItem="PleskServer" Title="@(SmartTranslateService.Translate("Edit"))" Field="@(x => x.Id)" Sortable="false" Filterable="false">
+                            <Template>
+                                <a href="/admin/websites/servers/edit/@(context.Id)/">
+                                    <TL>Manage</TL>
+                                </a>
+                            </Template>
+                        </Column>
+                        <Column TableItem="PleskServer" Title="@(SmartTranslateService.Translate("Manage"))" Field="@(x => x.Id)" Sortable="false" Filterable="false">
+                            <Template>
+                                <DeleteButton Confirm="true" OnClick="() => OnClick(context)">
+                                </DeleteButton>
+                            </Template>
+                        </Column>
+                        <Pager ShowPageNumber="true" ShowTotalCount="true"/>
+                    </Table>
+                </div>
+            </LazyLoader>
+        </div>
+    </div>
+</OnlyAdmin>
+
+@code
+{
+    private PleskServer[] PleskServers;
+
+    private LazyLoader LazyLoader;
+    private Dictionary<PleskServer, bool> OnlineCache = new();
+
+    private Task Load(LazyLoader arg)
+    {
+        PleskServers = PleskServerRepository
+            .Get()
+            .ToArray();
+
+        Task.Run(async () =>
+        {
+            foreach (var pleskServer in PleskServers)
+            {
+                OnlineCache.Add(pleskServer, await WebsiteService.IsHostUp(pleskServer));
+
+                await InvokeAsync(StateHasChanged);
+            }
+        });
+
+        return Task.CompletedTask;
+    }
+
+    private async Task OnClick(PleskServer pleskServer)
+    {
+        PleskServerRepository.Delete(pleskServer);
+
+        await LazyLoader.Reload();
+    }
+}

+ 55 - 0
Moonlight/Shared/Views/Admin/Websites/Servers/New.razor

@@ -0,0 +1,55 @@
+@page "/admin/websites/servers/new"
+@using Moonlight.App.Repositories
+@using Moonlight.App.Models.Forms
+
+@inject PleskServerRepository PleskServerRepository
+@inject NavigationManager NavigationManager
+
+<OnlyAdmin>
+    <div class="card card-body p-10">
+        <SmartForm Model="Model" OnValidSubmit="OnValidSubmit">
+            <label class="form-label">
+                <TL>Name</TL>
+            </label>
+            <div class="input-group mb-5">
+                <InputText @bind-Value="Model.Name" class="form-control"></InputText>
+            </div>
+            <label class="form-label">
+                <TL>Api Url</TL>
+            </label>
+            <div class="input-group mb-5">
+                <InputText @bind-Value="Model.ApiUrl" class="form-control"></InputText>
+            </div>
+            <label class="form-label">
+                <TL>Api Key</TL>
+            </label>
+            <div class="input-group mb-5">
+                <InputText @bind-Value="Model.ApiKey" class="form-control"></InputText>
+            </div>
+            <div>
+                <button type="submit" class="btn btn-primary float-end">
+                    <TL>Create</TL>
+                </button>
+            </div>
+        </SmartForm>
+    </div>
+</OnlyAdmin>
+
+@code
+{
+    private PleskServerDataModel Model = new();
+
+    private Task OnValidSubmit()
+    {
+        PleskServerRepository.Add(new()
+        {
+            Name = Model.Name,
+            ApiUrl = Model.ApiUrl,
+            ApiKey = Model.ApiKey
+        });
+        
+        NavigationManager.NavigateTo("/admin/websites/servers");
+
+        return Task.CompletedTask;
+    }
+}

+ 8 - 16
Moonlight/Shared/Views/Server/Index.razor

@@ -17,7 +17,7 @@
 @inject ServerRepository ServerRepository
 @inject WingsConsoleHelper WingsConsoleHelper
 @inject MessageService MessageService
-@inject NodeService NodeService
+@inject ServerService ServerService
 @inject NavigationManager NavigationManager
 
 @implements IDisposable
@@ -227,9 +227,6 @@
                 .Include(x => x.MainAllocation)
                 .Include(x => x.Owner)
                 .First(x => x.Uuid == uuid);
-
-            if (CurrentServer.Owner.Id != User!.Id && !User.Admin)
-                CurrentServer = null;
         }
         catch (Exception)
         {
@@ -238,20 +235,15 @@
 
         if (CurrentServer != null)
         {
-            await lazyLoader.SetText("Checking node online status");
+            if (CurrentServer.Owner.Id != User!.Id && !User.Admin)
+                CurrentServer = null;
+        }
 
-            try
-            {
-                //TODO: Implement status caching
-                var data = await NodeService.GetStatus(CurrentServer.Node);
+        if (CurrentServer != null)
+        {
+            await lazyLoader.SetText("Checking node online status");
 
-                if (data != null)
-                    NodeOnline = true;
-            }
-            catch (Exception)
-            {
-                // ignored
-            }
+            NodeOnline = await ServerService.IsHostUp(CurrentServer);
 
             if (NodeOnline)
             {

+ 132 - 0
Moonlight/Shared/Views/Website/Index.razor

@@ -0,0 +1,132 @@
+@page "/website/{Id:int}/{Route?}"
+@using Moonlight.App.Database.Entities
+@using Moonlight.App.Repositories
+@using Moonlight.App.Services
+@using Moonlight.Shared.Components.WebsiteControl
+@using Microsoft.EntityFrameworkCore
+@using Moonlight.App.Services.Interop
+
+@inject WebsiteRepository WebsiteRepository
+@inject WebsiteService WebsiteService
+@inject ToastService ToastService
+
+<LazyLoader Load="Load">
+    @if (CurrentWebsite == null)
+    {
+        <div class="d-flex justify-content-center flex-center">
+            <div class="card">
+                <img src="/assets/media/svg/nodata.svg" class="card-img-top w-50 mx-auto pt-5" alt="Not found image"/>
+                <div class="card-body text-center">
+                    <h1 class="card-title">
+                        <TL>Website not found</TL>
+                    </h1>
+                    <p class="card-text fs-4">
+                        <TL>A website with that id cannot be found or you have no access for this server</TL>
+                    </p>
+                </div>
+            </div>
+        </div>
+    }
+    else
+    {
+        if (HostOnline)
+        {
+            <CascadingValue Value="CurrentWebsite">
+                @{
+                    var index = 0;
+
+                    switch (Route)
+                    {
+                        case "files":
+                            index = 1;
+                            break;
+                        case "ftp":
+                            index = 2;
+                            break;
+                        case "databases":
+                            index = 3;
+                            break;
+                        default:
+                            index = 0;
+                            break;
+                    }
+                    
+                    <WebsiteNavigation Index="index" Website="CurrentWebsite" />
+                    
+                    @switch (Route)
+                    {
+                        case "files":
+                            <WebsiteFiles />
+                            break;
+                        case "ftp":
+                            <WebsiteFtp />
+                            break;
+                        case "databases":
+                            <WebsiteDatabases />
+                            break;
+                        default:
+                            <WebsiteDashboard />
+                            break;
+                    }
+                }
+            </CascadingValue>
+        }
+        else
+        {
+            <div class="d-flex justify-content-center flex-center">
+                <div class="card">
+                    <img src="/assets/media/svg/serverdown.svg" class="card-img-top w-50 mx-auto pt-5" alt="Not found image"/>
+                    <div class="card-body text-center">
+                        <h1 class="card-title">
+                            <TL>Host system offline</TL>
+                        </h1>
+                        <p class="card-text fs-4">
+                            <TL>The host system the website is running on is currently offline</TL>
+                        </p>
+                    </div>
+                </div>
+            </div>
+        }
+    }
+</LazyLoader>
+
+@code
+{
+    [Parameter]
+    public int Id { get; set; }
+
+    [Parameter]
+    public string? Route { get; set; }
+
+    [CascadingParameter]
+    public User User { get; set; }
+
+    private Website? CurrentWebsite;
+    private bool HostOnline = false;
+
+    private async Task Load(LazyLoader lazyLoader)
+    {
+        CurrentWebsite = WebsiteRepository
+            .Get()
+            .Include(x => x.PleskServer)
+            .Include(x => x.Owner)
+            .FirstOrDefault(x => x.Id == Id);
+
+        if (CurrentWebsite != null)
+        {
+            if (CurrentWebsite.Owner.Id != User!.Id && !User.Admin)
+                CurrentWebsite = null;
+        }
+
+        if (CurrentWebsite != null)
+        {
+            await lazyLoader.SetText("Checking host system online status");
+
+            HostOnline = await WebsiteService.IsHostUp(CurrentWebsite);
+
+            if (HostOnline)
+            {
+            }
+        }
+    }
+}

+ 1 - 0
Moonlight/Shared/Views/Websites/Index.razor

@@ -0,0 +1 @@
+@page "/websites"

+ 1 - 0
Moonlight/Shared/Views/Websites/New.razor

@@ -0,0 +1 @@
+@page "/websites/new"

+ 25 - 0
Moonlight/resources/lang/de_de.lang

@@ -487,3 +487,28 @@ Decompress;Decompress
 Moving;Moving
 Compressing;Compressing
 selected;selected
+New website;New website
+Plesk servers;Plesk servers
+Base domain;Base domain
+Plesk server;Plesk server
+Ftp;Ftp
+No SSL certificate found;No SSL certificate found
+Ftp Host;Ftp Host
+Ftp Port;Ftp Port
+Ftp Username;Ftp Username
+Ftp Password;Ftp Password
+Use;Use
+SSL Certificates;SSL Certificates
+SSL certificates;SSL certificates
+Issue certificate;Issue certificate
+New plesk server;New plesk server
+Api url;Api url
+Host system offline;Host system offline
+The host system the website is running on is currently offline;The host system the website is running on is currently offline
+No SSL certificates found;No SSL certificates found
+No databases found for this website;No databases found for this website
+The name should be at least 8 characters long;The name should be at least 8 characters long
+The name should only contain of lower case characters and numbers;The name should only contain of lower case characters and numbers
+Error from plesk;Error from plesk
+Host;Host
+Username;Username

+ 21 - 0
Moonlight/resources/lang/en_us.lang

@@ -0,0 +1,21 @@
+Open support;Open support
+About us;About us
+Imprint;Imprint
+Privacy;Privacy
+Create;Create
+Server;Server
+Domain;Domain
+Website;Website
+Login;Login
+Register;Register
+Email;Email
+Password;Password
+Sign In;Sign In
+Sign in to start with moonlight;Sign in to start with moonlight
+Sign in with Discord;Sign in with Discord
+Sign in with Google;Sign in with Google
+Or with email;Or with email
+Forgot password?;Forgot password?
+Sign-in;Sign-in
+Not registered yet?;Not registered yet?
+Sign up;Sign up