Added support chat. Added resource service. Added server backups

This commit is contained in:
Marcel Baumgartner 2023-02-21 21:22:40 +01:00
parent c3eadf9133
commit 0b6882d3f8
26 changed files with 2408 additions and 9 deletions

1
.gitignore vendored
View file

@ -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

View file

@ -26,6 +26,7 @@ public class DataContext : DbContext
public DbSet<LoadingMessage> LoadingMessages { get; set; }
public DbSet<AuditLogEntry> AuditLog { get; set; }
public DbSet<Entities.Database> Databases { get; set; }
public DbSet<SupportMessage> SupportMessages { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{

View file

@ -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;
}

View file

@ -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;
}

View file

@ -0,0 +1,617 @@
// <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("20230221173242_AddSupportMessage")]
partial class AddSupportMessage
{
/// <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.AuditLogEntry", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
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.Database", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("AaPanelId")
.HasColumnType("int");
b.Property<int>("OwnerId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("OwnerId");
b.ToTable("Databases");
});
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.Image", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.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<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<int?>("ImageId")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("ImageId");
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.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.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<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.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<string>("Message")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("SenderId")
.HasColumnType("int");
b.Property<int>("Type")
.HasColumnType("int");
b.HasKey("Id");
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<string>("DiscordDiscriminator")
.IsRequired()
.HasColumnType("longtext");
b.Property<long>("DiscordId")
.HasColumnType("bigint");
b.Property<string>("DiscordUsername")
.IsRequired()
.HasColumnType("longtext");
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<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.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
}
}
}

View file

@ -0,0 +1,55 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class AddSupportMessage : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "SupportMessages",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Message = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
SenderId = table.Column<int>(type: "int", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsQuestion = table.Column<bool>(type: "tinyint(1)", nullable: false),
Type = table.Column<int>(type: "int", nullable: false),
Answer = table.Column<string>(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");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "SupportMessages");
}
}
}

View file

@ -374,6 +374,50 @@ namespace Moonlight.App.Database.Migrations
b.ToTable("ServerVariables");
});
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")
@ -432,6 +476,9 @@ namespace Moonlight.App.Database.Migrations
b.Property<int>("Status")
.HasColumnType("int");
b.Property<bool>("SupportPending")
.HasColumnType("tinyint(1)");
b.Property<DateTime>("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");

View file

@ -0,0 +1,635 @@
// <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.Databse.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("20230221183730_UpdatedSupportAndUserModel")]
partial class UpdatedSupportAndUserModel
{
/// <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.AuditLogEntry", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
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.Database", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("AaPanelId")
.HasColumnType("int");
b.Property<int>("OwnerId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("OwnerId");
b.ToTable("Databases");
});
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.Image", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.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<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<int?>("ImageId")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("ImageId");
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.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.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<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.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<string>("DiscordDiscriminator")
.IsRequired()
.HasColumnType("longtext");
b.Property<long>("DiscordId")
.HasColumnType("bigint");
b.Property<string>("DiscordUsername")
.IsRequired()
.HasColumnType("longtext");
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<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.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
}
}
}

View file

@ -0,0 +1,122 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Databse.Migrations
{
/// <inheritdoc />
public partial class UpdatedSupportAndUserModel : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_SupportMessages_Users_SenderId",
table: "SupportMessages");
migrationBuilder.AddColumn<bool>(
name: "SupportPending",
table: "Users",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
migrationBuilder.AlterColumn<int>(
name: "SenderId",
table: "SupportMessages",
type: "int",
nullable: true,
oldClrType: typeof(int),
oldType: "int");
migrationBuilder.AddColumn<bool>(
name: "IsSupport",
table: "SupportMessages",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "IsSystem",
table: "SupportMessages",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<int>(
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");
}
/// <inheritdoc />
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<int>(
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);
}
}
}

View file

@ -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"));
}
}

View file

@ -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<string> Download(Guid uuid)
{
return "";
}
[HttpPost("{uuid}")]
public async Task<ActionResult> 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<ActionResult> 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();
}
}

View file

@ -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<ActionResult<SftpLoginResult>> 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);
}
}
}

View file

@ -0,0 +1,6 @@
namespace Moonlight.App.Models.Misc;
public enum QuestionType
{
ServerUrl
}

View file

@ -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<SupportMessage> 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();
}
}

View file

@ -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<string>("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}";
}
}

View file

@ -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<SupportMessage> 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<SupportClientService, SupportMessage>(
$"support.{Recipient.Id}.message",
this,
message =>
{
OnNewMessage?.Invoke(this, message);
return Task.CompletedTask;
});
}
public async Task<SupportMessage[]> 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);
}
}

View file

@ -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<SupportMessage> 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<SupportClientService, SupportMessage>(
$"support.{Self.Id}.message",
this,
message =>
{
OnNewMessage?.Invoke(this, message);
return Task.CompletedTask;
});
}
public async Task<SupportMessage[]> 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);
}
}

View file

@ -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<SupportMessage[]> 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<SupportMessageRepository>();
MessageService = ServiceScope
.ServiceProvider
.GetRequiredService<MessageService>();
UserRepository = ServiceScope
.ServiceProvider
.GetRequiredService<UserRepository>();
return Task.CompletedTask;
}
public void Dispose()
{
SupportMessageRepository.Dispose();
UserRepository.Dispose();
ServiceScope.Dispose();
}
}

View file

@ -60,6 +60,7 @@
<Folder Include="App\Http\Middleware" />
<Folder Include="App\Models\AuditLogData" />
<Folder Include="resources\lang" />
<Folder Include="Shared\Views\Admin\Support" />
<Folder Include="wwwroot\assets\media" />
</ItemGroup>

View file

@ -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<AuditLogRepository>();
builder.Services.AddScoped<DatabaseRepository>();
builder.Services.AddScoped<ImageRepository>();
builder.Services.AddScoped<SupportMessageRepository>();
// Services
builder.Services.AddSingleton<ConfigService>();
@ -53,10 +55,16 @@ namespace Moonlight
builder.Services.AddScoped<ServerService>();
builder.Services.AddSingleton<PaperService>();
builder.Services.AddScoped<ClipboardService>();
builder.Services.AddSingleton<ResourceService>();
builder.Services.AddScoped<AuditLogService>();
builder.Services.AddScoped<SystemAuditLogService>();
// Support
builder.Services.AddSingleton<SupportServerService>();
builder.Services.AddScoped<SupportAdminServer>();
builder.Services.AddScoped<SupportClientService>();
// Helpers
builder.Services.AddSingleton<SmartTranslateHelper>();
builder.Services.AddScoped<WingsApiHelper>();
@ -88,6 +96,9 @@ namespace Moonlight
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
// Support service
var supportServerService = app.Services.GetRequiredService<SupportServerService>();
app.Run();
}

View file

@ -37,8 +37,7 @@
<SidebarMenu></SidebarMenu>
<div class="app-sidebar-footer flex-column-auto pt-2 pb-6 px-6" id="kt_app_sidebar_footer">
<a id="support_ticket_toggle_sidebar"
class="btn btn-flex flex-center btn-custom btn-primary overflow-hidden text-nowrap px-0 h-40px w-100 btn-label">
<a href="/support" class="btn btn-flex flex-center btn-custom btn-primary overflow-hidden text-nowrap px-0 h-40px w-100 btn-label">
<TL>Open support</TL>
</a>
</div>

View file

@ -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
<GlobalErrorBoundary>
@{
@ -151,10 +155,21 @@
await SessionService.Register();
NavigationManager.LocationChanged += (sender, args) => { SessionService.Refresh(); };
MessageService.Subscribe<MainLayout, SupportMessage>(
$"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)

View file

@ -0,0 +1,135 @@
@page "/admin"
@using Moonlight.App.Repositories.Servers
@using Moonlight.App.Repositories
@inject ServerRepository ServerRepository
@inject DatabaseRepository DatabaseRepository
@inject UserRepository UserRepository
<OnlyAdmin>
<LazyLoader Load="Load">
<div class="row mb-5">
<div class="col-12 col-lg-6 col-xl">
<a class="mt-4 card" href="/servers">
<div class="card-body">
<div class="row align-items-center gx-0">
<div class="col">
<h6 class="text-uppercase text-muted mb-2">
<TL>Servers</TL>
</h6>
<span class="h2 mb-0">
@(ServerCount)
</span>
</div>
<div class="col-auto">
<span class="h2 text-muted mb-0">
<i class="text-primary bx bx-server bx-lg"></i>
</span>
</div>
</div>
</div>
</a>
</div>
<div class="col-12 col-lg-6 col-xl">
<a class="mt-4 card" href="/websites">
<div class="card-body">
<div class="row align-items-center gx-0">
<div class="col">
<h6 class="text-uppercase text-muted mb-2">
<TL>Websites</TL>
</h6>
<span class="h2 mb-0">
0
</span>
</div>
<div class="col-auto">
<span class="h2 text-muted mb-0">
<i class="text-primary bx bx-globe bx-lg"></i>
</span>
</div>
</div>
</div>
</a>
</div>
<div class="col-12 col-lg-6 col-xl">
<div class="mt-4 card" href="/databases">
<div class="card-body">
<div class="row align-items-center gx-0">
<div class="col">
<h6 class="text-uppercase text-muted mb-2">
<TL>Databases</TL>
</h6>
<span class="h2 mb-0">
@(DatabaseCount)
</span>
</div>
<div class="col-auto">
<span class="h2 text-muted mb-0">
<i class="text-primary bx bx-data bx-lg"></i>
</span>
</div>
</div>
</div>
</div>
</div>
<div class="col-12 col-lg-6 col-xl">
<a class="mt-4 card" href="/domains">
<div class="card-body">
<div class="row align-items-center gx-0">
<div class="col">
<h6 class="text-uppercase text-muted mb-2">
<TL>Domains</TL>
</h6>
<span class="h2 mb-0">
0
</span>
</div>
<div class="col-auto">
<span class="h2 text-muted mb-">
<i class="text-primary bx bx-purchase-tag bx-lg"></i>
</span>
</div>
</div>
</div>
</a>
</div>
<div class="col-12 col-lg-6 col-xl">
<a class="mt-4 card" href="/domains">
<div class="card-body">
<div class="row align-items-center gx-0">
<div class="col">
<h6 class="text-uppercase text-muted mb-2">
<TL>Users</TL>
</h6>
<span class="h2 mb-0">
@(UserCount)
</span>
</div>
<div class="col-auto">
<span class="h2 text-muted mb-">
<i class="text-primary bx bx-user bx-lg"></i>
</span>
</div>
</div>
</div>
</a>
</div>
</div>
</LazyLoader>
</OnlyAdmin>
@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;
}
}

View file

@ -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
<LazyLoader Load="Load">
<div class="row">
<div class="card">
<div class="card-body">
<div class="scroll-y me-n5 pe-5" style="max-height: 65vh; display: flex; flex-direction: column-reverse;">
@foreach (var message in Messages)
{
if (message.IsSystem || message.IsSupport)
{
<div class="d-flex justify-content-start mb-10 ">
<div class="d-flex flex-column align-items-start">
<div class="d-flex align-items-center mb-2">
<div class="symbol symbol-35px symbol-circle ">
<img alt="Logo" src="@(ResourceService.Image("logo.svg"))">
</div>
<div class="ms-3">
<a class="fs-5 fw-bold text-gray-900 text-hover-primary me-1">
@if (message.IsSupport && !message.IsSystem)
{
<span>@(message.Sender!.FirstName) @(message.Sender!.LastName)</span>
}
else
{
<span>System</span>
}
</a>
<span class="text-muted fs-7 mb-1">@(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService))</span>
</div>
</div>
<div class="p-5 rounded bg-light-info text-dark fw-semibold mw-lg-400px text-start">
@(message.Message)
</div>
</div>
</div>
}
else
{
<div class="d-flex justify-content-end mb-10 ">
<div class="d-flex flex-column align-items-end">
<div class="d-flex align-items-center mb-2">
<div class="me-3">
<span class="text-muted fs-7 mb-1">@(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService))</span>
<a class="fs-5 fw-bold text-gray-900 text-hover-primary ms-1">
<span>@(message.Sender!.FirstName) @(message.Sender!.LastName)</span>
</a>
</div>
<div class="symbol symbol-35px symbol-circle ">
<img alt="Avatar" src="@(ResourceService.Avatar(message.Sender))">
</div>
</div>
<div class="p-5 rounded bg-light-primary text-dark fw-semibold mw-lg-400px text-end">
@(message.Message)
</div>
</div>
</div>
}
}
<div class="d-flex justify-content-start mb-10 ">
<div class="d-flex flex-column align-items-start">
<div class="d-flex align-items-center mb-2">
<div class="symbol symbol-35px symbol-circle ">
<img alt="Logo" src="@(ResourceService.Image("logo.svg"))">
</div>
<div class="ms-3">
<a class="fs-5 fw-bold text-gray-900 text-hover-primary me-1">
<span>System</span>
</a>
</div>
</div>
<div class="p-5 rounded bg-light-info text-dark fw-semibold mw-lg-400px text-start">
<TL>Welcome to the support chat. Ask your question here and we will help you</TL>
</div>
</div>
</div>
</div>
</div>
<div class="card-footer">
<textarea @bind="Content" class="form-control form-control-flush mb-3" rows="1" placeholder="Type a message">
</textarea>
<div class="d-flex flex-stack">
<div class="d-flex align-items-center me-2">
<button class="btn btn-sm btn-icon btn-active-light-primary me-1" type="button">
<i class="bx bx-upload fs-3"></i>
</button>
</div>
<WButton Text="@(SmartTranslateService.Translate("Send"))"
WorkingText="@(SmartTranslateService.Translate("Sending"))"
CssClasses="btn-primary"
OnClick="Send">
</WButton>
</div>
</div>
</div>
</div>
</LazyLoader>
@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);
}
}

View file

@ -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 =

View file

@ -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