From de45ff40d84e2dfe65c9974ad4a8092679ae1ec7 Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Fri, 4 Aug 2023 14:00:25 +0200 Subject: [PATCH] Implemented a basic ticket system --- Moonlight/App/Configuration/ConfigV1.cs | 9 + Moonlight/App/Database/DataContext.cs | 3 + Moonlight/App/Database/Entities/Ticket.cs | 19 + .../App/Database/Entities/TicketMessage.cs | 14 + ...30803012947_AddNewTicketModels.Designer.cs | 1233 +++++++++++++++++ .../20230803012947_AddNewTicketModels.cs | 117 ++ .../Migrations/DataContextModelSnapshot.cs | 126 ++ .../App/Models/Forms/CreateTicketDataModel.cs | 21 + Moonlight/App/Models/Misc/TicketPriority.cs | 9 + Moonlight/App/Models/Misc/TicketStatus.cs | 9 + Moonlight/App/Models/Misc/TicketSubject.cs | 9 + .../Services/Tickets/TicketAdminService.cs | 95 ++ .../Services/Tickets/TicketClientService.cs | 68 + .../Services/Tickets/TicketServerService.cs | 249 ++++ Moonlight/Program.cs | 8 +- .../Tickets/TicketMessageView.razor | 219 +++ .../Shared/Views/Admin/Support/Index.razor | 420 +++++- .../Shared/Views/Admin/Support/View.razor | 313 ----- Moonlight/Shared/Views/Support.razor | 275 ---- Moonlight/Shared/Views/Support/Index.razor | 468 +++++++ 20 files changed, 3021 insertions(+), 663 deletions(-) create mode 100644 Moonlight/App/Database/Entities/Ticket.cs create mode 100644 Moonlight/App/Database/Entities/TicketMessage.cs create mode 100644 Moonlight/App/Database/Migrations/20230803012947_AddNewTicketModels.Designer.cs create mode 100644 Moonlight/App/Database/Migrations/20230803012947_AddNewTicketModels.cs create mode 100644 Moonlight/App/Models/Forms/CreateTicketDataModel.cs create mode 100644 Moonlight/App/Models/Misc/TicketPriority.cs create mode 100644 Moonlight/App/Models/Misc/TicketStatus.cs create mode 100644 Moonlight/App/Models/Misc/TicketSubject.cs create mode 100644 Moonlight/App/Services/Tickets/TicketAdminService.cs create mode 100644 Moonlight/App/Services/Tickets/TicketClientService.cs create mode 100644 Moonlight/App/Services/Tickets/TicketServerService.cs create mode 100644 Moonlight/Shared/Components/Tickets/TicketMessageView.razor delete mode 100644 Moonlight/Shared/Views/Admin/Support/View.razor delete mode 100644 Moonlight/Shared/Views/Support.razor create mode 100644 Moonlight/Shared/Views/Support/Index.razor diff --git a/Moonlight/App/Configuration/ConfigV1.cs b/Moonlight/App/Configuration/ConfigV1.cs index 11ce0d24..b6709c8b 100644 --- a/Moonlight/App/Configuration/ConfigV1.cs +++ b/Moonlight/App/Configuration/ConfigV1.cs @@ -59,6 +59,15 @@ public class ConfigV1 [JsonProperty("Sentry")] public SentryData Sentry { get; set; } = new(); [JsonProperty("Stripe")] public StripeData Stripe { get; set; } = new(); + + [JsonProperty("Tickets")] public TicketsData Tickets { get; set; } = new(); + } + + public class TicketsData + { + [JsonProperty("WelcomeMessage")] + [Description("The message that will be sent when a user created a ticket")] + public string WelcomeMessage { get; set; } = "Welcome to the support"; } public class StripeData diff --git a/Moonlight/App/Database/DataContext.cs b/Moonlight/App/Database/DataContext.cs index 58d58f6b..d872f3e5 100644 --- a/Moonlight/App/Database/DataContext.cs +++ b/Moonlight/App/Database/DataContext.cs @@ -44,6 +44,9 @@ public class DataContext : DbContext public DbSet SecurityLogs { get; set; } public DbSet BlocklistIps { get; set; } public DbSet WhitelistIps { get; set; } + + public DbSet Tickets { get; set; } + public DbSet TicketMessages { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { diff --git a/Moonlight/App/Database/Entities/Ticket.cs b/Moonlight/App/Database/Entities/Ticket.cs new file mode 100644 index 00000000..2a90d917 --- /dev/null +++ b/Moonlight/App/Database/Entities/Ticket.cs @@ -0,0 +1,19 @@ +using Moonlight.App.Models.Misc; + +namespace Moonlight.App.Database.Entities; + +public class Ticket +{ + public int Id { get; set; } + public string IssueTopic { get; set; } = ""; + public string IssueDescription { get; set; } = ""; + public string IssueTries { get; set; } = ""; + public User CreatedBy { get; set; } + public User? AssignedTo { get; set; } + public TicketPriority Priority { get; set; } + public TicketStatus Status { get; set; } + public TicketSubject Subject { get; set; } + public int SubjectId { get; set; } + public List Messages { get; set; } = new(); + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; +} \ No newline at end of file diff --git a/Moonlight/App/Database/Entities/TicketMessage.cs b/Moonlight/App/Database/Entities/TicketMessage.cs new file mode 100644 index 00000000..940631bc --- /dev/null +++ b/Moonlight/App/Database/Entities/TicketMessage.cs @@ -0,0 +1,14 @@ +namespace Moonlight.App.Database.Entities; + +public class TicketMessage +{ + public int Id { get; set; } + public string Content { get; set; } = ""; + public string? AttachmentUrl { get; set; } + public User? Sender { get; set; } + public bool IsSystemMessage { get; set; } + public bool IsEdited { get; set; } + public bool IsSupportMessage { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; +} \ No newline at end of file diff --git a/Moonlight/App/Database/Migrations/20230803012947_AddNewTicketModels.Designer.cs b/Moonlight/App/Database/Migrations/20230803012947_AddNewTicketModels.Designer.cs new file mode 100644 index 00000000..90562359 --- /dev/null +++ b/Moonlight/App/Database/Migrations/20230803012947_AddNewTicketModels.Designer.cs @@ -0,0 +1,1233 @@ +// +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("20230803012947_AddNewTicketModels")] + partial class AddNewTicketModels + { + /// + 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.BlocklistIp", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("Ip") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Packets") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("BlocklistIps"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.CloudPanel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ApiKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ApiUrl") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Host") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("CloudPanels"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.DdosAttack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("bigint"); + + b.Property("Ip") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("NodeId") + .HasColumnType("int"); + + b.Property("Ongoing") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("NodeId"); + + b.ToTable("DdosAttacks"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.DockerImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Default") + .HasColumnType("tinyint(1)"); + + b.Property("ImageId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ImageId"); + + b.ToTable("DockerImages"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Domain", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OwnerId") + .HasColumnType("int"); + + b.Property("SharedDomainId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("SharedDomainId"); + + b.ToTable("Domains"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Image", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Allocations") + .HasColumnType("int"); + + b.Property("BackgroundImageUrl") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ConfigFiles") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("InstallDockerImage") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("InstallEntrypoint") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("InstallScript") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Startup") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StartupDetection") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StopCommand") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TagsJson") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Uuid") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ImageVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("DefaultValue") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ImageId") + .HasColumnType("int"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ImageId"); + + b.ToTable("ImageVariables"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.IpBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Ip") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("IpBans"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.LoadingMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Message") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("LoadingMessages"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.MySqlDatabase", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Password") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("WebSpaceId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("WebSpaceId"); + + b.ToTable("Databases"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.NewsEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("Markdown") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Title") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("NewsEntries"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Node", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Fqdn") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("HttpPort") + .HasColumnType("int"); + + b.Property("MoonlightDaemonPort") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("SftpPort") + .HasColumnType("int"); + + b.Property("Ssl") + .HasColumnType("tinyint(1)"); + + b.Property("Token") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TokenId") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Nodes"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.NodeAllocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("NodeId") + .HasColumnType("int"); + + b.Property("Port") + .HasColumnType("int"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("NodeId"); + + b.HasIndex("ServerId"); + + b.ToTable("NodeAllocations"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Notification.NotificationAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Action") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("NotificationClientId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("NotificationClientId"); + + b.ToTable("NotificationActions"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Notification.NotificationClient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("NotificationClients"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.PermissionGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Permissions") + .IsRequired() + .HasColumnType("longblob"); + + b.HasKey("Id"); + + b.ToTable("PermissionGroups"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Revoke", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Identifier") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Revokes"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.SecurityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Text") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("SecurityLogs"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Server", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ArchiveId") + .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("IsArchived") + .HasColumnType("tinyint(1)"); + + b.Property("IsCleanupException") + .HasColumnType("tinyint(1)"); + + b.Property("MainAllocationId") + .HasColumnType("int"); + + b.Property("Memory") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("NodeId") + .HasColumnType("int"); + + b.Property("OverrideStartup") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OwnerId") + .HasColumnType("int"); + + b.Property("Suspended") + .HasColumnType("tinyint(1)"); + + b.Property("Uuid") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ArchiveId"); + + b.HasIndex("ImageId"); + + b.HasIndex("MainAllocationId"); + + b.HasIndex("NodeId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Servers"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ServerBackup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Bytes") + .HasColumnType("bigint"); + + b.Property("Created") + .HasColumnType("tinyint(1)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("Uuid") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.ToTable("ServerBackups"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ServerVariable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerId") + .HasColumnType("int"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ServerId"); + + b.ToTable("ServerVariables"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.SharedDomain", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CloudflareId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("SharedDomains"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.StatisticsData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Chart") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("double"); + + b.HasKey("Id"); + + b.ToTable("Statistics"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Subscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Currency") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Duration") + .HasColumnType("int"); + + b.Property("LimitsJson") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Price") + .HasColumnType("double"); + + b.Property("StripePriceId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StripeProductId") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Subscriptions"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.SupportChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Answer") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Attachment") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Content") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsQuestion") + .HasColumnType("tinyint(1)"); + + b.Property("QuestionType") + .HasColumnType("int"); + + b.Property("RecipientId") + .HasColumnType("int"); + + b.Property("SenderId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("RecipientId"); + + b.HasIndex("SenderId"); + + b.ToTable("SupportChatMessages"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AssignedToId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("IssueDescription") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IssueTopic") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IssueTries") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Priority") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Subject") + .HasColumnType("int"); + + b.Property("SubjectId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AssignedToId"); + + b.HasIndex("CreatedById"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.TicketMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AttachmentUrl") + .HasColumnType("longtext"); + + b.Property("Content") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsEdited") + .HasColumnType("tinyint(1)"); + + b.Property("IsSupportMessage") + .HasColumnType("tinyint(1)"); + + b.Property("IsSystemMessage") + .HasColumnType("tinyint(1)"); + + b.Property("SenderId") + .HasColumnType("int"); + + b.Property("TicketId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("SenderId"); + + b.HasIndex("TicketId"); + + b.ToTable("TicketMessages"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Address") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Admin") + .HasColumnType("tinyint(1)"); + + b.Property("City") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Country") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CurrentSubscriptionId") + .HasColumnType("int"); + + b.Property("DiscordId") + .HasColumnType("bigint unsigned"); + + b.Property("Email") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("HasRated") + .HasColumnType("tinyint(1)"); + + b.Property("LastIp") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LastVisitedAt") + .HasColumnType("datetime(6)"); + + b.Property("Password") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PermissionGroupId") + .HasColumnType("int"); + + b.Property("Permissions") + .IsRequired() + .HasColumnType("longblob"); + + b.Property("Rating") + .HasColumnType("int"); + + b.Property("RegisterIp") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ServerListLayoutJson") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("State") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StreamerMode") + .HasColumnType("tinyint(1)"); + + b.Property("SubscriptionExpires") + .HasColumnType("datetime(6)"); + + b.Property("SubscriptionSince") + .HasColumnType("datetime(6)"); + + b.Property("SupportPending") + .HasColumnType("tinyint(1)"); + + b.Property("TokenValidTime") + .HasColumnType("datetime(6)"); + + b.Property("TotpEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("TotpSecret") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CurrentSubscriptionId"); + + b.HasIndex("PermissionGroupId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.WebSpace", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CloudPanelId") + .HasColumnType("int"); + + b.Property("Domain") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OwnerId") + .HasColumnType("int"); + + b.Property("Password") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VHostTemplate") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("CloudPanelId"); + + b.HasIndex("OwnerId"); + + b.ToTable("WebSpaces"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.WhitelistIp", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Ip") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("WhitelistIps"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.DdosAttack", b => + { + b.HasOne("Moonlight.App.Database.Entities.Node", "Node") + .WithMany() + .HasForeignKey("NodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Node"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.DockerImage", b => + { + b.HasOne("Moonlight.App.Database.Entities.Image", null) + .WithMany("DockerImages") + .HasForeignKey("ImageId"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Domain", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.SharedDomain", "SharedDomain") + .WithMany() + .HasForeignKey("SharedDomainId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("SharedDomain"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ImageVariable", b => + { + b.HasOne("Moonlight.App.Database.Entities.Image", null) + .WithMany("Variables") + .HasForeignKey("ImageId"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.MySqlDatabase", b => + { + b.HasOne("Moonlight.App.Database.Entities.WebSpace", "WebSpace") + .WithMany("Databases") + .HasForeignKey("WebSpaceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("WebSpace"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.NodeAllocation", b => + { + b.HasOne("Moonlight.App.Database.Entities.Node", null) + .WithMany("Allocations") + .HasForeignKey("NodeId"); + + b.HasOne("Moonlight.App.Database.Entities.Server", null) + .WithMany("Allocations") + .HasForeignKey("ServerId"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Notification.NotificationAction", b => + { + b.HasOne("Moonlight.App.Database.Entities.Notification.NotificationClient", "NotificationClient") + .WithMany() + .HasForeignKey("NotificationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NotificationClient"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Notification.NotificationClient", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Server", b => + { + b.HasOne("Moonlight.App.Database.Entities.ServerBackup", "Archive") + .WithMany() + .HasForeignKey("ArchiveId"); + + b.HasOne("Moonlight.App.Database.Entities.Image", "Image") + .WithMany() + .HasForeignKey("ImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.NodeAllocation", "MainAllocation") + .WithMany() + .HasForeignKey("MainAllocationId"); + + b.HasOne("Moonlight.App.Database.Entities.Node", "Node") + .WithMany() + .HasForeignKey("NodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Archive"); + + b.Navigation("Image"); + + b.Navigation("MainAllocation"); + + b.Navigation("Node"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ServerBackup", b => + { + b.HasOne("Moonlight.App.Database.Entities.Server", null) + .WithMany("Backups") + .HasForeignKey("ServerId"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.ServerVariable", b => + { + b.HasOne("Moonlight.App.Database.Entities.Server", null) + .WithMany("Variables") + .HasForeignKey("ServerId"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.SupportChatMessage", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Recipient") + .WithMany() + .HasForeignKey("RecipientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.User", "Sender") + .WithMany() + .HasForeignKey("SenderId"); + + b.Navigation("Recipient"); + + b.Navigation("Sender"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Ticket", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "AssignedTo") + .WithMany() + .HasForeignKey("AssignedToId"); + + b.HasOne("Moonlight.App.Database.Entities.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AssignedTo"); + + b.Navigation("CreatedBy"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.TicketMessage", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Sender") + .WithMany() + .HasForeignKey("SenderId"); + + b.HasOne("Moonlight.App.Database.Entities.Ticket", null) + .WithMany("Messages") + .HasForeignKey("TicketId"); + + b.Navigation("Sender"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.User", b => + { + b.HasOne("Moonlight.App.Database.Entities.Subscription", "CurrentSubscription") + .WithMany() + .HasForeignKey("CurrentSubscriptionId"); + + b.HasOne("Moonlight.App.Database.Entities.PermissionGroup", "PermissionGroup") + .WithMany() + .HasForeignKey("PermissionGroupId"); + + b.Navigation("CurrentSubscription"); + + b.Navigation("PermissionGroup"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.WebSpace", b => + { + b.HasOne("Moonlight.App.Database.Entities.CloudPanel", "CloudPanel") + .WithMany() + .HasForeignKey("CloudPanelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CloudPanel"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Image", b => + { + b.Navigation("DockerImages"); + + b.Navigation("Variables"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Node", b => + { + b.Navigation("Allocations"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Server", b => + { + b.Navigation("Allocations"); + + b.Navigation("Backups"); + + b.Navigation("Variables"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Ticket", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.WebSpace", b => + { + b.Navigation("Databases"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Moonlight/App/Database/Migrations/20230803012947_AddNewTicketModels.cs b/Moonlight/App/Database/Migrations/20230803012947_AddNewTicketModels.cs new file mode 100644 index 00000000..abbd006c --- /dev/null +++ b/Moonlight/App/Database/Migrations/20230803012947_AddNewTicketModels.cs @@ -0,0 +1,117 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Moonlight.App.Database.Migrations +{ + /// + public partial class AddNewTicketModels : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Tickets", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + IssueTopic = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + IssueDescription = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + IssueTries = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + CreatedById = table.Column(type: "int", nullable: false), + AssignedToId = table.Column(type: "int", nullable: true), + Priority = table.Column(type: "int", nullable: false), + Status = table.Column(type: "int", nullable: false), + Subject = table.Column(type: "int", nullable: false), + SubjectId = table.Column(type: "int", nullable: false), + CreatedAt = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Tickets", x => x.Id); + table.ForeignKey( + name: "FK_Tickets_Users_AssignedToId", + column: x => x.AssignedToId, + principalTable: "Users", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Tickets_Users_CreatedById", + column: x => x.CreatedById, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "TicketMessages", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Content = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + AttachmentUrl = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + SenderId = table.Column(type: "int", nullable: true), + IsSystemMessage = table.Column(type: "tinyint(1)", nullable: false), + IsEdited = table.Column(type: "tinyint(1)", nullable: false), + IsSupportMessage = table.Column(type: "tinyint(1)", nullable: false), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + UpdatedAt = table.Column(type: "datetime(6)", nullable: false), + TicketId = table.Column(type: "int", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_TicketMessages", x => x.Id); + table.ForeignKey( + name: "FK_TicketMessages_Tickets_TicketId", + column: x => x.TicketId, + principalTable: "Tickets", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_TicketMessages_Users_SenderId", + column: x => x.SenderId, + principalTable: "Users", + principalColumn: "Id"); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_TicketMessages_SenderId", + table: "TicketMessages", + column: "SenderId"); + + migrationBuilder.CreateIndex( + name: "IX_TicketMessages_TicketId", + table: "TicketMessages", + column: "TicketId"); + + migrationBuilder.CreateIndex( + name: "IX_Tickets_AssignedToId", + table: "Tickets", + column: "AssignedToId"); + + migrationBuilder.CreateIndex( + name: "IX_Tickets_CreatedById", + table: "Tickets", + column: "CreatedById"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TicketMessages"); + + migrationBuilder.DropTable( + name: "Tickets"); + } + } +} diff --git a/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs b/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs index fc8fa18f..4a21928a 100644 --- a/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs +++ b/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs @@ -714,6 +714,97 @@ namespace Moonlight.App.Database.Migrations b.ToTable("SupportChatMessages"); }); + modelBuilder.Entity("Moonlight.App.Database.Entities.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AssignedToId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedById") + .HasColumnType("int"); + + b.Property("IssueDescription") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IssueTopic") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IssueTries") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Priority") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Subject") + .HasColumnType("int"); + + b.Property("SubjectId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AssignedToId"); + + b.HasIndex("CreatedById"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.TicketMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AttachmentUrl") + .HasColumnType("longtext"); + + b.Property("Content") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsEdited") + .HasColumnType("tinyint(1)"); + + b.Property("IsSupportMessage") + .HasColumnType("tinyint(1)"); + + b.Property("IsSystemMessage") + .HasColumnType("tinyint(1)"); + + b.Property("SenderId") + .HasColumnType("int"); + + b.Property("TicketId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("SenderId"); + + b.HasIndex("TicketId"); + + b.ToTable("TicketMessages"); + }); + modelBuilder.Entity("Moonlight.App.Database.Entities.User", b => { b.Property("Id") @@ -1039,6 +1130,36 @@ namespace Moonlight.App.Database.Migrations b.Navigation("Sender"); }); + modelBuilder.Entity("Moonlight.App.Database.Entities.Ticket", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "AssignedTo") + .WithMany() + .HasForeignKey("AssignedToId"); + + b.HasOne("Moonlight.App.Database.Entities.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AssignedTo"); + + b.Navigation("CreatedBy"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.TicketMessage", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Sender") + .WithMany() + .HasForeignKey("SenderId"); + + b.HasOne("Moonlight.App.Database.Entities.Ticket", null) + .WithMany("Messages") + .HasForeignKey("TicketId"); + + b.Navigation("Sender"); + }); + modelBuilder.Entity("Moonlight.App.Database.Entities.User", b => { b.HasOne("Moonlight.App.Database.Entities.Subscription", "CurrentSubscription") @@ -1094,6 +1215,11 @@ namespace Moonlight.App.Database.Migrations b.Navigation("Variables"); }); + modelBuilder.Entity("Moonlight.App.Database.Entities.Ticket", b => + { + b.Navigation("Messages"); + }); + modelBuilder.Entity("Moonlight.App.Database.Entities.WebSpace", b => { b.Navigation("Databases"); diff --git a/Moonlight/App/Models/Forms/CreateTicketDataModel.cs b/Moonlight/App/Models/Forms/CreateTicketDataModel.cs new file mode 100644 index 00000000..e733cc72 --- /dev/null +++ b/Moonlight/App/Models/Forms/CreateTicketDataModel.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using Moonlight.App.Models.Misc; + +namespace Moonlight.App.Models.Forms; + +public class CreateTicketDataModel +{ + [Required(ErrorMessage = "You need to specify a issue topic")] + [MinLength(5, ErrorMessage = "The issue topic needs to be longer than 5 characters")] + public string IssueTopic { get; set; } + + [Required(ErrorMessage = "You need to specify a issue description")] + [MinLength(10, ErrorMessage = "The issue description needs to be longer than 10 characters")] + public string IssueDescription { get; set; } + + [Required(ErrorMessage = "You need to specify your tries to solve this issue")] + public string IssueTries { get; set; } + + public TicketSubject Subject { get; set; } + public int SubjectId { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Models/Misc/TicketPriority.cs b/Moonlight/App/Models/Misc/TicketPriority.cs new file mode 100644 index 00000000..97fc5489 --- /dev/null +++ b/Moonlight/App/Models/Misc/TicketPriority.cs @@ -0,0 +1,9 @@ +namespace Moonlight.App.Models.Misc; + +public enum TicketPriority +{ + Low = 0, + Medium = 1, + High = 2, + Critical = 3 +} \ No newline at end of file diff --git a/Moonlight/App/Models/Misc/TicketStatus.cs b/Moonlight/App/Models/Misc/TicketStatus.cs new file mode 100644 index 00000000..2a59fae9 --- /dev/null +++ b/Moonlight/App/Models/Misc/TicketStatus.cs @@ -0,0 +1,9 @@ +namespace Moonlight.App.Models.Misc; + +public enum TicketStatus +{ + Closed = 0, + Open = 1, + WaitingForUser = 2, + Pending = 3 +} \ No newline at end of file diff --git a/Moonlight/App/Models/Misc/TicketSubject.cs b/Moonlight/App/Models/Misc/TicketSubject.cs new file mode 100644 index 00000000..ed6d4579 --- /dev/null +++ b/Moonlight/App/Models/Misc/TicketSubject.cs @@ -0,0 +1,9 @@ +namespace Moonlight.App.Models.Misc; + +public enum TicketSubject +{ + Webspace = 0, + Server = 1, + Domain = 2, + Other = 3 +} \ No newline at end of file diff --git a/Moonlight/App/Services/Tickets/TicketAdminService.cs b/Moonlight/App/Services/Tickets/TicketAdminService.cs new file mode 100644 index 00000000..bb797091 --- /dev/null +++ b/Moonlight/App/Services/Tickets/TicketAdminService.cs @@ -0,0 +1,95 @@ +using Microsoft.AspNetCore.Components.Forms; +using Moonlight.App.Database.Entities; +using Moonlight.App.Models.Misc; +using Moonlight.App.Services.Files; +using Moonlight.App.Services.Sessions; + +namespace Moonlight.App.Services.Tickets; + +public class TicketAdminService +{ + private readonly TicketServerService TicketServerService; + private readonly IdentityService IdentityService; + private readonly BucketService BucketService; + + public Ticket? Ticket { get; set; } + + public TicketAdminService( + TicketServerService ticketServerService, + IdentityService identityService, + BucketService bucketService) + { + TicketServerService = ticketServerService; + IdentityService = identityService; + BucketService = bucketService; + } + + public async Task> GetAssigned() + { + return await TicketServerService.GetUserAssignedTickets(IdentityService.User); + } + + public async Task> GetUnAssigned() + { + return await TicketServerService.GetUnAssignedTickets(); + } + + public async Task Create(string issueTopic, string issueDescription, string issueTries, + TicketSubject subject, int subjectId) + { + return await TicketServerService.Create( + IdentityService.User, + issueTopic, + issueDescription, + issueTries, + subject, + subjectId + ); + } + + public async Task Send(string content, IBrowserFile? file = null) + { + string? attachment = null; + + if (file != null) + { + attachment = await BucketService.StoreFile( + "tickets", + file.OpenReadStream(1024 * 1024 * 5), + file.Name); + } + + return await TicketServerService.SendMessage( + Ticket!, + IdentityService.User, + content, + attachment, + true + ); + } + + public async Task UpdateStatus(TicketStatus status) + { + await TicketServerService.UpdateStatus(Ticket!, status); + } + + public async Task UpdatePriority(TicketPriority priority) + { + await TicketServerService.UpdatePriority(Ticket!, priority); + } + + public async Task GetMessages() + { + return await TicketServerService.GetMessages(Ticket!); + } + + public async Task Claim() + { + await TicketServerService.Claim(Ticket!, IdentityService.User); + } + + public async Task UnClaim() + { + await TicketServerService.Claim(Ticket!); + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/Tickets/TicketClientService.cs b/Moonlight/App/Services/Tickets/TicketClientService.cs new file mode 100644 index 00000000..23043240 --- /dev/null +++ b/Moonlight/App/Services/Tickets/TicketClientService.cs @@ -0,0 +1,68 @@ +using Microsoft.AspNetCore.Components.Forms; +using Moonlight.App.Database.Entities; +using Moonlight.App.Models.Misc; +using Moonlight.App.Services.Files; +using Moonlight.App.Services.Sessions; + +namespace Moonlight.App.Services.Tickets; + +public class TicketClientService +{ + private readonly TicketServerService TicketServerService; + private readonly IdentityService IdentityService; + private readonly BucketService BucketService; + + public Ticket? Ticket { get; set; } + + public TicketClientService( + TicketServerService ticketServerService, + IdentityService identityService, + BucketService bucketService) + { + TicketServerService = ticketServerService; + IdentityService = identityService; + BucketService = bucketService; + } + + public async Task> Get() + { + return await TicketServerService.GetUserTickets(IdentityService.User); + } + + public async Task Create(string issueTopic, string issueDescription, string issueTries, TicketSubject subject, int subjectId) + { + return await TicketServerService.Create( + IdentityService.User, + issueTopic, + issueDescription, + issueTries, + subject, + subjectId + ); + } + + public async Task Send(string content, IBrowserFile? file = null) + { + string? attachment = null; + + if (file != null) + { + attachment = await BucketService.StoreFile( + "tickets", + file.OpenReadStream(1024 * 1024 * 5), + file.Name); + } + + return await TicketServerService.SendMessage( + Ticket!, + IdentityService.User, + content, + attachment + ); + } + + public async Task GetMessages() + { + return await TicketServerService.GetMessages(Ticket!); + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/Tickets/TicketServerService.cs b/Moonlight/App/Services/Tickets/TicketServerService.cs new file mode 100644 index 00000000..621e7d7c --- /dev/null +++ b/Moonlight/App/Services/Tickets/TicketServerService.cs @@ -0,0 +1,249 @@ +using Microsoft.EntityFrameworkCore; +using Moonlight.App.Database.Entities; +using Moonlight.App.Events; +using Moonlight.App.Models.Misc; +using Moonlight.App.Repositories; + +namespace Moonlight.App.Services.Tickets; + +public class TicketServerService +{ + private readonly IServiceScopeFactory ServiceScopeFactory; + private readonly EventSystem Event; + private readonly ConfigService ConfigService; + + public TicketServerService( + IServiceScopeFactory serviceScopeFactory, + EventSystem eventSystem, + ConfigService configService) + { + ServiceScopeFactory = serviceScopeFactory; + Event = eventSystem; + ConfigService = configService; + } + + public async Task Create(User creator, string issueTopic, string issueDescription, string issueTries, TicketSubject subject, int subjectId) + { + using var scope = ServiceScopeFactory.CreateScope(); + var ticketRepo = scope.ServiceProvider.GetRequiredService>(); + var userRepo = scope.ServiceProvider.GetRequiredService>(); + + var creatorUser = userRepo + .Get() + .First(x => x.Id == creator.Id); + + var ticket = ticketRepo.Add(new() + { + Priority = TicketPriority.Low, + Status = TicketStatus.Open, + AssignedTo = null, + IssueTopic = issueTopic, + IssueDescription = issueDescription, + IssueTries = issueTries, + Subject = subject, + SubjectId = subjectId, + CreatedBy = creatorUser + }); + + await Event.Emit("tickets.new", ticket); + + // Do automatic stuff here + await SendSystemMessage(ticket, ConfigService.Get().Moonlight.Tickets.WelcomeMessage); + //TODO: Check for opening times + + return ticket; + } + public async Task SendSystemMessage(Ticket t, string content, string? attachmentUrl = null) + { + using var scope = ServiceScopeFactory.CreateScope(); + var ticketRepo = scope.ServiceProvider.GetRequiredService>(); + + var ticket = ticketRepo.Get().First(x => x.Id == t.Id); + + var message = new TicketMessage() + { + Content = content, + Sender = null, + AttachmentUrl = attachmentUrl, + IsSystemMessage = true + }; + + ticket.Messages.Add(message); + ticketRepo.Update(ticket); + + await Event.Emit("tickets.message", message); + await Event.Emit($"tickets.{ticket.Id}.message", message); + } + public async Task UpdatePriority(Ticket t, TicketPriority priority) + { + if(t.Priority == priority) + return; + + using var scope = ServiceScopeFactory.CreateScope(); + var ticketRepo = scope.ServiceProvider.GetRequiredService>(); + + var ticket = ticketRepo.Get().First(x => x.Id == t.Id); + + ticket.Priority = priority; + + ticketRepo.Update(ticket); + + await Event.Emit("tickets.status", ticket); + await Event.Emit($"tickets.{ticket.Id}.status", ticket); + + await SendSystemMessage(ticket, $"The ticket priority has been changed to: {priority}"); + } + public async Task UpdateStatus(Ticket t, TicketStatus status) + { + if(t.Status == status) + return; + + using var scope = ServiceScopeFactory.CreateScope(); + var ticketRepo = scope.ServiceProvider.GetRequiredService>(); + + var ticket = ticketRepo.Get().First(x => x.Id == t.Id); + + ticket.Status = status; + + ticketRepo.Update(ticket); + + await Event.Emit("tickets.status", ticket); + await Event.Emit($"tickets.{ticket.Id}.status", ticket); + + await SendSystemMessage(ticket, $"The ticket status has been changed to: {status}"); + } + public async Task SendMessage(Ticket t, User sender, string content, string? attachmentUrl = null, bool isSupportMessage = false) + { + using var scope = ServiceScopeFactory.CreateScope(); + var ticketRepo = scope.ServiceProvider.GetRequiredService>(); + var userRepo = scope.ServiceProvider.GetRequiredService>(); + + var ticket = ticketRepo.Get().First(x => x.Id == t.Id); + var user = userRepo.Get().First(x => x.Id == sender.Id); + + var message = new TicketMessage() + { + Content = content, + Sender = user, + AttachmentUrl = attachmentUrl, + IsSupportMessage = isSupportMessage + }; + + ticket.Messages.Add(message); + ticketRepo.Update(ticket); + + await Event.Emit("tickets.message", message); + await Event.Emit($"tickets.{ticket.Id}.message", message); + + return message; + } + public Task> GetUserTickets(User u) + { + using var scope = ServiceScopeFactory.CreateScope(); + var ticketRepo = scope.ServiceProvider.GetRequiredService>(); + + var tickets = ticketRepo + .Get() + .Include(x => x.CreatedBy) + .Include(x => x.Messages) + .Where(x => x.CreatedBy.Id == u.Id) + .Where(x => x.Status != TicketStatus.Closed) + .ToArray(); + + var result = new Dictionary(); + + foreach (var ticket in tickets) + { + var message = ticket.Messages + .OrderByDescending(x => x.Id) + .FirstOrDefault(); + + result.Add(ticket, message); + } + + return Task.FromResult(result); + } + public Task> GetUserAssignedTickets(User u) + { + using var scope = ServiceScopeFactory.CreateScope(); + var ticketRepo = scope.ServiceProvider.GetRequiredService>(); + + var tickets = ticketRepo + .Get() + .Include(x => x.CreatedBy) + .Include(x => x.Messages) + .Where(x => x.Status != TicketStatus.Closed) + .Where(x => x.AssignedTo.Id == u.Id) + .ToArray(); + + var result = new Dictionary(); + + foreach (var ticket in tickets) + { + var message = ticket.Messages + .OrderByDescending(x => x.Id) + .FirstOrDefault(); + + result.Add(ticket, message); + } + + return Task.FromResult(result); + } + public Task> GetUnAssignedTickets() + { + using var scope = ServiceScopeFactory.CreateScope(); + var ticketRepo = scope.ServiceProvider.GetRequiredService>(); + + var tickets = ticketRepo + .Get() + .Include(x => x.CreatedBy) + .Include(x => x.Messages) + .Include(x => x.AssignedTo) + .Where(x => x.AssignedTo == null) + .Where(x => x.Status != TicketStatus.Closed) + .ToArray(); + + var result = new Dictionary(); + + foreach (var ticket in tickets) + { + var message = ticket.Messages + .OrderByDescending(x => x.Id) + .FirstOrDefault(); + + result.Add(ticket, message); + } + + return Task.FromResult(result); + } + public Task GetMessages(Ticket ticket) + { + using var scope = ServiceScopeFactory.CreateScope(); + var ticketRepo = scope.ServiceProvider.GetRequiredService>(); + + var tickets = ticketRepo + .Get() + .Include(x => x.CreatedBy) + .Include(x => x.Messages) + .First(x => x.Id == ticket.Id); + + return Task.FromResult(tickets.Messages.ToArray()); + } + + public async Task Claim(Ticket t, User? u = null) + { + using var scope = ServiceScopeFactory.CreateScope(); + var ticketRepo = scope.ServiceProvider.GetRequiredService>(); + var userRepo = scope.ServiceProvider.GetRequiredService>(); + + var ticket = ticketRepo.Get().Include(x => x.AssignedTo).First(x => x.Id == t.Id); + var user = u == null ? u : userRepo.Get().First(x => x.Id == u.Id); + + ticket.AssignedTo = user; + + ticketRepo.Update(ticket); + + await Event.Emit("tickets.status", ticket); + await Event.Emit($"tickets.{ticket.Id}.status", ticket); + } +} \ No newline at end of file diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index 03b0a313..541028d8 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -29,6 +29,7 @@ using Moonlight.App.Services.Plugins; using Moonlight.App.Services.Sessions; using Moonlight.App.Services.Statistics; using Moonlight.App.Services.SupportChat; +using Moonlight.App.Services.Tickets; using Sentry; using Serilog; using Serilog.Events; @@ -109,10 +110,6 @@ namespace Moonlight await databaseCheckupService.Perform(); - var backupHelper = new BackupHelper(); - await backupHelper.CreateBackup(PathBuilder.File("storage", "backups", - $"{new DateTimeOffset(DateTime.UtcNow).ToUnixTimeMilliseconds()}.zip")); - var builder = WebApplication.CreateBuilder(args); var pluginService = new PluginService(); @@ -217,6 +214,9 @@ namespace Moonlight builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); diff --git a/Moonlight/Shared/Components/Tickets/TicketMessageView.razor b/Moonlight/Shared/Components/Tickets/TicketMessageView.razor new file mode 100644 index 00000000..a7366ff3 --- /dev/null +++ b/Moonlight/Shared/Components/Tickets/TicketMessageView.razor @@ -0,0 +1,219 @@ +@using Moonlight.App.Database.Entities +@using Moonlight.App.Helpers +@using Moonlight.App.Services +@using Moonlight.App.Services.Files +@using System.Text.RegularExpressions + +@inject ResourceService ResourceService +@inject SmartTranslateService SmartTranslateService + +@foreach (var message in Messages.OrderByDescending(x => x.Id)) // Reverse messages to use auto scrolling +{ + if (message.IsSupportMessage) + { + if (ViewAsSupport) + { +
+
+
+
+ @(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService)) + @(message.Sender!.FirstName) @(message.Sender!.LastName) +
+
+ Avatar +
+
+
+ @{ + int i = 0; + var arr = message.Content.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + } + @foreach (var line in arr) + { + @line + if (i++ != arr.Length - 1) + { +
+ } + } + + @if (!string.IsNullOrEmpty(message.AttachmentUrl)) + { +
+ @if (Regex.IsMatch(message.AttachmentUrl, @"\.(jpg|jpeg|png|gif|bmp)$")) + { + Attachment + } + else + { + + @(message.AttachmentUrl) + + } +
+ } +
+
+
+ } + else + { +
+
+
+
+ Avatar +
+
+ @(message.Sender!.FirstName) @(message.Sender!.LastName) + @(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService)) +
+
+
+ @{ + int i = 0; + var arr = message.Content.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + } + @foreach (var line in arr) + { + @line + if (i++ != arr.Length - 1) + { +
+ } + } + + @if (!string.IsNullOrEmpty(message.AttachmentUrl)) + { +
+ @if (Regex.IsMatch(message.AttachmentUrl, @"\.(jpg|jpeg|png|gif|bmp)$")) + { + Attachment + } + else + { + + @(message.AttachmentUrl) + + } +
+ } +
+
+
+ } + } + else if (message.IsSystemMessage) + { +
+ + @(message.Content) + +
+ } + else + { + if (ViewAsSupport) + { +
+
+
+
+ Avatar +
+
+ @(message.Sender!.FirstName) @(message.Sender!.LastName) + @(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService)) +
+
+
+ @{ + int i = 0; + var arr = message.Content.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + } + @foreach (var line in arr) + { + @line + if (i++ != arr.Length - 1) + { +
+ } + } + + @if (!string.IsNullOrEmpty(message.AttachmentUrl)) + { +
+ @if (Regex.IsMatch(message.AttachmentUrl, @"\.(jpg|jpeg|png|gif|bmp)$")) + { + Attachment + } + else + { + + @(message.AttachmentUrl) + + } +
+ } +
+
+
+ } + else + { +
+
+
+
+ @(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService)) + @(message.Sender!.FirstName) @(message.Sender!.LastName) +
+
+ Avatar +
+
+
+ @{ + int i = 0; + var arr = message.Content.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + } + @foreach (var line in arr) + { + @line + if (i++ != arr.Length - 1) + { +
+ } + } + + @if (!string.IsNullOrEmpty(message.AttachmentUrl)) + { +
+ @if (Regex.IsMatch(message.AttachmentUrl, @"\.(jpg|jpeg|png|gif|bmp)$")) + { + Attachment + } + else + { + + @(message.AttachmentUrl) + + } +
+ } +
+
+
+ } + } +} + +@code +{ + [Parameter] + public IEnumerable Messages { get; set; } + + [Parameter] + public bool ViewAsSupport { get; set; } +} \ No newline at end of file diff --git a/Moonlight/Shared/Views/Admin/Support/Index.razor b/Moonlight/Shared/Views/Admin/Support/Index.razor index ca27d4d5..792c9d9d 100644 --- a/Moonlight/Shared/Views/Admin/Support/Index.razor +++ b/Moonlight/Shared/Views/Admin/Support/Index.razor @@ -1,101 +1,379 @@ @page "/admin/support" +@page "/admin/support/{Id:int}" + +@using Moonlight.App.Services.Tickets @using Moonlight.App.Database.Entities @using Moonlight.App.Events -@using Moonlight.App.Services.SupportChat +@using Moonlight.App.Helpers +@using Moonlight.App.Models.Misc +@using Moonlight.App.Services +@using Moonlight.App.Services.Sessions +@using Moonlight.Shared.Components.Tickets -@inject SupportChatServerService ServerService -@inject EventSystem Event - -@implements IDisposable +@inject TicketAdminService AdminService +@inject SmartTranslateService SmartTranslateService +@inject EventSystem EventSystem +@inject IdentityService IdentityService @attribute [PermissionRequired(nameof(Permissions.AdminSupport))] - -
-
-
-
-
-

- Open chats -

-
-
- @if (OpenChats.Any()) +@implements IDisposable + +
+
+
+
+
+
+ + Unassigned tickets + +
+ + @foreach (var ticket in UnAssignedTickets) + { +
+
+
+ @(ticket.Key.IssueTopic) + @if (ticket.Value != null) { - foreach (var chat in OpenChats) - { -
- - - - - - - - -
- - - - - - @(chat.Key.FirstName) @(chat.Key.LastName) - -
- - @if (chat.Value == null) - { - No message sent yet - } - else - { - @(chat.Value.Content) - } - -
-
-
- } - } - else - { -
- No support chat is currently open +
+ @(ticket.Value.Content)
}
+
+ @if (ticket.Value != null) + { + + @(Formatter.FormatAgoFromDateTime(ticket.Value.CreatedAt, SmartTranslateService)) + + } +
-
+ + if (ticket.Key != UnAssignedTickets.Last().Key) + { +
+ } + } + + @if (AssignedTickets.Any()) + { +
+ + Assigned tickets + +
+ } + + @foreach (var ticket in AssignedTickets) + { +
+
+
+ @(ticket.Key.IssueTopic) + @if (ticket.Value != null) + { +
+ @(ticket.Value.Content) +
+ } +
+
+
+ @if (ticket.Value != null) + { + + @(Formatter.FormatAgoFromDateTime(ticket.Value.CreatedAt, SmartTranslateService)) + + } +
+
+ + if (ticket.Key != AssignedTickets.Last().Key) + { +
+ } + }
- +
+
+
+
+
+ @if (AdminService.Ticket != null) + { +
+
+ @(AdminService.Ticket.IssueTopic) +
+ + Status + + @switch (AdminService.Ticket.Status) + { + case TicketStatus.Closed: + + break; + case TicketStatus.Open: + + break; + case TicketStatus.Pending: + + break; + case TicketStatus.WaitingForUser: + + break; + } + @(AdminService.Ticket.Status) + + + Priority + + @switch (AdminService.Ticket.Priority) + { + case TicketPriority.Low: + + break; + case TicketPriority.Medium: + + break; + case TicketPriority.High: + + break; + case TicketPriority.Critical: + + break; + } + @(AdminService.Ticket.Priority) +
+
+
+
+
+
+ @if (AdminService.Ticket!.AssignedTo == null) + { + + } + else + { + + } +
+ + + + + + +
+
+ } + else + { +
+
+ + + +
+
+ } +
+
+
+ @if (AdminService.Ticket == null) + { + } + else + { + + } +
+
+ @if (AdminService.Ticket != null) + { + + } +
+
+
@code { - private LazyLoader? LazyLoader; - private Dictionary OpenChats = new(); + [Parameter] + public int Id { get; set; } - protected override async Task OnInitializedAsync() + private Dictionary AssignedTickets; + private Dictionary UnAssignedTickets; + private List Messages = new(); + private string MessageText; + private SmartFileSelect FileSelect; + + private TicketPriority Priority; + private TicketStatus Status; + + protected override async Task OnParametersSetAsync() { - await Event.On("supportChat.new", this, async user => - { - //TODO: Play sound or smth. Add a config option + await Unsubscribe(); + await ReloadTickets(); + await Subscribe(); - OpenChats = await ServerService.GetOpenChats(); - - await InvokeAsync(StateHasChanged); - }); + await InvokeAsync(StateHasChanged); } - private async Task Load(LazyLoader arg) // Only for initial load + private async Task UpdatePriority() { - OpenChats = await ServerService.GetOpenChats(); + await AdminService.UpdatePriority(Priority); + } + + private async Task UpdateStatus() + { + await AdminService.UpdateStatus(Status); + } + + private async Task SendMessage() + { + if (string.IsNullOrEmpty(MessageText) && FileSelect.SelectedFile != null) + MessageText = "File upload"; + + if (string.IsNullOrEmpty(MessageText)) + return; + + var msg = await AdminService.Send(MessageText, FileSelect.SelectedFile); + Messages.Add(msg); + MessageText = ""; + FileSelect.SelectedFile = null; + + await InvokeAsync(StateHasChanged); + } + + private async Task Subscribe() + { + await EventSystem.On("tickets.new", this, async _ => + { + await ReloadTickets(false); + await InvokeAsync(StateHasChanged); + }); + + if (AdminService.Ticket != null) + { + await EventSystem.On($"tickets.{AdminService.Ticket.Id}.message", this, async message => + { + if (message.Sender != null && message.Sender.Id == IdentityService.User.Id && message.IsSupportMessage) + return; + + Messages.Add(message); + await InvokeAsync(StateHasChanged); + }); + + await EventSystem.On($"tickets.{AdminService.Ticket.Id}.status", this, async _ => + { + await ReloadTickets(false); + await InvokeAsync(StateHasChanged); + }); + } + } + + private async Task Unsubscribe() + { + await EventSystem.Off("tickets.new", this); + + if (AdminService.Ticket != null) + { + await EventSystem.Off($"tickets.{AdminService.Ticket.Id}.message", this); + await EventSystem.Off($"tickets.{AdminService.Ticket.Id}.status", this); + } + } + + private async Task ReloadTickets(bool reloadMessages = true) + { + AdminService.Ticket = null; + AssignedTickets = await AdminService.GetAssigned(); + UnAssignedTickets = await AdminService.GetUnAssigned(); + + if (Id != 0) + { + AdminService.Ticket = AssignedTickets + .FirstOrDefault(x => x.Key.Id == Id) + .Key ?? null; + + if (AdminService.Ticket == null) + { + AdminService.Ticket = UnAssignedTickets + .FirstOrDefault(x => x.Key.Id == Id) + .Key ?? null; + } + + if (AdminService.Ticket == null) + return; + + Status = AdminService.Ticket.Status; + Priority = AdminService.Ticket.Priority; + + if (reloadMessages) + { + var msgs = await AdminService.GetMessages(); + Messages = msgs.ToList(); + } + } } public async void Dispose() { - await Event.Off("supportChat.new", this); + await Unsubscribe(); } } \ No newline at end of file diff --git a/Moonlight/Shared/Views/Admin/Support/View.razor b/Moonlight/Shared/Views/Admin/Support/View.razor deleted file mode 100644 index 5b28d7fc..00000000 --- a/Moonlight/Shared/Views/Admin/Support/View.razor +++ /dev/null @@ -1,313 +0,0 @@ -@page "/admin/support/view/{Id:int}" -@using Moonlight.App.Database.Entities -@using Moonlight.App.Helpers -@using Moonlight.App.Repositories -@using Moonlight.App.Services -@using Moonlight.App.Services.SupportChat -@using System.Text.RegularExpressions -@using Moonlight.App.Services.Files - -@inject SupportChatAdminService AdminService -@inject UserRepository UserRepository -@inject SmartTranslateService SmartTranslateService -@inject ResourceService ResourceService - -@implements IDisposable - -@attribute [PermissionRequired(nameof(Permissions.AdminSupportView))] - - - @if (User == null) - { -
- User not found -
- } - else - { -
-
-
-
-
- -
- @foreach (var message in Messages) - { - if (message.Sender == null || message.Sender.Id != User.Id) - { -
-
-
- -
- Logo -
-
- -
- @if (message.Sender == null) - { - @(message.Content) - } - else - { - foreach (var line in message.Content.Split("\n")) - { - @(line)
- } - - if (message.Attachment != "") - { -
- @if (Regex.IsMatch(message.Attachment, @"\.(jpg|jpeg|png|gif|bmp)$")) - { - Attachment - } - else - { - - @(message.Attachment) - - } -
- } - } -
-
-
- } - else - { -
-
-
-
- Avatar -
-
- - @(message.Sender.FirstName) @(message.Sender.LastName) - - @(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService)) -
-
- -
- @{ - int i = 0; - var arr = message.Content.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);} - @foreach (var line in arr) - { - @line - if (i++ != arr.Length - 1) - { -
- } - } - - @if (message.Attachment != "") - { -
- @if (Regex.IsMatch(message.Attachment, @"\.(jpg|jpeg|png|gif|bmp)$")) - { - Attachment - } - else - { - - @(message.Attachment) - - } -
- } -
-
-
- } - } -
-
-
- -
-
-
-
-

- User information -

- -
- - Name: @(User.FirstName) @User.LastName - -
-
- - Email: @(User.Email) - -
-
- - - - -
-
-
-
-
- } -
- -@code -{ - [Parameter] - public int Id { get; set; } - - private User? User; - - private List Messages = new(); - private string[] Typing = Array.Empty(); - - private string Content = ""; - private DateTime LastTypingTimestamp = DateTime.UtcNow.AddMinutes(-10); - - private SmartFileSelect SmartFileSelect; - - private async Task Load(LazyLoader arg) - { - User = UserRepository - .Get() - .FirstOrDefault(x => x.Id == Id); - - if (User != null) - { - AdminService.OnMessage += OnMessage; - AdminService.OnTypingChanged += OnTypingChanged; - - await AdminService.Start(User); - } - } - - private async Task LoadMessages(LazyLoader arg) - { - Messages = (await AdminService.GetMessages()).ToList(); - } - - private async Task OnTypingChanged(string[] typing) - { - Typing = typing; - - await InvokeAsync(StateHasChanged); - } - - private async Task OnMessage(SupportChatMessage arg) - { - Messages.Insert(0, arg); - - //TODO: Sound when message from system or admin - - await InvokeAsync(StateHasChanged); - } - - private async Task Send() - { - if (SmartFileSelect.SelectedFile != null && string.IsNullOrEmpty(Content)) - Content = "File upload"; - - if (string.IsNullOrEmpty(Content)) - return; - - var message = await AdminService.SendMessage(Content, SmartFileSelect.SelectedFile); - Content = ""; - - await SmartFileSelect.RemoveSelection(); - - Messages.Insert(0, message); - - await InvokeAsync(StateHasChanged); - } - - private async Task CloseTicket() - { - await AdminService.Close(); - } - - private async Task OnTyping() - { - if ((DateTime.UtcNow - LastTypingTimestamp).TotalSeconds > 5) - { - LastTypingTimestamp = DateTime.UtcNow; - - await AdminService.SendTyping(); - } - } - - public void Dispose() - { - AdminService?.Dispose(); - } -} \ No newline at end of file diff --git a/Moonlight/Shared/Views/Support.razor b/Moonlight/Shared/Views/Support.razor deleted file mode 100644 index 039def04..00000000 --- a/Moonlight/Shared/Views/Support.razor +++ /dev/null @@ -1,275 +0,0 @@ -@page "/support" -@using Moonlight.App.Services -@using Moonlight.App.Database.Entities -@using Moonlight.App.Helpers -@using Moonlight.App.Services.SupportChat -@using System.Text.RegularExpressions -@using Moonlight.App.Services.Files -@using Moonlight.App.Services.Sessions - -@inject ResourceService ResourceService -@inject SupportChatClientService ClientService -@inject SmartTranslateService SmartTranslateService -@inject IdentityService IdentityService - -@implements IDisposable - - -
-
-
- -
- @foreach (var message in Messages.ToArray()) - { - if (message.Sender == null || message.Sender.Id != IdentityService.User.Id) - { -
-
-
-
- Logo -
- -
- -
- @if (message.Sender == null) - { - @(message.Content) - } - else - { - int i = 0; - var arr = message.Content.Split("\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - foreach (var line in arr) - { - @line - if (i++ != arr.Length - 1) - { -
- } - } - - if (message.Attachment != "") - { -
- @if (Regex.IsMatch(message.Attachment, @"\.(jpg|jpeg|png|gif|bmp)$")) - { - Attachment - } - else - { - - @(message.Attachment) - - } -
- } - } -
-
-
- } - else - { -
-
-
-
- @(Formatter.FormatAgoFromDateTime(message.CreatedAt, SmartTranslateService)) - - @(message.Sender.FirstName) @(message.Sender.LastName) - -
-
- Avatar -
-
- -
- @foreach (var line in message.Content.Split("\n")) - { - @(line)
- } - - @if (message.Attachment != "") - { -
- @if (Regex.IsMatch(message.Attachment, @"\.(jpg|jpeg|png|gif|bmp)$")) - { - Attachment - } - else - { - - @(message.Attachment) - - } -
- } -
-
-
- } - } -
-
-
-
- Logo -
- -
- -
- Welcome to the support chat. Ask your question here and we will help you -
-
-
-
-
-
- -
-
-
- -@code -{ - private List Messages = new(); - private string[] Typing = Array.Empty(); - - private string Content = ""; - private DateTime LastTypingTimestamp = DateTime.UtcNow.AddMinutes(-10); - - private SmartFileSelect SmartFileSelect; - - private async Task Load(LazyLoader lazyLoader) - { - await lazyLoader.SetText("Starting chat client"); - - ClientService.OnMessage += OnMessage; - ClientService.OnTypingChanged += OnTypingChanged; - - await ClientService.Start(); - } - - private async Task LoadMessages(LazyLoader arg) - { - Messages = (await ClientService.GetMessages()).ToList(); - } - - private async Task OnTypingChanged(string[] typing) - { - Typing = typing; - - await InvokeAsync(StateHasChanged); - } - - private async Task OnMessage(SupportChatMessage message) - { - Messages.Insert(0, message); - - //TODO: Sound when message from system or admin - - await InvokeAsync(StateHasChanged); - } - - private async Task Send() - { - if (SmartFileSelect.SelectedFile != null && string.IsNullOrEmpty(Content)) - Content = "File upload"; - - var message = await ClientService.SendMessage(Content, SmartFileSelect.SelectedFile); - Content = ""; - - await SmartFileSelect.RemoveSelection(); - - Messages.Insert(0, message); - - await InvokeAsync(StateHasChanged); - } - - private async void OnTyping() - { - if ((DateTime.UtcNow - LastTypingTimestamp).TotalSeconds > 5) - { - LastTypingTimestamp = DateTime.UtcNow; - await ClientService.SendTyping(); - } - } - - public void Dispose() - { - ClientService?.Dispose(); - } - - private void OnFileChange(InputFileChangeEventArgs obj) - { - } -} \ No newline at end of file diff --git a/Moonlight/Shared/Views/Support/Index.razor b/Moonlight/Shared/Views/Support/Index.razor new file mode 100644 index 00000000..cc478b42 --- /dev/null +++ b/Moonlight/Shared/Views/Support/Index.razor @@ -0,0 +1,468 @@ +@page "/support" +@page "/support/{Id:int}" + +@using Moonlight.App.Services.Tickets +@using Moonlight.App.Database.Entities +@using Moonlight.App.Helpers +@using Moonlight.App.Models.Forms +@using Moonlight.App.Models.Misc +@using Moonlight.App.Repositories +@using Moonlight.App.Services +@using Microsoft.EntityFrameworkCore +@using Moonlight.App.Events +@using Moonlight.App.Services.Files +@using Moonlight.App.Services.Sessions +@using Moonlight.Shared.Components.Tickets + +@inject TicketClientService ClientService +@inject Repository ServerRepository +@inject Repository WebSpaceRepository +@inject Repository DomainRepository +@inject SmartTranslateService SmartTranslateService +@inject IdentityService IdentityService +@inject NavigationManager NavigationManager +@inject ResourceService ResourceService +@inject EventSystem EventSystem + +
+
+
+
+
+ + +
+ + @foreach (var ticket in Tickets) + { +
+
+
+ @(ticket.Key.IssueTopic) + @if (ticket.Value != null) + { +
+ @(ticket.Value.Content) +
+ } +
+
+
+ @if (ticket.Value != null) + { + + @(Formatter.FormatAgoFromDateTime(ticket.Value.CreatedAt, SmartTranslateService)) + + } +
+
+ + if (ticket.Key != Tickets.Last().Key) + { +
+ } + } +
+
+
+
+
+
+
+ @if (ClientService.Ticket != null) + { +
+
+ @(ClientService.Ticket.IssueTopic) +
+ + Status + + @switch (ClientService.Ticket.Status) + { + case TicketStatus.Closed: + + break; + case TicketStatus.Open: + + break; + case TicketStatus.Pending: + + break; + case TicketStatus.WaitingForUser: + + break; + } + @(ClientService.Ticket.Status) + + + Priority + + @switch (ClientService.Ticket.Priority) + { + case TicketPriority.Low: + + break; + case TicketPriority.Medium: + + break; + case TicketPriority.High: + + break; + case TicketPriority.Critical: + + break; + } + @(ClientService.Ticket.Priority) +
+
+
+ + } + else + { +
+
+ + Create a new ticket + +
+
+ } +
+
+
+ @if (ClientService.Ticket == null) + { + + +
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ @if (Model.Subject == TicketSubject.Domain) + { + + } + else if (Model.Subject == TicketSubject.Server) + { + + } + else if (Model.Subject == TicketSubject.Webspace) + { + + } +
+
+ +
+
+
+ } + else + { + + } +
+
+@if (ClientService.Ticket != null) +{ + +} +
+
+
+ +@code +{ + [Parameter] + public int Id { get; set; } + + private Dictionary Tickets; + private List Messages = new(); + private CreateTicketDataModel Model = new(); + private string MessageText; + private SmartFileSelect FileSelect; + + private Server[] Servers; + private WebSpace[] WebSpaces; + private Domain[] Domains; + + protected override async Task OnParametersSetAsync() + { + await Unsubscribe(); + await ReloadTickets(); + await Subscribe(); + + await InvokeAsync(StateHasChanged); + } + + private Task LoadTicketCreate(LazyLoader _) + { + Servers = ServerRepository + .Get() + .Include(x => x.Owner) + .Where(x => x.Owner.Id == IdentityService.User.Id) + .ToArray(); + + WebSpaces = WebSpaceRepository + .Get() + .Include(x => x.Owner) + .Where(x => x.Owner.Id == IdentityService.User.Id) + .ToArray(); + + Domains = DomainRepository + .Get() + .Include(x => x.SharedDomain) + .Include(x => x.Owner) + .Where(x => x.Owner.Id == IdentityService.User.Id) + .ToArray(); + + return Task.CompletedTask; + } + + private async Task OnValidSubmit() + { + var ticket = await ClientService.Create( + Model.IssueTopic, + Model.IssueDescription, + Model.IssueTries, + Model.Subject, + Model.SubjectId + ); + + Model = new(); + + NavigationManager.NavigateTo("/support/" + ticket.Id); + } + + private async Task SendMessage() + { + if (string.IsNullOrEmpty(MessageText) && FileSelect.SelectedFile != null) + MessageText = "File upload"; + + if(string.IsNullOrEmpty(MessageText)) + return; + + var msg = await ClientService.Send(MessageText, FileSelect.SelectedFile); + Messages.Add(msg); + MessageText = ""; + FileSelect.SelectedFile = null; + + await InvokeAsync(StateHasChanged); + } + + private async Task Subscribe() + { + await EventSystem.On("tickets.new", this, async ticket => + { + if (ticket.CreatedBy != null && ticket.CreatedBy.Id != IdentityService.User.Id) + return; + + await ReloadTickets(false); + await InvokeAsync(StateHasChanged); + }); + + if (ClientService.Ticket != null) + { + await EventSystem.On($"tickets.{ClientService.Ticket.Id}.message", this, async message => + { + if (message.Sender != null && message.Sender.Id == IdentityService.User.Id && !message.IsSupportMessage) + return; + + Messages.Add(message); + await InvokeAsync(StateHasChanged); + }); + + await EventSystem.On($"tickets.{ClientService.Ticket.Id}.status", this, async _ => + { + await ReloadTickets(false); + await InvokeAsync(StateHasChanged); + }); + } + } + + private async Task Unsubscribe() + { + await EventSystem.Off("tickets.new", this); + + if (ClientService.Ticket != null) + { + await EventSystem.Off($"tickets.{ClientService.Ticket.Id}.message", this); + await EventSystem.Off($"tickets.{ClientService.Ticket.Id}.status", this); + } + } + + private async Task ReloadTickets(bool reloadMessages = true) + { + ClientService.Ticket = null; + Tickets = await ClientService.Get(); + + if (Id != 0) + { + ClientService.Ticket = Tickets + .FirstOrDefault(x => x.Key.Id == Id) + .Key ?? null; + + if (ClientService.Ticket == null) + return; + + if (reloadMessages) + { + var msgs = await ClientService.GetMessages(); + Messages = msgs.ToList(); + } + } + } +} \ No newline at end of file