diff --git a/Moonlight/App/Database/DataContext.cs b/Moonlight/App/Database/DataContext.cs index e4b3f607..a0ea380e 100644 --- a/Moonlight/App/Database/DataContext.cs +++ b/Moonlight/App/Database/DataContext.cs @@ -12,8 +12,6 @@ public class DataContext : DbContext private readonly ConfigService ConfigService; public DbSet Users { get; set; } - //public DbSet Tickets { get; set; } - //public DbSet TicketMessages { get; set; } // Store public DbSet Categories { get; set; } @@ -32,6 +30,10 @@ public class DataContext : DbContext public DbSet PostComments { get; set; } public DbSet PostLikes { get; set; } public DbSet WordFilters { get; set; } + + // Tickets + public DbSet Tickets { get; set; } + public DbSet TicketMessages { get; set; } public DataContext(ConfigService configService) { diff --git a/Moonlight/App/Database/Entities/Tickets/Ticket.cs b/Moonlight/App/Database/Entities/Tickets/Ticket.cs index ef6d485f..fb6e029a 100644 --- a/Moonlight/App/Database/Entities/Tickets/Ticket.cs +++ b/Moonlight/App/Database/Entities/Tickets/Ticket.cs @@ -1,4 +1,5 @@ -using Moonlight.App.Database.Enums; +using Moonlight.App.Database.Entities.Store; +using Moonlight.App.Database.Enums; namespace Moonlight.App.Database.Entities.Tickets; @@ -11,8 +12,9 @@ public class Ticket public string Tries { get; set; } = ""; public TicketPriority Priority { get; set; } = TicketPriority.Low; public bool Open { get; set; } = true; + public Service? Service { get; set; } - public List Messages = new(); + 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/Migrations/20231101161843_AddedTicketModels.Designer.cs b/Moonlight/App/Database/Migrations/20231101161843_AddedTicketModels.Designer.cs new file mode 100644 index 00000000..1419a13c --- /dev/null +++ b/Moonlight/App/Database/Migrations/20231101161843_AddedTicketModels.Designer.cs @@ -0,0 +1,665 @@ +// +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("20231101161843_AddedTicketModels")] + partial class AddedTicketModels + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.2"); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.Post", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.ToTable("Posts"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("PostId") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("PostId"); + + b.ToTable("PostComments"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostLike", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("PostId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PostId"); + + b.HasIndex("UserId"); + + b.ToTable("PostLikes"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.WordFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Filter") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("WordFilters"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Coupon", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Percent") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CouponId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CouponId"); + + b.HasIndex("UserId"); + + b.ToTable("CouponUses"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.ToTable("GiftCodes"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCodeUse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("GiftCodeId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GiftCodeId"); + + b.HasIndex("UserId"); + + b.ToTable("GiftCodeUses"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CategoryId") + .HasColumnType("INTEGER"); + + b.Property("ConfigJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Duration") + .HasColumnType("INTEGER"); + + b.Property("MaxPerUser") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("REAL"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Stock") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConfigJsonOverride") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Nickname") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("INTEGER"); + + b.Property("ProductId") + .HasColumnType("INTEGER"); + + b.Property("RenewAt") + .HasColumnType("TEXT"); + + b.Property("Suspended") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("ProductId"); + + b.ToTable("Services"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.ServiceShare", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ServiceId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ServiceId"); + + b.HasIndex("UserId"); + + b.ToTable("ServiceShares"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Transaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Price") + .HasColumnType("REAL"); + + b.Property("Text") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Transaction"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Tickets.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatorId") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Open") + .HasColumnType("INTEGER"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("ServiceId") + .HasColumnType("INTEGER"); + + b.Property("Tries") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatorId"); + + b.HasIndex("ServiceId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Tickets.TicketMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attachment") + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("IsSupport") + .HasColumnType("INTEGER"); + + b.Property("SenderId") + .HasColumnType("INTEGER"); + + b.Property("TicketId") + .HasColumnType("INTEGER"); + + 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("INTEGER"); + + b.Property("Avatar") + .HasColumnType("TEXT"); + + b.Property("Balance") + .HasColumnType("REAL"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Flags") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("INTEGER"); + + b.Property("TokenValidTimestamp") + .HasColumnType("TEXT"); + + b.Property("TotpKey") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.Post", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Author") + .WithMany() + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostComment", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Author") + .WithMany() + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.Community.Post", null) + .WithMany("Comments") + .HasForeignKey("PostId"); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostLike", b => + { + b.HasOne("Moonlight.App.Database.Entities.Community.Post", null) + .WithMany("Likes") + .HasForeignKey("PostId"); + + b.HasOne("Moonlight.App.Database.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b => + { + b.HasOne("Moonlight.App.Database.Entities.Store.Coupon", "Coupon") + .WithMany() + .HasForeignKey("CouponId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.User", null) + .WithMany("CouponUses") + .HasForeignKey("UserId"); + + b.Navigation("Coupon"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCodeUse", b => + { + b.HasOne("Moonlight.App.Database.Entities.Store.GiftCode", "GiftCode") + .WithMany() + .HasForeignKey("GiftCodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.User", null) + .WithMany("GiftCodeUses") + .HasForeignKey("UserId"); + + b.Navigation("GiftCode"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Product", b => + { + b.HasOne("Moonlight.App.Database.Entities.Store.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.Store.Product", "Product") + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.ServiceShare", b => + { + b.HasOne("Moonlight.App.Database.Entities.Store.Service", null) + .WithMany("Shares") + .HasForeignKey("ServiceId"); + + b.HasOne("Moonlight.App.Database.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Transaction", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", null) + .WithMany("Transactions") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Tickets.Ticket", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.Store.Service", "Service") + .WithMany() + .HasForeignKey("ServiceId"); + + b.Navigation("Creator"); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Tickets.TicketMessage", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Sender") + .WithMany() + .HasForeignKey("SenderId"); + + b.HasOne("Moonlight.App.Database.Entities.Tickets.Ticket", null) + .WithMany("Messages") + .HasForeignKey("TicketId"); + + b.Navigation("Sender"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.Post", b => + { + b.Navigation("Comments"); + + b.Navigation("Likes"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b => + { + b.Navigation("Shares"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Tickets.Ticket", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.User", b => + { + b.Navigation("CouponUses"); + + b.Navigation("GiftCodeUses"); + + b.Navigation("Transactions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Moonlight/App/Database/Migrations/20231101161843_AddedTicketModels.cs b/Moonlight/App/Database/Migrations/20231101161843_AddedTicketModels.cs new file mode 100644 index 00000000..f76fc0d9 --- /dev/null +++ b/Moonlight/App/Database/Migrations/20231101161843_AddedTicketModels.cs @@ -0,0 +1,104 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Moonlight.App.Database.Migrations +{ + /// + public partial class AddedTicketModels : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Tickets", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + CreatorId = table.Column(type: "INTEGER", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + Description = table.Column(type: "TEXT", nullable: false), + Tries = table.Column(type: "TEXT", nullable: false), + Priority = table.Column(type: "INTEGER", nullable: false), + Open = table.Column(type: "INTEGER", nullable: false), + ServiceId = table.Column(type: "INTEGER", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Tickets", x => x.Id); + table.ForeignKey( + name: "FK_Tickets_Services_ServiceId", + column: x => x.ServiceId, + principalTable: "Services", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Tickets_Users_CreatorId", + column: x => x.CreatorId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "TicketMessages", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + SenderId = table.Column(type: "INTEGER", nullable: true), + IsSupport = table.Column(type: "INTEGER", nullable: false), + Content = table.Column(type: "TEXT", nullable: false), + Attachment = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + TicketId = table.Column(type: "INTEGER", 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"); + }); + + 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_CreatorId", + table: "Tickets", + column: "CreatorId"); + + migrationBuilder.CreateIndex( + name: "IX_Tickets_ServiceId", + table: "Tickets", + column: "ServiceId"); + } + + /// + 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 d6c7ed52..6e5d8421 100644 --- a/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs +++ b/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs @@ -357,6 +357,82 @@ namespace Moonlight.App.Database.Migrations b.ToTable("Transaction"); }); + modelBuilder.Entity("Moonlight.App.Database.Entities.Tickets.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatorId") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Open") + .HasColumnType("INTEGER"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("ServiceId") + .HasColumnType("INTEGER"); + + b.Property("Tries") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatorId"); + + b.HasIndex("ServiceId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Tickets.TicketMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attachment") + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("IsSupport") + .HasColumnType("INTEGER"); + + b.Property("SenderId") + .HasColumnType("INTEGER"); + + b.Property("TicketId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SenderId"); + + b.HasIndex("TicketId"); + + b.ToTable("TicketMessages"); + }); + modelBuilder.Entity("Moonlight.App.Database.Entities.User", b => { b.Property("Id") @@ -525,6 +601,36 @@ namespace Moonlight.App.Database.Migrations .HasForeignKey("UserId"); }); + modelBuilder.Entity("Moonlight.App.Database.Entities.Tickets.Ticket", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Moonlight.App.Database.Entities.Store.Service", "Service") + .WithMany() + .HasForeignKey("ServiceId"); + + b.Navigation("Creator"); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Tickets.TicketMessage", b => + { + b.HasOne("Moonlight.App.Database.Entities.User", "Sender") + .WithMany() + .HasForeignKey("SenderId"); + + b.HasOne("Moonlight.App.Database.Entities.Tickets.Ticket", null) + .WithMany("Messages") + .HasForeignKey("TicketId"); + + b.Navigation("Sender"); + }); + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.Post", b => { b.Navigation("Comments"); @@ -537,6 +643,11 @@ namespace Moonlight.App.Database.Migrations b.Navigation("Shares"); }); + modelBuilder.Entity("Moonlight.App.Database.Entities.Tickets.Ticket", b => + { + b.Navigation("Messages"); + }); + modelBuilder.Entity("Moonlight.App.Database.Entities.User", b => { b.Navigation("CouponUses"); diff --git a/Moonlight/App/Event/Args/TicketMessageEventArgs.cs b/Moonlight/App/Event/Args/TicketMessageEventArgs.cs new file mode 100644 index 00000000..900be64f --- /dev/null +++ b/Moonlight/App/Event/Args/TicketMessageEventArgs.cs @@ -0,0 +1,9 @@ +using Moonlight.App.Database.Entities.Tickets; + +namespace Moonlight.App.Event.Args; + +public class TicketMessageEventArgs +{ + public Ticket Ticket { get; set; } + public TicketMessage TicketMessage { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Event/Events.cs b/Moonlight/App/Event/Events.cs index 37cdf75e..7ee34f8b 100644 --- a/Moonlight/App/Event/Events.cs +++ b/Moonlight/App/Event/Events.cs @@ -1,6 +1,7 @@ using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities.Community; using Moonlight.App.Database.Entities.Store; +using Moonlight.App.Database.Entities.Tickets; using Moonlight.App.Event.Args; namespace Moonlight.App.Event; @@ -19,5 +20,8 @@ public class Events public static EventHandler OnPostLiked; public static EventHandler OnPostCommentCreated; public static EventHandler OnPostCommentDeleted; + public static EventHandler OnTicketCreated; + public static EventHandler OnTicketMessage; + public static EventHandler OnTicketUpdated; public static EventHandler OnMoonlightRestart; } \ No newline at end of file diff --git a/Moonlight/App/Models/Forms/Ticketing/CreateTicketForm.cs b/Moonlight/App/Models/Forms/Ticketing/CreateTicketForm.cs new file mode 100644 index 00000000..2c57b965 --- /dev/null +++ b/Moonlight/App/Models/Forms/Ticketing/CreateTicketForm.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; +using Moonlight.App.Database.Entities.Store; + +namespace Moonlight.App.Models.Forms.Ticketing; + +public class CreateTicketForm +{ + [Required(ErrorMessage = "You need to enter a ticket name")] + [MinLength(8, ErrorMessage = "The title needs to be longer then 8 characters")] + [MaxLength(64, ErrorMessage = "The ticket name should not exceed 64 characters in lenght")] + public string Name { get; set; } = ""; + + [Required(ErrorMessage = "You need to enter a description")] + [MinLength(8, ErrorMessage = "The description needs to be longer then 8 characters")] + [MaxLength(256, ErrorMessage = "The description should not exceed 256 characters in lenght")] + public string Description { get; set; } = ""; + + [Required(ErrorMessage = "You need to specify what you have tried already")] + [MinLength(8, ErrorMessage = "The tries description needs to be longer then 8 characters")] + [MaxLength(256, ErrorMessage = "The tries description should not exceed 256 characters in lenght")] + public string Tries { get; set; } = ""; + + public Service? Service { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Services/BucketService.cs b/Moonlight/App/Services/BucketService.cs index 658ffa62..ced7ccdd 100644 --- a/Moonlight/App/Services/BucketService.cs +++ b/Moonlight/App/Services/BucketService.cs @@ -63,4 +63,22 @@ public class BucketService else throw new FileNotFoundException(); } + + public Task Delete(string bucket, string file, bool ignoreNotFound = false) + { + var filePath = PathBuilder.File(BasePath, bucket, file); + + if (File.Exists(filePath)) + { + File.Delete(filePath); + return Task.CompletedTask; + } + + // This section will only be reached if the file does not exist + + if (!ignoreNotFound) + throw new FileNotFoundException(); + + return Task.CompletedTask; + } } \ No newline at end of file diff --git a/Moonlight/App/Services/Ticketing/TicketChatService.cs b/Moonlight/App/Services/Ticketing/TicketChatService.cs new file mode 100644 index 00000000..e868f308 --- /dev/null +++ b/Moonlight/App/Services/Ticketing/TicketChatService.cs @@ -0,0 +1,179 @@ +using Microsoft.EntityFrameworkCore; +using Moonlight.App.Database.Entities.Tickets; +using Moonlight.App.Database.Enums; +using Moonlight.App.Event; +using Moonlight.App.Event.Args; +using Moonlight.App.Extensions; +using Moonlight.App.Repositories; + +namespace Moonlight.App.Services.Ticketing; + +public class TicketChatService +{ + private readonly IdentityService IdentityService; + private readonly Repository TicketRepository; + private readonly BucketService BucketService; + private readonly List MessageCache = new(); + + public Ticket Ticket; + public bool IsSupporter; + public Func? OnUpdate; + public TicketMessage[] Messages => MessageCache.ToArray(); + + public TicketChatService( + IdentityService identityService, + Repository ticketRepository, + BucketService bucketService) + { + IdentityService = identityService; + TicketRepository = ticketRepository; + BucketService = bucketService; + } + + public Task Start(Ticket ticket, bool isSupporter = false) + { + IsSupporter = isSupporter; + + // Load data into local cache + Ticket = TicketRepository + .Get() + .Include(x => x.Messages) + .Include(x => x.Creator) + .Include(x => x.Service) + .First(x => x.Id == ticket.Id); + + MessageCache.AddRange(Ticket.Messages); + + // Register event handlers + Events.OnTicketMessage += OnTicketMessage; + Events.OnTicketUpdated += OnTicketUpdated; + + return Task.CompletedTask; + } + + public async Task Update(bool open, TicketPriority priority) // Updated and syncs ticket states to all listeners + { + if (Ticket.Open != open) + { + Ticket.Open = open; + + if(open) + await SendSystemMessage("Ticket has been opened"); + else + await SendSystemMessage("Ticket has been closed"); + } + + if (Ticket.Priority != priority) + { + Ticket.Priority = priority; + + await SendSystemMessage($"Ticket priority to {priority}"); + } + + TicketRepository.Update(Ticket); + + await Events.OnTicketUpdated.InvokeAsync(Ticket); + } + + public Task Stop() // Clear cache and stop listeners + { + Events.OnTicketMessage -= OnTicketMessage; + Events.OnTicketUpdated -= OnTicketUpdated; + + MessageCache.Clear(); + + return Task.CompletedTask; + } + + #region Sending + + public async Task SendSystemMessage(string content) // use this to send a message shown in a seperator + { + // Build the message model + var message = new TicketMessage() + { + Content = content, + Attachment = null, + CreatedAt = DateTime.UtcNow, + Sender = null, + IsSupport = IsSupporter + }; + + await SyncMessage(message); + } + + public async Task SendMessage(string content, Stream? attachmentStream = null, string? attachmentName = null) // Regular send method + { + if(string.IsNullOrEmpty(content)) + return; + + string? attachmentBucketName = null; + + // Check and download attachments + if (attachmentStream != null && attachmentName != null) + { + attachmentBucketName = await BucketService.Store( + "ticketAttachments", + attachmentStream, + attachmentName + ); + } + + // Build the message model + var message = new TicketMessage() + { + Content = content, + Attachment = attachmentBucketName, + CreatedAt = DateTime.UtcNow, + Sender = IdentityService.CurrentUser, + IsSupport = IsSupporter + }; + + await SyncMessage(message); + } + + private async Task SyncMessage(TicketMessage message) // Use this function to save and sync function to others + { + // Save ticket to the db + var t = TicketRepository + .Get() + .First(x => x.Id == Ticket.Id); // We do this to get a clean reference + + t.Messages.Add(message); + TicketRepository.Update(t); + + // Now emit the events + await Events.OnTicketMessage.InvokeAsync(new() + { + Ticket = t, // We use this reference as it has less data attached to it + TicketMessage = message + }); + } + + #endregion + + // Event handlers + private async void OnTicketUpdated(object? _, Ticket ticket) + { + if(Ticket.Id != ticket.Id) // Only listen to our ticket + return; + + // Update the possible values + Ticket.Open = ticket.Open; + Ticket.Priority = ticket.Priority; + + if (OnUpdate != null) + await OnUpdate.Invoke(); + } + + private async void OnTicketMessage(object? _, TicketMessageEventArgs eventArgs) + { + if(Ticket.Id != eventArgs.Ticket.Id) // Only listen to our ticket + return; + + MessageCache.Add(eventArgs.TicketMessage); + + if (OnUpdate != null) + await OnUpdate.Invoke(); + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/Ticketing/TicketCreateService.cs b/Moonlight/App/Services/Ticketing/TicketCreateService.cs new file mode 100644 index 00000000..0a005fe1 --- /dev/null +++ b/Moonlight/App/Services/Ticketing/TicketCreateService.cs @@ -0,0 +1,41 @@ +using Moonlight.App.Database.Entities.Store; +using Moonlight.App.Database.Entities.Tickets; +using Moonlight.App.Database.Enums; +using Moonlight.App.Event; +using Moonlight.App.Extensions; +using Moonlight.App.Repositories; + +namespace Moonlight.App.Services.Ticketing; + +public class TicketCreateService +{ + private readonly Repository TicketRepository; + private readonly IdentityService IdentityService; + + public TicketCreateService(Repository ticketRepository, IdentityService identityService) + { + TicketRepository = ticketRepository; + IdentityService = identityService; + } + + public async Task Perform(string name, string description, string tries, Service? service) + { + var ticket = new Ticket() + { + Creator = IdentityService.CurrentUser, + Service = service, + Description = description, + Tries = tries, + Open = true, + CreatedAt = DateTime.UtcNow, + Name = name, + Priority = TicketPriority.Low + }; + + var finalTicket = TicketRepository.Add(ticket); + + await Events.OnTicketCreated.InvokeAsync(finalTicket); + + return finalTicket; + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/Ticketing/TicketService.cs b/Moonlight/App/Services/Ticketing/TicketService.cs new file mode 100644 index 00000000..b3b16cac --- /dev/null +++ b/Moonlight/App/Services/Ticketing/TicketService.cs @@ -0,0 +1,14 @@ +namespace Moonlight.App.Services.Ticketing; + +public class TicketService +{ + private readonly IServiceProvider ServiceProvider; + + public TicketChatService Chat => ServiceProvider.GetRequiredService(); + public TicketCreateService Create => ServiceProvider.GetRequiredService(); + + public TicketService(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/Users/UserDeleteService.cs b/Moonlight/App/Services/Users/UserDeleteService.cs index c9ce8daa..025802cf 100644 --- a/Moonlight/App/Services/Users/UserDeleteService.cs +++ b/Moonlight/App/Services/Users/UserDeleteService.cs @@ -2,6 +2,7 @@ using Moonlight.App.Database.Entities; using Moonlight.App.Database.Entities.Community; using Moonlight.App.Database.Entities.Store; +using Moonlight.App.Database.Entities.Tickets; using Moonlight.App.Repositories; using Moonlight.App.Services.Community; using Moonlight.App.Services.ServiceManage; @@ -16,6 +17,8 @@ public class UserDeleteService private readonly Repository TransactionRepository; private readonly Repository CouponUseRepository; private readonly Repository GiftCodeUseRepository; + private readonly Repository TicketRepository; + private readonly Repository TicketMessageRepository; private readonly ServiceService ServiceService; private readonly PostService PostService; @@ -27,7 +30,9 @@ public class UserDeleteService Repository userRepository, Repository giftCodeUseRepository, Repository couponUseRepository, - Repository transactionRepository) + Repository transactionRepository, + Repository ticketRepository, + Repository ticketMessageRepository) { ServiceRepository = serviceRepository; ServiceService = serviceService; @@ -37,6 +42,8 @@ public class UserDeleteService GiftCodeUseRepository = giftCodeUseRepository; CouponUseRepository = couponUseRepository; TransactionRepository = transactionRepository; + TicketRepository = ticketRepository; + TicketMessageRepository = ticketMessageRepository; } public async Task Perform(User user) @@ -103,6 +110,42 @@ public class UserDeleteService foreach (var transaction in transactions) TransactionRepository.Delete(transaction); + // Tickets and ticket messages + + // First we need to fetch every message this user has sent and delete it as admin accounts can have messages + // in tickets they dont own + var messagesFromUser = TicketMessageRepository + .Get() + .Where(x => x.Sender.Id == user.Id) + .ToArray(); + + foreach (var message in messagesFromUser) + { + TicketMessageRepository.Delete(message); + } + + // Now we can only delete the tickets the user actually owns + var tickets = TicketRepository + .Get() + .Include(x => x.Messages) + .Where(x => x.Creator.Id == user.Id) + .ToArray(); + + foreach (var ticket in tickets) + { + var messages = ticket.Messages.ToArray(); // Cache message models + + ticket.Messages.Clear(); + TicketRepository.Update(ticket); + + foreach (var ticketMessage in messages) + { + TicketMessageRepository.Delete(ticketMessage); + } + + TicketRepository.Delete(ticket); + } + // User // We need to use this in order to entity framework not crashing because of the previous deleted data diff --git a/Moonlight/App/Services/Users/UserDetailsService.cs b/Moonlight/App/Services/Users/UserDetailsService.cs index 6fdf51f5..f1070ff0 100644 --- a/Moonlight/App/Services/Users/UserDetailsService.cs +++ b/Moonlight/App/Services/Users/UserDetailsService.cs @@ -16,17 +16,25 @@ public class UserDetailsService public async Task UpdateAvatar(User user, Stream stream, string fileName) { + if (user.Avatar != null) + { + await BucketService.Delete("avatars", user.Avatar, true); + } + var file = await BucketService.Store("avatars", stream, fileName); user.Avatar = file; UserRepository.Update(user); } - public Task UpdateAvatar(User user) // Overload to reset avatar + public async Task UpdateAvatar(User user) // Overload to reset avatar { + if (user.Avatar != null) + { + await BucketService.Delete("avatars", user.Avatar, true); + } + user.Avatar = null; UserRepository.Update(user); - - return Task.CompletedTask; } } \ No newline at end of file diff --git a/Moonlight/Moonlight.csproj b/Moonlight/Moonlight.csproj index e6aec375..00875d8b 100644 --- a/Moonlight/Moonlight.csproj +++ b/Moonlight/Moonlight.csproj @@ -44,9 +44,4 @@ - - - <_ContentIncludedByDefault Remove="storage\config.json" /> - - diff --git a/Moonlight/Pages/_Host.cshtml b/Moonlight/Pages/_Host.cshtml index 305ab94a..fb6551df 100644 --- a/Moonlight/Pages/_Host.cshtml +++ b/Moonlight/Pages/_Host.cshtml @@ -11,6 +11,8 @@ Moonlight + + diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index d2cf345e..b33d43e3 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -12,6 +12,7 @@ using Moonlight.App.Services.Community; using Moonlight.App.Services.Interop; using Moonlight.App.Services.ServiceManage; using Moonlight.App.Services.Store; +using Moonlight.App.Services.Ticketing; using Moonlight.App.Services.Sys; using Moonlight.App.Services.Users; using Moonlight.App.Services.Utils; @@ -26,6 +27,7 @@ Directory.CreateDirectory(PathBuilder.Dir("storage", "logs")); var logConfig = new LoggerConfiguration(); logConfig = logConfig.Enrich.FromLogContext() + .MinimumLevel.Debug() .WriteTo.Console( outputTemplate: "{Timestamp:HH:mm:ss} [{Level:u3}] {SourceContext} {Message:lj}{NewLine}{Exception}"); @@ -79,6 +81,11 @@ builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddSingleton(); +// Services / Ticketing +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + // Services builder.Services.AddScoped(); builder.Services.AddSingleton(configService); diff --git a/Moonlight/Shared/Components/Auth/MailVerify.razor b/Moonlight/Shared/Components/Auth/MailVerify.razor index 15bbe1a3..b80bca7c 100644 --- a/Moonlight/Shared/Components/Auth/MailVerify.razor +++ b/Moonlight/Shared/Components/Auth/MailVerify.razor @@ -28,7 +28,7 @@ - + } diff --git a/Moonlight/Shared/Components/Forms/ChatFileSelect.razor b/Moonlight/Shared/Components/Forms/ChatFileSelect.razor new file mode 100644 index 00000000..2256bc32 --- /dev/null +++ b/Moonlight/Shared/Components/Forms/ChatFileSelect.razor @@ -0,0 +1,56 @@ +@using Microsoft.AspNetCore.Components.Forms + +@inject ToastService ToastService + +@{ + var id = $"fileUpload{GetHashCode()}"; +} + +