From 0b6882d3f84317ad32ceb4c927c89048357b76de Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Tue, 21 Feb 2023 21:22:40 +0100 Subject: [PATCH] Added support chat. Added resource service. Added server backups --- .gitignore | 1 + Moonlight/App/Database/DataContext.cs | 1 + .../App/Database/Entities/SupportMessage.cs | 17 + Moonlight/App/Database/Entities/User.cs | 11 +- ...230221173242_AddSupportMessage.Designer.cs | 617 +++++++++++++++++ .../20230221173242_AddSupportMessage.cs | 55 ++ .../Migrations/DataContextModelSnapshot.cs | 62 ++ ...730_UpdatedSupportAndUserModel.Designer.cs | 635 ++++++++++++++++++ ...230221183730_UpdatedSupportAndUserModel.cs | 122 ++++ Moonlight/App/Helpers/Formatter.cs | 32 +- .../Api/Remote/BackupController.cs | 95 +++ .../Api/Remote/SftpAuthController.cs | 115 ++++ Moonlight/App/Models/Misc/QuestionType.cs | 6 + .../Repositories/SupportMessageRepository.cs | 44 ++ Moonlight/App/Services/ResourceService.cs | 23 + .../Services/Support/SupportAdminServer.cs | 67 ++ .../Services/Support/SupportClientService.cs | 64 ++ .../Services/Support/SupportServerService.cs | 104 +++ Moonlight/Moonlight.csproj | 1 + Moonlight/Program.cs | 11 + .../Shared/Components/Partials/Sidebar.razor | 3 +- Moonlight/Shared/Layouts/MainLayout.razor | 22 +- Moonlight/Shared/Views/Admin/Index.razor | 135 ++++ Moonlight/Shared/Views/Support.razor | 144 ++++ ....GeneratedMSBuildEditorConfig.editorconfig | 8 + Moonlight/resources/lang/de_de.lang | 22 + 26 files changed, 2408 insertions(+), 9 deletions(-) create mode 100644 Moonlight/App/Database/Entities/SupportMessage.cs create mode 100644 Moonlight/App/Database/Migrations/20230221173242_AddSupportMessage.Designer.cs create mode 100644 Moonlight/App/Database/Migrations/20230221173242_AddSupportMessage.cs create mode 100644 Moonlight/App/Databse/Migrations/20230221183730_UpdatedSupportAndUserModel.Designer.cs create mode 100644 Moonlight/App/Databse/Migrations/20230221183730_UpdatedSupportAndUserModel.cs create mode 100644 Moonlight/App/Http/Controllers/Api/Remote/BackupController.cs create mode 100644 Moonlight/App/Http/Controllers/Api/Remote/SftpAuthController.cs create mode 100644 Moonlight/App/Models/Misc/QuestionType.cs create mode 100644 Moonlight/App/Repositories/SupportMessageRepository.cs create mode 100644 Moonlight/App/Services/ResourceService.cs create mode 100644 Moonlight/App/Services/Support/SupportAdminServer.cs create mode 100644 Moonlight/App/Services/Support/SupportClientService.cs create mode 100644 Moonlight/App/Services/Support/SupportServerService.cs create mode 100644 Moonlight/Shared/Views/Admin/Index.razor create mode 100644 Moonlight/Shared/Views/Support.razor diff --git a/.gitignore b/.gitignore index ce7c432..056f84d 100644 --- a/.gitignore +++ b/.gitignore @@ -442,3 +442,4 @@ Moonlight/obj/Moonlight.csproj.nuget.dgspec.json Moonlight/obj/project.assets.json Moonlight/obj/project.nuget.cache Moonlight/obj/project.packagespec.json +Moonlight/obj/Debug/net6.0/Moonlight.GeneratedMSBuildEditorConfig.editorconfig diff --git a/Moonlight/App/Database/DataContext.cs b/Moonlight/App/Database/DataContext.cs index 9872c22..3ecdf31 100644 --- a/Moonlight/App/Database/DataContext.cs +++ b/Moonlight/App/Database/DataContext.cs @@ -26,6 +26,7 @@ public class DataContext : DbContext public DbSet LoadingMessages { get; set; } public DbSet AuditLog { get; set; } public DbSet Databases { get; set; } + public DbSet SupportMessages { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { diff --git a/Moonlight/App/Database/Entities/SupportMessage.cs b/Moonlight/App/Database/Entities/SupportMessage.cs new file mode 100644 index 0000000..df1019d --- /dev/null +++ b/Moonlight/App/Database/Entities/SupportMessage.cs @@ -0,0 +1,17 @@ +using Moonlight.App.Models.Misc; + +namespace Moonlight.App.Database.Entities; + +public class SupportMessage +{ + public int Id { get; set; } + public string Message { get; set; } = ""; + public User? Sender { get; set; } = null; + public User? Recipient { get; set; } = null; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public bool IsQuestion { get; set; } = false; + public QuestionType Type { get; set; } + public string Answer { get; set; } = ""; + public bool IsSystem { get; set; } = false; + public bool IsSupport { get; set; } = false; +} \ No newline at end of file diff --git a/Moonlight/App/Database/Entities/User.cs b/Moonlight/App/Database/Entities/User.cs index d4eea6d..8bb1d26 100644 --- a/Moonlight/App/Database/Entities/User.cs +++ b/Moonlight/App/Database/Entities/User.cs @@ -14,13 +14,14 @@ public class User public string State { get; set; } = ""; public string Country { get; set; } = ""; public UserStatus Status { get; set; } = UserStatus.Unverified; - public bool TotpEnabled { get; set; } + public bool TotpEnabled { get; set; } = false; public string TotpSecret { get; set; } = ""; public DateTime TokenValidTime { get; set; } = DateTime.Now; - public long DiscordId { get; set; } + public long DiscordId { get; set; } = -1; public string DiscordUsername { get; set; } = ""; public string DiscordDiscriminator { get; set; } = ""; - public DateTime CreatedAt { get; set; } - public DateTime UpdatedAt { get; set; } - public bool Admin { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + public bool Admin { get; set; } = false; + public bool SupportPending { get; set; } = false; } \ No newline at end of file diff --git a/Moonlight/App/Database/Migrations/20230221173242_AddSupportMessage.Designer.cs b/Moonlight/App/Database/Migrations/20230221173242_AddSupportMessage.Designer.cs new file mode 100644 index 0000000..8c8f5ea --- /dev/null +++ b/Moonlight/App/Database/Migrations/20230221173242_AddSupportMessage.Designer.cs @@ -0,0 +1,617 @@ +// +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("20230221173242_AddSupportMessage")] + partial class AddSupportMessage + { + /// + 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.AuditLogEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Ip") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("JsonData") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("System") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("AuditLog"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Database", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AaPanelId") + .HasColumnType("int"); + + b.Property("OwnerId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("Databases"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.DockerImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Default") + .HasColumnType("tinyint(1)"); + + b.Property("ImageId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ImageId"); + + b.ToTable("DockerImages"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Image", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ConfigFiles") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("InstallDockerImage") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("InstallEntrypoint") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("InstallScript") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Startup") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StartupDetection") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StopCommand") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Uuid") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ImageTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ImageId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ImageId"); + + b.ToTable("ImageTags"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ImageVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("DefaultValue") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ImageId") + .HasColumnType("int"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ImageId"); + + b.ToTable("ImageVariables"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.LoadingMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Message") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("LoadingMessages"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Node", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Fqdn") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("HttpPort") + .HasColumnType("int"); + + b.Property("MoonlightDaemonPort") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("SftpPort") + .HasColumnType("int"); + + b.Property("Ssl") + .HasColumnType("tinyint(1)"); + + b.Property("Token") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TokenId") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Nodes"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.NodeAllocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("NodeId") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("NodeId"); + + b.HasIndex("ServerId"); + + b.ToTable("NodeAllocations"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Server", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Cpu") + .HasColumnType("int"); + + b.Property("Disk") + .HasColumnType("bigint"); + + b.Property("DockerImageIndex") + .HasColumnType("int"); + + b.Property("ImageId") + .HasColumnType("int"); + + b.Property("Installing") + .HasColumnType("tinyint(1)"); + + b.Property("MainAllocationId") + .HasColumnType("int"); + + b.Property("Memory") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("NodeId") + .HasColumnType("int"); + + b.Property("OverrideStartup") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OwnerId") + .HasColumnType("int"); + + b.Property("Suspended") + .HasColumnType("tinyint(1)"); + + b.Property("Uuid") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ImageId"); + + b.HasIndex("MainAllocationId"); + + b.HasIndex("NodeId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Servers"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ServerBackup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Bytes") + .HasColumnType("bigint"); + + b.Property("Created") + .HasColumnType("tinyint(1)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("Uuid") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.ToTable("ServerBackups"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ServerVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.ToTable("ServerVariables"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.SupportMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Answer") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsQuestion") + .HasColumnType("tinyint(1)"); + + b.Property("Message") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("SenderId") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("SenderId"); + + b.ToTable("SupportMessages"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Address") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Admin") + .HasColumnType("tinyint(1)"); + + b.Property("City") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Country") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DiscordDiscriminator") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("DiscordId") + .HasColumnType("bigint"); + + b.Property("DiscordUsername") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Email") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Password") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("State") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TokenValidTime") + .HasColumnType("datetime(6)"); + + b.Property("TotpEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("TotpSecret") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Database", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + 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.ImageTag", b => + { + b.HasOne("Moonlight.App.Database.Entities.Image", null) + .WithMany("Tags") + .HasForeignKey("ImageId"); + }); + + 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.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", "Sender") + .WithMany() + .HasForeignKey("SenderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Sender"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Image", b => + { + b.Navigation("DockerImages"); + + b.Navigation("Tags"); + + 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 + } + } +} diff --git a/Moonlight/App/Database/Migrations/20230221173242_AddSupportMessage.cs b/Moonlight/App/Database/Migrations/20230221173242_AddSupportMessage.cs new file mode 100644 index 0000000..4153bfe --- /dev/null +++ b/Moonlight/App/Database/Migrations/20230221173242_AddSupportMessage.cs @@ -0,0 +1,55 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Moonlight.App.Database.Migrations +{ + /// + public partial class AddSupportMessage : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "SupportMessages", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Message = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + SenderId = table.Column(type: "int", nullable: false), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + IsQuestion = table.Column(type: "tinyint(1)", nullable: false), + Type = table.Column(type: "int", nullable: false), + Answer = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_SupportMessages", x => x.Id); + table.ForeignKey( + name: "FK_SupportMessages_Users_SenderId", + column: x => x.SenderId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_SupportMessages_SenderId", + table: "SupportMessages", + column: "SenderId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "SupportMessages"); + } + } +} diff --git a/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs b/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs index 2c4e1d2..d8c3350 100644 --- a/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs +++ b/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs @@ -374,6 +374,50 @@ namespace Moonlight.App.Database.Migrations b.ToTable("ServerVariables"); }); + modelBuilder.Entity("Moonlight.App.Database.Entities.SupportMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Answer") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsQuestion") + .HasColumnType("tinyint(1)"); + + b.Property("IsSupport") + .HasColumnType("tinyint(1)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("Message") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RecipientId") + .HasColumnType("int"); + + b.Property("SenderId") + .HasColumnType("int"); + + b.Property("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("Id") @@ -432,6 +476,9 @@ namespace Moonlight.App.Database.Migrations b.Property("Status") .HasColumnType("int"); + b.Property("SupportPending") + .HasColumnType("tinyint(1)"); + b.Property("TokenValidTime") .HasColumnType("datetime(6)"); @@ -542,6 +589,21 @@ namespace Moonlight.App.Database.Migrations .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.Image", b => { b.Navigation("DockerImages"); diff --git a/Moonlight/App/Databse/Migrations/20230221183730_UpdatedSupportAndUserModel.Designer.cs b/Moonlight/App/Databse/Migrations/20230221183730_UpdatedSupportAndUserModel.Designer.cs new file mode 100644 index 0000000..01ee545 --- /dev/null +++ b/Moonlight/App/Databse/Migrations/20230221183730_UpdatedSupportAndUserModel.Designer.cs @@ -0,0 +1,635 @@ +// +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.Databse.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20230221183730_UpdatedSupportAndUserModel")] + partial class UpdatedSupportAndUserModel + { + /// + 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.AuditLogEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Ip") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("JsonData") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("System") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("AuditLog"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Database", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AaPanelId") + .HasColumnType("int"); + + b.Property("OwnerId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("Databases"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.DockerImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Default") + .HasColumnType("tinyint(1)"); + + b.Property("ImageId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ImageId"); + + b.ToTable("DockerImages"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Image", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ConfigFiles") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("InstallDockerImage") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("InstallEntrypoint") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("InstallScript") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Startup") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StartupDetection") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StopCommand") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Uuid") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ImageTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ImageId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ImageId"); + + b.ToTable("ImageTags"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ImageVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("DefaultValue") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ImageId") + .HasColumnType("int"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ImageId"); + + b.ToTable("ImageVariables"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.LoadingMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Message") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("LoadingMessages"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Node", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Fqdn") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("HttpPort") + .HasColumnType("int"); + + b.Property("MoonlightDaemonPort") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("SftpPort") + .HasColumnType("int"); + + b.Property("Ssl") + .HasColumnType("tinyint(1)"); + + b.Property("Token") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TokenId") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Nodes"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.NodeAllocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("NodeId") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("NodeId"); + + b.HasIndex("ServerId"); + + b.ToTable("NodeAllocations"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Server", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Cpu") + .HasColumnType("int"); + + b.Property("Disk") + .HasColumnType("bigint"); + + b.Property("DockerImageIndex") + .HasColumnType("int"); + + b.Property("ImageId") + .HasColumnType("int"); + + b.Property("Installing") + .HasColumnType("tinyint(1)"); + + b.Property("MainAllocationId") + .HasColumnType("int"); + + b.Property("Memory") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("NodeId") + .HasColumnType("int"); + + b.Property("OverrideStartup") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OwnerId") + .HasColumnType("int"); + + b.Property("Suspended") + .HasColumnType("tinyint(1)"); + + b.Property("Uuid") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ImageId"); + + b.HasIndex("MainAllocationId"); + + b.HasIndex("NodeId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Servers"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ServerBackup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Bytes") + .HasColumnType("bigint"); + + b.Property("Created") + .HasColumnType("tinyint(1)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("Uuid") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.ToTable("ServerBackups"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ServerVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.ToTable("ServerVariables"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.SupportMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Answer") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsQuestion") + .HasColumnType("tinyint(1)"); + + b.Property("IsSupport") + .HasColumnType("tinyint(1)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("Message") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RecipientId") + .HasColumnType("int"); + + b.Property("SenderId") + .HasColumnType("int"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Address") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Admin") + .HasColumnType("tinyint(1)"); + + b.Property("City") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Country") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DiscordDiscriminator") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("DiscordId") + .HasColumnType("bigint"); + + b.Property("DiscordUsername") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Email") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Password") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("State") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("SupportPending") + .HasColumnType("tinyint(1)"); + + b.Property("TokenValidTime") + .HasColumnType("datetime(6)"); + + b.Property("TotpEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("TotpSecret") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Database", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + 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.ImageTag", b => + { + b.HasOne("Moonlight.App.Database.Entities.Image", null) + .WithMany("Tags") + .HasForeignKey("ImageId"); + }); + + 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.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.Image", b => + { + b.Navigation("DockerImages"); + + b.Navigation("Tags"); + + 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 + } + } +} diff --git a/Moonlight/App/Databse/Migrations/20230221183730_UpdatedSupportAndUserModel.cs b/Moonlight/App/Databse/Migrations/20230221183730_UpdatedSupportAndUserModel.cs new file mode 100644 index 0000000..278cbca --- /dev/null +++ b/Moonlight/App/Databse/Migrations/20230221183730_UpdatedSupportAndUserModel.cs @@ -0,0 +1,122 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Moonlight.App.Databse.Migrations +{ + /// + public partial class UpdatedSupportAndUserModel : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_SupportMessages_Users_SenderId", + table: "SupportMessages"); + + migrationBuilder.AddColumn( + name: "SupportPending", + table: "Users", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + + migrationBuilder.AlterColumn( + name: "SenderId", + table: "SupportMessages", + type: "int", + nullable: true, + oldClrType: typeof(int), + oldType: "int"); + + migrationBuilder.AddColumn( + name: "IsSupport", + table: "SupportMessages", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "IsSystem", + table: "SupportMessages", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "RecipientId", + table: "SupportMessages", + type: "int", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_SupportMessages_RecipientId", + table: "SupportMessages", + column: "RecipientId"); + + migrationBuilder.AddForeignKey( + name: "FK_SupportMessages_Users_RecipientId", + table: "SupportMessages", + column: "RecipientId", + principalTable: "Users", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_SupportMessages_Users_SenderId", + table: "SupportMessages", + column: "SenderId", + principalTable: "Users", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_SupportMessages_Users_RecipientId", + table: "SupportMessages"); + + migrationBuilder.DropForeignKey( + name: "FK_SupportMessages_Users_SenderId", + table: "SupportMessages"); + + migrationBuilder.DropIndex( + name: "IX_SupportMessages_RecipientId", + table: "SupportMessages"); + + migrationBuilder.DropColumn( + name: "SupportPending", + table: "Users"); + + migrationBuilder.DropColumn( + name: "IsSupport", + table: "SupportMessages"); + + migrationBuilder.DropColumn( + name: "IsSystem", + table: "SupportMessages"); + + migrationBuilder.DropColumn( + name: "RecipientId", + table: "SupportMessages"); + + migrationBuilder.AlterColumn( + name: "SenderId", + table: "SupportMessages", + type: "int", + nullable: false, + defaultValue: 0, + oldClrType: typeof(int), + oldType: "int", + oldNullable: true); + + migrationBuilder.AddForeignKey( + name: "FK_SupportMessages_Users_SenderId", + table: "SupportMessages", + column: "SenderId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/Moonlight/App/Helpers/Formatter.cs b/Moonlight/App/Helpers/Formatter.cs index d1386f4..dcd3803 100644 --- a/Moonlight/App/Helpers/Formatter.cs +++ b/Moonlight/App/Helpers/Formatter.cs @@ -1,4 +1,6 @@ -namespace Moonlight.App.Helpers; +using Moonlight.App.Services; + +namespace Moonlight.App.Helpers; public static class Formatter { @@ -34,4 +36,32 @@ public static class Formatter return (i / (1024D * 1024D)).Round(2) + " GB"; } } + + public static string FormatAgoFromDateTime(DateTime dt, SmartTranslateService translateService = null) + { + TimeSpan timeSince = DateTime.UtcNow.Subtract(dt); + + if (timeSince.TotalMilliseconds < 1) + return translateService == null ? "just now" : translateService.Translate("just now"); + + if (timeSince.TotalMinutes < 1) + return translateService == null ? "less than a minute ago" : translateService.Translate("less than a minute ago"); + + if (timeSince.TotalMinutes < 2) + return translateService == null ? "1 minute ago" : translateService.Translate("1 minute ago"); + + if (timeSince.TotalMinutes < 60) + return Math.Round(timeSince.TotalMinutes) + (translateService == null ? " minutes ago" : translateService.Translate(" minutes ago")); + + if (timeSince.TotalHours < 2) + return translateService == null ? "1 hour ago" : translateService.Translate("1 hour ago"); + + if (timeSince.TotalHours < 24) + return Math.Round(timeSince.TotalHours) + (translateService == null ? " hours ago" : translateService.Translate(" hours ago")); + + if (timeSince.TotalDays < 2) + return translateService == null ? "1 day ago" : translateService.Translate("1 day ago"); + + return Math.Round(timeSince.TotalDays) + (translateService == null ? " days ago" : translateService.Translate(" days ago")); + } } \ No newline at end of file diff --git a/Moonlight/App/Http/Controllers/Api/Remote/BackupController.cs b/Moonlight/App/Http/Controllers/Api/Remote/BackupController.cs new file mode 100644 index 0000000..4697822 --- /dev/null +++ b/Moonlight/App/Http/Controllers/Api/Remote/BackupController.cs @@ -0,0 +1,95 @@ +using Microsoft.AspNetCore.Mvc; +using Moonlight.App.Http.Requests.Wings; +using Moonlight.App.Repositories; +using Moonlight.App.Repositories.Servers; +using Moonlight.App.Services; + +namespace Moonlight.App.Http.Controllers.Api.Remote; + +[Route("api/remote/backups")] +[ApiController] +public class BackupController : Controller +{ + private readonly ServerBackupRepository ServerBackupRepository; + private readonly MessageService MessageService; + private readonly NodeRepository NodeRepository; + + public BackupController( + ServerBackupRepository serverBackupRepository, + NodeRepository nodeRepository, + MessageService messageService) + { + ServerBackupRepository = serverBackupRepository; + NodeRepository = nodeRepository; + MessageService = messageService; + } + + [HttpGet("{uuid}")] + public ActionResult Download(Guid uuid) + { + return ""; + } + + [HttpPost("{uuid}")] + public async Task SetStatus([FromRoute] Guid uuid, [FromBody] ReportBackupCompleteRequest request) + { + var tokenData = Request.Headers.Authorization.ToString().Replace("Bearer ", ""); + var id = tokenData.Split(".")[0]; + var token = tokenData.Split(".")[1]; + + var node = NodeRepository.Get().FirstOrDefault(x => x.TokenId == id); + + if (node == null) + return NotFound(); + + if (token != node.Token) + return Unauthorized(); + + var backup = ServerBackupRepository.Get().FirstOrDefault(x => x.Uuid == uuid); + + if (backup == null) + return NotFound(); + + if (request.Successful) + { + backup.Created = true; + backup.Bytes = request.Size; + + ServerBackupRepository.Update(backup); + + await MessageService.Emit($"wings.backups.create", backup); + } + else + { + await MessageService.Emit($"wings.backups.createfailed", backup); + ServerBackupRepository.Delete(backup); + } + + return NoContent(); + } + + [HttpPost("{uuid}/restore")] + public async Task SetRestoreStatus([FromRoute] Guid uuid) + { + var tokenData = Request.Headers.Authorization.ToString().Replace("Bearer ", ""); + var id = tokenData.Split(".")[0]; + var token = tokenData.Split(".")[1]; + + var node = NodeRepository.Get().FirstOrDefault(x => x.TokenId == id); + + if (node == null) + return NotFound(); + + if (token != node.Token) + return Unauthorized(); + + var backup = ServerBackupRepository.Get().FirstOrDefault(x => x.Uuid == uuid); + + if (backup == null) + return NotFound(); + + await MessageService.Emit($"wings.backups.restore", backup); + + return NoContent(); + } +} \ No newline at end of file diff --git a/Moonlight/App/Http/Controllers/Api/Remote/SftpAuthController.cs b/Moonlight/App/Http/Controllers/Api/Remote/SftpAuthController.cs new file mode 100644 index 0000000..947f13f --- /dev/null +++ b/Moonlight/App/Http/Controllers/Api/Remote/SftpAuthController.cs @@ -0,0 +1,115 @@ +using Microsoft.AspNetCore.Mvc; +using Moonlight.App.Http.Requests.Wings; +using Moonlight.App.Http.Resources.Wings; +using Moonlight.App.Repositories; +using Moonlight.App.Services; + +namespace Moonlight.App.Http.Controllers.Api.Remote; + +[ApiController] +[Route("api/remote/sftp/auth")] +public class SftpAuthController : Controller +{ + private readonly ServerService ServerService; + private readonly NodeRepository NodeRepository; + + public SftpAuthController( + ServerService serverService, + NodeRepository nodeRepository) + { + ServerService = serverService; + NodeRepository = nodeRepository; + } + + [HttpPost] + public async Task> Login(SftpLoginRequest request) + { + var tokenData = Request.Headers.Authorization.ToString().Replace("Bearer ", ""); + var tokenId = tokenData.Split(".")[0]; + var token = tokenData.Split(".")[1]; + + var node = NodeRepository.Get().FirstOrDefault(x => x.TokenId == tokenId); + + if (node == null) + return NotFound(); + + if (token != node.Token) + return Unauthorized(); + + if (request.Type == "public_key") // Deny public key authentication, because moonlight does not implement that + { + return StatusCode(403); + } + + // Parse the username + var parts = request.Username.Split("."); + + if (parts.Length < 2) + return BadRequest(); + + if (!int.TryParse(parts[0], out int id)) + return BadRequest(); + + if (!int.TryParse(parts[1], out int serverId)) + return BadRequest(); + + try + { + var server = await ServerService.SftpServerLogin(serverId, id, request.Password); + + return Ok(new SftpLoginResult() + { + Server = server.Uuid.ToString(), + User = "", + Permissions = new() + { + "control.console", + "control.start", + "control.stop", + "control.restart", + "websocket.connect", + "file.create", + "file.read", + "file.read-content", + "file.update", + "file.delete", + "file.archive", + "file.sftp", + "user.create", + "user.read", + "user.update", + "user.delete", + "backup.create", + "backup.read", + "backup.delete", + "backup.download", + "backup.restore", + "allocation.read", + "allocation.create", + "allocation.update", + "allocation.delete", + "startup.read", + "startup.update", + "startup.docker-image", + "database.create", + "database.read", + "database.update", + "database.delete", + "database.view_password", + "schedule.create", + "schedule.read", + "schedule.update", + "schedule.delete", + "settings.rename", + "settings.reinstall" + } + }); + } + catch (Exception e) + { + // Most of the exception here will be because of stuff like a invalid server id and simular things + // so we ignore them and return 403 + return StatusCode(403); + } + } +} \ No newline at end of file diff --git a/Moonlight/App/Models/Misc/QuestionType.cs b/Moonlight/App/Models/Misc/QuestionType.cs new file mode 100644 index 0000000..75acdc9 --- /dev/null +++ b/Moonlight/App/Models/Misc/QuestionType.cs @@ -0,0 +1,6 @@ +namespace Moonlight.App.Models.Misc; + +public enum QuestionType +{ + ServerUrl +} \ No newline at end of file diff --git a/Moonlight/App/Repositories/SupportMessageRepository.cs b/Moonlight/App/Repositories/SupportMessageRepository.cs new file mode 100644 index 0000000..1f6ec2e --- /dev/null +++ b/Moonlight/App/Repositories/SupportMessageRepository.cs @@ -0,0 +1,44 @@ +using Microsoft.EntityFrameworkCore; +using Moonlight.App.Database; +using Moonlight.App.Database.Entities; + +namespace Moonlight.App.Repositories; + +public class SupportMessageRepository : IDisposable +{ + private readonly DataContext DataContext; + + public SupportMessageRepository(DataContext dataContext) + { + DataContext = dataContext; + } + + public DbSet Get() + { + return DataContext.SupportMessages; + } + + public SupportMessage Add(SupportMessage message) + { + var x = DataContext.SupportMessages.Add(message); + DataContext.SaveChanges(); + return x.Entity; + } + + public void Update(SupportMessage message) + { + DataContext.SupportMessages.Update(message); + DataContext.SaveChanges(); + } + + public void Delete(SupportMessage message) + { + DataContext.SupportMessages.Remove(message); + DataContext.SaveChanges(); + } + + public void Dispose() + { + DataContext.Dispose(); + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/ResourceService.cs b/Moonlight/App/Services/ResourceService.cs new file mode 100644 index 0000000..0225421 --- /dev/null +++ b/Moonlight/App/Services/ResourceService.cs @@ -0,0 +1,23 @@ +using Moonlight.App.Database.Entities; + +namespace Moonlight.App.Services; + +public class ResourceService +{ + private readonly string AppUrl; + + public ResourceService(ConfigService configService) + { + AppUrl = configService.GetSection("Moonlight").GetValue("AppUrl"); + } + + public string Image(string name) + { + return $"{AppUrl}/api/moonlight/resources/images/{name}"; + } + + public string Avatar(User user) + { + return $"{AppUrl}/api/moonlight/avatar/{user.Id}"; + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/Support/SupportAdminServer.cs b/Moonlight/App/Services/Support/SupportAdminServer.cs new file mode 100644 index 0000000..f3eaf44 --- /dev/null +++ b/Moonlight/App/Services/Support/SupportAdminServer.cs @@ -0,0 +1,67 @@ +using Moonlight.App.Database.Entities; +using Moonlight.App.Services.Sessions; + +namespace Moonlight.App.Services.Support; + +public class SupportAdminServer +{ + private readonly SupportServerService SupportServerService; + private readonly IdentityService IdentityService; + private readonly MessageService MessageService; + + public EventHandler OnNewMessage; + + private User Self; + private User Recipient; + + public SupportAdminServer( + SupportServerService supportServerService, + IdentityService identityService, + MessageService messageService) + { + SupportServerService = supportServerService; + IdentityService = identityService; + MessageService = messageService; + } + + public async Task Start(User user) + { + Self = (await IdentityService.Get())!; + Recipient = user; + + MessageService.Subscribe( + $"support.{Recipient.Id}.message", + this, + message => + { + OnNewMessage?.Invoke(this, message); + + return Task.CompletedTask; + }); + } + + public async Task GetMessages() + { + return await SupportServerService.GetMessages(Recipient); + } + + public async Task SendMessage(string content) + { + var message = new SupportMessage() + { + Message = content + }; + + await SupportServerService.SendMessage( + Recipient, + message, + Self, + true + ); + } + + public void Dispose() + { + MessageService.Unsubscribe($"support.{Recipient.Id}.message", this); + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/Support/SupportClientService.cs b/Moonlight/App/Services/Support/SupportClientService.cs new file mode 100644 index 0000000..c08b9e0 --- /dev/null +++ b/Moonlight/App/Services/Support/SupportClientService.cs @@ -0,0 +1,64 @@ +using Moonlight.App.Database.Entities; +using Moonlight.App.Services.Sessions; + +namespace Moonlight.App.Services.Support; + +public class SupportClientService : IDisposable +{ + private readonly SupportServerService SupportServerService; + private readonly IdentityService IdentityService; + private readonly MessageService MessageService; + + public EventHandler OnNewMessage; + + private User Self; + + public SupportClientService( + SupportServerService supportServerService, + IdentityService identityService, + MessageService messageService) + { + SupportServerService = supportServerService; + IdentityService = identityService; + MessageService = messageService; + } + + public async Task Start() + { + Self = (await IdentityService.Get())!; + + MessageService.Subscribe( + $"support.{Self.Id}.message", + this, + message => + { + OnNewMessage?.Invoke(this, message); + + return Task.CompletedTask; + }); + } + + public async Task GetMessages() + { + return await SupportServerService.GetMessages(Self); + } + + public async Task SendMessage(string content) + { + var message = new SupportMessage() + { + Message = content + }; + + await SupportServerService.SendMessage( + Self, + message, + Self + ); + } + + public void Dispose() + { + MessageService.Unsubscribe($"support.{Self.Id}.message", this); + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/Support/SupportServerService.cs b/Moonlight/App/Services/Support/SupportServerService.cs new file mode 100644 index 0000000..661bb43 --- /dev/null +++ b/Moonlight/App/Services/Support/SupportServerService.cs @@ -0,0 +1,104 @@ +using Logging.Net; +using Microsoft.EntityFrameworkCore; +using Moonlight.App.Database.Entities; +using Moonlight.App.Repositories; + +namespace Moonlight.App.Services.Support; + +public class SupportServerService : IDisposable +{ + private SupportMessageRepository SupportMessageRepository; + private MessageService MessageService; + private UserRepository UserRepository; + private readonly IServiceScopeFactory ServiceScopeFactory; + private IServiceScope ServiceScope; + + public SupportServerService(IServiceScopeFactory serviceScopeFactory) + { + ServiceScopeFactory = serviceScopeFactory; + + Task.Run(Run); + } + + public async Task SendMessage(User r, SupportMessage message, User s, bool isSupport = false) + { + var recipient = UserRepository.Get().First(x => x.Id == r.Id); + var sender = UserRepository.Get().First(x => x.Id == s.Id); + + Task.Run(async () => + { + message.CreatedAt = DateTime.UtcNow; + message.Sender = sender; + message.Recipient = recipient; + message.IsSupport = isSupport; + + SupportMessageRepository.Add(message); + + await MessageService.Emit($"support.{recipient.Id}.message", message); + + if (!recipient.SupportPending) + { + recipient.SupportPending = true; + UserRepository.Update(recipient); + + var systemMessage = new SupportMessage() + { + Recipient = recipient, + Sender = null, + IsSystem = true, + Message = "The support team has been notified. Please be patient" + }; + + SupportMessageRepository.Add(systemMessage); + + await MessageService.Emit($"support.{recipient.Id}.message", systemMessage); + + Logger.Info("Support ticket created: " + recipient.Id); + //TODO: Ping or so + } + }); + } + + public Task GetMessages(User r) + { + var recipient = UserRepository.Get().First(x => x.Id == r.Id); + + var messages = SupportMessageRepository + .Get() + .Include(x => x.Recipient) + .Include(x => x.Sender) + .Where(x => x.Recipient.Id == recipient.Id) + .AsEnumerable() + .TakeLast(50) + .OrderBy(x => x.Id) + .ToArray(); + + return Task.FromResult(messages); + } + + private Task Run() + { + ServiceScope = ServiceScopeFactory.CreateScope(); + + SupportMessageRepository = ServiceScope + .ServiceProvider + .GetRequiredService(); + + MessageService = ServiceScope + .ServiceProvider + .GetRequiredService(); + + UserRepository = ServiceScope + .ServiceProvider + .GetRequiredService(); + + return Task.CompletedTask; + } + + public void Dispose() + { + SupportMessageRepository.Dispose(); + UserRepository.Dispose(); + ServiceScope.Dispose(); + } +} \ No newline at end of file diff --git a/Moonlight/Moonlight.csproj b/Moonlight/Moonlight.csproj index 5e319e8..4b91965 100644 --- a/Moonlight/Moonlight.csproj +++ b/Moonlight/Moonlight.csproj @@ -60,6 +60,7 @@ + diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index 1f85523..8a1e08f 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -8,6 +8,7 @@ using Moonlight.App.Repositories.Servers; using Moonlight.App.Services; using Moonlight.App.Services.Interop; using Moonlight.App.Services.Sessions; +using Moonlight.App.Services.Support; namespace Moonlight { @@ -36,6 +37,7 @@ namespace Moonlight builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); // Services builder.Services.AddSingleton(); @@ -53,10 +55,16 @@ namespace Moonlight builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddScoped(); + builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); + // Support + builder.Services.AddSingleton(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + // Helpers builder.Services.AddSingleton(); builder.Services.AddScoped(); @@ -88,6 +96,9 @@ namespace Moonlight app.MapBlazorHub(); app.MapFallbackToPage("/_Host"); + + // Support service + var supportServerService = app.Services.GetRequiredService(); app.Run(); } diff --git a/Moonlight/Shared/Components/Partials/Sidebar.razor b/Moonlight/Shared/Components/Partials/Sidebar.razor index b572ce8..d20821a 100644 --- a/Moonlight/Shared/Components/Partials/Sidebar.razor +++ b/Moonlight/Shared/Components/Partials/Sidebar.razor @@ -37,8 +37,7 @@ diff --git a/Moonlight/Shared/Layouts/MainLayout.razor b/Moonlight/Shared/Layouts/MainLayout.razor index 60ef5b2..cf9ea07 100644 --- a/Moonlight/Shared/Layouts/MainLayout.razor +++ b/Moonlight/Shared/Layouts/MainLayout.razor @@ -5,6 +5,8 @@ @using Moonlight.App.Database.Entities @using Moonlight.App.Extensions @using Moonlight.App.Models.Misc +@using Moonlight.App.Services +@using Moonlight.App.Services.Interop @using Moonlight.App.Services.Sessions @layout ThemeInit @@ -15,6 +17,8 @@ @inject IdentityService IdentityService @inject SessionService SessionService @inject NavigationManager NavigationManager +@inject MessageService MessageService +@inject ToastService ToastService @{ @@ -151,10 +155,21 @@ await SessionService.Register(); NavigationManager.LocationChanged += (sender, args) => { SessionService.Refresh(); }; + + MessageService.Subscribe( + $"support.{User.Id}.message", + this, + async message => + { + if (!NavigationManager.Uri.EndsWith("/support") && (message.IsSupport || message.IsSystem)) + { + await ToastService.Info($"Support: {message.Message}"); + } + }); } catch (Exception) { - // ignored + // ignored } } } @@ -162,6 +177,11 @@ public void Dispose() { SessionService.Close(); + + if (User != null) + { + MessageService.Unsubscribe($"support.{User.Id}.message", this); + } } private void AddBodyAttribute(string attribute, string value) diff --git a/Moonlight/Shared/Views/Admin/Index.razor b/Moonlight/Shared/Views/Admin/Index.razor new file mode 100644 index 0000000..e4ccd49 --- /dev/null +++ b/Moonlight/Shared/Views/Admin/Index.razor @@ -0,0 +1,135 @@ +@page "/admin" +@using Moonlight.App.Repositories.Servers +@using Moonlight.App.Repositories + +@inject ServerRepository ServerRepository +@inject DatabaseRepository DatabaseRepository +@inject UserRepository UserRepository + + + + + + + +@code +{ + private int DatabaseCount = 0; + private int ServerCount = 0; + private int UserCount = 0; + + private Task Load(LazyLoader lazyLoader) + { + DatabaseCount = DatabaseRepository.Get().Count(); + ServerCount = ServerRepository.Get().Count(); + UserCount = UserRepository.Get().Count(); + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Views/Support.razor b/Moonlight/Shared/Views/Support.razor new file mode 100644 index 0000000..f1e4b81 --- /dev/null +++ b/Moonlight/Shared/Views/Support.razor @@ -0,0 +1,144 @@ +@page "/support" +@using Moonlight.App.Services +@using Moonlight.App.Services.Sessions +@using Moonlight.App.Database.Entities +@using Moonlight.App.Helpers +@using Moonlight.App.Services.Support + +@inject ResourceService ResourceService +@inject IdentityService IdentityService +@inject SupportClientService SupportClientService +@inject SmartTranslateService SmartTranslateService + + +
+
+
+
+ @foreach (var message in Messages) + { + if (message.IsSystem || message.IsSupport) + { +
+
+ + +
+ @(message.Message) +
+
+
+ } + else + { +
+
+
+
+ @(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService)) + + @(message.Sender!.FirstName) @(message.Sender!.LastName) + +
+
+ Avatar +
+
+ +
+ @(message.Message) +
+
+
+ } + } +
+
+
+
+ Logo +
+ +
+ +
+ Welcome to the support chat. Ask your question here and we will help you +
+
+
+
+
+ +
+
+
+ +@code +{ + private User User; + + private SupportMessage[] Messages; + private string Content = ""; + + private async Task Load(LazyLoader lazyLoader) + { + User = (await IdentityService.Get())!; + + await lazyLoader.SetText("Starting chat client"); + + SupportClientService.OnNewMessage += OnNewMessage; + + await SupportClientService.Start(); + + Messages = (await SupportClientService.GetMessages()).Reverse().ToArray(); + } + + private async void OnNewMessage(object? sender, SupportMessage e) + { + Messages = (await SupportClientService.GetMessages()).Reverse().ToArray(); + await InvokeAsync(StateHasChanged); + } + + private async Task Send() + { + await SupportClientService.SendMessage(Content); + Content = ""; + await InvokeAsync(StateHasChanged); + } +} \ No newline at end of file diff --git a/Moonlight/obj/Debug/net6.0/Moonlight.GeneratedMSBuildEditorConfig.editorconfig b/Moonlight/obj/Debug/net6.0/Moonlight.GeneratedMSBuildEditorConfig.editorconfig index 91ce93c..adcc83f 100644 --- a/Moonlight/obj/Debug/net6.0/Moonlight.GeneratedMSBuildEditorConfig.editorconfig +++ b/Moonlight/obj/Debug/net6.0/Moonlight.GeneratedMSBuildEditorConfig.editorconfig @@ -167,6 +167,10 @@ build_metadata.AdditionalFiles.CssScope = build_metadata.AdditionalFiles.TargetPath = U2hhcmVkXExheW91dHNcVGhlbWVJbml0LnJhem9y build_metadata.AdditionalFiles.CssScope = +[C:/Users/marce/source/repos/MoonlightPublic/Moonlight/Moonlight/Shared/Views/Admin/Index.razor] +build_metadata.AdditionalFiles.TargetPath = U2hhcmVkXFZpZXdzXEFkbWluXEluZGV4LnJhem9y +build_metadata.AdditionalFiles.CssScope = + [C:/Users/marce/source/repos/MoonlightPublic/Moonlight/Moonlight/Shared/Views/Admin/Nodes/Edit.razor] build_metadata.AdditionalFiles.TargetPath = U2hhcmVkXFZpZXdzXEFkbWluXE5vZGVzXEVkaXQucmF6b3I= build_metadata.AdditionalFiles.CssScope = @@ -223,6 +227,10 @@ build_metadata.AdditionalFiles.CssScope = build_metadata.AdditionalFiles.TargetPath = U2hhcmVkXFZpZXdzXFNldHVwXFVzZXJzLnJhem9y build_metadata.AdditionalFiles.CssScope = +[C:/Users/marce/source/repos/MoonlightPublic/Moonlight/Moonlight/Shared/Views/Support.razor] +build_metadata.AdditionalFiles.TargetPath = U2hhcmVkXFZpZXdzXFN1cHBvcnQucmF6b3I= +build_metadata.AdditionalFiles.CssScope = + [C:/Users/marce/source/repos/MoonlightPublic/Moonlight/Moonlight/_Imports.razor] build_metadata.AdditionalFiles.TargetPath = X0ltcG9ydHMucmF6b3I= build_metadata.AdditionalFiles.CssScope = diff --git a/Moonlight/resources/lang/de_de.lang b/Moonlight/resources/lang/de_de.lang index c1d7252..cea1c7d 100644 --- a/Moonlight/resources/lang/de_de.lang +++ b/Moonlight/resources/lang/de_de.lang @@ -193,3 +193,25 @@ Running;Running Loading backups;Loading backups Started backup creation;Started backup creation Backup is going to be created;Backup is going to be created +Rename;Rename +Move;Move +Archive;Archive +Unarchive;Unarchive +Download;Download +Starting download;Starting download +Backup successfully created;Backup successfully created +Restore;Restore +Copy url;Copy url +Backup deletion started;Backup deletion started +Backup successfully deleted;Backup successfully deleted +Primary;Primary +This feature is currently not available;This feature is currently not available +Send;Send +Sending;Sending +Welcome to the support chat. Ask your question here and we will help you;Welcome to the support chat. Ask your question here and we will help you + minutes ago; minutes ago +just now;just now +less than a minute ago;less than a minute ago +1 hour ago;1 hour ago +1 minute ago;1 minute ago +Failed;Failed