From 313d1c6c09263be559aede619f71369bd391f6ce Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Tue, 6 Jun 2023 14:51:58 +0700 Subject: [PATCH 01/56] add: user group schema --- .../Interfaces/IApplicationDbContext.cs | 3 + src/Domain/Entities/Digital/UserGroup.cs | 9 + src/Domain/Entities/User.cs | 3 + .../Persistence/ApplicationDbContext.cs | 3 + .../Configurations/UserGroupConfiguration.cs | 25 + .../20230606074719_AddUserGroup.Designer.cs | 533 ++++++++++++++++++ .../Migrations/20230606074719_AddUserGroup.cs | 67 +++ .../ApplicationDbContextModelSnapshot.cs | 47 ++ 8 files changed, 690 insertions(+) create mode 100644 src/Domain/Entities/Digital/UserGroup.cs create mode 100644 src/Infrastructure/Persistence/Configurations/UserGroupConfiguration.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230606074719_AddUserGroup.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230606074719_AddUserGroup.cs diff --git a/src/Application/Common/Interfaces/IApplicationDbContext.cs b/src/Application/Common/Interfaces/IApplicationDbContext.cs index 128372e2..d95348c3 100644 --- a/src/Application/Common/Interfaces/IApplicationDbContext.cs +++ b/src/Application/Common/Interfaces/IApplicationDbContext.cs @@ -1,4 +1,5 @@ using Domain.Entities; +using Domain.Entities.Digital; using Domain.Entities.Physical; using Microsoft.EntityFrameworkCore; @@ -15,6 +16,8 @@ public interface IApplicationDbContext public DbSet Folders { get; } public DbSet Documents { get; } public DbSet Borrows { get; } + + public DbSet UserGroups { get; } Task SaveChangesAsync(CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Domain/Entities/Digital/UserGroup.cs b/src/Domain/Entities/Digital/UserGroup.cs new file mode 100644 index 00000000..23355d1e --- /dev/null +++ b/src/Domain/Entities/Digital/UserGroup.cs @@ -0,0 +1,9 @@ +using Domain.Common; + +namespace Domain.Entities.Digital; + +public class UserGroup : BaseEntity +{ + public string Name { get; set; } = null!; + public ICollection Users { get; set; } = new List(); +} \ No newline at end of file diff --git a/src/Domain/Entities/User.cs b/src/Domain/Entities/User.cs index cc8c2345..527300bc 100644 --- a/src/Domain/Entities/User.cs +++ b/src/Domain/Entities/User.cs @@ -1,4 +1,5 @@ using Domain.Common; +using Domain.Entities.Digital; using Domain.Entities.Physical; namespace Domain.Entities; @@ -15,4 +16,6 @@ public class User : BaseAuditableEntity public string? Position { get; set; } public bool IsActive { get; set; } public bool IsActivated { get; set; } + + public ICollection UserGroups { get; set; } = new List(); } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/ApplicationDbContext.cs b/src/Infrastructure/Persistence/ApplicationDbContext.cs index dbae579c..90b5f6a6 100644 --- a/src/Infrastructure/Persistence/ApplicationDbContext.cs +++ b/src/Infrastructure/Persistence/ApplicationDbContext.cs @@ -1,6 +1,7 @@ using System.Reflection; using Application.Common.Interfaces; using Domain.Entities; +using Domain.Entities.Digital; using Domain.Entities.Physical; using Infrastructure.Common; using MediatR; @@ -26,6 +27,8 @@ public ApplicationDbContext( public DbSet Folders => Set(); public DbSet Documents => Set(); public DbSet Borrows => Set(); + + public DbSet UserGroups => Set(); public DbSet RefreshTokens => Set(); diff --git a/src/Infrastructure/Persistence/Configurations/UserGroupConfiguration.cs b/src/Infrastructure/Persistence/Configurations/UserGroupConfiguration.cs new file mode 100644 index 00000000..bd18e3ac --- /dev/null +++ b/src/Infrastructure/Persistence/Configurations/UserGroupConfiguration.cs @@ -0,0 +1,25 @@ +using Domain.Entities; +using Domain.Entities.Digital; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Infrastructure.Persistence.Configurations; + +public class UserGroupConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(x => x.Id); + builder.Property(x => x.Id) + .ValueGeneratedOnAdd(); + + builder.HasMany(x => x.Users) + .WithMany(x => x.UserGroups).UsingEntity( + "Memberships", + l => l.HasOne(typeof(User)).WithMany().HasForeignKey("UserId").HasPrincipalKey(nameof(User.Id)), + r => r.HasOne(typeof(UserGroup)).WithMany().HasForeignKey("UserGroupId").HasPrincipalKey(nameof(UserGroup.Id)), + j => j.HasKey("UserId", "UserGroupId")); + + builder.HasAlternateKey(x => x.Name); + } +} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Migrations/20230606074719_AddUserGroup.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230606074719_AddUserGroup.Designer.cs new file mode 100644 index 00000000..132fc48c --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230606074719_AddUserGroup.Designer.cs @@ -0,0 +1,533 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230606074719_AddUserGroup")] + partial class AddUserGroup + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId") + .IsUnique(); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithOne("Room") + .HasForeignKey("Domain.Entities.Physical.Room", "DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230606074719_AddUserGroup.cs b/src/Infrastructure/Persistence/Migrations/20230606074719_AddUserGroup.cs new file mode 100644 index 00000000..db90a1c1 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230606074719_AddUserGroup.cs @@ -0,0 +1,67 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class AddUserGroup : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "UserGroups", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserGroups", x => x.Id); + table.UniqueConstraint("AK_UserGroups_Name", x => x.Name); + }); + + migrationBuilder.CreateTable( + name: "Memberships", + columns: table => new + { + UserId = table.Column(type: "uuid", nullable: false), + UserGroupId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Memberships", x => new { x.UserId, x.UserGroupId }); + table.ForeignKey( + name: "FK_Memberships_UserGroups_UserGroupId", + column: x => x.UserGroupId, + principalTable: "UserGroups", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Memberships_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Memberships_UserGroupId", + table: "Memberships", + column: "UserGroupId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Memberships"); + + migrationBuilder.DropTable( + name: "UserGroups"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index 26a343b3..2b243d08 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -41,6 +41,23 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Departments"); }); + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => { b.Property("Id") @@ -346,6 +363,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Users"); }); + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => { b.HasOne("Domain.Entities.User", "Borrower") @@ -456,6 +488,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Department"); }); + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("Domain.Entities.Department", b => { b.Navigation("Room"); From 175418b0a76a21aad403da2bdf106c107ee4cafe Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Tue, 6 Jun 2023 16:16:26 +0700 Subject: [PATCH 02/56] add: digital files and entries schema --- .../Interfaces/IApplicationDbContext.cs | 2 + src/Domain/Entities/Digital/Entry.cs | 10 + src/Domain/Entities/Digital/FileEntity.cs | 9 + src/Domain/Entities/Physical/Document.cs | 4 + .../Persistence/ApplicationDbContext.cs | 2 + .../Configurations/DocumentConfiguration.cs | 5 + .../Configurations/EntryConfiguration.cs | 22 + .../Configurations/FileConfiguration.cs | 22 + ...6090121_AddDigitalFileAndEntry.Designer.cs | 599 ++++++++++++++++++ .../20230606090121_AddDigitalFileAndEntry.cs | 93 +++ .../ApplicationDbContextModelSnapshot.cs | 66 ++ 11 files changed, 834 insertions(+) create mode 100644 src/Domain/Entities/Digital/Entry.cs create mode 100644 src/Domain/Entities/Digital/FileEntity.cs create mode 100644 src/Infrastructure/Persistence/Configurations/EntryConfiguration.cs create mode 100644 src/Infrastructure/Persistence/Configurations/FileConfiguration.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230606090121_AddDigitalFileAndEntry.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230606090121_AddDigitalFileAndEntry.cs diff --git a/src/Application/Common/Interfaces/IApplicationDbContext.cs b/src/Application/Common/Interfaces/IApplicationDbContext.cs index d95348c3..8f12f9bd 100644 --- a/src/Application/Common/Interfaces/IApplicationDbContext.cs +++ b/src/Application/Common/Interfaces/IApplicationDbContext.cs @@ -18,6 +18,8 @@ public interface IApplicationDbContext public DbSet Borrows { get; } public DbSet UserGroups { get; } + public DbSet Files { get; } + public DbSet Entries { get; } Task SaveChangesAsync(CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Domain/Entities/Digital/Entry.cs b/src/Domain/Entities/Digital/Entry.cs new file mode 100644 index 00000000..03e41626 --- /dev/null +++ b/src/Domain/Entities/Digital/Entry.cs @@ -0,0 +1,10 @@ +using Domain.Common; + +namespace Domain.Entities.Digital; + +public class Entry : BaseEntity +{ + public string Name { get; set; } = null!; + public string Path { get; set; } = null!; + public FileEntity? File { get; set; } +} \ No newline at end of file diff --git a/src/Domain/Entities/Digital/FileEntity.cs b/src/Domain/Entities/Digital/FileEntity.cs new file mode 100644 index 00000000..e965dbc7 --- /dev/null +++ b/src/Domain/Entities/Digital/FileEntity.cs @@ -0,0 +1,9 @@ +using Domain.Common; + +namespace Domain.Entities.Digital; + +public class FileEntity : BaseEntity +{ + public string FileType { get; set; } = null!; + public byte[] FileData { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Domain/Entities/Physical/Document.cs b/src/Domain/Entities/Physical/Document.cs index 81313b56..a473a2b5 100644 --- a/src/Domain/Entities/Physical/Document.cs +++ b/src/Domain/Entities/Physical/Document.cs @@ -1,4 +1,5 @@ using Domain.Common; +using Domain.Entities.Digital; using Domain.Statuses; namespace Domain.Entities.Physical; @@ -12,4 +13,7 @@ public class Document : BaseEntity public User? Importer { get; set; } public Folder? Folder { get; set; } public DocumentStatus Status { get; set; } + public Guid? EntryId { get; set; } + + public virtual Entry? Entry { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/ApplicationDbContext.cs b/src/Infrastructure/Persistence/ApplicationDbContext.cs index 90b5f6a6..0b8da3db 100644 --- a/src/Infrastructure/Persistence/ApplicationDbContext.cs +++ b/src/Infrastructure/Persistence/ApplicationDbContext.cs @@ -29,6 +29,8 @@ public ApplicationDbContext( public DbSet Borrows => Set(); public DbSet UserGroups => Set(); + public DbSet Files => Set(); + public DbSet Entries => Set(); public DbSet RefreshTokens => Set(); diff --git a/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs b/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs index 4c7033c5..a9d89058 100644 --- a/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs +++ b/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs @@ -41,5 +41,10 @@ public void Configure(EntityTypeBuilder builder) builder.Property(x => x.Status) .IsRequired(); + + builder.HasOne(x => x.Entry) + .WithOne() + .HasForeignKey(x => x.EntryId) + .IsRequired(false); } } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Configurations/EntryConfiguration.cs b/src/Infrastructure/Persistence/Configurations/EntryConfiguration.cs new file mode 100644 index 00000000..3af84a01 --- /dev/null +++ b/src/Infrastructure/Persistence/Configurations/EntryConfiguration.cs @@ -0,0 +1,22 @@ +using Domain.Entities.Digital; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Infrastructure.Persistence.Configurations; + +public class EntryConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(x => x.Id); + builder.Property(x => x.Id) + .ValueGeneratedOnAdd(); + + builder.Property(x => x.Name) + .HasMaxLength(256) + .IsRequired(); + + builder.Property(x => x.Path) + .IsRequired(); + } +} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Configurations/FileConfiguration.cs b/src/Infrastructure/Persistence/Configurations/FileConfiguration.cs new file mode 100644 index 00000000..d1f34b90 --- /dev/null +++ b/src/Infrastructure/Persistence/Configurations/FileConfiguration.cs @@ -0,0 +1,22 @@ +using Domain.Entities.Digital; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Infrastructure.Persistence.Configurations; + +public class FileConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(x => x.Id); + builder.Property(x => x.Id) + .ValueGeneratedOnAdd(); + + builder.Property(x => x.FileType) + .HasMaxLength(256) + .IsRequired(); + + builder.Property(x => x.FileData) + .IsRequired(); + } +} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Migrations/20230606090121_AddDigitalFileAndEntry.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230606090121_AddDigitalFileAndEntry.Designer.cs new file mode 100644 index 00000000..518b226a --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230606090121_AddDigitalFileAndEntry.Designer.cs @@ -0,0 +1,599 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230606090121_AddDigitalFileAndEntry")] + partial class AddDigitalFileAndEntry + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId"); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId") + .IsUnique(); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithMany() + .HasForeignKey("FileId"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Entry"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithOne("Room") + .HasForeignKey("Domain.Entities.Physical.Room", "DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230606090121_AddDigitalFileAndEntry.cs b/src/Infrastructure/Persistence/Migrations/20230606090121_AddDigitalFileAndEntry.cs new file mode 100644 index 00000000..07308e76 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230606090121_AddDigitalFileAndEntry.cs @@ -0,0 +1,93 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class AddDigitalFileAndEntry : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "EntryId", + table: "Documents", + type: "uuid", + nullable: true); + + migrationBuilder.CreateTable( + name: "Files", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + FileType = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + FileData = table.Column(type: "bytea", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Files", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Entries", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + Path = table.Column(type: "text", nullable: false), + FileId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Entries", x => x.Id); + table.ForeignKey( + name: "FK_Entries_Files_FileId", + column: x => x.FileId, + principalTable: "Files", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_Documents_EntryId", + table: "Documents", + column: "EntryId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Entries_FileId", + table: "Entries", + column: "FileId"); + + migrationBuilder.AddForeignKey( + name: "FK_Documents_Entries_EntryId", + table: "Documents", + column: "EntryId", + principalTable: "Entries", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Documents_Entries_EntryId", + table: "Documents"); + + migrationBuilder.DropTable( + name: "Entries"); + + migrationBuilder.DropTable( + name: "Files"); + + migrationBuilder.DropIndex( + name: "IX_Documents_EntryId", + table: "Documents"); + + migrationBuilder.DropColumn( + name: "EntryId", + table: "Documents"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index 2b243d08..0a85ea8b 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -41,6 +41,51 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Departments"); }); + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId"); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => { b.Property("Id") @@ -113,6 +158,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(64) .HasColumnType("character varying(64)"); + b.Property("EntryId") + .HasColumnType("uuid"); + b.Property("FolderId") .HasColumnType("uuid"); @@ -131,6 +179,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("DepartmentId"); + b.HasIndex("EntryId") + .IsUnique(); + b.HasIndex("FolderId"); b.HasIndex("ImporterId"); @@ -378,6 +429,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Memberships"); }); + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithMany() + .HasForeignKey("FileId"); + + b.Navigation("File"); + }); + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => { b.HasOne("Domain.Entities.User", "Borrower") @@ -403,6 +463,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .WithMany() .HasForeignKey("DepartmentId"); + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + b.HasOne("Domain.Entities.Physical.Folder", "Folder") .WithMany("Documents") .HasForeignKey("FolderId"); @@ -413,6 +477,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Department"); + b.Navigation("Entry"); + b.Navigation("Folder"); b.Navigation("Importer"); From 7b7f01c27c78cbb2829a1cfb2f775e78e3789243 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Tue, 6 Jun 2023 16:39:11 +0700 Subject: [PATCH 03/56] update: digital files and entries schema --- src/Domain/Entities/Digital/Entry.cs | 4 +++- .../Persistence/Configurations/EntryConfiguration.cs | 5 +++++ ...=> 20230606092912_AddDigitalFileAndEntry.Designer.cs} | 9 +++++---- ...Entry.cs => 20230606092912_AddDigitalFileAndEntry.cs} | 3 ++- .../Migrations/ApplicationDbContextModelSnapshot.cs | 7 ++++--- 5 files changed, 19 insertions(+), 9 deletions(-) rename src/Infrastructure/Persistence/Migrations/{20230606090121_AddDigitalFileAndEntry.Designer.cs => 20230606092912_AddDigitalFileAndEntry.Designer.cs} (98%) rename src/Infrastructure/Persistence/Migrations/{20230606090121_AddDigitalFileAndEntry.cs => 20230606092912_AddDigitalFileAndEntry.cs} (97%) diff --git a/src/Domain/Entities/Digital/Entry.cs b/src/Domain/Entities/Digital/Entry.cs index 03e41626..712bc0c5 100644 --- a/src/Domain/Entities/Digital/Entry.cs +++ b/src/Domain/Entities/Digital/Entry.cs @@ -6,5 +6,7 @@ public class Entry : BaseEntity { public string Name { get; set; } = null!; public string Path { get; set; } = null!; - public FileEntity? File { get; set; } + public Guid? FileId { get; set; } + + public virtual FileEntity? File { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Configurations/EntryConfiguration.cs b/src/Infrastructure/Persistence/Configurations/EntryConfiguration.cs index 3af84a01..433d872e 100644 --- a/src/Infrastructure/Persistence/Configurations/EntryConfiguration.cs +++ b/src/Infrastructure/Persistence/Configurations/EntryConfiguration.cs @@ -18,5 +18,10 @@ public void Configure(EntityTypeBuilder builder) builder.Property(x => x.Path) .IsRequired(); + + builder.HasOne(x => x.File) + .WithOne() + .HasForeignKey(x => x.FileId) + .IsRequired(false); } } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Migrations/20230606090121_AddDigitalFileAndEntry.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230606092912_AddDigitalFileAndEntry.Designer.cs similarity index 98% rename from src/Infrastructure/Persistence/Migrations/20230606090121_AddDigitalFileAndEntry.Designer.cs rename to src/Infrastructure/Persistence/Migrations/20230606092912_AddDigitalFileAndEntry.Designer.cs index 518b226a..63d37967 100644 --- a/src/Infrastructure/Persistence/Migrations/20230606090121_AddDigitalFileAndEntry.Designer.cs +++ b/src/Infrastructure/Persistence/Migrations/20230606092912_AddDigitalFileAndEntry.Designer.cs @@ -13,7 +13,7 @@ namespace Infrastructure.Persistence.Migrations { [DbContext(typeof(ApplicationDbContext))] - [Migration("20230606090121_AddDigitalFileAndEntry")] + [Migration("20230606092912_AddDigitalFileAndEntry")] partial class AddDigitalFileAndEntry { /// @@ -64,7 +64,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.HasIndex("FileId"); + b.HasIndex("FileId") + .IsUnique(); b.ToTable("Entries"); }); @@ -435,8 +436,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("Domain.Entities.Digital.Entry", b => { b.HasOne("Domain.Entities.Digital.FileEntity", "File") - .WithMany() - .HasForeignKey("FileId"); + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); b.Navigation("File"); }); diff --git a/src/Infrastructure/Persistence/Migrations/20230606090121_AddDigitalFileAndEntry.cs b/src/Infrastructure/Persistence/Migrations/20230606092912_AddDigitalFileAndEntry.cs similarity index 97% rename from src/Infrastructure/Persistence/Migrations/20230606090121_AddDigitalFileAndEntry.cs rename to src/Infrastructure/Persistence/Migrations/20230606092912_AddDigitalFileAndEntry.cs index 07308e76..ecd2d66a 100644 --- a/src/Infrastructure/Persistence/Migrations/20230606090121_AddDigitalFileAndEntry.cs +++ b/src/Infrastructure/Persistence/Migrations/20230606092912_AddDigitalFileAndEntry.cs @@ -58,7 +58,8 @@ protected override void Up(MigrationBuilder migrationBuilder) migrationBuilder.CreateIndex( name: "IX_Entries_FileId", table: "Entries", - column: "FileId"); + column: "FileId", + unique: true); migrationBuilder.AddForeignKey( name: "FK_Documents_Entries_EntryId", diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index 0a85ea8b..deeca7c5 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -61,7 +61,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.HasIndex("FileId"); + b.HasIndex("FileId") + .IsUnique(); b.ToTable("Entries"); }); @@ -432,8 +433,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Domain.Entities.Digital.Entry", b => { b.HasOne("Domain.Entities.Digital.FileEntity", "File") - .WithMany() - .HasForeignKey("FileId"); + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); b.Navigation("File"); }); From 3442efff2b10a2c41006e60ec08ab9fbed2ea432 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Tue, 6 Jun 2023 21:25:40 +0700 Subject: [PATCH 04/56] add: dtos for new tables --- .../Common/Models/Dtos/Digital/EntryDto.cs | 12 ++++++++++++ .../Common/Models/Dtos/Digital/FileDto.cs | 10 ++++++++++ .../Common/Models/Dtos/Digital/UserGroupDto.cs | 10 ++++++++++ .../Common/Models/Dtos/Physical/DocumentDto.cs | 4 +++- src/Application/Common/Models/Dtos/UserDto.cs | 3 +++ .../Common/Mappings/MappingTests.cs | 5 +++++ 6 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 src/Application/Common/Models/Dtos/Digital/EntryDto.cs create mode 100644 src/Application/Common/Models/Dtos/Digital/FileDto.cs create mode 100644 src/Application/Common/Models/Dtos/Digital/UserGroupDto.cs diff --git a/src/Application/Common/Models/Dtos/Digital/EntryDto.cs b/src/Application/Common/Models/Dtos/Digital/EntryDto.cs new file mode 100644 index 00000000..fe5445ab --- /dev/null +++ b/src/Application/Common/Models/Dtos/Digital/EntryDto.cs @@ -0,0 +1,12 @@ +using Application.Common.Mappings; +using Domain.Entities.Digital; + +namespace Application.Common.Models.Dtos.Digital; + +public class EntryDto : IMapFrom +{ + public Guid Id { get; set; } + public string Name { get; set; } = null!; + public string Path { get; set; } = null!; + public FileDto? File { get; set; } +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Digital/FileDto.cs b/src/Application/Common/Models/Dtos/Digital/FileDto.cs new file mode 100644 index 00000000..cf2f7531 --- /dev/null +++ b/src/Application/Common/Models/Dtos/Digital/FileDto.cs @@ -0,0 +1,10 @@ +using Application.Common.Mappings; +using Domain.Entities.Digital; + +namespace Application.Common.Models.Dtos.Digital; + +public class FileDto : IMapFrom +{ + public Guid Id { get; set; } + public string FileType { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Digital/UserGroupDto.cs b/src/Application/Common/Models/Dtos/Digital/UserGroupDto.cs new file mode 100644 index 00000000..88fb1d13 --- /dev/null +++ b/src/Application/Common/Models/Dtos/Digital/UserGroupDto.cs @@ -0,0 +1,10 @@ +using Application.Common.Mappings; +using Domain.Entities.Digital; + +namespace Application.Common.Models.Dtos.Digital; + +public class UserGroupDto : IMapFrom +{ + public Guid Id { get; set; } + public string Name { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs b/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs index 994b3ac8..6c517ce1 100644 --- a/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs @@ -1,4 +1,5 @@ using Application.Common.Mappings; +using Application.Common.Models.Dtos.Digital; using Application.Users.Queries; using AutoMapper; using Domain.Entities.Physical; @@ -14,7 +15,8 @@ public class DocumentDto : IMapFrom public DepartmentDto? Department { get; set; } public UserDto? Importer { get; set; } public FolderDto? Folder { get; set; } - public string Status { get; set; } + public string Status { get; set; } = null!; + public EntryDto? Entry { get; set; } public void Mapping(Profile profile) { diff --git a/src/Application/Common/Models/Dtos/UserDto.cs b/src/Application/Common/Models/Dtos/UserDto.cs index 64cf73dc..3fe39e96 100644 --- a/src/Application/Common/Models/Dtos/UserDto.cs +++ b/src/Application/Common/Models/Dtos/UserDto.cs @@ -2,6 +2,7 @@ using Application.Common.Models.Dtos; using AutoMapper; using Domain.Entities; +using Domain.Entities.Digital; namespace Application.Users.Queries; @@ -22,6 +23,8 @@ public class UserDto : IMapFrom public DateTime? LastModified { get; set; } public Guid? LastModifiedBy { get; set; } + public IEnumerable UserGroups { get; set; } + public void Mapping(Profile profile) { profile.CreateMap() diff --git a/tests/Application.Tests.Unit/Common/Mappings/MappingTests.cs b/tests/Application.Tests.Unit/Common/Mappings/MappingTests.cs index 28df4253..49db0eca 100644 --- a/tests/Application.Tests.Unit/Common/Mappings/MappingTests.cs +++ b/tests/Application.Tests.Unit/Common/Mappings/MappingTests.cs @@ -1,10 +1,12 @@ using System.Runtime.Serialization; using Application.Common.Mappings; using Application.Common.Models.Dtos; +using Application.Common.Models.Dtos.Digital; using Application.Common.Models.Dtos.Physical; using Application.Users.Queries; using AutoMapper; using Domain.Entities; +using Domain.Entities.Digital; using Domain.Entities.Physical; using Xunit; @@ -43,6 +45,9 @@ public void ShouldHaveValidConfiguration() [InlineData(typeof(Document), typeof(DocumentItemDto))] [InlineData(typeof(Borrow), typeof(BorrowDto))] [InlineData(typeof(RefreshToken), typeof(RefreshTokenDto))] + [InlineData(typeof(FileEntity), typeof(FileDto))] + [InlineData(typeof(Entry), typeof(EntryDto))] + [InlineData(typeof(UserGroup), typeof(UserGroupDto))] public void ShouldSupportMappingFromSourceToDestination(Type source, Type destination) { // Arrange From 305856a064c84e3e60b7b0230565f6482e7d06ba Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Tue, 6 Jun 2023 22:23:33 +0700 Subject: [PATCH 05/56] add: base methods for entities --- .../BaseClassFixture.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/Application.Tests.Integration/BaseClassFixture.cs b/tests/Application.Tests.Integration/BaseClassFixture.cs index 370fbc40..2dcda614 100644 --- a/tests/Application.Tests.Integration/BaseClassFixture.cs +++ b/tests/Application.Tests.Integration/BaseClassFixture.cs @@ -1,7 +1,9 @@ +using System.Text; using Application.Helpers; using Bogus; using Domain.Common; using Domain.Entities; +using Domain.Entities.Digital; using Domain.Entities.Physical; using Infrastructure.Persistence; using MediatR; @@ -196,4 +198,36 @@ protected static Staff CreateStaff(User user, Room? room) Room = room, }; } + + protected static UserGroup CreateUserGroup(User[] users) + { + return new UserGroup() + { + Id = Guid.NewGuid(), + Name = new Faker().Commerce.ProductName(), + Users = users, + }; + } + + protected static FileEntity CreateFile() + { + return new FileEntity() + { + Id = Guid.NewGuid(), + FileType = new Faker().Database.Type(), + FileData = Encoding.ASCII.GetBytes(new Faker().Lorem.Random.Words()) + }; + } + + protected static Entry CreateEntry(FileEntity file) + { + return new Entry() + { + Id = Guid.NewGuid(), + Name = new Faker().Commerce.ProductName(), + File = file, + Path = new Faker().Commerce.ProductDescription(), + FileId = file.Id, + }; + } } \ No newline at end of file From 19f3969a34b3bf7b4351c7e59754420202c0864a Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Tue, 6 Jun 2023 22:29:38 +0700 Subject: [PATCH 06/56] update: migrations indexing --- ...rGroup.Designer.cs => 00000000000011_AddUserGroup.Designer.cs} | 0 ...30606074719_AddUserGroup.cs => 00000000000011_AddUserGroup.cs} | 0 ...igner.cs => 00000000000012_AddDigitalFileAndEntry.Designer.cs} | 0 ...alFileAndEntry.cs => 00000000000012_AddDigitalFileAndEntry.cs} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename src/Infrastructure/Persistence/Migrations/{20230606074719_AddUserGroup.Designer.cs => 00000000000011_AddUserGroup.Designer.cs} (100%) rename src/Infrastructure/Persistence/Migrations/{20230606074719_AddUserGroup.cs => 00000000000011_AddUserGroup.cs} (100%) rename src/Infrastructure/Persistence/Migrations/{20230606092912_AddDigitalFileAndEntry.Designer.cs => 00000000000012_AddDigitalFileAndEntry.Designer.cs} (100%) rename src/Infrastructure/Persistence/Migrations/{20230606092912_AddDigitalFileAndEntry.cs => 00000000000012_AddDigitalFileAndEntry.cs} (100%) diff --git a/src/Infrastructure/Persistence/Migrations/20230606074719_AddUserGroup.Designer.cs b/src/Infrastructure/Persistence/Migrations/00000000000011_AddUserGroup.Designer.cs similarity index 100% rename from src/Infrastructure/Persistence/Migrations/20230606074719_AddUserGroup.Designer.cs rename to src/Infrastructure/Persistence/Migrations/00000000000011_AddUserGroup.Designer.cs diff --git a/src/Infrastructure/Persistence/Migrations/20230606074719_AddUserGroup.cs b/src/Infrastructure/Persistence/Migrations/00000000000011_AddUserGroup.cs similarity index 100% rename from src/Infrastructure/Persistence/Migrations/20230606074719_AddUserGroup.cs rename to src/Infrastructure/Persistence/Migrations/00000000000011_AddUserGroup.cs diff --git a/src/Infrastructure/Persistence/Migrations/20230606092912_AddDigitalFileAndEntry.Designer.cs b/src/Infrastructure/Persistence/Migrations/00000000000012_AddDigitalFileAndEntry.Designer.cs similarity index 100% rename from src/Infrastructure/Persistence/Migrations/20230606092912_AddDigitalFileAndEntry.Designer.cs rename to src/Infrastructure/Persistence/Migrations/00000000000012_AddDigitalFileAndEntry.Designer.cs diff --git a/src/Infrastructure/Persistence/Migrations/20230606092912_AddDigitalFileAndEntry.cs b/src/Infrastructure/Persistence/Migrations/00000000000012_AddDigitalFileAndEntry.cs similarity index 100% rename from src/Infrastructure/Persistence/Migrations/20230606092912_AddDigitalFileAndEntry.cs rename to src/Infrastructure/Persistence/Migrations/00000000000012_AddDigitalFileAndEntry.cs From 1ed6b0139ecec5ff6d9d03dfb46d50c749fff636 Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Wed, 7 Jun 2023 15:27:26 +0700 Subject: [PATCH 07/56] test: add integration tests (#209) --- .../BaseClassFixture.cs | 4 +- .../Borrows/Commands/UpdateBorrowTests.cs | 186 ++++++++++++++++++ 2 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 tests/Application.Tests.Integration/Borrows/Commands/UpdateBorrowTests.cs diff --git a/tests/Application.Tests.Integration/BaseClassFixture.cs b/tests/Application.Tests.Integration/BaseClassFixture.cs index 133c19f9..0339e595 100644 --- a/tests/Application.Tests.Integration/BaseClassFixture.cs +++ b/tests/Application.Tests.Integration/BaseClassFixture.cs @@ -241,8 +241,8 @@ protected static Borrow CreateBorrowRequest(User borrower, Document document, Bo Document = document, Reason = "something something", Status = status, - BorrowTime = LocalDateTime.FromDateTime(DateTime.UtcNow), - DueTime = LocalDateTime.FromDateTime(DateTime.UtcNow + TimeSpan.FromDays(1)) + BorrowTime = LocalDateTime.FromDateTime(DateTime.Now), + DueTime = LocalDateTime.FromDateTime(DateTime.Now + TimeSpan.FromDays(1)) }; } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Borrows/Commands/UpdateBorrowTests.cs b/tests/Application.Tests.Integration/Borrows/Commands/UpdateBorrowTests.cs new file mode 100644 index 00000000..f00d3e25 --- /dev/null +++ b/tests/Application.Tests.Integration/Borrows/Commands/UpdateBorrowTests.cs @@ -0,0 +1,186 @@ +using Application.Borrows.Commands; +using Application.Common.Exceptions; +using Application.Identity; +using Domain.Entities.Physical; +using Domain.Statuses; +using FluentAssertions; +using Infrastructure.Persistence; +using Microsoft.Extensions.DependencyInjection; +using NodaTime; +using Xunit; + +namespace Application.Tests.Integration.Borrows.Commands; + +public class UpdateBorrowTests : BaseClassFixture +{ + public UpdateBorrowTests(CustomApiFactory apiFactory) : base(apiFactory) + { + + } + + [Fact] + public async Task ShouldUpdateBorrow_WhenDetailsAreValid() + { + // Arrange + var user = CreateUser(IdentityData.Roles.Employee, "aaaaaa"); + + var document = CreateNDocuments(1).First(); + + var borrow = CreateBorrowRequest(user, document, BorrowRequestStatus.Pending); + + await AddAsync(borrow); + + var command = new UpdateBorrow.Command() + { + Reason = "Example Update", + BorrowFrom = DateTime.Now.AddDays(3), + BorrowTo = DateTime.Now.AddDays(12), + BorrowId = borrow.Id, + }; + + // Act + var result = await SendAsync(command); + + // Assert + result.Reason.Should().Be(command.Reason); + result.BorrowTime.Should().Be(command.BorrowFrom); + result.DueTime.Should().Be(command.BorrowTo); + + // Cleanup + Remove(await FindAsync(borrow.Id)); + Remove(user); + Remove(document); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenRequestDoesNotExist() + { + // Arrange + var command = new UpdateBorrow.Command() + { + Reason = "adsda", + BorrowFrom = DateTime.Now.AddHours(1), + BorrowTo = DateTime.Now.AddHours(2), + BorrowId = Guid.NewGuid(), + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("Borrow request does not exist."); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenRequestStatusIsNotPending() + { + // Arrange + var user = CreateUser(IdentityData.Roles.Employee, "a"); + + var document = CreateNDocuments(1).First(); + + var borrow = CreateBorrowRequest(user, document, BorrowRequestStatus.Approved); + + await AddAsync(borrow); + + var command = new UpdateBorrow.Command() + { + Reason = "Example Update", + BorrowFrom = DateTime.Now.AddDays(3), + BorrowTo = DateTime.Now.AddDays(12), + BorrowId = borrow.Id, + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("Cannot update borrow request."); + + // Cleanup + Remove(borrow); + Remove(user); + Remove(document); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenDocumentIsLost() + { + // Arrange + var user = CreateUser(IdentityData.Roles.Employee, "a"); + + var document = CreateNDocuments(1).First(); + document.Status = DocumentStatus.Lost; + + var borrow = CreateBorrowRequest(user, document, BorrowRequestStatus.Pending); + + await AddAsync(borrow); + + var command = new UpdateBorrow.Command() + { + Reason = "Example Update", + BorrowFrom = DateTime.Now.AddDays(3), + BorrowTo = DateTime.Now.AddDays(12), + BorrowId = borrow.Id, + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("Document is lost."); + + // Cleanup + Remove(borrow); + Remove(user); + Remove(document); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenRequestTimespanOverlapAnApprovedOrCheckedOutRequestTimespan() + { + // Arrange + using var scope = ScopeFactory.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + + var user1 = CreateUser(IdentityData.Roles.Employee, "a"); + var user2 = CreateUser(IdentityData.Roles.Employee, "a"); + + var document = CreateNDocuments(1).First(); + + var borrow1 = CreateBorrowRequest(user1, document, BorrowRequestStatus.Pending); + var borrow2 = CreateBorrowRequest(user2, document, BorrowRequestStatus.Approved); + borrow2.BorrowTime = LocalDateTime.FromDateTime(DateTime.Now.AddDays(4)); + borrow2.DueTime = LocalDateTime.FromDateTime(DateTime.Now.AddDays(12)); + + await context.AddAsync(borrow1); + await context.AddAsync(borrow2); + + await context.SaveChangesAsync(); + + var command = new UpdateBorrow.Command() + { + Reason = "Example Update", + BorrowFrom = DateTime.Now.AddDays(5), + BorrowTo = DateTime.Now.AddDays(13), + BorrowId = borrow1.Id, + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("This document cannot be borrowed."); + + // Cleanup + Remove(borrow1); + Remove(borrow2); + Remove(user1); + Remove(user2); + Remove(document); + } +} \ No newline at end of file From 94c85c6e3af11c0175e62b6ff4a4d8439f64320b Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Wed, 7 Jun 2023 15:58:48 +0700 Subject: [PATCH 08/56] test: add integration tests for approve borrow request (#204) * test: add integration tests * fix: some stuff and add an assertion --- .../Commands/ApproveBorrowRequestTests.cs | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 tests/Application.Tests.Integration/Borrows/Commands/ApproveBorrowRequestTests.cs diff --git a/tests/Application.Tests.Integration/Borrows/Commands/ApproveBorrowRequestTests.cs b/tests/Application.Tests.Integration/Borrows/Commands/ApproveBorrowRequestTests.cs new file mode 100644 index 00000000..87a40fd6 --- /dev/null +++ b/tests/Application.Tests.Integration/Borrows/Commands/ApproveBorrowRequestTests.cs @@ -0,0 +1,168 @@ +using Application.Borrows.Commands; +using Application.Common.Exceptions; +using Application.Identity; +using Domain.Entities.Physical; +using Domain.Statuses; +using FluentAssertions; +using Infrastructure.Persistence; +using Microsoft.Extensions.DependencyInjection; +using NodaTime; +using Xunit; + +namespace Application.Tests.Integration.Borrows.Commands; + +public class ApproveBorrowRequestTests : BaseClassFixture +{ + public ApproveBorrowRequestTests(CustomApiFactory apiFactory) : base(apiFactory) + { + + } + + [Fact] + public async Task ShouldApproveRequest_WhenRequestIsValid() + { + // Arrange + var document = CreateNDocuments(1).First(); + + var user = CreateUser(IdentityData.Roles.Employee, "abcdef"); + + var request = CreateBorrowRequest(user, document, BorrowRequestStatus.Pending); + + await AddAsync(request); + + var command = new ApproveBorrowRequest.Command() + { + BorrowId = request.Id, + }; + + // Act + var result = await SendAsync(command); + + // Assert + result.Status.Should().Be(BorrowRequestStatus.Approved.ToString()); + + // Cleanup + Remove(request); + Remove(user); + Remove(document); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenRequestDoesNotExist() + { + // Arrange + var command = new ApproveBorrowRequest.Command() + { + BorrowId = Guid.NewGuid(), + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("Borrow request does not exist."); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenDocumentIsLost() + { + // Arrange + var document = CreateNDocuments(1).First(); + + document.Status = DocumentStatus.Lost; + + var user = CreateUser(IdentityData.Roles.Employee, "abcdef"); + + var request = CreateBorrowRequest(user, document, BorrowRequestStatus.Pending); + + await AddAsync(request); + + var command = new ApproveBorrowRequest.Command() + { + BorrowId = request.Id, + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("Document is lost. Request is unprocessable."); + (await FindAsync(request.Id))!.Status.Should().Be(BorrowRequestStatus.NotProcessable); + + // Cleanup + Remove(request); + Remove(user); + Remove(document); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenRequestStatusIsNotPendingAndRejected() + { + var document = CreateNDocuments(1).First(); + + var user = CreateUser(IdentityData.Roles.Employee, "abcdef"); + + var request = CreateBorrowRequest(user, document, BorrowRequestStatus.CheckedOut); + + await AddAsync(request); + + var command = new ApproveBorrowRequest.Command() + { + BorrowId = request.Id, + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("Request cannot be approved."); + + // Cleanup + Remove(request); + Remove(user); + Remove(document); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenRequestTimespanOverlapAnApprovedOrCheckedOutRequestTimespan() + { + using var scope = ScopeFactory.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + var document = CreateNDocuments(1).First(); + + var user1 = CreateUser(IdentityData.Roles.Employee, "abcdef"); + var user2 = CreateUser(IdentityData.Roles.Employee, "aaaaaa"); + + var request1 = CreateBorrowRequest(user1, document, BorrowRequestStatus.Approved); + + var request2 = CreateBorrowRequest(user2, document, BorrowRequestStatus.Pending); + request2.BorrowTime = request2.BorrowTime.Plus(Period.FromMinutes(30)); + request2.DueTime = request2.DueTime.Plus(Period.FromHours(1)); + + await context.AddAsync(request1); + await context.AddAsync(request2); + await context.SaveChangesAsync(); + + var command = new ApproveBorrowRequest.Command() + { + BorrowId = request2.Id, + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("This document cannot be borrowed."); + + // Cleanup + Remove(request1); + Remove(request2); + Remove(user1); + Remove(user2); + Remove(document); + } +} \ No newline at end of file From 3991b50cc66723b3afb806dba76da32c4a2ff5bd Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Wed, 7 Jun 2023 16:27:05 +0700 Subject: [PATCH 09/56] test: add integration tests for borrow document (#208) * test: add integration tests fix: time * fix and add stuffs * cleanup --- .../Borrows/Commands/BorrowDocumentTests.cs | 354 ++++++++++++++++++ 1 file changed, 354 insertions(+) create mode 100644 tests/Application.Tests.Integration/Borrows/Commands/BorrowDocumentTests.cs diff --git a/tests/Application.Tests.Integration/Borrows/Commands/BorrowDocumentTests.cs b/tests/Application.Tests.Integration/Borrows/Commands/BorrowDocumentTests.cs new file mode 100644 index 00000000..b595d20f --- /dev/null +++ b/tests/Application.Tests.Integration/Borrows/Commands/BorrowDocumentTests.cs @@ -0,0 +1,354 @@ +using Application.Borrows.Commands; +using Application.Common.Exceptions; +using Application.Identity; +using Domain.Entities; +using Domain.Entities.Physical; +using Domain.Statuses; +using FluentAssertions; +using Infrastructure.Persistence; +using Microsoft.Extensions.DependencyInjection; +using NodaTime; +using Xunit; + +namespace Application.Tests.Integration.Borrows.Commands; + +public class BorrowDocumentTests : BaseClassFixture +{ + public BorrowDocumentTests(CustomApiFactory apiFactory) : base(apiFactory) + { + + } + + [Fact] + public async Task ShouldCreateBorrowRequest_WhenDetailsAreValid() + { + // Arrange + using var scope = ScopeFactory.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + var department = CreateDepartment(); + await context.AddAsync(department); + + var user = CreateUser(IdentityData.Roles.Employee, "aaaaaa"); + user.Department = department; + await context.AddAsync(user); + + var document = CreateNDocuments(1).First(); + document.Status = DocumentStatus.Available; + document.Department = department; + await context.AddAsync(document); + await context.SaveChangesAsync(); + + var command = new BorrowDocument.Command() + { + BorrowerId = user.Id, + DocumentId = document.Id, + Reason = "Example", + BorrowFrom = DateTime.Now.Add(TimeSpan.FromHours(1)), + BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(1)), + }; + + // Act + var result = await SendAsync(command); + + // Assert + result.DocumentId.Should().Be(command.DocumentId); + result.BorrowerId.Should().Be(command.BorrowerId); + result.Reason.Should().Be(command.Reason); + result.BorrowTime.Should().Be(command.BorrowFrom); + result.DueTime.Should().Be(command.BorrowTo); + result.Status.Should().Be(BorrowRequestStatus.Pending.ToString()); + + // Cleanup + Remove(await FindAsync(result.Id)); + Remove(user); + Remove(document); + Remove(await FindAsync(department.Id)); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenUserDoesNotExist() + { + // Arrange + var document = CreateNDocuments(1).First(); + document.Status = DocumentStatus.Available; + await AddAsync(document); + + var command = new BorrowDocument.Command() + { + BorrowerId = Guid.NewGuid(), + DocumentId = document.Id, + BorrowFrom = DateTime.Now.Add(TimeSpan.FromDays(1)), + BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), + Reason = "Example", + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("User does not exist."); + + // Cleanup + Remove(document); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenUserIsNotActive() + { + // Arrange + var document = CreateNDocuments(1).First(); + document.Status = DocumentStatus.Available; + await AddAsync(document); + + var user = CreateUser(IdentityData.Roles.Employee, "aaaaaa"); + user.IsActive = false; + await AddAsync(user); + + var command = new BorrowDocument.Command() + { + BorrowerId = user.Id, + DocumentId = document.Id, + BorrowFrom = DateTime.Now.Add(TimeSpan.FromDays(1)), + BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), + Reason = "Example", + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("User is not active."); + + // Cleanup + Remove(document); + Remove(user); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenUserIsNotActivated() + { + // Arrange + var document = CreateNDocuments(1).First(); + document.Status = DocumentStatus.Available; + await AddAsync(document); + + var user = CreateUser(IdentityData.Roles.Employee, "aaaaaa"); + user.IsActivated = false; + await AddAsync(user); + + var command = new BorrowDocument.Command() + { + BorrowerId = user.Id, + DocumentId = document.Id, + BorrowFrom = DateTime.Now.Add(TimeSpan.FromDays(1)), + BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), + Reason = "Example", + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("User is not activated."); + + // Cleanup + Remove(document); + Remove(user); + } + + [Fact] + public async Task ShouldThrowKeyNotFoundException_WhenDocumentDoesNotExist() + { + // Arrange + var user = CreateUser(IdentityData.Roles.Employee, "bbbbbb"); + await AddAsync(user); + + var command = new BorrowDocument.Command() + { + BorrowerId = user.Id, + DocumentId = Guid.NewGuid(), + BorrowFrom = DateTime.Now.Add(TimeSpan.FromDays(1)), + BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), + Reason = "Example", + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("Document does not exist."); + + // Cleanup + Remove(user); + } + + [Fact] + public async Task ShouldConflictException_WhenDocumentIsNotAvailable() + { + // Arrange + var user = CreateUser(IdentityData.Roles.Employee, "bbbbbb"); + await AddAsync(user); + + var document = CreateNDocuments(1).First(); + document.Status = DocumentStatus.Borrowed; + await AddAsync(document); + + var command = new BorrowDocument.Command() + { + BorrowerId = user.Id, + DocumentId = document.Id, + BorrowFrom = DateTime.Now.Add(TimeSpan.FromDays(1)), + BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), + Reason = "Example", + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("Document is not available."); + + // Cleanup + Remove(user); + Remove(document); + } + + [Fact] + public async Task ShouldConflictException_WhenUserAndDocumentDoesNotBelongToTheSameDepartment() + { + // Arrange + using var scope = ScopeFactory.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + var department1 = CreateDepartment(); + var department2 = CreateDepartment(); + + var user = CreateUser(IdentityData.Roles.Employee, "bbbbbb"); + user.Department = department1; + await context.AddAsync(user); + + var document = CreateNDocuments(1).First(); + document.Status = DocumentStatus.Available; + document.Department = department2; + await context.AddAsync(document); + + await context.SaveChangesAsync(); + + var command = new BorrowDocument.Command() + { + BorrowerId = user.Id, + DocumentId = document.Id, + BorrowFrom = DateTime.Now.Add(TimeSpan.FromDays(1)), + BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), + Reason = "Example", + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("User is not allowed to borrow this document."); + + // Cleanup + Remove(user); + Remove(document); + Remove(department1); + Remove(department2); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenRequestWithSameUserAndDocumentAlreadyExists() + { + // Arrange + using var scope = ScopeFactory.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + var department = CreateDepartment(); + + var user = CreateUser(IdentityData.Roles.Employee, "bbbbbb"); + user.Department = department; + + var document = CreateNDocuments(1).First(); + document.Status = DocumentStatus.Available; + document.Department = department; + + var borrow = CreateBorrowRequest(user, document, BorrowRequestStatus.Pending); + await context.AddAsync(borrow); + + await context.SaveChangesAsync(); + + var command = new BorrowDocument.Command() + { + BorrowerId = user.Id, + DocumentId = document.Id, + BorrowFrom = DateTime.Now.Add(TimeSpan.FromDays(1)), + BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), + Reason = "Example", + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("This document is already requested borrow from the same user."); + + // Cleanup; + Remove(borrow); + Remove(user); + Remove(document); + Remove(department); + } + + [Fact] + public async Task ShouldThrowConflictException_WhenARequestIsMadeWhileDocumentIsAlreadyBeingBorrowed() + { + // Arrange + using var scope = ScopeFactory.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + var department = CreateDepartment(); + + var user1 = CreateUser(IdentityData.Roles.Employee, "bbbbbb"); + user1.Department = department; + + var user2 = CreateUser(IdentityData.Roles.Employee, "bbbbbb"); + user2.Department = department; + await context.AddAsync(user2); + + var document = CreateNDocuments(1).First(); + document.Status = DocumentStatus.Available; + document.Department = department; + + var borrow = CreateBorrowRequest(user1, document, BorrowRequestStatus.Approved); + await context.AddAsync(borrow); + + await context.SaveChangesAsync(); + + var command = new BorrowDocument.Command() + { + BorrowerId = user2.Id, + DocumentId = document.Id, + BorrowFrom = DateTime.Now.AddHours(1), + BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), + Reason = "Example", + }; + + // Act + var result = async () => await SendAsync(command); + + // Assert + await result.Should().ThrowAsync() + .WithMessage("This document cannot be borrowed."); + + // Cleanup; + Remove(borrow); + Remove(user1); + Remove(user2); + Remove(document); + Remove(department); + } +} \ No newline at end of file From d577f593304041b7d9f9956a34bcfe42fec2fb31 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Thu, 8 Jun 2023 08:58:25 +0700 Subject: [PATCH 10/56] Feat/reset password (#215) * add: reset password token entity, configuration, and added to app db context also renamed the parameter of OnModelCreating in ApplicationDbContext.cs to modelBuilder to satisfy SonarLint * chore: refactored SendResetPasswordHtmlMail method of IMailService.cs also: - Pass a reset password token hash so frontend can call the right link - Change one template variable name to ResetPasswordTokenHash and send the token along - Renamed HTMLMailData.cs to HtmlMailData.cs to satisfy sonarlint * add: IAuthDbContext.cs and registered it * chore: refactored IdentityService to be using abstractions instead of implementations * chore: add reset token creation logic to UserCreatedEventHandler.cs * chore: UserId foreign key naming in configuration * chore: add migrations for reset password token table * chore: modified service a little bit - Changing the way service registrations work so that 2 interfaces share the same instance - Pass the whole entity to event so that auth db context can get its id - I forgot the db context model snapshot * chore: tweaks how tokens are sent - send the token instead of the hashed version of it * add: endpoint and service to reset password --- src/Api/Controllers/AuthController.cs | 17 + .../Requests/Auth/ResetPasswordRequest.cs | 8 + .../Common/Interfaces/IAuthDbContext.cs | 12 + .../Common/Interfaces/IIdentityService.cs | 1 + .../Common/Interfaces/IMailService.cs | 2 +- .../{HTMLMailData.cs => HtmlMailData.cs} | 6 +- src/Application/Users/Commands/AddUser.cs | 2 +- .../EventHandlers/UserCreatedEventHandler.cs | 24 +- src/Domain/Entities/ResetPasswordToken.cs | 12 + src/Domain/Events/UserCreatedEvent.cs | 6 +- src/Infrastructure/ConfigureServices.cs | 3 +- .../Identity/IdentityService.cs | 74 ++- .../Persistence/ApplicationDbContext.cs | 9 +- .../ResetPasswordTokenConfiguration.cs | 24 + ...07093007_AddResetPasswordToken.Designer.cs | 520 ++++++++++++++++++ .../20230607093007_AddResetPasswordToken.cs | 48 ++ .../ApplicationDbContextModelSnapshot.cs | 34 ++ src/Infrastructure/Services/MailService.cs | 8 +- .../CustomMailService.cs | 2 +- 19 files changed, 773 insertions(+), 39 deletions(-) create mode 100644 src/Api/Controllers/Payload/Requests/Auth/ResetPasswordRequest.cs create mode 100644 src/Application/Common/Interfaces/IAuthDbContext.cs rename src/Application/Common/Models/{HTMLMailData.cs => HtmlMailData.cs} (86%) create mode 100644 src/Domain/Entities/ResetPasswordToken.cs create mode 100644 src/Infrastructure/Persistence/Configurations/ResetPasswordTokenConfiguration.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230607093007_AddResetPasswordToken.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230607093007_AddResetPasswordToken.cs diff --git a/src/Api/Controllers/AuthController.cs b/src/Api/Controllers/AuthController.cs index a770a036..0190f016 100644 --- a/src/Api/Controllers/AuthController.cs +++ b/src/Api/Controllers/AuthController.cs @@ -116,6 +116,23 @@ public async Task Logout() return Ok(); } + [HttpPost("reset-password")] + public async Task ResetPassword([FromBody] ResetPasswordRequest request) + { + if (string.IsNullOrEmpty(request.NewPassword)) + { + return BadRequest("Password cannot be empty."); + } + + if (!request.NewPassword.Equals(request.ConfirmPassword)) + { + return BadRequest("Confirm password must match with new password."); + } + + await _identityService.ResetPassword(request.Token, request.NewPassword); + return Ok(); + } + private void SetJweToken(SecurityToken jweToken, RefreshTokenDto newRefreshToken) { var cookieOptions = new CookieOptions diff --git a/src/Api/Controllers/Payload/Requests/Auth/ResetPasswordRequest.cs b/src/Api/Controllers/Payload/Requests/Auth/ResetPasswordRequest.cs new file mode 100644 index 00000000..2e18993f --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Auth/ResetPasswordRequest.cs @@ -0,0 +1,8 @@ +namespace Api.Controllers.Payload.Requests.Auth; + +public class ResetPasswordRequest +{ + public string Token { get; set; } + public string NewPassword { get; set; } + public string ConfirmPassword { get; set; } +} \ No newline at end of file diff --git a/src/Application/Common/Interfaces/IAuthDbContext.cs b/src/Application/Common/Interfaces/IAuthDbContext.cs new file mode 100644 index 00000000..49801213 --- /dev/null +++ b/src/Application/Common/Interfaces/IAuthDbContext.cs @@ -0,0 +1,12 @@ +using Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Application.Common.Interfaces; + +public interface IAuthDbContext +{ + public DbSet RefreshTokens { get; } + public DbSet ResetPasswordTokens { get; } + + Task SaveChangesAsync(CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/Application/Common/Interfaces/IIdentityService.cs b/src/Application/Common/Interfaces/IIdentityService.cs index 048300af..c4d91102 100644 --- a/src/Application/Common/Interfaces/IIdentityService.cs +++ b/src/Application/Common/Interfaces/IIdentityService.cs @@ -9,4 +9,5 @@ public interface IIdentityService Task RefreshTokenAsync(string token, string refreshToken); Task<(AuthenticationResult AuthResult, UserDto UserCredentials)> LoginAsync(string email, string password); Task LogoutAsync(string token, string refreshToken); + Task ResetPassword(string token, string newPassword); } \ No newline at end of file diff --git a/src/Application/Common/Interfaces/IMailService.cs b/src/Application/Common/Interfaces/IMailService.cs index e7fe9d67..0dbc58c7 100644 --- a/src/Application/Common/Interfaces/IMailService.cs +++ b/src/Application/Common/Interfaces/IMailService.cs @@ -4,5 +4,5 @@ namespace Application.Common.Interfaces; public interface IMailService { - bool SendResetPasswordHtmlMail(string userEmail, string password); + bool SendResetPasswordHtmlMail(string userEmail, string temporaryPassword, string resetPasswordTokenHash); } \ No newline at end of file diff --git a/src/Application/Common/Models/HTMLMailData.cs b/src/Application/Common/Models/HtmlMailData.cs similarity index 86% rename from src/Application/Common/Models/HTMLMailData.cs rename to src/Application/Common/Models/HtmlMailData.cs index edc8dae0..9fa0959b 100644 --- a/src/Application/Common/Models/HTMLMailData.cs +++ b/src/Application/Common/Models/HtmlMailData.cs @@ -2,7 +2,7 @@ namespace Application.Common.Models; -public class HTMLMailData +public class HtmlMailData { [JsonPropertyName("from")] public From From { get; set; } @@ -32,8 +32,8 @@ public class TemplateVariables { [JsonPropertyName("user_email")] public string UserEmail { get; set; } - [JsonPropertyName("pass_reset_link")] - public string PassResetLink { get; set; } + [JsonPropertyName("reset_password_token_hash")] + public string ResetPasswordTokenHash { get; set; } [JsonPropertyName("user_password")] public string UserPassword { get; set; } } \ No newline at end of file diff --git a/src/Application/Users/Commands/AddUser.cs b/src/Application/Users/Commands/AddUser.cs index 52ca1f80..c077511c 100644 --- a/src/Application/Users/Commands/AddUser.cs +++ b/src/Application/Users/Commands/AddUser.cs @@ -106,7 +106,7 @@ public async Task Handle(Command request, CancellationToken cancellatio IsActivated = false, Created = LocalDateTime.FromDateTime(DateTime.UtcNow) }; - entity.AddDomainEvent(new UserCreatedEvent(entity.Email, password)); + entity.AddDomainEvent(new UserCreatedEvent(entity, password)); var result = await _context.Users.AddAsync(entity, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); diff --git a/src/Application/Users/EventHandlers/UserCreatedEventHandler.cs b/src/Application/Users/EventHandlers/UserCreatedEventHandler.cs index 7c2efe9b..41b48e34 100644 --- a/src/Application/Users/EventHandlers/UserCreatedEventHandler.cs +++ b/src/Application/Users/EventHandlers/UserCreatedEventHandler.cs @@ -1,20 +1,40 @@ using Application.Common.Interfaces; +using Application.Helpers; +using Domain.Entities; using Domain.Events; using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Users.EventHandlers; public class UserCreatedEventHandler : INotificationHandler { private readonly IMailService _mailService; + private readonly IAuthDbContext _authDbContext; - public UserCreatedEventHandler(IMailService mailService) + public UserCreatedEventHandler(IMailService mailService, IAuthDbContext authDbContext) { _mailService = mailService; + _authDbContext = authDbContext; } public async Task Handle(UserCreatedEvent notification, CancellationToken cancellationToken) { - _mailService.SendResetPasswordHtmlMail(notification.Email, notification.Password); + var expirationDate = LocalDateTime.FromDateTime(DateTime.Now.AddDays(1)); + + var token = Guid.NewGuid().ToString(); + var resetPasswordToken = new ResetPasswordToken() + { + User = notification.User, + TokenHash = SecurityUtil.Hash(token), + ExpirationDate = expirationDate, + IsInvalidated = false, + }; + + await _authDbContext.ResetPasswordTokens.AddAsync(resetPasswordToken, cancellationToken); + await _authDbContext.SaveChangesAsync(cancellationToken); + + _mailService.SendResetPasswordHtmlMail(notification.User.Email, notification.Password, token); } } \ No newline at end of file diff --git a/src/Domain/Entities/ResetPasswordToken.cs b/src/Domain/Entities/ResetPasswordToken.cs new file mode 100644 index 00000000..0ae14f1a --- /dev/null +++ b/src/Domain/Entities/ResetPasswordToken.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; +using NodaTime; + +namespace Domain.Entities; + +public class ResetPasswordToken +{ + [Key] public string TokenHash { get; set; } = null!; + public User User { get; set; } = null!; + public LocalDateTime ExpirationDate { get; set; } + public bool IsInvalidated { get; set; } +} \ No newline at end of file diff --git a/src/Domain/Events/UserCreatedEvent.cs b/src/Domain/Events/UserCreatedEvent.cs index ee1e507e..eb43de5a 100644 --- a/src/Domain/Events/UserCreatedEvent.cs +++ b/src/Domain/Events/UserCreatedEvent.cs @@ -5,12 +5,12 @@ namespace Domain.Events; public class UserCreatedEvent : BaseEvent { - public UserCreatedEvent(string email, string password) + public UserCreatedEvent(User user, string password) { - Email = email; + User = user; Password = password; } - public string Email { get; } + public User User { get; } public string Password { get; } } \ No newline at end of file diff --git a/src/Infrastructure/ConfigureServices.cs b/src/Infrastructure/ConfigureServices.cs index d3820329..3c6c8af2 100644 --- a/src/Infrastructure/ConfigureServices.cs +++ b/src/Infrastructure/ConfigureServices.cs @@ -17,7 +17,8 @@ public static class ConfigureServices public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration) { services.AddApplicationDbContext(configuration); - services.AddScoped(); + services.AddScoped(sp => sp.GetService()!); + services.AddScoped(sp => sp.GetService()!); services.AddScoped(); services.AddMailService(configuration); diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index 1947979e..8e913ca5 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -1,10 +1,9 @@ -using System.Data; using System.Globalization; using System.IdentityModel.Tokens.Jwt; -using System.Reflection.Metadata.Ecma335; using System.Security.Authentication; using System.Security.Claims; using System.Security.Cryptography; +using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Models; using Application.Common.Models.Dtos; @@ -12,7 +11,6 @@ using Application.Users.Queries; using AutoMapper; using Domain.Entities; -using Infrastructure.Persistence; using Infrastructure.Shared; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; @@ -26,16 +24,25 @@ public class IdentityService : IIdentityService { private readonly TokenValidationParameters _tokenValidationParameters; private readonly JweSettings _jweSettings; - private readonly ApplicationDbContext _context; + private readonly IApplicationDbContext _applicationDbContext; + private readonly IAuthDbContext _authDbContext; private readonly RSA _encryptionKey; private readonly ECDsa _signingKey; private readonly IMapper _mapper; - public IdentityService(TokenValidationParameters tokenValidationParameters, IOptions jweSettingsOptions, ApplicationDbContext context, RSA encryptionKey, ECDsa signingKey, IMapper mapper) + public IdentityService( + TokenValidationParameters tokenValidationParameters, + IOptions jweSettingsOptions, + IApplicationDbContext applicationDbContext, + IAuthDbContext authDbContext, + RSA encryptionKey, + ECDsa signingKey, + IMapper mapper) { _tokenValidationParameters = tokenValidationParameters; _jweSettings = jweSettingsOptions.Value; - _context = context; + _applicationDbContext = applicationDbContext; + _authDbContext = authDbContext; _encryptionKey = encryptionKey; _signingKey = signingKey; _mapper = mapper; @@ -54,7 +61,7 @@ public async Task Validate(string token, string refreshToken) var email = validatedToken.Claims.Single(y => y.Type.Equals(emailClaim)).Value; - var user = await _context.Users.FirstOrDefaultAsync(x => + var user = await _applicationDbContext.Users.FirstOrDefaultAsync(x => x.Username.Equals(email) || x.Email!.Equals(email)); @@ -75,7 +82,7 @@ public async Task Validate(string token, string refreshToken) } var jti = validatedToken.Claims.Single(x => x.Type.Equals(JwtRegisteredClaimNames.Jti)).Value; - var storedRefreshToken = await _context.RefreshTokens.SingleOrDefaultAsync(x => x.Token.Equals(Guid.Parse(refreshToken))); + var storedRefreshToken = await _authDbContext.RefreshTokens.SingleOrDefaultAsync(x => x.Token.Equals(Guid.Parse(refreshToken))); if (storedRefreshToken is null) { @@ -113,7 +120,7 @@ public async Task RefreshTokenAsync(string token, string r var email = validatedToken.Claims.Single(y => y.Type.Equals(emailClaim)).Value; - var user = await _context.Users.FirstOrDefaultAsync(x => + var user = await _applicationDbContext.Users.FirstOrDefaultAsync(x => x.Username.Equals(email) || x.Email!.Equals(email)); @@ -123,7 +130,7 @@ public async Task RefreshTokenAsync(string token, string r } var jti = validatedToken.Claims.Single(x => x.Type.Equals(JwtRegisteredClaimNames.Jti)).Value; - var storedRefreshToken = await _context.RefreshTokens.SingleOrDefaultAsync(x => x.Token.Equals(Guid.Parse(refreshToken))); + var storedRefreshToken = await _authDbContext.RefreshTokens.SingleOrDefaultAsync(x => x.Token.Equals(Guid.Parse(refreshToken))); if (storedRefreshToken is null) { @@ -185,7 +192,7 @@ public async Task RefreshTokenAsync(string token, string r public async Task<(AuthenticationResult, UserDto)> LoginAsync(string email, string password) { - var user = _context.Users + var user = _applicationDbContext.Users .Include(x => x.Department) .FirstOrDefault(x => x.Email!.Equals(email)); @@ -194,10 +201,10 @@ public async Task RefreshTokenAsync(string token, string r throw new AuthenticationException("Username or password is invalid."); } - var existedRefreshTokens = _context.RefreshTokens.Where(x => x.User.Email!.Equals(user.Email)); + var existedRefreshTokens = _authDbContext.RefreshTokens.Where(x => x.User.Email!.Equals(user.Email)); - _context.RemoveRange(existedRefreshTokens); - await _context.SaveChangesAsync(); + _authDbContext.RefreshTokens.RemoveRange(existedRefreshTokens); + await _authDbContext.SaveChangesAsync(CancellationToken.None); return (await GenerateAuthenticationResultForUserAsync(user), _mapper.Map(user)); } @@ -213,7 +220,7 @@ public async Task LogoutAsync(string token, string refreshToken) var jti = validatedToken.Claims.Single(x => x.Type.Equals(JwtRegisteredClaimNames.Jti)).Value; var storedRefreshToken = - await _context.RefreshTokens.SingleOrDefaultAsync(x => x.Token.Equals(Guid.Parse(refreshToken))); + await _authDbContext.RefreshTokens.SingleOrDefaultAsync(x => x.Token.Equals(Guid.Parse(refreshToken))); if (storedRefreshToken is null) return; @@ -222,8 +229,37 @@ public async Task LogoutAsync(string token, string refreshToken) throw new AuthenticationException("This refresh token does not match this Jwt."); } - _context.RefreshTokens.Remove(storedRefreshToken); - await _context.SaveChangesAsync(); + _authDbContext.RefreshTokens.Remove(storedRefreshToken); + await _authDbContext.SaveChangesAsync(CancellationToken.None); + } + + public async Task ResetPassword(string token, string newPassword) + { + var tokenHash = SecurityUtil.Hash(token); + var resetPasswordToken = await _authDbContext.ResetPasswordTokens + .Include(x => x.User) + .FirstOrDefaultAsync(x => x.TokenHash.Equals(tokenHash)); + if (resetPasswordToken is null) + { + throw new KeyNotFoundException("Token is invalid."); + } + + if (resetPasswordToken.IsInvalidated) + { + throw new ConflictException("Token is invalid."); + } + + var user = resetPasswordToken.User; + + if (user.IsActivated is false) + { + user.IsActivated = true; + } + + user.PasswordHash = SecurityUtil.Hash(newPassword); + resetPasswordToken.IsInvalidated = true; + await _applicationDbContext.SaveChangesAsync(CancellationToken.None); + await _authDbContext.SaveChangesAsync(CancellationToken.None); } private async Task GenerateAuthenticationResultForUserAsync(User user) @@ -239,8 +275,8 @@ private async Task GenerateAuthenticationResultForUserAsyn ExpiryDateTime = LocalDateTime.FromDateTime(utcNow.AddDays(_jweSettings.RefreshTokenLifetimeInDays)) }; - await _context.RefreshTokens.AddAsync(refreshToken); - await _context.SaveChangesAsync(); + await _authDbContext.RefreshTokens.AddAsync(refreshToken); + await _authDbContext.SaveChangesAsync(CancellationToken.None); return new() { Token = token, diff --git a/src/Infrastructure/Persistence/ApplicationDbContext.cs b/src/Infrastructure/Persistence/ApplicationDbContext.cs index 0b8da3db..91c14162 100644 --- a/src/Infrastructure/Persistence/ApplicationDbContext.cs +++ b/src/Infrastructure/Persistence/ApplicationDbContext.cs @@ -9,7 +9,7 @@ namespace Infrastructure.Persistence; -public class ApplicationDbContext : DbContext, IApplicationDbContext +public class ApplicationDbContext : DbContext, IApplicationDbContext, IAuthDbContext { private readonly IMediator _mediator; public ApplicationDbContext( @@ -33,13 +33,14 @@ public ApplicationDbContext( public DbSet Entries => Set(); public DbSet RefreshTokens => Set(); + public DbSet ResetPasswordTokens => Set(); - protected override void OnModelCreating(ModelBuilder builder) + protected override void OnModelCreating(ModelBuilder modelBuilder) { // Scan for entity configurations using FluentAPI - builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); + modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); - base.OnModelCreating(builder); + base.OnModelCreating(modelBuilder); } public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) diff --git a/src/Infrastructure/Persistence/Configurations/ResetPasswordTokenConfiguration.cs b/src/Infrastructure/Persistence/Configurations/ResetPasswordTokenConfiguration.cs new file mode 100644 index 00000000..36b168e6 --- /dev/null +++ b/src/Infrastructure/Persistence/Configurations/ResetPasswordTokenConfiguration.cs @@ -0,0 +1,24 @@ +using Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Infrastructure.Persistence.Configurations; + +public class ResetPasswordTokenConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(x => x.TokenHash); + + builder.Property(x => x.ExpirationDate) + .IsRequired(); + + builder.Property(x => x.IsInvalidated) + .HasDefaultValue(false); + + builder.HasOne(x => x.User) + .WithMany() + .HasForeignKey("UserId") + .IsRequired(); + } +} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Migrations/20230607093007_AddResetPasswordToken.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230607093007_AddResetPasswordToken.Designer.cs new file mode 100644 index 00000000..22ae64be --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230607093007_AddResetPasswordToken.Designer.cs @@ -0,0 +1,520 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230607093007_AddResetPasswordToken")] + partial class AddResetPasswordToken + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId") + .IsUnique(); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithOne("Room") + .HasForeignKey("Domain.Entities.Physical.Room", "DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230607093007_AddResetPasswordToken.cs b/src/Infrastructure/Persistence/Migrations/20230607093007_AddResetPasswordToken.cs new file mode 100644 index 00000000..3a34e1da --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230607093007_AddResetPasswordToken.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class AddResetPasswordToken : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ResetPasswordTokens", + columns: table => new + { + TokenHash = table.Column(type: "text", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + ExpirationDate = table.Column(type: "timestamp without time zone", nullable: false), + IsInvalidated = table.Column(type: "boolean", nullable: false, defaultValue: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ResetPasswordTokens", x => x.TokenHash); + table.ForeignKey( + name: "FK_ResetPasswordTokens_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ResetPasswordTokens_UserId", + table: "ResetPasswordTokens", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ResetPasswordTokens"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index deeca7c5..d7177ccb 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -349,6 +349,29 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("RefreshTokens"); }); + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + modelBuilder.Entity("Domain.Entities.User", b => { b.Property("Id") @@ -546,6 +569,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + modelBuilder.Entity("Domain.Entities.User", b => { b.HasOne("Domain.Entities.Department", "Department") diff --git a/src/Infrastructure/Services/MailService.cs b/src/Infrastructure/Services/MailService.cs index 32feba59..8515cad5 100644 --- a/src/Infrastructure/Services/MailService.cs +++ b/src/Infrastructure/Services/MailService.cs @@ -17,9 +17,9 @@ public MailService(IOptions mailSettingsOptions) _mailSettings = mailSettingsOptions.Value; } - public bool SendResetPasswordHtmlMail(string userEmail, string password) + public bool SendResetPasswordHtmlMail(string userEmail, string temporaryPassword, string resetPasswordTokenHash) { - var data = new HTMLMailData() + var data = new HtmlMailData() { From = new From() { @@ -34,8 +34,8 @@ public bool SendResetPasswordHtmlMail(string userEmail, string password) TemplateVariables = new TemplateVariables() { UserEmail = userEmail, - PassResetLink = "random_shit", - UserPassword = password, + ResetPasswordTokenHash = resetPasswordTokenHash, + UserPassword = temporaryPassword, }, }; diff --git a/tests/Application.Tests.Integration/CustomMailService.cs b/tests/Application.Tests.Integration/CustomMailService.cs index e439bf07..32c8d360 100644 --- a/tests/Application.Tests.Integration/CustomMailService.cs +++ b/tests/Application.Tests.Integration/CustomMailService.cs @@ -4,7 +4,7 @@ namespace Application.Tests.Integration; public class CustomMailService : IMailService { - public bool SendResetPasswordHtmlMail(string userEmail, string password) + public bool SendResetPasswordHtmlMail(string userEmail, string temporaryPassword, string resetPasswordTokenHash) { return true; } From 77075f29c634a6f76261f41c6545caf82232d8ca Mon Sep 17 00:00:00 2001 From: kaitozu <43519768+kaitoz11@users.noreply.github.com> Date: Sat, 10 Jun 2023 20:34:49 +0700 Subject: [PATCH 11/56] refactor: add salt (#224) * test: add unit test for password hashing * feat(SecurityUtil.cs): add password hashing implementation * refactor: add salt and pepper for password hashing * fix(IdentityService.cs): remove unused dependencies --- src/Api/Controllers/AuthController.cs | 3 +- src/Api/appsettings.Development.json | 4 + src/Api/appsettings.Testing.json | 3 + .../Common/Interfaces/ISecurityService.cs | 6 + src/Application/Helpers/SecurityUtil.cs | 7 + src/Application/Helpers/StringUtil.cs | 4 + src/Application/Users/Commands/AddUser.cs | 11 +- src/Domain/Entities/User.cs | 1 + src/Infrastructure/ConfigureServices.cs | 15 + .../Identity/IdentityService.cs | 12 +- .../Persistence/ApplicationDbContextSeed.cs | 12 +- .../Configurations/UserConfiguration.cs | 4 + ...13_AddPasswordSaltFieldToUsers.Designer.cs | 491 ++++++++++++++++++ ...00000000013_AddPasswordSaltFieldToUsers.cs | 30 ++ .../ApplicationDbContextModelSnapshot.cs | 5 + .../Services/SecurityService.cs | 21 + src/Infrastructure/Shared/SecuritySettings.cs | 6 + .../BaseClassFixture.cs | 5 +- .../Helpers/SecurityUtilTests.cs | 27 + 19 files changed, 653 insertions(+), 14 deletions(-) create mode 100644 src/Application/Common/Interfaces/ISecurityService.cs create mode 100644 src/Infrastructure/Persistence/Migrations/00000000000013_AddPasswordSaltFieldToUsers.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/00000000000013_AddPasswordSaltFieldToUsers.cs create mode 100644 src/Infrastructure/Services/SecurityService.cs create mode 100644 src/Infrastructure/Shared/SecuritySettings.cs create mode 100644 tests/Application.Tests.Unit/Helpers/SecurityUtilTests.cs diff --git a/src/Api/Controllers/AuthController.cs b/src/Api/Controllers/AuthController.cs index 0190f016..2f8f4e4f 100644 --- a/src/Api/Controllers/AuthController.cs +++ b/src/Api/Controllers/AuthController.cs @@ -115,8 +115,7 @@ public async Task Logout() return Ok(); } - - [HttpPost("reset-password")] + public async Task ResetPassword([FromBody] ResetPasswordRequest request) { if (string.IsNullOrEmpty(request.NewPassword)) diff --git a/src/Api/appsettings.Development.json b/src/Api/appsettings.Development.json index ef1296b0..c0380e8b 100644 --- a/src/Api/appsettings.Development.json +++ b/src/Api/appsettings.Development.json @@ -9,6 +9,10 @@ "TokenLifetime": "00:20:00", "RefreshTokenLifetimeInDays": 3 }, + "SecuritySettings": { + "Pepper": "1f952d7238f35083abc3d6bf28410702c65f54afc0be29af7f1c89f5859d1d53" + }, + "MailSettings": { "ClientUrl": "https://send.api.mailtrap.io/api/send", "Token": "745f040659edff0ce87b545567da72d2", diff --git a/src/Api/appsettings.Testing.json b/src/Api/appsettings.Testing.json index 5dbdc362..aafebb14 100644 --- a/src/Api/appsettings.Testing.json +++ b/src/Api/appsettings.Testing.json @@ -5,6 +5,9 @@ "TokenLifetime": "00:20:00", "RefreshTokenLifetimeInDays": 3 }, + "SecuritySettings": { + "Pepper": "1f952d7238f35083abc3d6bf28410702c65f54afc0be29af7f1c89f5859d1d53" + }, "Seed": true, "Serilog" : { "MinimumLevel" : { diff --git a/src/Application/Common/Interfaces/ISecurityService.cs b/src/Application/Common/Interfaces/ISecurityService.cs new file mode 100644 index 00000000..5cbd6f96 --- /dev/null +++ b/src/Application/Common/Interfaces/ISecurityService.cs @@ -0,0 +1,6 @@ +namespace Application.Common.Interfaces; + +public interface ISecurityService +{ + string Hash(string input, string salt); +} \ No newline at end of file diff --git a/src/Application/Helpers/SecurityUtil.cs b/src/Application/Helpers/SecurityUtil.cs index 1ea689af..9936b212 100644 --- a/src/Application/Helpers/SecurityUtil.cs +++ b/src/Application/Helpers/SecurityUtil.cs @@ -19,4 +19,11 @@ public static string Hash(string input) return stringBuilder.ToString(); } + + public static string HashPasswordWith(this string input, string salt, string pepper) + { + pepper = Convert.ToBase64String(Encoding.UTF8.GetBytes(pepper)); + salt = Convert.ToBase64String(Encoding.UTF8.GetBytes(salt)); + return Hash(salt + input + pepper); + } } \ No newline at end of file diff --git a/src/Application/Helpers/StringUtil.cs b/src/Application/Helpers/StringUtil.cs index 09f68c24..ec3c3b74 100644 --- a/src/Application/Helpers/StringUtil.cs +++ b/src/Application/Helpers/StringUtil.cs @@ -16,4 +16,8 @@ public static string RandomString(int n) return stringBuilder.ToString(); } + + public static string RandomPassword() => RandomString(8); + + public static string RandomSalt() => RandomString(24); } \ No newline at end of file diff --git a/src/Application/Users/Commands/AddUser.cs b/src/Application/Users/Commands/AddUser.cs index c077511c..2ccef831 100644 --- a/src/Application/Users/Commands/AddUser.cs +++ b/src/Application/Users/Commands/AddUser.cs @@ -66,11 +66,13 @@ public class AddUserCommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly ISecurityService _securityService; - public AddUserCommandHandler(IApplicationDbContext context, IMapper mapper) + public AddUserCommandHandler(IApplicationDbContext context, IMapper mapper, ISecurityService securityService) { _context = context; _mapper = mapper; + _securityService = securityService; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -91,11 +93,14 @@ public async Task Handle(Command request, CancellationToken cancellatio throw new KeyNotFoundException("Department does not exist."); } - var password = StringUtil.RandomString(8); + var password = StringUtil.RandomPassword(); + var salt = StringUtil.RandomSalt(); + var entity = new User { Username = request.Username, - PasswordHash = SecurityUtil.Hash(password), + PasswordHash = _securityService.Hash(password, salt), + PasswordSalt = salt, Email = request.Email, FirstName = request.FirstName?.Trim(), LastName = request.LastName?.Trim(), diff --git a/src/Domain/Entities/User.cs b/src/Domain/Entities/User.cs index 527300bc..014d974e 100644 --- a/src/Domain/Entities/User.cs +++ b/src/Domain/Entities/User.cs @@ -9,6 +9,7 @@ public class User : BaseAuditableEntity public string Username { get; set; } = null!; public string Email { get; set; } = null!; public string PasswordHash { get; set; } = null!; + public string PasswordSalt { get; set; } = null!; public string? FirstName { get; set; } public string? LastName { get; set; } public Department? Department { get; set; } diff --git a/src/Infrastructure/ConfigureServices.cs b/src/Infrastructure/ConfigureServices.cs index 3c6c8af2..3d4ef3bb 100644 --- a/src/Infrastructure/ConfigureServices.cs +++ b/src/Infrastructure/ConfigureServices.cs @@ -25,6 +25,7 @@ public static IServiceCollection AddInfrastructureServices(this IServiceCollecti services.AddJweAuthentication(configuration); services.AddAuthorization(); + services.AddSecurityService(configuration); return services; } @@ -109,4 +110,18 @@ private static IServiceCollection AddMailService(this IServiceCollection service return services; } + + private static IServiceCollection AddSecurityService(this IServiceCollection services, IConfiguration configuration) + { + var securitySettings = configuration.GetSection(nameof(SecuritySettings)).Get(); + + services.Configure(option => + { + option.Pepper = securitySettings!.Pepper; + }); + + services.AddTransient(); + + return services; + } } \ No newline at end of file diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index 8e913ca5..ef335a22 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -29,6 +29,7 @@ public class IdentityService : IIdentityService private readonly RSA _encryptionKey; private readonly ECDsa _signingKey; private readonly IMapper _mapper; + private readonly SecuritySettings _securitySettings; public IdentityService( TokenValidationParameters tokenValidationParameters, @@ -37,7 +38,8 @@ public IdentityService( IAuthDbContext authDbContext, RSA encryptionKey, ECDsa signingKey, - IMapper mapper) + IMapper mapper, + IOptions securitySettingsOptions) { _tokenValidationParameters = tokenValidationParameters; _jweSettings = jweSettingsOptions.Value; @@ -46,6 +48,7 @@ public IdentityService( _encryptionKey = encryptionKey; _signingKey = signingKey; _mapper = mapper; + _securitySettings = securitySettingsOptions.Value; } public async Task Validate(string token, string refreshToken) @@ -196,7 +199,7 @@ public async Task RefreshTokenAsync(string token, string r .Include(x => x.Department) .FirstOrDefault(x => x.Email!.Equals(email)); - if (user is null || !user.PasswordHash.Equals(SecurityUtil.Hash(password))) + if (user is null || !user.PasswordHash.Equals(password.HashPasswordWith(user.PasswordSalt, _securitySettings.Pepper))) { throw new AuthenticationException("Username or password is invalid."); } @@ -255,8 +258,9 @@ public async Task ResetPassword(string token, string newPassword) { user.IsActivated = true; } - - user.PasswordHash = SecurityUtil.Hash(newPassword); + var salt = StringUtil.RandomSalt(); + user.PasswordSalt = salt; + user.PasswordHash = newPassword.HashPasswordWith(salt, newPassword); resetPasswordToken.IsInvalidated = true; await _applicationDbContext.SaveChangesAsync(CancellationToken.None); await _authDbContext.SaveChangesAsync(CancellationToken.None); diff --git a/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs b/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs index a6593d43..37a37324 100644 --- a/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs +++ b/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs @@ -14,10 +14,11 @@ public class ApplicationDbContextSeed public static async Task Seed(ApplicationDbContext context, IConfiguration configuration, ILogger logger) { if (!configuration.GetValue("Seed")) return; - + + var securitySettings = configuration.GetSection(nameof(SecuritySettings)).Get(); try { - await TrySeedAsync(context); + await TrySeedAsync(context, securitySettings!.Pepper); } catch (Exception ex) { @@ -26,19 +27,22 @@ public static async Task Seed(ApplicationDbContext context, IConfiguration confi } } - private static async Task TrySeedAsync(ApplicationDbContext context) + private static async Task TrySeedAsync(ApplicationDbContext context, string pepper) { var department = new Department() { Name = "Admin" }; + + var salt = StringUtil.RandomSalt(); // Default users var admin = new User { Username = "admin", Email = "admin@profile.dev", - PasswordHash = SecurityUtil.Hash("admin"), + PasswordHash = "admin".HashPasswordWith(salt, pepper), + PasswordSalt = salt, IsActive = true, IsActivated = true, Created = LocalDateTime.FromDateTime(DateTime.UtcNow), diff --git a/src/Infrastructure/Persistence/Configurations/UserConfiguration.cs b/src/Infrastructure/Persistence/Configurations/UserConfiguration.cs index b3e55685..3240464d 100644 --- a/src/Infrastructure/Persistence/Configurations/UserConfiguration.cs +++ b/src/Infrastructure/Persistence/Configurations/UserConfiguration.cs @@ -24,6 +24,10 @@ public void Configure(EntityTypeBuilder builder) builder.Property(x => x.PasswordHash) .HasMaxLength(64) .IsRequired(); + + builder.Property(x => x.PasswordSalt) + .HasMaxLength(32) + .IsRequired(); builder.Property(x => x.FirstName) .HasMaxLength(50) diff --git a/src/Infrastructure/Persistence/Migrations/00000000000013_AddPasswordSaltFieldToUsers.Designer.cs b/src/Infrastructure/Persistence/Migrations/00000000000013_AddPasswordSaltFieldToUsers.Designer.cs new file mode 100644 index 00000000..d702cff5 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/00000000000013_AddPasswordSaltFieldToUsers.Designer.cs @@ -0,0 +1,491 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230608044119_AddPasswordSaltFieldToUsers")] + partial class AddPasswordSaltFieldToUsers + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId") + .IsUnique(); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithOne("Room") + .HasForeignKey("Domain.Entities.Physical.Room", "DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/00000000000013_AddPasswordSaltFieldToUsers.cs b/src/Infrastructure/Persistence/Migrations/00000000000013_AddPasswordSaltFieldToUsers.cs new file mode 100644 index 00000000..6b4e0f69 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/00000000000013_AddPasswordSaltFieldToUsers.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class AddPasswordSaltFieldToUsers : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PasswordSalt", + table: "Users", + type: "character varying(32)", + maxLength: 32, + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "PasswordSalt", + table: "Users"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index d7177ccb..9fab7c8c 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -417,6 +417,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(64) .HasColumnType("character varying(64)"); + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + b.Property("Position") .HasMaxLength(64) .HasColumnType("character varying(64)"); diff --git a/src/Infrastructure/Services/SecurityService.cs b/src/Infrastructure/Services/SecurityService.cs new file mode 100644 index 00000000..9e38f89e --- /dev/null +++ b/src/Infrastructure/Services/SecurityService.cs @@ -0,0 +1,21 @@ +using Application.Common.Interfaces; +using Application.Helpers; +using Infrastructure.Shared; +using Microsoft.Extensions.Options; + +namespace Infrastructure.Services; + +public class SecurityService : ISecurityService +{ + private readonly SecuritySettings _securitySettings; + + public SecurityService(IOptions securitySettingsOptions) + { + _securitySettings = securitySettingsOptions.Value; + } + + public string Hash(string input, string salt) + { + return input.HashPasswordWith(salt, _securitySettings.Pepper); + } +} \ No newline at end of file diff --git a/src/Infrastructure/Shared/SecuritySettings.cs b/src/Infrastructure/Shared/SecuritySettings.cs new file mode 100644 index 00000000..beba3ec4 --- /dev/null +++ b/src/Infrastructure/Shared/SecuritySettings.cs @@ -0,0 +1,6 @@ +namespace Infrastructure.Shared; + +public class SecuritySettings +{ + public string Pepper { get; set; } +} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/BaseClassFixture.cs b/tests/Application.Tests.Integration/BaseClassFixture.cs index 0339e595..eaa0afa5 100644 --- a/tests/Application.Tests.Integration/BaseClassFixture.cs +++ b/tests/Application.Tests.Integration/BaseClassFixture.cs @@ -174,6 +174,8 @@ protected static Department CreateDepartment() protected static User CreateUser(string role, string password) { + var salt = StringUtil.RandomSalt(); + return new User() { Id = Guid.NewGuid(), @@ -186,7 +188,8 @@ protected static User CreateUser(string role, string password) IsActivated = true, IsActive = true, Created = LocalDateTime.FromDateTime(DateTime.Now), - PasswordHash = SecurityUtil.Hash(password) + PasswordHash = password.HashPasswordWith(salt, "random pepper"), + PasswordSalt = salt }; } diff --git a/tests/Application.Tests.Unit/Helpers/SecurityUtilTests.cs b/tests/Application.Tests.Unit/Helpers/SecurityUtilTests.cs new file mode 100644 index 00000000..259638f7 --- /dev/null +++ b/tests/Application.Tests.Unit/Helpers/SecurityUtilTests.cs @@ -0,0 +1,27 @@ +using Application.Helpers; +using FluentAssertions; +using Xunit; + +namespace Application.Tests.Unit.Helpers; + +public class SecurityUtilTests +{ + public SecurityUtilTests() + { + } + + [Fact] + public void ShouldReturnHash_WhenHashPasswordWithSaltAndPepper() + { + // Arrange + string salt = "dwnqjkdqwW4q"; + string input = "ThizIsAveRyl0000GandS3cur4dP@ssWord"; + string pepper = "Some secret here"; + string expectedHash = "27745bdd5e09aae12213a1ea7a3f9056b7de257050370d6cfa1c2d1b954de335"; + // Act + string password = input.HashPasswordWith(salt, pepper); + + // Assert + password.Should().Be(expectedHash); + } +} \ No newline at end of file From 88c70072e8cc9403a98a5dfd6443549013cc26bd Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Mon, 12 Jun 2023 18:05:27 +0700 Subject: [PATCH 12/56] fix: seed some more data and fix swagger not loading --- src/Api/Controllers/AuthController.cs | 1 + .../Persistence/ApplicationDbContextSeed.cs | 60 ++++++++++++++++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/Api/Controllers/AuthController.cs b/src/Api/Controllers/AuthController.cs index 2f8f4e4f..7d11f4f2 100644 --- a/src/Api/Controllers/AuthController.cs +++ b/src/Api/Controllers/AuthController.cs @@ -116,6 +116,7 @@ public async Task Logout() return Ok(); } + [HttpPost] public async Task ResetPassword([FromBody] ResetPasswordRequest request) { if (string.IsNullOrEmpty(request.NewPassword)) diff --git a/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs b/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs index 37a37324..f788d804 100644 --- a/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs +++ b/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs @@ -3,7 +3,6 @@ using Domain.Entities; using Infrastructure.Shared; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Options; using NodaTime; using Serilog; @@ -49,6 +48,36 @@ private static async Task TrySeedAsync(ApplicationDbContext context, string pepp Role = IdentityData.Roles.Admin, }; + salt = StringUtil.RandomSalt(); + var staff = new User() + { + Username = "staff", + Email = "staff@profile.dev", + PasswordHash = "staff".HashPasswordWith(salt, pepper), + PasswordSalt = salt, + IsActive = true, + IsActivated = true, + Created = LocalDateTime.FromDateTime(DateTime.UtcNow), + Role = IdentityData.Roles.Staff, + }; + + var itDepartment = new Department() + { + Name = "IT" + }; + salt = StringUtil.RandomSalt(); + var employee = new User() + { + Username = "employee", + Email = "employee@profile.dev", + PasswordHash = "employee".HashPasswordWith(salt, pepper), + PasswordSalt = salt, + IsActive = true, + IsActivated = true, + Created = LocalDateTime.FromDateTime(DateTime.UtcNow), + Role = IdentityData.Roles.Employee, + }; + if (context.Departments.All(u => u.Name != department.Name)) { await context.Departments.AddAsync(department); @@ -57,6 +86,11 @@ private static async Task TrySeedAsync(ApplicationDbContext context, string pepp admin.Department = department; await context.Users.AddAsync(admin); } + if (context.Users.All(u => u.Username != staff.Username)) + { + staff.Department = department; + await context.Users.AddAsync(staff); + } } else { @@ -66,6 +100,30 @@ private static async Task TrySeedAsync(ApplicationDbContext context, string pepp admin.Department = departmentEntity; await context.Users.AddAsync(admin); } + if (context.Users.All(u => u.Username != staff.Username)) + { + staff.Department = departmentEntity; + await context.Users.AddAsync(staff); + } + } + + if (context.Departments.All(u => u.Name != itDepartment.Name)) + { + await context.Departments.AddAsync(itDepartment); + if (context.Users.All(u => u.Username != employee.Username)) + { + employee.Department = itDepartment; + await context.Users.AddAsync(employee); + } + } + else + { + var departmentEntity = context.Departments.Single(x => x.Name.Equals(itDepartment.Name)); + if (context.Users.All(u => u.Username != employee.Username)) + { + employee.Department = departmentEntity; + await context.Users.AddAsync(employee); + } } await context.SaveChangesAsync(); From 3240c6793e9ec30f719d8c67b8ebfceb115ab2a3 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Tue, 13 Jun 2023 05:32:35 +0700 Subject: [PATCH 13/56] add: logging entities --- .../Interfaces/IApplicationDbContext.cs | 6 + src/Domain/Common/BaseLoggingEntity.cs | 15 + src/Domain/Entities/Logging/DocumentLog.cs | 9 + src/Domain/Entities/Logging/FolderLog.cs | 8 + src/Domain/Entities/Logging/LockerLog.cs | 8 + src/Domain/Entities/Logging/RoomLog.cs | 8 + src/Domain/Entities/Physical/Borrow.cs | 2 +- src/Domain/Entities/Physical/Document.cs | 2 +- src/Domain/Entities/Physical/Folder.cs | 2 +- src/Domain/Entities/Physical/Locker.cs | 2 +- src/Domain/Entities/Physical/Room.cs | 2 +- src/Domain/Statuses/BorrowRequestStatus.cs | 6 +- src/Domain/Statuses/DocumentStatus.cs | 2 + .../Persistence/ApplicationDbContext.cs | 7 + .../20230612222216_Logging.Designer.cs | 879 ++++++++++++++++++ .../Migrations/20230612222216_Logging.cs | 381 ++++++++ .../ApplicationDbContextModelSnapshot.cs | 240 +++++ 17 files changed, 1571 insertions(+), 8 deletions(-) create mode 100644 src/Domain/Common/BaseLoggingEntity.cs create mode 100644 src/Domain/Entities/Logging/DocumentLog.cs create mode 100644 src/Domain/Entities/Logging/FolderLog.cs create mode 100644 src/Domain/Entities/Logging/LockerLog.cs create mode 100644 src/Domain/Entities/Logging/RoomLog.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230612222216_Logging.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230612222216_Logging.cs diff --git a/src/Application/Common/Interfaces/IApplicationDbContext.cs b/src/Application/Common/Interfaces/IApplicationDbContext.cs index 8f12f9bd..a35398bf 100644 --- a/src/Application/Common/Interfaces/IApplicationDbContext.cs +++ b/src/Application/Common/Interfaces/IApplicationDbContext.cs @@ -1,5 +1,6 @@ using Domain.Entities; using Domain.Entities.Digital; +using Domain.Entities.Logging; using Domain.Entities.Physical; using Microsoft.EntityFrameworkCore; @@ -20,6 +21,11 @@ public interface IApplicationDbContext public DbSet UserGroups { get; } public DbSet Files { get; } public DbSet Entries { get; } + + public DbSet RoomLogs { get; } + public DbSet LockerLogs { get; } + public DbSet FolderLogs { get; } + public DbSet DocumentLogs { get; } Task SaveChangesAsync(CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Domain/Common/BaseLoggingEntity.cs b/src/Domain/Common/BaseLoggingEntity.cs new file mode 100644 index 00000000..f6dfe0df --- /dev/null +++ b/src/Domain/Common/BaseLoggingEntity.cs @@ -0,0 +1,15 @@ +using Domain.Entities; +using NodaTime; + +namespace Domain.Common; + +public class BaseLoggingEntity : BaseEntity + where T : BaseEntity +{ + public string Action { get; set; } = null!; + public Guid UserId { get; set; } + public T? Object { get; set; } + public LocalDateTime Time { get; set; } + + public User User { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Domain/Entities/Logging/DocumentLog.cs b/src/Domain/Entities/Logging/DocumentLog.cs new file mode 100644 index 00000000..13425e10 --- /dev/null +++ b/src/Domain/Entities/Logging/DocumentLog.cs @@ -0,0 +1,9 @@ +using Domain.Common; +using Domain.Entities.Physical; +using NodaTime; + +namespace Domain.Entities.Logging; + +public class DocumentLog : BaseLoggingEntity +{ +} \ No newline at end of file diff --git a/src/Domain/Entities/Logging/FolderLog.cs b/src/Domain/Entities/Logging/FolderLog.cs new file mode 100644 index 00000000..fcefc9a2 --- /dev/null +++ b/src/Domain/Entities/Logging/FolderLog.cs @@ -0,0 +1,8 @@ +using Domain.Common; +using Domain.Entities.Physical; + +namespace Domain.Entities.Logging; + +public class FolderLog : BaseLoggingEntity +{ +} \ No newline at end of file diff --git a/src/Domain/Entities/Logging/LockerLog.cs b/src/Domain/Entities/Logging/LockerLog.cs new file mode 100644 index 00000000..b1354a29 --- /dev/null +++ b/src/Domain/Entities/Logging/LockerLog.cs @@ -0,0 +1,8 @@ +using Domain.Common; +using Domain.Entities.Physical; + +namespace Domain.Entities.Logging; + +public class LockerLog : BaseLoggingEntity +{ +} \ No newline at end of file diff --git a/src/Domain/Entities/Logging/RoomLog.cs b/src/Domain/Entities/Logging/RoomLog.cs new file mode 100644 index 00000000..666c0733 --- /dev/null +++ b/src/Domain/Entities/Logging/RoomLog.cs @@ -0,0 +1,8 @@ +using Domain.Common; +using Domain.Entities.Physical; + +namespace Domain.Entities.Logging; + +public class RoomLog : BaseLoggingEntity +{ +} \ No newline at end of file diff --git a/src/Domain/Entities/Physical/Borrow.cs b/src/Domain/Entities/Physical/Borrow.cs index 8b3076ac..e5ece11f 100644 --- a/src/Domain/Entities/Physical/Borrow.cs +++ b/src/Domain/Entities/Physical/Borrow.cs @@ -4,7 +4,7 @@ namespace Domain.Entities.Physical; -public class Borrow : BaseEntity +public class Borrow : BaseAuditableEntity { public User Borrower { get; set; } = null!; public Document Document { get; set; } = null!; diff --git a/src/Domain/Entities/Physical/Document.cs b/src/Domain/Entities/Physical/Document.cs index a473a2b5..ec13872a 100644 --- a/src/Domain/Entities/Physical/Document.cs +++ b/src/Domain/Entities/Physical/Document.cs @@ -4,7 +4,7 @@ namespace Domain.Entities.Physical; -public class Document : BaseEntity +public class Document : BaseAuditableEntity { public string Title { get; set; } = null!; public string? Description { get; set; } diff --git a/src/Domain/Entities/Physical/Folder.cs b/src/Domain/Entities/Physical/Folder.cs index 596e648c..bb7aacee 100644 --- a/src/Domain/Entities/Physical/Folder.cs +++ b/src/Domain/Entities/Physical/Folder.cs @@ -2,7 +2,7 @@ namespace Domain.Entities.Physical; -public class Folder : BaseEntity +public class Folder : BaseAuditableEntity { public string Name { get; set; } = null!; public string? Description { get; set; } diff --git a/src/Domain/Entities/Physical/Locker.cs b/src/Domain/Entities/Physical/Locker.cs index 5f0af84c..3bd9f836 100644 --- a/src/Domain/Entities/Physical/Locker.cs +++ b/src/Domain/Entities/Physical/Locker.cs @@ -2,7 +2,7 @@ namespace Domain.Entities.Physical; -public class Locker : BaseEntity +public class Locker : BaseAuditableEntity { public string Name { get; set; } = null!; public string? Description { get; set; } diff --git a/src/Domain/Entities/Physical/Room.cs b/src/Domain/Entities/Physical/Room.cs index 3d2318ac..e6b988db 100644 --- a/src/Domain/Entities/Physical/Room.cs +++ b/src/Domain/Entities/Physical/Room.cs @@ -2,7 +2,7 @@ namespace Domain.Entities.Physical; -public class Room : BaseEntity +public class Room : BaseAuditableEntity { public string Name { get; set; } = null!; public string? Description { get; set; } diff --git a/src/Domain/Statuses/BorrowRequestStatus.cs b/src/Domain/Statuses/BorrowRequestStatus.cs index 15c19a0b..8ef13936 100644 --- a/src/Domain/Statuses/BorrowRequestStatus.cs +++ b/src/Domain/Statuses/BorrowRequestStatus.cs @@ -2,13 +2,13 @@ namespace Domain.Statuses; public enum BorrowRequestStatus { - Approved, Pending, + Approved, Rejected, - Overdue, - Cancelled, CheckedOut, Returned, + Overdue, + Cancelled, Lost, NotProcessable, } \ No newline at end of file diff --git a/src/Domain/Statuses/DocumentStatus.cs b/src/Domain/Statuses/DocumentStatus.cs index 133c1cd1..2e6bc23e 100644 --- a/src/Domain/Statuses/DocumentStatus.cs +++ b/src/Domain/Statuses/DocumentStatus.cs @@ -3,6 +3,8 @@ namespace Domain.Statuses; public enum DocumentStatus { Issued, + Approved, + Rejected, Available, Borrowed, Lost, diff --git a/src/Infrastructure/Persistence/ApplicationDbContext.cs b/src/Infrastructure/Persistence/ApplicationDbContext.cs index 91c14162..0f3de028 100644 --- a/src/Infrastructure/Persistence/ApplicationDbContext.cs +++ b/src/Infrastructure/Persistence/ApplicationDbContext.cs @@ -2,6 +2,7 @@ using Application.Common.Interfaces; using Domain.Entities; using Domain.Entities.Digital; +using Domain.Entities.Logging; using Domain.Entities.Physical; using Infrastructure.Common; using MediatR; @@ -34,6 +35,12 @@ public ApplicationDbContext( public DbSet RefreshTokens => Set(); public DbSet ResetPasswordTokens => Set(); + + public DbSet RoomLogs => Set(); + public DbSet LockerLogs => Set(); + public DbSet FolderLogs => Set(); + public DbSet DocumentLogs => Set(); + protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/src/Infrastructure/Persistence/Migrations/20230612222216_Logging.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230612222216_Logging.Designer.cs new file mode 100644 index 00000000..69cd1b45 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230612222216_Logging.Designer.cs @@ -0,0 +1,879 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230612222216_Logging")] + partial class Logging + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId") + .IsUnique(); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Entry"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithOne("Room") + .HasForeignKey("Domain.Entities.Physical.Room", "DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230612222216_Logging.cs b/src/Infrastructure/Persistence/Migrations/20230612222216_Logging.cs new file mode 100644 index 00000000..322b6a3c --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230612222216_Logging.cs @@ -0,0 +1,381 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class Logging : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Created", + table: "Rooms", + type: "timestamp without time zone", + nullable: false, + defaultValue: new NodaTime.LocalDateTime(1, 1, 1, 0, 0)); + + migrationBuilder.AddColumn( + name: "CreatedBy", + table: "Rooms", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModified", + table: "Rooms", + type: "timestamp without time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModifiedBy", + table: "Rooms", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "Created", + table: "Lockers", + type: "timestamp without time zone", + nullable: false, + defaultValue: new NodaTime.LocalDateTime(1, 1, 1, 0, 0)); + + migrationBuilder.AddColumn( + name: "CreatedBy", + table: "Lockers", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModified", + table: "Lockers", + type: "timestamp without time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModifiedBy", + table: "Lockers", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "Created", + table: "Folders", + type: "timestamp without time zone", + nullable: false, + defaultValue: new NodaTime.LocalDateTime(1, 1, 1, 0, 0)); + + migrationBuilder.AddColumn( + name: "CreatedBy", + table: "Folders", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModified", + table: "Folders", + type: "timestamp without time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModifiedBy", + table: "Folders", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "Created", + table: "Documents", + type: "timestamp without time zone", + nullable: false, + defaultValue: new NodaTime.LocalDateTime(1, 1, 1, 0, 0)); + + migrationBuilder.AddColumn( + name: "CreatedBy", + table: "Documents", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModified", + table: "Documents", + type: "timestamp without time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModifiedBy", + table: "Documents", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "Created", + table: "Borrows", + type: "timestamp without time zone", + nullable: false, + defaultValue: new NodaTime.LocalDateTime(1, 1, 1, 0, 0)); + + migrationBuilder.AddColumn( + name: "CreatedBy", + table: "Borrows", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModified", + table: "Borrows", + type: "timestamp without time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastModifiedBy", + table: "Borrows", + type: "uuid", + nullable: true); + + migrationBuilder.CreateTable( + name: "DocumentLogs", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Action = table.Column(type: "text", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + ObjectId = table.Column(type: "uuid", nullable: true), + Time = table.Column(type: "timestamp without time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_DocumentLogs", x => x.Id); + table.ForeignKey( + name: "FK_DocumentLogs_Documents_ObjectId", + column: x => x.ObjectId, + principalTable: "Documents", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_DocumentLogs_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "FolderLogs", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Action = table.Column(type: "text", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + ObjectId = table.Column(type: "uuid", nullable: true), + Time = table.Column(type: "timestamp without time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_FolderLogs", x => x.Id); + table.ForeignKey( + name: "FK_FolderLogs_Folders_ObjectId", + column: x => x.ObjectId, + principalTable: "Folders", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_FolderLogs_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "LockerLogs", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Action = table.Column(type: "text", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + ObjectId = table.Column(type: "uuid", nullable: true), + Time = table.Column(type: "timestamp without time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_LockerLogs", x => x.Id); + table.ForeignKey( + name: "FK_LockerLogs_Lockers_ObjectId", + column: x => x.ObjectId, + principalTable: "Lockers", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_LockerLogs_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "RoomLogs", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Action = table.Column(type: "text", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + ObjectId = table.Column(type: "uuid", nullable: true), + Time = table.Column(type: "timestamp without time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RoomLogs", x => x.Id); + table.ForeignKey( + name: "FK_RoomLogs_Rooms_ObjectId", + column: x => x.ObjectId, + principalTable: "Rooms", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_RoomLogs_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_DocumentLogs_ObjectId", + table: "DocumentLogs", + column: "ObjectId"); + + migrationBuilder.CreateIndex( + name: "IX_DocumentLogs_UserId", + table: "DocumentLogs", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_FolderLogs_ObjectId", + table: "FolderLogs", + column: "ObjectId"); + + migrationBuilder.CreateIndex( + name: "IX_FolderLogs_UserId", + table: "FolderLogs", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_LockerLogs_ObjectId", + table: "LockerLogs", + column: "ObjectId"); + + migrationBuilder.CreateIndex( + name: "IX_LockerLogs_UserId", + table: "LockerLogs", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_RoomLogs_ObjectId", + table: "RoomLogs", + column: "ObjectId"); + + migrationBuilder.CreateIndex( + name: "IX_RoomLogs_UserId", + table: "RoomLogs", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "DocumentLogs"); + + migrationBuilder.DropTable( + name: "FolderLogs"); + + migrationBuilder.DropTable( + name: "LockerLogs"); + + migrationBuilder.DropTable( + name: "RoomLogs"); + + migrationBuilder.DropColumn( + name: "Created", + table: "Rooms"); + + migrationBuilder.DropColumn( + name: "CreatedBy", + table: "Rooms"); + + migrationBuilder.DropColumn( + name: "LastModified", + table: "Rooms"); + + migrationBuilder.DropColumn( + name: "LastModifiedBy", + table: "Rooms"); + + migrationBuilder.DropColumn( + name: "Created", + table: "Lockers"); + + migrationBuilder.DropColumn( + name: "CreatedBy", + table: "Lockers"); + + migrationBuilder.DropColumn( + name: "LastModified", + table: "Lockers"); + + migrationBuilder.DropColumn( + name: "LastModifiedBy", + table: "Lockers"); + + migrationBuilder.DropColumn( + name: "Created", + table: "Folders"); + + migrationBuilder.DropColumn( + name: "CreatedBy", + table: "Folders"); + + migrationBuilder.DropColumn( + name: "LastModified", + table: "Folders"); + + migrationBuilder.DropColumn( + name: "LastModifiedBy", + table: "Folders"); + + migrationBuilder.DropColumn( + name: "Created", + table: "Documents"); + + migrationBuilder.DropColumn( + name: "CreatedBy", + table: "Documents"); + + migrationBuilder.DropColumn( + name: "LastModified", + table: "Documents"); + + migrationBuilder.DropColumn( + name: "LastModifiedBy", + table: "Documents"); + + migrationBuilder.DropColumn( + name: "Created", + table: "Borrows"); + + migrationBuilder.DropColumn( + name: "CreatedBy", + table: "Borrows"); + + migrationBuilder.DropColumn( + name: "LastModified", + table: "Borrows"); + + migrationBuilder.DropColumn( + name: "LastModifiedBy", + table: "Borrows"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index 9fab7c8c..61b372bc 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -104,6 +104,118 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("UserGroups"); }); + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => { b.Property("Id") @@ -119,12 +231,24 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("BorrowerId") .HasColumnType("uuid"); + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + b.Property("DocumentId") .HasColumnType("uuid"); b.Property("DueTime") .HasColumnType("timestamp without time zone"); + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + b.Property("Reason") .IsRequired() .HasColumnType("text"); @@ -147,6 +271,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uuid"); + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + b.Property("DepartmentId") .HasColumnType("uuid"); @@ -168,6 +298,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ImporterId") .HasColumnType("uuid"); + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + b.Property("Status") .HasColumnType("integer"); @@ -199,6 +335,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Capacity") .HasColumnType("integer"); + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + b.Property("Description") .HasMaxLength(256) .HasColumnType("character varying(256)"); @@ -206,6 +348,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsAvailable") .HasColumnType("boolean"); + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + b.Property("LockerId") .HasColumnType("uuid"); @@ -233,6 +381,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Capacity") .HasColumnType("integer"); + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + b.Property("Description") .HasMaxLength(256) .HasColumnType("character varying(256)"); @@ -240,6 +394,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsAvailable") .HasColumnType("boolean"); + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + b.Property("Name") .IsRequired() .HasMaxLength(64) @@ -267,6 +427,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Capacity") .HasColumnType("integer"); + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + b.Property("DepartmentId") .HasColumnType("uuid"); @@ -277,6 +443,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsAvailable") .HasColumnType("boolean"); + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + b.Property("Name") .IsRequired() .HasMaxLength(64) @@ -467,6 +639,74 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("File"); }); + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => { b.HasOne("Domain.Entities.User", "Borrower") From 46d098030a384f4cce0bed09ffe2589851363ef6 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Tue, 13 Jun 2023 05:53:21 +0700 Subject: [PATCH 14/56] add: permission entity --- .../Interfaces/IApplicationDbContext.cs | 1 + src/Domain/Entities/Physical/Permission.cs | 11 + .../Persistence/ApplicationDbContext.cs | 1 + .../Configurations/PermissionConfiguration.cs | 16 + .../20230612223900_Permission.Designer.cs | 917 ++++++++++++++++++ .../Migrations/20230612223900_Permission.cs | 52 + .../ApplicationDbContextModelSnapshot.cs | 38 + 7 files changed, 1036 insertions(+) create mode 100644 src/Domain/Entities/Physical/Permission.cs create mode 100644 src/Infrastructure/Persistence/Configurations/PermissionConfiguration.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230612223900_Permission.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230612223900_Permission.cs diff --git a/src/Application/Common/Interfaces/IApplicationDbContext.cs b/src/Application/Common/Interfaces/IApplicationDbContext.cs index a35398bf..5ba4e077 100644 --- a/src/Application/Common/Interfaces/IApplicationDbContext.cs +++ b/src/Application/Common/Interfaces/IApplicationDbContext.cs @@ -17,6 +17,7 @@ public interface IApplicationDbContext public DbSet Folders { get; } public DbSet Documents { get; } public DbSet Borrows { get; } + public DbSet Permissions { get; } public DbSet UserGroups { get; } public DbSet Files { get; } diff --git a/src/Domain/Entities/Physical/Permission.cs b/src/Domain/Entities/Physical/Permission.cs new file mode 100644 index 00000000..3842eb28 --- /dev/null +++ b/src/Domain/Entities/Physical/Permission.cs @@ -0,0 +1,11 @@ +namespace Domain.Entities.Physical; + +public class Permission +{ + public Guid EmployeeId { get; set; } + public Guid DocumentId { get; set; } + public string AllowedOperations { get; set; } = null!; + + public User Employee { get; set; } = null!; + public Document Document { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/ApplicationDbContext.cs b/src/Infrastructure/Persistence/ApplicationDbContext.cs index 0f3de028..880fca32 100644 --- a/src/Infrastructure/Persistence/ApplicationDbContext.cs +++ b/src/Infrastructure/Persistence/ApplicationDbContext.cs @@ -28,6 +28,7 @@ public ApplicationDbContext( public DbSet Folders => Set(); public DbSet Documents => Set(); public DbSet Borrows => Set(); + public DbSet Permissions => Set(); public DbSet UserGroups => Set(); public DbSet Files => Set(); diff --git a/src/Infrastructure/Persistence/Configurations/PermissionConfiguration.cs b/src/Infrastructure/Persistence/Configurations/PermissionConfiguration.cs new file mode 100644 index 00000000..0f5db055 --- /dev/null +++ b/src/Infrastructure/Persistence/Configurations/PermissionConfiguration.cs @@ -0,0 +1,16 @@ +using Domain.Entities.Physical; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Infrastructure.Persistence.Configurations; + +public class PermissionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(x => new { x.DocumentId, x.EmployeeId }); + + builder.Property(x => x.AllowedOperations) + .IsRequired(); + } +} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Migrations/20230612223900_Permission.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230612223900_Permission.Designer.cs new file mode 100644 index 00000000..303b1ca8 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230612223900_Permission.Designer.cs @@ -0,0 +1,917 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230612223900_Permission")] + partial class Permission + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId") + .IsUnique(); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Entry"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithOne("Room") + .HasForeignKey("Domain.Entities.Physical.Room", "DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230612223900_Permission.cs b/src/Infrastructure/Persistence/Migrations/20230612223900_Permission.cs new file mode 100644 index 00000000..d910fecb --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230612223900_Permission.cs @@ -0,0 +1,52 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class Permission : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Permissions", + columns: table => new + { + EmployeeId = table.Column(type: "uuid", nullable: false), + DocumentId = table.Column(type: "uuid", nullable: false), + AllowedOperations = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Permissions", x => new { x.DocumentId, x.EmployeeId }); + table.ForeignKey( + name: "FK_Permissions_Documents_DocumentId", + column: x => x.DocumentId, + principalTable: "Documents", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Permissions_Users_EmployeeId", + column: x => x.EmployeeId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Permissions_EmployeeId", + table: "Permissions", + column: "EmployeeId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Permissions"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index 61b372bc..290ad10b 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -418,6 +418,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Lockers"); }); + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + modelBuilder.Entity("Domain.Entities.Physical.Room", b => { b.Property("Id") @@ -775,6 +794,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Room"); }); + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + modelBuilder.Entity("Domain.Entities.Physical.Room", b => { b.HasOne("Domain.Entities.Department", "Department") From 2ada1756797d8111607f8e5eab0ec70c536e09e6 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Tue, 13 Jun 2023 06:06:48 +0700 Subject: [PATCH 15/56] add: request logs --- .../Interfaces/IApplicationDbContext.cs | 1 + src/Domain/Entities/Logging/RequestLog.cs | 10 + src/Domain/Enums/RequestType.cs | 7 + .../Persistence/ApplicationDbContext.cs | 1 + .../20230612230312_RequestLog.Designer.cs | 965 ++++++++++++++++++ .../Migrations/20230612230312_RequestLog.cs | 60 ++ .../ApplicationDbContextModelSnapshot.cs | 48 + 7 files changed, 1092 insertions(+) create mode 100644 src/Domain/Entities/Logging/RequestLog.cs create mode 100644 src/Domain/Enums/RequestType.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230612230312_RequestLog.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230612230312_RequestLog.cs diff --git a/src/Application/Common/Interfaces/IApplicationDbContext.cs b/src/Application/Common/Interfaces/IApplicationDbContext.cs index 5ba4e077..0d22a58d 100644 --- a/src/Application/Common/Interfaces/IApplicationDbContext.cs +++ b/src/Application/Common/Interfaces/IApplicationDbContext.cs @@ -27,6 +27,7 @@ public interface IApplicationDbContext public DbSet LockerLogs { get; } public DbSet FolderLogs { get; } public DbSet DocumentLogs { get; } + public DbSet RequestLogs { get; } Task SaveChangesAsync(CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Domain/Entities/Logging/RequestLog.cs b/src/Domain/Entities/Logging/RequestLog.cs new file mode 100644 index 00000000..fbf7d568 --- /dev/null +++ b/src/Domain/Entities/Logging/RequestLog.cs @@ -0,0 +1,10 @@ +using Domain.Common; +using Domain.Entities.Physical; +using Domain.Enums; + +namespace Domain.Entities.Logging; + +public class RequestLog : BaseLoggingEntity +{ + public RequestType Type { get; set; } +} \ No newline at end of file diff --git a/src/Domain/Enums/RequestType.cs b/src/Domain/Enums/RequestType.cs new file mode 100644 index 00000000..f6dbac41 --- /dev/null +++ b/src/Domain/Enums/RequestType.cs @@ -0,0 +1,7 @@ +namespace Domain.Enums; + +public enum RequestType +{ + Import, + Borrow, +} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/ApplicationDbContext.cs b/src/Infrastructure/Persistence/ApplicationDbContext.cs index 880fca32..ee52b923 100644 --- a/src/Infrastructure/Persistence/ApplicationDbContext.cs +++ b/src/Infrastructure/Persistence/ApplicationDbContext.cs @@ -41,6 +41,7 @@ public ApplicationDbContext( public DbSet LockerLogs => Set(); public DbSet FolderLogs => Set(); public DbSet DocumentLogs => Set(); + public DbSet RequestLogs => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) diff --git a/src/Infrastructure/Persistence/Migrations/20230612230312_RequestLog.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230612230312_RequestLog.Designer.cs new file mode 100644 index 00000000..b34b7bfe --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230612230312_RequestLog.Designer.cs @@ -0,0 +1,965 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230612230312_RequestLog")] + partial class RequestLog + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId") + .IsUnique(); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Entry"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithOne("Room") + .HasForeignKey("Domain.Entities.Physical.Room", "DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230612230312_RequestLog.cs b/src/Infrastructure/Persistence/Migrations/20230612230312_RequestLog.cs new file mode 100644 index 00000000..e06dcb91 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230612230312_RequestLog.cs @@ -0,0 +1,60 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class RequestLog : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "RequestLogs", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Type = table.Column(type: "integer", nullable: false), + Action = table.Column(type: "text", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + ObjectId = table.Column(type: "uuid", nullable: true), + Time = table.Column(type: "timestamp without time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RequestLogs", x => x.Id); + table.ForeignKey( + name: "FK_RequestLogs_Documents_ObjectId", + column: x => x.ObjectId, + principalTable: "Documents", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_RequestLogs_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_RequestLogs_ObjectId", + table: "RequestLogs", + column: "ObjectId"); + + migrationBuilder.CreateIndex( + name: "IX_RequestLogs_UserId", + table: "RequestLogs", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RequestLogs"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index 290ad10b..f7139052 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -188,6 +188,37 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("LockerLogs"); }); + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => { b.Property("Id") @@ -709,6 +740,23 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => { b.HasOne("Domain.Entities.Physical.Room", "Object") From 7e960c2b38014387b60246656cd7285a46c590e4 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Tue, 13 Jun 2023 06:46:40 +0700 Subject: [PATCH 16/56] add: document now has IsPrivate property and user log --- .../Interfaces/IApplicationDbContext.cs | 1 + src/Domain/Entities/Logging/UserLog.cs | 7 + src/Domain/Entities/Physical/Document.cs | 1 + .../Persistence/ApplicationDbContext.cs | 1 + .../Configurations/DocumentConfiguration.cs | 3 + .../Configurations/UserLogConfiguration.cs | 25 + ...30612231014_DocumentVisibility.Designer.cs | 968 ++++++++++++++++ .../20230612231014_DocumentVisibility.cs | 29 + .../20230612231759_UserLog.Designer.cs | 1015 +++++++++++++++++ .../Migrations/20230612231759_UserLog.cs | 60 + .../ApplicationDbContextModelSnapshot.cs | 50 + 11 files changed, 2160 insertions(+) create mode 100644 src/Domain/Entities/Logging/UserLog.cs create mode 100644 src/Infrastructure/Persistence/Configurations/UserLogConfiguration.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230612231014_DocumentVisibility.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230612231014_DocumentVisibility.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230612231759_UserLog.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230612231759_UserLog.cs diff --git a/src/Application/Common/Interfaces/IApplicationDbContext.cs b/src/Application/Common/Interfaces/IApplicationDbContext.cs index 0d22a58d..88d61ad1 100644 --- a/src/Application/Common/Interfaces/IApplicationDbContext.cs +++ b/src/Application/Common/Interfaces/IApplicationDbContext.cs @@ -28,6 +28,7 @@ public interface IApplicationDbContext public DbSet FolderLogs { get; } public DbSet DocumentLogs { get; } public DbSet RequestLogs { get; } + public DbSet UserLogs { get; } Task SaveChangesAsync(CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Domain/Entities/Logging/UserLog.cs b/src/Domain/Entities/Logging/UserLog.cs new file mode 100644 index 00000000..2310fb75 --- /dev/null +++ b/src/Domain/Entities/Logging/UserLog.cs @@ -0,0 +1,7 @@ +using Domain.Common; + +namespace Domain.Entities.Logging; + +public class UserLog : BaseLoggingEntity +{ +} \ No newline at end of file diff --git a/src/Domain/Entities/Physical/Document.cs b/src/Domain/Entities/Physical/Document.cs index ec13872a..c5dd876a 100644 --- a/src/Domain/Entities/Physical/Document.cs +++ b/src/Domain/Entities/Physical/Document.cs @@ -14,6 +14,7 @@ public class Document : BaseAuditableEntity public Folder? Folder { get; set; } public DocumentStatus Status { get; set; } public Guid? EntryId { get; set; } + public bool IsPrivate { get; set; } public virtual Entry? Entry { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/ApplicationDbContext.cs b/src/Infrastructure/Persistence/ApplicationDbContext.cs index ee52b923..5ee7171b 100644 --- a/src/Infrastructure/Persistence/ApplicationDbContext.cs +++ b/src/Infrastructure/Persistence/ApplicationDbContext.cs @@ -42,6 +42,7 @@ public ApplicationDbContext( public DbSet FolderLogs => Set(); public DbSet DocumentLogs => Set(); public DbSet RequestLogs => Set(); + public DbSet UserLogs => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) diff --git a/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs b/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs index a9d89058..287224e1 100644 --- a/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs +++ b/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs @@ -46,5 +46,8 @@ public void Configure(EntityTypeBuilder builder) .WithOne() .HasForeignKey(x => x.EntryId) .IsRequired(false); + + builder.Property(x => x.IsPrivate) + .IsRequired(); } } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Configurations/UserLogConfiguration.cs b/src/Infrastructure/Persistence/Configurations/UserLogConfiguration.cs new file mode 100644 index 00000000..92f2fe16 --- /dev/null +++ b/src/Infrastructure/Persistence/Configurations/UserLogConfiguration.cs @@ -0,0 +1,25 @@ +using Domain.Entities.Logging; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Infrastructure.Persistence.Configurations; + +public class UserLogConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(x => x.Id); + builder.Property(x => x.Id) + .ValueGeneratedOnAdd(); + + builder.HasOne(x => x.User) + .WithMany() + .HasForeignKey(x => x.UserId) + .IsRequired(); + + builder.HasOne(x => x.Object) + .WithMany() + .HasForeignKey("ObjectId") + .IsRequired(); + } +} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Migrations/20230612231014_DocumentVisibility.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230612231014_DocumentVisibility.Designer.cs new file mode 100644 index 00000000..0974cf5a --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230612231014_DocumentVisibility.Designer.cs @@ -0,0 +1,968 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230612231014_DocumentVisibility")] + partial class DocumentVisibility + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId") + .IsUnique(); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Entry"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithOne("Room") + .HasForeignKey("Domain.Entities.Physical.Room", "DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230612231014_DocumentVisibility.cs b/src/Infrastructure/Persistence/Migrations/20230612231014_DocumentVisibility.cs new file mode 100644 index 00000000..673b2ad5 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230612231014_DocumentVisibility.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class DocumentVisibility : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsPrivate", + table: "Documents", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsPrivate", + table: "Documents"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230612231759_UserLog.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230612231759_UserLog.Designer.cs new file mode 100644 index 00000000..ea6e1761 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230612231759_UserLog.Designer.cs @@ -0,0 +1,1015 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230612231759_UserLog")] + partial class UserLog + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId") + .IsUnique(); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.HasOne("Domain.Entities.User", "Object") + .WithMany() + .HasForeignKey("ObjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Entry"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithOne("Room") + .HasForeignKey("Domain.Entities.Physical.Room", "DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230612231759_UserLog.cs b/src/Infrastructure/Persistence/Migrations/20230612231759_UserLog.cs new file mode 100644 index 00000000..c26f9e4b --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230612231759_UserLog.cs @@ -0,0 +1,60 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class UserLog : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "UserLogs", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Action = table.Column(type: "text", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + ObjectId = table.Column(type: "uuid", nullable: false), + Time = table.Column(type: "timestamp without time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserLogs", x => x.Id); + table.ForeignKey( + name: "FK_UserLogs_Users_ObjectId", + column: x => x.ObjectId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_UserLogs_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_UserLogs_ObjectId", + table: "UserLogs", + column: "ObjectId"); + + migrationBuilder.CreateIndex( + name: "IX_UserLogs_UserId", + table: "UserLogs", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UserLogs"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index f7139052..679625fe 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -247,6 +247,34 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("RoomLogs"); }); + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogs"); + }); + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => { b.Property("Id") @@ -329,6 +357,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ImporterId") .HasColumnType("uuid"); + b.Property("IsPrivate") + .HasColumnType("boolean"); + b.Property("LastModified") .HasColumnType("timestamp without time zone"); @@ -774,6 +805,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.HasOne("Domain.Entities.User", "Object") + .WithMany() + .HasForeignKey("ObjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => { b.HasOne("Domain.Entities.User", "Borrower") From 55ded40380e8614d25d7e2ceaec94bc1fa1ac14c Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Tue, 13 Jun 2023 06:56:21 +0700 Subject: [PATCH 17/56] add: current user service and permission manager --- src/Api/Services/CurrentUserService.cs | 38 ++++---- .../Common/Interfaces/ICurrentUserService.cs | 3 +- .../Common/Interfaces/IPermissionManager.cs | 12 +++ .../Models/Operations/DocumentOperation.cs | 7 ++ src/Infrastructure/ConfigureServices.cs | 1 + src/Infrastructure/Infrastructure.csproj | 1 + .../Services/PermissionManager.cs | 90 +++++++++++++++++++ 7 files changed, 129 insertions(+), 23 deletions(-) create mode 100644 src/Application/Common/Interfaces/IPermissionManager.cs create mode 100644 src/Application/Common/Models/Operations/DocumentOperation.cs create mode 100644 src/Infrastructure/Services/PermissionManager.cs diff --git a/src/Api/Services/CurrentUserService.cs b/src/Api/Services/CurrentUserService.cs index 920c6361..25c349de 100644 --- a/src/Api/Services/CurrentUserService.cs +++ b/src/Api/Services/CurrentUserService.cs @@ -16,10 +16,17 @@ public CurrentUserService(IHttpContextAccessor httpContextAccessor, IApplication _context = context; } + public Guid GetId() + { + var id = _httpContextAccessor.HttpContext!.User.Claims + .FirstOrDefault(x => x.Type.Equals(JwtRegisteredClaimNames.NameId))!.Value; + return Guid.Parse(id); + } + public string GetRole() { var userName = _httpContextAccessor.HttpContext!.User.Claims - .FirstOrDefault(x => x.Type.Equals("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"))!.Value; + .FirstOrDefault(x => x.Type.Equals(JwtRegisteredClaimNames.Sub))!.Value; if (userName is null) { throw new UnauthorizedAccessException(); @@ -35,31 +42,18 @@ public string GetRole() return user.Role; } - public string? GetDepartment() + public Guid? GetDepartmentId() { - var userName = _httpContextAccessor.HttpContext!.User.Claims - .FirstOrDefault(x => x.Type.Equals("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"))!.Value; - if (userName is null) - { - throw new UnauthorizedAccessException(); - } - - var user = _context.Users - .Include(x => x.Department) - .FirstOrDefault(x => x.Username.Equals(userName)); - - if (user is null) - { - throw new UnauthorizedAccessException(); - } - - return user.Department?.Name; + var claim = _httpContextAccessor.HttpContext!.User.Claims + .FirstOrDefault(x => x.Type.Equals("departmentId")); + var id = claim?.Value; + return id is not null ? Guid.Parse(id) : null; } public User GetCurrentUser() { var userName = _httpContextAccessor.HttpContext!.User.Claims - .FirstOrDefault(x => x.Type.Equals("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"))!.Value; + .FirstOrDefault(x => x.Type.Equals(JwtRegisteredClaimNames.Sub))!.Value; if (userName is null) { throw new UnauthorizedAccessException(); @@ -80,7 +74,7 @@ public User GetCurrentUser() public Guid? GetCurrentRoomForStaff() { var userName = _httpContextAccessor.HttpContext!.User.Claims - .FirstOrDefault(x => x.Type.Equals("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"))!.Value; + .FirstOrDefault(x => x.Type.Equals(JwtRegisteredClaimNames.Sub))!.Value; if (userName is null) { throw new UnauthorizedAccessException(); @@ -102,7 +96,7 @@ public User GetCurrentUser() public Guid? GetCurrentDepartmentForStaff() { var userName = _httpContextAccessor.HttpContext!.User.Claims - .FirstOrDefault(x => x.Type.Equals("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"))!.Value; + .FirstOrDefault(x => x.Type.Equals(JwtRegisteredClaimNames.Sub))!.Value; if (userName is null) { throw new UnauthorizedAccessException(); diff --git a/src/Application/Common/Interfaces/ICurrentUserService.cs b/src/Application/Common/Interfaces/ICurrentUserService.cs index 0c4aed4b..91f58a14 100644 --- a/src/Application/Common/Interfaces/ICurrentUserService.cs +++ b/src/Application/Common/Interfaces/ICurrentUserService.cs @@ -4,8 +4,9 @@ namespace Application.Common.Interfaces; public interface ICurrentUserService { + Guid GetId(); string GetRole(); - string? GetDepartment(); + Guid? GetDepartmentId(); User GetCurrentUser(); Guid? GetCurrentRoomForStaff(); Guid? GetCurrentDepartmentForStaff(); diff --git a/src/Application/Common/Interfaces/IPermissionManager.cs b/src/Application/Common/Interfaces/IPermissionManager.cs new file mode 100644 index 00000000..6c0e7093 --- /dev/null +++ b/src/Application/Common/Interfaces/IPermissionManager.cs @@ -0,0 +1,12 @@ +using Application.Common.Models.Operations; +using Domain.Entities; +using Domain.Entities.Physical; + +namespace Application.Common.Interfaces; + +public interface IPermissionManager +{ + bool IsGranted(Guid documentId, DocumentOperation operation, params Guid[] userIds); + Task GrantAsync(Document document, DocumentOperation operation, User[] users, CancellationToken cancellationToken); + Task RevokeAsync(Document document, DocumentOperation operation, User[] users, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/Application/Common/Models/Operations/DocumentOperation.cs b/src/Application/Common/Models/Operations/DocumentOperation.cs new file mode 100644 index 00000000..da6faa45 --- /dev/null +++ b/src/Application/Common/Models/Operations/DocumentOperation.cs @@ -0,0 +1,7 @@ +namespace Application.Common.Models.Operations; + +public enum DocumentOperation +{ + Read, + Borrow, +} \ No newline at end of file diff --git a/src/Infrastructure/ConfigureServices.cs b/src/Infrastructure/ConfigureServices.cs index 3d4ef3bb..997980c5 100644 --- a/src/Infrastructure/ConfigureServices.cs +++ b/src/Infrastructure/ConfigureServices.cs @@ -20,6 +20,7 @@ public static IServiceCollection AddInfrastructureServices(this IServiceCollecti services.AddScoped(sp => sp.GetService()!); services.AddScoped(sp => sp.GetService()!); services.AddScoped(); + services.AddScoped(); services.AddMailService(configuration); services.AddJweAuthentication(configuration); diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index 8b76879b..82d1f679 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Infrastructure/Services/PermissionManager.cs b/src/Infrastructure/Services/PermissionManager.cs new file mode 100644 index 00000000..74af4cc6 --- /dev/null +++ b/src/Infrastructure/Services/PermissionManager.cs @@ -0,0 +1,90 @@ +using System.Configuration; +using Application.Common.Interfaces; +using Application.Common.Models.Operations; +using Domain.Entities; +using Domain.Entities.Physical; + +namespace Infrastructure.Services; + +public class PermissionManager : IPermissionManager +{ + private readonly IApplicationDbContext _context; + + public PermissionManager(IApplicationDbContext context) + { + _context = context; + } + + public bool IsGranted(Guid documentId, DocumentOperation operation, params Guid[] userIds) + { + return Array.TrueForAll(userIds, id => !_context.Permissions.Any(x => + x.DocumentId == documentId && x.EmployeeId == id && + !x.AllowedOperations.Contains(operation.ToString()))); + } + + public async Task GrantAsync(Document document, DocumentOperation operation, User[] users, CancellationToken cancellationToken) + { + foreach (var user in users) + { + var existedPermission = + _context.Permissions.FirstOrDefault(x => x.DocumentId == document.Id && x.EmployeeId == user.Id); + + if (existedPermission is not null) + { + var operations = existedPermission.AllowedOperations.Split(","); + if (operations.Contains(operation.ToString())) + { + continue; + } + + var x = new CommaDelimitedStringCollection + { + existedPermission.AllowedOperations, + operation.ToString() + }; + existedPermission.AllowedOperations = x.ToString(); + _context.Permissions.Update(existedPermission); + } + else + { + existedPermission = new Permission() + { + DocumentId = document.Id, + EmployeeId = user.Id, + Document = document, + Employee = user, + AllowedOperations = operation.ToString(), + }; + await _context.Permissions.AddAsync(existedPermission, cancellationToken); + } + } + + await _context.SaveChangesAsync(cancellationToken); + } + + public async Task RevokeAsync(Document document, DocumentOperation operation, User[] users, CancellationToken cancellationToken) + { + foreach (var user in users) + { + var existedPermission = + _context.Permissions.FirstOrDefault(x => x.DocumentId == document.Id && x.EmployeeId == user.Id); + if (existedPermission is null) continue; + var operations = existedPermission.AllowedOperations.Split(","); + if (!operations.Contains(operation.ToString())) continue; + + var x = new CommaDelimitedStringCollection(); + x.AddRange(operations); + x.Remove(operation.ToString()); + if (x.Count == 0) + { + _context.Permissions.Remove(existedPermission); + } + else + { + existedPermission.AllowedOperations = x.ToString(); + _context.Permissions.Update(existedPermission); + } + } + await _context.SaveChangesAsync(cancellationToken); + } +} \ No newline at end of file From 916881b0ee62144714f4fe7f3d9d41dc6e8947ca Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Tue, 13 Jun 2023 07:18:24 +0700 Subject: [PATCH 18/56] update: clear mapping for jwt claims --- src/Infrastructure/ConfigureServices.cs | 3 ++- src/Infrastructure/Identity/IdentityService.cs | 12 ++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Infrastructure/ConfigureServices.cs b/src/Infrastructure/ConfigureServices.cs index 997980c5..9c8c6b72 100644 --- a/src/Infrastructure/ConfigureServices.cs +++ b/src/Infrastructure/ConfigureServices.cs @@ -1,3 +1,4 @@ +using System.IdentityModel.Tokens.Jwt; using System.Security.Cryptography; using Application.Common.Interfaces; using Infrastructure.Identity; @@ -83,7 +84,7 @@ private static IServiceCollection AddJweAuthentication(this IServiceCollection s services.AddSingleton(encryptionKey); services.AddSingleton(signingKey); services.AddSingleton(tokenValidationParameters); - + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); services.AddAuthentication(JweAuthenticationOptions.DefaultScheme) .AddScheme(JweAuthenticationOptions.DefaultScheme, options => diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index ef335a22..8e913ca5 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -29,7 +29,6 @@ public class IdentityService : IIdentityService private readonly RSA _encryptionKey; private readonly ECDsa _signingKey; private readonly IMapper _mapper; - private readonly SecuritySettings _securitySettings; public IdentityService( TokenValidationParameters tokenValidationParameters, @@ -38,8 +37,7 @@ public IdentityService( IAuthDbContext authDbContext, RSA encryptionKey, ECDsa signingKey, - IMapper mapper, - IOptions securitySettingsOptions) + IMapper mapper) { _tokenValidationParameters = tokenValidationParameters; _jweSettings = jweSettingsOptions.Value; @@ -48,7 +46,6 @@ public IdentityService( _encryptionKey = encryptionKey; _signingKey = signingKey; _mapper = mapper; - _securitySettings = securitySettingsOptions.Value; } public async Task Validate(string token, string refreshToken) @@ -199,7 +196,7 @@ public async Task RefreshTokenAsync(string token, string r .Include(x => x.Department) .FirstOrDefault(x => x.Email!.Equals(email)); - if (user is null || !user.PasswordHash.Equals(password.HashPasswordWith(user.PasswordSalt, _securitySettings.Pepper))) + if (user is null || !user.PasswordHash.Equals(SecurityUtil.Hash(password))) { throw new AuthenticationException("Username or password is invalid."); } @@ -258,9 +255,8 @@ public async Task ResetPassword(string token, string newPassword) { user.IsActivated = true; } - var salt = StringUtil.RandomSalt(); - user.PasswordSalt = salt; - user.PasswordHash = newPassword.HashPasswordWith(salt, newPassword); + + user.PasswordHash = SecurityUtil.Hash(newPassword); resetPasswordToken.IsInvalidated = true; await _applicationDbContext.SaveChangesAsync(CancellationToken.None); await _authDbContext.SaveChangesAsync(CancellationToken.None); From 0e83a5d09b4d41dc409b6b2bc9366e884e63a357 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Tue, 13 Jun 2023 07:22:18 +0700 Subject: [PATCH 19/56] update: fix login --- src/Api/Controllers/DocumentsController.cs | 14 +++++-- .../Documents/ImportDocumentRequest.cs | 8 ---- .../Common/Messages/DocumentLogMessages.cs | 9 +++++ .../Documents/Commands/ImportDocument.cs | 37 ++++++++++--------- .../Identity/IdentityService.cs | 20 +++++----- 5 files changed, 49 insertions(+), 39 deletions(-) create mode 100644 src/Application/Common/Messages/DocumentLogMessages.cs diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index 2a2f369f..5175544f 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -1,4 +1,5 @@ using Api.Controllers.Payload.Requests.Documents; +using Application.Common.Interfaces; using Application.Common.Models; using Application.Common.Models.Dtos.Physical; using Application.Documents.Commands; @@ -11,6 +12,13 @@ namespace Api.Controllers; public class DocumentsController : ApiControllerBase { + private readonly ICurrentUserService _currentUserService; + + public DocumentsController(ICurrentUserService currentUserService) + { + _currentUserService = currentUserService; + } + /// /// Get a document by id /// @@ -77,7 +85,7 @@ public async Task>>> GetAllDocumentTypes /// /// Import document details /// A DocumentDto of the imported document - [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] + [RequiresRole(IdentityData.Roles.Employee)] [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -86,13 +94,13 @@ public async Task>>> GetAllDocumentTypes [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> Import([FromBody] ImportDocumentRequest request) { + var userId = _currentUserService.GetId(); var command = new ImportDocument.Command() { Title = request.Title, Description = request.Description, DocumentType = request.DocumentType, - FolderId = request.FolderId, - ImporterId = request.ImporterId, + ImporterId = userId, }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); diff --git a/src/Api/Controllers/Payload/Requests/Documents/ImportDocumentRequest.cs b/src/Api/Controllers/Payload/Requests/Documents/ImportDocumentRequest.cs index 0bbb2723..7b1b1cd8 100644 --- a/src/Api/Controllers/Payload/Requests/Documents/ImportDocumentRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Documents/ImportDocumentRequest.cs @@ -17,12 +17,4 @@ public class ImportDocumentRequest /// Document type of the document to be imported /// public string DocumentType { get; set; } = null!; - /// - /// Id of the importer - /// - public Guid ImporterId { get; set; } - /// - /// Id of the folder that this document will be in - /// - public Guid FolderId { get; set; } } \ No newline at end of file diff --git a/src/Application/Common/Messages/DocumentLogMessages.cs b/src/Application/Common/Messages/DocumentLogMessages.cs new file mode 100644 index 00000000..acfd4f83 --- /dev/null +++ b/src/Application/Common/Messages/DocumentLogMessages.cs @@ -0,0 +1,9 @@ +namespace Application.Common.Messages; + +public static class DocumentLogMessages +{ + public static class Import + { + public const string NewImport = "Created new document request"; + } +} \ No newline at end of file diff --git a/src/Application/Documents/Commands/ImportDocument.cs b/src/Application/Documents/Commands/ImportDocument.cs index d33d4942..6cfab2f4 100644 --- a/src/Application/Documents/Commands/ImportDocument.cs +++ b/src/Application/Documents/Commands/ImportDocument.cs @@ -1,11 +1,15 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities.Logging; using Domain.Entities.Physical; using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using NodaTime; namespace Application.Documents.Commands; @@ -17,7 +21,6 @@ public record Command : IRequest public string? Description { get; init; } public string DocumentType { get; init; } = null!; public Guid ImporterId { get; init; } - public Guid FolderId { get; init; } } public class CommandHandler : IRequestHandler @@ -25,10 +28,13 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + private readonly ILogger _logger; + + public CommandHandler(IApplicationDbContext context, IMapper mapper, ILogger logger) { _context = context; _mapper = mapper; + _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -38,7 +44,7 @@ public async Task Handle(Command request, CancellationToken cancell .FirstOrDefaultAsync(x => x.Id == request.ImporterId, cancellationToken); if (importer is null) { - throw new KeyNotFoundException("User does not exist."); + throw new UnauthorizedAccessException(); } var document = _context.Documents.FirstOrDefault(x => @@ -50,18 +56,6 @@ public async Task Handle(Command request, CancellationToken cancell throw new ConflictException($"Document title already exists for user {importer.LastName}."); } - var folder = await _context.Folders - .FirstOrDefaultAsync(x => x.Id == request.FolderId, cancellationToken); - if (folder is null) - { - throw new KeyNotFoundException("Folder does not exist."); - } - - if (folder.Capacity == folder.NumberOfDocuments) - { - throw new ConflictException("This folder cannot accept more documents."); - } - var entity = new Document() { Title = request.Title.Trim(), @@ -69,13 +63,20 @@ public async Task Handle(Command request, CancellationToken cancell DocumentType = request.DocumentType.Trim(), Importer = importer, Department = importer.Department, - Folder = folder, Status = DocumentStatus.Issued, }; + var log = new DocumentLog() + { + Object = entity, + Time = LocalDateTime.FromDateTime(DateTime.UtcNow), + User = importer, + UserId = importer.Id, + Action = DocumentLogMessages.Import.NewImport + }; + var result = await _context.Documents.AddAsync(entity, cancellationToken); - folder.NumberOfDocuments += 1; - _context.Folders.Update(folder); + await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index 8e913ca5..9b311700 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -29,6 +29,7 @@ public class IdentityService : IIdentityService private readonly RSA _encryptionKey; private readonly ECDsa _signingKey; private readonly IMapper _mapper; + private readonly SecuritySettings _securitySettings; public IdentityService( TokenValidationParameters tokenValidationParameters, @@ -37,7 +38,8 @@ public IdentityService( IAuthDbContext authDbContext, RSA encryptionKey, ECDsa signingKey, - IMapper mapper) + IMapper mapper, + IOptions securitySettingsOptions) { _tokenValidationParameters = tokenValidationParameters; _jweSettings = jweSettingsOptions.Value; @@ -46,6 +48,7 @@ public IdentityService( _encryptionKey = encryptionKey; _signingKey = signingKey; _mapper = mapper; + _securitySettings = securitySettingsOptions.Value; } public async Task Validate(string token, string refreshToken) @@ -57,9 +60,7 @@ public async Task Validate(string token, string refreshToken) return false; } - const string emailClaim = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"; - - var email = validatedToken.Claims.Single(y => y.Type.Equals(emailClaim)).Value; + var email = validatedToken.Claims.Single(y => y.Type.Equals(JwtRegisteredClaimNames.Email)).Value; var user = await _applicationDbContext.Users.FirstOrDefaultAsync(x => x.Username.Equals(email) @@ -115,10 +116,8 @@ public async Task RefreshTokenAsync(string token, string r { throw new AuthenticationException("Invalid token."); } - - const string emailClaim = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"; - var email = validatedToken.Claims.Single(y => y.Type.Equals(emailClaim)).Value; + var email = validatedToken.Claims.Single(y => y.Type.Equals(JwtRegisteredClaimNames.Email)).Value; var user = await _applicationDbContext.Users.FirstOrDefaultAsync(x => x.Username.Equals(email) @@ -196,7 +195,7 @@ public async Task RefreshTokenAsync(string token, string r .Include(x => x.Department) .FirstOrDefault(x => x.Email!.Equals(email)); - if (user is null || !user.PasswordHash.Equals(SecurityUtil.Hash(password))) + if (user is null || !user.PasswordHash.Equals(password.HashPasswordWith(user.PasswordSalt, _securitySettings.Pepper))) { throw new AuthenticationException("Username or password is invalid."); } @@ -255,8 +254,9 @@ public async Task ResetPassword(string token, string newPassword) { user.IsActivated = true; } - - user.PasswordHash = SecurityUtil.Hash(newPassword); + var salt = StringUtil.RandomSalt(); + user.PasswordSalt = salt; + user.PasswordHash = newPassword.HashPasswordWith(salt, newPassword); resetPasswordToken.IsInvalidated = true; await _applicationDbContext.SaveChangesAsync(CancellationToken.None); await _authDbContext.SaveChangesAsync(CancellationToken.None); From a445726ef4e2d7d693640b6a011a69eddec3dc36 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Tue, 13 Jun 2023 07:27:44 +0700 Subject: [PATCH 20/56] fix: something --- src/Application/Documents/Commands/ImportDocument.cs | 2 +- .../Identity/Authorization/RequiresRoleAttribute.cs | 4 ++-- src/Infrastructure/Identity/IdentityService.cs | 6 +++++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Application/Documents/Commands/ImportDocument.cs b/src/Application/Documents/Commands/ImportDocument.cs index 6cfab2f4..fb904034 100644 --- a/src/Application/Documents/Commands/ImportDocument.cs +++ b/src/Application/Documents/Commands/ImportDocument.cs @@ -69,7 +69,7 @@ public async Task Handle(Command request, CancellationToken cancell var log = new DocumentLog() { Object = entity, - Time = LocalDateTime.FromDateTime(DateTime.UtcNow), + Time = LocalDateTime.FromDateTime(DateTime.Now), User = importer, UserId = importer.Id, Action = DocumentLogMessages.Import.NewImport diff --git a/src/Infrastructure/Identity/Authorization/RequiresRoleAttribute.cs b/src/Infrastructure/Identity/Authorization/RequiresRoleAttribute.cs index 863a36d7..469e1e98 100644 --- a/src/Infrastructure/Identity/Authorization/RequiresRoleAttribute.cs +++ b/src/Infrastructure/Identity/Authorization/RequiresRoleAttribute.cs @@ -1,3 +1,4 @@ +using System.IdentityModel.Tokens.Jwt; using Application.Identity; using Infrastructure.Persistence; using Microsoft.AspNetCore.Mvc; @@ -20,8 +21,7 @@ public void OnAuthorization(AuthorizationFilterContext context) { var dbContext = context.HttpContext.RequestServices.GetRequiredService(); - const string emailClaim = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"; - var email = context.HttpContext.User.Claims.SingleOrDefault(y => y.Type.Equals(emailClaim))!.Value; + var email = context.HttpContext.User.Claims.SingleOrDefault(y => y.Type.Equals(JwtRegisteredClaimNames.Email))!.Value; var user = dbContext.Users.FirstOrDefault(x => x.Email!.Equals(email)); diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index 9b311700..f3c22be7 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -119,7 +119,9 @@ public async Task RefreshTokenAsync(string token, string r var email = validatedToken.Claims.Single(y => y.Type.Equals(JwtRegisteredClaimNames.Email)).Value; - var user = await _applicationDbContext.Users.FirstOrDefaultAsync(x => + var user = await _applicationDbContext.Users + .Include(x => x.Department) + .FirstOrDefaultAsync(x => x.Username.Equals(email) || x.Email!.Equals(email)); @@ -289,10 +291,12 @@ private SecurityToken CreateJweToken(User user) var utcNow = DateTime.UtcNow; var authClaims = new List { + new(JwtRegisteredClaimNames.NameId, user.Id.ToString()), new(JwtRegisteredClaimNames.Sub, user.Username), new(JwtRegisteredClaimNames.Email, user.Email!), new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new(JwtRegisteredClaimNames.Iat, utcNow.ToString(CultureInfo.InvariantCulture)), + new("departmentId", user.Department!.Id.ToString()), }; var publicEncryptionKey = new RsaSecurityKey(_encryptionKey.ExportParameters(false)) {KeyId = _jweSettings.EncryptionKeyId}; var privateSigningKey = new ECDsaSecurityKey(_signingKey) {KeyId = _jweSettings.SigningKeyId}; From f0fcff3c8ee9832e1635ec0900bd95994d0296dc Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Tue, 13 Jun 2023 09:15:10 +0700 Subject: [PATCH 21/56] add: logging to room, locker, folder creation and update --- src/Api/Controllers/FoldersController.cs | 12 ++++++++++++ src/Api/Controllers/LockersController.cs | 12 ++++++++++++ src/Api/Controllers/RoomsController.cs | 15 ++++++++++++++- .../Common/Messages/DocumentLogMessages.cs | 2 +- .../Common/Messages/FolderLogMessage.cs | 7 +++++++ .../Common/Messages/LockerLogMessage.cs | 7 +++++++ .../Common/Messages/RoomLogMessage.cs | 7 +++++++ src/Application/Folders/Commands/AddFolder.cs | 18 +++++++++++++++++- .../Folders/Commands/UpdateFolder.cs | 18 +++++++++++++++++- src/Application/Lockers/Commands/AddLocker.cs | 19 ++++++++++++++++++- .../Lockers/Commands/UpdateLocker.cs | 17 ++++++++++++++++- src/Application/Rooms/Commands/AddRoom.cs | 16 ++++++++++++++++ src/Application/Rooms/Commands/UpdateRoom.cs | 19 ++++++++++++++++++- .../JweAuthenticationHandler.cs | 4 ++++ .../Identity/IdentityService.cs | 1 + 15 files changed, 167 insertions(+), 7 deletions(-) create mode 100644 src/Application/Common/Messages/FolderLogMessage.cs create mode 100644 src/Application/Common/Messages/LockerLogMessage.cs create mode 100644 src/Application/Common/Messages/RoomLogMessage.cs diff --git a/src/Api/Controllers/FoldersController.cs b/src/Api/Controllers/FoldersController.cs index a08b36cc..977d196b 100644 --- a/src/Api/Controllers/FoldersController.cs +++ b/src/Api/Controllers/FoldersController.cs @@ -1,4 +1,5 @@ using Api.Controllers.Payload.Requests.Folders; +using Application.Common.Interfaces; using Application.Common.Models; using Application.Common.Models.Dtos.Physical; using Application.Folders.Commands; @@ -11,6 +12,13 @@ namespace Api.Controllers; public class FoldersController : ApiControllerBase { + private readonly ICurrentUserService _currentUserService; + + public FoldersController(ICurrentUserService currentUserService) + { + _currentUserService = currentUserService; + } + /// /// Get a folder by id /// @@ -71,8 +79,10 @@ public async Task>>> GetAllPaginate [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> AddFolder([FromBody] AddFolderRequest request) { + var performingUserId = _currentUserService.GetId(); var command = new AddFolder.Command() { + PerformingUserId = performingUserId, Name = request.Name, Description = request.Description, Capacity = request.Capacity, @@ -161,8 +171,10 @@ public async Task>> DisableFolder([FromRoute] Gui [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> Update([FromRoute] Guid folderId, [FromBody] UpdateFolderRequest request) { + var performingUserId = _currentUserService.GetId(); var command = new UpdateFolder.Command() { + PerformingUserId = performingUserId, FolderId = folderId, Name = request.Name, Description = request.Description, diff --git a/src/Api/Controllers/LockersController.cs b/src/Api/Controllers/LockersController.cs index 2445df95..2799e670 100644 --- a/src/Api/Controllers/LockersController.cs +++ b/src/Api/Controllers/LockersController.cs @@ -1,4 +1,5 @@ using Api.Controllers.Payload.Requests.Lockers; +using Application.Common.Interfaces; using Application.Common.Models; using Application.Common.Models.Dtos.Physical; using Application.Identity; @@ -11,6 +12,13 @@ namespace Api.Controllers; public class LockersController : ApiControllerBase { + private readonly ICurrentUserService _currentUserService; + + public LockersController(ICurrentUserService currentUserService) + { + _currentUserService = currentUserService; + } + /// /// Get a locker by id /// @@ -68,8 +76,10 @@ public async Task>>> GetAllPaginate [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> Add([FromBody] AddLockerRequest request) { + var performingUserId = _currentUserService.GetId(); var command = new AddLocker.Command() { + PerformingUserId = performingUserId, Name = request.Name, Description = request.Description, Capacity = request.Capacity, @@ -156,8 +166,10 @@ public async Task>> Disable([FromRoute] Guid lock [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> Update([FromRoute] Guid lockerId, [FromBody] UpdateLockerRequest request) { + var performingUserId = _currentUserService.GetId(); var command = new UpdateLocker.Command() { + PerformingUserId = performingUserId, LockerId = lockerId, Name = request.Name, Description = request.Description, diff --git a/src/Api/Controllers/RoomsController.cs b/src/Api/Controllers/RoomsController.cs index fa5198ee..53f111af 100644 --- a/src/Api/Controllers/RoomsController.cs +++ b/src/Api/Controllers/RoomsController.cs @@ -1,5 +1,6 @@ using Api.Controllers.Payload.Requests.Lockers; using Api.Controllers.Payload.Requests.Rooms; +using Application.Common.Interfaces; using Application.Common.Models; using Application.Common.Models.Dtos.Physical; using Application.Identity; @@ -12,6 +13,13 @@ namespace Api.Controllers; public class RoomsController : ApiControllerBase { + private readonly ICurrentUserService _currentUserService; + + public RoomsController(ICurrentUserService currentUserService) + { + _currentUserService = currentUserService; + } + /// /// Get a room by id /// @@ -54,10 +62,11 @@ public async Task>>> GetAllPaginated( var result = await Mediator.Send(query); return Ok(Result>.Succeed(result)); } - + /// /// Get empty containers in a room /// + /// /// Get empty containers paginated details /// A paginated list of EmptyLockerDto [RequiresRole(IdentityData.Roles.Staff)] @@ -93,8 +102,10 @@ public async Task>> GetEmptyContainer [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> AddRoom([FromBody] AddRoomRequest request) { + var performingUserId = _currentUserService.GetId(); var command = new AddRoom.Command() { + PerformingUserId = performingUserId, Name = request.Name, Description = request.Description, Capacity = request.Capacity, @@ -179,8 +190,10 @@ public async Task>> DisableRoom([FromRoute] Guid ro [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> Update([FromRoute] Guid roomId, [FromBody] UpdateRoomRequest request) { + var performingUserId = _currentUserService.GetId(); var command = new UpdateRoom.Command() { + PerformingUserId = performingUserId, RoomId = roomId, Name = request.Name, Description = request.Description, diff --git a/src/Application/Common/Messages/DocumentLogMessages.cs b/src/Application/Common/Messages/DocumentLogMessages.cs index acfd4f83..010e580b 100644 --- a/src/Application/Common/Messages/DocumentLogMessages.cs +++ b/src/Application/Common/Messages/DocumentLogMessages.cs @@ -4,6 +4,6 @@ public static class DocumentLogMessages { public static class Import { - public const string NewImport = "Created new document request"; + public const string NewImport = "Created new import request"; } } \ No newline at end of file diff --git a/src/Application/Common/Messages/FolderLogMessage.cs b/src/Application/Common/Messages/FolderLogMessage.cs new file mode 100644 index 00000000..b6922a83 --- /dev/null +++ b/src/Application/Common/Messages/FolderLogMessage.cs @@ -0,0 +1,7 @@ +namespace Application.Common.Messages; + +public static class FolderLogMessage +{ + public const string Add = "Added folder"; + public const string Update = "Updated folder"; +} \ No newline at end of file diff --git a/src/Application/Common/Messages/LockerLogMessage.cs b/src/Application/Common/Messages/LockerLogMessage.cs new file mode 100644 index 00000000..20d1018f --- /dev/null +++ b/src/Application/Common/Messages/LockerLogMessage.cs @@ -0,0 +1,7 @@ +namespace Application.Common.Messages; + +public static class LockerLogMessage +{ + public const string Add = "Added locker"; + public const string Update = "Updated locker"; +} \ No newline at end of file diff --git a/src/Application/Common/Messages/RoomLogMessage.cs b/src/Application/Common/Messages/RoomLogMessage.cs new file mode 100644 index 00000000..cd105a65 --- /dev/null +++ b/src/Application/Common/Messages/RoomLogMessage.cs @@ -0,0 +1,7 @@ +namespace Application.Common.Messages; + +public static class RoomLogMessage +{ + public const string Add = "Added room"; + public const string Update = "Updated room"; +} \ No newline at end of file diff --git a/src/Application/Folders/Commands/AddFolder.cs b/src/Application/Folders/Commands/AddFolder.cs index 4b7bd9b9..05a7bf34 100644 --- a/src/Application/Folders/Commands/AddFolder.cs +++ b/src/Application/Folders/Commands/AddFolder.cs @@ -1,12 +1,15 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities.Logging; using Domain.Entities.Physical; using Domain.Exceptions; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Folders.Commands; @@ -36,6 +39,7 @@ public Validator() public record Command : IRequest { + public Guid PerformingUserId { get; init; } public string Name { get; init; } = null!; public string? Description { get; init; } public int Capacity { get; init; } @@ -76,6 +80,7 @@ public async Task Handle(Command request, CancellationToken cancellat throw new ConflictException("Folder name already exists."); } + var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); var entity = new Folder { Name = request.Name.Trim(), @@ -83,11 +88,22 @@ public async Task Handle(Command request, CancellationToken cancellat NumberOfDocuments = 0, Capacity = request.Capacity, Locker = locker, - IsAvailable = true + IsAvailable = true, + Created = LocalDateTime.FromDateTime(DateTime.Now), + CreatedBy = performingUser!.Id, + }; + var log = new FolderLog() + { + User = performingUser, + UserId = performingUser.Id, + Object = entity, + Time = LocalDateTime.FromDateTime(DateTime.Now), + Action = FolderLogMessage.Add, }; var result = await _context.Folders.AddAsync(entity, cancellationToken); locker.NumberOfFolders += 1; _context.Lockers.Update(locker); + await _context.FolderLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } diff --git a/src/Application/Folders/Commands/UpdateFolder.cs b/src/Application/Folders/Commands/UpdateFolder.cs index edfcfe76..f5924992 100644 --- a/src/Application/Folders/Commands/UpdateFolder.cs +++ b/src/Application/Folders/Commands/UpdateFolder.cs @@ -1,10 +1,13 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities.Logging; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Folders.Commands; @@ -31,6 +34,7 @@ public Validator() public record Command : IRequest { + public Guid PerformingUserId { get; init; } public Guid FolderId { get; init; } public string Name { get; init; } = null!; public string? Description { get; init; } @@ -77,11 +81,23 @@ public async Task Handle(Command request, CancellationToken cancellat throw new ConflictException("New capacity cannot be less than current number of documents."); } + var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); folder.Name = request.Name; folder.Description = request.Description; folder.Capacity = request.Capacity; - + folder.LastModified = LocalDateTime.FromDateTime(DateTime.Now); + folder.LastModifiedBy = performingUser!.Id; + + var log = new FolderLog() + { + User = performingUser, + UserId = performingUser.Id, + Object = folder, + Time = LocalDateTime.FromDateTime(DateTime.Now), + Action = FolderLogMessage.Update, + }; var result = _context.Folders.Update(folder); + await _context.FolderLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } diff --git a/src/Application/Lockers/Commands/AddLocker.cs b/src/Application/Lockers/Commands/AddLocker.cs index 78cbb371..1f16bbe3 100644 --- a/src/Application/Lockers/Commands/AddLocker.cs +++ b/src/Application/Lockers/Commands/AddLocker.cs @@ -1,12 +1,16 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities.Logging; using Domain.Entities.Physical; using Domain.Exceptions; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; +using Org.BouncyCastle.Math.EC.Rfc8032; namespace Application.Lockers.Commands; @@ -35,6 +39,7 @@ public Validator() public record Command : IRequest { + public Guid PerformingUserId { get; init; } public string Name { get; init; } = null!; public string? Description { get; init; } public Guid RoomId { get; init; } @@ -76,6 +81,7 @@ public async Task Handle(Command request, CancellationToken cancellat throw new ConflictException("Locker name already exists."); } + var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); var entity = new Locker { Name = request.Name.Trim(), @@ -83,12 +89,23 @@ public async Task Handle(Command request, CancellationToken cancellat NumberOfFolders = 0, Capacity = request.Capacity, Room = room, - IsAvailable = true + IsAvailable = true, + Created = LocalDateTime.FromDateTime(DateTime.Now), + CreatedBy = performingUser!.Id, + }; + var log = new LockerLog() + { + User = performingUser, + UserId = performingUser.Id, + Object = entity, + Time = LocalDateTime.FromDateTime(DateTime.Now), + Action = LockerLogMessage.Add, }; var result = await _context.Lockers.AddAsync(entity, cancellationToken); room.NumberOfLockers += 1; _context.Rooms.Update(room); + await _context.LockerLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } diff --git a/src/Application/Lockers/Commands/UpdateLocker.cs b/src/Application/Lockers/Commands/UpdateLocker.cs index 20383ef9..d9a1a33e 100644 --- a/src/Application/Lockers/Commands/UpdateLocker.cs +++ b/src/Application/Lockers/Commands/UpdateLocker.cs @@ -1,12 +1,15 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities.Logging; using Domain.Entities.Physical; using Domain.Exceptions; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Lockers.Commands; @@ -31,6 +34,7 @@ public Validator() } public record Command : IRequest { + public Guid PerformingUserId { get; init; } public Guid LockerId { get; init; } public string Name { get; init; } = null!; public string? Description { get; init; } @@ -74,12 +78,23 @@ public async Task Handle(Command request, CancellationToken cancellat { throw new ConflictException("New capacity cannot be less than current number of folders."); } - + var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); locker.Name = request.Name; locker.Description = request.Description; locker.Capacity = request.Capacity; + locker.LastModified = LocalDateTime.FromDateTime(DateTime.Now); + locker.LastModifiedBy = performingUser!.Id; + var log = new LockerLog() + { + User = performingUser, + UserId = performingUser.Id, + Object = locker, + Time = LocalDateTime.FromDateTime(DateTime.Now), + Action = LockerLogMessage.Update, + }; var result = _context.Lockers.Update(locker); + await _context.LockerLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } diff --git a/src/Application/Rooms/Commands/AddRoom.cs b/src/Application/Rooms/Commands/AddRoom.cs index 13449b93..c22f9d0e 100644 --- a/src/Application/Rooms/Commands/AddRoom.cs +++ b/src/Application/Rooms/Commands/AddRoom.cs @@ -1,11 +1,14 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities.Logging; using Domain.Entities.Physical; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Rooms.Commands; @@ -41,6 +44,7 @@ private bool BeUnique(string name) public record Command : IRequest { + public Guid PerformingUserId { get; init; } public string Name { get; init; } = null!; public string? Description { get; init; } public int Capacity { get; init; } @@ -75,6 +79,7 @@ public async Task Handle(Command request, CancellationToken cancellatio throw new ConflictException("Room name already exists."); } + var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); var entity = new Room { Name = request.Name.Trim(), @@ -84,8 +89,19 @@ public async Task Handle(Command request, CancellationToken cancellatio Department = department, DepartmentId = request.DepartmentId, IsAvailable = true, + Created = LocalDateTime.FromDateTime(DateTime.Now), + CreatedBy = performingUser!.Id, + }; + var log = new RoomLog() + { + User = performingUser, + UserId = performingUser.Id, + Object = entity, + Time = LocalDateTime.FromDateTime(DateTime.Now), + Action = RoomLogMessage.Add, }; var result = await _context.Rooms.AddAsync(entity, cancellationToken); + await _context.RoomLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } diff --git a/src/Application/Rooms/Commands/UpdateRoom.cs b/src/Application/Rooms/Commands/UpdateRoom.cs index a4fbe702..ab720c52 100644 --- a/src/Application/Rooms/Commands/UpdateRoom.cs +++ b/src/Application/Rooms/Commands/UpdateRoom.cs @@ -1,11 +1,14 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities.Logging; using Domain.Entities.Physical; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Rooms.Commands; @@ -30,6 +33,7 @@ public Validator() } public record Command : IRequest { + public Guid PerformingUserId { get; init; } public Guid RoomId { get; init; } public string Name { get; init; } = null!; public string? Description { get; init; } @@ -74,6 +78,8 @@ public async Task Handle(Command request, CancellationToken cancellatio throw new ConflictException("New capacity cannot be less than current number of lockers."); } + var performingUser = await _context.Users + .FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); var updatedRoom = new Room { Id = room.Id, @@ -85,11 +91,22 @@ public async Task Handle(Command request, CancellationToken cancellatio Capacity = request.Capacity, NumberOfLockers = room.NumberOfLockers, IsAvailable = room.IsAvailable, - Lockers = room.Lockers + Lockers = room.Lockers, + LastModified = LocalDateTime.FromDateTime(DateTime.Now), + LastModifiedBy = performingUser!.Id, + }; + var log = new RoomLog() + { + User = performingUser, + UserId = performingUser.Id, + Object = updatedRoom, + Time = LocalDateTime.FromDateTime(DateTime.Now), + Action = RoomLogMessage.Update, }; _context.Rooms.Entry(room).State = EntityState.Detached; _context.Rooms.Entry(updatedRoom).State = EntityState.Modified; + await _context.RoomLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); diff --git a/src/Infrastructure/Identity/Authentication/JweAuthenticationHandler.cs b/src/Infrastructure/Identity/Authentication/JweAuthenticationHandler.cs index 795d1585..8c706291 100644 --- a/src/Infrastructure/Identity/Authentication/JweAuthenticationHandler.cs +++ b/src/Infrastructure/Identity/Authentication/JweAuthenticationHandler.cs @@ -40,6 +40,10 @@ protected override async Task HandleAuthenticateAsync() var claimsPrincipal = handler.ValidateToken(token, Options.TokenValidationParameters, out var validatedToken); + if (claimsPrincipal.Claims.Single(x => x.Type.Equals("isActive")).Value.Equals(false.ToString())) + { + return AuthenticateResult.Fail("User is not active."); + } Context.User = claimsPrincipal; return validatedToken is null diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index f3c22be7..f4b8e5b7 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -297,6 +297,7 @@ private SecurityToken CreateJweToken(User user) new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new(JwtRegisteredClaimNames.Iat, utcNow.ToString(CultureInfo.InvariantCulture)), new("departmentId", user.Department!.Id.ToString()), + new("isActive", user.IsActive.ToString()), }; var publicEncryptionKey = new RsaSecurityKey(_encryptionKey.ExportParameters(false)) {KeyId = _jweSettings.EncryptionKeyId}; var privateSigningKey = new ECDsaSecurityKey(_signingKey) {KeyId = _jweSettings.SigningKeyId}; From 4981de2d86a4c49139126717b3576998b9671d88 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Tue, 13 Jun 2023 09:44:27 +0700 Subject: [PATCH 22/56] add: logging to users and staffs --- src/Api/Controllers/StaffsController.cs | 14 +++++++++++ src/Api/Controllers/UsersController.cs | 14 +++++++++++ .../Common/Messages/UserLogMessages.cs | 15 +++++++++++ src/Application/Staffs/Commands/AddStaff.cs | 25 ++++++++++++++++++- .../Staffs/Commands/RemoveStaff.cs | 14 +++++++++++ .../Staffs/Commands/RemoveStaffFromRoom.cs | 14 +++++++++++ src/Application/Users/Commands/AddUser.cs | 16 +++++++++++- src/Application/Users/Commands/DisableUser.cs | 17 ++++++++++++- src/Application/Users/Commands/EnableUser.cs | 1 + src/Application/Users/Commands/UpdateUser.cs | 17 ++++++++++++- 10 files changed, 143 insertions(+), 4 deletions(-) create mode 100644 src/Application/Common/Messages/UserLogMessages.cs diff --git a/src/Api/Controllers/StaffsController.cs b/src/Api/Controllers/StaffsController.cs index ecd2b9c8..c8947d4e 100644 --- a/src/Api/Controllers/StaffsController.cs +++ b/src/Api/Controllers/StaffsController.cs @@ -1,4 +1,5 @@ using Api.Controllers.Payload.Requests.Staffs; +using Application.Common.Interfaces; using Application.Common.Models; using Application.Common.Models.Dtos.Physical; using Application.Identity; @@ -11,6 +12,13 @@ namespace Api.Controllers; public class StaffsController : ApiControllerBase { + private readonly ICurrentUserService _currentUserService; + + public StaffsController(ICurrentUserService currentUserService) + { + _currentUserService = currentUserService; + } + /// /// Get a staff by id /// @@ -84,8 +92,10 @@ public async Task>>> GetAllPaginated [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> Add([FromBody] AddStaffRequest request) { + var performingUserId = _currentUserService.GetId(); var command = new AddStaff.Command() { + PerformingUserId = performingUserId, RoomId = request.RoomId, UserId = request.UserId, }; @@ -106,8 +116,10 @@ public async Task>> Add([FromBody] AddStaffRequest public async Task>> RemoveFromRoom( [FromRoute] Guid staffId) { + var performingUserId = _currentUserService.GetId(); var command = new RemoveStaffFromRoom.Command() { + PerformingUserId = performingUserId, StaffId = staffId, }; var result = await Mediator.Send(command); @@ -126,8 +138,10 @@ public async Task>> RemoveFromRoom( public async Task>> Remove( [FromRoute] Guid staffId) { + var performingUserId = _currentUserService.GetId(); var command = new RemoveStaff.Command() { + PerformingUserId = performingUserId, StaffId = staffId }; diff --git a/src/Api/Controllers/UsersController.cs b/src/Api/Controllers/UsersController.cs index 5b8efe5f..095e9605 100644 --- a/src/Api/Controllers/UsersController.cs +++ b/src/Api/Controllers/UsersController.cs @@ -1,4 +1,5 @@ using Api.Controllers.Payload.Requests.Users; +using Application.Common.Interfaces; using Application.Common.Models; using Application.Identity; using Application.Users.Commands; @@ -10,6 +11,13 @@ namespace Api.Controllers; public class UsersController : ApiControllerBase { + private readonly ICurrentUserService _currentUserService; + + public UsersController(ICurrentUserService currentUserService) + { + _currentUserService = currentUserService; + } + /// /// Get a user by id /// @@ -68,8 +76,10 @@ public async Task>>> GetAllPaginated( [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> Add([FromBody] AddUserRequest request) { + var performingUserId = _currentUserService.GetId(); var command = new AddUser.Command() { + PerformingUserId = performingUserId, Username = request.Username, Email = request.Email, FirstName = request.FirstName, @@ -115,8 +125,10 @@ public async Task>> Enable([FromRoute] Guid userId) [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> Disable([FromRoute] Guid userId) { + var performingUserId = _currentUserService.GetId(); var command = new DisableUser.Command() { + PerformingUserId = performingUserId, UserId = userId, }; var result = await Mediator.Send(command); @@ -136,8 +148,10 @@ public async Task>> Disable([FromRoute] Guid userId [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> Update([FromRoute] Guid userId, [FromBody] UpdateUserRequest request) { + var performingUserId = _currentUserService.GetId(); var command = new UpdateUser.Command() { + PerformingUserId = performingUserId, UserId = userId, FirstName = request.FirstName, LastName = request.LastName, diff --git a/src/Application/Common/Messages/UserLogMessages.cs b/src/Application/Common/Messages/UserLogMessages.cs new file mode 100644 index 00000000..f205aa49 --- /dev/null +++ b/src/Application/Common/Messages/UserLogMessages.cs @@ -0,0 +1,15 @@ +namespace Application.Common.Messages; + +public static class UserLogMessages +{ + public const string Add = "Added user"; + public const string Update = "Updated user"; + public const string Disable = "Disabled user"; + + public static class Staff + { + public static string AddStaff(string roomId) => $"Assigned user to be staff of room {roomId}"; + public const string RemoveFromRoom = "Removed staff from room"; + public const string Remove = "Removed staff"; + } +} \ No newline at end of file diff --git a/src/Application/Staffs/Commands/AddStaff.cs b/src/Application/Staffs/Commands/AddStaff.cs index 0d78536f..cb381aae 100644 --- a/src/Application/Staffs/Commands/AddStaff.cs +++ b/src/Application/Staffs/Commands/AddStaff.cs @@ -1,10 +1,13 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities.Logging; using Domain.Entities.Physical; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Staffs.Commands; @@ -12,6 +15,7 @@ public class AddStaff { public record Command : IRequest { + public Guid PerformingUserId { get; init; } public Guid UserId { get; init; } public Guid? RoomId { get; init; } } @@ -46,6 +50,7 @@ public async Task Handle(Command request, CancellationToken cancellati .Include(x => x.Room) .Include(x => x.User) .FirstOrDefaultAsync(x => x.Id == user.Id, cancellationToken); + var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); if (existedStaff is not null) { if (existedStaff.Room is not null) @@ -54,7 +59,16 @@ public async Task Handle(Command request, CancellationToken cancellati } existedStaff.Room = room; + var log = new UserLog() + { + User = performingUser!, + UserId = performingUser!.Id, + Object = user, + Time = LocalDateTime.FromDateTime(DateTime.Now), + Action = UserLogMessages.Staff.AddStaff(room.Id.ToString()), + }; var result = _context.Staffs.Update(existedStaff); + await _context.UserLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } @@ -64,10 +78,19 @@ public async Task Handle(Command request, CancellationToken cancellati { Id = user.Id, User = user, - Room = room + Room = room, + }; + var log = new UserLog() + { + User = performingUser!, + UserId = performingUser!.Id, + Object = user, + Time = LocalDateTime.FromDateTime(DateTime.Now), + Action = UserLogMessages.Staff.AddStaff(room.Id.ToString()), }; var result = await _context.Staffs.AddAsync(staff, cancellationToken); + await _context.UserLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } diff --git a/src/Application/Staffs/Commands/RemoveStaff.cs b/src/Application/Staffs/Commands/RemoveStaff.cs index d550094b..d3d42d3c 100644 --- a/src/Application/Staffs/Commands/RemoveStaff.cs +++ b/src/Application/Staffs/Commands/RemoveStaff.cs @@ -1,9 +1,12 @@ using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities.Logging; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Staffs.Commands; @@ -11,6 +14,7 @@ public class RemoveStaff { public record Command : IRequest { + public Guid PerformingUserId { get; init; } public Guid StaffId { get; init; } } @@ -37,7 +41,17 @@ public async Task Handle(Command request, CancellationToken cancellati throw new KeyNotFoundException("Staff does not exist."); } + var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); + var log = new UserLog() + { + User = performingUser!, + UserId = performingUser!.Id, + Object = staff.User, + Time = LocalDateTime.FromDateTime(DateTime.Now), + Action = UserLogMessages.Staff.Remove, + }; var result = _context.Staffs.Remove(staff); + await _context.UserLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } diff --git a/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs b/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs index b4e8be49..197acc8e 100644 --- a/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs +++ b/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs @@ -1,9 +1,12 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities.Logging; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Staffs.Commands; @@ -11,6 +14,7 @@ public class RemoveStaffFromRoom { public record Command : IRequest { + public Guid PerformingUserId { get; init; } public Guid StaffId { get; init; } } @@ -45,7 +49,17 @@ public async Task Handle(Command request, CancellationToken cancellati staff.Room.Staff = null; _context.Rooms.Update(staff.Room!); staff.Room = null; + var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); + var log = new UserLog() + { + User = performingUser!, + UserId = performingUser!.Id, + Object = staff.User, + Time = LocalDateTime.FromDateTime(DateTime.Now), + Action = UserLogMessages.Staff.RemoveFromRoom, + }; var result = _context.Staffs.Update(staff); + await _context.UserLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); diff --git a/src/Application/Users/Commands/AddUser.cs b/src/Application/Users/Commands/AddUser.cs index 2ccef831..5d3f63c5 100644 --- a/src/Application/Users/Commands/AddUser.cs +++ b/src/Application/Users/Commands/AddUser.cs @@ -1,10 +1,12 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Helpers; using Application.Identity; using Application.Users.Queries; using AutoMapper; using Domain.Entities; +using Domain.Entities.Logging; using Domain.Events; using FluentValidation; using MediatR; @@ -53,6 +55,7 @@ private static bool BeNotAdmin(string role) public record Command : IRequest { + public Guid PerformingUserId { get; init; } public string Username { get; init; } = null!; public string Email { get; init; } = null!; public string? FirstName { get; init; } @@ -96,6 +99,7 @@ public async Task Handle(Command request, CancellationToken cancellatio var password = StringUtil.RandomPassword(); var salt = StringUtil.RandomSalt(); + var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); var entity = new User { Username = request.Username, @@ -109,10 +113,20 @@ public async Task Handle(Command request, CancellationToken cancellatio Position = request.Position, IsActive = true, IsActivated = false, - Created = LocalDateTime.FromDateTime(DateTime.UtcNow) + Created = LocalDateTime.FromDateTime(DateTime.Now), + CreatedBy = performingUser!.Id, + }; + var log = new UserLog() + { + User = performingUser, + UserId = performingUser.Id, + Object = entity, + Time = LocalDateTime.FromDateTime(DateTime.Now), + Action = UserLogMessages.Add, }; entity.AddDomainEvent(new UserCreatedEvent(entity, password)); var result = await _context.Users.AddAsync(entity, cancellationToken); + await _context.UserLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } diff --git a/src/Application/Users/Commands/DisableUser.cs b/src/Application/Users/Commands/DisableUser.cs index dafb4b31..c84113b1 100644 --- a/src/Application/Users/Commands/DisableUser.cs +++ b/src/Application/Users/Commands/DisableUser.cs @@ -1,9 +1,12 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Users.Queries; using AutoMapper; +using Domain.Entities.Logging; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Users.Commands; @@ -11,6 +14,7 @@ public class DisableUser { public record Command : IRequest { + public Guid PerformingUserId { get; init; } public Guid UserId { get; init; } } @@ -37,9 +41,20 @@ public async Task Handle(Command request, CancellationToken cancellatio throw new ConflictException("User has already been disabled."); } + var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); user.IsActive = false; - + user.LastModified = LocalDateTime.FromDateTime(DateTime.Now); + user.LastModifiedBy = performingUser!.Id; + var log = new UserLog() + { + User = performingUser, + UserId = performingUser.Id, + Object = user, + Time = LocalDateTime.FromDateTime(DateTime.Now), + Action = UserLogMessages.Disable, + }; var result = _context.Users.Update(user); + await _context.UserLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } diff --git a/src/Application/Users/Commands/EnableUser.cs b/src/Application/Users/Commands/EnableUser.cs index 7da8ab48..38609da8 100644 --- a/src/Application/Users/Commands/EnableUser.cs +++ b/src/Application/Users/Commands/EnableUser.cs @@ -7,6 +7,7 @@ public class EnableUser { public record Command : IRequest { + public Guid PerformingUserId { get; init; } public Guid UserId { get; init; } } } \ No newline at end of file diff --git a/src/Application/Users/Commands/UpdateUser.cs b/src/Application/Users/Commands/UpdateUser.cs index 00b27531..c13836d9 100644 --- a/src/Application/Users/Commands/UpdateUser.cs +++ b/src/Application/Users/Commands/UpdateUser.cs @@ -1,9 +1,12 @@ using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Users.Queries; using AutoMapper; +using Domain.Entities.Logging; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Users.Commands; @@ -27,6 +30,7 @@ public Validator() } public record Command : IRequest { + public Guid PerformingUserId { get; init; } public Guid UserId { get; init; } public string? FirstName { get; init; } public string? LastName { get; init; } @@ -54,11 +58,22 @@ public async Task Handle(Command request, CancellationToken cancellatio throw new KeyNotFoundException("User does not exist."); } + var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); user.FirstName = request.FirstName; user.LastName = request.LastName; user.Position = request.Position; - + user.LastModified = LocalDateTime.FromDateTime(DateTime.Now); + user.LastModifiedBy = performingUser!.Id; + var log = new UserLog() + { + User = performingUser, + UserId = performingUser.Id, + Object = user, + Time = LocalDateTime.FromDateTime(DateTime.Now), + Action = UserLogMessages.Update, + }; var result = _context.Users.Update(user); + await _context.UserLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } From 1816244aea6a4e640b280d0f28b137b077d18b3f Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Tue, 13 Jun 2023 11:13:05 +0700 Subject: [PATCH 23/56] update: restore import endpoint --- src/Api/Controllers/DocumentsController.cs | 34 +++++++- .../Documents/ImportDocumentRequest.cs | 8 ++ .../Documents/RequestImportDocumentRequest.cs | 20 +++++ .../Common/Messages/DocumentLogMessages.cs | 3 +- .../Documents/Commands/ImportDocument.cs | 35 +++++--- .../Commands/RequestImportDocument.cs | 85 +++++++++++++++++++ 6 files changed, 171 insertions(+), 14 deletions(-) create mode 100644 src/Api/Controllers/Payload/Requests/Documents/RequestImportDocumentRequest.cs create mode 100644 src/Application/Documents/Commands/RequestImportDocument.cs diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index 5175544f..9b7931ca 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -85,7 +85,7 @@ public async Task>>> GetAllDocumentTypes /// /// Import document details /// A DocumentDto of the imported document - [RequiresRole(IdentityData.Roles.Employee)] + [RequiresRole(IdentityData.Roles.Staff)] [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -94,13 +94,41 @@ public async Task>>> GetAllDocumentTypes [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> Import([FromBody] ImportDocumentRequest request) { - var userId = _currentUserService.GetId(); + var performingUserId = _currentUserService.GetId(); var command = new ImportDocument.Command() + { + PerformingUserId = performingUserId, + Title = request.Title, + Description = request.Description, + DocumentType = request.DocumentType, + FolderId = request.FolderId, + ImporterId = request.ImporterId, + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } + + /// + /// Request to import a document + /// + /// Import document request details + /// A DocumentDto of the imported document + [RequiresRole(IdentityData.Roles.Employee)] + [HttpPost("request")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> RequestImport([FromBody] RequestImportDocumentRequest request) + { + var performingUserId = _currentUserService.GetId(); + var command = new RequestImportDocument.Command() { Title = request.Title, Description = request.Description, DocumentType = request.DocumentType, - ImporterId = userId, + IssuerId = performingUserId, }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); diff --git a/src/Api/Controllers/Payload/Requests/Documents/ImportDocumentRequest.cs b/src/Api/Controllers/Payload/Requests/Documents/ImportDocumentRequest.cs index 7b1b1cd8..0bbb2723 100644 --- a/src/Api/Controllers/Payload/Requests/Documents/ImportDocumentRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Documents/ImportDocumentRequest.cs @@ -17,4 +17,12 @@ public class ImportDocumentRequest /// Document type of the document to be imported /// public string DocumentType { get; set; } = null!; + /// + /// Id of the importer + /// + public Guid ImporterId { get; set; } + /// + /// Id of the folder that this document will be in + /// + public Guid FolderId { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/RequestImportDocumentRequest.cs b/src/Api/Controllers/Payload/Requests/Documents/RequestImportDocumentRequest.cs new file mode 100644 index 00000000..4c8fb941 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Documents/RequestImportDocumentRequest.cs @@ -0,0 +1,20 @@ +namespace Api.Controllers.Payload.Requests.Documents; + +/// +/// Request details to import a document +/// +public class RequestImportDocumentRequest +{ + /// + /// Title of the document to be imported + /// + public string Title { get; set; } = null!; + /// + /// Description of the document to be imported + /// + public string? Description { get; set; } + /// + /// Document type of the document to be imported + /// + public string DocumentType { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Application/Common/Messages/DocumentLogMessages.cs b/src/Application/Common/Messages/DocumentLogMessages.cs index 010e580b..b6a62cae 100644 --- a/src/Application/Common/Messages/DocumentLogMessages.cs +++ b/src/Application/Common/Messages/DocumentLogMessages.cs @@ -4,6 +4,7 @@ public static class DocumentLogMessages { public static class Import { - public const string NewImport = "Created new import request"; + public const string NewImport = "Imported new document"; + public const string NewImportRequest = "Created new import request"; } } \ No newline at end of file diff --git a/src/Application/Documents/Commands/ImportDocument.cs b/src/Application/Documents/Commands/ImportDocument.cs index fb904034..6f1d441e 100644 --- a/src/Application/Documents/Commands/ImportDocument.cs +++ b/src/Application/Documents/Commands/ImportDocument.cs @@ -8,7 +8,6 @@ using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; using NodaTime; namespace Application.Documents.Commands; @@ -17,10 +16,12 @@ public class ImportDocument { public record Command : IRequest { + public Guid PerformingUserId { get; init; } public string Title { get; init; } = null!; public string? Description { get; init; } public string DocumentType { get; init; } = null!; public Guid ImporterId { get; init; } + public Guid FolderId { get; init; } } public class CommandHandler : IRequestHandler @@ -28,13 +29,10 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; - private readonly ILogger _logger; - - public CommandHandler(IApplicationDbContext context, IMapper mapper, ILogger logger) + public CommandHandler(IApplicationDbContext context, IMapper mapper) { _context = context; _mapper = mapper; - _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -44,7 +42,7 @@ public async Task Handle(Command request, CancellationToken cancell .FirstOrDefaultAsync(x => x.Id == request.ImporterId, cancellationToken); if (importer is null) { - throw new UnauthorizedAccessException(); + throw new KeyNotFoundException("User does not exist."); } var document = _context.Documents.FirstOrDefault(x => @@ -56,6 +54,19 @@ public async Task Handle(Command request, CancellationToken cancell throw new ConflictException($"Document title already exists for user {importer.LastName}."); } + var folder = await _context.Folders + .FirstOrDefaultAsync(x => x.Id == request.FolderId, cancellationToken); + if (folder is null) + { + throw new KeyNotFoundException("Folder does not exist."); + } + + if (folder.Capacity == folder.NumberOfDocuments) + { + throw new ConflictException("This folder cannot accept more documents."); + } + + var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); var entity = new Document() { Title = request.Title.Trim(), @@ -63,19 +74,23 @@ public async Task Handle(Command request, CancellationToken cancell DocumentType = request.DocumentType.Trim(), Importer = importer, Department = importer.Department, + Folder = folder, Status = DocumentStatus.Issued, + Created = LocalDateTime.FromDateTime(DateTime.Now), + CreatedBy = performingUser!.Id, }; - var log = new DocumentLog() { + User = performingUser, + UserId = performingUser.Id, Object = entity, Time = LocalDateTime.FromDateTime(DateTime.Now), - User = importer, - UserId = importer.Id, - Action = DocumentLogMessages.Import.NewImport + Action = DocumentLogMessages.Import.NewImport, }; var result = await _context.Documents.AddAsync(entity, cancellationToken); + folder.NumberOfDocuments += 1; + _context.Folders.Update(folder); await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); diff --git a/src/Application/Documents/Commands/RequestImportDocument.cs b/src/Application/Documents/Commands/RequestImportDocument.cs new file mode 100644 index 00000000..acc3df27 --- /dev/null +++ b/src/Application/Documents/Commands/RequestImportDocument.cs @@ -0,0 +1,85 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Messages; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities.Logging; +using Domain.Entities.Physical; +using Domain.Statuses; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using NodaTime; + +namespace Application.Documents.Commands; + +public class RequestImportDocument +{ + public record Command : IRequest + { + public string Title { get; init; } = null!; + public string? Description { get; init; } + public string DocumentType { get; init; } = null!; + public Guid IssuerId { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + private readonly ILogger _logger; + + public CommandHandler(IApplicationDbContext context, IMapper mapper, ILogger logger) + { + _context = context; + _mapper = mapper; + _logger = logger; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var issuer = await _context.Users + .Include(x => x.Department) + .FirstOrDefaultAsync(x => x.Id == request.IssuerId, cancellationToken); + if (issuer is null) + { + throw new UnauthorizedAccessException(); + } + + var document = _context.Documents.FirstOrDefault(x => + x.Title.Trim().ToLower().Equals(request.Title.Trim().ToLower()) + && x.Importer != null + && x.Importer.Id == request.IssuerId); + if (document is not null) + { + throw new ConflictException($"Document title already exists for user {issuer.LastName}."); + } + + var entity = new Document() + { + Title = request.Title.Trim(), + Description = request.Description?.Trim(), + DocumentType = request.DocumentType.Trim(), + Importer = issuer, + Department = issuer.Department, + Status = DocumentStatus.Issued, + Created = LocalDateTime.FromDateTime(DateTime.Now), + CreatedBy = issuer.Id, + }; + var log = new DocumentLog() + { + Object = entity, + Time = LocalDateTime.FromDateTime(DateTime.Now), + User = issuer, + UserId = issuer.Id, + Action = DocumentLogMessages.Import.NewImportRequest + }; + + var result = await _context.Documents.AddAsync(entity, cancellationToken); + await _context.DocumentLogs.AddAsync(log, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file From 65fdc2e04345b177e55f49a80dce225a82ae1bff Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Tue, 13 Jun 2023 22:39:31 +0700 Subject: [PATCH 24/56] Update/import (#239) * update: add audit and private status to document when create new import request * add: get all issued documents * add: checkin endpoint * add: approve and reject endpoint for document * finish: almost everything * reason * rename endpoint --- src/Api/Controllers/DocumentsController.cs | 193 +++- .../Documents/ApproveImportRequest.cs | 6 + .../AssignDocumentToFolderRequest.cs | 6 + ...cumentsForStaffPaginatedQueryParameters.cs | 21 + .../GetAllIssuedPaginatedQueryParameters.cs | 6 + .../Requests/Documents/RejectImportRequest.cs | 6 + .../Documents/RequestImportDocumentRequest.cs | 1 + .../Common/Messages/DocumentLogMessages.cs | 4 + .../Common/Messages/FolderLogMessage.cs | 1 + .../Common/Messages/RequestLogMessages.cs | 7 + .../Dtos/ImportDocument/IssuedDocumentDto.cs | 25 + .../Models/Dtos/ImportDocument/IssuerDto.cs | 17 + .../Common/Models/Dtos/ReasonDto.cs | 11 + .../Documents/Commands/ApproveDocument.cs | 78 ++ .../Documents/Commands/AssignDocument.cs | 90 ++ .../Documents/Commands/CheckinDocument.cs | 75 ++ .../Documents/Commands/ImportDocument.cs | 5 +- .../Documents/Commands/RejectDocument.cs | 75 ++ .../Commands/RequestImportDocument.cs | 18 +- .../Documents/Commands/UpdateDocument.cs | 1 + .../Queries/GetAllIssuedDocumentsPaginated.cs | 74 ++ .../Documents/Queries/GetDocumentReason.cs | 43 + src/Application/Staffs/Commands/AddStaff.cs | 9 +- src/Domain/Entities/Logging/RequestLog.cs | 1 + ...0230613140433_RequestLogReason.Designer.cs | 1019 +++++++++++++++++ .../20230613140433_RequestLogReason.cs | 29 + .../ApplicationDbContextModelSnapshot.cs | 4 + 27 files changed, 1812 insertions(+), 13 deletions(-) create mode 100644 src/Api/Controllers/Payload/Requests/Documents/ApproveImportRequest.cs create mode 100644 src/Api/Controllers/Payload/Requests/Documents/AssignDocumentToFolderRequest.cs create mode 100644 src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForStaffPaginatedQueryParameters.cs create mode 100644 src/Api/Controllers/Payload/Requests/Documents/GetAllIssuedPaginatedQueryParameters.cs create mode 100644 src/Api/Controllers/Payload/Requests/Documents/RejectImportRequest.cs create mode 100644 src/Application/Common/Messages/RequestLogMessages.cs create mode 100644 src/Application/Common/Models/Dtos/ImportDocument/IssuedDocumentDto.cs create mode 100644 src/Application/Common/Models/Dtos/ImportDocument/IssuerDto.cs create mode 100644 src/Application/Common/Models/Dtos/ReasonDto.cs create mode 100644 src/Application/Documents/Commands/ApproveDocument.cs create mode 100644 src/Application/Documents/Commands/AssignDocument.cs create mode 100644 src/Application/Documents/Commands/CheckinDocument.cs create mode 100644 src/Application/Documents/Commands/RejectDocument.cs create mode 100644 src/Application/Documents/Queries/GetAllIssuedDocumentsPaginated.cs create mode 100644 src/Application/Documents/Queries/GetDocumentReason.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230613140433_RequestLogReason.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230613140433_RequestLogReason.cs diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index 9b7931ca..2a6b6036 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -1,10 +1,13 @@ using Api.Controllers.Payload.Requests.Documents; using Application.Common.Interfaces; using Application.Common.Models; +using Application.Common.Models.Dtos; +using Application.Common.Models.Dtos.ImportDocument; using Application.Common.Models.Dtos.Physical; using Application.Documents.Commands; using Application.Documents.Queries; using Application.Identity; +using Domain.Enums; using Infrastructure.Identity.Authorization; using Microsoft.AspNetCore.Mvc; @@ -43,12 +46,45 @@ public async Task>> GetById([FromRoute] Guid do /// /// Get all documents query parameters /// A paginated list of DocumentDto + [RequiresRole(IdentityData.Roles.Staff)] + [HttpGet("issued")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>>> GetAllIssuedPaginated( + [FromQuery] GetAllIssuedPaginatedQueryParameters queryParameters) + { + var departmentId = _currentUserService.GetCurrentDepartmentForStaff(); + if (departmentId is null) + { + return Result>.Fail(new Exception("Staff does not have a room")); + } + var query = new GetAllIssuedDocumentsPaginated.Query() + { + DepartmentId = departmentId.Value, + SearchTerm = queryParameters.SearchTerm, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } + + /// + /// Get all documents paginated + /// + /// Get all documents query parameters + /// A paginated list of DocumentDto + [RequiresRole(IdentityData.Roles.Admin)] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>>> GetAllPaginated( + public async Task>>> GetAllForAdminPaginated( [FromQuery] GetAllDocumentsPaginatedQueryParameters queryParameters) { var query = new GetAllDocumentsPaginated.Query() @@ -66,6 +102,36 @@ public async Task>>> GetAllPagina return Ok(Result>.Succeed(result)); } + /// + /// Get all documents for staff paginated + /// + /// Get all documents for staff query parameters + /// A paginated list of DocumentDto + [RequiresRole(IdentityData.Roles.Staff)] + [HttpGet("staff")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>>> GetAllForStaffPaginated( + [FromQuery] GetAllDocumentsForStaffPaginatedQueryParameters queryParameters) + { + var roomId = _currentUserService.GetCurrentRoomForStaff(); + var query = new GetAllDocumentsPaginated.Query() + { + RoomId = roomId, + LockerId = queryParameters.LockerId, + FolderId = queryParameters.FolderId, + SearchTerm = queryParameters.SearchTerm, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } + /// /// Get all document types /// @@ -120,7 +186,7 @@ public async Task>> Import([FromBody] ImportDoc [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> RequestImport([FromBody] RequestImportDocumentRequest request) + public async Task>> RequestImport([FromBody] RequestImportDocumentRequest request) { var performingUserId = _currentUserService.GetId(); var command = new RequestImportDocument.Command() @@ -128,9 +194,35 @@ public async Task>> RequestImport([FromBody] Re Title = request.Title, Description = request.Description, DocumentType = request.DocumentType, + IsPrivate = request.IsPrivate, IssuerId = performingUserId, }; var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } + + /// + /// Checkin a document + /// + /// + /// A DocumentDto of the imported document + [RequiresRole(IdentityData.Roles.Staff)] + [HttpPost("checkin{documentId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> Checkin( + [FromRoute] Guid documentId) + { + var performingUserId = _currentUserService.GetId(); + var command = new CheckinDocument.Command() + { + PerformingUserId = performingUserId, + DocumentId = documentId, + }; + var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } @@ -177,4 +269,101 @@ public async Task>> Delete([FromRoute] Guid doc var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); } + + /// + /// Approve a document request + /// + /// Id of the document to be approved + /// + /// A DocumentDto of the approved document + [HttpPost("approve/{documentId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> Approve( + [FromRoute] Guid documentId, + [FromBody] ApproveImportRequest request) + { + var performingUserId = _currentUserService.GetId(); + var query = new ApproveDocument.Command() + { + PerformingUserId = performingUserId, + DocumentId = documentId, + Reason = request.Reason, + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } + + /// + /// Reject a document request + /// + /// Id of the document to be rejected + /// + /// A DocumentDto of the rejected document + [HttpPost("reject/{documentId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> Reject( + [FromRoute] Guid documentId, + [FromBody] RejectImportRequest request) + { + var performingUserId = _currentUserService.GetId(); + var query = new RejectDocument.Command() + { + PerformingUserId = performingUserId, + DocumentId = documentId, + Reason = request.Reason, + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } + + /// + /// Get a document request reason + /// + /// Id of the document to be rejected + /// A DocumentDto of the rejected document + [RequiresRole(IdentityData.Roles.Staff)] + [HttpPost("reason/{documentId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> Reason( + [FromRoute] Guid documentId) + { + var query = new GetDocumentReason.Query() + { + DocumentId = documentId, + Type = RequestType.Import, + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } + + /// + /// Assign a document to + /// + /// Id of the document to be rejected + /// + /// A DocumentDto of the rejected document + [HttpPost("{documentId:guid}/assign")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> Assign( + [FromRoute] Guid documentId, + [FromBody] AssignDocumentToFolderRequest request) + { + var performingUserId = _currentUserService.GetId(); + var query = new AssignDocument.Command() + { + PerformingUserId = performingUserId, + DocumentId = documentId, + FolderId = request.FolderId, + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/ApproveImportRequest.cs b/src/Api/Controllers/Payload/Requests/Documents/ApproveImportRequest.cs new file mode 100644 index 00000000..d617bb8b --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Documents/ApproveImportRequest.cs @@ -0,0 +1,6 @@ +namespace Api.Controllers.Payload.Requests.Documents; + +public class ApproveImportRequest +{ + public string Reason { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/AssignDocumentToFolderRequest.cs b/src/Api/Controllers/Payload/Requests/Documents/AssignDocumentToFolderRequest.cs new file mode 100644 index 00000000..bcca52f5 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Documents/AssignDocumentToFolderRequest.cs @@ -0,0 +1,6 @@ +namespace Api.Controllers.Payload.Requests.Documents; + +public class AssignDocumentToFolderRequest +{ + public Guid FolderId { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForStaffPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForStaffPaginatedQueryParameters.cs new file mode 100644 index 00000000..7860021d --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForStaffPaginatedQueryParameters.cs @@ -0,0 +1,21 @@ +namespace Api.Controllers.Payload.Requests.Documents; + +public class GetAllDocumentsForStaffPaginatedQueryParameters : PaginatedQueryParameters +{ + /// + /// Id of the room to find documents in + /// + public Guid? RoomId { get; set; } + /// + /// Id of the locker to find documents in + /// + public Guid? LockerId { get; set; } + /// + /// Id of the folder to find documents in + /// + public Guid? FolderId { get; set; } + /// + /// Search term + /// + public string? SearchTerm { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/GetAllIssuedPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Documents/GetAllIssuedPaginatedQueryParameters.cs new file mode 100644 index 00000000..ed060421 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Documents/GetAllIssuedPaginatedQueryParameters.cs @@ -0,0 +1,6 @@ +namespace Api.Controllers.Payload.Requests.Documents; + +public class GetAllIssuedPaginatedQueryParameters : PaginatedQueryParameters +{ + public string? SearchTerm { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/RejectImportRequest.cs b/src/Api/Controllers/Payload/Requests/Documents/RejectImportRequest.cs new file mode 100644 index 00000000..1e9c4b80 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Documents/RejectImportRequest.cs @@ -0,0 +1,6 @@ +namespace Api.Controllers.Payload.Requests.Documents; + +public class RejectImportRequest +{ + public string Reason { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/RequestImportDocumentRequest.cs b/src/Api/Controllers/Payload/Requests/Documents/RequestImportDocumentRequest.cs index 4c8fb941..ecdc79a5 100644 --- a/src/Api/Controllers/Payload/Requests/Documents/RequestImportDocumentRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Documents/RequestImportDocumentRequest.cs @@ -17,4 +17,5 @@ public class RequestImportDocumentRequest /// Document type of the document to be imported /// public string DocumentType { get; set; } = null!; + public bool IsPrivate { get; set; } } \ No newline at end of file diff --git a/src/Application/Common/Messages/DocumentLogMessages.cs b/src/Application/Common/Messages/DocumentLogMessages.cs index b6a62cae..5fb37c1f 100644 --- a/src/Application/Common/Messages/DocumentLogMessages.cs +++ b/src/Application/Common/Messages/DocumentLogMessages.cs @@ -6,5 +6,9 @@ public static class Import { public const string NewImport = "Imported new document"; public const string NewImportRequest = "Created new import request"; + public const string Checkin = "Checked in document"; + public const string Approve = "Approved import request"; + public const string Reject = "Rejected import request"; + public const string Assign = "Assigned to a folder"; } } \ No newline at end of file diff --git a/src/Application/Common/Messages/FolderLogMessage.cs b/src/Application/Common/Messages/FolderLogMessage.cs index b6922a83..97f97818 100644 --- a/src/Application/Common/Messages/FolderLogMessage.cs +++ b/src/Application/Common/Messages/FolderLogMessage.cs @@ -4,4 +4,5 @@ public static class FolderLogMessage { public const string Add = "Added folder"; public const string Update = "Updated folder"; + public const string AssignDocument = "Assigned document to folder"; } \ No newline at end of file diff --git a/src/Application/Common/Messages/RequestLogMessages.cs b/src/Application/Common/Messages/RequestLogMessages.cs new file mode 100644 index 00000000..ae9bab77 --- /dev/null +++ b/src/Application/Common/Messages/RequestLogMessages.cs @@ -0,0 +1,7 @@ +namespace Application.Common.Messages; + +public static class RequestLogMessages +{ + public const string ApproveImport = "Approved import request"; + public const string RejectImport = "Rejected import request"; +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/ImportDocument/IssuedDocumentDto.cs b/src/Application/Common/Models/Dtos/ImportDocument/IssuedDocumentDto.cs new file mode 100644 index 00000000..1c573d00 --- /dev/null +++ b/src/Application/Common/Models/Dtos/ImportDocument/IssuedDocumentDto.cs @@ -0,0 +1,25 @@ +using Application.Common.Mappings; +using AutoMapper; +using Domain.Entities.Physical; + +namespace Application.Common.Models.Dtos.ImportDocument; + +public class IssuedDocumentDto : IMapFrom +{ + public Guid Id { get; set; } + public string Title { get; set; } = null!; + public string? Description { get; set; } + public string DocumentType { get; set; } = null!; + public IssuerDto? Issuer { get; set; } + public string Status { get; set; } = null!; + public bool IsPrivate { get; set; } + + public void Mapping(Profile profile) + { + profile.CreateMap() + .ForMember(dest => dest.Status, + opt => opt.MapFrom(src => src.Status.ToString())) + .ForMember(dest => dest.Issuer, + opt => opt.MapFrom(x => x.Importer)); + } +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/ImportDocument/IssuerDto.cs b/src/Application/Common/Models/Dtos/ImportDocument/IssuerDto.cs new file mode 100644 index 00000000..3818dc77 --- /dev/null +++ b/src/Application/Common/Models/Dtos/ImportDocument/IssuerDto.cs @@ -0,0 +1,17 @@ +using Application.Common.Mappings; +using Domain.Entities; + +namespace Application.Common.Models.Dtos.ImportDocument; + +public class IssuerDto : IMapFrom +{ + public Guid Id { get; set; } + public string Username { get; set; } + public string Email { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string Role { get; set; } + public string Position { get; set; } + public bool IsActive { get; set; } + public bool IsActivated { get; set; } +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/ReasonDto.cs b/src/Application/Common/Models/Dtos/ReasonDto.cs new file mode 100644 index 00000000..8e82c755 --- /dev/null +++ b/src/Application/Common/Models/Dtos/ReasonDto.cs @@ -0,0 +1,11 @@ +using Application.Common.Mappings; +using Domain.Entities.Logging; +using Domain.Enums; + +namespace Application.Common.Models.Dtos; + +public class ReasonDto : IMapFrom +{ + public RequestType Type { get; set; } + public string Reason { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Application/Documents/Commands/ApproveDocument.cs b/src/Application/Documents/Commands/ApproveDocument.cs new file mode 100644 index 00000000..15bb65cd --- /dev/null +++ b/src/Application/Documents/Commands/ApproveDocument.cs @@ -0,0 +1,78 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Messages; +using Application.Common.Models.Dtos.ImportDocument; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities.Logging; +using Domain.Statuses; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using NodaTime; + +namespace Application.Documents.Commands; + +public class ApproveDocument +{ + public record Command : IRequest + { + public Guid PerformingUserId { get; init; } + public Guid DocumentId { get; init; } + public string Reason { get; init; } = null!; + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var document = await _context.Documents + .Include(x => x.Department) + .FirstOrDefaultAsync(x => + x.Id == request.DocumentId, cancellationToken); + if (document is null) + { + throw new ConflictException("Document does not exist."); + } + + if (document.Status is not DocumentStatus.Issued) + { + throw new ConflictException("Request cannot be approved."); + } + + document.Status = DocumentStatus.Approved; + var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); + var log = new DocumentLog() + { + Object = document, + Time = LocalDateTime.FromDateTime(DateTime.Now), + User = performingUser!, + UserId = performingUser!.Id, + Action = DocumentLogMessages.Import.Approve, + }; + var requestLog = new RequestLog() + { + Object = document, + Time = LocalDateTime.FromDateTime(DateTime.Now), + User = performingUser, + UserId = performingUser.Id, + Action = RequestLogMessages.ApproveImport, + Reason = request.Reason, + }; + var result = _context.Documents.Update(document); + await _context.DocumentLogs.AddAsync(log, cancellationToken); + await _context.RequestLogs.AddAsync(requestLog, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/Documents/Commands/AssignDocument.cs b/src/Application/Documents/Commands/AssignDocument.cs new file mode 100644 index 00000000..80096146 --- /dev/null +++ b/src/Application/Documents/Commands/AssignDocument.cs @@ -0,0 +1,90 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Messages; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities.Logging; +using Domain.Statuses; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.Documents.Commands; + +public class AssignDocument +{ + public record Command : IRequest + { + public Guid PerformingUserId { get; init; } + public Guid DocumentId { get; init; } + public Guid FolderId { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var document = await _context.Documents + .Include(x => x.Folder) + .FirstOrDefaultAsync(x => + x.Id == request.DocumentId, cancellationToken); + if (document is null) + { + throw new ConflictException("Document does not exist."); + } + + if (document.Status is not DocumentStatus.Approved) + { + throw new ConflictException("Document cannot be assigned."); + } + + var folder = await _context.Folders + .FirstOrDefaultAsync(x => x.Id == request.FolderId, cancellationToken); + + if (folder is null) + { + throw new ConflictException("Folder does not exist."); + } + + if (folder.NumberOfDocuments >= folder.Capacity) + { + throw new ConflictException("This folder cannot accept more documents."); + } + + var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); + document.Folder = folder; + document.LastModified = LocalDateTime.FromDateTime(DateTime.Now); + document.LastModifiedBy = performingUser!.Id; + var log = new DocumentLog() + { + Object = document, + Time = LocalDateTime.FromDateTime(DateTime.Now), + User = performingUser!, + UserId = performingUser!.Id, + Action = DocumentLogMessages.Import.Assign, + }; + var folderLog = new FolderLog() + { + Object = folder, + Time = LocalDateTime.FromDateTime(DateTime.Now), + User = performingUser!, + UserId = performingUser!.Id, + Action = FolderLogMessage.AssignDocument, + }; + var result = _context.Documents.Update(document); + await _context.DocumentLogs.AddAsync(log, cancellationToken); + await _context.FolderLogs.AddAsync(folderLog, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/Documents/Commands/CheckinDocument.cs b/src/Application/Documents/Commands/CheckinDocument.cs new file mode 100644 index 00000000..73351023 --- /dev/null +++ b/src/Application/Documents/Commands/CheckinDocument.cs @@ -0,0 +1,75 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Messages; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities.Logging; +using Domain.Entities.Physical; +using Domain.Statuses; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.Documents.Commands; + +public class CheckinDocument +{ + public record Command : IRequest + { + public Guid PerformingUserId { get; init; } + public Guid DocumentId { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var document = await _context.Documents + .Include(x => x.Department) + .Include(x => x.Folder) + .FirstOrDefaultAsync(x => x.Id == request.DocumentId, cancellationToken); + + if (document is null) + { + throw new KeyNotFoundException("Document does not exist."); + } + + if (document.Status is not DocumentStatus.Approved) + { + throw new ConflictException("Request cannot be checked in."); + } + + if (document.Folder is null) + { + throw new ConflictException("Request cannot be checked in."); + } + + var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); + document.Status = DocumentStatus.Available; + document.LastModified = LocalDateTime.FromDateTime(DateTime.Now); + document.LastModifiedBy = performingUser!.Id; + var log = new DocumentLog() + { + User = performingUser, + UserId = performingUser.Id, + Object = document, + Time = LocalDateTime.FromDateTime(DateTime.Now), + Action = DocumentLogMessages.Import.Checkin, + }; + + var result = _context.Documents.Update(document); + await _context.DocumentLogs.AddAsync(log, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/Documents/Commands/ImportDocument.cs b/src/Application/Documents/Commands/ImportDocument.cs index 6f1d441e..eaf17236 100644 --- a/src/Application/Documents/Commands/ImportDocument.cs +++ b/src/Application/Documents/Commands/ImportDocument.cs @@ -1,6 +1,7 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Messages; +using Application.Common.Models.Dtos.ImportDocument; using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities.Logging; @@ -22,6 +23,7 @@ public record Command : IRequest public string DocumentType { get; init; } = null!; public Guid ImporterId { get; init; } public Guid FolderId { get; init; } + public bool IsPrivate { get; init; } } public class CommandHandler : IRequestHandler @@ -75,7 +77,8 @@ public async Task Handle(Command request, CancellationToken cancell Importer = importer, Department = importer.Department, Folder = folder, - Status = DocumentStatus.Issued, + Status = DocumentStatus.Available, + IsPrivate = request.IsPrivate, Created = LocalDateTime.FromDateTime(DateTime.Now), CreatedBy = performingUser!.Id, }; diff --git a/src/Application/Documents/Commands/RejectDocument.cs b/src/Application/Documents/Commands/RejectDocument.cs new file mode 100644 index 00000000..d58bb281 --- /dev/null +++ b/src/Application/Documents/Commands/RejectDocument.cs @@ -0,0 +1,75 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Messages; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities.Logging; +using Domain.Statuses; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.Documents.Commands; + +public class RejectDocument +{ + public record Command : IRequest + { + public Guid PerformingUserId { get; init; } + public Guid DocumentId { get; init; } + public string Reason { get; init; } = null!; + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var document = await _context.Documents + .Include(x => x.Department) + .FirstOrDefaultAsync(x => + x.Id == request.DocumentId, cancellationToken); + if (document is null) + { + throw new ConflictException("Document does not exist."); + } + + if (document.Status is not DocumentStatus.Issued) + { + throw new ConflictException("Request cannot be rejected."); + } + + document.Status = DocumentStatus.Rejected; + var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); + var log = new DocumentLog() + { + Object = document, + Time = LocalDateTime.FromDateTime(DateTime.Now), + User = performingUser!, + UserId = performingUser!.Id, + Action = DocumentLogMessages.Import.Reject, + }; + var requestLog = new RequestLog() + { + Object = document, + Time = LocalDateTime.FromDateTime(DateTime.Now), + User = performingUser, + UserId = performingUser.Id, + Action = RequestLogMessages.RejectImport, + Reason = request.Reason, + }; + var result = _context.Documents.Update(document); + await _context.DocumentLogs.AddAsync(log, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/Documents/Commands/RequestImportDocument.cs b/src/Application/Documents/Commands/RequestImportDocument.cs index acc3df27..4efe3ca1 100644 --- a/src/Application/Documents/Commands/RequestImportDocument.cs +++ b/src/Application/Documents/Commands/RequestImportDocument.cs @@ -1,6 +1,7 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Messages; +using Application.Common.Models.Dtos.ImportDocument; using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities.Logging; @@ -15,29 +16,27 @@ namespace Application.Documents.Commands; public class RequestImportDocument { - public record Command : IRequest + public record Command : IRequest { public string Title { get; init; } = null!; public string? Description { get; init; } public string DocumentType { get; init; } = null!; public Guid IssuerId { get; init; } + public bool IsPrivate { get; set; } } - public class CommandHandler : IRequestHandler + public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; - private readonly ILogger _logger; - - public CommandHandler(IApplicationDbContext context, IMapper mapper, ILogger logger) + public CommandHandler(IApplicationDbContext context, IMapper mapper) { _context = context; _mapper = mapper; - _logger = logger; } - public async Task Handle(Command request, CancellationToken cancellationToken) + public async Task Handle(Command request, CancellationToken cancellationToken) { var issuer = await _context.Users .Include(x => x.Department) @@ -64,6 +63,7 @@ public async Task Handle(Command request, CancellationToken cancell Importer = issuer, Department = issuer.Department, Status = DocumentStatus.Issued, + IsPrivate = request.IsPrivate, Created = LocalDateTime.FromDateTime(DateTime.Now), CreatedBy = issuer.Id, }; @@ -73,13 +73,13 @@ public async Task Handle(Command request, CancellationToken cancell Time = LocalDateTime.FromDateTime(DateTime.Now), User = issuer, UserId = issuer.Id, - Action = DocumentLogMessages.Import.NewImportRequest + Action = DocumentLogMessages.Import.NewImportRequest, }; var result = await _context.Documents.AddAsync(entity, cancellationToken); await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); + return _mapper.Map(result.Entity); } } } \ No newline at end of file diff --git a/src/Application/Documents/Commands/UpdateDocument.cs b/src/Application/Documents/Commands/UpdateDocument.cs index d670ed86..442da5ce 100644 --- a/src/Application/Documents/Commands/UpdateDocument.cs +++ b/src/Application/Documents/Commands/UpdateDocument.cs @@ -51,6 +51,7 @@ public CommandHandler(IApplicationDbContext context, IMapper mapper) public async Task Handle(Command request, CancellationToken cancellationToken) { var document = await _context.Documents + .Include(x => x.Department) .Include( x => x.Importer) .FirstOrDefaultAsync( x => x.Id.Equals(request.DocumentId), cancellationToken); diff --git a/src/Application/Documents/Queries/GetAllIssuedDocumentsPaginated.cs b/src/Application/Documents/Queries/GetAllIssuedDocumentsPaginated.cs new file mode 100644 index 00000000..60a1cd78 --- /dev/null +++ b/src/Application/Documents/Queries/GetAllIssuedDocumentsPaginated.cs @@ -0,0 +1,74 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Mappings; +using Application.Common.Models; +using Application.Common.Models.Dtos.ImportDocument; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities; +using Domain.Entities.Physical; +using Domain.Statuses; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Documents.Queries; + +public class GetAllIssuedDocumentsPaginated +{ + public record Query : IRequest> + { + public Guid DepartmentId { get; init; } + public string? SearchTerm { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } + } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, + CancellationToken cancellationToken) + { + var documents = _context.Documents + .Include(x => x.Importer) + .Where(x => x.Status == DocumentStatus.Issued); + + documents = documents.Where(x => x.Department!.Id == request.DepartmentId); + + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + documents = documents.Where(x => + x.Title.ToLower().Contains(request.SearchTerm.ToLower())); + } + + var sortBy = request.SortBy; + if (sortBy is null || !sortBy.MatchesPropertyName()) + { + sortBy = nameof(IssuedDocumentDto.Id); + } + var sortOrder = request.SortOrder ?? "asc"; + var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; + var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + + var count = await documents.CountAsync(cancellationToken); + var list = await documents + .OrderByCustom(sortBy, sortOrder) + .Paginate(pageNumber.Value, sizeNumber.Value) + .ToListAsync(cancellationToken); + + var result = _mapper.Map>(list); + + return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + } + } +} \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetDocumentReason.cs b/src/Application/Documents/Queries/GetDocumentReason.cs new file mode 100644 index 00000000..9088f785 --- /dev/null +++ b/src/Application/Documents/Queries/GetDocumentReason.cs @@ -0,0 +1,43 @@ +using Application.Common.Interfaces; +using Application.Common.Models.Dtos; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Enums; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Documents.Queries; + +public class GetDocumentReason +{ + public record Query : IRequest + { + public Guid DocumentId { get; init; } + public RequestType Type { get; set; } + } + + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var log = await _context.RequestLogs + .FirstOrDefaultAsync(x => x.Object!.Id == request.DocumentId + && x.Type == request.Type, cancellationToken); + + if (log is null) + { + throw new KeyNotFoundException("Document does not have a request."); + } + + return _mapper.Map(log); + } + } +} \ No newline at end of file diff --git a/src/Application/Staffs/Commands/AddStaff.cs b/src/Application/Staffs/Commands/AddStaff.cs index cb381aae..343fb367 100644 --- a/src/Application/Staffs/Commands/AddStaff.cs +++ b/src/Application/Staffs/Commands/AddStaff.cs @@ -39,13 +39,20 @@ public async Task Handle(Command request, CancellationToken cancellati throw new KeyNotFoundException("User does not exist."); } - var room = await _context.Rooms.FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); + var room = await _context.Rooms + .Include(x => x.Staff) + .FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); if (room is null) { throw new KeyNotFoundException("Room does not exist."); } + if (room.Staff is not null) + { + throw new ConflictException("Room already has a staff."); + } + var existedStaff = await _context.Staffs .Include(x => x.Room) .Include(x => x.User) diff --git a/src/Domain/Entities/Logging/RequestLog.cs b/src/Domain/Entities/Logging/RequestLog.cs index fbf7d568..e9605ed3 100644 --- a/src/Domain/Entities/Logging/RequestLog.cs +++ b/src/Domain/Entities/Logging/RequestLog.cs @@ -7,4 +7,5 @@ namespace Domain.Entities.Logging; public class RequestLog : BaseLoggingEntity { public RequestType Type { get; set; } + public string Reason { get; set; } = null!; } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Migrations/20230613140433_RequestLogReason.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230613140433_RequestLogReason.Designer.cs new file mode 100644 index 00000000..710cd220 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230613140433_RequestLogReason.Designer.cs @@ -0,0 +1,1019 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230613140433_RequestLogReason")] + partial class RequestLogReason + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId") + .IsUnique(); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.HasOne("Domain.Entities.User", "Object") + .WithMany() + .HasForeignKey("ObjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Entry"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithOne("Room") + .HasForeignKey("Domain.Entities.Physical.Room", "DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230613140433_RequestLogReason.cs b/src/Infrastructure/Persistence/Migrations/20230613140433_RequestLogReason.cs new file mode 100644 index 00000000..5ca28aa1 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230613140433_RequestLogReason.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class RequestLogReason : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Reason", + table: "RequestLogs", + type: "text", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Reason", + table: "RequestLogs"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index 679625fe..ac299d19 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -201,6 +201,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ObjectId") .HasColumnType("uuid"); + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + b.Property("Time") .HasColumnType("timestamp without time zone"); From 1b109b3990150be6fb55e856b8b9cd262a33f1b4 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Tue, 13 Jun 2023 22:52:46 +0700 Subject: [PATCH 25/56] a --- src/Api/Controllers/BorrowsController.cs | 12 +++++++++++ .../Borrows/Commands/ApproveBorrowRequest.cs | 21 +++++++++++++++++++ .../Borrows/Commands/BorrowDocument.cs | 11 ++++++++++ .../Borrows/Commands/CancelBorrowRequest.cs | 15 +++++++++++++ .../Borrows/Commands/CheckoutDocument.cs | 14 +++++++++++++ .../Borrows/Commands/RejectBorrowRequest.cs | 1 + .../Borrows/Commands/ReturnDocument.cs | 1 + .../Borrows/Commands/UpdateBorrow.cs | 1 + .../Common/Messages/DocumentLogMessages.cs | 10 +++++++++ 9 files changed, 86 insertions(+) diff --git a/src/Api/Controllers/BorrowsController.cs b/src/Api/Controllers/BorrowsController.cs index 484c3949..14a98b3b 100644 --- a/src/Api/Controllers/BorrowsController.cs +++ b/src/Api/Controllers/BorrowsController.cs @@ -187,8 +187,10 @@ public async Task>>> GetAllRequests [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> ApproveRequest([FromRoute] Guid borrowId) { + var performingUserId = _currentUserService.GetId(); var command = new ApproveBorrowRequest.Command() { + PerformingUserId = performingUserId, BorrowId = borrowId, }; var result = await Mediator.Send(command); @@ -208,8 +210,10 @@ public async Task>> ApproveRequest([FromRoute] Gu [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> RejectRequest([FromRoute] Guid borrowId) { + var performingUserId = _currentUserService.GetId(); var command = new RejectBorrowRequest.Command() { + PerformingUserId = performingUserId, BorrowId = borrowId, }; var result = await Mediator.Send(command); @@ -229,8 +233,10 @@ public async Task>> RejectRequest([FromRoute] Gui [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> Checkout([FromRoute] Guid borrowId) { + var performingUserId = _currentUserService.GetId(); var command = new CheckoutDocument.Command() { + PerformingUserId = performingUserId, BorrowId = borrowId, }; var result = await Mediator.Send(command); @@ -250,8 +256,10 @@ public async Task>> Checkout([FromRoute] Guid bor [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> Return([FromRoute] Guid documentId) { + var performingUserId = _currentUserService.GetId(); var command = new ReturnDocument.Command() { + PerformingUserId = performingUserId, DocumentId = documentId, }; var result = await Mediator.Send(command); @@ -274,8 +282,10 @@ public async Task>> Update( [FromRoute] Guid borrowId, [FromBody] UpdateBorrowRequest request) { + var performingUserId = _currentUserService.GetId(); var command = new UpdateBorrow.Command() { + PerformingUserId = performingUserId, BorrowId = borrowId, BorrowFrom = request.BorrowFrom, BorrowTo = request.BorrowTo, @@ -298,8 +308,10 @@ public async Task>> Update( [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> Cancel([FromRoute] Guid borrowId) { + var performingUserId = _currentUserService.GetId(); var command = new CancelBorrowRequest.Command() { + PerformingUserId = performingUserId, BorrowId = borrowId, }; var result = await Mediator.Send(command); diff --git a/src/Application/Borrows/Commands/ApproveBorrowRequest.cs b/src/Application/Borrows/Commands/ApproveBorrowRequest.cs index 9d7b345d..8f3db1fa 100644 --- a/src/Application/Borrows/Commands/ApproveBorrowRequest.cs +++ b/src/Application/Borrows/Commands/ApproveBorrowRequest.cs @@ -1,7 +1,9 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities.Logging; using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; @@ -13,6 +15,7 @@ public class ApproveBorrowRequest { public record Command : IRequest { + public Guid PerformingUserId { get; init; } public Guid BorrowId { get; init; } } @@ -71,8 +74,26 @@ or BorrowRequestStatus.CheckedOut } } + var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); borrowRequest.Status = BorrowRequestStatus.Approved; + var log = new DocumentLog() + { + Object = borrowRequest.Document, + UserId = performingUser!.Id, + User = performingUser, + Time = LocalDateTime.FromDateTime(DateTime.Now), + Action = DocumentLogMessages.Borrow.Approve, + }; + var requestLog = new RequestLog() + { + Object = borrowRequest.Document, + UserId = performingUser.Id, + User = performingUser, + Time = LocalDateTime.FromDateTime(DateTime.Now), + Action = DocumentLogMessages.Borrow.Approve, + }; var result = _context.Borrows.Update(borrowRequest); + await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } diff --git a/src/Application/Borrows/Commands/BorrowDocument.cs b/src/Application/Borrows/Commands/BorrowDocument.cs index ad1df6fd..2dad1e17 100644 --- a/src/Application/Borrows/Commands/BorrowDocument.cs +++ b/src/Application/Borrows/Commands/BorrowDocument.cs @@ -1,7 +1,9 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities.Logging; using Domain.Entities.Physical; using Domain.Statuses; using FluentValidation; @@ -130,8 +132,17 @@ or BorrowRequestStatus.CheckedOut Reason = request.Reason, Status = BorrowRequestStatus.Pending, }; + var log = new DocumentLog() + { + UserId = user.Id, + User = user, + Object = document, + Time = LocalDateTime.FromDateTime(DateTime.Now), + Action = DocumentLogMessages.Borrow.NewBorrowRequest, + }; var result = await _context.Borrows.AddAsync(entity, cancellationToken); + await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); diff --git a/src/Application/Borrows/Commands/CancelBorrowRequest.cs b/src/Application/Borrows/Commands/CancelBorrowRequest.cs index 97f111d4..669bafbb 100644 --- a/src/Application/Borrows/Commands/CancelBorrowRequest.cs +++ b/src/Application/Borrows/Commands/CancelBorrowRequest.cs @@ -1,10 +1,13 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities.Logging; using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Borrows.Commands; @@ -12,6 +15,7 @@ public class CancelBorrowRequest { public record Command : IRequest { + public Guid PerformingUserId { get; init; } public Guid BorrowId { get; init; } } @@ -42,8 +46,19 @@ public async Task Handle(Command request, CancellationToken cancellat throw new ConflictException("Request cannot be cancelled."); } + var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); + var log = new DocumentLog() + { + Object = borrowRequest.Document, + UserId = performingUser!.Id, + User = performingUser, + Time = LocalDateTime.FromDateTime(DateTime.Now), + Action = DocumentLogMessages.Borrow.CanCel, + }; + borrowRequest.Status = BorrowRequestStatus.Cancelled; var result = _context.Borrows.Update(borrowRequest); + await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } diff --git a/src/Application/Borrows/Commands/CheckoutDocument.cs b/src/Application/Borrows/Commands/CheckoutDocument.cs index 6578225f..9a9e26a8 100644 --- a/src/Application/Borrows/Commands/CheckoutDocument.cs +++ b/src/Application/Borrows/Commands/CheckoutDocument.cs @@ -1,10 +1,13 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities.Logging; using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Borrows.Commands; @@ -12,6 +15,7 @@ public class CheckoutDocument { public record Command : IRequest { + public Guid PerformingUserId { get; init; } public Guid BorrowId { get; init; } } @@ -47,10 +51,20 @@ public async Task Handle(Command request, CancellationToken cancellat throw new ConflictException("Request cannot be checked out."); } + var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); borrowRequest.Status = BorrowRequestStatus.CheckedOut; borrowRequest.Document.Status = DocumentStatus.Borrowed; + var log = new DocumentLog() + { + Object = borrowRequest.Document, + UserId = performingUser!.Id, + User = performingUser, + Time = LocalDateTime.FromDateTime(DateTime.Now), + Action = DocumentLogMessages.Borrow.Checkout, + }; var result = _context.Borrows.Update(borrowRequest); _context.Documents.Update(borrowRequest.Document); + await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } diff --git a/src/Application/Borrows/Commands/RejectBorrowRequest.cs b/src/Application/Borrows/Commands/RejectBorrowRequest.cs index e1290e83..c738707d 100644 --- a/src/Application/Borrows/Commands/RejectBorrowRequest.cs +++ b/src/Application/Borrows/Commands/RejectBorrowRequest.cs @@ -12,6 +12,7 @@ public class RejectBorrowRequest { public record Command : IRequest { + public Guid PerformingUserId { get; init; } public Guid BorrowId { get; init; } } diff --git a/src/Application/Borrows/Commands/ReturnDocument.cs b/src/Application/Borrows/Commands/ReturnDocument.cs index 918a979d..37af412e 100644 --- a/src/Application/Borrows/Commands/ReturnDocument.cs +++ b/src/Application/Borrows/Commands/ReturnDocument.cs @@ -13,6 +13,7 @@ public class ReturnDocument { public record Command : IRequest { + public Guid PerformingUserId { get; init; } public Guid DocumentId { get; init; } } diff --git a/src/Application/Borrows/Commands/UpdateBorrow.cs b/src/Application/Borrows/Commands/UpdateBorrow.cs index fe39c471..c1b3ba70 100644 --- a/src/Application/Borrows/Commands/UpdateBorrow.cs +++ b/src/Application/Borrows/Commands/UpdateBorrow.cs @@ -33,6 +33,7 @@ public Validator() public record Command : IRequest { + public Guid PerformingUserId { get; init; } public Guid BorrowId { get; init; } public DateTime BorrowFrom { get; init; } public DateTime BorrowTo { get; init; } diff --git a/src/Application/Common/Messages/DocumentLogMessages.cs b/src/Application/Common/Messages/DocumentLogMessages.cs index b6a62cae..ce02d3b6 100644 --- a/src/Application/Common/Messages/DocumentLogMessages.cs +++ b/src/Application/Common/Messages/DocumentLogMessages.cs @@ -7,4 +7,14 @@ public static class Import public const string NewImport = "Imported new document"; public const string NewImportRequest = "Created new import request"; } + public static class Borrow + { + public const string NewBorrowRequest = "Created new borrow request"; + public const string CanCel = "Cancelled borrow request"; + public const string Approve = "Approved borrow request"; + public const string Reject = "Rejected borrow request"; + public const string Checkout = "Checked out borrow request"; + public const string Return = "Returned borrow request"; + public const string Update = "Updated borrow request"; + } } \ No newline at end of file From d29e9fc232c0a4592cd1bd62ad669184bff48406 Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Wed, 14 Jun 2023 14:37:03 +0700 Subject: [PATCH 26/56] modified: get all document now only return public document (#236) --- .../Documents/Queries/GetAllDocumentsPaginated.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs b/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs index fd5c6a0b..a8af3480 100644 --- a/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs +++ b/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs @@ -6,6 +6,7 @@ using Application.Common.Models.Dtos.Physical; using AutoMapper; using AutoMapper.QueryableExtensions; +using Domain.Statuses; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; @@ -63,7 +64,9 @@ public async Task> Handle(Query request, .Include(x => x.Folder) .ThenInclude(y => y.Locker) .ThenInclude(z => z.Room) - .ThenInclude(t => t.Department); + .ThenInclude(t => t.Department) + .Where(x => !x.IsPrivate && x.Status != DocumentStatus.Issued); + if (folderExists) { From 96bf826b2e36bab678851ed7f20ff295455c609e Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Wed, 14 Jun 2023 14:37:34 +0700 Subject: [PATCH 27/56] get document log by id + get document logs paginated (#244) --- src/Api/Controllers/DocumentsController.cs | 47 ++++++++++++- .../GetAllLogsPaginatedQueryParameters.cs | 9 +++ .../Models/Dtos/Logging/DocumentLogDto.cs | 28 ++++++++ .../Queries/GetAllDocumentLogsPaginated.cs | 67 +++++++++++++++++++ .../Documents/Queries/GetLogOfDocumentById.cs | 43 ++++++++++++ 5 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs create mode 100644 src/Application/Common/Models/Dtos/Logging/DocumentLogDto.cs create mode 100644 src/Application/Documents/Queries/GetAllDocumentLogsPaginated.cs create mode 100644 src/Application/Documents/Queries/GetLogOfDocumentById.cs diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index 2a6b6036..57aea634 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -1,8 +1,10 @@ +using Api.Controllers.Payload.Requests; using Api.Controllers.Payload.Requests.Documents; using Application.Common.Interfaces; using Application.Common.Models; using Application.Common.Models.Dtos; using Application.Common.Models.Dtos.ImportDocument; +using Application.Common.Models.Dtos.Logging; using Application.Common.Models.Dtos.Physical; using Application.Documents.Commands; using Application.Documents.Queries; @@ -72,6 +74,26 @@ public async Task>>> GetAll var result = await Mediator.Send(query); return Ok(Result>.Succeed(result)); } + + /// + /// Get a document log by Id + /// + /// + /// Return a DocumentLogDto + [RequiresRole(IdentityData.Roles.Admin)] + [HttpGet("log/{logId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetLogById([FromRoute] Guid logId) + { + var query = new GetLogOfDocumentById.Query() + { + LogId = logId + }; + + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } /// /// Get all documents paginated @@ -101,7 +123,30 @@ public async Task>>> GetAllForAdm var result = await Mediator.Send(query); return Ok(Result>.Succeed(result)); } - + + /// + /// Get all log of document + /// + /// + /// Paginated list of DocumentLogDto + [RequiresRole(IdentityData.Roles.Admin)] + [HttpGet("logs")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>>> GetAllLogsPaginated( + [FromQuery] GetAllLogsPaginatedQueryParameters queryParameters) + { + var query = new GetAllDocumentLogsPaginated.Query() + { + SearchTerm = queryParameters.SearchTerm, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } + /// /// Get all documents for staff paginated /// diff --git a/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs new file mode 100644 index 00000000..eca75ee0 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs @@ -0,0 +1,9 @@ +namespace Api.Controllers.Payload.Requests; + +/// +/// get all logs paginated +/// +public class GetAllLogsPaginatedQueryParameters : PaginatedQueryParameters +{ + public string? SearchTerm { get; set; } +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Logging/DocumentLogDto.cs b/src/Application/Common/Models/Dtos/Logging/DocumentLogDto.cs new file mode 100644 index 00000000..c998ccf4 --- /dev/null +++ b/src/Application/Common/Models/Dtos/Logging/DocumentLogDto.cs @@ -0,0 +1,28 @@ +using Application.Common.Mappings; +using Application.Common.Models.Dtos.Physical; +using Application.Users.Queries; +using AutoMapper; +using Domain.Entities.Logging; + +namespace Application.Common.Models.Dtos.Logging; + +public class DocumentLogDto : IMapFrom +{ + public Guid Id { get; set; } + public Guid UserId { get; set; } + public string Action { get; set; } + public DocumentDto? Object { get; set; } + public DateTime Time { get; set; } + public UserDto User { get; set; } + + public void Mapping(Profile profile) + { + + profile.CreateMap() + .ForMember(dest => dest.Time, + opt => opt.MapFrom(src => src.Time.ToDateTimeUnspecified())) + .ForMember(dest => dest.Object, + opt => opt.MapFrom(src => src.Object)); + + } +} \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetAllDocumentLogsPaginated.cs b/src/Application/Documents/Queries/GetAllDocumentLogsPaginated.cs new file mode 100644 index 00000000..a0bf18ae --- /dev/null +++ b/src/Application/Documents/Queries/GetAllDocumentLogsPaginated.cs @@ -0,0 +1,67 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Logging; +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Documents.Queries; + +public class GetAllDocumentLogsPaginated +{ + public record Query : IRequest> + { + public string? SearchTerm { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } + } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var logs = _context.DocumentLogs + .Include(x => x.Object) + .Include(x => x.User) + .ThenInclude(x => x.Department) + .AsQueryable(); + + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + logs = logs.Where(x => + x.Action.ToLower().Contains(request.SearchTerm.ToLower())); + } + + var sortBy = request.SortBy; + if (sortBy is null || !sortBy.MatchesPropertyName()) + { + sortBy = nameof(DocumentLogDto.Time); + } + var sortOrder = request.SortOrder ?? "desc"; + var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; + var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + + var count = await logs.CountAsync(cancellationToken); + var list = await logs + .OrderByCustom(sortBy, sortOrder) + .Paginate(pageNumber.Value, sizeNumber.Value) + .ToListAsync(cancellationToken); + + var result = _mapper.Map>(list); + + return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + } + } +} \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetLogOfDocumentById.cs b/src/Application/Documents/Queries/GetLogOfDocumentById.cs new file mode 100644 index 00000000..2a6fd6a2 --- /dev/null +++ b/src/Application/Documents/Queries/GetLogOfDocumentById.cs @@ -0,0 +1,43 @@ +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Logging; +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Documents.Queries; + +public class GetLogOfDocumentById +{ + public record Query : IRequest + { + public Guid LogId { get; init; } + } + + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var log = await _context.DocumentLogs + .Include(x => x.Object) + .Include(x => x.User) + .ThenInclude(x => x.Department) + .FirstOrDefaultAsync(x => x.Id.Equals(request.LogId), cancellationToken); + + if (log is null) + { + throw new KeyNotFoundException("Log does not exist."); + } + + return _mapper.Map(log); + } + } +} \ No newline at end of file From fe366c104809ba709cde10578d9c1c279209537f Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Wed, 14 Jun 2023 16:04:06 +0700 Subject: [PATCH 28/56] permission --- src/Api/ConfigureServices.cs | 1 + src/Api/Controllers/DocumentsController.cs | 52 +- .../Documents/SharePermissionsRequest.cs | 9 + src/Api/Services/ExpiryPermissionService.cs | 29 + .../Borrows/Commands/ApproveBorrowRequest.cs | 1 + .../Borrows/Commands/BorrowDocument.cs | 2 + .../Borrows/Commands/CheckoutDocument.cs | 4 +- .../Borrows/Commands/RejectBorrowRequest.cs | 13 + .../Common/Interfaces/IPermissionManager.cs | 4 +- .../Common/Messages/RequestLogMessages.cs | 2 + .../Models/Dtos/Physical/PermissionDto.cs | 24 + .../Documents/Commands/ShareDocument.cs | 80 ++ .../Documents/Queries/GetPermissions.cs | 80 ++ src/Domain/Entities/Physical/Permission.cs | 3 + src/Infrastructure/ConfigureServices.cs | 1 + ...0230614084428_PermissionExpiry.Designer.cs | 1022 +++++++++++++++++ .../20230614084428_PermissionExpiry.cs | 30 + .../ApplicationDbContextModelSnapshot.cs | 3 + .../Services/PermissionManager.cs | 13 +- 19 files changed, 1364 insertions(+), 9 deletions(-) create mode 100644 src/Api/Controllers/Payload/Requests/Documents/SharePermissionsRequest.cs create mode 100644 src/Api/Services/ExpiryPermissionService.cs create mode 100644 src/Application/Common/Models/Dtos/Physical/PermissionDto.cs create mode 100644 src/Application/Documents/Commands/ShareDocument.cs create mode 100644 src/Application/Documents/Queries/GetPermissions.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230614084428_PermissionExpiry.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230614084428_PermissionExpiry.cs diff --git a/src/Api/ConfigureServices.cs b/src/Api/ConfigureServices.cs index 4c4f747f..0a8f60f0 100644 --- a/src/Api/ConfigureServices.cs +++ b/src/Api/ConfigureServices.cs @@ -14,6 +14,7 @@ public static IServiceCollection AddApiServices(this IServiceCollection services { // Register services services.AddServices(); + services.AddHostedService(); services.AddControllers(opt => opt.Conventions.Add(new RouteTokenTransformerConvention(new SlugifyParameterTransformer()))); diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index 2a6b6036..181eda05 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -348,7 +348,7 @@ public async Task>> Reason( /// Id of the document to be rejected /// /// A DocumentDto of the rejected document - [HttpPost("{documentId:guid}/assign")] + [HttpPost("assign/{documentId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -366,4 +366,54 @@ public async Task>> Assign( var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); } + + /// + /// Share permissions for an employee of a specific document + /// + /// Id of the document + /// + /// A DocumentDto + [HttpPost("{documentId:guid}/permissions")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> SharePermissions( + [FromRoute] Guid documentId, + [FromBody] SharePermissionsRequest request) + { + var performingUserId = _currentUserService.GetId(); + var query = new ShareDocument.Command() + { + PerformingUserId = performingUserId, + DocumentId = documentId, + UserIds = request.UserIds, + CanRead = request.CanRead, + CanBorrow = request.CanBorrow, + ExpiryDate = request.ExpiryDate, + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } + + /// + /// Get permissions for an employee of a specific document + /// + /// Id of the document to be getting permissions from + /// A DocumentDto of the rejected document + [HttpGet("{documentId:guid}/permissions")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetPermissions( + [FromRoute] Guid documentId) + { + var performingUserId = _currentUserService.GetId(); + var query = new GetPermissions.Query() + { + PerformingUserId = performingUserId, + DocumentId = documentId, + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/SharePermissionsRequest.cs b/src/Api/Controllers/Payload/Requests/Documents/SharePermissionsRequest.cs new file mode 100644 index 00000000..be85ccef --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Documents/SharePermissionsRequest.cs @@ -0,0 +1,9 @@ +namespace Api.Controllers.Payload.Requests.Documents; + +public class SharePermissionsRequest +{ + public Guid[] UserIds { get; set; } + public bool CanRead { get; set; } + public bool CanBorrow { get; set; } + public DateTime ExpiryDate { get; set; } +} \ No newline at end of file diff --git a/src/Api/Services/ExpiryPermissionService.cs b/src/Api/Services/ExpiryPermissionService.cs new file mode 100644 index 00000000..f1e855ae --- /dev/null +++ b/src/Api/Services/ExpiryPermissionService.cs @@ -0,0 +1,29 @@ +using Application.Common.Interfaces; +using NodaTime; + +namespace Api.Services; + +public class ExpiryPermissionService : BackgroundService +{ + private readonly IServiceProvider _serviceProvider; + + public ExpiryPermissionService(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + var localDateTimeNow = LocalDateTime.FromDateTime(DateTime.Now); + using var scope = _serviceProvider.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + + var expiredPermissions = context.Permissions.Where(x => x.ExpiryDateTime < localDateTimeNow); + context.Permissions.RemoveRange(expiredPermissions); + await context.SaveChangesAsync(stoppingToken); + await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); + } + } +} \ No newline at end of file diff --git a/src/Application/Borrows/Commands/ApproveBorrowRequest.cs b/src/Application/Borrows/Commands/ApproveBorrowRequest.cs index 8f3db1fa..348ff63f 100644 --- a/src/Application/Borrows/Commands/ApproveBorrowRequest.cs +++ b/src/Application/Borrows/Commands/ApproveBorrowRequest.cs @@ -94,6 +94,7 @@ or BorrowRequestStatus.CheckedOut }; var result = _context.Borrows.Update(borrowRequest); await _context.DocumentLogs.AddAsync(log, cancellationToken); + await _context.RequestLogs.AddAsync(requestLog, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } diff --git a/src/Application/Borrows/Commands/BorrowDocument.cs b/src/Application/Borrows/Commands/BorrowDocument.cs index 2dad1e17..4596f88e 100644 --- a/src/Application/Borrows/Commands/BorrowDocument.cs +++ b/src/Application/Borrows/Commands/BorrowDocument.cs @@ -5,6 +5,7 @@ using AutoMapper; using Domain.Entities.Logging; using Domain.Entities.Physical; +using Domain.Events; using Domain.Statuses; using FluentValidation; using MediatR; @@ -75,6 +76,7 @@ public async Task Handle(Command request, CancellationToken cancellat var document = await _context.Documents .Include(x => x.Department) + .Include(x => x.Importer) .FirstOrDefaultAsync(x => x.Id == request.DocumentId, cancellationToken); if (document is null) { diff --git a/src/Application/Borrows/Commands/CheckoutDocument.cs b/src/Application/Borrows/Commands/CheckoutDocument.cs index 9a9e26a8..a3fbea79 100644 --- a/src/Application/Borrows/Commands/CheckoutDocument.cs +++ b/src/Application/Borrows/Commands/CheckoutDocument.cs @@ -54,10 +54,12 @@ public async Task Handle(Command request, CancellationToken cancellat var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); borrowRequest.Status = BorrowRequestStatus.CheckedOut; borrowRequest.Document.Status = DocumentStatus.Borrowed; + borrowRequest.Document.LastModified = LocalDateTime.FromDateTime(DateTime.Now); + borrowRequest.Document.LastModifiedBy = performingUser!.Id; var log = new DocumentLog() { Object = borrowRequest.Document, - UserId = performingUser!.Id, + UserId = performingUser.Id, User = performingUser, Time = LocalDateTime.FromDateTime(DateTime.Now), Action = DocumentLogMessages.Borrow.Checkout, diff --git a/src/Application/Borrows/Commands/RejectBorrowRequest.cs b/src/Application/Borrows/Commands/RejectBorrowRequest.cs index c738707d..d07eebcb 100644 --- a/src/Application/Borrows/Commands/RejectBorrowRequest.cs +++ b/src/Application/Borrows/Commands/RejectBorrowRequest.cs @@ -1,10 +1,13 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities.Logging; using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Borrows.Commands; @@ -42,8 +45,18 @@ public async Task Handle(Command request, CancellationToken cancellat throw new ConflictException("Request cannot be rejected."); } + var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); borrowRequest.Status = BorrowRequestStatus.Rejected; + var requestLog = new RequestLog() + { + Object = borrowRequest.Document, + UserId = performingUser!.Id, + User = performingUser, + Time = LocalDateTime.FromDateTime(DateTime.Now), + Action = DocumentLogMessages.Borrow.Approve, + }; var result = _context.Borrows.Update(borrowRequest); + await _context.RequestLogs.AddAsync(requestLog, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } diff --git a/src/Application/Common/Interfaces/IPermissionManager.cs b/src/Application/Common/Interfaces/IPermissionManager.cs index 6c0e7093..4e7c32fc 100644 --- a/src/Application/Common/Interfaces/IPermissionManager.cs +++ b/src/Application/Common/Interfaces/IPermissionManager.cs @@ -7,6 +7,6 @@ namespace Application.Common.Interfaces; public interface IPermissionManager { bool IsGranted(Guid documentId, DocumentOperation operation, params Guid[] userIds); - Task GrantAsync(Document document, DocumentOperation operation, User[] users, CancellationToken cancellationToken); - Task RevokeAsync(Document document, DocumentOperation operation, User[] users, CancellationToken cancellationToken); + Task GrantAsync(Document document, DocumentOperation operation, User[] users, DateTime expiryDate, CancellationToken cancellationToken); + Task RevokeAsync(Guid documentId, DocumentOperation operation, Guid[] userIds, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Application/Common/Messages/RequestLogMessages.cs b/src/Application/Common/Messages/RequestLogMessages.cs index ae9bab77..69fa8565 100644 --- a/src/Application/Common/Messages/RequestLogMessages.cs +++ b/src/Application/Common/Messages/RequestLogMessages.cs @@ -4,4 +4,6 @@ public static class RequestLogMessages { public const string ApproveImport = "Approved import request"; public const string RejectImport = "Rejected import request"; + public const string ApproveBorrow = "Rejected borrow request"; + public const string RejectBorrow = "Rejected borrow request"; } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Physical/PermissionDto.cs b/src/Application/Common/Models/Dtos/Physical/PermissionDto.cs new file mode 100644 index 00000000..b9cc456d --- /dev/null +++ b/src/Application/Common/Models/Dtos/Physical/PermissionDto.cs @@ -0,0 +1,24 @@ +using Application.Common.Mappings; +using Application.Common.Models.Operations; +using Application.Users.Queries; +using AutoMapper; +using Domain.Entities.Physical; + +namespace Application.Common.Models.Dtos.Physical; + +public class PermissionDto : IMapFrom +{ + public bool CanRead { get; set; } + public bool CanBorrow { get; set; } + public Guid EmployeeId { get; set; } + public Guid DocumentId { get; set; } + + public void Mapping(Profile profile) + { + profile.CreateMap() + .ForMember(dest => dest.CanRead, + opt => opt.MapFrom(src => src.AllowedOperations.Contains(DocumentOperation.Read.ToString()))) + .ForMember(dest => dest.CanBorrow, + opt => opt.MapFrom(src => src.AllowedOperations.Contains(DocumentOperation.Borrow.ToString()))); + } +} \ No newline at end of file diff --git a/src/Application/Documents/Commands/ShareDocument.cs b/src/Application/Documents/Commands/ShareDocument.cs new file mode 100644 index 00000000..3dbbfe08 --- /dev/null +++ b/src/Application/Documents/Commands/ShareDocument.cs @@ -0,0 +1,80 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Operations; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Documents.Commands; + +public class ShareDocument +{ + public record Command : IRequest + { + public Guid PerformingUserId { get; init; } + public Guid DocumentId { get; init; } + public Guid[] UserIds { get; init; } = null!; + public bool CanRead { get; init; } + public bool CanBorrow { get; init; } + public DateTime ExpiryDate { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _applicationDbContext; + private readonly IPermissionManager _permissionManager; + + public CommandHandler(IApplicationDbContext applicationDbContext, IPermissionManager permissionManager) + { + _permissionManager = permissionManager; + _applicationDbContext = applicationDbContext; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var document = await + _applicationDbContext.Documents + .Include(x => x.Importer) + .FirstOrDefaultAsync(x => x.Id == request.DocumentId, cancellationToken); + if (document is null) + { + throw new KeyNotFoundException("Document does not exist."); + } + + if (document.Importer!.Id != request.PerformingUserId) + { + throw new UnauthorizedAccessException("You are not the owner of the document."); + } + + if (request.ExpiryDate.ToUniversalTime() < DateTime.UtcNow) + { + throw new ConflictException("Expiry date cannot be in the past."); + } + + var users = _applicationDbContext.Users + .Where(x => request.UserIds.Contains(x.Id)) + .ToList(); + users.RemoveAll(x => x.Id == request.PerformingUserId); + + if (request.CanRead) + { + await _permissionManager.GrantAsync(document, DocumentOperation.Read, users.ToArray(), request.ExpiryDate.ToLocalTime(), cancellationToken); + } + else + { + await _permissionManager.RevokeAsync(document.Id, DocumentOperation.Read, request.UserIds, cancellationToken); + } + + if (request.CanBorrow) + { + await _permissionManager.GrantAsync(document, DocumentOperation.Borrow, users.ToArray(), request.ExpiryDate.ToLocalTime(), cancellationToken); + } + else + { + await _permissionManager.RevokeAsync(document.Id, DocumentOperation.Borrow, request.UserIds, cancellationToken); + } + + return true; + } + } +} \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetPermissions.cs b/src/Application/Documents/Queries/GetPermissions.cs new file mode 100644 index 00000000..6f560a7a --- /dev/null +++ b/src/Application/Documents/Queries/GetPermissions.cs @@ -0,0 +1,80 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using Application.Users.Queries; +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Documents.Queries; + +public class GetPermissions +{ + public record Query : IRequest + { + public Guid PerformingUserId { get; init; } + public Guid DocumentId { get; init; } + } + + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var performingUser = + await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); + + if (performingUser is null) + { + throw new UnauthorizedAccessException(); + } + + var document = await _context.Documents + .Include(x => x.Folder) + .Include(x => x.Importer) + .FirstOrDefaultAsync(x => x.Id == request.DocumentId, cancellationToken); + if (document is null) + { + throw new ConflictException("Document does not exist."); + } + + if (document.Importer!.Id == request.PerformingUserId) + { + var result = new PermissionDto() + { + DocumentId = document.Id, + EmployeeId = performingUser.Id, + CanRead = true, + CanBorrow = true, + }; + + return result; + } + + var permission = await _context.Permissions.FirstOrDefaultAsync( + x => x.DocumentId == request.DocumentId && x.EmployeeId == request.PerformingUserId, + cancellationToken); + + if (permission is null) + { + return new PermissionDto() + { + DocumentId = document.Id, + EmployeeId = performingUser.Id, + CanRead = false, + CanBorrow = false, + }; + } + + return _mapper.Map(permission); + } + } +} \ No newline at end of file diff --git a/src/Domain/Entities/Physical/Permission.cs b/src/Domain/Entities/Physical/Permission.cs index 3842eb28..29b01181 100644 --- a/src/Domain/Entities/Physical/Permission.cs +++ b/src/Domain/Entities/Physical/Permission.cs @@ -1,3 +1,5 @@ +using NodaTime; + namespace Domain.Entities.Physical; public class Permission @@ -5,6 +7,7 @@ public class Permission public Guid EmployeeId { get; set; } public Guid DocumentId { get; set; } public string AllowedOperations { get; set; } = null!; + public LocalDateTime ExpiryDateTime { get; set; } public User Employee { get; set; } = null!; public Document Document { get; set; } = null!; diff --git a/src/Infrastructure/ConfigureServices.cs b/src/Infrastructure/ConfigureServices.cs index 9c8c6b72..e79da5ec 100644 --- a/src/Infrastructure/ConfigureServices.cs +++ b/src/Infrastructure/ConfigureServices.cs @@ -1,6 +1,7 @@ using System.IdentityModel.Tokens.Jwt; using System.Security.Cryptography; using Application.Common.Interfaces; +using Application.Common.Models; using Infrastructure.Identity; using Infrastructure.Identity.Authentication; using Infrastructure.Persistence; diff --git a/src/Infrastructure/Persistence/Migrations/20230614084428_PermissionExpiry.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230614084428_PermissionExpiry.Designer.cs new file mode 100644 index 00000000..9b76af20 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230614084428_PermissionExpiry.Designer.cs @@ -0,0 +1,1022 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230614084428_PermissionExpiry")] + partial class PermissionExpiry + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId") + .IsUnique(); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.HasOne("Domain.Entities.User", "Object") + .WithMany() + .HasForeignKey("ObjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Entry"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithOne("Room") + .HasForeignKey("Domain.Entities.Physical.Room", "DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230614084428_PermissionExpiry.cs b/src/Infrastructure/Persistence/Migrations/20230614084428_PermissionExpiry.cs new file mode 100644 index 00000000..61e9daaa --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230614084428_PermissionExpiry.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class PermissionExpiry : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ExpiryDateTime", + table: "Permissions", + type: "timestamp without time zone", + nullable: false, + defaultValue: new NodaTime.LocalDateTime(1, 1, 1, 0, 0)); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ExpiryDateTime", + table: "Permissions"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index ac299d19..98624b11 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -496,6 +496,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + b.HasKey("DocumentId", "EmployeeId"); b.HasIndex("EmployeeId"); diff --git a/src/Infrastructure/Services/PermissionManager.cs b/src/Infrastructure/Services/PermissionManager.cs index 74af4cc6..8fb62dc0 100644 --- a/src/Infrastructure/Services/PermissionManager.cs +++ b/src/Infrastructure/Services/PermissionManager.cs @@ -3,6 +3,7 @@ using Application.Common.Models.Operations; using Domain.Entities; using Domain.Entities.Physical; +using NodaTime; namespace Infrastructure.Services; @@ -22,7 +23,7 @@ public bool IsGranted(Guid documentId, DocumentOperation operation, params Guid[ !x.AllowedOperations.Contains(operation.ToString()))); } - public async Task GrantAsync(Document document, DocumentOperation operation, User[] users, CancellationToken cancellationToken) + public async Task GrantAsync(Document document, DocumentOperation operation, User[] users, DateTime expiryDate, CancellationToken cancellationToken) { foreach (var user in users) { @@ -43,6 +44,7 @@ public async Task GrantAsync(Document document, DocumentOperation operation, Use operation.ToString() }; existedPermission.AllowedOperations = x.ToString(); + existedPermission.ExpiryDateTime = LocalDateTime.FromDateTime(expiryDate); _context.Permissions.Update(existedPermission); } else @@ -55,6 +57,7 @@ public async Task GrantAsync(Document document, DocumentOperation operation, Use Employee = user, AllowedOperations = operation.ToString(), }; + existedPermission.ExpiryDateTime = LocalDateTime.FromDateTime(expiryDate); await _context.Permissions.AddAsync(existedPermission, cancellationToken); } } @@ -62,12 +65,12 @@ public async Task GrantAsync(Document document, DocumentOperation operation, Use await _context.SaveChangesAsync(cancellationToken); } - public async Task RevokeAsync(Document document, DocumentOperation operation, User[] users, CancellationToken cancellationToken) + public async Task RevokeAsync(Guid documentId, DocumentOperation operation, Guid[] userIds, CancellationToken cancellationToken) { - foreach (var user in users) + foreach (var userId in userIds) { var existedPermission = - _context.Permissions.FirstOrDefault(x => x.DocumentId == document.Id && x.EmployeeId == user.Id); + _context.Permissions.FirstOrDefault(x => x.DocumentId == documentId && x.EmployeeId == userId); if (existedPermission is null) continue; var operations = existedPermission.AllowedOperations.Split(","); if (!operations.Contains(operation.ToString())) continue; @@ -75,7 +78,7 @@ public async Task RevokeAsync(Document document, DocumentOperation operation, Us var x = new CommaDelimitedStringCollection(); x.AddRange(operations); x.Remove(operation.ToString()); - if (x.Count == 0) + if (x.Count == 0 || existedPermission.ExpiryDateTime < LocalDateTime.FromDateTime(DateTime.Now)) { _context.Permissions.Remove(existedPermission); } From 52a2e711300d5bb148e894fb70f38aead1d63a07 Mon Sep 17 00:00:00 2001 From: kaitozu <43519768+kaitoz11@users.noreply.github.com> Date: Wed, 14 Jun 2023 19:15:42 +0700 Subject: [PATCH 29/56] feat: get user log by id + get user logs paginated (#247) Co-authored-by: Vzart <85790072+Vzart@users.noreply.github.com> --- .../GetAllLogsPaginatedQueryParameters.cs | 2 +- src/Api/Controllers/UsersController.cs | 46 +++++++++++++ .../Common/Models/Dtos/Logging/UserLogDto.cs | 25 +++++++ .../Users/Queries/GetAllUserLogsPaginated.cs | 66 +++++++++++++++++++ .../Users/Queries/GetUserLogById.cs | 43 ++++++++++++ 5 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 src/Application/Common/Models/Dtos/Logging/UserLogDto.cs create mode 100644 src/Application/Users/Queries/GetAllUserLogsPaginated.cs create mode 100644 src/Application/Users/Queries/GetUserLogById.cs diff --git a/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs index eca75ee0..a4464a98 100644 --- a/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs @@ -1,4 +1,4 @@ -namespace Api.Controllers.Payload.Requests; +namespace Api.Controllers.Payload.Requests; /// /// get all logs paginated diff --git a/src/Api/Controllers/UsersController.cs b/src/Api/Controllers/UsersController.cs index 095e9605..795769bc 100644 --- a/src/Api/Controllers/UsersController.cs +++ b/src/Api/Controllers/UsersController.cs @@ -1,6 +1,8 @@ +using Api.Controllers.Payload.Requests; using Api.Controllers.Payload.Requests.Users; using Application.Common.Interfaces; using Application.Common.Models; +using Application.Common.Models.Dtos.Logging; using Application.Identity; using Application.Users.Commands; using Application.Users.Queries; @@ -160,4 +162,48 @@ public async Task>> Update([FromRoute] Guid userId, var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } + + /// + /// Get all user related logs paginated + /// + /// Get all users related logs query parameters + /// A paginated list of UserLogDto + [RequiresRole(IdentityData.Roles.Admin)] + [HttpGet("logs")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task>>> GetAllUserLogs( + [FromQuery] GetAllLogsPaginatedQueryParameters queryParameters) + { + var query = new GetAllUserLogsPaginated.Query() + { + SearchTerm = queryParameters.SearchTerm, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } + + /// + /// Get user related log by Id + /// + /// Id of the logged user + /// UserLogDto of the logged user + [RequiresRole(IdentityData.Roles.Admin)] + [HttpGet("log/{logId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task>> GetUserLogById([FromRoute] Guid logId) + { + var query = new GetUserLogById.Query() + { + LogId = logId + }; + + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } } diff --git a/src/Application/Common/Models/Dtos/Logging/UserLogDto.cs b/src/Application/Common/Models/Dtos/Logging/UserLogDto.cs new file mode 100644 index 00000000..e20ba4e3 --- /dev/null +++ b/src/Application/Common/Models/Dtos/Logging/UserLogDto.cs @@ -0,0 +1,25 @@ +using Application.Common.Mappings; +using Application.Users.Queries; +using AutoMapper; +using Domain.Entities.Logging; + +namespace Application.Common.Models.Dtos.Logging; + +public class UserLogDto : IMapFrom +{ + public Guid Id { get; set; } + public string Action { get; set; } = null!; + public UserDto? Object { get; set; } + public DateTime Time { get; set; } + public UserDto User { get; set; } = null!; + + public void Mapping(Profile profile) + { + profile.CreateMap() + .ForMember( dest => dest.Time, + opt => opt.MapFrom( src => src.Time.ToDateTimeUnspecified())) + .ForMember(dest => dest.Object, + opt => opt.MapFrom( src => src.Object)); + + } +} \ No newline at end of file diff --git a/src/Application/Users/Queries/GetAllUserLogsPaginated.cs b/src/Application/Users/Queries/GetAllUserLogsPaginated.cs new file mode 100644 index 00000000..67528d67 --- /dev/null +++ b/src/Application/Users/Queries/GetAllUserLogsPaginated.cs @@ -0,0 +1,66 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Logging; +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Users.Queries; + +public class GetAllUserLogsPaginated +{ + public record Query : IRequest> + { + public string? SearchTerm { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } + } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var logs = _context.UserLogs + .Include(x => x.Object) + .AsQueryable(); + + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + logs = logs.Where(x => + x.Action.Trim().ToLower().Contains(request.SearchTerm.Trim().ToLower())); + } + + var sortBy = request.SortBy; + if (sortBy is null || !sortBy.MatchesPropertyName()) + { + sortBy = nameof(UserLogDto.Time); + } + + var sortOrder = request.SortOrder ?? "dsc"; + var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; + var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + + var count = await logs.CountAsync(cancellationToken); + var list = await logs + .Paginate(pageNumber.Value, sizeNumber.Value) + .OrderByCustom(sortBy, sortOrder) + .ToListAsync(cancellationToken); + + var result = _mapper.Map>(list); + + return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + } + } +} \ No newline at end of file diff --git a/src/Application/Users/Queries/GetUserLogById.cs b/src/Application/Users/Queries/GetUserLogById.cs new file mode 100644 index 00000000..7e561ae7 --- /dev/null +++ b/src/Application/Users/Queries/GetUserLogById.cs @@ -0,0 +1,43 @@ +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Logging; +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Users.Queries; + +public class GetUserLogById +{ + public record Query : IRequest + { + public Guid LogId { get; init; } + } + + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var log = await _context.UserLogs + .Include(x => x.Object) + .Include(x => x.User) + .ThenInclude(x => x.Department) + .FirstOrDefaultAsync(x => x.Id.Equals(request.LogId), cancellationToken); + + if (log is null) + { + throw new KeyNotFoundException("Log does not exist."); + } + + return _mapper.Map(log); + } + } +} \ No newline at end of file From 8be0cf291d064a53642e266de2a3c753d8117fe2 Mon Sep 17 00:00:00 2001 From: kaitozu <43519768+kaitoz11@users.noreply.github.com> Date: Wed, 14 Jun 2023 19:16:31 +0700 Subject: [PATCH 30/56] feat: get folder log by id + get folder logs paginated (#251) * feat: get folder log by id + get folder logs paginated * fix: my skill issue --------- Co-authored-by: Vzart <85790072+Vzart@users.noreply.github.com> --- src/Api/Controllers/FoldersController.cs | 47 +++++++++++++ .../Models/Dtos/Logging/FolderLogDto.cs | 25 +++++++ .../Queries/GetAllFolderLogsPaginated.cs | 66 +++++++++++++++++++ .../Folders/Queries/GetFolderLogById.cs | 43 ++++++++++++ 4 files changed, 181 insertions(+) create mode 100644 src/Application/Common/Models/Dtos/Logging/FolderLogDto.cs create mode 100644 src/Application/Folders/Queries/GetAllFolderLogsPaginated.cs create mode 100644 src/Application/Folders/Queries/GetFolderLogById.cs diff --git a/src/Api/Controllers/FoldersController.cs b/src/Api/Controllers/FoldersController.cs index 977d196b..5670a143 100644 --- a/src/Api/Controllers/FoldersController.cs +++ b/src/Api/Controllers/FoldersController.cs @@ -1,6 +1,8 @@ +using Api.Controllers.Payload.Requests; using Api.Controllers.Payload.Requests.Folders; using Application.Common.Interfaces; using Application.Common.Models; +using Application.Common.Models.Dtos.Logging; using Application.Common.Models.Dtos.Physical; using Application.Folders.Commands; using Application.Folders.Queries; @@ -183,4 +185,49 @@ public async Task>> Update([FromRoute] Guid folde var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } + + /// + /// + /// + /// + /// + [RequiresRole(IdentityData.Roles.Admin)] + [HttpGet("logs")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task>>> GetAllFolderLogs( + [FromQuery] GetAllLogsPaginatedQueryParameters queryParameters) + { + var query = new GetAllFolderLogsPaginated.Query() + { + SearchTerm = queryParameters.SearchTerm, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } + + /// + /// + /// + /// + /// + [RequiresRole(IdentityData.Roles.Admin)] + [HttpGet("log/{logId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetFolderLogById([FromRoute] Guid logId) + { + var query = new GetFolderLogById.Query() + { + LogId = logId + }; + + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Logging/FolderLogDto.cs b/src/Application/Common/Models/Dtos/Logging/FolderLogDto.cs new file mode 100644 index 00000000..67fe5e22 --- /dev/null +++ b/src/Application/Common/Models/Dtos/Logging/FolderLogDto.cs @@ -0,0 +1,25 @@ +using Application.Common.Mappings; +using Application.Common.Models.Dtos.Physical; +using Application.Users.Queries; +using AutoMapper; +using Domain.Entities.Logging; + +namespace Application.Common.Models.Dtos.Logging; + +public class FolderLogDto : IMapFrom +{ + public Guid Id { get; set; } + public string Action { get; set; } = null!; + public FolderDto? Object { get; set; } + public DateTime Time { get; set; } + public UserDto User { get; set; } = null!; + + public void Mapping(Profile profile) + { + profile.CreateMap() + .ForMember( dest => dest.Time, + opt => opt.MapFrom( src => src.Time.ToDateTimeUnspecified())) + .ForMember(dest => dest.Object, + opt => opt.MapFrom( src => src.Object)); + } +} \ No newline at end of file diff --git a/src/Application/Folders/Queries/GetAllFolderLogsPaginated.cs b/src/Application/Folders/Queries/GetAllFolderLogsPaginated.cs new file mode 100644 index 00000000..d7a3240e --- /dev/null +++ b/src/Application/Folders/Queries/GetAllFolderLogsPaginated.cs @@ -0,0 +1,66 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Logging; +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Folders.Queries; + +public class GetAllFolderLogsPaginated +{ + public record Query : IRequest> + { + public string? SearchTerm { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } + } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var logs = _context.FolderLogs + .Include(x => x.Object) + .AsQueryable(); + + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + logs = logs.Where(x => + x.Action.Trim().ToLower().Contains(request.SearchTerm.Trim().ToLower())); + } + + var sortBy = request.SortBy; + if (sortBy is null || !sortBy.MatchesPropertyName()) + { + sortBy = nameof(FolderLogDto.Time); + } + + var sortOrder = request.SortOrder ?? "dsc"; + var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; + var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + + var count = await logs.CountAsync(cancellationToken); + var list = await logs + .Paginate(pageNumber.Value, sizeNumber.Value) + .OrderByCustom(sortBy, sortOrder) + .ToListAsync(cancellationToken); + + var result = _mapper.Map>(list); + + return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + } + } +} \ No newline at end of file diff --git a/src/Application/Folders/Queries/GetFolderLogById.cs b/src/Application/Folders/Queries/GetFolderLogById.cs new file mode 100644 index 00000000..340ba6a3 --- /dev/null +++ b/src/Application/Folders/Queries/GetFolderLogById.cs @@ -0,0 +1,43 @@ +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Logging; +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Folders.Queries; + +public class GetFolderLogById +{ + public record Query : IRequest + { + public Guid LogId { get; init; } + } + + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var log = await _context.FolderLogs + .Include(x => x.Object) + .Include(x => x.User) + .ThenInclude(x => x.Department) + .FirstOrDefaultAsync(x => x.Id.Equals(request.LogId), cancellationToken); + + if (log is null) + { + throw new KeyNotFoundException("Log does not exist."); + } + + return _mapper.Map(log); + } + } +} \ No newline at end of file From e99f9c389be3689c8eb283d8890320bf2e2ea7fe Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Wed, 14 Jun 2023 19:17:21 +0700 Subject: [PATCH 31/56] Feat: implement get locker logs paginated + get locker log by id (#256) * feat: implement getting all logs and by Id for lockers * refactoring * refactoring * Update GetAllLockerLogsPaginated.cs --- src/Api/Controllers/LockersController.cs | 49 +++++++++++++- .../Models/Dtos/Logging/LockerLogDto.cs | 25 +++++++ .../Queries/GetAllLockerLogsPaginated.cs | 66 +++++++++++++++++++ .../Lockers/Queries/GetLockerLogById.cs | 44 +++++++++++++ 4 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 src/Application/Common/Models/Dtos/Logging/LockerLogDto.cs create mode 100644 src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs create mode 100644 src/Application/Lockers/Queries/GetLockerLogById.cs diff --git a/src/Api/Controllers/LockersController.cs b/src/Api/Controllers/LockersController.cs index 2799e670..5952c14e 100644 --- a/src/Api/Controllers/LockersController.cs +++ b/src/Api/Controllers/LockersController.cs @@ -1,6 +1,8 @@ -using Api.Controllers.Payload.Requests.Lockers; +using Api.Controllers.Payload.Requests; +using Api.Controllers.Payload.Requests.Lockers; using Application.Common.Interfaces; using Application.Common.Models; +using Application.Common.Models.Dtos.Logging; using Application.Common.Models.Dtos.Physical; using Application.Identity; using Application.Lockers.Commands; @@ -178,4 +180,49 @@ public async Task>> Update([FromRoute] Guid locke var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } + + /// + /// Get all logs related to locker. + /// + /// Query parameters + /// A list of LockerLogsDtos. + [RequiresRole(IdentityData.Roles.Admin)] + [HttpGet("logs")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task>>> GetAllLockerLogs( + [FromQuery] GetAllLogsPaginatedQueryParameters queryParameters) + { + var query = new GetAllLockerLogsPaginated.Query() + { + SearchTerm = queryParameters.SearchTerm, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } + + /// + /// Get a log related to locker by Id. + /// + /// Id of the requested log + /// A LockerLogDto of the requested log. + [RequiresRole(IdentityData.Roles.Admin)] + [HttpGet("log/{logId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetLockerLogById([FromRoute] Guid logId) + { + var query = new GetLockerLogById.Query() + { + LogId = logId + }; + + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } } diff --git a/src/Application/Common/Models/Dtos/Logging/LockerLogDto.cs b/src/Application/Common/Models/Dtos/Logging/LockerLogDto.cs new file mode 100644 index 00000000..b37cf09e --- /dev/null +++ b/src/Application/Common/Models/Dtos/Logging/LockerLogDto.cs @@ -0,0 +1,25 @@ +using Application.Common.Mappings; +using Application.Common.Models.Dtos.Physical; +using Application.Users.Queries; +using AutoMapper; +using Domain.Entities.Logging; + +namespace Application.Common.Models.Dtos.Logging; + +public class LockerLogDto : IMapFrom +{ + public Guid Id { get; set; } + public string Action { get; set; } = null!; + public LockerDto? Object { get; set; } + public DateTime Time { get; set; } + public UserDto User { get; set; } = null!; + + public void Mapping(Profile profile) + { + profile.CreateMap() + .ForMember( dest => dest.Time, + opt => opt.MapFrom( src => src.Time.ToDateTimeUnspecified())) + .ForMember(dest => dest.Object, + opt => opt.MapFrom( src => src.Object)); + } +} \ No newline at end of file diff --git a/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs b/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs new file mode 100644 index 00000000..475bdf84 --- /dev/null +++ b/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs @@ -0,0 +1,66 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Logging; +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Lockers.Queries; + +public class GetAllLockerLogsPaginated +{ + public record Query : IRequest> + { + public string? SearchTerm { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } + } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var logs = _context.LockerLogs + .Include(x => x.Object) + .AsQueryable(); + + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + logs = logs.Where(x => + x.Action.Trim().ToLower().Contains(request.SearchTerm.Trim().ToLower())); + } + + var sortBy = request.SortBy; + if (sortBy is null || !sortBy.MatchesPropertyName()) + { + sortBy = nameof(LockerLogDto.Time); + } + + var sortOrder = request.SortOrder ?? "dsc"; + var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; + var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + + var count = await logs.CountAsync(cancellationToken); + var list = await logs + .Paginate(pageNumber.Value, sizeNumber.Value) + .OrderByCustom(sortBy, sortOrder) + .ToListAsync(cancellationToken); + + var result = _mapper.Map>(list); + + return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + } + } +} diff --git a/src/Application/Lockers/Queries/GetLockerLogById.cs b/src/Application/Lockers/Queries/GetLockerLogById.cs new file mode 100644 index 00000000..33380268 --- /dev/null +++ b/src/Application/Lockers/Queries/GetLockerLogById.cs @@ -0,0 +1,44 @@ +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Logging; +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Lockers.Queries; + +public class GetLockerLogById +{ + public record Query : IRequest + { + public Guid LogId { get; init; } + } + + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var log = await _context.LockerLogs + .Include(x => x.Object) + .ThenInclude(x => x!.Room) + .Include(x => x.User) + .ThenInclude(x => x.Department) + .FirstOrDefaultAsync(x => x.Id.Equals(request.LogId), cancellationToken); + + if (log is null) + { + throw new KeyNotFoundException("Log does not exist."); + } + + return _mapper.Map(log); + } + } +} \ No newline at end of file From 58ae80ab11bb90ac14368008eaecd5848e830751 Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Wed, 14 Jun 2023 19:20:53 +0700 Subject: [PATCH 32/56] feat: implement get employees for the department the current user is in (#235) * feat: implement get employees for the department the current user is in * refactoring * Update GetAllEmployeesPaginated.cs * refactoring --------- Co-authored-by: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> --- ...GetAllEmployeesPaginatedQueryParameters.cs | 6 ++ src/Api/Controllers/UsersController.cs | 28 +++++++++ src/Api/Services/CurrentUserService.cs | 2 +- .../Users/Queries/GetAllEmployeesPaginated.cs | 62 +++++++++++++++++++ .../Identity/IdentityService.cs | 2 +- 5 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 src/Api/Controllers/Payload/Requests/Users/GetAllEmployeesPaginatedQueryParameters.cs create mode 100644 src/Application/Users/Queries/GetAllEmployeesPaginated.cs diff --git a/src/Api/Controllers/Payload/Requests/Users/GetAllEmployeesPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Users/GetAllEmployeesPaginatedQueryParameters.cs new file mode 100644 index 00000000..4a53d7f5 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Users/GetAllEmployeesPaginatedQueryParameters.cs @@ -0,0 +1,6 @@ +namespace Api.Controllers.Payload.Requests.Users; + +public class GetAllEmployeesPaginatedQueryParameters : PaginatedQueryParameters +{ + +} \ No newline at end of file diff --git a/src/Api/Controllers/UsersController.cs b/src/Api/Controllers/UsersController.cs index 795769bc..ac5ec81e 100644 --- a/src/Api/Controllers/UsersController.cs +++ b/src/Api/Controllers/UsersController.cs @@ -8,6 +8,7 @@ using Application.Users.Queries; using Infrastructure.Identity.Authorization; using Microsoft.AspNetCore.Mvc; +using Org.BouncyCastle.Security; namespace Api.Controllers; @@ -164,6 +165,33 @@ public async Task>> Update([FromRoute] Guid userId, } /// + /// Get all users with the "Employee" role of the current user's department. + /// + /// Query parameters + /// A list of UserDtos with the employee role of that department + [RequiresRole(IdentityData.Roles.Employee)] + [HttpGet("employees")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>>> GetAllEmployeesPaginated( + [FromQuery] GetAllEmployeesPaginatedQueryParameters queryParameters) + { + var performingUserDepartmentId = _currentUserService.GetDepartmentId(); + + if (performingUserDepartmentId is null) + { + throw new KeyNotFoundException("User does not belong to a department."); + } + + var query = new GetAllEmployeesPaginated.Query() + { + DepartmentId = performingUserDepartmentId.Value, + } + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } + /// Get all user related logs paginated /// /// Get all users related logs query parameters diff --git a/src/Api/Services/CurrentUserService.cs b/src/Api/Services/CurrentUserService.cs index 25c349de..b0b7d5ea 100644 --- a/src/Api/Services/CurrentUserService.cs +++ b/src/Api/Services/CurrentUserService.cs @@ -47,7 +47,7 @@ public string GetRole() var claim = _httpContextAccessor.HttpContext!.User.Claims .FirstOrDefault(x => x.Type.Equals("departmentId")); var id = claim?.Value; - return id is not null ? Guid.Parse(id) : null; + return id is not null && Guid.TryParse(id, out _) ? Guid.Parse(id) : null; } public User GetCurrentUser() diff --git a/src/Application/Users/Queries/GetAllEmployeesPaginated.cs b/src/Application/Users/Queries/GetAllEmployeesPaginated.cs new file mode 100644 index 00000000..d6189405 --- /dev/null +++ b/src/Application/Users/Queries/GetAllEmployeesPaginated.cs @@ -0,0 +1,62 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Identity; +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Users.Queries; + +public class GetAllEmployeesPaginated +{ + public record Query : IRequest> + { + public Guid DepartmentId { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } + } + + public class Handler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public Handler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var users = _context.Users.AsQueryable() + .Include(x => x.Department) + .Where(x => x.Department!.Id == request.DepartmentId + && x.Role.Equals(IdentityData.Roles.Employee) + && x.IsActive + && x.IsActivated); + + var sortBy = request.SortBy; + if (sortBy is null || !sortBy.MatchesPropertyName()) + { + sortBy = nameof(UserDto.Id); + } + var sortOrder = request.SortOrder ?? "asc"; + var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; + var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + + var count = await users.CountAsync(cancellationToken); + var list = await users + .Paginate(pageNumber.Value, sizeNumber.Value) + .OrderByCustom(sortBy, sortOrder) + .ToListAsync(cancellationToken); + + var result = _mapper.Map>(list); + + return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + } + } +} diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index f4b8e5b7..d4fa8552 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -296,7 +296,7 @@ private SecurityToken CreateJweToken(User user) new(JwtRegisteredClaimNames.Email, user.Email!), new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new(JwtRegisteredClaimNames.Iat, utcNow.ToString(CultureInfo.InvariantCulture)), - new("departmentId", user.Department!.Id.ToString()), + new("departmentId", user.Department is not null ? user.Department.Id.ToString() : String.Empty), new("isActive", user.IsActive.ToString()), }; var publicEncryptionKey = new RsaSecurityKey(_encryptionKey.ExportParameters(false)) {KeyId = _jweSettings.EncryptionKeyId}; From 77bdfc1ef9ff205bc14c88e376e5ceb580ce6c5e Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Wed, 14 Jun 2023 19:23:48 +0700 Subject: [PATCH 33/56] feat: get room log by ID and get room logs paginated (#241) * feat: get room log by ID and logs paginated * add: documentation for endpoints --------- Co-authored-by: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> --- src/Api/Controllers/RoomsController.cs | 46 +++++++++++++ .../Common/Models/Dtos/Logging/RoomLogDto.cs | 28 ++++++++ .../Rooms/Queries/GetAllRoomLogsPaginated.cs | 67 +++++++++++++++++++ .../Rooms/Queries/GetLogOfRoomById.cs | 43 ++++++++++++ 4 files changed, 184 insertions(+) create mode 100644 src/Application/Common/Models/Dtos/Logging/RoomLogDto.cs create mode 100644 src/Application/Rooms/Queries/GetAllRoomLogsPaginated.cs create mode 100644 src/Application/Rooms/Queries/GetLogOfRoomById.cs diff --git a/src/Api/Controllers/RoomsController.cs b/src/Api/Controllers/RoomsController.cs index 53f111af..fc001505 100644 --- a/src/Api/Controllers/RoomsController.cs +++ b/src/Api/Controllers/RoomsController.cs @@ -1,7 +1,9 @@ +using Api.Controllers.Payload.Requests; using Api.Controllers.Payload.Requests.Lockers; using Api.Controllers.Payload.Requests.Rooms; using Application.Common.Interfaces; using Application.Common.Models; +using Application.Common.Models.Dtos.Logging; using Application.Common.Models.Dtos.Physical; using Application.Identity; using Application.Rooms.Commands; @@ -63,6 +65,50 @@ public async Task>>> GetAllPaginated( return Ok(Result>.Succeed(result)); } + /// + /// Get a room log by id + /// + /// + /// return a RoomLogDto + [RequiresRole(IdentityData.Roles.Admin)] + [HttpGet("log/{logId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetLogById([FromRoute] Guid logId) + { + var query = new GetLogOfRoomById.Query() + { + LogId = logId + }; + + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } + + /// + /// Get all room logs paginated + /// + /// + /// A paginated list of RoomLogDto + [RequiresRole(IdentityData.Roles.Admin)] + [HttpGet("logs")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>>> GetAllLogsPaginated( + [FromQuery] GetAllLogsPaginatedQueryParameters queryParameters) + { + var query = new GetAllRoomLogsPaginated.Query() + { + SearchTerm = queryParameters.SearchTerm, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + }; + + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } + /// /// Get empty containers in a room /// diff --git a/src/Application/Common/Models/Dtos/Logging/RoomLogDto.cs b/src/Application/Common/Models/Dtos/Logging/RoomLogDto.cs new file mode 100644 index 00000000..079c87ef --- /dev/null +++ b/src/Application/Common/Models/Dtos/Logging/RoomLogDto.cs @@ -0,0 +1,28 @@ +using Application.Common.Mappings; +using Application.Common.Models.Dtos.Physical; +using Application.Users.Queries; +using AutoMapper; +using Domain.Entities.Logging; + +namespace Application.Common.Models.Dtos.Logging; + +public class RoomLogDto : IMapFrom +{ + public Guid Id { get; set; } + public Guid UserId { get; set; } + public string Action { get; set; } + public RoomDto? Object { get; set; } + public DateTime Time { get; set; } + public UserDto User { get; set; } + + public void Mapping(Profile profile) + { + + profile.CreateMap() + .ForMember(dest => dest.Time, + opt => opt.MapFrom(src => src.Time.ToDateTimeUnspecified())) + .ForMember(dest => dest.Object, + opt => opt.MapFrom(src => src.Object)); + + } +} \ No newline at end of file diff --git a/src/Application/Rooms/Queries/GetAllRoomLogsPaginated.cs b/src/Application/Rooms/Queries/GetAllRoomLogsPaginated.cs new file mode 100644 index 00000000..1099f5ba --- /dev/null +++ b/src/Application/Rooms/Queries/GetAllRoomLogsPaginated.cs @@ -0,0 +1,67 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Logging; +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Rooms.Queries; + +public class GetAllRoomLogsPaginated +{ + public record Query : IRequest> + { + public string? SearchTerm { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } + } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var logs = _context.RoomLogs + .Include(x => x.Object) + .Include(x => x.User) + .ThenInclude(x => x.Department) + .AsQueryable(); + + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + logs = logs.Where(x => + x.Action.ToLower().Contains(request.SearchTerm.ToLower())); + } + + var sortBy = request.SortBy; + if (sortBy is null || !sortBy.MatchesPropertyName()) + { + sortBy = nameof(RoomLogDto.Time); + } + var sortOrder = request.SortOrder ?? "desc"; + var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; + var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + + var count = await logs.CountAsync(cancellationToken); + var list = await logs + .OrderByCustom(sortBy, sortOrder) + .Paginate(pageNumber.Value, sizeNumber.Value) + .ToListAsync(cancellationToken); + + var result = _mapper.Map>(list); + + return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + } + } +} \ No newline at end of file diff --git a/src/Application/Rooms/Queries/GetLogOfRoomById.cs b/src/Application/Rooms/Queries/GetLogOfRoomById.cs new file mode 100644 index 00000000..cee96966 --- /dev/null +++ b/src/Application/Rooms/Queries/GetLogOfRoomById.cs @@ -0,0 +1,43 @@ +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Logging; +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Rooms.Queries; + +public class GetLogOfRoomById +{ + public record Query : IRequest + { + public Guid LogId { get; init; } + } + + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var log = await _context.RoomLogs + .Include(x => x.Object) + .Include(x => x.User) + .ThenInclude(x => x.Department) + .FirstOrDefaultAsync(x => x.Id.Equals(request.LogId), cancellationToken); + + if (log is null) + { + throw new KeyNotFoundException("Log does not exist."); + } + + return _mapper.Map(log); + } + } +} \ No newline at end of file From 39755ee19b29c7c86115c2b07e9764b79adaccc1 Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Wed, 14 Jun 2023 19:36:00 +0700 Subject: [PATCH 34/56] feat: implement get documents of a user (#240) * feat: implement get documents of a user * refactoring --- src/Api/Controllers/DocumentsController.cs | 26 +++++++ ...DocumentsOfUserPaginatedQueryParameters.cs | 6 ++ .../Queries/GetDocumentsOfUserPaginated.cs | 70 +++++++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 src/Api/Controllers/Payload/Requests/Documents/GetDocumentsOfUserPaginatedQueryParameters.cs create mode 100644 src/Application/Documents/Queries/GetDocumentsOfUserPaginated.cs diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index 57aea634..75a00abc 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -314,6 +314,32 @@ public async Task>> Delete([FromRoute] Guid doc var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); } + + /// + /// Get all documents of a user. + /// + /// Id of the user + /// Query parameters + /// A list of DocumentDtos of the user. + [RequiresRole(IdentityData.Roles.Employee)] + [HttpGet("user/{userId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetDocumentsOfUserPaginated([FromRoute] Guid userId, + [FromQuery] GetDocumentsOfUserPaginatedQueryParameters queryParameters) + { + var query = new GetDocumentsOfUserPaginated.Query() + { + UserId = userId, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } /// /// Approve a document request diff --git a/src/Api/Controllers/Payload/Requests/Documents/GetDocumentsOfUserPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Documents/GetDocumentsOfUserPaginatedQueryParameters.cs new file mode 100644 index 00000000..a09e0eee --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Documents/GetDocumentsOfUserPaginatedQueryParameters.cs @@ -0,0 +1,6 @@ +namespace Api.Controllers.Payload.Requests.Documents; + +public class GetDocumentsOfUserPaginatedQueryParameters : PaginatedQueryParameters +{ + +} \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetDocumentsOfUserPaginated.cs b/src/Application/Documents/Queries/GetDocumentsOfUserPaginated.cs new file mode 100644 index 00000000..1f14d615 --- /dev/null +++ b/src/Application/Documents/Queries/GetDocumentsOfUserPaginated.cs @@ -0,0 +1,70 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Documents.Queries; + +public class GetDocumentsOfUserPaginated +{ + public record Query : IRequest> + { + public Guid UserId { get; set; } + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } + } + + public class Handler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public Handler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var user = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.UserId + && x.IsActive + && x.IsActivated, cancellationToken); + + if (user is null) + { + throw new KeyNotFoundException("User does not exist."); + } + + var documents = _context.Documents + .Include(x => x.Department) + .Include(x => x.Folder) + .AsQueryable() + .Where(x => x.Importer!.Id.Equals(request.UserId) && !x.IsPrivate); + + var sortBy = request.SortBy; + if (sortBy is null || !sortBy.MatchesPropertyName()) + { + sortBy = nameof(DocumentDto.Id); + } + var sortOrder = request.SortOrder ?? "asc"; + var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; + var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + + var count = await documents.CountAsync(cancellationToken); + var list = await documents + .Paginate(pageNumber.Value, sizeNumber.Value) + .OrderByCustom(sortBy, sortOrder) + .ToListAsync(cancellationToken); + + var result = _mapper.Map>(list); + + return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + } + } +} \ No newline at end of file From 127646f7510cc6003bbc3396878985c44230537f Mon Sep 17 00:00:00 2001 From: StarryFolf <67864500+StarryFolf@users.noreply.github.com> Date: Wed, 14 Jun 2023 19:36:30 +0700 Subject: [PATCH 35/56] feat: implement get request logs paginated + get request log by id (#257) * feat: implement get borrow request logs paginated + get borrow request log by id * added borrow type filter * refactoring * Update GetAllRequestLogsPaginated.cs --- src/Api/Controllers/BorrowsController.cs | 47 +++++++++++++ .../Queries/GetAllRequestLogsPaginated.cs | 67 +++++++++++++++++++ .../Borrows/Queries/GetRequestLogById.cs | 48 +++++++++++++ .../Models/Dtos/Logging/RequestLogDto.cs | 29 ++++++++ 4 files changed, 191 insertions(+) create mode 100644 src/Application/Borrows/Queries/GetAllRequestLogsPaginated.cs create mode 100644 src/Application/Borrows/Queries/GetRequestLogById.cs create mode 100644 src/Application/Common/Models/Dtos/Logging/RequestLogDto.cs diff --git a/src/Api/Controllers/BorrowsController.cs b/src/Api/Controllers/BorrowsController.cs index 484c3949..d6199594 100644 --- a/src/Api/Controllers/BorrowsController.cs +++ b/src/Api/Controllers/BorrowsController.cs @@ -1,8 +1,10 @@ +using Api.Controllers.Payload.Requests; using Api.Controllers.Payload.Requests.Borrows; using Application.Borrows.Commands; using Application.Borrows.Queries; using Application.Common.Interfaces; using Application.Common.Models; +using Application.Common.Models.Dtos.Logging; using Application.Common.Models.Dtos.Physical; using Application.Identity; using Infrastructure.Identity.Authorization; @@ -305,4 +307,49 @@ public async Task>> Cancel([FromRoute] Guid borro var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } + + /// + /// Get all logs related to requests. + /// + /// Query parameters + /// A list of RequestLogsDtos. + [RequiresRole(IdentityData.Roles.Admin)] + [HttpGet("logs")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task>>> GetAllRequestLogs( + [FromQuery] GetAllLogsPaginatedQueryParameters queryParameters) + { + var query = new GetAllRequestLogsPaginated.Query() + { + SearchTerm = queryParameters.SearchTerm, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } + + /// + /// Get a log related to request by Id. + /// + /// Id of the requested log + /// A LockerLogDto of the requested log. + [RequiresRole(IdentityData.Roles.Admin)] + [HttpGet("log/{logId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetRequestLogById([FromRoute] Guid logId) + { + var query = new GetRequestLogById.Query() + { + LogId = logId + }; + + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } } \ No newline at end of file diff --git a/src/Application/Borrows/Queries/GetAllRequestLogsPaginated.cs b/src/Application/Borrows/Queries/GetAllRequestLogsPaginated.cs new file mode 100644 index 00000000..7a2002d1 --- /dev/null +++ b/src/Application/Borrows/Queries/GetAllRequestLogsPaginated.cs @@ -0,0 +1,67 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Logging; +using AutoMapper; +using Domain.Enums; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Borrows.Queries; + +public class GetAllRequestLogsPaginated +{ + public record Query : IRequest> + { + public string? SearchTerm { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } + } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var logs = _context.RequestLogs + .Include(x => x.Object) + .AsQueryable(); + + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + logs = logs.Where(x => + x.Action.Trim().ToLower().Contains(request.SearchTerm.Trim().ToLower())); + } + + var sortBy = request.SortBy; + if (sortBy is null || !sortBy.MatchesPropertyName()) + { + sortBy = nameof(RequestLogDto.Time); + } + + var sortOrder = request.SortOrder ?? "desc"; + var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; + var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + + var count = await logs.CountAsync(cancellationToken); + var list = await logs + .Paginate(pageNumber.Value, sizeNumber.Value) + .OrderByCustom(sortBy, sortOrder) + .ToListAsync(cancellationToken); + + var result = _mapper.Map>(list); + + return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + } + } +} diff --git a/src/Application/Borrows/Queries/GetRequestLogById.cs b/src/Application/Borrows/Queries/GetRequestLogById.cs new file mode 100644 index 00000000..4d53f316 --- /dev/null +++ b/src/Application/Borrows/Queries/GetRequestLogById.cs @@ -0,0 +1,48 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Logging; +using AutoMapper; +using Domain.Enums; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Borrows.Queries; + +public class GetRequestLogById +{ + public record Query : IRequest + { + public Guid LogId { get; init; } + } + + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var log = await _context.RequestLogs + .Include(x => x.Object) + .ThenInclude(x => x!.Importer) + .Include(x => x.Object) + .ThenInclude(x => x!.Folder) + .Include(x => x.User) + .ThenInclude(x => x.Department) + .FirstOrDefaultAsync(x => x.Id.Equals(request.LogId), cancellationToken); + + if (log is null) + { + throw new KeyNotFoundException("Log does not exist."); + } + + return _mapper.Map(log); + } + } +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Logging/RequestLogDto.cs b/src/Application/Common/Models/Dtos/Logging/RequestLogDto.cs new file mode 100644 index 00000000..a291c228 --- /dev/null +++ b/src/Application/Common/Models/Dtos/Logging/RequestLogDto.cs @@ -0,0 +1,29 @@ +using Application.Common.Mappings; +using Application.Common.Models.Dtos.Physical; +using Application.Users.Queries; +using AutoMapper; +using Domain.Entities.Logging; + +namespace Application.Common.Models.Dtos.Logging; + +public class RequestLogDto : IMapFrom +{ + public Guid Id { get; set; } + public string Action { get; set; } = null!; + public DocumentDto? Object { get; set; } + public DateTime Time { get; set; } + public UserDto User { get; set; } = null!; + public string Reason { get; set; } = null!; + public string Type { get; set; } = null!; + + public void Mapping(Profile profile) + { + profile.CreateMap() + .ForMember(dest => dest.Time, + opt => opt.MapFrom(src => src.Time.ToDateTimeUnspecified())) + .ForMember(dest => dest.Object, + opt => opt.MapFrom(src => src.Object)) + .ForMember(dest => dest.Type, + opt => opt.MapFrom(src => src.Type.ToString())); + } +} \ No newline at end of file From 94cbe94f0f90823ed679501a1413f9ede8b7560d Mon Sep 17 00:00:00 2001 From: Vzart <85790072+Vzart@users.noreply.github.com> Date: Wed, 14 Jun 2023 19:37:40 +0700 Subject: [PATCH 36/56] feat: get self documents (#238) * feat: get self documents * fix: minor stuff --------- Co-authored-by: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> --- src/Api/Controllers/DocumentsController.cs | 27 ++++++++ ...etSelfDocumentsPaginatedQueryParameters.cs | 9 +++ .../Queries/GetSelfDocumentsPaginated.cs | 68 +++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 src/Api/Controllers/Payload/Requests/Documents/GetSelfDocumentsPaginatedQueryParameters.cs create mode 100644 src/Application/Documents/Queries/GetSelfDocumentsPaginated.cs diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index 75a00abc..cffc233d 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -102,6 +102,7 @@ public async Task>> GetLogById([FromRoute] G /// A paginated list of DocumentDto [RequiresRole(IdentityData.Roles.Admin)] [HttpGet] + [RequiresRole(IdentityData.Roles.Admin)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -125,6 +126,32 @@ public async Task>>> GetAllForAdm } /// + /// Get documents of the employee + /// + /// + /// + [HttpGet("get-self-documents")] + [RequiresRole(IdentityData.Roles.Employee)] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>>> GetSelfPaginated( + [FromQuery] GetSelfDocumentsPaginatedQueryParameters queryParameters) + { + var userId = _currentUserService.GetId(); + + var query = new GetSelfDocumentsPaginated.Query() + { + EmployeeId = userId, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SearchTerm = queryParameters.SearchTerm, + SortOrder = queryParameters.SortOrder + }; + + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } + /// Get all log of document /// /// diff --git a/src/Api/Controllers/Payload/Requests/Documents/GetSelfDocumentsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Documents/GetSelfDocumentsPaginatedQueryParameters.cs new file mode 100644 index 00000000..3f7d6472 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Documents/GetSelfDocumentsPaginatedQueryParameters.cs @@ -0,0 +1,9 @@ +namespace Api.Controllers.Payload.Requests.Documents; + +/// +/// Query parameters for getting all documents that belong to an employee +/// +public class GetSelfDocumentsPaginatedQueryParameters : PaginatedQueryParameters +{ + public string? SearchTerm { get; set; } +} \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetSelfDocumentsPaginated.cs b/src/Application/Documents/Queries/GetSelfDocumentsPaginated.cs new file mode 100644 index 00000000..ceebbed1 --- /dev/null +++ b/src/Application/Documents/Queries/GetSelfDocumentsPaginated.cs @@ -0,0 +1,68 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Documents.Queries; + +public class GetSelfDocumentsPaginated +{ + public record Query : IRequest> + { + public Guid EmployeeId { get; init; } + public string? SearchTerm { get; set; } + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var documents = _context.Documents.AsQueryable(); + + documents = documents + .Include(x => x.Department) + .Where(x => x.Importer!.Id.Equals(request.EmployeeId)); + + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + documents = documents.Where(x => + x.Title.ToLower().Contains(request.SearchTerm.ToLower())); + } + + var sortBy = request.SortBy; + if (sortBy is null || !sortBy.MatchesPropertyName()) + { + sortBy = nameof(DocumentDto.Id); + } + var sortOrder = request.SortOrder ?? "asc"; + var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; + var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + + var count = await documents.CountAsync(cancellationToken); + var list = await documents + .OrderByCustom(sortBy, sortOrder) + .Paginate(pageNumber.Value, sizeNumber.Value) + .ToListAsync(cancellationToken); + + var result = _mapper.Map>(list); + + return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + } + } + } +} \ No newline at end of file From a2e6e0149338a8cf75b08a775c9f675ac345d7ad Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Wed, 14 Jun 2023 19:38:26 +0700 Subject: [PATCH 37/56] adsfads --- src/Api/Controllers/DocumentsController.cs | 31 +++++++---- ...cumentsForStaffPaginatedQueryParameters.cs | 4 -- .../Borrows/Commands/BorrowDocument.cs | 11 +++- .../Documents/Queries/GetDocumentById.cs | 38 ++++++++++++-- .../Documents/Queries/GetDocumentReason.cs | 52 +++++++++++++++++-- .../Documents/Queries/GetPermissions.cs | 33 ++++++------ 6 files changed, 127 insertions(+), 42 deletions(-) diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index 181eda05..f200e432 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -1,9 +1,11 @@ using Api.Controllers.Payload.Requests.Documents; +using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Models; using Application.Common.Models.Dtos; using Application.Common.Models.Dtos.ImportDocument; using Application.Common.Models.Dtos.Physical; +using Application.Common.Models.Operations; using Application.Documents.Commands; using Application.Documents.Queries; using Application.Identity; @@ -16,10 +18,12 @@ namespace Api.Controllers; public class DocumentsController : ApiControllerBase { private readonly ICurrentUserService _currentUserService; + private readonly IPermissionManager _permissionManager; - public DocumentsController(ICurrentUserService currentUserService) + public DocumentsController(ICurrentUserService currentUserService, IPermissionManager permissionManager) { _currentUserService = currentUserService; + _permissionManager = permissionManager; } /// @@ -56,13 +60,9 @@ public async Task>>> GetAll [FromQuery] GetAllIssuedPaginatedQueryParameters queryParameters) { var departmentId = _currentUserService.GetCurrentDepartmentForStaff(); - if (departmentId is null) - { - return Result>.Fail(new Exception("Staff does not have a room")); - } var query = new GetAllIssuedDocumentsPaginated.Query() { - DepartmentId = departmentId.Value, + DepartmentId = departmentId!.Value, SearchTerm = queryParameters.SearchTerm, Page = queryParameters.Page, Size = queryParameters.Size, @@ -238,7 +238,9 @@ public async Task>> Checkin( [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Update([FromRoute] Guid documentId, [FromBody] UpdateDocumentRequest request) + public async Task>> Update( + [FromRoute] Guid documentId, + [FromBody] UpdateDocumentRequest request) { var query = new UpdateDocument.Command() { @@ -256,11 +258,13 @@ public async Task>> Update([FromRoute] Guid doc /// /// Id of the document to be deleted /// A DocumentDto of the deleted document + [HttpDelete("{documentId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> Delete([FromRoute] Guid documentId) + public async Task>> Delete( + [FromRoute] Guid documentId) { var query = new DeleteDocument.Command() { @@ -276,6 +280,7 @@ public async Task>> Delete([FromRoute] Guid doc /// Id of the document to be approved /// /// A DocumentDto of the approved document + [RequiresRole(IdentityData.Roles.Staff)] [HttpPost("approve/{documentId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -301,6 +306,7 @@ public async Task>> Approve( /// Id of the document to be rejected /// /// A DocumentDto of the rejected document + [RequiresRole(IdentityData.Roles.Staff)] [HttpPost("reject/{documentId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -325,7 +331,7 @@ public async Task>> Reject( /// /// Id of the document to be rejected /// A DocumentDto of the rejected document - [RequiresRole(IdentityData.Roles.Staff)] + [RequiresRole(IdentityData.Roles.Staff, IdentityData.Roles.Employee)] [HttpPost("reason/{documentId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -348,6 +354,7 @@ public async Task>> Reason( /// Id of the document to be rejected /// /// A DocumentDto of the rejected document + [RequiresRole(IdentityData.Roles.Staff)] [HttpPost("assign/{documentId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -373,6 +380,7 @@ public async Task>> Assign( /// Id of the document /// /// A DocumentDto + [RequiresRole(IdentityData.Roles.Employee)] [HttpPost("{documentId:guid}/permissions")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -400,6 +408,7 @@ public async Task>> SharePermissions( /// /// Id of the document to be getting permissions from /// A DocumentDto of the rejected document + [RequiresRole(IdentityData.Roles.Employee)] [HttpGet("{documentId:guid}/permissions")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -407,10 +416,10 @@ public async Task>> SharePermissions( public async Task>> GetPermissions( [FromRoute] Guid documentId) { - var performingUserId = _currentUserService.GetId(); + var performingUser = _currentUserService.GetCurrentUser(); var query = new GetPermissions.Query() { - PerformingUserId = performingUserId, + PerformingUser = performingUser, DocumentId = documentId, }; var result = await Mediator.Send(query); diff --git a/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForStaffPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForStaffPaginatedQueryParameters.cs index 7860021d..375a6597 100644 --- a/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForStaffPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForStaffPaginatedQueryParameters.cs @@ -2,10 +2,6 @@ namespace Api.Controllers.Payload.Requests.Documents; public class GetAllDocumentsForStaffPaginatedQueryParameters : PaginatedQueryParameters { - /// - /// Id of the room to find documents in - /// - public Guid? RoomId { get; set; } /// /// Id of the locker to find documents in /// diff --git a/src/Application/Borrows/Commands/BorrowDocument.cs b/src/Application/Borrows/Commands/BorrowDocument.cs index 4596f88e..5a463cf2 100644 --- a/src/Application/Borrows/Commands/BorrowDocument.cs +++ b/src/Application/Borrows/Commands/BorrowDocument.cs @@ -2,6 +2,7 @@ using Application.Common.Interfaces; using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; +using Application.Common.Models.Operations; using AutoMapper; using Domain.Entities.Logging; using Domain.Entities.Physical; @@ -47,11 +48,13 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IPermissionManager _permissionManager; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IPermissionManager permissionManager) { _context = context; _mapper = mapper; + _permissionManager = permissionManager; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -92,6 +95,12 @@ public async Task Handle(Command request, CancellationToken cancellat { throw new ConflictException("User is not allowed to borrow this document."); } + + var isGranted = _permissionManager.IsGranted(request.DocumentId, DocumentOperation.Borrow, request.BorrowerId); + if (!isGranted) + { + throw new UnauthorizedAccessException("You don't have permission to borrow this document."); + } // getting out a request of that document which is either not due or overdue // if the request is in time, meaning not overdue, diff --git a/src/Application/Documents/Queries/GetDocumentById.cs b/src/Application/Documents/Queries/GetDocumentById.cs index bf7c29d7..054bf40f 100644 --- a/src/Application/Documents/Queries/GetDocumentById.cs +++ b/src/Application/Documents/Queries/GetDocumentById.cs @@ -1,5 +1,8 @@ +using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; +using Application.Common.Models.Operations; +using Application.Identity; using AutoMapper; using MediatR; using Microsoft.EntityFrameworkCore; @@ -17,27 +20,52 @@ public class QueryHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly ICurrentUserService _currentUserService; + private readonly IPermissionManager _permissionManager; - public QueryHandler(IApplicationDbContext context, IMapper mapper) + public QueryHandler(IApplicationDbContext context, IMapper mapper, ICurrentUserService currentUserService, IPermissionManager permissionManager) { _context = context; _mapper = mapper; + _currentUserService = currentUserService; + _permissionManager = permissionManager; } public async Task Handle(Query request, CancellationToken cancellationToken) { var document = await _context.Documents .Include(x => x.Department) .Include(x => x.Importer) - .Include(x => x.Folder) - .ThenInclude(y => y.Locker) - .ThenInclude(z => z.Room) .FirstOrDefaultAsync(x => x.Id == request.DocumentId, cancellationToken); - + if (document is null) { throw new KeyNotFoundException("Document does not exist."); } + + var performingUser = _currentUserService.GetCurrentUser(); + + if (performingUser.Role.Equals(IdentityData.Roles.Admin)) + { + return _mapper.Map(document); + } + + if (performingUser.Role.Equals(IdentityData.Roles.Staff)) + { + var departmentIdOfStaff = _currentUserService.GetCurrentDepartmentForStaff(); + + if (departmentIdOfStaff!.Value != document.Department!.Id) + { + throw new ConflictException("You don't have permission to view this document."); + } + return _mapper.Map(document); + } + var isGranted = _permissionManager.IsGranted(document.Id, DocumentOperation.Read, performingUser.Id); + if (!isGranted) + { + throw new UnauthorizedAccessException("You don't have permission to view this document."); + } + return _mapper.Map(document); } } diff --git a/src/Application/Documents/Queries/GetDocumentReason.cs b/src/Application/Documents/Queries/GetDocumentReason.cs index 9088f785..fc517fdf 100644 --- a/src/Application/Documents/Queries/GetDocumentReason.cs +++ b/src/Application/Documents/Queries/GetDocumentReason.cs @@ -1,7 +1,11 @@ +using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Models.Dtos; using Application.Common.Models.Dtos.Physical; +using Application.Identity; using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; using Domain.Enums; using MediatR; using Microsoft.EntityFrameworkCore; @@ -12,32 +16,74 @@ public class GetDocumentReason { public record Query : IRequest { + public User User { get; set; } public Guid DocumentId { get; init; } - public RequestType Type { get; set; } + public RequestType Type { get; init; } } public class QueryHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly ICurrentUserService _currentUserService; - public QueryHandler(IApplicationDbContext context, IMapper mapper) + public QueryHandler(IApplicationDbContext context, IMapper mapper, ICurrentUserService currentUserService) { _context = context; _mapper = mapper; + _currentUserService = currentUserService; } + public async Task Handle(Query request, CancellationToken cancellationToken) { var log = await _context.RequestLogs + .Include(x => x.Object) + .ThenInclude(y => y.Department) + .Include(x => x.Object) + .ThenInclude(y => y.Importer) .FirstOrDefaultAsync(x => x.Object!.Id == request.DocumentId - && x.Type == request.Type, cancellationToken); + && x.Type == request.Type, cancellationToken); if (log is null) { throw new KeyNotFoundException("Document does not have a request."); } + + await EnforceRoleConstraintsAsync(_currentUserService.GetCurrentUser(), log); return _mapper.Map(log); } + + private async Task EnforceRoleConstraintsAsync(User user, RequestLog log) + { + var role = user.Role; + + switch (role) + { + case IdentityData.Roles.Staff when log.Object!.Department!.Id != user.Department!.Id: + throw new ConflictException("Staff cannot access this request."); + case IdentityData.Roles.Employee when log.Type == RequestType.Import: + { + if (log.Object!.Importer!.Id != user.Id) + { + throw new ConflictException("User cannot access this request."); + } + + break; + } + case IdentityData.Roles.Employee: + { + var borrow = await _context.Borrows.FirstOrDefaultAsync(x => + x.Borrower.Id == user.Id && x.Document.Id == log.Object!.Id); + + if (borrow is null) + { + throw new ConflictException("User cannot access this request."); + } + + break; + } + } + } } } \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetPermissions.cs b/src/Application/Documents/Queries/GetPermissions.cs index 6f560a7a..ed9588a2 100644 --- a/src/Application/Documents/Queries/GetPermissions.cs +++ b/src/Application/Documents/Queries/GetPermissions.cs @@ -1,8 +1,11 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; +using Application.Identity; using Application.Users.Queries; using AutoMapper; +using Domain.Entities; +using Domain.Entities.Physical; using MediatR; using Microsoft.EntityFrameworkCore; @@ -12,10 +15,10 @@ public class GetPermissions { public record Query : IRequest { - public Guid PerformingUserId { get; init; } + public User PerformingUser { get; init; } = null!; public Guid DocumentId { get; init; } } - + public class QueryHandler : IRequestHandler { private readonly IApplicationDbContext _context; @@ -29,16 +32,7 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task Handle(Query request, CancellationToken cancellationToken) { - var performingUser = - await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); - - if (performingUser is null) - { - throw new UnauthorizedAccessException(); - } - var document = await _context.Documents - .Include(x => x.Folder) .Include(x => x.Importer) .FirstOrDefaultAsync(x => x.Id == request.DocumentId, cancellationToken); if (document is null) @@ -46,21 +40,19 @@ public async Task Handle(Query request, CancellationToken cancell throw new ConflictException("Document does not exist."); } - if (document.Importer!.Id == request.PerformingUserId) + if (IsOwner(request.PerformingUser.Id, document)) { - var result = new PermissionDto() + return new PermissionDto() { DocumentId = document.Id, - EmployeeId = performingUser.Id, + EmployeeId = request.PerformingUser.Id, CanRead = true, CanBorrow = true, }; - - return result; } var permission = await _context.Permissions.FirstOrDefaultAsync( - x => x.DocumentId == request.DocumentId && x.EmployeeId == request.PerformingUserId, + x => x.DocumentId == request.DocumentId && x.EmployeeId == request.PerformingUser.Id, cancellationToken); if (permission is null) @@ -68,7 +60,7 @@ public async Task Handle(Query request, CancellationToken cancell return new PermissionDto() { DocumentId = document.Id, - EmployeeId = performingUser.Id, + EmployeeId = request.PerformingUser.Id, CanRead = false, CanBorrow = false, }; @@ -76,5 +68,10 @@ public async Task Handle(Query request, CancellationToken cancell return _mapper.Map(permission); } + + private static bool IsOwner(Guid userId, Document document) + { + return document.Importer!.Id == userId; + } } } \ No newline at end of file From b361ebae668599624ab49f3cc0471e3902a1b1ac Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Wed, 14 Jun 2023 20:13:45 +0700 Subject: [PATCH 38/56] fuck me --- src/Api/Controllers/BorrowsController.cs | 2 -- src/Api/Controllers/DocumentsController.cs | 3 --- src/Api/Controllers/FoldersController.cs | 2 -- src/Api/Controllers/LockersController.cs | 2 -- .../GetAllLogsPaginatedQueryParameters.cs | 16 ++++++++++++---- src/Api/Controllers/RoomsController.cs | 2 -- src/Api/Controllers/UsersController.cs | 7 +++---- .../Queries/GetAllRequestLogsPaginated.cs | 11 +---------- .../Queries/GetAllDocumentLogsPaginated.cs | 10 +--------- .../Folders/Queries/GetAllFolderLogsPaginated.cs | 13 ++----------- .../Lockers/Queries/GetAllLockerLogsPaginated.cs | 13 ++----------- .../Users/Queries/GetAllEmployeesPaginated.cs | 2 +- .../Users/Queries/GetAllUserLogsPaginated.cs | 11 +---------- .../Users/Queries/GetAllUsersPaginated.cs | 2 +- src/Infrastructure/Identity/IdentityService.cs | 2 +- 15 files changed, 25 insertions(+), 73 deletions(-) diff --git a/src/Api/Controllers/BorrowsController.cs b/src/Api/Controllers/BorrowsController.cs index d6199594..5bfa8772 100644 --- a/src/Api/Controllers/BorrowsController.cs +++ b/src/Api/Controllers/BorrowsController.cs @@ -325,8 +325,6 @@ public async Task>>> GetAllRequ SearchTerm = queryParameters.SearchTerm, Page = queryParameters.Page, Size = queryParameters.Size, - SortBy = queryParameters.SortBy, - SortOrder = queryParameters.SortOrder, }; var result = await Mediator.Send(query); return Ok(Result>.Succeed(result)); diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index cffc233d..859a35a4 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -102,7 +102,6 @@ public async Task>> GetLogById([FromRoute] G /// A paginated list of DocumentDto [RequiresRole(IdentityData.Roles.Admin)] [HttpGet] - [RequiresRole(IdentityData.Roles.Admin)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -167,8 +166,6 @@ public async Task>>> GetAllLog SearchTerm = queryParameters.SearchTerm, Page = queryParameters.Page, Size = queryParameters.Size, - SortBy = queryParameters.SortBy, - SortOrder = queryParameters.SortOrder, }; var result = await Mediator.Send(query); return Ok(Result>.Succeed(result)); diff --git a/src/Api/Controllers/FoldersController.cs b/src/Api/Controllers/FoldersController.cs index 5670a143..6d235599 100644 --- a/src/Api/Controllers/FoldersController.cs +++ b/src/Api/Controllers/FoldersController.cs @@ -203,8 +203,6 @@ public async Task>>> GetAllFolde SearchTerm = queryParameters.SearchTerm, Page = queryParameters.Page, Size = queryParameters.Size, - SortBy = queryParameters.SortBy, - SortOrder = queryParameters.SortOrder, }; var result = await Mediator.Send(query); return Ok(Result>.Succeed(result)); diff --git a/src/Api/Controllers/LockersController.cs b/src/Api/Controllers/LockersController.cs index 5952c14e..16a40300 100644 --- a/src/Api/Controllers/LockersController.cs +++ b/src/Api/Controllers/LockersController.cs @@ -198,8 +198,6 @@ public async Task>>> GetAllLocke SearchTerm = queryParameters.SearchTerm, Page = queryParameters.Page, Size = queryParameters.Size, - SortBy = queryParameters.SortBy, - SortOrder = queryParameters.SortOrder, }; var result = await Mediator.Send(query); return Ok(Result>.Succeed(result)); diff --git a/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs index a4464a98..7eceac63 100644 --- a/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs @@ -1,9 +1,17 @@ -namespace Api.Controllers.Payload.Requests; +namespace Api.Controllers.Payload.Requests; /// -/// get all logs paginated +/// Get all logs paginated /// -public class GetAllLogsPaginatedQueryParameters : PaginatedQueryParameters +public class GetAllLogsPaginatedQueryParameters { - public string? SearchTerm { get; set; } + public string? SearchTerm { get; set; } + /// + /// Page number + /// + public int? Page { get; set; } + /// + /// Size number + /// + public int? Size { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/RoomsController.cs b/src/Api/Controllers/RoomsController.cs index fc001505..efc664bc 100644 --- a/src/Api/Controllers/RoomsController.cs +++ b/src/Api/Controllers/RoomsController.cs @@ -101,8 +101,6 @@ public async Task>>> GetAllLogsPag SearchTerm = queryParameters.SearchTerm, Page = queryParameters.Page, Size = queryParameters.Size, - SortBy = queryParameters.SortBy, - SortOrder = queryParameters.SortOrder, }; var result = await Mediator.Send(query); diff --git a/src/Api/Controllers/UsersController.cs b/src/Api/Controllers/UsersController.cs index ac5ec81e..097254c2 100644 --- a/src/Api/Controllers/UsersController.cs +++ b/src/Api/Controllers/UsersController.cs @@ -183,15 +183,16 @@ public async Task>>> GetAllEmployeesP { throw new KeyNotFoundException("User does not belong to a department."); } - + var query = new GetAllEmployeesPaginated.Query() { DepartmentId = performingUserDepartmentId.Value, - } + }; var result = await Mediator.Send(query); return Ok(Result>.Succeed(result)); } + /// /// Get all user related logs paginated /// /// Get all users related logs query parameters @@ -208,8 +209,6 @@ public async Task>>> GetAllUserLog SearchTerm = queryParameters.SearchTerm, Page = queryParameters.Page, Size = queryParameters.Size, - SortBy = queryParameters.SortBy, - SortOrder = queryParameters.SortOrder, }; var result = await Mediator.Send(query); return Ok(Result>.Succeed(result)); diff --git a/src/Application/Borrows/Queries/GetAllRequestLogsPaginated.cs b/src/Application/Borrows/Queries/GetAllRequestLogsPaginated.cs index 7a2002d1..56dc9173 100644 --- a/src/Application/Borrows/Queries/GetAllRequestLogsPaginated.cs +++ b/src/Application/Borrows/Queries/GetAllRequestLogsPaginated.cs @@ -16,8 +16,6 @@ public record Query : IRequest> public string? SearchTerm { get; init; } public int? Page { get; init; } public int? Size { get; init; } - public string? SortBy { get; init; } - public string? SortOrder { get; init; } } public class QueryHandler : IRequestHandler> @@ -43,20 +41,13 @@ public async Task> Handle(Query request, Cancellati x.Action.Trim().ToLower().Contains(request.SearchTerm.Trim().ToLower())); } - var sortBy = request.SortBy; - if (sortBy is null || !sortBy.MatchesPropertyName()) - { - sortBy = nameof(RequestLogDto.Time); - } - - var sortOrder = request.SortOrder ?? "desc"; var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; var count = await logs.CountAsync(cancellationToken); var list = await logs + .OrderByDescending(x => x.Time) .Paginate(pageNumber.Value, sizeNumber.Value) - .OrderByCustom(sortBy, sortOrder) .ToListAsync(cancellationToken); var result = _mapper.Map>(list); diff --git a/src/Application/Documents/Queries/GetAllDocumentLogsPaginated.cs b/src/Application/Documents/Queries/GetAllDocumentLogsPaginated.cs index a0bf18ae..b018e472 100644 --- a/src/Application/Documents/Queries/GetAllDocumentLogsPaginated.cs +++ b/src/Application/Documents/Queries/GetAllDocumentLogsPaginated.cs @@ -15,8 +15,6 @@ public record Query : IRequest> public string? SearchTerm { get; init; } public int? Page { get; init; } public int? Size { get; init; } - public string? SortBy { get; init; } - public string? SortOrder { get; init; } } public class QueryHandler : IRequestHandler> @@ -44,18 +42,12 @@ public async Task> Handle(Query request, Cancellat x.Action.ToLower().Contains(request.SearchTerm.ToLower())); } - var sortBy = request.SortBy; - if (sortBy is null || !sortBy.MatchesPropertyName()) - { - sortBy = nameof(DocumentLogDto.Time); - } - var sortOrder = request.SortOrder ?? "desc"; var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; var count = await logs.CountAsync(cancellationToken); var list = await logs - .OrderByCustom(sortBy, sortOrder) + .OrderByDescending(x => x.Time) .Paginate(pageNumber.Value, sizeNumber.Value) .ToListAsync(cancellationToken); diff --git a/src/Application/Folders/Queries/GetAllFolderLogsPaginated.cs b/src/Application/Folders/Queries/GetAllFolderLogsPaginated.cs index d7a3240e..ba286e23 100644 --- a/src/Application/Folders/Queries/GetAllFolderLogsPaginated.cs +++ b/src/Application/Folders/Queries/GetAllFolderLogsPaginated.cs @@ -15,8 +15,6 @@ public record Query : IRequest> public string? SearchTerm { get; init; } public int? Page { get; init; } public int? Size { get; init; } - public string? SortBy { get; init; } - public string? SortOrder { get; init; } } public class QueryHandler : IRequestHandler> @@ -41,21 +39,14 @@ public async Task> Handle(Query request, Cancellatio logs = logs.Where(x => x.Action.Trim().ToLower().Contains(request.SearchTerm.Trim().ToLower())); } - - var sortBy = request.SortBy; - if (sortBy is null || !sortBy.MatchesPropertyName()) - { - sortBy = nameof(FolderLogDto.Time); - } - - var sortOrder = request.SortOrder ?? "dsc"; + var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; var count = await logs.CountAsync(cancellationToken); var list = await logs + .OrderByDescending(x => x.Time) .Paginate(pageNumber.Value, sizeNumber.Value) - .OrderByCustom(sortBy, sortOrder) .ToListAsync(cancellationToken); var result = _mapper.Map>(list); diff --git a/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs b/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs index 475bdf84..af4bbdf6 100644 --- a/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs +++ b/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs @@ -15,8 +15,6 @@ public record Query : IRequest> public string? SearchTerm { get; init; } public int? Page { get; init; } public int? Size { get; init; } - public string? SortBy { get; init; } - public string? SortOrder { get; init; } } public class QueryHandler : IRequestHandler> @@ -41,21 +39,14 @@ public async Task> Handle(Query request, Cancellatio logs = logs.Where(x => x.Action.Trim().ToLower().Contains(request.SearchTerm.Trim().ToLower())); } - - var sortBy = request.SortBy; - if (sortBy is null || !sortBy.MatchesPropertyName()) - { - sortBy = nameof(LockerLogDto.Time); - } - - var sortOrder = request.SortOrder ?? "dsc"; + var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; var count = await logs.CountAsync(cancellationToken); var list = await logs + .OrderByDescending(x => x.Time) .Paginate(pageNumber.Value, sizeNumber.Value) - .OrderByCustom(sortBy, sortOrder) .ToListAsync(cancellationToken); var result = _mapper.Map>(list); diff --git a/src/Application/Users/Queries/GetAllEmployeesPaginated.cs b/src/Application/Users/Queries/GetAllEmployeesPaginated.cs index d6189405..d5375a02 100644 --- a/src/Application/Users/Queries/GetAllEmployeesPaginated.cs +++ b/src/Application/Users/Queries/GetAllEmployeesPaginated.cs @@ -50,8 +50,8 @@ public async Task> Handle(Query request, CancellationToke var count = await users.CountAsync(cancellationToken); var list = await users - .Paginate(pageNumber.Value, sizeNumber.Value) .OrderByCustom(sortBy, sortOrder) + .Paginate(pageNumber.Value, sizeNumber.Value) .ToListAsync(cancellationToken); var result = _mapper.Map>(list); diff --git a/src/Application/Users/Queries/GetAllUserLogsPaginated.cs b/src/Application/Users/Queries/GetAllUserLogsPaginated.cs index 67528d67..50ea4eb3 100644 --- a/src/Application/Users/Queries/GetAllUserLogsPaginated.cs +++ b/src/Application/Users/Queries/GetAllUserLogsPaginated.cs @@ -15,8 +15,6 @@ public record Query : IRequest> public string? SearchTerm { get; init; } public int? Page { get; init; } public int? Size { get; init; } - public string? SortBy { get; init; } - public string? SortOrder { get; init; } } public class QueryHandler : IRequestHandler> @@ -42,20 +40,13 @@ public async Task> Handle(Query request, CancellationT x.Action.Trim().ToLower().Contains(request.SearchTerm.Trim().ToLower())); } - var sortBy = request.SortBy; - if (sortBy is null || !sortBy.MatchesPropertyName()) - { - sortBy = nameof(UserLogDto.Time); - } - - var sortOrder = request.SortOrder ?? "dsc"; var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; var count = await logs.CountAsync(cancellationToken); var list = await logs + .OrderByDescending(x => x.Time) .Paginate(pageNumber.Value, sizeNumber.Value) - .OrderByCustom(sortBy, sortOrder) .ToListAsync(cancellationToken); var result = _mapper.Map>(list); diff --git a/src/Application/Users/Queries/GetAllUsersPaginated.cs b/src/Application/Users/Queries/GetAllUsersPaginated.cs index f8613324..e0690bb4 100644 --- a/src/Application/Users/Queries/GetAllUsersPaginated.cs +++ b/src/Application/Users/Queries/GetAllUsersPaginated.cs @@ -61,8 +61,8 @@ public async Task> Handle(Query request, CancellationToke var count = await users.CountAsync(cancellationToken); var list = await users - .Paginate(pageNumber.Value, sizeNumber.Value) .OrderByCustom(sortBy, sortOrder) + .Paginate(pageNumber.Value, sizeNumber.Value) .ToListAsync(cancellationToken); var result = _mapper.Map>(list); diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index d4fa8552..b59e7d45 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -258,7 +258,7 @@ public async Task ResetPassword(string token, string newPassword) } var salt = StringUtil.RandomSalt(); user.PasswordSalt = salt; - user.PasswordHash = newPassword.HashPasswordWith(salt, newPassword); + user.PasswordHash = newPassword.HashPasswordWith(salt, _securitySettings.Pepper); resetPasswordToken.IsInvalidated = true; await _applicationDbContext.SaveChangesAsync(CancellationToken.None); await _authDbContext.SaveChangesAsync(CancellationToken.None); From a90d24d3ac1b945097f35e7cff020ef7cc0c9d09 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Thu, 15 Jun 2023 10:12:11 +0700 Subject: [PATCH 39/56] fuck me --- .../Requests/Borrows/ApproveRequest.cs | 6 + .../Payload/Requests/Borrows/RejectRequest.cs | 6 + src/Api/Controllers/UsersController.cs | 234 +----------------- src/Application/Common/Models/Dtos/BaseDto.cs | 6 + 4 files changed, 20 insertions(+), 232 deletions(-) create mode 100644 src/Api/Controllers/Payload/Requests/Borrows/ApproveRequest.cs create mode 100644 src/Api/Controllers/Payload/Requests/Borrows/RejectRequest.cs create mode 100644 src/Application/Common/Models/Dtos/BaseDto.cs diff --git a/src/Api/Controllers/Payload/Requests/Borrows/ApproveRequest.cs b/src/Api/Controllers/Payload/Requests/Borrows/ApproveRequest.cs new file mode 100644 index 00000000..6629af10 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Borrows/ApproveRequest.cs @@ -0,0 +1,6 @@ +namespace Api.Controllers.Payload.Requests.Borrows; + +public class ApproveRequest +{ + +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Borrows/RejectRequest.cs b/src/Api/Controllers/Payload/Requests/Borrows/RejectRequest.cs new file mode 100644 index 00000000..fc361490 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Borrows/RejectRequest.cs @@ -0,0 +1,6 @@ +namespace Api.Controllers.Payload.Requests.Borrows; + +public class RejectBorrowRequest +{ + public string Reason { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/UsersController.cs b/src/Api/Controllers/UsersController.cs index 097254c2..7c1f0134 100644 --- a/src/Api/Controllers/UsersController.cs +++ b/src/Api/Controllers/UsersController.cs @@ -1,236 +1,6 @@ -using Api.Controllers.Payload.Requests; -using Api.Controllers.Payload.Requests.Users; -using Application.Common.Interfaces; -using Application.Common.Models; -using Application.Common.Models.Dtos.Logging; -using Application.Identity; -using Application.Users.Commands; -using Application.Users.Queries; -using Infrastructure.Identity.Authorization; -using Microsoft.AspNetCore.Mvc; -using Org.BouncyCastle.Security; - namespace Api.Controllers; -public class UsersController : ApiControllerBase +public class UsersController { - private readonly ICurrentUserService _currentUserService; - - public UsersController(ICurrentUserService currentUserService) - { - _currentUserService = currentUserService; - } - - /// - /// Get a user by id - /// - /// Id of the user to be retrieved - /// A UserDto of the retrieved user - [HttpGet("{userId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetById([FromRoute] Guid userId) - { - var query = new GetUserById.Query - { - UserId = userId, - }; - var result = await Mediator.Send(query); - return Ok(Result.Succeed(result)); - } - - /// - /// Get all users paginated - /// - /// Get all users query parameters - /// A paginated list of UserDto - [RequiresRole(IdentityData.Roles.Admin)] - [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task>>> GetAllPaginated( - [FromQuery] GetAllUsersPaginatedQueryParameters queryParameters) - { - var query = new GetAllUsersPaginated.Query() - { - DepartmentId = queryParameters.DepartmentId, - SearchTerm = queryParameters.SearchTerm, - Page = queryParameters.Page, - Size = queryParameters.Size, - SortBy = queryParameters.SortBy, - SortOrder = queryParameters.SortOrder, - }; - var result = await Mediator.Send(query); - return Ok(Result>.Succeed(result)); - } - - /// - /// Add a user - /// - /// Add user details - /// A UserDto of the added user - [RequiresRole(IdentityData.Roles.Admin)] - [HttpPost] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Add([FromBody] AddUserRequest request) - { - var performingUserId = _currentUserService.GetId(); - var command = new AddUser.Command() - { - PerformingUserId = performingUserId, - Username = request.Username, - Email = request.Email, - FirstName = request.FirstName, - LastName = request.LastName, - Role = request.Role, - Position = request.Position, - DepartmentId = request.DepartmentId, - }; - var result = await Mediator.Send(command); - return Ok(Result.Succeed(result)); - } - - /// - /// Enable a user - /// - /// Id of the user to be enabled - /// A UserDto of the enabled user - [RequiresRole(IdentityData.Roles.Admin)] - [HttpPost("enable/{userId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> Enable([FromRoute] Guid userId) - { - var command = new EnableUser.Command() - { - UserId = userId - }; - var result = await Mediator.Send(command); - return Ok(Result.Succeed(result)); - } - - /// - /// Disable a user - /// - /// Id of the user to be disabled - /// A UserDto of the disabled user - [RequiresRole(IdentityData.Roles.Admin)] - [HttpPut("disable/{userId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Disable([FromRoute] Guid userId) - { - var performingUserId = _currentUserService.GetId(); - var command = new DisableUser.Command() - { - PerformingUserId = performingUserId, - UserId = userId, - }; - var result = await Mediator.Send(command); - return Ok(Result.Succeed(result)); - } - /// - /// Update a user - /// - /// Id of the user to be updated - /// Update user details - /// A UserDto of the updated user - [HttpPut("{userId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Update([FromRoute] Guid userId, [FromBody] UpdateUserRequest request) - { - var performingUserId = _currentUserService.GetId(); - var command = new UpdateUser.Command() - { - PerformingUserId = performingUserId, - UserId = userId, - FirstName = request.FirstName, - LastName = request.LastName, - Position = request.Position, - }; - var result = await Mediator.Send(command); - return Ok(Result.Succeed(result)); - } - - /// - /// Get all users with the "Employee" role of the current user's department. - /// - /// Query parameters - /// A list of UserDtos with the employee role of that department - [RequiresRole(IdentityData.Roles.Employee)] - [HttpGet("employees")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>>> GetAllEmployeesPaginated( - [FromQuery] GetAllEmployeesPaginatedQueryParameters queryParameters) - { - var performingUserDepartmentId = _currentUserService.GetDepartmentId(); - - if (performingUserDepartmentId is null) - { - throw new KeyNotFoundException("User does not belong to a department."); - } - - var query = new GetAllEmployeesPaginated.Query() - { - DepartmentId = performingUserDepartmentId.Value, - }; - var result = await Mediator.Send(query); - return Ok(Result>.Succeed(result)); - } - - /// - /// Get all user related logs paginated - /// - /// Get all users related logs query parameters - /// A paginated list of UserLogDto - [RequiresRole(IdentityData.Roles.Admin)] - [HttpGet("logs")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task>>> GetAllUserLogs( - [FromQuery] GetAllLogsPaginatedQueryParameters queryParameters) - { - var query = new GetAllUserLogsPaginated.Query() - { - SearchTerm = queryParameters.SearchTerm, - Page = queryParameters.Page, - Size = queryParameters.Size, - }; - var result = await Mediator.Send(query); - return Ok(Result>.Succeed(result)); - } - - /// - /// Get user related log by Id - /// - /// Id of the logged user - /// UserLogDto of the logged user - [RequiresRole(IdentityData.Roles.Admin)] - [HttpGet("log/{logId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task>> GetUserLogById([FromRoute] Guid logId) - { - var query = new GetUserLogById.Query() - { - LogId = logId - }; - - var result = await Mediator.Send(query); - return Ok(Result.Succeed(result)); - } -} +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/BaseDto.cs b/src/Application/Common/Models/Dtos/BaseDto.cs new file mode 100644 index 00000000..fc92e317 --- /dev/null +++ b/src/Application/Common/Models/Dtos/BaseDto.cs @@ -0,0 +1,6 @@ +namespace Application.Common.Models.Dtos; + +public class BaseDto +{ + +} \ No newline at end of file From 279f03e9581672433ec28c971c83a7c461fe8bf5 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Thu, 15 Jun 2023 10:12:18 +0700 Subject: [PATCH 40/56] fuck me 2 --- .vscode/settings.json | 3 + src/Api/Controllers/BorrowsController.cs | 6 +- src/Api/Controllers/DepartmentsController.cs | 4 +- src/Api/Controllers/DocumentsController.cs | 2 - .../Requests/Borrows/ApproveRequest.cs | 2 +- .../Payload/Requests/Borrows/RejectRequest.cs | 2 +- .../Requests/Users/UpdateUserRequest.cs | 2 + src/Api/Controllers/UsersController.cs | 191 +++++++++++++++++- .../Borrows/Commands/ApproveBorrowRequest.cs | 2 + .../Borrows/Commands/BorrowDocument.cs | 23 ++- .../Borrows/Commands/RejectBorrowRequest.cs | 5 +- .../Common/Extensions/QueryableExtensions.cs | 61 ++++++ src/Application/Common/Models/Dtos/BaseDto.cs | 2 +- .../Common/Models/Dtos/DepartmentDto.cs | 3 +- .../Common/Models/Dtos/Digital/EntryDto.cs | 3 +- .../Common/Models/Dtos/Digital/FileDto.cs | 3 +- .../Models/Dtos/Digital/UserGroupDto.cs | 3 +- .../Dtos/ImportDocument/IssuedDocumentDto.cs | 3 +- .../Models/Dtos/ImportDocument/IssuerDto.cs | 3 +- .../Models/Dtos/Logging/DocumentLogDto.cs | 3 +- .../Models/Dtos/Logging/FolderLogDto.cs | 3 +- .../Models/Dtos/Logging/LockerLogDto.cs | 3 +- .../Models/Dtos/Logging/RequestLogDto.cs | 3 +- .../Common/Models/Dtos/Logging/RoomLogDto.cs | 3 +- .../Common/Models/Dtos/Logging/UserLogDto.cs | 3 +- .../Common/Models/Dtos/Physical/BorrowDto.cs | 3 +- .../Models/Dtos/Physical/DocumentDto.cs | 3 +- .../Models/Dtos/Physical/DocumentItemDto.cs | 3 +- .../Models/Dtos/Physical/EmptyFolderDto.cs | 2 +- .../Models/Dtos/Physical/EmptyLockerDto.cs | 3 +- .../Common/Models/Dtos/Physical/FolderDto.cs | 3 +- .../Common/Models/Dtos/Physical/LockerDto.cs | 3 +- .../Common/Models/Dtos/Physical/RoomDto.cs | 3 +- .../Common/Models/Dtos/Physical/StaffDto.cs | 3 +- src/Application/Common/Models/Dtos/UserDto.cs | 3 +- .../Queries/GetAllDocumentLogsPaginated.cs | 20 +- .../Queries/GetAllDocumentsPaginated.cs | 27 +-- .../Queries/GetAllIssuedDocumentsPaginated.cs | 26 +-- .../Queries/GetDocumentsOfUserPaginated.cs | 27 +-- .../Queries/GetSelfDocumentsPaginated.cs | 27 +-- .../Queries/GetAllFolderLogsPaginated.cs | 20 +- .../Folders/Queries/GetAllFoldersPaginated.cs | 26 +-- .../Queries/GetAllLockerLogsPaginated.cs | 20 +- .../Lockers/Queries/GetAllLockersPaginated.cs | 27 +-- .../Rooms/Queries/GetAllRoomLogsPaginated.cs | 26 +-- .../Rooms/Queries/GetAllRoomsPaginated.cs | 27 +-- .../Staffs/Queries/GetAllStaffsPaginated.cs | 28 +-- src/Application/Users/Commands/AddUser.cs | 9 +- src/Application/Users/Commands/UpdateUser.cs | 15 +- .../Users/Queries/GetAllEmployeesPaginated.cs | 31 +-- .../Users/Queries/GetAllUserLogsPaginated.cs | 20 +- .../Users/Queries/GetAllUsersPaginated.cs | 27 +-- 52 files changed, 451 insertions(+), 322 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..cdc2203c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dotnet.defaultSolution": "ProFile.sln" +} \ No newline at end of file diff --git a/src/Api/Controllers/BorrowsController.cs b/src/Api/Controllers/BorrowsController.cs index 89e62a6e..9ebb4b9a 100644 --- a/src/Api/Controllers/BorrowsController.cs +++ b/src/Api/Controllers/BorrowsController.cs @@ -187,13 +187,14 @@ public async Task>>> GetAllRequests [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> ApproveRequest([FromRoute] Guid borrowId) + public async Task>> ApproveRequest([FromRoute] Guid borrowId, [FromBody] ApproveRequest request) { var performingUserId = _currentUserService.GetId(); var command = new ApproveBorrowRequest.Command() { PerformingUserId = performingUserId, BorrowId = borrowId, + Reason = request.Reason, }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); @@ -210,13 +211,14 @@ public async Task>> ApproveRequest([FromRoute] Gu [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> RejectRequest([FromRoute] Guid borrowId) + public async Task>> RejectRequest([FromRoute] Guid borrowId, [FromBody] RejectRequest request) { var performingUserId = _currentUserService.GetId(); var command = new RejectBorrowRequest.Command() { PerformingUserId = performingUserId, BorrowId = borrowId, + Reason = request.Reason, }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); diff --git a/src/Api/Controllers/DepartmentsController.cs b/src/Api/Controllers/DepartmentsController.cs index bf01c28a..4414b2af 100644 --- a/src/Api/Controllers/DepartmentsController.cs +++ b/src/Api/Controllers/DepartmentsController.cs @@ -25,7 +25,7 @@ public async Task>> GetById([FromRoute] Guid { var query = new GetDepartmentById.Query() { - DepartmentId = departmentId + DepartmentId = departmentId, }; var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); @@ -63,7 +63,7 @@ public async Task>> Add([FromBody] AddDepartm var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } - + /// /// Update a department /// diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index 36fdfc49..ab2e94df 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -1,13 +1,11 @@ using Api.Controllers.Payload.Requests; using Api.Controllers.Payload.Requests.Documents; -using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Models; using Application.Common.Models.Dtos; using Application.Common.Models.Dtos.ImportDocument; using Application.Common.Models.Dtos.Logging; using Application.Common.Models.Dtos.Physical; -using Application.Common.Models.Operations; using Application.Documents.Commands; using Application.Documents.Queries; using Application.Identity; diff --git a/src/Api/Controllers/Payload/Requests/Borrows/ApproveRequest.cs b/src/Api/Controllers/Payload/Requests/Borrows/ApproveRequest.cs index 6629af10..f6c0c0d2 100644 --- a/src/Api/Controllers/Payload/Requests/Borrows/ApproveRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Borrows/ApproveRequest.cs @@ -2,5 +2,5 @@ namespace Api.Controllers.Payload.Requests.Borrows; public class ApproveRequest { - + public string Reason { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Borrows/RejectRequest.cs b/src/Api/Controllers/Payload/Requests/Borrows/RejectRequest.cs index fc361490..dcbfbfce 100644 --- a/src/Api/Controllers/Payload/Requests/Borrows/RejectRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Borrows/RejectRequest.cs @@ -1,6 +1,6 @@ namespace Api.Controllers.Payload.Requests.Borrows; -public class RejectBorrowRequest +public class RejectRequest { public string Reason { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Users/UpdateUserRequest.cs b/src/Api/Controllers/Payload/Requests/Users/UpdateUserRequest.cs index c30e9871..959567d4 100644 --- a/src/Api/Controllers/Payload/Requests/Users/UpdateUserRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Users/UpdateUserRequest.cs @@ -17,4 +17,6 @@ public class UpdateUserRequest /// New position of the user to be updated /// public string? Position { get; set; } + public string Role { get; set; } = null!; + public bool IsActive { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/UsersController.cs b/src/Api/Controllers/UsersController.cs index 7c1f0134..556006be 100644 --- a/src/Api/Controllers/UsersController.cs +++ b/src/Api/Controllers/UsersController.cs @@ -1,6 +1,195 @@ +using Api.Controllers.Payload.Requests; +using Api.Controllers.Payload.Requests.Users; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Logging; +using Application.Identity; +using Application.Users.Commands; +using Application.Users.Queries; +using Infrastructure.Identity.Authorization; +using MediatR; +using Microsoft.AspNetCore.Mvc; + namespace Api.Controllers; -public class UsersController +public class UsersController : ApiControllerBase { + private readonly ICurrentUserService _currentUserService; + + public UsersController(ICurrentUserService currentUserService) + { + _currentUserService = currentUserService; + } + + /// + /// Get a user by id + /// + /// Id of the user to be retrieved + /// A UserDto of the retrieved user + [HttpGet("{userId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetById([FromRoute] Guid userId) + { + var query = new GetUserById.Query + { + UserId = userId, + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } + + /// + /// Get all users paginated + /// + /// Get all users query parameters + /// A paginated list of UserDto + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task>>> GetAllPaginated( + [FromQuery] GetAllUsersPaginatedQueryParameters queryParameters) + { + var query = new GetAllUsersPaginated.Query() + { + DepartmentId = queryParameters.DepartmentId, + SearchTerm = queryParameters.SearchTerm, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } + + /// + /// Add a user + /// + /// Add user details + /// A UserDto of the added user + [HttpPost] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> Add([FromBody] AddUserRequest request) + { + var performingUser = _currentUserService.GetCurrentUser(); + var command = new AddUser.Command() + { + PerformingUser = performingUser, + Username = request.Username, + Email = request.Email, + FirstName = request.FirstName, + LastName = request.LastName, + Role = request.Role, + Position = request.Position, + DepartmentId = request.DepartmentId, + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } + /// + /// Update a user + /// + /// Id of the user to be updated + /// Update user details + /// A UserDto of the updated user + [HttpPut("{userId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> Update([FromRoute] Guid userId, [FromBody] UpdateUserRequest request) + { + var performingUser = _currentUserService.GetCurrentUser(); + var command = new UpdateUser.Command() + { + PerformingUser = performingUser, + UserId = userId, + FirstName = request.FirstName, + LastName = request.LastName, + Position = request.Position, + Role = request.Role, + IsActive = request.IsActive, + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } + + /// + /// Get all employees in the same department + /// + /// Query parameters + /// A list of UserDtos + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>>> GetAllEmployeesPaginated( + [FromQuery] GetAllEmployeesPaginatedQueryParameters queryParameters) + { + var departmentId = _currentUserService.GetDepartmentId(); + + if (departmentId is null) + { + throw new KeyNotFoundException("User does not belong to a department."); + } + + var query = new GetAllEmployeesPaginated.Query() + { + DepartmentId = departmentId.Value, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } + + /// + /// Get all user related logs paginated + /// + /// Get all users related logs query parameters + /// A paginated list of UserLogDto + [HttpGet("logs")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task>>> GetAllUserLogs( + [FromQuery] GetAllLogsPaginatedQueryParameters queryParameters) + { + var query = new GetAllUserLogsPaginated.Query() + { + SearchTerm = queryParameters.SearchTerm, + Page = queryParameters.Page, + Size = queryParameters.Size, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } + + /// + /// Get user related log by Id + /// + /// Id of the logged user + /// UserLogDto of the logged user + [RequiresRole(IdentityData.Roles.Admin)] + [HttpGet("log/{logId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task>> GetUserLogById([FromRoute] Guid logId) + { + var query = new GetUserLogById.Query() + { + LogId = logId + }; + + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } } \ No newline at end of file diff --git a/src/Application/Borrows/Commands/ApproveBorrowRequest.cs b/src/Application/Borrows/Commands/ApproveBorrowRequest.cs index 348ff63f..d66bda15 100644 --- a/src/Application/Borrows/Commands/ApproveBorrowRequest.cs +++ b/src/Application/Borrows/Commands/ApproveBorrowRequest.cs @@ -17,6 +17,7 @@ public record Command : IRequest { public Guid PerformingUserId { get; init; } public Guid BorrowId { get; init; } + public string Reason { get; init; } = null!; } public class CommandHandler : IRequestHandler @@ -91,6 +92,7 @@ or BorrowRequestStatus.CheckedOut User = performingUser, Time = LocalDateTime.FromDateTime(DateTime.Now), Action = DocumentLogMessages.Borrow.Approve, + Reason = request.Reason, }; var result = _context.Borrows.Update(borrowRequest); await _context.DocumentLogs.AddAsync(log, cancellationToken); diff --git a/src/Application/Borrows/Commands/BorrowDocument.cs b/src/Application/Borrows/Commands/BorrowDocument.cs index 5a463cf2..7f810868 100644 --- a/src/Application/Borrows/Commands/BorrowDocument.cs +++ b/src/Application/Borrows/Commands/BorrowDocument.cs @@ -95,12 +95,6 @@ public async Task Handle(Command request, CancellationToken cancellat { throw new ConflictException("User is not allowed to borrow this document."); } - - var isGranted = _permissionManager.IsGranted(request.DocumentId, DocumentOperation.Borrow, request.BorrowerId); - if (!isGranted) - { - throw new UnauthorizedAccessException("You don't have permission to borrow this document."); - } // getting out a request of that document which is either not due or overdue // if the request is in time, meaning not overdue, @@ -133,7 +127,7 @@ or BorrowRequestStatus.CheckedOut throw new ConflictException("This document cannot be borrowed."); } } - + var entity = new Borrow() { Borrower = user, @@ -142,13 +136,26 @@ or BorrowRequestStatus.CheckedOut DueTime = LocalDateTime.FromDateTime(request.BorrowTo), Reason = request.Reason, Status = BorrowRequestStatus.Pending, + Created = LocalDateTime.FromDateTime(DateTime.Now), + CreatedBy = user.Id, }; + + if (document.IsPrivate) + { + var isGranted = _permissionManager.IsGranted(request.DocumentId, DocumentOperation.Borrow, request.BorrowerId); + if (!isGranted) + { + throw new UnauthorizedAccessException("You don't have permission to borrow this document."); + } + entity.Status = BorrowRequestStatus.Approved; + } + var log = new DocumentLog() { UserId = user.Id, User = user, Object = document, - Time = LocalDateTime.FromDateTime(DateTime.Now), + Time = localDateTimeNow, Action = DocumentLogMessages.Borrow.NewBorrowRequest, }; diff --git a/src/Application/Borrows/Commands/RejectBorrowRequest.cs b/src/Application/Borrows/Commands/RejectBorrowRequest.cs index d07eebcb..b38e4e02 100644 --- a/src/Application/Borrows/Commands/RejectBorrowRequest.cs +++ b/src/Application/Borrows/Commands/RejectBorrowRequest.cs @@ -7,6 +7,7 @@ using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; using NodaTime; namespace Application.Borrows.Commands; @@ -17,6 +18,7 @@ public record Command : IRequest { public Guid PerformingUserId { get; init; } public Guid BorrowId { get; init; } + public string Reason { get; init; } = null!; } public class CommandHandler : IRequestHandler @@ -53,7 +55,8 @@ public async Task Handle(Command request, CancellationToken cancellat UserId = performingUser!.Id, User = performingUser, Time = LocalDateTime.FromDateTime(DateTime.Now), - Action = DocumentLogMessages.Borrow.Approve, + Action = DocumentLogMessages.Borrow.Reject, + Reason = request.Reason, }; var result = _context.Borrows.Update(borrowRequest); await _context.RequestLogs.AddAsync(requestLog, cancellationToken); diff --git a/src/Application/Common/Extensions/QueryableExtensions.cs b/src/Application/Common/Extensions/QueryableExtensions.cs index a670cca7..5df0eedd 100644 --- a/src/Application/Common/Extensions/QueryableExtensions.cs +++ b/src/Application/Common/Extensions/QueryableExtensions.cs @@ -1,4 +1,10 @@ using System.Linq.Expressions; +using Application.Common.Mappings; +using Application.Common.Models; +using Application.Common.Models.Dtos; +using AutoMapper; +using Domain.Common; +using Microsoft.EntityFrameworkCore; namespace Application.Common.Extensions; @@ -25,4 +31,59 @@ public static IQueryable Paginate(this IQueryable ite { return items.Skip((page - 1) * size).Take(size); } + + public static async Task> LoggingListPaginateAsync( + this IQueryable items, + int? page, + int? size, + IConfigurationProvider mapperConfiguration, + CancellationToken cancellationToken) + where TEntityDto : BaseDto, IMapFrom + where TLoggingEntity : BaseLoggingEntity + where TEntity : BaseEntity + { + var pageNumber = page is null or <= 0 ? 1 : page; + var sizeNumber = size is null or <= 0 ? 5 : size; + + var count = await items.CountAsync(cancellationToken); + var list = await items + .OrderByDescending(x => x.Time) + .Paginate(pageNumber.Value, sizeNumber.Value) + .ToListAsync(cancellationToken); + + var mapper = mapperConfiguration.CreateMapper(); + var result = mapper.Map>(list); + + return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + } + + public static async Task> ListPaginateWithFilterAsync( + this IQueryable items, + int? page, + int? size, + string? sortBy, + string? sortOrder, + IConfigurationProvider mapperConfiguration, + CancellationToken cancellationToken) + where TEntityDto : BaseDto, IMapFrom + { + if (sortBy is null || !sortBy.MatchesPropertyName()) + { + sortBy = nameof(BaseDto.Id); + } + + sortOrder ??= "asc"; + var pageNumber = page is null or <= 0 ? 1 : page; + var sizeNumber = size is null or <= 0 ? 5 : size; + + var count = await items.CountAsync(cancellationToken); + var list = await items + .OrderByCustom(sortBy, sortOrder) + .Paginate(pageNumber.Value, sizeNumber.Value) + .ToListAsync(cancellationToken); + + var mapper = mapperConfiguration.CreateMapper(); + var result = mapper.Map>(list); + return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + } } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/BaseDto.cs b/src/Application/Common/Models/Dtos/BaseDto.cs index fc92e317..28da7387 100644 --- a/src/Application/Common/Models/Dtos/BaseDto.cs +++ b/src/Application/Common/Models/Dtos/BaseDto.cs @@ -2,5 +2,5 @@ namespace Application.Common.Models.Dtos; public class BaseDto { - + public Guid Id { get; set; } } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/DepartmentDto.cs b/src/Application/Common/Models/Dtos/DepartmentDto.cs index 9ae2e4e6..516e9974 100644 --- a/src/Application/Common/Models/Dtos/DepartmentDto.cs +++ b/src/Application/Common/Models/Dtos/DepartmentDto.cs @@ -4,9 +4,8 @@ namespace Application.Common.Models.Dtos; -public class DepartmentDto : IMapFrom +public class DepartmentDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public string Name { get; set; } = null!; public Guid? RoomId { get; set; } diff --git a/src/Application/Common/Models/Dtos/Digital/EntryDto.cs b/src/Application/Common/Models/Dtos/Digital/EntryDto.cs index fe5445ab..b3882a91 100644 --- a/src/Application/Common/Models/Dtos/Digital/EntryDto.cs +++ b/src/Application/Common/Models/Dtos/Digital/EntryDto.cs @@ -3,9 +3,8 @@ namespace Application.Common.Models.Dtos.Digital; -public class EntryDto : IMapFrom +public class EntryDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public string Name { get; set; } = null!; public string Path { get; set; } = null!; public FileDto? File { get; set; } diff --git a/src/Application/Common/Models/Dtos/Digital/FileDto.cs b/src/Application/Common/Models/Dtos/Digital/FileDto.cs index cf2f7531..c23b2383 100644 --- a/src/Application/Common/Models/Dtos/Digital/FileDto.cs +++ b/src/Application/Common/Models/Dtos/Digital/FileDto.cs @@ -3,8 +3,7 @@ namespace Application.Common.Models.Dtos.Digital; -public class FileDto : IMapFrom +public class FileDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public string FileType { get; set; } = null!; } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Digital/UserGroupDto.cs b/src/Application/Common/Models/Dtos/Digital/UserGroupDto.cs index 88fb1d13..3707c15c 100644 --- a/src/Application/Common/Models/Dtos/Digital/UserGroupDto.cs +++ b/src/Application/Common/Models/Dtos/Digital/UserGroupDto.cs @@ -3,8 +3,7 @@ namespace Application.Common.Models.Dtos.Digital; -public class UserGroupDto : IMapFrom +public class UserGroupDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public string Name { get; set; } = null!; } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/ImportDocument/IssuedDocumentDto.cs b/src/Application/Common/Models/Dtos/ImportDocument/IssuedDocumentDto.cs index 1c573d00..a16aa23f 100644 --- a/src/Application/Common/Models/Dtos/ImportDocument/IssuedDocumentDto.cs +++ b/src/Application/Common/Models/Dtos/ImportDocument/IssuedDocumentDto.cs @@ -4,9 +4,8 @@ namespace Application.Common.Models.Dtos.ImportDocument; -public class IssuedDocumentDto : IMapFrom +public class IssuedDocumentDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public string Title { get; set; } = null!; public string? Description { get; set; } public string DocumentType { get; set; } = null!; diff --git a/src/Application/Common/Models/Dtos/ImportDocument/IssuerDto.cs b/src/Application/Common/Models/Dtos/ImportDocument/IssuerDto.cs index 3818dc77..bb812d7e 100644 --- a/src/Application/Common/Models/Dtos/ImportDocument/IssuerDto.cs +++ b/src/Application/Common/Models/Dtos/ImportDocument/IssuerDto.cs @@ -3,9 +3,8 @@ namespace Application.Common.Models.Dtos.ImportDocument; -public class IssuerDto : IMapFrom +public class IssuerDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public string Username { get; set; } public string Email { get; set; } public string FirstName { get; set; } diff --git a/src/Application/Common/Models/Dtos/Logging/DocumentLogDto.cs b/src/Application/Common/Models/Dtos/Logging/DocumentLogDto.cs index c998ccf4..c8ad9fdd 100644 --- a/src/Application/Common/Models/Dtos/Logging/DocumentLogDto.cs +++ b/src/Application/Common/Models/Dtos/Logging/DocumentLogDto.cs @@ -6,9 +6,8 @@ namespace Application.Common.Models.Dtos.Logging; -public class DocumentLogDto : IMapFrom +public class DocumentLogDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public Guid UserId { get; set; } public string Action { get; set; } public DocumentDto? Object { get; set; } diff --git a/src/Application/Common/Models/Dtos/Logging/FolderLogDto.cs b/src/Application/Common/Models/Dtos/Logging/FolderLogDto.cs index 67fe5e22..e31d63bc 100644 --- a/src/Application/Common/Models/Dtos/Logging/FolderLogDto.cs +++ b/src/Application/Common/Models/Dtos/Logging/FolderLogDto.cs @@ -6,9 +6,8 @@ namespace Application.Common.Models.Dtos.Logging; -public class FolderLogDto : IMapFrom +public class FolderLogDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public string Action { get; set; } = null!; public FolderDto? Object { get; set; } public DateTime Time { get; set; } diff --git a/src/Application/Common/Models/Dtos/Logging/LockerLogDto.cs b/src/Application/Common/Models/Dtos/Logging/LockerLogDto.cs index b37cf09e..53e64a29 100644 --- a/src/Application/Common/Models/Dtos/Logging/LockerLogDto.cs +++ b/src/Application/Common/Models/Dtos/Logging/LockerLogDto.cs @@ -6,9 +6,8 @@ namespace Application.Common.Models.Dtos.Logging; -public class LockerLogDto : IMapFrom +public class LockerLogDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public string Action { get; set; } = null!; public LockerDto? Object { get; set; } public DateTime Time { get; set; } diff --git a/src/Application/Common/Models/Dtos/Logging/RequestLogDto.cs b/src/Application/Common/Models/Dtos/Logging/RequestLogDto.cs index a291c228..ecf32e14 100644 --- a/src/Application/Common/Models/Dtos/Logging/RequestLogDto.cs +++ b/src/Application/Common/Models/Dtos/Logging/RequestLogDto.cs @@ -6,9 +6,8 @@ namespace Application.Common.Models.Dtos.Logging; -public class RequestLogDto : IMapFrom +public class RequestLogDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public string Action { get; set; } = null!; public DocumentDto? Object { get; set; } public DateTime Time { get; set; } diff --git a/src/Application/Common/Models/Dtos/Logging/RoomLogDto.cs b/src/Application/Common/Models/Dtos/Logging/RoomLogDto.cs index 079c87ef..833c14eb 100644 --- a/src/Application/Common/Models/Dtos/Logging/RoomLogDto.cs +++ b/src/Application/Common/Models/Dtos/Logging/RoomLogDto.cs @@ -6,9 +6,8 @@ namespace Application.Common.Models.Dtos.Logging; -public class RoomLogDto : IMapFrom +public class RoomLogDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public Guid UserId { get; set; } public string Action { get; set; } public RoomDto? Object { get; set; } diff --git a/src/Application/Common/Models/Dtos/Logging/UserLogDto.cs b/src/Application/Common/Models/Dtos/Logging/UserLogDto.cs index e20ba4e3..7929f13d 100644 --- a/src/Application/Common/Models/Dtos/Logging/UserLogDto.cs +++ b/src/Application/Common/Models/Dtos/Logging/UserLogDto.cs @@ -5,9 +5,8 @@ namespace Application.Common.Models.Dtos.Logging; -public class UserLogDto : IMapFrom +public class UserLogDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public string Action { get; set; } = null!; public UserDto? Object { get; set; } public DateTime Time { get; set; } diff --git a/src/Application/Common/Models/Dtos/Physical/BorrowDto.cs b/src/Application/Common/Models/Dtos/Physical/BorrowDto.cs index 49afc176..1401cb19 100644 --- a/src/Application/Common/Models/Dtos/Physical/BorrowDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/BorrowDto.cs @@ -5,9 +5,8 @@ namespace Application.Common.Models.Dtos.Physical; -public class BorrowDto : IMapFrom +public class BorrowDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public Guid BorrowerId { get; set; } public Guid DocumentId { get; set; } public DateTime BorrowTime { get; set; } diff --git a/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs b/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs index 6c517ce1..b8129b60 100644 --- a/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs @@ -6,9 +6,8 @@ namespace Application.Common.Models.Dtos.Physical; -public class DocumentDto : IMapFrom +public class DocumentDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public string Title { get; set; } = null!; public string? Description { get; set; } public string DocumentType { get; set; } = null!; diff --git a/src/Application/Common/Models/Dtos/Physical/DocumentItemDto.cs b/src/Application/Common/Models/Dtos/Physical/DocumentItemDto.cs index 7420ad90..1b1e6fb9 100644 --- a/src/Application/Common/Models/Dtos/Physical/DocumentItemDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/DocumentItemDto.cs @@ -4,9 +4,8 @@ namespace Application.Common.Models.Dtos.Physical; -public class DocumentItemDto : IMapFrom +public class DocumentItemDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public string Title { get; set; } = null!; public string? Description { get; set; } public string DocumentType { get; set; } = null!; diff --git a/src/Application/Common/Models/Dtos/Physical/EmptyFolderDto.cs b/src/Application/Common/Models/Dtos/Physical/EmptyFolderDto.cs index 10e77646..b05091d7 100644 --- a/src/Application/Common/Models/Dtos/Physical/EmptyFolderDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/EmptyFolderDto.cs @@ -4,7 +4,7 @@ namespace Application.Common.Models.Dtos.Physical; -public class EmptyFolderDto : IMapFrom +public class EmptyFolderDto : BaseDto, IMapFrom { public Guid Id { get; set; } public string Name { get; set; } diff --git a/src/Application/Common/Models/Dtos/Physical/EmptyLockerDto.cs b/src/Application/Common/Models/Dtos/Physical/EmptyLockerDto.cs index 6d7537c7..13135db4 100644 --- a/src/Application/Common/Models/Dtos/Physical/EmptyLockerDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/EmptyLockerDto.cs @@ -4,9 +4,8 @@ namespace Application.Common.Models.Dtos.Physical; -public class EmptyLockerDto : IMapFrom +public class EmptyLockerDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public string Name { get; set; } public string Description { get; set; } public int Capacity { get; set; } diff --git a/src/Application/Common/Models/Dtos/Physical/FolderDto.cs b/src/Application/Common/Models/Dtos/Physical/FolderDto.cs index 8460746b..87acaa71 100644 --- a/src/Application/Common/Models/Dtos/Physical/FolderDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/FolderDto.cs @@ -3,9 +3,8 @@ namespace Application.Common.Models.Dtos.Physical; -public class FolderDto : IMapFrom +public class FolderDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public string Name { get; set; } public string Description { get; set; } public LockerDto Locker { get; set; } diff --git a/src/Application/Common/Models/Dtos/Physical/LockerDto.cs b/src/Application/Common/Models/Dtos/Physical/LockerDto.cs index e7c9d975..6f44a7da 100644 --- a/src/Application/Common/Models/Dtos/Physical/LockerDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/LockerDto.cs @@ -3,9 +3,8 @@ namespace Application.Common.Models.Dtos.Physical; -public class LockerDto : IMapFrom +public class LockerDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public string Name { get; set; } public string Description { get; set; } public RoomDto Room { get; set; } diff --git a/src/Application/Common/Models/Dtos/Physical/RoomDto.cs b/src/Application/Common/Models/Dtos/Physical/RoomDto.cs index 44fe55e0..ce9be4e2 100644 --- a/src/Application/Common/Models/Dtos/Physical/RoomDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/RoomDto.cs @@ -5,9 +5,8 @@ namespace Application.Common.Models.Dtos.Physical; -public class RoomDto : IMapFrom +public class RoomDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public string Name { get; set; } = null!; public string? Description { get; set; } public Guid? StaffId { get; set; } diff --git a/src/Application/Common/Models/Dtos/Physical/StaffDto.cs b/src/Application/Common/Models/Dtos/Physical/StaffDto.cs index cc6394f4..62ec45dc 100644 --- a/src/Application/Common/Models/Dtos/Physical/StaffDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/StaffDto.cs @@ -5,9 +5,8 @@ namespace Application.Common.Models.Dtos.Physical; -public class StaffDto : IMapFrom +public class StaffDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public UserDto User { get; set; } = null!; public RoomDto? Room { get; set; } diff --git a/src/Application/Common/Models/Dtos/UserDto.cs b/src/Application/Common/Models/Dtos/UserDto.cs index 3fe39e96..737b6e77 100644 --- a/src/Application/Common/Models/Dtos/UserDto.cs +++ b/src/Application/Common/Models/Dtos/UserDto.cs @@ -6,9 +6,8 @@ namespace Application.Users.Queries; -public class UserDto : IMapFrom +public class UserDto : BaseDto, IMapFrom { - public Guid Id { get; set; } public string Username { get; set; } public string Email { get; set; } public string FirstName { get; set; } diff --git a/src/Application/Documents/Queries/GetAllDocumentLogsPaginated.cs b/src/Application/Documents/Queries/GetAllDocumentLogsPaginated.cs index b018e472..fa3767f8 100644 --- a/src/Application/Documents/Queries/GetAllDocumentLogsPaginated.cs +++ b/src/Application/Documents/Queries/GetAllDocumentLogsPaginated.cs @@ -3,6 +3,8 @@ using Application.Common.Models; using Application.Common.Models.Dtos.Logging; using AutoMapper; +using Domain.Entities.Logging; +using Domain.Entities.Physical; using MediatR; using Microsoft.EntityFrameworkCore; @@ -42,18 +44,12 @@ public async Task> Handle(Query request, Cancellat x.Action.ToLower().Contains(request.SearchTerm.ToLower())); } - var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; - var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; - - var count = await logs.CountAsync(cancellationToken); - var list = await logs - .OrderByDescending(x => x.Time) - .Paginate(pageNumber.Value, sizeNumber.Value) - .ToListAsync(cancellationToken); - - var result = _mapper.Map>(list); - - return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + return await logs + .LoggingListPaginateAsync( + request.Page, + request.Size, + _mapper.ConfigurationProvider, + cancellationToken); } } } \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs b/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs index a8af3480..4f2f555f 100644 --- a/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs +++ b/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs @@ -6,6 +6,7 @@ using Application.Common.Models.Dtos.Physical; using AutoMapper; using AutoMapper.QueryableExtensions; +using Domain.Entities.Physical; using Domain.Statuses; using FluentValidation; using MediatR; @@ -126,24 +127,14 @@ public async Task> Handle(Query request, x.Title.ToLower().Contains(request.SearchTerm.ToLower())); } - var sortBy = request.SortBy; - if (sortBy is null || !sortBy.MatchesPropertyName()) - { - sortBy = nameof(DocumentDto.Id); - } - var sortOrder = request.SortOrder ?? "asc"; - var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; - var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; - - var count = await documents.CountAsync(cancellationToken); - var list = await documents - .OrderByCustom(sortBy, sortOrder) - .Paginate(pageNumber.Value, sizeNumber.Value) - .ToListAsync(cancellationToken); - - var result = _mapper.Map>(list); - - return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + return await documents + .ListPaginateWithFilterAsync( + request.Page, + request.Size, + request.SortBy, + request.SortOrder, + _mapper.ConfigurationProvider, + cancellationToken); } } } \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetAllIssuedDocumentsPaginated.cs b/src/Application/Documents/Queries/GetAllIssuedDocumentsPaginated.cs index 60a1cd78..6e1a7afe 100644 --- a/src/Application/Documents/Queries/GetAllIssuedDocumentsPaginated.cs +++ b/src/Application/Documents/Queries/GetAllIssuedDocumentsPaginated.cs @@ -51,24 +51,14 @@ public async Task> Handle(Query request, x.Title.ToLower().Contains(request.SearchTerm.ToLower())); } - var sortBy = request.SortBy; - if (sortBy is null || !sortBy.MatchesPropertyName()) - { - sortBy = nameof(IssuedDocumentDto.Id); - } - var sortOrder = request.SortOrder ?? "asc"; - var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; - var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; - - var count = await documents.CountAsync(cancellationToken); - var list = await documents - .OrderByCustom(sortBy, sortOrder) - .Paginate(pageNumber.Value, sizeNumber.Value) - .ToListAsync(cancellationToken); - - var result = _mapper.Map>(list); - - return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + return await documents + .ListPaginateWithFilterAsync( + request.Page, + request.Size, + request.SortBy, + request.SortOrder, + _mapper.ConfigurationProvider, + cancellationToken); } } } \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetDocumentsOfUserPaginated.cs b/src/Application/Documents/Queries/GetDocumentsOfUserPaginated.cs index 1f14d615..8bd86463 100644 --- a/src/Application/Documents/Queries/GetDocumentsOfUserPaginated.cs +++ b/src/Application/Documents/Queries/GetDocumentsOfUserPaginated.cs @@ -3,6 +3,7 @@ using Application.Common.Models; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities.Physical; using MediatR; using Microsoft.EntityFrameworkCore; @@ -47,24 +48,14 @@ public async Task> Handle(Query request, Cancellation .AsQueryable() .Where(x => x.Importer!.Id.Equals(request.UserId) && !x.IsPrivate); - var sortBy = request.SortBy; - if (sortBy is null || !sortBy.MatchesPropertyName()) - { - sortBy = nameof(DocumentDto.Id); - } - var sortOrder = request.SortOrder ?? "asc"; - var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; - var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; - - var count = await documents.CountAsync(cancellationToken); - var list = await documents - .Paginate(pageNumber.Value, sizeNumber.Value) - .OrderByCustom(sortBy, sortOrder) - .ToListAsync(cancellationToken); - - var result = _mapper.Map>(list); - - return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + return await documents + .ListPaginateWithFilterAsync( + request.Page, + request.Size, + request.SortBy, + request.SortOrder, + _mapper.ConfigurationProvider, + cancellationToken); } } } \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetSelfDocumentsPaginated.cs b/src/Application/Documents/Queries/GetSelfDocumentsPaginated.cs index ceebbed1..b531bd1a 100644 --- a/src/Application/Documents/Queries/GetSelfDocumentsPaginated.cs +++ b/src/Application/Documents/Queries/GetSelfDocumentsPaginated.cs @@ -3,6 +3,7 @@ using Application.Common.Models; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities.Physical; using MediatR; using Microsoft.EntityFrameworkCore; @@ -44,24 +45,14 @@ public async Task> Handle(Query request, Cancellation x.Title.ToLower().Contains(request.SearchTerm.ToLower())); } - var sortBy = request.SortBy; - if (sortBy is null || !sortBy.MatchesPropertyName()) - { - sortBy = nameof(DocumentDto.Id); - } - var sortOrder = request.SortOrder ?? "asc"; - var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; - var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; - - var count = await documents.CountAsync(cancellationToken); - var list = await documents - .OrderByCustom(sortBy, sortOrder) - .Paginate(pageNumber.Value, sizeNumber.Value) - .ToListAsync(cancellationToken); - - var result = _mapper.Map>(list); - - return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + return await documents + .ListPaginateWithFilterAsync( + request.Page, + request.Size, + request.SortBy, + request.SortOrder, + _mapper.ConfigurationProvider, + cancellationToken); } } } diff --git a/src/Application/Folders/Queries/GetAllFolderLogsPaginated.cs b/src/Application/Folders/Queries/GetAllFolderLogsPaginated.cs index ba286e23..41708a51 100644 --- a/src/Application/Folders/Queries/GetAllFolderLogsPaginated.cs +++ b/src/Application/Folders/Queries/GetAllFolderLogsPaginated.cs @@ -3,6 +3,8 @@ using Application.Common.Models; using Application.Common.Models.Dtos.Logging; using AutoMapper; +using Domain.Entities.Logging; +using Domain.Entities.Physical; using MediatR; using Microsoft.EntityFrameworkCore; @@ -40,18 +42,12 @@ public async Task> Handle(Query request, Cancellatio x.Action.Trim().ToLower().Contains(request.SearchTerm.Trim().ToLower())); } - var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; - var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; - - var count = await logs.CountAsync(cancellationToken); - var list = await logs - .OrderByDescending(x => x.Time) - .Paginate(pageNumber.Value, sizeNumber.Value) - .ToListAsync(cancellationToken); - - var result = _mapper.Map>(list); - - return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + return await logs + .LoggingListPaginateAsync( + request.Page, + request.Size, + _mapper.ConfigurationProvider, + cancellationToken); } } } \ No newline at end of file diff --git a/src/Application/Folders/Queries/GetAllFoldersPaginated.cs b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs index 530d039f..9f38c039 100644 --- a/src/Application/Folders/Queries/GetAllFoldersPaginated.cs +++ b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs @@ -95,24 +95,14 @@ public async Task> Handle(Query request, CancellationTo x.Name.ToLower().Contains(request.SearchTerm.ToLower())); } - var sortBy = request.SortBy; - if (sortBy is null || !sortBy.MatchesPropertyName()) - { - sortBy = nameof(LockerDto.Id); - } - var sortOrder = request.SortOrder ?? "asc"; - var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; - var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; - - var count = await folders.CountAsync(cancellationToken); - var list = await folders - .OrderByCustom(sortBy, sortOrder) - .Paginate(pageNumber.Value, sizeNumber.Value) - .ToListAsync(cancellationToken); - - var result = _mapper.Map>(list); - - return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + return await folders + .ListPaginateWithFilterAsync( + request.Page, + request.Size, + request.SortBy, + request.SortOrder, + _mapper.ConfigurationProvider, + cancellationToken); } } } \ No newline at end of file diff --git a/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs b/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs index af4bbdf6..2819ebbd 100644 --- a/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs +++ b/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs @@ -3,6 +3,8 @@ using Application.Common.Models; using Application.Common.Models.Dtos.Logging; using AutoMapper; +using Domain.Entities.Logging; +using Domain.Entities.Physical; using MediatR; using Microsoft.EntityFrameworkCore; @@ -40,18 +42,12 @@ public async Task> Handle(Query request, Cancellatio x.Action.Trim().ToLower().Contains(request.SearchTerm.Trim().ToLower())); } - var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; - var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; - - var count = await logs.CountAsync(cancellationToken); - var list = await logs - .OrderByDescending(x => x.Time) - .Paginate(pageNumber.Value, sizeNumber.Value) - .ToListAsync(cancellationToken); - - var result = _mapper.Map>(list); - - return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + return await logs + .LoggingListPaginateAsync( + request.Page, + request.Size, + _mapper.ConfigurationProvider, + cancellationToken); } } } diff --git a/src/Application/Lockers/Queries/GetAllLockersPaginated.cs b/src/Application/Lockers/Queries/GetAllLockersPaginated.cs index 3e08e523..5b10d9b2 100644 --- a/src/Application/Lockers/Queries/GetAllLockersPaginated.cs +++ b/src/Application/Lockers/Queries/GetAllLockersPaginated.cs @@ -5,6 +5,7 @@ using Application.Common.Models.Dtos.Physical; using AutoMapper; using AutoMapper.QueryableExtensions; +using Domain.Entities.Physical; using MediatR; using Microsoft.EntityFrameworkCore; @@ -51,24 +52,14 @@ public async Task> Handle(Query request, CancellationTo x.Name.ToLower().Contains(request.SearchTerm.ToLower())); } - var sortBy = request.SortBy; - if (sortBy is null || !sortBy.MatchesPropertyName()) - { - sortBy = nameof(LockerDto.Id); - } - var sortOrder = request.SortOrder ?? "asc"; - var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; - var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; - - var count = await lockers.CountAsync(cancellationToken); - var list = await lockers - .OrderByCustom(sortBy, sortOrder) - .Paginate(pageNumber.Value, sizeNumber.Value) - .ToListAsync(cancellationToken); - - var result = _mapper.Map>(list); - - return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + return await lockers + .ListPaginateWithFilterAsync( + request.Page, + request.Size, + request.SortBy, + request.SortOrder, + _mapper.ConfigurationProvider, + cancellationToken); } } } \ No newline at end of file diff --git a/src/Application/Rooms/Queries/GetAllRoomLogsPaginated.cs b/src/Application/Rooms/Queries/GetAllRoomLogsPaginated.cs index 1099f5ba..c3b7a501 100644 --- a/src/Application/Rooms/Queries/GetAllRoomLogsPaginated.cs +++ b/src/Application/Rooms/Queries/GetAllRoomLogsPaginated.cs @@ -3,6 +3,8 @@ using Application.Common.Models; using Application.Common.Models.Dtos.Logging; using AutoMapper; +using Domain.Entities.Logging; +using Domain.Entities.Physical; using MediatR; using Microsoft.EntityFrameworkCore; @@ -44,24 +46,12 @@ public async Task> Handle(Query request, CancellationT x.Action.ToLower().Contains(request.SearchTerm.ToLower())); } - var sortBy = request.SortBy; - if (sortBy is null || !sortBy.MatchesPropertyName()) - { - sortBy = nameof(RoomLogDto.Time); - } - var sortOrder = request.SortOrder ?? "desc"; - var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; - var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; - - var count = await logs.CountAsync(cancellationToken); - var list = await logs - .OrderByCustom(sortBy, sortOrder) - .Paginate(pageNumber.Value, sizeNumber.Value) - .ToListAsync(cancellationToken); - - var result = _mapper.Map>(list); - - return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + return await logs + .LoggingListPaginateAsync( + request.Page, + request.Size, + _mapper.ConfigurationProvider, + cancellationToken); } } } \ No newline at end of file diff --git a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs index ff89fe2b..f87b6778 100644 --- a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs +++ b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs @@ -5,6 +5,7 @@ using Application.Common.Models.Dtos.Physical; using AutoMapper; using AutoMapper.QueryableExtensions; +using Domain.Entities.Physical; using MediatR; using Microsoft.EntityFrameworkCore; @@ -45,24 +46,14 @@ public async Task> Handle(Query request, CancellationToke x.Name.ToLower().Contains(request.SearchTerm.ToLower())); } - var sortBy = request.SortBy; - if (sortBy is null || !sortBy.MatchesPropertyName()) - { - sortBy = nameof(RoomDto.Id); - } - var sortOrder = request.SortOrder ?? "asc"; - var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; - var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; - - var count = await rooms.CountAsync(cancellationToken); - var list = await rooms - .OrderByCustom(sortBy, sortOrder) - .Paginate(pageNumber.Value, sizeNumber.Value) - .ToListAsync(cancellationToken); - - var result = _mapper.Map>(list); - - return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + return await rooms + .ListPaginateWithFilterAsync( + request.Page, + request.Size, + request.SortBy, + request.SortOrder, + _mapper.ConfigurationProvider, + cancellationToken); } } } \ No newline at end of file diff --git a/src/Application/Staffs/Queries/GetAllStaffsPaginated.cs b/src/Application/Staffs/Queries/GetAllStaffsPaginated.cs index 61f62046..76a62584 100644 --- a/src/Application/Staffs/Queries/GetAllStaffsPaginated.cs +++ b/src/Application/Staffs/Queries/GetAllStaffsPaginated.cs @@ -5,6 +5,7 @@ using Application.Common.Models.Dtos.Physical; using AutoMapper; using AutoMapper.QueryableExtensions; +using Domain.Entities.Physical; using MediatR; using Microsoft.EntityFrameworkCore; @@ -45,25 +46,14 @@ public async Task> Handle(Query request, CancellationTok staffs = staffs.Where(x => x.User.Username.ToLower().Contains(request.SearchTerm.ToLower())); } - var sortBy = request.SortBy; - if (sortBy is null || !sortBy.MatchesPropertyName()) - { - sortBy = nameof(StaffDto.Id); - } - - var sortOrder = request.SortOrder ?? "asc"; - var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; - var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; - - var count = await staffs.CountAsync(cancellationToken); - var list = await staffs - .OrderByCustom(sortBy, sortOrder) - .Paginate(pageNumber.Value,sizeNumber.Value) - .ToListAsync(cancellationToken); - - var result = _mapper.Map>(list); - - return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + return await staffs + .ListPaginateWithFilterAsync( + request.Page, + request.Size, + request.SortBy, + request.SortOrder, + _mapper.ConfigurationProvider, + cancellationToken); } } } \ No newline at end of file diff --git a/src/Application/Users/Commands/AddUser.cs b/src/Application/Users/Commands/AddUser.cs index 5d3f63c5..cebb593b 100644 --- a/src/Application/Users/Commands/AddUser.cs +++ b/src/Application/Users/Commands/AddUser.cs @@ -55,7 +55,7 @@ private static bool BeNotAdmin(string role) public record Command : IRequest { - public Guid PerformingUserId { get; init; } + public User PerformingUser { get; init; } = null!; public string Username { get; init; } = null!; public string Email { get; init; } = null!; public string? FirstName { get; init; } @@ -99,7 +99,6 @@ public async Task Handle(Command request, CancellationToken cancellatio var password = StringUtil.RandomPassword(); var salt = StringUtil.RandomSalt(); - var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); var entity = new User { Username = request.Username, @@ -114,12 +113,12 @@ public async Task Handle(Command request, CancellationToken cancellatio IsActive = true, IsActivated = false, Created = LocalDateTime.FromDateTime(DateTime.Now), - CreatedBy = performingUser!.Id, + CreatedBy = request.PerformingUser.Id, }; var log = new UserLog() { - User = performingUser, - UserId = performingUser.Id, + User = request.PerformingUser, + UserId = request.PerformingUser.Id, Object = entity, Time = LocalDateTime.FromDateTime(DateTime.Now), Action = UserLogMessages.Add, diff --git a/src/Application/Users/Commands/UpdateUser.cs b/src/Application/Users/Commands/UpdateUser.cs index c13836d9..30e401c2 100644 --- a/src/Application/Users/Commands/UpdateUser.cs +++ b/src/Application/Users/Commands/UpdateUser.cs @@ -2,6 +2,7 @@ using Application.Common.Messages; using Application.Users.Queries; using AutoMapper; +using Domain.Entities; using Domain.Entities.Logging; using FluentValidation; using MediatR; @@ -30,12 +31,13 @@ public Validator() } public record Command : IRequest { - public Guid PerformingUserId { get; init; } + public User PerformingUser { get; init; } = null!; public Guid UserId { get; init; } public string? FirstName { get; init; } public string? LastName { get; init; } public string? Position { get; init; } - } + public string Role { get; init; } = null!; + public bool IsActive { get; init; } } public class CommandHandler : IRequestHandler { @@ -58,16 +60,17 @@ public async Task Handle(Command request, CancellationToken cancellatio throw new KeyNotFoundException("User does not exist."); } - var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); user.FirstName = request.FirstName; user.LastName = request.LastName; user.Position = request.Position; + user.Role = request.Role; + user.IsActive = request.IsActive; user.LastModified = LocalDateTime.FromDateTime(DateTime.Now); - user.LastModifiedBy = performingUser!.Id; + user.LastModifiedBy = request.PerformingUser.Id; var log = new UserLog() { - User = performingUser, - UserId = performingUser.Id, + User = request.PerformingUser, + UserId = request.PerformingUser.Id, Object = user, Time = LocalDateTime.FromDateTime(DateTime.Now), Action = UserLogMessages.Update, diff --git a/src/Application/Users/Queries/GetAllEmployeesPaginated.cs b/src/Application/Users/Queries/GetAllEmployeesPaginated.cs index d5375a02..670f772b 100644 --- a/src/Application/Users/Queries/GetAllEmployeesPaginated.cs +++ b/src/Application/Users/Queries/GetAllEmployeesPaginated.cs @@ -3,6 +3,7 @@ using Application.Common.Models; using Application.Identity; using AutoMapper; +using Domain.Entities; using MediatR; using Microsoft.EntityFrameworkCore; @@ -32,31 +33,21 @@ public Handler(IApplicationDbContext context, IMapper mapper) public async Task> Handle(Query request, CancellationToken cancellationToken) { - var users = _context.Users.AsQueryable() + var users = _context.Users .Include(x => x.Department) - .Where(x => x.Department!.Id == request.DepartmentId + .Where(x => x.Department!.Id == request.DepartmentId && x.Role.Equals(IdentityData.Roles.Employee) && x.IsActive && x.IsActivated); - - var sortBy = request.SortBy; - if (sortBy is null || !sortBy.MatchesPropertyName()) - { - sortBy = nameof(UserDto.Id); - } - var sortOrder = request.SortOrder ?? "asc"; - var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; - var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; - - var count = await users.CountAsync(cancellationToken); - var list = await users - .OrderByCustom(sortBy, sortOrder) - .Paginate(pageNumber.Value, sizeNumber.Value) - .ToListAsync(cancellationToken); - var result = _mapper.Map>(list); - - return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + return await users + .ListPaginateWithFilterAsync( + request.Page, + request.Size, + request.SortBy, + request.SortOrder, + _mapper.ConfigurationProvider, + cancellationToken); } } } diff --git a/src/Application/Users/Queries/GetAllUserLogsPaginated.cs b/src/Application/Users/Queries/GetAllUserLogsPaginated.cs index 50ea4eb3..890ee474 100644 --- a/src/Application/Users/Queries/GetAllUserLogsPaginated.cs +++ b/src/Application/Users/Queries/GetAllUserLogsPaginated.cs @@ -3,6 +3,8 @@ using Application.Common.Models; using Application.Common.Models.Dtos.Logging; using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; using MediatR; using Microsoft.EntityFrameworkCore; @@ -40,18 +42,12 @@ public async Task> Handle(Query request, CancellationT x.Action.Trim().ToLower().Contains(request.SearchTerm.Trim().ToLower())); } - var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; - var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; - - var count = await logs.CountAsync(cancellationToken); - var list = await logs - .OrderByDescending(x => x.Time) - .Paginate(pageNumber.Value, sizeNumber.Value) - .ToListAsync(cancellationToken); - - var result = _mapper.Map>(list); - - return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + return await logs + .LoggingListPaginateAsync( + request.Page, + request.Size, + _mapper.ConfigurationProvider, + cancellationToken); } } } \ No newline at end of file diff --git a/src/Application/Users/Queries/GetAllUsersPaginated.cs b/src/Application/Users/Queries/GetAllUsersPaginated.cs index e0690bb4..591d1897 100644 --- a/src/Application/Users/Queries/GetAllUsersPaginated.cs +++ b/src/Application/Users/Queries/GetAllUsersPaginated.cs @@ -6,6 +6,7 @@ using Application.Identity; using AutoMapper; using AutoMapper.QueryableExtensions; +using Domain.Entities; using MediatR; using Microsoft.EntityFrameworkCore; @@ -50,24 +51,14 @@ public async Task> Handle(Query request, CancellationToke x.FirstName!.ToLower().Contains(request.SearchTerm.Trim().ToLower())); } - var sortBy = request.SortBy; - if (sortBy is null || !sortBy.MatchesPropertyName()) - { - sortBy = nameof(UserDto.Id); - } - var sortOrder = request.SortOrder ?? "asc"; - var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; - var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; - - var count = await users.CountAsync(cancellationToken); - var list = await users - .OrderByCustom(sortBy, sortOrder) - .Paginate(pageNumber.Value, sizeNumber.Value) - .ToListAsync(cancellationToken); - - var result = _mapper.Map>(list); - - return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + return await users + .ListPaginateWithFilterAsync( + request.Page, + request.Size, + request.SortBy, + request.SortOrder, + _mapper.ConfigurationProvider, + cancellationToken); } } } \ No newline at end of file From 5935e06a33bf5f57b67e49bbea4b9b67e3d02c10 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Fri, 16 Jun 2023 14:13:51 +0700 Subject: [PATCH 41/56] fuck me 3 --- .vscode/settings.json | 3 - src/Api/Controllers/DepartmentsController.cs | 52 ++++++- src/Api/Controllers/LockersController.cs | 13 +- .../GetAllLogsPaginatedQueryParameters.cs | 3 + .../Requests/Rooms/UpdateRoomRequest.cs | 4 + ...GetAllEmployeesPaginatedQueryParameters.cs | 5 +- .../GetAllUsersPaginatedQueryParameters.cs | 3 +- .../Requests/Users/UpdateSelfRequest.cs | 16 +++ src/Api/Controllers/RoomsController.cs | 129 +++++++----------- src/Api/Controllers/UsersController.cs | 107 ++++++++++----- src/Api/Services/CurrentUserService.cs | 4 +- src/Application/Application.csproj | 1 + .../Common/Extensions/StringExtensions.cs | 11 ++ .../Common/Interfaces/ICurrentUserService.cs | 2 +- .../Departments/Queries/GetDepartmentById.cs | 9 ++ .../Lockers/Queries/GetAllLockersPaginated.cs | 33 ++++- .../Lockers/Queries/GetLockerById.cs | 15 ++ src/Application/Rooms/Commands/RemoveRoom.cs | 7 +- src/Application/Rooms/Commands/UpdateRoom.cs | 34 ++--- .../Queries/GetEmptyContainersPaginated.cs | 2 +- .../Rooms/Queries/GetRoomByDepartmentId.cs | 43 ++++++ src/Application/Rooms/Queries/GetRoomById.cs | 2 +- ...{GetLogOfRoomById.cs => GetRoomLogById.cs} | 4 +- src/Application/Users/Commands/UpdateUser.cs | 20 ++- .../Users/Queries/GetAllEmployeesPaginated.cs | 53 ------- .../Users/Queries/GetAllUserLogsPaginated.cs | 4 +- .../Users/Queries/GetAllUsersPaginated.cs | 21 +-- src/Application/Users/Queries/GetUserById.cs | 19 ++- .../Identity/IdentityService.cs | 2 +- 29 files changed, 393 insertions(+), 228 deletions(-) delete mode 100644 .vscode/settings.json create mode 100644 src/Api/Controllers/Payload/Requests/Users/UpdateSelfRequest.cs create mode 100644 src/Application/Rooms/Queries/GetRoomByDepartmentId.cs rename src/Application/Rooms/Queries/{GetLogOfRoomById.cs => GetRoomLogById.cs} (90%) delete mode 100644 src/Application/Users/Queries/GetAllEmployeesPaginated.cs diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index cdc2203c..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "dotnet.defaultSolution": "ProFile.sln" -} \ No newline at end of file diff --git a/src/Api/Controllers/DepartmentsController.cs b/src/Api/Controllers/DepartmentsController.cs index 4414b2af..9771b95c 100644 --- a/src/Api/Controllers/DepartmentsController.cs +++ b/src/Api/Controllers/DepartmentsController.cs @@ -1,9 +1,12 @@ using Api.Controllers.Payload.Requests.Departments; +using Application.Common.Interfaces; using Application.Common.Models; using Application.Common.Models.Dtos; +using Application.Common.Models.Dtos.Physical; using Application.Departments.Commands; using Application.Departments.Queries; using Application.Identity; +using Application.Rooms.Queries; using Application.Users.Queries; using Infrastructure.Identity.Authorization; using Microsoft.AspNetCore.Mvc; @@ -12,29 +15,67 @@ namespace Api.Controllers; public class DepartmentsController : ApiControllerBase { + private readonly ICurrentUserService _currentUserService; + + public DepartmentsController(ICurrentUserService currentUserService) + { + _currentUserService = currentUserService; + } + /// /// Get back a department based on its id /// /// id of the department to be retrieved /// A DepartmentDto of the retrieved department + [RequiresRole( + IdentityData.Roles.Admin, + IdentityData.Roles.Staff, + IdentityData.Roles.Employee)] [HttpGet("{departmentId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetById([FromRoute] Guid departmentId) + public async Task>> GetById( + [FromRoute] Guid departmentId) { + var role = _currentUserService.GetRole(); + var userDepartmentId = _currentUserService.GetDepartmentId(); var query = new GetDepartmentById.Query() { + UserRole = role, + UserDepartmentId = userDepartmentId, DepartmentId = departmentId, }; var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); } + /// + /// Get back a department based on its id + /// + /// id of the department to be retrieved + /// A DepartmentDto of the retrieved department + [RequiresRole(IdentityData.Roles.Admin)] + [HttpGet("{departmentId:guid}/rooms")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetRoomByDepartmentId( + [FromRoute] Guid departmentId) + { + var query = new GetRoomByDepartmentId.Query() + { + DepartmentId = departmentId, + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } + /// /// Get all documents /// /// A list of DocumentDto + [RequiresRole(IdentityData.Roles.Admin)] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -54,7 +95,8 @@ public async Task>>> GetAll() [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Add([FromBody] AddDepartmentRequest request) + public async Task>> Add( + [FromBody] AddDepartmentRequest request) { var command = new AddDepartment.Command() { @@ -75,12 +117,14 @@ public async Task>> Add([FromBody] AddDepartm [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> Update([FromRoute] Guid departmentId, [FromBody] UpdateDepartmentRequest request) + public async Task>> Update( + [FromRoute] Guid departmentId, + [FromBody] UpdateDepartmentRequest request) { var command = new UpdateDepartment.Command() { DepartmentId = departmentId, - Name = request.Name + Name = request.Name, }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); diff --git a/src/Api/Controllers/LockersController.cs b/src/Api/Controllers/LockersController.cs index 16a40300..24a7ad4e 100644 --- a/src/Api/Controllers/LockersController.cs +++ b/src/Api/Controllers/LockersController.cs @@ -26,14 +26,20 @@ public LockersController(ICurrentUserService currentUserService) /// /// Id of the locker to be retrieved /// A LockerDto of the retrieved locker + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpGet("{lockerId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetById([FromRoute] Guid lockerId) + public async Task>> GetById( + [FromRoute] Guid lockerId) { + var currentUserRole = _currentUserService.GetRole(); + var currentUserDepartmentId = _currentUserService.GetDepartmentId(); var query = new GetLockerById.Query() { + CurrentUserRole = currentUserRole, + CurrentUserDepartmentId = currentUserDepartmentId, LockerId = lockerId, }; var result = await Mediator.Send(query); @@ -45,14 +51,19 @@ public async Task>> GetById([FromRoute] Guid lock /// /// Get all lockers paginated query parameters /// A paginated list of LockerDto + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task>>> GetAllPaginated( [FromQuery] GetAllLockersPaginatedQueryParameters queryParameters) { + var currentUserRole = _currentUserService.GetRole(); + var currentUserDepartmentId = _currentUserService.GetDepartmentId(); var query = new GetAllLockersPaginated.Query() { + CurrentUserRole = currentUserRole, + CurrentUserDepartmentId = currentUserDepartmentId, RoomId = queryParameters.RoomId, SearchTerm = queryParameters.SearchTerm, Page = queryParameters.Page, diff --git a/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs index 7eceac63..6bbb0f60 100644 --- a/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs @@ -5,6 +5,9 @@ /// public class GetAllLogsPaginatedQueryParameters { + /// + /// Search term + /// public string? SearchTerm { get; set; } /// /// Page number diff --git a/src/Api/Controllers/Payload/Requests/Rooms/UpdateRoomRequest.cs b/src/Api/Controllers/Payload/Requests/Rooms/UpdateRoomRequest.cs index 40d4965c..d277d097 100644 --- a/src/Api/Controllers/Payload/Requests/Rooms/UpdateRoomRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Rooms/UpdateRoomRequest.cs @@ -17,4 +17,8 @@ public class UpdateRoomRequest /// New capacity of the room to be updated /// public int Capacity { get; set; } + /// + /// Room availability + /// + public bool IsAvailable { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Users/GetAllEmployeesPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Users/GetAllEmployeesPaginatedQueryParameters.cs index 4a53d7f5..171bce79 100644 --- a/src/Api/Controllers/Payload/Requests/Users/GetAllEmployeesPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Users/GetAllEmployeesPaginatedQueryParameters.cs @@ -2,5 +2,8 @@ public class GetAllEmployeesPaginatedQueryParameters : PaginatedQueryParameters { - + /// + /// Search term + /// + public string? SearchTerm { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs index 47db40c8..7175d8c1 100644 --- a/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs @@ -8,7 +8,8 @@ public class GetAllUsersPaginatedQueryParameters : PaginatedQueryParameters /// /// Id of the department to find users in /// - public Guid? DepartmentId { get; set; } + public Guid[]? DepartmentIds { get; set; } + public string Role { get; set; } /// /// Search term /// diff --git a/src/Api/Controllers/Payload/Requests/Users/UpdateSelfRequest.cs b/src/Api/Controllers/Payload/Requests/Users/UpdateSelfRequest.cs new file mode 100644 index 00000000..7b12e5bf --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Users/UpdateSelfRequest.cs @@ -0,0 +1,16 @@ +namespace Api.Controllers.Payload.Requests.Users; + +/// +/// Request details to update that user +/// +public class UpdateSelfRequest +{ + /// + /// New first name of the user to be updated + /// + public string? FirstName { get; set; } + /// + /// New last name of the user to be updated + /// + public string? LastName { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/RoomsController.cs b/src/Api/Controllers/RoomsController.cs index efc664bc..9483d469 100644 --- a/src/Api/Controllers/RoomsController.cs +++ b/src/Api/Controllers/RoomsController.cs @@ -27,11 +27,13 @@ public RoomsController(ICurrentUserService currentUserService) /// /// Id of the room to be retrieved /// A RoomDto of the retrieved room + [RequiresRole(IdentityData.Roles.Admin)] [HttpGet("{roomId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetById([FromRoute] Guid roomId) + public async Task>> GetById( + [FromRoute] Guid roomId) { var query = new GetRoomById.Query() { @@ -65,48 +67,6 @@ public async Task>>> GetAllPaginated( return Ok(Result>.Succeed(result)); } - /// - /// Get a room log by id - /// - /// - /// return a RoomLogDto - [RequiresRole(IdentityData.Roles.Admin)] - [HttpGet("log/{logId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetLogById([FromRoute] Guid logId) - { - var query = new GetLogOfRoomById.Query() - { - LogId = logId - }; - - var result = await Mediator.Send(query); - return Ok(Result.Succeed(result)); - } - - /// - /// Get all room logs paginated - /// - /// - /// A paginated list of RoomLogDto - [RequiresRole(IdentityData.Roles.Admin)] - [HttpGet("logs")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task>>> GetAllLogsPaginated( - [FromQuery] GetAllLogsPaginatedQueryParameters queryParameters) - { - var query = new GetAllRoomLogsPaginated.Query() - { - SearchTerm = queryParameters.SearchTerm, - Page = queryParameters.Page, - Size = queryParameters.Size, - }; - - var result = await Mediator.Send(query); - return Ok(Result>.Succeed(result)); - } - /// /// Get empty containers in a room /// @@ -114,7 +74,7 @@ public async Task>>> GetAllLogsPag /// Get empty containers paginated details /// A paginated list of EmptyLockerDto [RequiresRole(IdentityData.Roles.Staff)] - [HttpPost("empty-containers/{roomId:guid}")] + [HttpPost("{roomId:guid}/empty-containers")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -144,7 +104,8 @@ public async Task>> GetEmptyContainer [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> AddRoom([FromBody] AddRoomRequest request) + public async Task>> AddRoom( + [FromBody] AddRoomRequest request) { var performingUserId = _currentUserService.GetId(); var command = new AddRoom.Command() @@ -170,7 +131,8 @@ public async Task>> AddRoom([FromBody] AddRoomReque [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> RemoveRoom([FromRoute] Guid roomId) + public async Task>> RemoveRoom( + [FromRoute] Guid roomId) { var command = new RemoveRoom.Command() { @@ -181,69 +143,72 @@ public async Task>> RemoveRoom([FromRoute] Guid roo } /// - /// Enable a room + /// Update a room /// - /// Id of the room to be enabled - /// A RoomDto of the enabled room - [HttpPut("enable/{roomId:guid}")] + /// Id of the room to be updated + /// Update room details + /// A RoomDto of the updated room + [HttpPut("{roomId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> EnableRoom([FromRoute] Guid roomId) + public async Task>> Update( + [FromRoute] Guid roomId, + [FromBody] UpdateRoomRequest request) { - var command = new EnableRoom.Command() + var performingUserId = _currentUserService.GetId(); + var command = new UpdateRoom.Command() { + PerformingUserId = performingUserId, RoomId = roomId, + Name = request.Name, + Description = request.Description, + Capacity = request.Capacity, + IsAvailable = request.IsAvailable, }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } - + /// - /// Disable a room + /// Get all room logs paginated /// - /// Id of the room to be disabled - /// A RoomDto of the disabled room + /// + /// A paginated list of RoomLogDto [RequiresRole(IdentityData.Roles.Admin)] - [HttpPut("disable/{roomId:guid}")] + [HttpGet("logs")] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> DisableRoom([FromRoute] Guid roomId) + public async Task>>> GetAllLogsPaginated( + [FromQuery] GetAllLogsPaginatedQueryParameters queryParameters) { - var command = new DisableRoom.Command() + var query = new GetAllRoomLogsPaginated.Query() { - RoomId = roomId, + SearchTerm = queryParameters.SearchTerm, + Page = queryParameters.Page, + Size = queryParameters.Size, }; - var result = await Mediator.Send(command); - return Ok(Result.Succeed(result)); + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); } /// - /// Update a room + /// Get a room log by id /// - /// Id of the room to be updated - /// Update room details - /// A RoomDto of the updated room - [HttpPut("{roomId:guid}")] + /// + /// A RoomLogDto + [RequiresRole(IdentityData.Roles.Admin)] + [HttpGet("logs/{logId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Update([FromRoute] Guid roomId, [FromBody] UpdateRoomRequest request) + public async Task>> GetLogById( + [FromRoute] Guid logId) { - var performingUserId = _currentUserService.GetId(); - var command = new UpdateRoom.Command() + var query = new GetRoomLogById.Query() { - PerformingUserId = performingUserId, - RoomId = roomId, - Name = request.Name, - Description = request.Description, - Capacity = request.Capacity, + LogId = logId, }; - var result = await Mediator.Send(command); - return Ok(Result.Succeed(result)); + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); } } \ No newline at end of file diff --git a/src/Api/Controllers/UsersController.cs b/src/Api/Controllers/UsersController.cs index 556006be..974ea323 100644 --- a/src/Api/Controllers/UsersController.cs +++ b/src/Api/Controllers/UsersController.cs @@ -26,14 +26,19 @@ public UsersController(ICurrentUserService currentUserService) /// /// Id of the user to be retrieved /// A UserDto of the retrieved user + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff, IdentityData.Roles.Employee)] [HttpGet("{userId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetById([FromRoute] Guid userId) { - var query = new GetUserById.Query + var role = _currentUserService.GetRole(); + var userDepartmentId = _currentUserService.GetDepartmentId(); + var query = new GetUserById.Query() { + UserRole = role, + UserDepartmentId = userDepartmentId, UserId = userId, }; var result = await Mediator.Send(query); @@ -45,6 +50,7 @@ public async Task>> GetById([FromRoute] Guid userId /// /// Get all users query parameters /// A paginated list of UserDto + [RequiresRole(IdentityData.Roles.Admin)] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -53,7 +59,36 @@ public async Task>>> GetAllPaginated( { var query = new GetAllUsersPaginated.Query() { - DepartmentId = queryParameters.DepartmentId, + DepartmentIds = queryParameters.DepartmentIds, + Role = queryParameters.Role, + SearchTerm = queryParameters.SearchTerm, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } + + /// + /// Get all employees in the same department + /// + /// Query parameters + /// A list of UserDtos + [RequiresRole(IdentityData.Roles.Staff, IdentityData.Roles.Employee)] + [HttpGet("employees")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>>> GetAllEmployeesPaginated( + [FromQuery] GetAllEmployeesPaginatedQueryParameters queryParameters) + { + var departmentId = _currentUserService.GetDepartmentId(); + var query = new GetAllUsersPaginated.Query() + { + DepartmentIds = new []{ departmentId }, + Role = IdentityData.Roles.Employee, SearchTerm = queryParameters.SearchTerm, Page = queryParameters.Page, Size = queryParameters.Size, @@ -69,6 +104,7 @@ public async Task>>> GetAllPaginated( /// /// Add user details /// A UserDto of the added user + [RequiresRole(IdentityData.Roles.Admin)] [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -99,17 +135,20 @@ public async Task>> Add([FromBody] AddUserRequest r /// Id of the user to be updated /// Update user details /// A UserDto of the updated user + [RequiresRole(IdentityData.Roles.Admin)] [HttpPut("{userId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Update([FromRoute] Guid userId, [FromBody] UpdateUserRequest request) + public async Task>> Update( + [FromRoute] Guid userId, + [FromBody] UpdateUserRequest request) { - var performingUser = _currentUserService.GetCurrentUser(); + var currentUser = _currentUserService.GetCurrentUser(); var command = new UpdateUser.Command() { - PerformingUser = performingUser, + CurrentUser = currentUser, UserId = userId, FirstName = request.FirstName, LastName = request.LastName, @@ -120,47 +159,46 @@ public async Task>> Update([FromRoute] Guid userId, var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } - + /// - /// Get all employees in the same department + /// Update a user /// - /// Query parameters - /// A list of UserDtos - [HttpGet] + /// Update user details + /// A UserDto of the updated user + [RequiresRole(IdentityData.Roles.Staff, IdentityData.Roles.Employee)] + [HttpPut("self")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>>> GetAllEmployeesPaginated( - [FromQuery] GetAllEmployeesPaginatedQueryParameters queryParameters) + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> UpdateSelf( + [FromBody] UpdateSelfRequest request) { - var departmentId = _currentUserService.GetDepartmentId(); - - if (departmentId is null) - { - throw new KeyNotFoundException("User does not belong to a department."); - } - - var query = new GetAllEmployeesPaginated.Query() + var currentUser = _currentUserService.GetCurrentUser(); + var command = new UpdateUser.Command() { - DepartmentId = departmentId.Value, - Page = queryParameters.Page, - Size = queryParameters.Size, - SortBy = queryParameters.SortBy, - SortOrder = queryParameters.SortOrder, + CurrentUser = currentUser, + UserId = currentUser.Id, + FirstName = request.FirstName, + LastName = request.LastName, + Position = currentUser.Position, + Role = currentUser.Role, + IsActive = currentUser.IsActive, }; - var result = await Mediator.Send(query); - return Ok(Result>.Succeed(result)); + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); } - + /// /// Get all user related logs paginated /// /// Get all users related logs query parameters /// A paginated list of UserLogDto + [RequiresRole(IdentityData.Roles.Admin)] [HttpGet("logs")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task>>> GetAllUserLogs( + public async Task>>> GetAllLogsPaginated( [FromQuery] GetAllLogsPaginatedQueryParameters queryParameters) { var query = new GetAllUserLogsPaginated.Query() @@ -177,18 +215,19 @@ public async Task>>> GetAllUserLog /// Get user related log by Id /// /// Id of the logged user - /// UserLogDto of the logged user + /// A UserLogDto of the logged user [RequiresRole(IdentityData.Roles.Admin)] - [HttpGet("log/{logId:guid}")] + [HttpGet("logs/{logId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task>> GetUserLogById([FromRoute] Guid logId) + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetLogById( + [FromRoute] Guid logId) { var query = new GetUserLogById.Query() { - LogId = logId + LogId = logId, }; - var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); } diff --git a/src/Api/Services/CurrentUserService.cs b/src/Api/Services/CurrentUserService.cs index b0b7d5ea..fd8d2b2f 100644 --- a/src/Api/Services/CurrentUserService.cs +++ b/src/Api/Services/CurrentUserService.cs @@ -42,12 +42,12 @@ public string GetRole() return user.Role; } - public Guid? GetDepartmentId() + public Guid GetDepartmentId() { var claim = _httpContextAccessor.HttpContext!.User.Claims .FirstOrDefault(x => x.Type.Equals("departmentId")); var id = claim?.Value; - return id is not null && Guid.TryParse(id, out _) ? Guid.Parse(id) : null; + return Guid.Parse(id!); } public User GetCurrentUser() diff --git a/src/Application/Application.csproj b/src/Application/Application.csproj index f1789974..2f6bfb3b 100644 --- a/src/Application/Application.csproj +++ b/src/Application/Application.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Application/Common/Extensions/StringExtensions.cs b/src/Application/Common/Extensions/StringExtensions.cs index b2a723de..f0646068 100644 --- a/src/Application/Common/Extensions/StringExtensions.cs +++ b/src/Application/Common/Extensions/StringExtensions.cs @@ -1,3 +1,5 @@ +using Application.Identity; + namespace Application.Common.Extensions; public static class StringExtensions @@ -10,4 +12,13 @@ public static bool MatchesPropertyName(this string input) return properties.Any(property => string.Equals(property.Name, input)); } + + public static bool IsAdmin(this string role) + => role.Equals(IdentityData.Roles.Admin); + + public static bool IsStaff(this string role) + => role.Equals(IdentityData.Roles.Staff); + + public static bool IsEmployee(this string role) + => role.Equals(IdentityData.Roles.Employee); } \ No newline at end of file diff --git a/src/Application/Common/Interfaces/ICurrentUserService.cs b/src/Application/Common/Interfaces/ICurrentUserService.cs index 91f58a14..a5528245 100644 --- a/src/Application/Common/Interfaces/ICurrentUserService.cs +++ b/src/Application/Common/Interfaces/ICurrentUserService.cs @@ -6,7 +6,7 @@ public interface ICurrentUserService { Guid GetId(); string GetRole(); - Guid? GetDepartmentId(); + Guid GetDepartmentId(); User GetCurrentUser(); Guid? GetCurrentRoomForStaff(); Guid? GetCurrentDepartmentForStaff(); diff --git a/src/Application/Departments/Queries/GetDepartmentById.cs b/src/Application/Departments/Queries/GetDepartmentById.cs index 83f3f725..0073a6e0 100644 --- a/src/Application/Departments/Queries/GetDepartmentById.cs +++ b/src/Application/Departments/Queries/GetDepartmentById.cs @@ -1,6 +1,7 @@ using Application.Common.Interfaces; using Application.Common.Models.Dtos; using Application.Common.Models.Dtos.Physical; +using Application.Identity; using AutoMapper; using MediatR; using Microsoft.EntityFrameworkCore; @@ -11,6 +12,8 @@ public class GetDepartmentById { public record Query : IRequest { + public string UserRole { get; init; } = null!; + public Guid UserDepartmentId { get; init; } public Guid DepartmentId { get; init; } } public class QueryHandler : IRequestHandler @@ -26,6 +29,12 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task Handle(Query request, CancellationToken cancellationToken) { + if ((request.UserRole.Equals(IdentityData.Roles.Staff) || request.UserRole.Equals(IdentityData.Roles.Employee)) + && request.UserDepartmentId != request.DepartmentId) + { + throw new UnauthorizedAccessException("User cannot access this department."); + } + var department = await _context.Departments.FirstOrDefaultAsync(x => x.Id.Equals(request.DepartmentId), cancellationToken); if (department is null) diff --git a/src/Application/Lockers/Queries/GetAllLockersPaginated.cs b/src/Application/Lockers/Queries/GetAllLockersPaginated.cs index 5b10d9b2..c324ba80 100644 --- a/src/Application/Lockers/Queries/GetAllLockersPaginated.cs +++ b/src/Application/Lockers/Queries/GetAllLockersPaginated.cs @@ -1,3 +1,4 @@ +using Application.Common.Exceptions; using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Mappings; @@ -15,7 +16,9 @@ public class GetAllLockersPaginated { public record Query : IRequest> { - public Guid? RoomId { get; init; } + public string CurrentUserRole { get; init; } = null!; + public Guid CurrentUserDepartmentId { get; init; } + public Guid? RoomId { get; set; } public string? SearchTerm { get; init; } public int? Page { get; init; } public int? Size { get; init; } @@ -41,6 +44,26 @@ public async Task> Handle(Query request, CancellationTo .ThenInclude(y => y.Department) .AsQueryable(); + if (request.CurrentUserRole.IsStaff()) + { + if (request.RoomId is null) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + + var currentUserRoom = await GetRoomByDepartmentIdAsync(request.CurrentUserDepartmentId, cancellationToken); + + if (currentUserRoom is null) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + + if (!IsSameRoom(currentUserRoom.Id, request.RoomId.Value)) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + } + if (request.RoomId is not null) { lockers = lockers.Where(x => x.Room.Id == request.RoomId); @@ -61,5 +84,13 @@ public async Task> Handle(Query request, CancellationTo _mapper.ConfigurationProvider, cancellationToken); } + + private async Task GetRoomByDepartmentIdAsync(Guid departmentId, CancellationToken cancellationToken) + => await _context.Rooms.FirstOrDefaultAsync( + x => x.DepartmentId == departmentId, + cancellationToken); + + private static bool IsSameRoom(Guid currentUserRoomId, Guid roomId) + => currentUserRoomId == roomId; } } \ No newline at end of file diff --git a/src/Application/Lockers/Queries/GetLockerById.cs b/src/Application/Lockers/Queries/GetLockerById.cs index 64a1bf5a..42f07b8a 100644 --- a/src/Application/Lockers/Queries/GetLockerById.cs +++ b/src/Application/Lockers/Queries/GetLockerById.cs @@ -1,5 +1,7 @@ +using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; +using Application.Identity; using AutoMapper; using MediatR; using Microsoft.EntityFrameworkCore; @@ -10,6 +12,8 @@ public class GetLockerById { public record Query : IRequest { + public string CurrentUserRole { get; init; } = null!; + public Guid CurrentUserDepartmentId { get; init; } public Guid LockerId { get; init; } } @@ -36,7 +40,18 @@ public async Task Handle(Query request, CancellationToken cancellatio throw new KeyNotFoundException("Locker does not exist."); } + if (request.CurrentUserRole.IsStaff() + && !LockerInSameDepartment(locker.Room.DepartmentId, request.CurrentUserDepartmentId)) + { + throw new UnauthorizedAccessException(); + } + return _mapper.Map(locker); } + + private static bool LockerInSameDepartment( + Guid lockerDepartmentId, + Guid currentUserDepartmentId) + => lockerDepartmentId == currentUserDepartmentId; } } \ No newline at end of file diff --git a/src/Application/Rooms/Commands/RemoveRoom.cs b/src/Application/Rooms/Commands/RemoveRoom.cs index b389e8d6..1dad6da1 100644 --- a/src/Application/Rooms/Commands/RemoveRoom.cs +++ b/src/Application/Rooms/Commands/RemoveRoom.cs @@ -1,3 +1,4 @@ +using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; using AutoMapper; @@ -48,12 +49,10 @@ public async Task Handle(Command request, CancellationToken cancellatio } var canNotRemove = await _context.Documents - .CountAsync(x => x.Folder!.Locker.Room.Id.Equals(request.RoomId), cancellationToken: cancellationToken) - > 0; - + .AnyAsync(x => x.Folder!.Locker.Room.Id.Equals(request.RoomId), cancellationToken: cancellationToken); if (canNotRemove) { - throw new InvalidOperationException("Room cannot be removed because it contains documents."); + throw new ConflictException("Room cannot be removed because it contains documents."); } var result = _context.Rooms.Remove(room); diff --git a/src/Application/Rooms/Commands/UpdateRoom.cs b/src/Application/Rooms/Commands/UpdateRoom.cs index ab720c52..1bd222ca 100644 --- a/src/Application/Rooms/Commands/UpdateRoom.cs +++ b/src/Application/Rooms/Commands/UpdateRoom.cs @@ -38,6 +38,7 @@ public record Command : IRequest public string Name { get; init; } = null!; public string? Description { get; init; } public int Capacity { get; init; } + public bool IsAvailable { get; init; } } public class CommandHandler : IRequestHandler @@ -80,37 +81,28 @@ public async Task Handle(Command request, CancellationToken cancellatio var performingUser = await _context.Users .FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); - var updatedRoom = new Room - { - Id = room.Id, - Name = request.Name, - Description = request.Description, - Staff = room.Staff, - Department = room.Department, - DepartmentId = room.DepartmentId, - Capacity = request.Capacity, - NumberOfLockers = room.NumberOfLockers, - IsAvailable = room.IsAvailable, - Lockers = room.Lockers, - LastModified = LocalDateTime.FromDateTime(DateTime.Now), - LastModifiedBy = performingUser!.Id, - }; + + // update work + room.Name = request.Name; + room.Description = request.Description; + room.Capacity = request.Capacity; + room.IsAvailable = request.IsAvailable; + room.LastModified = LocalDateTime.FromDateTime(DateTime.Now); + room.LastModifiedBy = performingUser!.Id; + var log = new RoomLog() { User = performingUser, UserId = performingUser.Id, - Object = updatedRoom, + Object = room, Time = LocalDateTime.FromDateTime(DateTime.Now), Action = RoomLogMessage.Update, }; - _context.Rooms.Entry(room).State = EntityState.Detached; - _context.Rooms.Entry(updatedRoom).State = EntityState.Modified; + var result = _context.Rooms.Update(room); await _context.RoomLogs.AddAsync(log, cancellationToken); - await _context.SaveChangesAsync(cancellationToken); - - return _mapper.Map(updatedRoom); + return _mapper.Map(result.Entity); } } } \ No newline at end of file diff --git a/src/Application/Rooms/Queries/GetEmptyContainersPaginated.cs b/src/Application/Rooms/Queries/GetEmptyContainersPaginated.cs index 3534dacb..96157358 100644 --- a/src/Application/Rooms/Queries/GetEmptyContainersPaginated.cs +++ b/src/Application/Rooms/Queries/GetEmptyContainersPaginated.cs @@ -44,7 +44,7 @@ public async Task> Handle(Query request, Cancellat .ProjectTo(_mapper.ConfigurationProvider) .AsEnumerable() .ToList(); - + lockers.ForEach(x => x.Folders = x.Folders.Where(y => y.Slot > 0)); var result = new PaginatedList(lockers.ToList(), lockers.Count, pageNumber, sizeNumber); diff --git a/src/Application/Rooms/Queries/GetRoomByDepartmentId.cs b/src/Application/Rooms/Queries/GetRoomByDepartmentId.cs new file mode 100644 index 00000000..45e99e75 --- /dev/null +++ b/src/Application/Rooms/Queries/GetRoomByDepartmentId.cs @@ -0,0 +1,43 @@ +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using Application.Identity; +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Rooms.Queries; + +public class GetRoomByDepartmentId +{ + public record Query : IRequest + { + public Guid DepartmentId { get; init; } + } + + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var room = await _context.Rooms + .Include(x => x.Department) + .Include(x => x.Staff) + .FirstOrDefaultAsync(x => x.DepartmentId == request.DepartmentId, cancellationToken: cancellationToken); + + if (room is null) + { + throw new KeyNotFoundException("Room does not exist."); + } + + return _mapper.Map(room); + } + } +} \ No newline at end of file diff --git a/src/Application/Rooms/Queries/GetRoomById.cs b/src/Application/Rooms/Queries/GetRoomById.cs index 2d420d10..97dc9a56 100644 --- a/src/Application/Rooms/Queries/GetRoomById.cs +++ b/src/Application/Rooms/Queries/GetRoomById.cs @@ -30,7 +30,7 @@ public async Task Handle(Query request, CancellationToken cancellationT var room = await _context.Rooms .Include(x => x.Department) .Include(x => x.Staff) - .FirstOrDefaultAsync(x => x.Id.Equals(request.RoomId), cancellationToken: cancellationToken); + .FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken: cancellationToken); if (room is null) { diff --git a/src/Application/Rooms/Queries/GetLogOfRoomById.cs b/src/Application/Rooms/Queries/GetRoomLogById.cs similarity index 90% rename from src/Application/Rooms/Queries/GetLogOfRoomById.cs rename to src/Application/Rooms/Queries/GetRoomLogById.cs index cee96966..df0f4d45 100644 --- a/src/Application/Rooms/Queries/GetLogOfRoomById.cs +++ b/src/Application/Rooms/Queries/GetRoomLogById.cs @@ -6,7 +6,7 @@ namespace Application.Rooms.Queries; -public class GetLogOfRoomById +public class GetRoomLogById { public record Query : IRequest { @@ -30,7 +30,7 @@ public async Task Handle(Query request, CancellationToken cancellati .Include(x => x.Object) .Include(x => x.User) .ThenInclude(x => x.Department) - .FirstOrDefaultAsync(x => x.Id.Equals(request.LogId), cancellationToken); + .FirstOrDefaultAsync(x => x.Id == request.LogId, cancellationToken); if (log is null) { diff --git a/src/Application/Users/Commands/UpdateUser.cs b/src/Application/Users/Commands/UpdateUser.cs index 30e401c2..370a580a 100644 --- a/src/Application/Users/Commands/UpdateUser.cs +++ b/src/Application/Users/Commands/UpdateUser.cs @@ -1,5 +1,7 @@ +using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Messages; +using Application.Identity; using Application.Users.Queries; using AutoMapper; using Domain.Entities; @@ -31,7 +33,7 @@ public Validator() } public record Command : IRequest { - public User PerformingUser { get; init; } = null!; + public User CurrentUser { get; init; } = null!; public Guid UserId { get; init; } public string? FirstName { get; init; } public string? LastName { get; init; } @@ -52,6 +54,13 @@ public CommandHandler(IApplicationDbContext context, IMapper mapper) public async Task Handle(Command request, CancellationToken cancellationToken) { + // save a roundtrip to db + if (request.CurrentUser.Role.IsAdmin() + && UpdateSelf(request.CurrentUser.Id, request.UserId)) + { + throw new UnauthorizedAccessException("User cannot update this resource."); + } + var user = await _context.Users .FirstOrDefaultAsync(x => x.Id.Equals(request.UserId), cancellationToken: cancellationToken); @@ -66,11 +75,11 @@ public async Task Handle(Command request, CancellationToken cancellatio user.Role = request.Role; user.IsActive = request.IsActive; user.LastModified = LocalDateTime.FromDateTime(DateTime.Now); - user.LastModifiedBy = request.PerformingUser.Id; + user.LastModifiedBy = request.CurrentUser.Id; var log = new UserLog() { - User = request.PerformingUser, - UserId = request.PerformingUser.Id, + User = request.CurrentUser, + UserId = request.CurrentUser.Id, Object = user, Time = LocalDateTime.FromDateTime(DateTime.Now), Action = UserLogMessages.Update, @@ -80,5 +89,8 @@ public async Task Handle(Command request, CancellationToken cancellatio await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } + + private static bool UpdateSelf(Guid currentUserId, Guid updatingUserId) + => updatingUserId == currentUserId; } } \ No newline at end of file diff --git a/src/Application/Users/Queries/GetAllEmployeesPaginated.cs b/src/Application/Users/Queries/GetAllEmployeesPaginated.cs deleted file mode 100644 index 670f772b..00000000 --- a/src/Application/Users/Queries/GetAllEmployeesPaginated.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Application.Common.Extensions; -using Application.Common.Interfaces; -using Application.Common.Models; -using Application.Identity; -using AutoMapper; -using Domain.Entities; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Users.Queries; - -public class GetAllEmployeesPaginated -{ - public record Query : IRequest> - { - public Guid DepartmentId { get; init; } - public int? Page { get; init; } - public int? Size { get; init; } - public string? SortBy { get; init; } - public string? SortOrder { get; init; } - } - - public class Handler : IRequestHandler> - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public Handler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task> Handle(Query request, CancellationToken cancellationToken) - { - var users = _context.Users - .Include(x => x.Department) - .Where(x => x.Department!.Id == request.DepartmentId - && x.Role.Equals(IdentityData.Roles.Employee) - && x.IsActive - && x.IsActivated); - - return await users - .ListPaginateWithFilterAsync( - request.Page, - request.Size, - request.SortBy, - request.SortOrder, - _mapper.ConfigurationProvider, - cancellationToken); - } - } -} diff --git a/src/Application/Users/Queries/GetAllUserLogsPaginated.cs b/src/Application/Users/Queries/GetAllUserLogsPaginated.cs index 890ee474..ddf00959 100644 --- a/src/Application/Users/Queries/GetAllUserLogsPaginated.cs +++ b/src/Application/Users/Queries/GetAllUserLogsPaginated.cs @@ -35,13 +35,13 @@ public async Task> Handle(Query request, CancellationT var logs = _context.UserLogs .Include(x => x.Object) .AsQueryable(); - + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) { logs = logs.Where(x => x.Action.Trim().ToLower().Contains(request.SearchTerm.Trim().ToLower())); } - + return await logs .LoggingListPaginateAsync( request.Page, diff --git a/src/Application/Users/Queries/GetAllUsersPaginated.cs b/src/Application/Users/Queries/GetAllUsersPaginated.cs index 591d1897..d72dad46 100644 --- a/src/Application/Users/Queries/GetAllUsersPaginated.cs +++ b/src/Application/Users/Queries/GetAllUsersPaginated.cs @@ -1,14 +1,10 @@ using Application.Common.Extensions; using Application.Common.Interfaces; -using Application.Common.Mappings; using Application.Common.Models; -using Application.Common.Models.Dtos.Physical; using Application.Identity; using AutoMapper; -using AutoMapper.QueryableExtensions; using Domain.Entities; using MediatR; -using Microsoft.EntityFrameworkCore; namespace Application.Users.Queries; @@ -16,7 +12,8 @@ public class GetAllUsersPaginated { public record Query : IRequest> { - public Guid? DepartmentId { get; init; } + public Guid[]? DepartmentIds { get; init; } + public string? Role { get; init; } public string? SearchTerm { get; init; } public int? Page { get; init; } public int? Size { get; init; } @@ -37,14 +34,22 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task> Handle(Query request, CancellationToken cancellationToken) { - var users = _context.Users.AsQueryable() + var users = _context.Users .Where(x => !x.Role.Equals(IdentityData.Roles.Admin)); - if (request.DepartmentId is not null) + // Filter by department + if (request.DepartmentIds is not null) { - users = users.Where(x => x.Department!.Id == request.DepartmentId); + users = users.Where(x => request.DepartmentIds.Contains(x.Department!.Id) ); + } + + // Filter by role + if (request.Role is not null) + { + users = users.Where(x => x.Role.Equals(request.Role)); } + // Search if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) { users = users.Where(x => diff --git a/src/Application/Users/Queries/GetUserById.cs b/src/Application/Users/Queries/GetUserById.cs index 7b22fe51..45f20c2e 100644 --- a/src/Application/Users/Queries/GetUserById.cs +++ b/src/Application/Users/Queries/GetUserById.cs @@ -1,5 +1,8 @@ +using Application.Common.Extensions; using Application.Common.Interfaces; +using Application.Identity; using AutoMapper; +using Domain.Entities; using MediatR; using Microsoft.EntityFrameworkCore; @@ -9,9 +12,11 @@ public class GetUserById { public record Query : IRequest { + public string UserRole { get; init; } = null!; + public Guid UserDepartmentId { get; init; } public Guid UserId { get; init; } } - + public class QueryHandler : IRequestHandler { private readonly IApplicationDbContext _context; @@ -34,7 +39,19 @@ public async Task Handle(Query request, CancellationToken cancellationT throw new KeyNotFoundException("User does not exist."); } + if (ViolateConstraints(request.UserRole, request.UserDepartmentId, user)) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + return _mapper.Map(user); } + + private static bool ViolateConstraints(string userRole, Guid userDepartmentId, User foundUser) + => (userRole.IsStaff() || userRole.IsEmployee()) + && GetUserInOtherDepartment(userDepartmentId, foundUser); + + private static bool GetUserInOtherDepartment(Guid userDepartmentId, User foundUser) + => foundUser.Department?.Id != userDepartmentId; } } \ No newline at end of file diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index b59e7d45..eab0be8b 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -296,7 +296,7 @@ private SecurityToken CreateJweToken(User user) new(JwtRegisteredClaimNames.Email, user.Email!), new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new(JwtRegisteredClaimNames.Iat, utcNow.ToString(CultureInfo.InvariantCulture)), - new("departmentId", user.Department is not null ? user.Department.Id.ToString() : String.Empty), + new("departmentId", user.Department is not null ? user.Department.Id.ToString() : Guid.Empty.ToString()), new("isActive", user.IsActive.ToString()), }; var publicEncryptionKey = new RsaSecurityKey(_encryptionKey.ExportParameters(false)) {KeyId = _jweSettings.EncryptionKeyId}; From 3cad8695b9e836bdf2ca189762a9606557b34329 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Fri, 16 Jun 2023 16:26:18 +0700 Subject: [PATCH 42/56] fuck me 4 --- src/Api/Controllers/DepartmentsController.cs | 26 +--- src/Api/Controllers/FoldersController.cs | 68 +++------- src/Api/Controllers/LockersController.cs | 70 +++------- .../Requests/Lockers/AddLockerRequest.cs | 8 +- src/Api/Controllers/RoomsController.cs | 14 +- .../Common/Extensions/QueryableExtensions.cs | 8 +- .../Common/Interfaces/IDateTimeProvider.cs | 6 + .../Departments/Commands/AddDepartment.cs | 2 +- .../Departments/Commands/UpdateDepartment.cs | 14 -- src/Application/Folders/Commands/AddFolder.cs | 55 ++++++-- .../Folders/Commands/DisableFolder.cs | 69 ---------- .../Folders/Commands/EnableFolder.cs | 52 -------- .../Folders/Commands/RemoveFolder.cs | 18 ++- .../Folders/Commands/UpdateFolder.cs | 53 ++++++-- .../Folders/Queries/GetAllFoldersPaginated.cs | 38 +++++- .../Folders/Queries/GetFolderById.cs | 13 ++ src/Application/Lockers/Commands/AddLocker.cs | 48 ++++--- .../Lockers/Commands/DisableLocker.cs | 80 ------------ .../Lockers/Commands/EnableLocker.cs | 62 --------- .../Lockers/Commands/RemoveLocker.cs | 11 +- .../Lockers/Commands/UpdateLocker.cs | 58 ++++++--- .../Queries/GetAllLockerLogsPaginated.cs | 2 + .../Lockers/Queries/GetAllLockersPaginated.cs | 16 +-- .../Lockers/Queries/GetLockerById.cs | 9 +- src/Application/Rooms/Commands/AddRoom.cs | 20 +-- src/Application/Rooms/Commands/DisableRoom.cs | 86 ------------ src/Application/Rooms/Commands/EnableRoom.cs | 61 --------- src/Application/Rooms/Commands/RemoveRoom.cs | 6 +- src/Application/Rooms/Commands/UpdateRoom.cs | 48 ++++--- src/Application/Rooms/Queries/GetRoomById.cs | 16 ++- src/Application/Users/Commands/AddUser.cs | 11 +- src/Application/Users/Commands/DisableUser.cs | 62 --------- src/Application/Users/Commands/EnableUser.cs | 13 -- src/Application/Users/Commands/UpdateUser.cs | 11 +- .../Users/Queries/GetAllUserLogsPaginated.cs | 2 + src/Infrastructure/ConfigureServices.cs | 1 + .../Services/DateTimeService.cs | 8 ++ .../Folders/Commands/DisableFolderTests.cs | 119 ----------------- .../Folders/Commands/EnableFolderTests.cs | 90 ------------- .../Lockers/Commands/DisableLockerTests.cs | 88 ------------- .../Lockers/Commands/EnableLockerTests.cs | 86 ------------ .../Rooms/Commands/DisableRoomTests.cs | 123 ------------------ .../Rooms/Commands/EnableRoomTests.cs | 94 ------------- 43 files changed, 377 insertions(+), 1368 deletions(-) create mode 100644 src/Application/Common/Interfaces/IDateTimeProvider.cs delete mode 100644 src/Application/Departments/Commands/UpdateDepartment.cs delete mode 100644 src/Application/Folders/Commands/DisableFolder.cs delete mode 100644 src/Application/Folders/Commands/EnableFolder.cs delete mode 100644 src/Application/Lockers/Commands/DisableLocker.cs delete mode 100644 src/Application/Lockers/Commands/EnableLocker.cs delete mode 100644 src/Application/Rooms/Commands/DisableRoom.cs delete mode 100644 src/Application/Rooms/Commands/EnableRoom.cs delete mode 100644 src/Application/Users/Commands/DisableUser.cs delete mode 100644 src/Application/Users/Commands/EnableUser.cs create mode 100644 src/Infrastructure/Services/DateTimeService.cs delete mode 100644 tests/Application.Tests.Integration/Folders/Commands/DisableFolderTests.cs delete mode 100644 tests/Application.Tests.Integration/Folders/Commands/EnableFolderTests.cs delete mode 100644 tests/Application.Tests.Integration/Lockers/Commands/DisableLockerTests.cs delete mode 100644 tests/Application.Tests.Integration/Lockers/Commands/EnableLockerTests.cs delete mode 100644 tests/Application.Tests.Integration/Rooms/Commands/DisableRoomTests.cs delete mode 100644 tests/Application.Tests.Integration/Rooms/Commands/EnableRoomTests.cs diff --git a/src/Api/Controllers/DepartmentsController.cs b/src/Api/Controllers/DepartmentsController.cs index 9771b95c..d66d3456 100644 --- a/src/Api/Controllers/DepartmentsController.cs +++ b/src/Api/Controllers/DepartmentsController.cs @@ -105,31 +105,7 @@ public async Task>> Add( var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } - - /// - /// Update a department - /// - /// Id of the department to be updated - /// Update department details - /// A DepartmentDto of the updated department - [RequiresRole(IdentityData.Roles.Admin)] - [HttpPut("{departmentId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> Update( - [FromRoute] Guid departmentId, - [FromBody] UpdateDepartmentRequest request) - { - var command = new UpdateDepartment.Command() - { - DepartmentId = departmentId, - Name = request.Name, - }; - var result = await Mediator.Send(command); - return Ok(Result.Succeed(result)); - } - + /// /// Delete a department /// diff --git a/src/Api/Controllers/FoldersController.cs b/src/Api/Controllers/FoldersController.cs index 6d235599..329f5928 100644 --- a/src/Api/Controllers/FoldersController.cs +++ b/src/Api/Controllers/FoldersController.cs @@ -33,8 +33,12 @@ public FoldersController(ICurrentUserService currentUserService) [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetById([FromRoute] Guid folderId) { + var currentUserRole = _currentUserService.GetRole(); + var currentUserDepartmentId = _currentUserService.GetDepartmentId(); var query = new GetFolderById.Query() { + CurrentUserRole = currentUserRole, + CurrentUserDepartmentId = currentUserDepartmentId, FolderId = folderId, }; var result = await Mediator.Send(query); @@ -53,8 +57,12 @@ public async Task>> GetById([FromRoute] Guid fold public async Task>>> GetAllPaginated( [FromQuery] GetAllFoldersPaginatedQueryParameters queryParameters) { + var currentUserRole = _currentUserService.GetRole(); + var currentUserDepartmentId = _currentUserService.GetDepartmentId(); var query = new GetAllFoldersPaginated.Query() { + CurrentUserRole = currentUserRole, + CurrentUserDepartmentId = currentUserDepartmentId, RoomId = queryParameters.RoomId, LockerId = queryParameters.LockerId, SearchTerm = queryParameters.SearchTerm, @@ -72,7 +80,7 @@ public async Task>>> GetAllPaginate /// /// Add folder details /// A FolderDto of the added folder - [RequiresRole(IdentityData.Roles.Admin)] + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -81,10 +89,10 @@ public async Task>>> GetAllPaginate [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> AddFolder([FromBody] AddFolderRequest request) { - var performingUserId = _currentUserService.GetId(); + var currentUser = _currentUserService.GetCurrentUser(); var command = new AddFolder.Command() { - PerformingUserId = performingUserId, + CurrentUser = currentUser, Name = request.Name, Description = request.Description, Capacity = request.Capacity, @@ -99,7 +107,7 @@ public async Task>> AddFolder([FromBody] AddFolde /// /// Id of the folder to be removed /// A FolderDto of the removed folder - [RequiresRole(IdentityData.Roles.Admin)] + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpDelete("{folderId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -108,58 +116,18 @@ public async Task>> AddFolder([FromBody] AddFolde [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> RemoveFolder([FromRoute] Guid folderId) { + var currentUserRole = _currentUserService.GetRole(); + var currentUserDepartmentId = _currentUserService.GetDepartmentId(); var command = new RemoveFolder.Command() { - FolderId = folderId, - }; - var result = await Mediator.Send(command); - return Ok(Result.Succeed(result)); - } - - /// - /// Enable a folder - /// - /// Id of the folder to be enabled - /// A FolderDto of the enabled folder - [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] - [HttpPut("enable/{folderId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> EnableFolder([FromRoute] Guid folderId) - { - var command = new EnableFolder.Command() - { + CurrentUserRole = currentUserRole, + CurrentUserDepartmentId = currentUserDepartmentId, FolderId = folderId, }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } - /// - /// Disable a folder - /// - /// Id of the disabled folder - /// A FolderDto of the disabled folder - [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] - [HttpPut("disable/{folderId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> DisableFolder([FromRoute] Guid folderId) - { - var command = new DisableFolder.Command() - { - FolderId = folderId, - }; - var result = await Mediator.Send(command); - return Ok(Result.Succeed(result)); - } - /// /// Update a folder /// @@ -173,10 +141,10 @@ public async Task>> DisableFolder([FromRoute] Gui [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> Update([FromRoute] Guid folderId, [FromBody] UpdateFolderRequest request) { - var performingUserId = _currentUserService.GetId(); + var currentUser = _currentUserService.GetCurrentUser(); var command = new UpdateFolder.Command() { - PerformingUserId = performingUserId, + CurrentUser = currentUser, FolderId = folderId, Name = request.Name, Description = request.Description, diff --git a/src/Api/Controllers/LockersController.cs b/src/Api/Controllers/LockersController.cs index 24a7ad4e..df798faf 100644 --- a/src/Api/Controllers/LockersController.cs +++ b/src/Api/Controllers/LockersController.cs @@ -87,12 +87,13 @@ public async Task>>> GetAllPaginate [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Add([FromBody] AddLockerRequest request) + public async Task>> Add( + [FromBody] AddLockerRequest request) { - var performingUserId = _currentUserService.GetId(); + var currentUser = _currentUserService.GetCurrentUser(); var command = new AddLocker.Command() { - PerformingUserId = performingUserId, + CurrentUser = currentUser, Name = request.Name, Description = request.Description, Capacity = request.Capacity, @@ -107,6 +108,7 @@ public async Task>> Add([FromBody] AddLockerReque /// /// Id of the locker to be removed /// A LockerDto of the removed locker + [RequiresRole(IdentityData.Roles.Admin)] [HttpDelete("{lockerId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -122,67 +124,26 @@ public async Task>> Remove([FromRoute] Guid locke return Ok(Result.Succeed(result)); } - /// - /// Enable a locker - /// - /// Id of the locker to be enabled - /// A LockerDto of the enabled locker - [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] - [HttpPut("enable/{lockerId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Enable([FromRoute] Guid lockerId) - { - var command = new EnableLocker.Command() - { - LockerId = lockerId, - }; - var result = await Mediator.Send(command); - return Ok(Result.Succeed(result)); - } - - /// - /// Disable a locker - /// - /// Id of the locker to be disabled - /// A LockerDto of the disabled locker - [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] - [HttpPut("disable/{lockerId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Disable([FromRoute] Guid lockerId) - { - var command = new DisableLocker.Command() - { - LockerId = lockerId, - }; - var result = await Mediator.Send(command); - return Ok(Result.Succeed(result)); - } - /// /// Update a locker /// /// Id of the locker to be updated /// Update locker details /// A LockerDto of the updated locker + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpPut("{lockerId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Update([FromRoute] Guid lockerId, [FromBody] UpdateLockerRequest request) + public async Task>> Update( + [FromRoute] Guid lockerId, + [FromBody] UpdateLockerRequest request) { - var performingUserId = _currentUserService.GetId(); + var currentUser = _currentUserService.GetCurrentUser(); var command = new UpdateLocker.Command() { - PerformingUserId = performingUserId, + CurrentUser = currentUser, LockerId = lockerId, Name = request.Name, Description = request.Description, @@ -193,10 +154,10 @@ public async Task>> Update([FromRoute] Guid locke } /// - /// Get all logs related to locker. + /// Get all logs related to locker /// /// Query parameters - /// A list of LockerLogsDtos. + /// A list of LockerLogsDtos [RequiresRole(IdentityData.Roles.Admin)] [HttpGet("logs")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -220,7 +181,7 @@ public async Task>>> GetAllLocke /// Id of the requested log /// A LockerLogDto of the requested log. [RequiresRole(IdentityData.Roles.Admin)] - [HttpGet("log/{logId:guid}")] + [HttpGet("logs/{logId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -228,9 +189,8 @@ public async Task>> GetLockerLogById([FromRout { var query = new GetLockerLogById.Query() { - LogId = logId + LogId = logId, }; - var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); } diff --git a/src/Api/Controllers/Payload/Requests/Lockers/AddLockerRequest.cs b/src/Api/Controllers/Payload/Requests/Lockers/AddLockerRequest.cs index 67988e72..21374f9a 100644 --- a/src/Api/Controllers/Payload/Requests/Lockers/AddLockerRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Lockers/AddLockerRequest.cs @@ -8,17 +8,17 @@ public class AddLockerRequest /// /// Name of the locker to be updated /// - public string Name { get; init; } = null!; + public string Name { get; set; } = null!; /// /// Description of the locker to be updated /// - public string? Description { get; init; } + public string? Description { get; set; } /// /// Id of the room that this locker will be in /// - public Guid RoomId { get; init; } + public Guid RoomId { get; set; } /// /// Number of folders this locker can hold /// - public int Capacity { get; init; } + public int Capacity { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/RoomsController.cs b/src/Api/Controllers/RoomsController.cs index 9483d469..a9740d4c 100644 --- a/src/Api/Controllers/RoomsController.cs +++ b/src/Api/Controllers/RoomsController.cs @@ -27,7 +27,7 @@ public RoomsController(ICurrentUserService currentUserService) /// /// Id of the room to be retrieved /// A RoomDto of the retrieved room - [RequiresRole(IdentityData.Roles.Admin)] + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpGet("{roomId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -35,8 +35,12 @@ public RoomsController(ICurrentUserService currentUserService) public async Task>> GetById( [FromRoute] Guid roomId) { + var currentUserRole = _currentUserService.GetRole(); + var currentUserDepartmentId = _currentUserService.GetDepartmentId(); var query = new GetRoomById.Query() { + CurrentUserRole = currentUserRole, + CurrentUserDepartmentId = currentUserDepartmentId, RoomId = roomId, }; var result = await Mediator.Send(query); @@ -107,10 +111,10 @@ public async Task>> GetEmptyContainer public async Task>> AddRoom( [FromBody] AddRoomRequest request) { - var performingUserId = _currentUserService.GetId(); + var currentUser = _currentUserService.GetCurrentUser(); var command = new AddRoom.Command() { - PerformingUserId = performingUserId, + CurrentUser = currentUser, Name = request.Name, Description = request.Description, Capacity = request.Capacity, @@ -157,10 +161,10 @@ public async Task>> Update( [FromRoute] Guid roomId, [FromBody] UpdateRoomRequest request) { - var performingUserId = _currentUserService.GetId(); + var currentUser = _currentUserService.GetCurrentUser(); var command = new UpdateRoom.Command() { - PerformingUserId = performingUserId, + CurrentUser = currentUser, RoomId = roomId, Name = request.Name, Description = request.Description, diff --git a/src/Application/Common/Extensions/QueryableExtensions.cs b/src/Application/Common/Extensions/QueryableExtensions.cs index 5df0eedd..84a2ba94 100644 --- a/src/Application/Common/Extensions/QueryableExtensions.cs +++ b/src/Application/Common/Extensions/QueryableExtensions.cs @@ -15,12 +15,12 @@ public static IQueryable OrderByCustom(this IQueryable> LoggingListPaginateAsync> ListPaginateWithFilterAsync< sortOrder ??= "asc"; var pageNumber = page is null or <= 0 ? 1 : page; - var sizeNumber = size is null or <= 0 ? 5 : size; + var sizeNumber = size is null or <= 0 ? 10 : size; var count = await items.CountAsync(cancellationToken); var list = await items diff --git a/src/Application/Common/Interfaces/IDateTimeProvider.cs b/src/Application/Common/Interfaces/IDateTimeProvider.cs new file mode 100644 index 00000000..379ff470 --- /dev/null +++ b/src/Application/Common/Interfaces/IDateTimeProvider.cs @@ -0,0 +1,6 @@ +namespace Application.Common.Interfaces; + +public interface IDateTimeProvider +{ + public DateTime DateTimeNow { get; } +} \ No newline at end of file diff --git a/src/Application/Departments/Commands/AddDepartment.cs b/src/Application/Departments/Commands/AddDepartment.cs index 525d83c4..a057d0d7 100644 --- a/src/Application/Departments/Commands/AddDepartment.cs +++ b/src/Application/Departments/Commands/AddDepartment.cs @@ -39,7 +39,7 @@ public async Task Handle(Command request, CancellationToken cance var entity = new Department { - Name = request.Name + Name = request.Name, }; var result = await _context.Departments.AddAsync(entity, cancellationToken); diff --git a/src/Application/Departments/Commands/UpdateDepartment.cs b/src/Application/Departments/Commands/UpdateDepartment.cs deleted file mode 100644 index a080e22d..00000000 --- a/src/Application/Departments/Commands/UpdateDepartment.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Application.Common.Models.Dtos; -using Application.Users.Queries; -using MediatR; - -namespace Application.Departments.Commands; - -public class UpdateDepartment -{ - public record Command : IRequest - { - public Guid DepartmentId { get; set; } - public string Name { get; init; } = null!; - } -} \ No newline at end of file diff --git a/src/Application/Folders/Commands/AddFolder.cs b/src/Application/Folders/Commands/AddFolder.cs index 05a7bf34..6d47e513 100644 --- a/src/Application/Folders/Commands/AddFolder.cs +++ b/src/Application/Folders/Commands/AddFolder.cs @@ -1,8 +1,10 @@ using Application.Common.Exceptions; +using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; using Domain.Entities.Logging; using Domain.Entities.Physical; using Domain.Exceptions; @@ -39,7 +41,7 @@ public Validator() public record Command : IRequest { - public Guid PerformingUserId { get; init; } + public User CurrentUser { get; init; } = null!; public string Name { get; init; } = null!; public string? Description { get; init; } public int Capacity { get; init; } @@ -50,16 +52,20 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) { - var locker = await _context.Lockers.FirstOrDefaultAsync(l => l.Id == request.LockerId, cancellationToken); + var locker = await _context.Lockers + .Include(x => x.Room) + .FirstOrDefaultAsync(l => l.Id == request.LockerId, cancellationToken); if (locker is null) { @@ -71,16 +77,19 @@ public async Task Handle(Command request, CancellationToken cancellat throw new LimitExceededException("This locker cannot accept more folders."); } - var folder = await _context.Folders.FirstOrDefaultAsync(x => - x.Name.Trim().ToLower().Equals(request.Name.Trim().ToLower()) - && x.Locker.Id.Equals(request.LockerId), cancellationToken); + if (request.CurrentUser.Role.IsStaff() + && !LockerExistsAndInSameDepartment(locker, request.CurrentUser.Department?.Id)) + { + throw new UnauthorizedAccessException("User cannot add this resource."); + } - if (folder is not null) + if (await DuplicatedNameFolderExistsInSameLockerAsync(request.Name, locker.Id, cancellationToken)) { throw new ConflictException("Folder name already exists."); } - - var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); + + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + var entity = new Folder { Name = request.Name.Trim(), @@ -89,15 +98,16 @@ public async Task Handle(Command request, CancellationToken cancellat Capacity = request.Capacity, Locker = locker, IsAvailable = true, - Created = LocalDateTime.FromDateTime(DateTime.Now), - CreatedBy = performingUser!.Id, + Created = localDateTimeNow, + CreatedBy = request.CurrentUser.Id, }; + var log = new FolderLog() { - User = performingUser, - UserId = performingUser.Id, + User = request.CurrentUser, + UserId = request.CurrentUser.Id, Object = entity, - Time = LocalDateTime.FromDateTime(DateTime.Now), + Time = localDateTimeNow, Action = FolderLogMessage.Add, }; var result = await _context.Folders.AddAsync(entity, cancellationToken); @@ -107,5 +117,22 @@ public async Task Handle(Command request, CancellationToken cancellat await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } + + private async Task DuplicatedNameFolderExistsInSameLockerAsync(string folderName, Guid lockerId, CancellationToken cancellationToken) + { + var folder = await _context.Folders.FirstOrDefaultAsync( + x => EqualsInvariant(x.Name, folderName) + && IsSameLocker(x.Locker.Id, lockerId), cancellationToken); + return folder is not null; + } + + private static bool EqualsInvariant(string x, string y) + => x.Trim().ToLower().Equals(y.Trim().ToLower()); + + private static bool IsSameLocker(Guid lockerId1, Guid lockerId2) + => lockerId1 == lockerId2; + + private static bool LockerExistsAndInSameDepartment(Locker locker, Guid? departmentId) + => departmentId is not null && locker.Room.DepartmentId == departmentId; } } \ No newline at end of file diff --git a/src/Application/Folders/Commands/DisableFolder.cs b/src/Application/Folders/Commands/DisableFolder.cs deleted file mode 100644 index f57ab34b..00000000 --- a/src/Application/Folders/Commands/DisableFolder.cs +++ /dev/null @@ -1,69 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using FluentValidation; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Folders.Commands; - -public class DisableFolder -{ - public class Validator : AbstractValidator - { - public Validator() - { - RuleLevelCascadeMode = CascadeMode.Stop; - - RuleFor(f => f.FolderId) - .NotEmpty().WithMessage("FolderId is required."); - } - } - - public record Command : IRequest - { - public Guid FolderId { get; init; } - } - - public class CommandHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public CommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - var folder = await _context.Folders - .Include(x => x.Locker) - .ThenInclude(x => x.Room) - .ThenInclude(x => x.Department) - .FirstOrDefaultAsync(f => f.Id.Equals(request.FolderId), cancellationToken); - - if (folder is null) - { - throw new KeyNotFoundException("Folder does not exist."); - } - - if (!folder.IsAvailable) - { - throw new ConflictException("Folder has already been disabled."); - } - - if (folder.NumberOfDocuments > 0) - { - throw new InvalidOperationException("Folder cannot be disabled because it contains documents."); - } - - folder.IsAvailable = false; - _context.Folders.Update(folder); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(folder); - } - } -} \ No newline at end of file diff --git a/src/Application/Folders/Commands/EnableFolder.cs b/src/Application/Folders/Commands/EnableFolder.cs deleted file mode 100644 index e7acf703..00000000 --- a/src/Application/Folders/Commands/EnableFolder.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Folders.Commands; - -public class EnableFolder -{ - public record Command : IRequest - { - public Guid FolderId { get; init; } - } - - public class CommandHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public CommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - var folder = await _context.Folders - .Include(x => x.Locker) - .ThenInclude(x => x.Room) - .ThenInclude(x => x.Department) - .FirstOrDefaultAsync(x => x.Id.Equals(request.FolderId), cancellationToken); - - if (folder is null) - { - throw new KeyNotFoundException("Folder does not exist."); - } - - if (folder.IsAvailable) - { - throw new ConflictException("Folder has already been enabled."); - } - - folder.IsAvailable = true; - var result = _context.Folders.Update(folder); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } - } -} \ No newline at end of file diff --git a/src/Application/Folders/Commands/RemoveFolder.cs b/src/Application/Folders/Commands/RemoveFolder.cs index e8bc05f2..dee17b93 100644 --- a/src/Application/Folders/Commands/RemoveFolder.cs +++ b/src/Application/Folders/Commands/RemoveFolder.cs @@ -1,9 +1,9 @@ using Application.Common.Exceptions; +using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities.Physical; -using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; @@ -13,6 +13,8 @@ public class RemoveFolder { public record Command : IRequest { + public string CurrentUserRole { get; init; } = null!; + public Guid CurrentUserDepartmentId { get; init; } public Guid FolderId { get; init; } } @@ -40,9 +42,15 @@ public async Task Handle(Command request, CancellationToken cancellat throw new KeyNotFoundException("Folder does not exist."); } - var containDocument = folder.NumberOfDocuments > 0; + if (request.CurrentUserRole.IsStaff() + && !FolderIsInDepartment(folder, request.CurrentUserDepartmentId)) + { + throw new UnauthorizedAccessException("User cannot remove this resource."); + } - if (containDocument) + var canNotRemove = folder.NumberOfDocuments > 0; + + if (canNotRemove) { throw new ConflictException("Folder cannot be removed because it contains documents."); } @@ -50,9 +58,11 @@ public async Task Handle(Command request, CancellationToken cancellat var locker = folder.Locker; var result = _context.Folders.Remove(folder); locker.NumberOfFolders -= 1; - await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } + + private static bool FolderIsInDepartment(Folder folder, Guid departmentId) + => folder.Locker.Room.DepartmentId == departmentId; } } \ No newline at end of file diff --git a/src/Application/Folders/Commands/UpdateFolder.cs b/src/Application/Folders/Commands/UpdateFolder.cs index f5924992..19b443bb 100644 --- a/src/Application/Folders/Commands/UpdateFolder.cs +++ b/src/Application/Folders/Commands/UpdateFolder.cs @@ -1,9 +1,12 @@ using Application.Common.Exceptions; +using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; using Domain.Entities.Logging; +using Domain.Entities.Physical; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; @@ -34,7 +37,7 @@ public Validator() public record Command : IRequest { - public Guid PerformingUserId { get; init; } + public User CurrentUser { get; init; } = null!; public Guid FolderId { get; init; } public string Name { get; init; } = null!; public string? Description { get; init; } @@ -64,14 +67,14 @@ public async Task Handle(Command request, CancellationToken cancellat { throw new KeyNotFoundException("Folder does not exist."); } - - var nameExisted = await _context.Folders.AnyAsync( x => - x.Name.Trim().ToLower().Equals(request.Name.Trim().ToLower()) - && x.Id != folder.Id - && x.Locker.Id == folder.Locker.Id - , cancellationToken); - - if (nameExisted) + + if (request.CurrentUser.Role.IsStaff() + && !FolderIsInDepartment(folder, request.CurrentUser.Department!.Id)) + { + throw new UnauthorizedAccessException("User cannot remove this resource."); + } + + if (await DuplicatedNameFolderExistsInSameLockerAsync(request.Name, folder.Id, folder.Locker.Id, cancellationToken)) { throw new ConflictException("Folder name already exists."); } @@ -81,17 +84,16 @@ public async Task Handle(Command request, CancellationToken cancellat throw new ConflictException("New capacity cannot be less than current number of documents."); } - var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); folder.Name = request.Name; folder.Description = request.Description; folder.Capacity = request.Capacity; folder.LastModified = LocalDateTime.FromDateTime(DateTime.Now); - folder.LastModifiedBy = performingUser!.Id; + folder.LastModifiedBy = request.CurrentUser.Id; var log = new FolderLog() { - User = performingUser, - UserId = performingUser.Id, + User = request.CurrentUser, + UserId = request.CurrentUser.Id, Object = folder, Time = LocalDateTime.FromDateTime(DateTime.Now), Action = FolderLogMessage.Update, @@ -101,5 +103,30 @@ public async Task Handle(Command request, CancellationToken cancellat await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } + private async Task DuplicatedNameFolderExistsInSameLockerAsync( + string folderName, + Guid lockerId, + Guid folderId, + CancellationToken cancellationToken) + { + var folder = await _context.Lockers.FirstOrDefaultAsync( + x => EqualsInvariant(x.Name, folderName) + && IsNotSameFolder(x.Id, folderId) + && IsSameLocker(x.Room.Id, lockerId), + cancellationToken); + return folder is not null; + } + + private static bool EqualsInvariant(string x, string y) + => x.Trim().ToLower().Equals(y.Trim().ToLower()); + + private static bool IsSameLocker(Guid lockerId1, Guid lockerId2) + => lockerId1 == lockerId2; + + private static bool IsNotSameFolder(Guid folderId1, Guid folderId2) + => folderId1 != folderId2; + + private static bool FolderIsInDepartment(Folder folder, Guid departmentId) + => folder.Locker.Room.DepartmentId == departmentId; } } \ No newline at end of file diff --git a/src/Application/Folders/Queries/GetAllFoldersPaginated.cs b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs index 9f38c039..a03f8a28 100644 --- a/src/Application/Folders/Queries/GetAllFoldersPaginated.cs +++ b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs @@ -28,6 +28,8 @@ public Validator() public record Query : IRequest> { + public string CurrentUserRole { get; init; } = null!; + public Guid CurrentUserDepartmentId { get; init; } public Guid? RoomId { get; init; } public Guid? LockerId { get; init; } public string? SearchTerm { get; init; } @@ -50,15 +52,35 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task> Handle(Query request, CancellationToken cancellationToken) { + if (request.CurrentUserRole.IsStaff()) + { + if (request.RoomId is null) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + + var currentUserRoom = await GetRoomByDepartmentIdAsync(request.CurrentUserDepartmentId, cancellationToken); + + if (currentUserRoom is null) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + + if (!IsSameRoom(currentUserRoom.Id, request.RoomId.Value)) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + } + var folders = _context.Folders .Include(x => x.Locker) .ThenInclude(y => y.Room) .ThenInclude(z => z.Department) .AsQueryable(); - var roomExists = request.RoomId is not null; - var lockerExists = request.LockerId is not null; + var roomIdProvided = request.RoomId is not null; + var lockerIdProvided = request.LockerId is not null; - if (lockerExists) + if (lockerIdProvided) { var locker = await _context.Lockers .Include(x => x.Room) @@ -76,7 +98,7 @@ public async Task> Handle(Query request, CancellationTo folders = folders.Where(x => x.Locker.Id == request.LockerId); } - else if (roomExists) + else if (roomIdProvided) { var room = await _context.Rooms .FirstOrDefaultAsync(x => x.Id == request.RoomId @@ -104,5 +126,13 @@ public async Task> Handle(Query request, CancellationTo _mapper.ConfigurationProvider, cancellationToken); } + + private async Task GetRoomByDepartmentIdAsync(Guid departmentId, CancellationToken cancellationToken) + => await _context.Rooms.FirstOrDefaultAsync( + x => x.DepartmentId == departmentId, + cancellationToken); + + private static bool IsSameRoom(Guid roomId1, Guid roomId2) + => roomId1 == roomId2; } } \ No newline at end of file diff --git a/src/Application/Folders/Queries/GetFolderById.cs b/src/Application/Folders/Queries/GetFolderById.cs index c74c30c8..4a1aa1af 100644 --- a/src/Application/Folders/Queries/GetFolderById.cs +++ b/src/Application/Folders/Queries/GetFolderById.cs @@ -1,6 +1,8 @@ +using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities.Physical; using MediatR; using Microsoft.EntityFrameworkCore; @@ -10,6 +12,8 @@ public class GetFolderById { public record Query : IRequest { + public string CurrentUserRole { get; init; } = null!; + public Guid CurrentUserDepartmentId { get; init; } public Guid FolderId { get; init; } } @@ -36,8 +40,17 @@ public async Task Handle(Query request, CancellationToken cancellatio { throw new KeyNotFoundException("Folder does not exist."); } + + if (request.CurrentUserRole.IsStaff() + && !FolderInSameDepartment(folder, request.CurrentUserDepartmentId)) + { + throw new UnauthorizedAccessException(); + } return _mapper.Map(folder); } + + private static bool FolderInSameDepartment(Folder folder, Guid departmentId) + => folder.Locker.Room.DepartmentId == departmentId; } } \ No newline at end of file diff --git a/src/Application/Lockers/Commands/AddLocker.cs b/src/Application/Lockers/Commands/AddLocker.cs index 1f16bbe3..60803a84 100644 --- a/src/Application/Lockers/Commands/AddLocker.cs +++ b/src/Application/Lockers/Commands/AddLocker.cs @@ -3,6 +3,7 @@ using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; using Domain.Entities.Logging; using Domain.Entities.Physical; using Domain.Exceptions; @@ -10,7 +11,6 @@ using MediatR; using Microsoft.EntityFrameworkCore; using NodaTime; -using Org.BouncyCastle.Math.EC.Rfc8032; namespace Application.Lockers.Commands; @@ -39,7 +39,7 @@ public Validator() public record Command : IRequest { - public Guid PerformingUserId { get; init; } + public User CurrentUser { get; init; } = null!; public string Name { get; init; } = null!; public string? Description { get; init; } public Guid RoomId { get; init; } @@ -50,11 +50,13 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -68,21 +70,17 @@ public async Task Handle(Command request, CancellationToken cancellat if (room.NumberOfLockers >= room.Capacity) { - throw new LimitExceededException( - "This room cannot accept more lockers." - ); + throw new LimitExceededException("This room cannot accept more lockers."); } - var locker = await _context.Lockers.FirstOrDefaultAsync( - x => x.Name.Trim().ToLower().Equals(request.Name.Trim().ToLower()) && x.Room.Id.Equals(request.RoomId), - cancellationToken); - if (locker is not null) + if (await DuplicatedNameLockerExistsInSameRoomAsync(request.Name, request.RoomId, cancellationToken)) { throw new ConflictException("Locker name already exists."); } - var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); - var entity = new Locker + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + + var entity = new Locker() { Name = request.Name.Trim(), Description = request.Description?.Trim(), @@ -90,18 +88,18 @@ public async Task Handle(Command request, CancellationToken cancellat Capacity = request.Capacity, Room = room, IsAvailable = true, - Created = LocalDateTime.FromDateTime(DateTime.Now), - CreatedBy = performingUser!.Id, + Created = localDateTimeNow, + CreatedBy = request.CurrentUser.Id, }; + var log = new LockerLog() { - User = performingUser, - UserId = performingUser.Id, + User = request.CurrentUser, + UserId = request.CurrentUser.Id, Object = entity, - Time = LocalDateTime.FromDateTime(DateTime.Now), + Time = localDateTimeNow, Action = LockerLogMessage.Add, }; - var result = await _context.Lockers.AddAsync(entity, cancellationToken); room.NumberOfLockers += 1; _context.Rooms.Update(room); @@ -109,5 +107,19 @@ public async Task Handle(Command request, CancellationToken cancellat await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } + + private async Task DuplicatedNameLockerExistsInSameRoomAsync(string lockerName, Guid roomId, CancellationToken cancellationToken) + { + var locker = await _context.Lockers.FirstOrDefaultAsync( + x => EqualsInvariant(x.Name, lockerName) + && IsSameRoom(x.Room.Id, roomId), cancellationToken); + return locker is not null; + } + + private static bool EqualsInvariant(string x, string y) + => x.Trim().ToLower().Equals(y.Trim().ToLower()); + + private static bool IsSameRoom(Guid roomId1, Guid roomId2) + => roomId1 == roomId2; } } \ No newline at end of file diff --git a/src/Application/Lockers/Commands/DisableLocker.cs b/src/Application/Lockers/Commands/DisableLocker.cs deleted file mode 100644 index 10a78f79..00000000 --- a/src/Application/Lockers/Commands/DisableLocker.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using FluentValidation; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Lockers.Commands; - -public class DisableLocker -{ - public class Validator : AbstractValidator - { - public Validator() - { - RuleLevelCascadeMode = CascadeMode.Stop; - - RuleFor(x => x.LockerId) - .NotEmpty().WithMessage("LockerId is required."); - } - } - - public record Command : IRequest - { - public Guid LockerId { get; init; } - } - - public class CommandHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public CommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - var locker = await _context.Lockers - .Include(x => x.Room) - .ThenInclude(x => x.Department) - .FirstOrDefaultAsync(x => x.Id.Equals(request.LockerId), cancellationToken); - - if (locker is null) - { - throw new KeyNotFoundException("Locker does not exist."); - } - - if (!locker.IsAvailable) - { - throw new ConflictException("Locker has already been disabled."); - } - - var canNotDisable = await _context.Documents - .CountAsync(x => x.Folder!.Locker.Id.Equals(request.LockerId), cancellationToken) - > 0; - - if (canNotDisable) - { - throw new InvalidOperationException("Locker cannot be disabled because it contains documents."); - } - - var folders = _context.Folders.Where(x => x.Locker.Room.Id.Equals(locker.Id)); - - foreach (var folder in folders) - { - folder.IsAvailable = false; - } - _context.Folders.UpdateRange(folders); - - locker.IsAvailable = false; - var result = _context.Lockers.Update(locker); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } - } -} \ No newline at end of file diff --git a/src/Application/Lockers/Commands/EnableLocker.cs b/src/Application/Lockers/Commands/EnableLocker.cs deleted file mode 100644 index 49722a1a..00000000 --- a/src/Application/Lockers/Commands/EnableLocker.cs +++ /dev/null @@ -1,62 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using FluentValidation; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Lockers.Commands; - -public class EnableLocker -{ - public class Validator : AbstractValidator - { - public Validator() - { - RuleLevelCascadeMode = CascadeMode.Stop; - - RuleFor(x => x.LockerId) - .NotEmpty().WithMessage("LockerId is required."); - } - } - - public record Command : IRequest - { - public Guid LockerId { get; init; } - } - - public class CommandHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public CommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - var locker = await _context.Lockers - .Include(x => x.Room) - .ThenInclude(x => x.Department) - .FirstOrDefaultAsync(x => x.Id.Equals(request.LockerId), cancellationToken); - if (locker is null) - { - throw new KeyNotFoundException("Locker does not exist."); - } - - if (locker.IsAvailable) - { - throw new ConflictException("Locker has already been enabled."); - } - - locker.IsAvailable = true; - var result = _context.Lockers.Update(locker); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } - } -} \ No newline at end of file diff --git a/src/Application/Lockers/Commands/RemoveLocker.cs b/src/Application/Lockers/Commands/RemoveLocker.cs index a6ab6ad9..6aa7a79f 100644 --- a/src/Application/Lockers/Commands/RemoveLocker.cs +++ b/src/Application/Lockers/Commands/RemoveLocker.cs @@ -43,23 +43,20 @@ public async Task Handle(Command request, CancellationToken cancellat .Include(x => x.Room) .ThenInclude(x => x.Department) .FirstOrDefaultAsync(x => x.Id.Equals(request.LockerId), cancellationToken); - + if (locker is null) { throw new KeyNotFoundException("Locker does not exist."); } - + var canNotRemove = await _context.Documents - .CountAsync(x => x.Folder!.Locker.Id.Equals(request.LockerId), cancellationToken) - > 0; - + .AnyAsync(x => x.Folder!.Locker.Id.Equals(request.LockerId), cancellationToken); if (canNotRemove) { - throw new InvalidOperationException("Locker cannot be removed because it contains documents."); + throw new ConflictException("Locker cannot be removed because it contains documents."); } var room = locker.Room; - var result = _context.Lockers.Remove(locker); room.NumberOfLockers -= 1; _context.Rooms.Update(room); diff --git a/src/Application/Lockers/Commands/UpdateLocker.cs b/src/Application/Lockers/Commands/UpdateLocker.cs index d9a1a33e..a670703d 100644 --- a/src/Application/Lockers/Commands/UpdateLocker.cs +++ b/src/Application/Lockers/Commands/UpdateLocker.cs @@ -3,6 +3,7 @@ using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; using Domain.Entities.Logging; using Domain.Entities.Physical; using Domain.Exceptions; @@ -34,7 +35,7 @@ public Validator() } public record Command : IRequest { - public Guid PerformingUserId { get; init; } + public User CurrentUser { get; init; } = null!; public Guid LockerId { get; init; } public string Name { get; init; } = null!; public string? Description { get; init; } @@ -45,11 +46,13 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -58,39 +61,37 @@ public async Task Handle(Command request, CancellationToken cancellat .Include(x => x.Room) .ThenInclude(x => x.Department) .FirstOrDefaultAsync(x => x.Id.Equals(request.LockerId), cancellationToken); - + if (locker is null) { throw new KeyNotFoundException("Locker does not exist."); } - - var duplicateLocker = await _context.Lockers.FirstOrDefaultAsync( - x => x.Name.Trim().ToLower().Equals(request.Name.Trim().ToLower()) - && x.Id != locker.Id - && x.Room.Id == locker.Room.Id, cancellationToken); - if (duplicateLocker is not null && !duplicateLocker.Equals(locker)) + if (await DuplicatedNameLockerExistsInSameRoomAsync(request.Name, locker.Room.Id, request.LockerId, cancellationToken)) { throw new ConflictException("New locker name already exists."); } - + if (locker.NumberOfFolders > request.Capacity) { throw new ConflictException("New capacity cannot be less than current number of folders."); } - var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); + + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + + // update work locker.Name = request.Name; locker.Description = request.Description; locker.Capacity = request.Capacity; - locker.LastModified = LocalDateTime.FromDateTime(DateTime.Now); - locker.LastModifiedBy = performingUser!.Id; - + locker.LastModified = localDateTimeNow; + locker.LastModifiedBy = request.CurrentUser.Id; + var log = new LockerLog() { - User = performingUser, - UserId = performingUser.Id, + User = request.CurrentUser, + UserId = request.CurrentUser.Id, Object = locker, - Time = LocalDateTime.FromDateTime(DateTime.Now), + Time = localDateTimeNow, Action = LockerLogMessage.Update, }; var result = _context.Lockers.Update(locker); @@ -98,5 +99,28 @@ public async Task Handle(Command request, CancellationToken cancellat await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } + + private async Task DuplicatedNameLockerExistsInSameRoomAsync( + string lockerName, + Guid roomId, + Guid lockerId, + CancellationToken cancellationToken) + { + var locker = await _context.Lockers.FirstOrDefaultAsync( + x => EqualsInvariant(x.Name, lockerName) + && IsNotSameLocker(x.Id, lockerId) + && IsSameRoom(x.Room.Id, roomId), + cancellationToken); + return locker is not null; + } + + private static bool EqualsInvariant(string x, string y) + => x.Trim().ToLower().Equals(y.Trim().ToLower()); + + private static bool IsSameRoom(Guid roomId1, Guid roomId2) + => roomId1 == roomId2; + + private static bool IsNotSameLocker(Guid lockerId1, Guid lockerId2) + => lockerId1 != lockerId2; } } \ No newline at end of file diff --git a/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs b/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs index 2819ebbd..8d8e4922 100644 --- a/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs +++ b/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs @@ -34,6 +34,8 @@ public async Task> Handle(Query request, Cancellatio { var logs = _context.LockerLogs .Include(x => x.Object) + .Include(x => x.User) + .ThenInclude(x => x.Department) .AsQueryable(); if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) diff --git a/src/Application/Lockers/Queries/GetAllLockersPaginated.cs b/src/Application/Lockers/Queries/GetAllLockersPaginated.cs index c324ba80..db207f57 100644 --- a/src/Application/Lockers/Queries/GetAllLockersPaginated.cs +++ b/src/Application/Lockers/Queries/GetAllLockersPaginated.cs @@ -18,7 +18,7 @@ public record Query : IRequest> { public string CurrentUserRole { get; init; } = null!; public Guid CurrentUserDepartmentId { get; init; } - public Guid? RoomId { get; set; } + public Guid? RoomId { get; init; } public string? SearchTerm { get; init; } public int? Page { get; init; } public int? Size { get; init; } @@ -39,11 +39,6 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task> Handle(Query request, CancellationToken cancellationToken) { - var lockers = _context.Lockers - .Include(x => x.Room) - .ThenInclude(y => y.Department) - .AsQueryable(); - if (request.CurrentUserRole.IsStaff()) { if (request.RoomId is null) @@ -64,6 +59,11 @@ public async Task> Handle(Query request, CancellationTo } } + var lockers = _context.Lockers + .Include(x => x.Room) + .ThenInclude(y => y.Department) + .AsQueryable(); + if (request.RoomId is not null) { lockers = lockers.Where(x => x.Room.Id == request.RoomId); @@ -90,7 +90,7 @@ public async Task> Handle(Query request, CancellationTo x => x.DepartmentId == departmentId, cancellationToken); - private static bool IsSameRoom(Guid currentUserRoomId, Guid roomId) - => currentUserRoomId == roomId; + private static bool IsSameRoom(Guid roomId1, Guid roomId2) + => roomId1 == roomId2; } } \ No newline at end of file diff --git a/src/Application/Lockers/Queries/GetLockerById.cs b/src/Application/Lockers/Queries/GetLockerById.cs index 42f07b8a..1f962c43 100644 --- a/src/Application/Lockers/Queries/GetLockerById.cs +++ b/src/Application/Lockers/Queries/GetLockerById.cs @@ -3,6 +3,7 @@ using Application.Common.Models.Dtos.Physical; using Application.Identity; using AutoMapper; +using Domain.Entities.Physical; using MediatR; using Microsoft.EntityFrameworkCore; @@ -41,7 +42,7 @@ public async Task Handle(Query request, CancellationToken cancellatio } if (request.CurrentUserRole.IsStaff() - && !LockerInSameDepartment(locker.Room.DepartmentId, request.CurrentUserDepartmentId)) + && !LockerInSameDepartment(locker, request.CurrentUserDepartmentId)) { throw new UnauthorizedAccessException(); } @@ -50,8 +51,8 @@ public async Task Handle(Query request, CancellationToken cancellatio } private static bool LockerInSameDepartment( - Guid lockerDepartmentId, - Guid currentUserDepartmentId) - => lockerDepartmentId == currentUserDepartmentId; + Locker locker, + Guid departmentId) + => locker.Room.DepartmentId == departmentId; } } \ No newline at end of file diff --git a/src/Application/Rooms/Commands/AddRoom.cs b/src/Application/Rooms/Commands/AddRoom.cs index c22f9d0e..e62bfb75 100644 --- a/src/Application/Rooms/Commands/AddRoom.cs +++ b/src/Application/Rooms/Commands/AddRoom.cs @@ -3,6 +3,7 @@ using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; using Domain.Entities.Logging; using Domain.Entities.Physical; using FluentValidation; @@ -44,7 +45,7 @@ private bool BeUnique(string name) public record Command : IRequest { - public Guid PerformingUserId { get; init; } + public User CurrentUser { get; init; } public string Name { get; init; } = null!; public string? Description { get; init; } public int Capacity { get; init; } @@ -55,10 +56,12 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + private readonly IDateTimeProvider _dateTimeProvider; + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -79,7 +82,7 @@ public async Task Handle(Command request, CancellationToken cancellatio throw new ConflictException("Room name already exists."); } - var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); var entity = new Room { Name = request.Name.Trim(), @@ -89,15 +92,16 @@ public async Task Handle(Command request, CancellationToken cancellatio Department = department, DepartmentId = request.DepartmentId, IsAvailable = true, - Created = LocalDateTime.FromDateTime(DateTime.Now), - CreatedBy = performingUser!.Id, + Created = localDateTimeNow, + CreatedBy = request.CurrentUser.Id, }; + var log = new RoomLog() { - User = performingUser, - UserId = performingUser.Id, + User = request.CurrentUser, + UserId = request.CurrentUser.Id, Object = entity, - Time = LocalDateTime.FromDateTime(DateTime.Now), + Time = localDateTimeNow, Action = RoomLogMessage.Add, }; var result = await _context.Rooms.AddAsync(entity, cancellationToken); diff --git a/src/Application/Rooms/Commands/DisableRoom.cs b/src/Application/Rooms/Commands/DisableRoom.cs deleted file mode 100644 index c70fe9a5..00000000 --- a/src/Application/Rooms/Commands/DisableRoom.cs +++ /dev/null @@ -1,86 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using FluentValidation; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Rooms.Commands; - -public class DisableRoom -{ - public class Validator : AbstractValidator - { - public Validator() - { - RuleLevelCascadeMode = CascadeMode.Stop; - - RuleFor(x => x.RoomId) - .NotEmpty().WithMessage("RoomId is required."); - } - } - - public record Command : IRequest - { - public Guid RoomId { get; init; } - } - - public class CommandHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public CommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - var room = await _context.Rooms - .Include(x => x.Department) - .Include(x => x.Staff) - .Include(x => x.Lockers) - .ThenInclude(y => y.Folders) - .FirstOrDefaultAsync(x => x.Id.Equals(request.RoomId), cancellationToken: cancellationToken); - - if (room is null) - { - throw new KeyNotFoundException("Room does not exist."); - } - - if (!room.IsAvailable) - { - throw new ConflictException("Room have already been disabled."); - } - - var canNotDisable = await _context.Documents - .CountAsync(x => x.Folder!.Locker.Room.Id.Equals(request.RoomId), cancellationToken) - > 0; - - if (canNotDisable) - { - throw new InvalidOperationException("Room cannot be disabled because it contains documents."); - } - - var lockers = _context.Lockers.Include(x=> x.Folders) - .Where(x => x.Room.Id.Equals(room.Id)); - - foreach (var locker in lockers) - { - foreach (var folder in locker.Folders) - { - folder.IsAvailable = false; - } - locker.IsAvailable = false; - } - _context.Lockers.UpdateRange(lockers); - room.IsAvailable = false; - var result = _context.Rooms.Update(room); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } - } -} \ No newline at end of file diff --git a/src/Application/Rooms/Commands/EnableRoom.cs b/src/Application/Rooms/Commands/EnableRoom.cs deleted file mode 100644 index d27e9faf..00000000 --- a/src/Application/Rooms/Commands/EnableRoom.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using FluentValidation; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Rooms.Commands; - -public class EnableRoom -{ - public class Validator : AbstractValidator - { - public Validator() - { - RuleLevelCascadeMode = CascadeMode.Stop; - RuleFor(x => x.RoomId) - .NotEmpty().WithMessage("RoomId is required."); - } - } - public record Command : IRequest - { - public Guid RoomId { get; init; } - } - - public class CommandHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public CommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - var room = await _context.Rooms - .Include(x => x.Department) - .Include(x => x.Staff) - .FirstOrDefaultAsync(x => x.Id.Equals(request.RoomId), cancellationToken: cancellationToken); - - if (room is null) - { - throw new KeyNotFoundException("Room does not exist."); - } - - if (room.IsAvailable) - { - throw new ConflictException("Room has already been enabled."); - } - - room.IsAvailable = true; - var result = _context.Rooms.Update(room); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } - } -} \ No newline at end of file diff --git a/src/Application/Rooms/Commands/RemoveRoom.cs b/src/Application/Rooms/Commands/RemoveRoom.cs index 1dad6da1..740da920 100644 --- a/src/Application/Rooms/Commands/RemoveRoom.cs +++ b/src/Application/Rooms/Commands/RemoveRoom.cs @@ -41,15 +41,15 @@ public async Task Handle(Command request, CancellationToken cancellatio { var room = await _context.Rooms .Include(x => x.Department) - .FirstOrDefaultAsync(x => x.Id.Equals(request.RoomId), cancellationToken: cancellationToken); - + .FirstOrDefaultAsync(x => x.Id.Equals(request.RoomId), cancellationToken); + if (room is null) { throw new KeyNotFoundException("Room does not exist."); } var canNotRemove = await _context.Documents - .AnyAsync(x => x.Folder!.Locker.Room.Id.Equals(request.RoomId), cancellationToken: cancellationToken); + .AnyAsync(x => x.Department!.Id == room.DepartmentId, cancellationToken); if (canNotRemove) { throw new ConflictException("Room cannot be removed because it contains documents."); diff --git a/src/Application/Rooms/Commands/UpdateRoom.cs b/src/Application/Rooms/Commands/UpdateRoom.cs index 1bd222ca..e215c25f 100644 --- a/src/Application/Rooms/Commands/UpdateRoom.cs +++ b/src/Application/Rooms/Commands/UpdateRoom.cs @@ -3,6 +3,7 @@ using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; using Domain.Entities.Logging; using Domain.Entities.Physical; using FluentValidation; @@ -33,7 +34,7 @@ public Validator() } public record Command : IRequest { - public Guid PerformingUserId { get; init; } + public User CurrentUser { get; init; } public Guid RoomId { get; init; } public string Name { get; init; } = null!; public string? Description { get; init; } @@ -45,11 +46,13 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -64,12 +67,7 @@ public async Task Handle(Command request, CancellationToken cancellatio throw new KeyNotFoundException("Room does not exist."); } - var nameExisted = await _context.Rooms.AnyAsync(x => x.Name - .ToLower().Equals(request.Name.ToLower()) - && x.Id != room.Id - , cancellationToken: cancellationToken); - - if (nameExisted) + if (await DuplicatedNameRoomExistsAsync(request.Name, request.RoomId, cancellationToken)) { throw new ConflictException("Name has already exists."); } @@ -79,30 +77,46 @@ public async Task Handle(Command request, CancellationToken cancellatio throw new ConflictException("New capacity cannot be less than current number of lockers."); } - var performingUser = await _context.Users - .FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); - + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + // update work room.Name = request.Name; room.Description = request.Description; room.Capacity = request.Capacity; room.IsAvailable = request.IsAvailable; - room.LastModified = LocalDateTime.FromDateTime(DateTime.Now); - room.LastModifiedBy = performingUser!.Id; + room.LastModified = localDateTimeNow; + room.LastModifiedBy = request.CurrentUser.Id; var log = new RoomLog() { - User = performingUser, - UserId = performingUser.Id, + User = request.CurrentUser, + UserId = request.CurrentUser.Id, Object = room, - Time = LocalDateTime.FromDateTime(DateTime.Now), + Time = localDateTimeNow, Action = RoomLogMessage.Update, }; - var result = _context.Rooms.Update(room); await _context.RoomLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } + + private async Task DuplicatedNameRoomExistsAsync( + string roomName, + Guid roomId, + CancellationToken cancellationToken) + { + var room = await _context.Rooms.FirstOrDefaultAsync( + x => EqualsInvariant(x.Name, roomName) + && IsNotSameRoom(x.Id, roomId), + cancellationToken); + return room is not null; + } + + private static bool EqualsInvariant(string x, string y) + => x.Trim().ToLower().Equals(y.Trim().ToLower()); + + private static bool IsNotSameRoom(Guid roomId1, Guid roomId2) + => roomId1 != roomId2; } } \ No newline at end of file diff --git a/src/Application/Rooms/Queries/GetRoomById.cs b/src/Application/Rooms/Queries/GetRoomById.cs index 97dc9a56..61108259 100644 --- a/src/Application/Rooms/Queries/GetRoomById.cs +++ b/src/Application/Rooms/Queries/GetRoomById.cs @@ -1,3 +1,4 @@ +using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; using AutoMapper; @@ -11,6 +12,8 @@ public class GetRoomById { public record Query : IRequest { + public string CurrentUserRole { get; init; } = null!; + public Guid CurrentUserDepartmentId { get; init; } public Guid RoomId { get; init; } } @@ -31,13 +34,22 @@ public async Task Handle(Query request, CancellationToken cancellationT .Include(x => x.Department) .Include(x => x.Staff) .FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken: cancellationToken); - + if (room is null) { throw new KeyNotFoundException("Room does not exist."); } - + + if (request.CurrentUserRole.IsStaff() + && !IsSameDepartment(request.CurrentUserDepartmentId, room.DepartmentId)) + { + throw new UnauthorizedAccessException("User cannot update this resource."); + } + return _mapper.Map(room); } + + private static bool IsSameDepartment(Guid departmentId1, Guid departmentId2) + => departmentId1 == departmentId2; } } \ No newline at end of file diff --git a/src/Application/Users/Commands/AddUser.cs b/src/Application/Users/Commands/AddUser.cs index cebb593b..6094c0ff 100644 --- a/src/Application/Users/Commands/AddUser.cs +++ b/src/Application/Users/Commands/AddUser.cs @@ -70,12 +70,14 @@ public class AddUserCommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly ISecurityService _securityService; + private readonly IDateTimeProvider _dateTimeProvider; - public AddUserCommandHandler(IApplicationDbContext context, IMapper mapper, ISecurityService securityService) + public AddUserCommandHandler(IApplicationDbContext context, IMapper mapper, ISecurityService securityService, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; _securityService = securityService; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -99,6 +101,8 @@ public async Task Handle(Command request, CancellationToken cancellatio var password = StringUtil.RandomPassword(); var salt = StringUtil.RandomSalt(); + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + var entity = new User { Username = request.Username, @@ -112,15 +116,16 @@ public async Task Handle(Command request, CancellationToken cancellatio Position = request.Position, IsActive = true, IsActivated = false, - Created = LocalDateTime.FromDateTime(DateTime.Now), + Created = localDateTimeNow, CreatedBy = request.PerformingUser.Id, }; + var log = new UserLog() { User = request.PerformingUser, UserId = request.PerformingUser.Id, Object = entity, - Time = LocalDateTime.FromDateTime(DateTime.Now), + Time = localDateTimeNow, Action = UserLogMessages.Add, }; entity.AddDomainEvent(new UserCreatedEvent(entity, password)); diff --git a/src/Application/Users/Commands/DisableUser.cs b/src/Application/Users/Commands/DisableUser.cs deleted file mode 100644 index c84113b1..00000000 --- a/src/Application/Users/Commands/DisableUser.cs +++ /dev/null @@ -1,62 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Messages; -using Application.Users.Queries; -using AutoMapper; -using Domain.Entities.Logging; -using MediatR; -using Microsoft.EntityFrameworkCore; -using NodaTime; - -namespace Application.Users.Commands; - -public class DisableUser -{ - public record Command : IRequest - { - public Guid PerformingUserId { get; init; } - public Guid UserId { get; init; } - } - - public class CommandHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - public CommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - var user = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.UserId, cancellationToken); - if (user is null) - { - throw new KeyNotFoundException("User does not exist."); - } - - if (!user.IsActive) - { - throw new ConflictException("User has already been disabled."); - } - - var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); - user.IsActive = false; - user.LastModified = LocalDateTime.FromDateTime(DateTime.Now); - user.LastModifiedBy = performingUser!.Id; - var log = new UserLog() - { - User = performingUser, - UserId = performingUser.Id, - Object = user, - Time = LocalDateTime.FromDateTime(DateTime.Now), - Action = UserLogMessages.Disable, - }; - var result = _context.Users.Update(user); - await _context.UserLogs.AddAsync(log, cancellationToken); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } - } -} \ No newline at end of file diff --git a/src/Application/Users/Commands/EnableUser.cs b/src/Application/Users/Commands/EnableUser.cs deleted file mode 100644 index 38609da8..00000000 --- a/src/Application/Users/Commands/EnableUser.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Application.Users.Queries; -using MediatR; - -namespace Application.Users.Commands; - -public class EnableUser -{ - public record Command : IRequest - { - public Guid PerformingUserId { get; init; } - public Guid UserId { get; init; } - } -} \ No newline at end of file diff --git a/src/Application/Users/Commands/UpdateUser.cs b/src/Application/Users/Commands/UpdateUser.cs index 370a580a..f28a8a66 100644 --- a/src/Application/Users/Commands/UpdateUser.cs +++ b/src/Application/Users/Commands/UpdateUser.cs @@ -45,11 +45,13 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -69,19 +71,22 @@ public async Task Handle(Command request, CancellationToken cancellatio throw new KeyNotFoundException("User does not exist."); } + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + user.FirstName = request.FirstName; user.LastName = request.LastName; user.Position = request.Position; user.Role = request.Role; user.IsActive = request.IsActive; - user.LastModified = LocalDateTime.FromDateTime(DateTime.Now); + user.LastModified = localDateTimeNow; user.LastModifiedBy = request.CurrentUser.Id; + var log = new UserLog() { User = request.CurrentUser, UserId = request.CurrentUser.Id, Object = user, - Time = LocalDateTime.FromDateTime(DateTime.Now), + Time = localDateTimeNow, Action = UserLogMessages.Update, }; var result = _context.Users.Update(user); diff --git a/src/Application/Users/Queries/GetAllUserLogsPaginated.cs b/src/Application/Users/Queries/GetAllUserLogsPaginated.cs index ddf00959..6d6d037c 100644 --- a/src/Application/Users/Queries/GetAllUserLogsPaginated.cs +++ b/src/Application/Users/Queries/GetAllUserLogsPaginated.cs @@ -34,6 +34,8 @@ public async Task> Handle(Query request, CancellationT { var logs = _context.UserLogs .Include(x => x.Object) + .Include(x => x.User) + .ThenInclude(x => x.Department) .AsQueryable(); if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) diff --git a/src/Infrastructure/ConfigureServices.cs b/src/Infrastructure/ConfigureServices.cs index e79da5ec..1523448f 100644 --- a/src/Infrastructure/ConfigureServices.cs +++ b/src/Infrastructure/ConfigureServices.cs @@ -23,6 +23,7 @@ public static IServiceCollection AddInfrastructureServices(this IServiceCollecti services.AddScoped(sp => sp.GetService()!); services.AddScoped(); services.AddScoped(); + services.AddTransient(); services.AddMailService(configuration); services.AddJweAuthentication(configuration); diff --git a/src/Infrastructure/Services/DateTimeService.cs b/src/Infrastructure/Services/DateTimeService.cs new file mode 100644 index 00000000..bfed60e1 --- /dev/null +++ b/src/Infrastructure/Services/DateTimeService.cs @@ -0,0 +1,8 @@ +using Application.Common.Interfaces; + +namespace Infrastructure.Services; + +public class DateTimeService : IDateTimeProvider +{ + public DateTime DateTimeNow => DateTime.Now; +} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Folders/Commands/DisableFolderTests.cs b/tests/Application.Tests.Integration/Folders/Commands/DisableFolderTests.cs deleted file mode 100644 index cf2628d7..00000000 --- a/tests/Application.Tests.Integration/Folders/Commands/DisableFolderTests.cs +++ /dev/null @@ -1,119 +0,0 @@ -using Application.Common.Exceptions; -using Application.Folders.Commands; -using Domain.Entities; -using Domain.Entities.Physical; -using FluentAssertions; -using Xunit; - -namespace Application.Tests.Integration.Folders.Commands; - -public class DisableFolderTests : BaseClassFixture -{ - public DisableFolderTests(CustomApiFactory apiFactory) : base(apiFactory) - { - } - - [Fact] - public async Task ShouldDisableFolder_WhenFolderHaveNoDocument() - { - // Arrange - var department = CreateDepartment(); - var folder = CreateFolder(); - var locker = CreateLocker(folder); - var room = CreateRoom(department, locker); - await AddAsync(room); - - var disableFolderCommand = new DisableFolder.Command() - { - FolderId = folder.Id - }; - - // Act - var disabledFolder = await SendAsync(disableFolderCommand); - - // Assert - disabledFolder.IsAvailable.Should().BeFalse(); - - // Cleanup - Remove(folder); - Remove(locker); - Remove(room); - Remove(await FindAsync(department.Id)); - } - - [Fact] - public async Task ShouldThrowKeyNotFoundException_WhenFolderDoesNotExist() - { - // Arrange - var disableFolderCommand = new DisableFolder.Command() - { - FolderId = Guid.NewGuid() - }; - - // Act - var result = async () => await SendAsync(disableFolderCommand); - - // Assert - await result.Should().ThrowAsync() - .WithMessage("Folder does not exist."); - } - - [Fact] - public async Task ShouldThrowInvalidOperationException_WhenFolderIsAlreadyDisabled() - { - // Arrange - var department = CreateDepartment(); - var folder = CreateFolder(); - folder.IsAvailable = false; - var locker = CreateLocker(folder); - var room = CreateRoom(department, locker); - await AddAsync(room); - var disableFolderCommand = new DisableFolder.Command() - { - FolderId = folder.Id - }; - - // Act - var result = async () => await SendAsync(disableFolderCommand); - - // Assert - await result.Should().ThrowAsync() - .WithMessage("Folder has already been disabled."); - - // Cleanup - Remove(folder); - Remove(locker); - Remove(room); - Remove(await FindAsync(department.Id)); - } - - [Fact] - public async Task ShouldThrowInvalidOperationException_WhenFolderHasDocuments() - { - // Arrange - var department = CreateDepartment(); - var document = CreateNDocuments(1).First(); - var folder = CreateFolder(document); - var locker = CreateLocker(folder); - var room = CreateRoom(department, locker); - await AddAsync(room); - var disableFolderCommand = new DisableFolder.Command() - { - FolderId = folder.Id - }; - - // Act - var result = async () => await SendAsync(disableFolderCommand); - - // Assert - await result.Should().ThrowAsync() - .WithMessage("Folder cannot be disabled because it contains documents."); - - // Cleanup - Remove(document); - Remove(folder); - Remove(locker); - Remove(room); - Remove(await FindAsync(department.Id)); - } -} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Folders/Commands/EnableFolderTests.cs b/tests/Application.Tests.Integration/Folders/Commands/EnableFolderTests.cs deleted file mode 100644 index 62f0d165..00000000 --- a/tests/Application.Tests.Integration/Folders/Commands/EnableFolderTests.cs +++ /dev/null @@ -1,90 +0,0 @@ -using Application.Common.Exceptions; -using Application.Folders.Commands; -using Domain.Entities; -using FluentAssertions; -using Xunit; - -namespace Application.Tests.Integration.Folders.Commands; - -public class EnableFolderTests : BaseClassFixture -{ - public EnableFolderTests(CustomApiFactory apiFactory) : base(apiFactory) - { - } - - [Fact] - public async Task ShouldEnableFolder_WhenThatFolderExistsAndIsDisabled() - { - // Arrange - var department = CreateDepartment(); - var folder = CreateFolder(); - var locker = CreateLocker(folder); - var room = CreateRoom(department, locker); - folder.IsAvailable = false; - await AddAsync(room); - - var command = new EnableFolder.Command() - { - FolderId = folder.Id, - }; - - // Act - var result = await SendAsync(command); - - // Assert - result.IsAvailable.Should().BeTrue(); - - // Cleanup - Remove(folder); - Remove(locker); - Remove(room); - Remove(await FindAsync(department.Id)); - } - - [Fact] - public async Task ShouldThrowKeyNotFoundException_WhenThatFolderDoesNotExist() - { - // Arrange - var command = new EnableFolder.Command() - { - FolderId = Guid.NewGuid(), - }; - - // Act - var action = async () => await SendAsync(command); - - // Assert - await action.Should().ThrowAsync() - .WithMessage("Folder does not exist."); - } - - [Fact] - public async Task ShouldThrowConflictException_WhenFolderIsAlreadyAvailable() - { - // Arrange - var department = CreateDepartment(); - var folder = CreateFolder(); - folder.IsAvailable = true; - var locker = CreateLocker(folder); - var room = CreateRoom(department, locker); - await AddAsync(room); - - var command = new EnableFolder.Command() - { - FolderId = folder.Id, - }; - - // Act - var action = async () => await SendAsync(command); - - // Assert - await action.Should().ThrowAsync() - .WithMessage("Folder has already been enabled."); - - // Cleanup - Remove(folder); - Remove(locker); - Remove(room); - Remove(await FindAsync(department.Id)); - } -} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Lockers/Commands/DisableLockerTests.cs b/tests/Application.Tests.Integration/Lockers/Commands/DisableLockerTests.cs deleted file mode 100644 index 5cf76d23..00000000 --- a/tests/Application.Tests.Integration/Lockers/Commands/DisableLockerTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -using Application.Common.Exceptions; -using Application.Lockers.Commands; -using Domain.Entities; -using Domain.Entities.Physical; -using FluentAssertions; -using Xunit; - -namespace Application.Tests.Integration.Lockers.Commands; - -public class DisableLockerTests : BaseClassFixture -{ - public DisableLockerTests(CustomApiFactory apiFactory) : base(apiFactory) - { - } - - [Fact] - public async Task ShouldDisableLocker_WhenLockerExistsAndIsAvailable() - { - // Arrange - var department = CreateDepartment(); - var locker = CreateLocker(); - var room = CreateRoom(department, locker); - await AddAsync(room); - - var disableLockerCommand = new DisableLocker.Command() - { - LockerId = locker.Id, - }; - - // Act - var result = await SendAsync(disableLockerCommand); - - // Assert - result.Name.Should().Be(locker.Name); - result.Description.Should().Be(locker.Description); - result.Capacity.Should().Be(locker.Capacity); - result.IsAvailable.Should().BeFalse(); - result.NumberOfFolders.Should().Be(locker.NumberOfFolders); - - // Cleanup - Remove(room); - Remove(await FindAsync(department.Id)); - } - - [Fact] - public async Task ShouldThrowKeyNotFoundException_WhenLockerDoesNotExist() - { - // Arrange - var disableLockerCommand = new DisableLocker.Command() - { - LockerId = Guid.NewGuid(), - }; - - // Act - var action = async () => await SendAsync(disableLockerCommand); - - // Assert - await action.Should() - .ThrowAsync() - .WithMessage("Locker does not exist."); - } - - [Fact] - public async Task ShouldThrowConflictException_WhenLockerIsAlreadyDisabled() - { - // Arrange - var department = CreateDepartment(); - var locker = CreateLocker(); - var room = CreateRoom(department, locker); - await AddAsync(room); - - var disableLockerCommand = new DisableLocker.Command() - { - LockerId = locker.Id, - }; - - // Act - await SendAsync(disableLockerCommand); - var action = async () => await SendAsync(disableLockerCommand); - - // Assert - await action.Should().ThrowAsync().WithMessage("Locker has already been disabled."); - - // Cleanup - Remove(room); - Remove(await FindAsync(department.Id)); - } -} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Lockers/Commands/EnableLockerTests.cs b/tests/Application.Tests.Integration/Lockers/Commands/EnableLockerTests.cs deleted file mode 100644 index b4ec3df7..00000000 --- a/tests/Application.Tests.Integration/Lockers/Commands/EnableLockerTests.cs +++ /dev/null @@ -1,86 +0,0 @@ -using Application.Common.Exceptions; -using Application.Lockers.Commands; -using Domain.Entities; -using FluentAssertions; -using Xunit; - -namespace Application.Tests.Integration.Lockers.Commands; - -public class EnableLockerTests : BaseClassFixture -{ - public EnableLockerTests(CustomApiFactory apiFactory) : base(apiFactory) - { - - } - - [Fact] - public async Task ShouldEnableLocker_WhenLockerExistsAndIsNotAvailable() - { - // Arrange - var department = CreateDepartment(); - var locker = CreateLocker(); - locker.IsAvailable = false; - var room = CreateRoom(department, locker); - await AddAsync(room); - - // Act - var command = new EnableLocker.Command() - { - LockerId = locker.Id, - }; - - var result = await SendAsync(command); - - // Assert - result.IsAvailable.Should().BeTrue(); - - // Cleanup - Remove(room); - Remove(await FindAsync(department.Id)); - } - - [Fact] - public async Task ShouldThrowKeyNotFoundException_WhenLockerDoesNotExist() - { - // Arrange - var command = new EnableLocker.Command() - { - LockerId = Guid.NewGuid(), - }; - - // Act - var action = async () => await SendAsync(command); - - // Assert - await action.Should() - .ThrowAsync() - .WithMessage("Locker does not exist."); - } - - [Fact] - public async Task ShouldThrowConflictException_WhenLockerIsAlreadyEnabled() - { - // Arrange - var department = CreateDepartment(); - var locker = CreateLocker(); - var room = CreateRoom(department, locker); - await AddAsync(room); - - var enableLockerCommand = new EnableLocker.Command() - { - LockerId = locker.Id, - }; - - // Act - var action = async () => await SendAsync(enableLockerCommand); - - // Assert - await action.Should() - .ThrowAsync() - .WithMessage("Locker has already been enabled."); - - // Cleanup - Remove(room); - Remove(await FindAsync(department.Id)); - } -} diff --git a/tests/Application.Tests.Integration/Rooms/Commands/DisableRoomTests.cs b/tests/Application.Tests.Integration/Rooms/Commands/DisableRoomTests.cs deleted file mode 100644 index 39badcb8..00000000 --- a/tests/Application.Tests.Integration/Rooms/Commands/DisableRoomTests.cs +++ /dev/null @@ -1,123 +0,0 @@ -using Application.Common.Exceptions; -using Application.Rooms.Commands; -using Domain.Entities; -using Domain.Entities.Physical; -using FluentAssertions; -using Xunit; - -namespace Application.Tests.Integration.Rooms.Commands; - -public class DisableRoomTests : BaseClassFixture -{ - public DisableRoomTests(CustomApiFactory apiFactory) : base(apiFactory) - { - } - - [Fact] - public async Task ShouldDisableRoom_WhenRoomHaveNoDocument() - { - // Arrange - var department = CreateDepartment(); - var folder = CreateFolder(); - var locker = CreateLocker(folder); - var room = CreateRoom(department, locker); - await AddAsync(room); - - var disableRoomCommand = new DisableRoom.Command() - { - RoomId = room.Id - }; - - // Act - var result = await SendAsync(disableRoomCommand); - - // Assert - var folderResult = await FindAsync(folder.Id); - var lockerResult = await FindAsync(locker.Id); - - result.IsAvailable.Should().BeFalse(); - folderResult.IsAvailable.Should().BeFalse(); - lockerResult.IsAvailable.Should().BeFalse(); - - // Cleanup - Remove(folder); - Remove(locker); - Remove(room); - Remove(await FindAsync(department.Id)); - } - - [Fact] - public async Task ShouldThrowKeyNotFoundException_WhenRoomDoesNotExist() - { - // Arrange - var disableRoomCommand = new DisableRoom.Command() - { - RoomId = Guid.NewGuid() - }; - - // Act - var action = async () => await SendAsync(disableRoomCommand); - - // Assert - await action.Should().ThrowAsync() - .WithMessage("Room does not exist."); - } - - [Fact] - public async Task ShouldThrowInvalidOperationException_WhenRoomIsNotEmptyOfDocuments() - { - // Arrange - var department = CreateDepartment(); - var documents = CreateNDocuments(1); - var folder = CreateFolder(documents); - var locker = CreateLocker(folder); - var room = CreateRoom(department, locker); - - await AddAsync(room); - - var disableRoomCommand = new DisableRoom.Command() - { - RoomId = room.Id - }; - - // Act - var action = async () => await SendAsync(disableRoomCommand); - - // Assert - await action.Should().ThrowAsync() - .WithMessage("Room cannot be disabled because it contains documents."); - - // Cleanup - Remove(documents.First()); - Remove(folder); - Remove(locker); - Remove(room); - Remove(await FindAsync(department.Id)); - } - - [Fact] - public async Task ShouldThrowInvalidOperationException_WhenRoomIsNotAvailable() - { - // Arrange - var department = CreateDepartment(); - var room = CreateRoom(department); - room.IsAvailable = false; - await AddAsync(room); - - var command = new DisableRoom.Command() - { - RoomId = room.Id - }; - - // Act - var action = async () => await SendAsync(command); - - // Assert - await action.Should().ThrowAsync() - .WithMessage("Room have already been disabled."); - - // Cleanup - Remove(room); - Remove(await FindAsync(department.Id)); - } -} \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Rooms/Commands/EnableRoomTests.cs b/tests/Application.Tests.Integration/Rooms/Commands/EnableRoomTests.cs deleted file mode 100644 index fa07d0b8..00000000 --- a/tests/Application.Tests.Integration/Rooms/Commands/EnableRoomTests.cs +++ /dev/null @@ -1,94 +0,0 @@ -using Application.Common.Exceptions; -using Application.Rooms.Commands; -using Domain.Entities; -using Domain.Entities.Physical; -using FluentAssertions; -using Xunit; - -namespace Application.Tests.Integration.Rooms.Commands; - -public class EnableRoomTests : BaseClassFixture -{ - public EnableRoomTests(CustomApiFactory apiFactory) : base(apiFactory) - { - } - - [Fact] - public async Task ShouldEnableRoom_WhenRoomExistsAndIsDisabled() - { - // Arrange - var department = CreateDepartment(); - var folder = CreateFolder(); - var locker = CreateLocker(folder); - var room = CreateRoom(department, locker); - folder.IsAvailable = false; - locker.IsAvailable = false; - room.IsAvailable = false; - await AddAsync(room); - - var command = new EnableRoom.Command() - { - RoomId = room.Id - }; - - // Act - var result = await SendAsync(command); - - // Assert - var folderResult = await FindAsync(folder.Id); - var lockerResult = await FindAsync(locker.Id); - - result.IsAvailable.Should().BeTrue(); - folderResult!.IsAvailable.Should().BeFalse(); - lockerResult!.IsAvailable.Should().BeFalse(); - - // Cleanup - Remove(folder); - Remove(locker); - Remove(room); - Remove(await FindAsync(department.Id)); - } - - [Fact] - public async Task ShouldThrowKeyNotFoundException_WhenRoomDoesNotExist() - { - // Arrange - var command = new EnableRoom.Command() - { - RoomId = Guid.NewGuid() - }; - - // Act - var action = async () => await SendAsync(command); - - // Assert - await action.Should().ThrowAsync() - .WithMessage("Room does not exist."); - } - - [Fact] - public async Task ShouldThrowConflictException_WhenRoomIsAlreadyAvailable() - { - // Arrange - var department = CreateDepartment(); - var room = CreateRoom(department); - room.IsAvailable = true; - await AddAsync(room); - - var command = new EnableRoom.Command() - { - RoomId = room.Id - }; - - // Act - var action = async () => await SendAsync(command); - - // Assert - await action.Should().ThrowAsync() - .WithMessage("Room has already been enabled."); - - // Cleanup - Remove(room); - Remove(await FindAsync(department.Id)); - } -} \ No newline at end of file From 2047da06e93432b9fbb351fe3f94427ecfd64dad Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Sat, 17 Jun 2023 20:22:08 +0700 Subject: [PATCH 43/56] haha --- src/Api/Controllers/DocumentsController.cs | 173 ++++++------------ ...entsForEmployeePaginatedQueryParameters.cs | 8 + ...GetAllDocumentsPaginatedQueryParameters.cs | 3 + .../Requests/Staffs/AddStaffRequest.cs | 2 +- src/Api/Controllers/RoomsController.cs | 21 +++ src/Api/Controllers/StaffsController.cs | 63 +++---- src/Api/Controllers/UsersController.cs | 4 +- .../Common/Extensions/QueryableExtensions.cs | 2 +- .../Common/Messages/UserLogMessages.cs | 5 +- .../Common/Models/Dtos/DepartmentDto.cs | 9 +- .../GetAllDocumentsForEmployeePaginated.cs | 110 +++++++++++ .../Queries/GetAllDocumentsPaginated.cs | 26 ++- .../Queries/GetAllIssuedDocumentsPaginated.cs | 2 +- .../Documents/Queries/GetDocumentById.cs | 47 +++-- .../Queries/GetDocumentsOfUserPaginated.cs | 2 +- .../Queries/GetSelfDocumentsPaginated.cs | 2 +- .../Folders/Queries/GetAllFoldersPaginated.cs | 2 +- .../Lockers/Queries/GetAllLockersPaginated.cs | 2 +- src/Application/Rooms/Commands/UpdateRoom.cs | 2 +- .../Rooms/Queries/GetAllRoomsPaginated.cs | 2 +- .../Rooms/Queries/GetRoomByStaffId.cs | 43 +++++ src/Application/Staffs/Commands/AddStaff.cs | 79 +++----- .../Staffs/Commands/RemoveStaffFromRoom.cs | 17 +- .../EventHandlers/StaffCreatedEventHandler.cs | 28 +++ .../Staffs/Queries/GetAllStaffsPaginated.cs | 2 +- ...{GetStaffByRoom.cs => GetStaffByRoomId.cs} | 2 +- src/Application/Users/Commands/AddUser.cs | 20 +- .../Users/Queries/GetAllUsersPaginated.cs | 2 +- src/Domain/Entities/Department.cs | 3 +- src/Domain/Entities/Physical/Document.cs | 3 +- src/Domain/Events/StaffCreatedEvent.cs | 16 ++ .../Configurations/DocumentConfiguration.cs | 2 +- .../Configurations/RoomConfiguration.cs | 7 +- .../ApplicationDbContextModelSnapshot.cs | 9 +- .../Queries/GetAllStaffsPaginatedTests.cs | 3 +- .../Staffs/Queries/GetStaffByRoomTests.cs | 6 +- 36 files changed, 445 insertions(+), 284 deletions(-) create mode 100644 src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForEmployeePaginatedQueryParameters.cs create mode 100644 src/Application/Documents/Queries/GetAllDocumentsForEmployeePaginated.cs create mode 100644 src/Application/Rooms/Queries/GetRoomByStaffId.cs create mode 100644 src/Application/Staffs/EventHandlers/StaffCreatedEventHandler.cs rename src/Application/Staffs/Queries/{GetStaffByRoom.cs => GetStaffByRoomId.cs} (97%) create mode 100644 src/Domain/Events/StaffCreatedEvent.cs diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index ab2e94df..2249208f 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -18,12 +18,10 @@ namespace Api.Controllers; public class DocumentsController : ApiControllerBase { private readonly ICurrentUserService _currentUserService; - private readonly IPermissionManager _permissionManager; - public DocumentsController(ICurrentUserService currentUserService, IPermissionManager permissionManager) + public DocumentsController(ICurrentUserService currentUserService) { _currentUserService = currentUserService; - _permissionManager = permissionManager; } /// @@ -37,78 +35,33 @@ public DocumentsController(ICurrentUserService currentUserService, IPermissionMa [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetById([FromRoute] Guid documentId) { + var currentUser = _currentUserService.GetCurrentUser(); var query = new GetDocumentById.Query() { + CurrentUser = currentUser, DocumentId = documentId, }; var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); } - - /// - /// Get all documents paginated - /// - /// Get all documents query parameters - /// A paginated list of DocumentDto - [RequiresRole(IdentityData.Roles.Staff)] - [HttpGet("issued")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>>> GetAllIssuedPaginated( - [FromQuery] GetAllIssuedPaginatedQueryParameters queryParameters) - { - var departmentId = _currentUserService.GetCurrentDepartmentForStaff(); - var query = new GetAllIssuedDocumentsPaginated.Query() - { - DepartmentId = departmentId!.Value, - SearchTerm = queryParameters.SearchTerm, - Page = queryParameters.Page, - Size = queryParameters.Size, - SortBy = queryParameters.SortBy, - SortOrder = queryParameters.SortOrder, - }; - var result = await Mediator.Send(query); - return Ok(Result>.Succeed(result)); - } - - /// - /// Get a document log by Id - /// - /// - /// Return a DocumentLogDto - [RequiresRole(IdentityData.Roles.Admin)] - [HttpGet("log/{logId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetLogById([FromRoute] Guid logId) - { - var query = new GetLogOfDocumentById.Query() - { - LogId = logId - }; - - var result = await Mediator.Send(query); - return Ok(Result.Succeed(result)); - } /// /// Get all documents paginated /// /// Get all documents query parameters /// A paginated list of DocumentDto - [RequiresRole(IdentityData.Roles.Admin)] + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>>> GetAllForAdminPaginated( + public async Task>>> GetAllPaginated( [FromQuery] GetAllDocumentsPaginatedQueryParameters queryParameters) { var query = new GetAllDocumentsPaginated.Query() { + UserId = queryParameters.UserId, RoomId = queryParameters.RoomId, LockerId = queryParameters.LockerId, FolderId = queryParameters.FolderId, @@ -117,88 +70,43 @@ public async Task>>> GetAllForAdm Size = queryParameters.Size, SortBy = queryParameters.SortBy, SortOrder = queryParameters.SortOrder, + IsPrivate = queryParameters.IsPrivate, + DocumentStatus = queryParameters.DocumentStatus, }; var result = await Mediator.Send(query); return Ok(Result>.Succeed(result)); } /// - /// Get documents of the employee - /// - /// - /// - [HttpGet("get-self-documents")] - [RequiresRole(IdentityData.Roles.Employee)] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task>>> GetSelfPaginated( - [FromQuery] GetSelfDocumentsPaginatedQueryParameters queryParameters) - { - var userId = _currentUserService.GetId(); - - var query = new GetSelfDocumentsPaginated.Query() - { - EmployeeId = userId, - Page = queryParameters.Page, - Size = queryParameters.Size, - SortBy = queryParameters.SortBy, - SearchTerm = queryParameters.SearchTerm, - SortOrder = queryParameters.SortOrder - }; - - var result = await Mediator.Send(query); - return Ok(Result>.Succeed(result)); - } - - /// Get all log of document - /// - /// - /// Paginated list of DocumentLogDto - [RequiresRole(IdentityData.Roles.Admin)] - [HttpGet("logs")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task>>> GetAllLogsPaginated( - [FromQuery] GetAllLogsPaginatedQueryParameters queryParameters) - { - var query = new GetAllDocumentLogsPaginated.Query() - { - SearchTerm = queryParameters.SearchTerm, - Page = queryParameters.Page, - Size = queryParameters.Size, - }; - var result = await Mediator.Send(query); - return Ok(Result>.Succeed(result)); - } - - /// - /// Get all documents for staff paginated + /// Get all documents paginated /// - /// Get all documents for staff query parameters + /// Get all documents query parameters /// A paginated list of DocumentDto - [RequiresRole(IdentityData.Roles.Staff)] - [HttpGet("staff")] + [RequiresRole(IdentityData.Roles.Employee)] + [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>>> GetAllForStaffPaginated( - [FromQuery] GetAllDocumentsForStaffPaginatedQueryParameters queryParameters) + public async Task>>> GetAllForEmployeePaginated( + [FromQuery] GetAllDocumentsForEmployeePaginatedQueryParameters queryParameters) { - var roomId = _currentUserService.GetCurrentRoomForStaff(); - var query = new GetAllDocumentsPaginated.Query() + var currentUser = _currentUserService.GetCurrentUser(); + var query = new GetAllDocumentsForEmployeePaginated.Query() { - RoomId = roomId, - LockerId = queryParameters.LockerId, - FolderId = queryParameters.FolderId, + CurrentUser = currentUser, SearchTerm = queryParameters.SearchTerm, Page = queryParameters.Page, Size = queryParameters.Size, SortBy = queryParameters.SortBy, SortOrder = queryParameters.SortOrder, + DocumentStatus = queryParameters.DocumentStatus, + IsPrivate = queryParameters.IsPrivate, }; var result = await Mediator.Send(query); return Ok(Result>.Succeed(result)); } - + /// /// Get all document types /// @@ -496,6 +404,47 @@ public async Task>> SharePermissions( return Ok(Result.Succeed(result)); } + /// + /// Get a document log by Id + /// + /// + /// Return a DocumentLogDto + [RequiresRole(IdentityData.Roles.Admin)] + [HttpGet("log/{logId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetLogById([FromRoute] Guid logId) + { + var query = new GetLogOfDocumentById.Query() + { + LogId = logId + }; + + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } + + /// + /// Get all log of document + /// + /// + /// Paginated list of DocumentLogDto + [RequiresRole(IdentityData.Roles.Admin)] + [HttpGet("logs")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>>> GetAllLogsPaginated( + [FromQuery] GetAllLogsPaginatedQueryParameters queryParameters) + { + var query = new GetAllDocumentLogsPaginated.Query() + { + SearchTerm = queryParameters.SearchTerm, + Page = queryParameters.Page, + Size = queryParameters.Size, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } + /// /// Get permissions for an employee of a specific document /// diff --git a/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForEmployeePaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForEmployeePaginatedQueryParameters.cs new file mode 100644 index 00000000..88b9ff9e --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForEmployeePaginatedQueryParameters.cs @@ -0,0 +1,8 @@ +namespace Api.Controllers.Payload.Requests.Documents; + +public class GetAllDocumentsForEmployeePaginatedQueryParameters : PaginatedQueryParameters +{ + public string? SearchTerm { get; set; } + public string? DocumentStatus { get; set; } + public bool IsPrivate { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsPaginatedQueryParameters.cs index 7b97fc8f..fb226c4f 100644 --- a/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsPaginatedQueryParameters.cs @@ -5,6 +5,7 @@ namespace Api.Controllers.Payload.Requests.Documents; /// public class GetAllDocumentsPaginatedQueryParameters : PaginatedQueryParameters { + public Guid? UserId { get; set; } /// /// Id of the room to find documents in /// @@ -21,4 +22,6 @@ public class GetAllDocumentsPaginatedQueryParameters : PaginatedQueryParameters /// Search term /// public string? SearchTerm { get; set; } + public string? DocumentStatus { get; set; } + public bool? IsPrivate { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Staffs/AddStaffRequest.cs b/src/Api/Controllers/Payload/Requests/Staffs/AddStaffRequest.cs index 47ebbc06..6e33caa6 100644 --- a/src/Api/Controllers/Payload/Requests/Staffs/AddStaffRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Staffs/AddStaffRequest.cs @@ -8,7 +8,7 @@ public class AddStaffRequest /// /// User id of the new staff /// - public Guid UserId { get; init; } + public Guid StaffId { get; init; } /// /// Id of the room this staff will be in /// diff --git a/src/Api/Controllers/RoomsController.cs b/src/Api/Controllers/RoomsController.cs index a9740d4c..02771afd 100644 --- a/src/Api/Controllers/RoomsController.cs +++ b/src/Api/Controllers/RoomsController.cs @@ -8,6 +8,7 @@ using Application.Identity; using Application.Rooms.Commands; using Application.Rooms.Queries; +using Application.Staffs.Queries; using Infrastructure.Identity.Authorization; using Microsoft.AspNetCore.Mvc; @@ -96,6 +97,26 @@ public async Task>> GetEmptyContainer return Ok(Result>.Succeed(result)); } + /// + /// Get a staff by room + /// + /// Id of the room to retrieve staff + /// A StaffDto of the retrieved staff + [HttpGet("{roomId:guid}/staffs")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetByRoom( + [FromRoute] Guid roomId) + { + var query = new GetStaffByRoomId.Query() + { + RoomId = roomId, + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } + /// /// Add a room /// diff --git a/src/Api/Controllers/StaffsController.cs b/src/Api/Controllers/StaffsController.cs index c8947d4e..b9d9ea64 100644 --- a/src/Api/Controllers/StaffsController.cs +++ b/src/Api/Controllers/StaffsController.cs @@ -3,6 +3,7 @@ using Application.Common.Models; using Application.Common.Models.Dtos.Physical; using Application.Identity; +using Application.Rooms.Queries; using Application.Staffs.Commands; using Application.Staffs.Queries; using Infrastructure.Identity.Authorization; @@ -24,37 +25,41 @@ public StaffsController(ICurrentUserService currentUserService) /// /// Id of the staff to be retrieved /// A StaffDto of the retrieved staff + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpGet("{staffId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetById([FromRoute] Guid staffId) + public async Task>> GetById( + [FromRoute] Guid staffId) { var query = new GetStaffById.Query() { - StaffId = staffId + StaffId = staffId, }; var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); } /// - /// Get a staff by room + /// Get room by staff id /// - /// Id of the room to retrieve staff + /// Id of the room to retrieve staff /// A StaffDto of the retrieved staff - [HttpGet("get-by-room/{roomId:guid}")] + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] + [HttpGet("{staffId:guid}/rooms")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetByRoom([FromRoute] Guid roomId) + public async Task>> GetRoomByStaffId( + [FromRoute] Guid staffId) { - var query = new GetStaffByRoom.Query() + var query = new GetRoomByStaffId.Query() { - RoomId = roomId + StaffId = staffId, }; var result = await Mediator.Send(query); - return Ok(Result.Succeed(result)); + return Ok(Result.Succeed(result)); } /// @@ -62,6 +67,7 @@ public async Task>> GetByRoom([FromRoute] Guid roo /// /// Get all staffs query parameters /// A paginated list of StaffDto + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -90,14 +96,15 @@ public async Task>>> GetAllPaginated [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> Add([FromBody] AddStaffRequest request) + public async Task>> Assign( + [FromBody] AddStaffRequest request) { - var performingUserId = _currentUserService.GetId(); + var currentUser = _currentUserService.GetCurrentUser(); var command = new AddStaff.Command() { - PerformingUserId = performingUserId, + CurrentUser = currentUser, RoomId = request.RoomId, - UserId = request.UserId, + StaffId = request.StaffId, }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); @@ -108,44 +115,22 @@ public async Task>> Add([FromBody] AddStaffRequest /// /// Id of the staff to be removed from room /// A StaffDto of the removed staff + [RequiresRole(IdentityData.Roles.Admin)] [HttpPut("{staffId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> RemoveFromRoom( + public async Task>> RemoveStaffFromRoom( [FromRoute] Guid staffId) { - var performingUserId = _currentUserService.GetId(); + var currentUser = _currentUserService.GetCurrentUser(); var command = new RemoveStaffFromRoom.Command() { - PerformingUserId = performingUserId, + CurrentUser = currentUser, StaffId = staffId, }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } - - /// - /// Remove a staff - /// - /// Id of the staff to be removed - /// A StaffDto of the removed staff - [HttpDelete("{staffId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> Remove( - [FromRoute] Guid staffId) - { - var performingUserId = _currentUserService.GetId(); - var command = new RemoveStaff.Command() - { - PerformingUserId = performingUserId, - StaffId = staffId - }; - - var result = await Mediator.Send(command); - return Ok(Result.Succeed(result)); - } } \ No newline at end of file diff --git a/src/Api/Controllers/UsersController.cs b/src/Api/Controllers/UsersController.cs index 974ea323..7f2e791b 100644 --- a/src/Api/Controllers/UsersController.cs +++ b/src/Api/Controllers/UsersController.cs @@ -113,10 +113,10 @@ public async Task>>> GetAllEmployeesP [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> Add([FromBody] AddUserRequest request) { - var performingUser = _currentUserService.GetCurrentUser(); + var currentUser = _currentUserService.GetCurrentUser(); var command = new AddUser.Command() { - PerformingUser = performingUser, + CurrentUser = currentUser, Username = request.Username, Email = request.Email, FirstName = request.FirstName, diff --git a/src/Application/Common/Extensions/QueryableExtensions.cs b/src/Application/Common/Extensions/QueryableExtensions.cs index 84a2ba94..c3d0afc7 100644 --- a/src/Application/Common/Extensions/QueryableExtensions.cs +++ b/src/Application/Common/Extensions/QueryableExtensions.cs @@ -57,7 +57,7 @@ public static async Task> LoggingListPaginateAsync(result, count, pageNumber.Value, sizeNumber.Value); } - public static async Task> ListPaginateWithFilterAsync( + public static async Task> ListPaginateWithSortAsync( this IQueryable items, int? page, int? size, diff --git a/src/Application/Common/Messages/UserLogMessages.cs b/src/Application/Common/Messages/UserLogMessages.cs index f205aa49..5599cb71 100644 --- a/src/Application/Common/Messages/UserLogMessages.cs +++ b/src/Application/Common/Messages/UserLogMessages.cs @@ -2,13 +2,14 @@ namespace Application.Common.Messages; public static class UserLogMessages { - public const string Add = "Added user"; + public static string Add(string role) => $"Added user with role {role}"; public const string Update = "Updated user"; public const string Disable = "Disabled user"; public static class Staff { - public static string AddStaff(string roomId) => $"Assigned user to be staff of room {roomId}"; + public const string AddStaff = "Added a new staff"; + public static string AssignStaff(string roomId) => $"Assigned user to be staff of room {roomId}"; public const string RemoveFromRoom = "Removed staff from room"; public const string Remove = "Removed staff"; } diff --git a/src/Application/Common/Models/Dtos/DepartmentDto.cs b/src/Application/Common/Models/Dtos/DepartmentDto.cs index 516e9974..2a22ab44 100644 --- a/src/Application/Common/Models/Dtos/DepartmentDto.cs +++ b/src/Application/Common/Models/Dtos/DepartmentDto.cs @@ -1,4 +1,5 @@ using Application.Common.Mappings; +using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities; @@ -7,12 +8,4 @@ namespace Application.Common.Models.Dtos; public class DepartmentDto : BaseDto, IMapFrom { public string Name { get; set; } = null!; - public Guid? RoomId { get; set; } - - public void Mapping(Profile profile) - { - profile.CreateMap() - .ForMember(dest => dest.RoomId, - opt => opt.MapFrom(src => src.Room!.Id)); - } } \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetAllDocumentsForEmployeePaginated.cs b/src/Application/Documents/Queries/GetAllDocumentsForEmployeePaginated.cs new file mode 100644 index 00000000..cb9c52cf --- /dev/null +++ b/src/Application/Documents/Queries/GetAllDocumentsForEmployeePaginated.cs @@ -0,0 +1,110 @@ +using Application.Common.Exceptions; +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.Physical; +using Application.Common.Models.Operations; +using AutoMapper; +using Domain.Entities; +using Domain.Entities.Physical; +using Domain.Statuses; +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Documents.Queries; + +public class GetAllDocumentsForEmployeePaginated +{ + public record Query : IRequest> + { + public User CurrentUser { get; init; } = null!; + public string? SearchTerm { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } + public string? DocumentStatus { get; init; } + public bool IsPrivate { get; init; } + } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, + CancellationToken cancellationToken) + { + var documents = _context.Documents.AsQueryable(); + + documents = documents + .Include(x => x.Department) + .Include(x => x.Folder) + .ThenInclude(y => y.Locker) + .ThenInclude(z => z.Room); + + if (request.IsPrivate) + { + var permissions = _context.Permissions.Where(x => + IsSameUser(x.EmployeeId, request.CurrentUser.Id) + && InSameDepartmentAsUser(x.Document, request.CurrentUser) + && HasReadPermission(x.AllowedOperations)) + .Select(x => x.DocumentId); + + documents = documents.Where(x => + InSameDepartmentAsUser(x, request.CurrentUser) + && x.IsPrivate + && (CanRead(permissions, x.Id) || IsImporter(x, request.CurrentUser))); + } + else + { + documents = documents.Where(x => + InSameDepartmentAsUser(x, request.CurrentUser) + && !x.IsPrivate); + } + + if (request.DocumentStatus is not null + && Enum.TryParse(request.DocumentStatus, true, out DocumentStatus status)) + { + documents = documents.Where(x => x.Status == status); + } + + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + documents = documents.Where(x => + x.Title.ToLower().Contains(request.SearchTerm.ToLower())); + } + + return await documents + .ListPaginateWithSortAsync( + request.Page, + request.Size, + request.SortBy, + request.SortOrder, + _mapper.ConfigurationProvider, + cancellationToken); + } + + private static bool IsSameUser(Guid userId1, Guid userId2) + => userId1 == userId2; + + private static bool InSameDepartmentAsUser(Document document, User user) + => document.Department!.Id == user.Department!.Id; + + private static bool HasReadPermission(string allowedPermissions) + => allowedPermissions.Contains(DocumentOperation.Read.ToString()); + + private static bool CanRead(IEnumerable documentIds, Guid documentId) + => documentIds.Contains(documentId); + + private static bool IsImporter(Document document, User currentUser) + => document.ImporterId == currentUser.Id; + } +} \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs b/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs index 4f2f555f..a8af8b3b 100644 --- a/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs +++ b/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs @@ -31,6 +31,7 @@ public Validator() public record Query : IRequest> { + public Guid? UserId { get; init; } public Guid? RoomId { get; init; } public Guid? LockerId { get; init; } public Guid? FolderId { get; init; } @@ -39,6 +40,8 @@ public record Query : IRequest> public int? Size { get; init; } public string? SortBy { get; init; } public string? SortOrder { get; init; } + public string? DocumentStatus { get; init; } + public bool? IsPrivate { get; init; } } public class QueryHandler : IRequestHandler> @@ -64,10 +67,23 @@ public async Task> Handle(Query request, .Include(x => x.Department) .Include(x => x.Folder) .ThenInclude(y => y.Locker) - .ThenInclude(z => z.Room) - .ThenInclude(t => t.Department) - .Where(x => !x.IsPrivate && x.Status != DocumentStatus.Issued); - + .ThenInclude(z => z.Room); + + if (request.DocumentStatus is not null + && Enum.TryParse(request.DocumentStatus, true, out DocumentStatus status)) + { + documents = documents.Where(x => x.Status == status); + } + + if (request.IsPrivate is not null) + { + documents = documents.Where(x => x.IsPrivate == request.IsPrivate); + } + + if (request.UserId is not null) + { + documents = documents.Where(x => x.Importer!.Id == request.UserId); + } if (folderExists) { @@ -128,7 +144,7 @@ public async Task> Handle(Query request, } return await documents - .ListPaginateWithFilterAsync( + .ListPaginateWithSortAsync( request.Page, request.Size, request.SortBy, diff --git a/src/Application/Documents/Queries/GetAllIssuedDocumentsPaginated.cs b/src/Application/Documents/Queries/GetAllIssuedDocumentsPaginated.cs index 6e1a7afe..207309c8 100644 --- a/src/Application/Documents/Queries/GetAllIssuedDocumentsPaginated.cs +++ b/src/Application/Documents/Queries/GetAllIssuedDocumentsPaginated.cs @@ -52,7 +52,7 @@ public async Task> Handle(Query request, } return await documents - .ListPaginateWithFilterAsync( + .ListPaginateWithSortAsync( request.Page, request.Size, request.SortBy, diff --git a/src/Application/Documents/Queries/GetDocumentById.cs b/src/Application/Documents/Queries/GetDocumentById.cs index 054bf40f..5300072a 100644 --- a/src/Application/Documents/Queries/GetDocumentById.cs +++ b/src/Application/Documents/Queries/GetDocumentById.cs @@ -1,9 +1,12 @@ using Application.Common.Exceptions; +using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; using Application.Common.Models.Operations; using Application.Identity; using AutoMapper; +using Domain.Entities; +using Domain.Entities.Physical; using MediatR; using Microsoft.EntityFrameworkCore; @@ -13,23 +16,26 @@ public class GetDocumentById { public record Query : IRequest { - public Guid DocumentId { get; init; } + public User CurrentUser { get; init; } = null!; + public Guid DocumentId { get; init; } } public class QueryHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; - private readonly ICurrentUserService _currentUserService; private readonly IPermissionManager _permissionManager; - public QueryHandler(IApplicationDbContext context, IMapper mapper, ICurrentUserService currentUserService, IPermissionManager permissionManager) + public QueryHandler( + IApplicationDbContext context, + IMapper mapper, + IPermissionManager permissionManager) { _context = context; _mapper = mapper; - _currentUserService = currentUserService; _permissionManager = permissionManager; } + public async Task Handle(Query request, CancellationToken cancellationToken) { var document = await _context.Documents @@ -42,31 +48,24 @@ public async Task Handle(Query request, CancellationToken cancellat throw new KeyNotFoundException("Document does not exist."); } - var performingUser = _currentUserService.GetCurrentUser(); - - if (performingUser.Role.Equals(IdentityData.Roles.Admin)) - { - return _mapper.Map(document); - } - - if (performingUser.Role.Equals(IdentityData.Roles.Staff)) - { - var departmentIdOfStaff = _currentUserService.GetCurrentDepartmentForStaff(); - - if (departmentIdOfStaff!.Value != document.Department!.Id) - { - throw new ConflictException("You don't have permission to view this document."); - } - return _mapper.Map(document); - } - - var isGranted = _permissionManager.IsGranted(document.Id, DocumentOperation.Read, performingUser.Id); - if (!isGranted) + if (ViolateConstraints(request.CurrentUser, document)) { throw new UnauthorizedAccessException("You don't have permission to view this document."); } return _mapper.Map(document); } + + private bool ViolateConstraints(User user, Document document) + => IsStaffAndNotInSameDepartment(user, document) + || IsEmployeeAndDoesNotHasReadPermission(user, document); + + private static bool IsStaffAndNotInSameDepartment(User user, Document document) + => user.Role.IsStaff() + && user.Department!.Id != document.Department!.Id; + + private bool IsEmployeeAndDoesNotHasReadPermission(User user, Document document) + => user.Role.IsEmployee() + && !_permissionManager.IsGranted(document.Id, DocumentOperation.Read, user.Id); } } \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetDocumentsOfUserPaginated.cs b/src/Application/Documents/Queries/GetDocumentsOfUserPaginated.cs index 8bd86463..d77dbf8d 100644 --- a/src/Application/Documents/Queries/GetDocumentsOfUserPaginated.cs +++ b/src/Application/Documents/Queries/GetDocumentsOfUserPaginated.cs @@ -49,7 +49,7 @@ public async Task> Handle(Query request, Cancellation .Where(x => x.Importer!.Id.Equals(request.UserId) && !x.IsPrivate); return await documents - .ListPaginateWithFilterAsync( + .ListPaginateWithSortAsync( request.Page, request.Size, request.SortBy, diff --git a/src/Application/Documents/Queries/GetSelfDocumentsPaginated.cs b/src/Application/Documents/Queries/GetSelfDocumentsPaginated.cs index b531bd1a..7c4d67cb 100644 --- a/src/Application/Documents/Queries/GetSelfDocumentsPaginated.cs +++ b/src/Application/Documents/Queries/GetSelfDocumentsPaginated.cs @@ -46,7 +46,7 @@ public async Task> Handle(Query request, Cancellation } return await documents - .ListPaginateWithFilterAsync( + .ListPaginateWithSortAsync( request.Page, request.Size, request.SortBy, diff --git a/src/Application/Folders/Queries/GetAllFoldersPaginated.cs b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs index a03f8a28..3161b4c7 100644 --- a/src/Application/Folders/Queries/GetAllFoldersPaginated.cs +++ b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs @@ -118,7 +118,7 @@ public async Task> Handle(Query request, CancellationTo } return await folders - .ListPaginateWithFilterAsync( + .ListPaginateWithSortAsync( request.Page, request.Size, request.SortBy, diff --git a/src/Application/Lockers/Queries/GetAllLockersPaginated.cs b/src/Application/Lockers/Queries/GetAllLockersPaginated.cs index db207f57..ad6b845c 100644 --- a/src/Application/Lockers/Queries/GetAllLockersPaginated.cs +++ b/src/Application/Lockers/Queries/GetAllLockersPaginated.cs @@ -76,7 +76,7 @@ public async Task> Handle(Query request, CancellationTo } return await lockers - .ListPaginateWithFilterAsync( + .ListPaginateWithSortAsync( request.Page, request.Size, request.SortBy, diff --git a/src/Application/Rooms/Commands/UpdateRoom.cs b/src/Application/Rooms/Commands/UpdateRoom.cs index e215c25f..aa14ad82 100644 --- a/src/Application/Rooms/Commands/UpdateRoom.cs +++ b/src/Application/Rooms/Commands/UpdateRoom.cs @@ -34,7 +34,7 @@ public Validator() } public record Command : IRequest { - public User CurrentUser { get; init; } + public User CurrentUser { get; init; } = null!; public Guid RoomId { get; init; } public string Name { get; init; } = null!; public string? Description { get; init; } diff --git a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs index f87b6778..b53043c5 100644 --- a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs +++ b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs @@ -47,7 +47,7 @@ public async Task> Handle(Query request, CancellationToke } return await rooms - .ListPaginateWithFilterAsync( + .ListPaginateWithSortAsync( request.Page, request.Size, request.SortBy, diff --git a/src/Application/Rooms/Queries/GetRoomByStaffId.cs b/src/Application/Rooms/Queries/GetRoomByStaffId.cs new file mode 100644 index 00000000..12bf6d1c --- /dev/null +++ b/src/Application/Rooms/Queries/GetRoomByStaffId.cs @@ -0,0 +1,43 @@ +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Rooms.Queries; + +public class GetRoomByStaffId +{ + public record Query : IRequest + { + public Guid StaffId { get; init; } + } + + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var room = await _context.Rooms + .Include(x => x.Staff) + .ThenInclude(y => y!.User) + .Include(x => x.Department) + .FirstOrDefaultAsync(x => x.Staff!.Id == request.StaffId, cancellationToken); + + if (room is null) + { + throw new KeyNotFoundException("Room does not exists."); + } + + return _mapper.Map(room); + } + } +} \ No newline at end of file diff --git a/src/Application/Staffs/Commands/AddStaff.cs b/src/Application/Staffs/Commands/AddStaff.cs index 343fb367..01b3ad50 100644 --- a/src/Application/Staffs/Commands/AddStaff.cs +++ b/src/Application/Staffs/Commands/AddStaff.cs @@ -3,6 +3,7 @@ using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; using Domain.Entities.Logging; using Domain.Entities.Physical; using MediatR; @@ -15,8 +16,8 @@ public class AddStaff { public record Command : IRequest { - public Guid PerformingUserId { get; init; } - public Guid UserId { get; init; } + public User CurrentUser { get; init; } = null!; + public Guid StaffId { get; init; } public Guid? RoomId { get; init; } } @@ -33,74 +34,42 @@ public CommandHandler(IApplicationDbContext context, IMapper mapper) public async Task Handle(Command request, CancellationToken cancellationToken) { - var user = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.UserId, cancellationToken); - if (user is null) + var staff = await _context.Staffs + .FirstOrDefaultAsync(x => x.Id == request.StaffId, cancellationToken); + + if (staff is null) { - throw new KeyNotFoundException("User does not exist."); + throw new KeyNotFoundException("Staff does not exist."); } - + var room = await _context.Rooms .Include(x => x.Staff) .FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); - + if (room is null) { throw new KeyNotFoundException("Room does not exist."); } - + if (room.Staff is not null) { throw new ConflictException("Room already has a staff."); } - - var existedStaff = await _context.Staffs - .Include(x => x.Room) - .Include(x => x.User) - .FirstOrDefaultAsync(x => x.Id == user.Id, cancellationToken); - var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); - if (existedStaff is not null) - { - if (existedStaff.Room is not null) - { - throw new ConflictException("This user is already a staff."); - } - - existedStaff.Room = room; - var log = new UserLog() - { - User = performingUser!, - UserId = performingUser!.Id, - Object = user, - Time = LocalDateTime.FromDateTime(DateTime.Now), - Action = UserLogMessages.Staff.AddStaff(room.Id.ToString()), - }; - var result = _context.Staffs.Update(existedStaff); - await _context.UserLogs.AddAsync(log, cancellationToken); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } - else + + var log = new UserLog() { - var staff = new Staff - { - Id = user.Id, - User = user, - Room = room, - }; - var log = new UserLog() - { - User = performingUser!, - UserId = performingUser!.Id, - Object = user, - Time = LocalDateTime.FromDateTime(DateTime.Now), - Action = UserLogMessages.Staff.AddStaff(room.Id.ToString()), - }; + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + Object = staff.User, + Time = LocalDateTime.FromDateTime(DateTime.Now), + Action = UserLogMessages.Staff.AssignStaff(room.Id.ToString()), + }; - var result = await _context.Staffs.AddAsync(staff, cancellationToken); - await _context.UserLogs.AddAsync(log, cancellationToken); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } + var result = await _context.Staffs.AddAsync(staff, cancellationToken); + await _context.UserLogs.AddAsync(log, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } } } \ No newline at end of file diff --git a/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs b/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs index 197acc8e..54a9ed99 100644 --- a/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs +++ b/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs @@ -3,6 +3,7 @@ using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; using Domain.Entities.Logging; using MediatR; using Microsoft.EntityFrameworkCore; @@ -14,7 +15,7 @@ public class RemoveStaffFromRoom { public record Command : IRequest { - public Guid PerformingUserId { get; init; } + public User CurrentUser { get; init; } = null!; public Guid StaffId { get; init; } } @@ -22,11 +23,13 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -46,16 +49,18 @@ public async Task Handle(Command request, CancellationToken cancellati throw new ConflictException("Staff is not assigned to a room."); } + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + staff.Room.Staff = null; _context.Rooms.Update(staff.Room!); staff.Room = null; - var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); + var log = new UserLog() { - User = performingUser!, - UserId = performingUser!.Id, + User = request.CurrentUser, + UserId = request.CurrentUser.Id, Object = staff.User, - Time = LocalDateTime.FromDateTime(DateTime.Now), + Time = localDateTimeNow, Action = UserLogMessages.Staff.RemoveFromRoom, }; var result = _context.Staffs.Update(staff); diff --git a/src/Application/Staffs/EventHandlers/StaffCreatedEventHandler.cs b/src/Application/Staffs/EventHandlers/StaffCreatedEventHandler.cs new file mode 100644 index 00000000..2a8baf29 --- /dev/null +++ b/src/Application/Staffs/EventHandlers/StaffCreatedEventHandler.cs @@ -0,0 +1,28 @@ +using Application.Common.Interfaces; +using Domain.Entities.Physical; +using Domain.Events; +using MediatR; + +namespace Application.Staffs.EventHandlers; + +public class StaffCreatedEventHandler : INotificationHandler +{ + private readonly IApplicationDbContext _context; + + public StaffCreatedEventHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task Handle(StaffCreatedEvent notification, CancellationToken cancellationToken) + { + var staff = new Staff() + { + Id = notification.Staff.Id, + User = notification.Staff, + }; + + await _context.Staffs.AddAsync(staff, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/src/Application/Staffs/Queries/GetAllStaffsPaginated.cs b/src/Application/Staffs/Queries/GetAllStaffsPaginated.cs index 76a62584..c0987de1 100644 --- a/src/Application/Staffs/Queries/GetAllStaffsPaginated.cs +++ b/src/Application/Staffs/Queries/GetAllStaffsPaginated.cs @@ -47,7 +47,7 @@ public async Task> Handle(Query request, CancellationTok } return await staffs - .ListPaginateWithFilterAsync( + .ListPaginateWithSortAsync( request.Page, request.Size, request.SortBy, diff --git a/src/Application/Staffs/Queries/GetStaffByRoom.cs b/src/Application/Staffs/Queries/GetStaffByRoomId.cs similarity index 97% rename from src/Application/Staffs/Queries/GetStaffByRoom.cs rename to src/Application/Staffs/Queries/GetStaffByRoomId.cs index 60db1c83..4b75b7f1 100644 --- a/src/Application/Staffs/Queries/GetStaffByRoom.cs +++ b/src/Application/Staffs/Queries/GetStaffByRoomId.cs @@ -6,7 +6,7 @@ namespace Application.Staffs.Queries; -public class GetStaffByRoom +public class GetStaffByRoomId { public record Query : IRequest { diff --git a/src/Application/Users/Commands/AddUser.cs b/src/Application/Users/Commands/AddUser.cs index 6094c0ff..2437ea20 100644 --- a/src/Application/Users/Commands/AddUser.cs +++ b/src/Application/Users/Commands/AddUser.cs @@ -1,4 +1,5 @@ using Application.Common.Exceptions; +using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Messages; using Application.Helpers; @@ -55,7 +56,7 @@ private static bool BeNotAdmin(string role) public record Command : IRequest { - public User PerformingUser { get; init; } = null!; + public User CurrentUser { get; init; } = null!; public string Username { get; init; } = null!; public string Email { get; init; } = null!; public string? FirstName { get; init; } @@ -82,6 +83,11 @@ public AddUserCommandHandler(IApplicationDbContext context, IMapper mapper, ISec public async Task Handle(Command request, CancellationToken cancellationToken) { + if (request.Role.IsAdmin()) + { + throw new UnauthorizedAccessException(); + } + var user = await _context.Users.FirstOrDefaultAsync( x => x.Username.Equals(request.Username) || x.Email.Equals(request.Email), cancellationToken); @@ -117,18 +123,22 @@ public async Task Handle(Command request, CancellationToken cancellatio IsActive = true, IsActivated = false, Created = localDateTimeNow, - CreatedBy = request.PerformingUser.Id, + CreatedBy = request.CurrentUser.Id, }; var log = new UserLog() { - User = request.PerformingUser, - UserId = request.PerformingUser.Id, + User = request.CurrentUser, + UserId = request.CurrentUser.Id, Object = entity, Time = localDateTimeNow, - Action = UserLogMessages.Add, + Action = UserLogMessages.Add(entity.Role), }; entity.AddDomainEvent(new UserCreatedEvent(entity, password)); + if (request.Role.IsStaff()) + { + entity.AddDomainEvent(new StaffCreatedEvent(entity, request.CurrentUser)); + } var result = await _context.Users.AddAsync(entity, cancellationToken); await _context.UserLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); diff --git a/src/Application/Users/Queries/GetAllUsersPaginated.cs b/src/Application/Users/Queries/GetAllUsersPaginated.cs index d72dad46..f9a0a167 100644 --- a/src/Application/Users/Queries/GetAllUsersPaginated.cs +++ b/src/Application/Users/Queries/GetAllUsersPaginated.cs @@ -57,7 +57,7 @@ public async Task> Handle(Query request, CancellationToke } return await users - .ListPaginateWithFilterAsync( + .ListPaginateWithSortAsync( request.Page, request.Size, request.SortBy, diff --git a/src/Domain/Entities/Department.cs b/src/Domain/Entities/Department.cs index b43bbfe7..6a353b38 100644 --- a/src/Domain/Entities/Department.cs +++ b/src/Domain/Entities/Department.cs @@ -6,5 +6,6 @@ namespace Domain.Entities; public class Department : BaseEntity { public string Name { get; set; } = null!; - public Room? Room { get; set; } + + public ICollection Rooms { get; set; } = new List(); } \ No newline at end of file diff --git a/src/Domain/Entities/Physical/Document.cs b/src/Domain/Entities/Physical/Document.cs index c5dd876a..0de04af1 100644 --- a/src/Domain/Entities/Physical/Document.cs +++ b/src/Domain/Entities/Physical/Document.cs @@ -9,12 +9,13 @@ public class Document : BaseAuditableEntity public string Title { get; set; } = null!; public string? Description { get; set; } public string DocumentType { get; set; } = null!; + public Guid? ImporterId { get; set; } public Department? Department { get; set; } - public User? Importer { get; set; } public Folder? Folder { get; set; } public DocumentStatus Status { get; set; } public Guid? EntryId { get; set; } public bool IsPrivate { get; set; } + public User? Importer { get; set; } public virtual Entry? Entry { get; set; } } \ No newline at end of file diff --git a/src/Domain/Events/StaffCreatedEvent.cs b/src/Domain/Events/StaffCreatedEvent.cs new file mode 100644 index 00000000..2b6adfb1 --- /dev/null +++ b/src/Domain/Events/StaffCreatedEvent.cs @@ -0,0 +1,16 @@ +using Domain.Common; +using Domain.Entities; + +namespace Domain.Events; + +public class StaffCreatedEvent : BaseEvent +{ + public StaffCreatedEvent(User staff, User currentUser) + { + Staff = staff; + CurrentUser = currentUser; + } + + public User Staff { get; } + public User CurrentUser { get; } +} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs b/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs index 287224e1..3bb00c66 100644 --- a/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs +++ b/src/Infrastructure/Persistence/Configurations/DocumentConfiguration.cs @@ -36,7 +36,7 @@ public void Configure(EntityTypeBuilder builder) builder.HasOne(x => x.Importer) .WithMany() - .HasForeignKey("ImporterId") + .HasForeignKey(x => x.ImporterId) .IsRequired(false); builder.Property(x => x.Status) diff --git a/src/Infrastructure/Persistence/Configurations/RoomConfiguration.cs b/src/Infrastructure/Persistence/Configurations/RoomConfiguration.cs index 2ad65961..98b4fea3 100644 --- a/src/Infrastructure/Persistence/Configurations/RoomConfiguration.cs +++ b/src/Infrastructure/Persistence/Configurations/RoomConfiguration.cs @@ -1,4 +1,3 @@ -using Domain.Entities; using Domain.Entities.Physical; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -27,10 +26,16 @@ public void Configure(EntityTypeBuilder builder) builder.Property(x => x.NumberOfLockers) .IsRequired(); + builder.Property(x => x.Capacity) .IsRequired(); builder.Property(x => x.IsAvailable) .IsRequired(); + + builder.HasOne(x => x.Department) + .WithMany(x => x.Rooms) + .HasForeignKey(x => x.DepartmentId) + .IsRequired(); } } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index 98624b11..7896db84 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -549,8 +549,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasAlternateKey("Name"); - b.HasIndex("DepartmentId") - .IsUnique(); + b.HasIndex("DepartmentId"); b.ToTable("Rooms"); }); @@ -921,8 +920,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Domain.Entities.Physical.Room", b => { b.HasOne("Domain.Entities.Department", "Department") - .WithOne("Room") - .HasForeignKey("Domain.Entities.Physical.Room", "DepartmentId") + .WithMany("Rooms") + .HasForeignKey("DepartmentId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -994,7 +993,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Domain.Entities.Department", b => { - b.Navigation("Room"); + b.Navigation("Rooms"); }); modelBuilder.Entity("Domain.Entities.Physical.Folder", b => diff --git a/tests/Application.Tests.Integration/Staffs/Queries/GetAllStaffsPaginatedTests.cs b/tests/Application.Tests.Integration/Staffs/Queries/GetAllStaffsPaginatedTests.cs index 4c0547ef..8033323b 100644 --- a/tests/Application.Tests.Integration/Staffs/Queries/GetAllStaffsPaginatedTests.cs +++ b/tests/Application.Tests.Integration/Staffs/Queries/GetAllStaffsPaginatedTests.cs @@ -66,8 +66,7 @@ public async Task ShouldReturnASpecificStaff() // Assert result.TotalCount.Should().Be(1); result.Items.First().Should() - .BeEquivalentTo(_mapper.Map(staff1), - config => config.Excluding(x => x.User.Created)); + .BeEquivalentTo(_mapper.Map(staff1)); // Cleanup Remove(staff2); diff --git a/tests/Application.Tests.Integration/Staffs/Queries/GetStaffByRoomTests.cs b/tests/Application.Tests.Integration/Staffs/Queries/GetStaffByRoomTests.cs index 123e2542..b0da147c 100644 --- a/tests/Application.Tests.Integration/Staffs/Queries/GetStaffByRoomTests.cs +++ b/tests/Application.Tests.Integration/Staffs/Queries/GetStaffByRoomTests.cs @@ -31,7 +31,7 @@ public async Task ShouldReturnStaff_WhenRoomHaveStaff() var staff = CreateStaff(user, room); await AddAsync(staff); - var query = new GetStaffByRoom.Query() + var query = new GetStaffByRoomId.Query() { RoomId = room.Id }; @@ -57,7 +57,7 @@ public async Task ShouldReturnStaff_WhenRoomHaveStaff() public async Task ShouldThrowKeyNotFoundException_WhenRoomDoesNotExist() { // Arrange - var query = new GetStaffByRoom.Query() + var query = new GetStaffByRoomId.Query() { RoomId = Guid.NewGuid() }; @@ -78,7 +78,7 @@ public async Task ShouldThrowKeyNotFoundException_WhenRoomDoesNotHaveStaff() var room = CreateRoom(department); await AddAsync(room); - var query = new GetStaffByRoom.Query() + var query = new GetStaffByRoomId.Query() { RoomId = room.Id }; From 4518f5b19ce660287c6a880004ecde090a77d241 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Sun, 18 Jun 2023 10:45:58 +0700 Subject: [PATCH 44/56] fuck me --- src/Api/Controllers/DocumentsController.cs | 120 +++++++----------- src/Api/Controllers/FoldersController.cs | 16 ++- src/Api/Controllers/LockersController.cs | 4 +- ...est.cs => ApproveOrRejectImportRequest.cs} | 3 +- ...entsForEmployeePaginatedQueryParameters.cs | 1 + ...GetAllDocumentsPaginatedQueryParameters.cs | 1 + .../Documents/RequestImportDocumentRequest.cs | 1 + .../Documents/UpdateDocumentRequest.cs | 1 + .../GetAllRoomsPaginatedQueryParameters.cs | 1 + src/Api/Controllers/RoomsController.cs | 5 +- src/Api/Services/CurrentUserService.cs | 15 +-- .../Common/Messages/DocumentLogMessages.cs | 4 +- .../Models/Dtos/Physical/DocumentDto.cs | 1 + .../Documents/Commands/ApproveDocument.cs | 32 +++-- .../Documents/Commands/DeleteDocument.cs | 30 ++++- .../Documents/Commands/ImportDocument.cs | 30 +++-- .../Commands/RequestImportDocument.cs | 41 +++--- .../Documents/Commands/UpdateDocument.cs | 45 ++++++- .../GetAllDocumentsForEmployeePaginated.cs | 29 +++-- .../Queries/GetAllDocumentsPaginated.cs | 6 + .../Documents/Queries/GetDocumentReason.cs | 10 +- src/Application/Folders/Commands/AddFolder.cs | 10 +- .../Folders/Commands/RemoveFolder.cs | 8 +- .../Folders/Commands/UpdateFolder.cs | 7 +- .../Folders/Queries/GetAllFoldersPaginated.cs | 28 +--- .../Folders/Queries/GetFolderById.cs | 10 +- .../Lockers/Queries/GetLockerById.cs | 14 +- .../Rooms/Queries/GetAllRoomsPaginated.cs | 11 ++ src/Domain/Entities/Physical/ImportRequest.cs | 9 ++ 29 files changed, 288 insertions(+), 205 deletions(-) rename src/Api/Controllers/Payload/Requests/Documents/{ApproveImportRequest.cs => ApproveOrRejectImportRequest.cs} (53%) create mode 100644 src/Domain/Entities/Physical/ImportRequest.cs diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index 2249208f..14f358c8 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -1,5 +1,6 @@ using Api.Controllers.Payload.Requests; using Api.Controllers.Payload.Requests.Documents; +using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Models; using Application.Common.Models.Dtos; @@ -72,6 +73,7 @@ public async Task>>> GetAllPagina SortOrder = queryParameters.SortOrder, IsPrivate = queryParameters.IsPrivate, DocumentStatus = queryParameters.DocumentStatus, + Role = queryParameters.UserRole, }; var result = await Mediator.Send(query); return Ok(Result>.Succeed(result)); @@ -83,7 +85,7 @@ public async Task>>> GetAllPagina /// Get all documents query parameters /// A paginated list of DocumentDto [RequiresRole(IdentityData.Roles.Employee)] - [HttpGet] + [HttpGet("employees")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -91,10 +93,12 @@ public async Task>>> GetAllPagina public async Task>>> GetAllForEmployeePaginated( [FromQuery] GetAllDocumentsForEmployeePaginatedQueryParameters queryParameters) { - var currentUser = _currentUserService.GetCurrentUser(); + var currentUserId = _currentUserService.GetId(); + var currentUserDepartmentId = _currentUserService.GetDepartmentId(); var query = new GetAllDocumentsForEmployeePaginated.Query() { - CurrentUser = currentUser, + CurrentUserId = currentUserId, + CurrentUserDepartmentId = currentUserDepartmentId, SearchTerm = queryParameters.SearchTerm, Page = queryParameters.Page, Size = queryParameters.Size, @@ -133,12 +137,17 @@ public async Task>>> GetAllDocumentTypes [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Import([FromBody] ImportDocumentRequest request) + public async Task>> Import( + [FromBody] ImportDocumentRequest request) { - var performingUserId = _currentUserService.GetId(); + var currentUser = _currentUserService.GetCurrentUser(); + if (currentUser.Department is null) + { + return Forbid(); + } var command = new ImportDocument.Command() { - PerformingUserId = performingUserId, + CurrentUser = currentUser, Title = request.Title, Description = request.Description, DocumentType = request.DocumentType, @@ -155,22 +164,24 @@ public async Task>> Import([FromBody] ImportDoc /// Import document request details /// A DocumentDto of the imported document [RequiresRole(IdentityData.Roles.Employee)] - [HttpPost("request")] + [HttpPost("import-requests")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> RequestImport([FromBody] RequestImportDocumentRequest request) + public async Task>> RequestImport( + [FromBody] RequestImportDocumentRequest request) { - var performingUserId = _currentUserService.GetId(); + var currentUser = _currentUserService.GetCurrentUser(); var command = new RequestImportDocument.Command() { Title = request.Title, Description = request.Description, DocumentType = request.DocumentType, IsPrivate = request.IsPrivate, - IssuerId = performingUserId, + Issuer = currentUser, + RoomId = request.RoomId, }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); @@ -207,7 +218,7 @@ public async Task>> Checkin( /// Id of the document to be updated /// Update document details /// A DocumentDto of the updated document - [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff, IdentityData.Roles.Employee)] [HttpPut("{documentId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -217,12 +228,19 @@ public async Task>> Update( [FromRoute] Guid documentId, [FromBody] UpdateDocumentRequest request) { + var currentUser = _currentUserService.GetCurrentUser(); + if (currentUser.Department is null) + { + return Forbid(); + } var query = new UpdateDocument.Command() { + CurrentUser = currentUser, DocumentId = documentId, Title = request.Title, Description = request.Description, - DocumentType = request.DocumentType + DocumentType = request.DocumentType, + IsPrivate = request.IsPrivate, }; var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); @@ -232,8 +250,8 @@ public async Task>> Update( /// Delete a document /// /// Id of the document to be deleted - /// A DocumentDto of the deleted document - + /// A DocumentDto of the deleted document + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpDelete("{documentId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -241,40 +259,21 @@ public async Task>> Update( public async Task>> Delete( [FromRoute] Guid documentId) { + var currentUser = _currentUserService.GetCurrentUser(); + if (currentUser.Role.IsStaff() + && currentUser.Department is null) + { + return Forbid(); + } var query = new DeleteDocument.Command() { + CurrentUser = currentUser, DocumentId = documentId, }; var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); } - /// - /// Get all documents of a user. - /// - /// Id of the user - /// Query parameters - /// A list of DocumentDtos of the user. - [RequiresRole(IdentityData.Roles.Employee)] - [HttpGet("user/{userId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetDocumentsOfUserPaginated([FromRoute] Guid userId, - [FromQuery] GetDocumentsOfUserPaginatedQueryParameters queryParameters) - { - var query = new GetDocumentsOfUserPaginated.Query() - { - UserId = userId, - Page = queryParameters.Page, - Size = queryParameters.Size, - SortBy = queryParameters.SortBy, - SortOrder = queryParameters.SortOrder, - }; - var result = await Mediator.Send(query); - return Ok(Result>.Succeed(result)); - } - /// /// Approve a document request /// @@ -282,46 +281,21 @@ public async Task>> GetDocumentsOfUserPaginated /// /// A DocumentDto of the approved document [RequiresRole(IdentityData.Roles.Staff)] - [HttpPost("approve/{documentId:guid}")] + [HttpPut("import-requests/{documentId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> Approve( + public async Task>> ApproveOrReject( [FromRoute] Guid documentId, - [FromBody] ApproveImportRequest request) + [FromBody] ApproveOrRejectImportRequest request) { - var performingUserId = _currentUserService.GetId(); + var currentUser = _currentUserService.GetCurrentUser(); var query = new ApproveDocument.Command() { - PerformingUserId = performingUserId, - DocumentId = documentId, - Reason = request.Reason, - }; - var result = await Mediator.Send(query); - return Ok(Result.Succeed(result)); - } - - /// - /// Reject a document request - /// - /// Id of the document to be rejected - /// - /// A DocumentDto of the rejected document - [RequiresRole(IdentityData.Roles.Staff)] - [HttpPost("reject/{documentId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> Reject( - [FromRoute] Guid documentId, - [FromBody] RejectImportRequest request) - { - var performingUserId = _currentUserService.GetId(); - var query = new RejectDocument.Command() - { - PerformingUserId = performingUserId, + CurrentUser = currentUser, DocumentId = documentId, Reason = request.Reason, + Decision = request.Decision, }; var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); @@ -333,15 +307,17 @@ public async Task>> Reject( /// Id of the document to be rejected /// A DocumentDto of the rejected document [RequiresRole(IdentityData.Roles.Staff, IdentityData.Roles.Employee)] - [HttpPost("reason/{documentId:guid}")] + [HttpPost("import-requests/{documentId:guid}/reasons")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> Reason( [FromRoute] Guid documentId) { + var currentUser = _currentUserService.GetCurrentUser(); var query = new GetDocumentReason.Query() { + CurrentUser = currentUser, DocumentId = documentId, Type = RequestType.Import, }; diff --git a/src/Api/Controllers/FoldersController.cs b/src/Api/Controllers/FoldersController.cs index 329f5928..b4460ec3 100644 --- a/src/Api/Controllers/FoldersController.cs +++ b/src/Api/Controllers/FoldersController.cs @@ -34,11 +34,11 @@ public FoldersController(ICurrentUserService currentUserService) public async Task>> GetById([FromRoute] Guid folderId) { var currentUserRole = _currentUserService.GetRole(); - var currentUserDepartmentId = _currentUserService.GetDepartmentId(); + var staffRoomId = _currentUserService.GetCurrentRoomForStaff(); var query = new GetFolderById.Query() { CurrentUserRole = currentUserRole, - CurrentUserDepartmentId = currentUserDepartmentId, + CurrentStaffRoomId = staffRoomId, FolderId = folderId, }; var result = await Mediator.Send(query); @@ -58,11 +58,11 @@ public async Task>>> GetAllPaginate [FromQuery] GetAllFoldersPaginatedQueryParameters queryParameters) { var currentUserRole = _currentUserService.GetRole(); - var currentUserDepartmentId = _currentUserService.GetDepartmentId(); + var staffRoomId = _currentUserService.GetCurrentRoomForStaff(); var query = new GetAllFoldersPaginated.Query() { CurrentUserRole = currentUserRole, - CurrentUserDepartmentId = currentUserDepartmentId, + CurrentStaffRoomId = staffRoomId, RoomId = queryParameters.RoomId, LockerId = queryParameters.LockerId, SearchTerm = queryParameters.SearchTerm, @@ -90,9 +90,11 @@ public async Task>>> GetAllPaginate public async Task>> AddFolder([FromBody] AddFolderRequest request) { var currentUser = _currentUserService.GetCurrentUser(); + var staffRoomId = _currentUserService.GetCurrentRoomForStaff(); var command = new AddFolder.Command() { CurrentUser = currentUser, + CurrentStaffRoomId = staffRoomId, Name = request.Name, Description = request.Description, Capacity = request.Capacity, @@ -117,11 +119,11 @@ public async Task>> AddFolder([FromBody] AddFolde public async Task>> RemoveFolder([FromRoute] Guid folderId) { var currentUserRole = _currentUserService.GetRole(); - var currentUserDepartmentId = _currentUserService.GetDepartmentId(); + var staffRoomId = _currentUserService.GetCurrentRoomForStaff(); var command = new RemoveFolder.Command() { CurrentUserRole = currentUserRole, - CurrentUserDepartmentId = currentUserDepartmentId, + CurrentStaffRoomId = staffRoomId, FolderId = folderId, }; var result = await Mediator.Send(command); @@ -142,9 +144,11 @@ public async Task>> RemoveFolder([FromRoute] Guid public async Task>> Update([FromRoute] Guid folderId, [FromBody] UpdateFolderRequest request) { var currentUser = _currentUserService.GetCurrentUser(); + var staffRoomId = _currentUserService.GetCurrentRoomForStaff(); var command = new UpdateFolder.Command() { CurrentUser = currentUser, + CurrentStaffRoomId = staffRoomId, FolderId = folderId, Name = request.Name, Description = request.Description, diff --git a/src/Api/Controllers/LockersController.cs b/src/Api/Controllers/LockersController.cs index df798faf..bc9310eb 100644 --- a/src/Api/Controllers/LockersController.cs +++ b/src/Api/Controllers/LockersController.cs @@ -35,11 +35,11 @@ public async Task>> GetById( [FromRoute] Guid lockerId) { var currentUserRole = _currentUserService.GetRole(); - var currentUserDepartmentId = _currentUserService.GetDepartmentId(); + var staffRoomId = _currentUserService.GetCurrentRoomForStaff(); var query = new GetLockerById.Query() { CurrentUserRole = currentUserRole, - CurrentUserDepartmentId = currentUserDepartmentId, + CurrentStaffRoomId = staffRoomId, LockerId = lockerId, }; var result = await Mediator.Send(query); diff --git a/src/Api/Controllers/Payload/Requests/Documents/ApproveImportRequest.cs b/src/Api/Controllers/Payload/Requests/Documents/ApproveOrRejectImportRequest.cs similarity index 53% rename from src/Api/Controllers/Payload/Requests/Documents/ApproveImportRequest.cs rename to src/Api/Controllers/Payload/Requests/Documents/ApproveOrRejectImportRequest.cs index d617bb8b..83e90fe3 100644 --- a/src/Api/Controllers/Payload/Requests/Documents/ApproveImportRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Documents/ApproveOrRejectImportRequest.cs @@ -1,6 +1,7 @@ namespace Api.Controllers.Payload.Requests.Documents; -public class ApproveImportRequest +public class ApproveOrRejectImportRequest { + public string Decision { get; set; } = null!; public string Reason { get; set; } = null!; } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForEmployeePaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForEmployeePaginatedQueryParameters.cs index 88b9ff9e..9b6be2b0 100644 --- a/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForEmployeePaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForEmployeePaginatedQueryParameters.cs @@ -2,6 +2,7 @@ namespace Api.Controllers.Payload.Requests.Documents; public class GetAllDocumentsForEmployeePaginatedQueryParameters : PaginatedQueryParameters { + public Guid? UserId { get; set; } public string? SearchTerm { get; set; } public string? DocumentStatus { get; set; } public bool IsPrivate { get; set; } diff --git a/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsPaginatedQueryParameters.cs index fb226c4f..94cca9b8 100644 --- a/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsPaginatedQueryParameters.cs @@ -23,5 +23,6 @@ public class GetAllDocumentsPaginatedQueryParameters : PaginatedQueryParameters /// public string? SearchTerm { get; set; } public string? DocumentStatus { get; set; } + public string? UserRole { get; set; } public bool? IsPrivate { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/RequestImportDocumentRequest.cs b/src/Api/Controllers/Payload/Requests/Documents/RequestImportDocumentRequest.cs index ecdc79a5..4f2e99d6 100644 --- a/src/Api/Controllers/Payload/Requests/Documents/RequestImportDocumentRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Documents/RequestImportDocumentRequest.cs @@ -17,5 +17,6 @@ public class RequestImportDocumentRequest /// Document type of the document to be imported /// public string DocumentType { get; set; } = null!; + public Guid RoomId { get; set; } public bool IsPrivate { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/UpdateDocumentRequest.cs b/src/Api/Controllers/Payload/Requests/Documents/UpdateDocumentRequest.cs index a31cec88..97626d8a 100644 --- a/src/Api/Controllers/Payload/Requests/Documents/UpdateDocumentRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Documents/UpdateDocumentRequest.cs @@ -17,4 +17,5 @@ public class UpdateDocumentRequest /// New document type of the document to be updated /// public string DocumentType { get; set; } = null!; + public bool IsPrivate { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs index 390ae4be..3bf5e63a 100644 --- a/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Rooms/GetAllRoomsPaginatedQueryParameters.cs @@ -9,4 +9,5 @@ public class GetAllRoomsPaginatedQueryParameters : PaginatedQueryParameters /// Search term /// public string? SearchTerm { get; set; } + public Guid? DepartmentId { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/RoomsController.cs b/src/Api/Controllers/RoomsController.cs index 02771afd..0daf20f2 100644 --- a/src/Api/Controllers/RoomsController.cs +++ b/src/Api/Controllers/RoomsController.cs @@ -53,15 +53,18 @@ public async Task>> GetById( /// /// Get all rooms paginated details /// A paginated list of rooms - [RequiresRole(IdentityData.Roles.Admin)] + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff, IdentityData.Roles.Employee)] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task>>> GetAllPaginated( [FromQuery] GetAllRoomsPaginatedQueryParameters queryParameters) { + var currentUser = _currentUserService.GetCurrentUser(); var query = new GetAllRoomsPaginated.Query() { + CurrentUser = currentUser, + DepartmentId = queryParameters.DepartmentId, SearchTerm = queryParameters.SearchTerm, Page = queryParameters.Page, Size = queryParameters.Size, diff --git a/src/Api/Services/CurrentUserService.cs b/src/Api/Services/CurrentUserService.cs index fd8d2b2f..6eb40f38 100644 --- a/src/Api/Services/CurrentUserService.cs +++ b/src/Api/Services/CurrentUserService.cs @@ -73,9 +73,9 @@ public User GetCurrentUser() public Guid? GetCurrentRoomForStaff() { - var userName = _httpContextAccessor.HttpContext!.User.Claims - .FirstOrDefault(x => x.Type.Equals(JwtRegisteredClaimNames.Sub))!.Value; - if (userName is null) + var userIdString = _httpContextAccessor.HttpContext!.User.Claims + .FirstOrDefault(x => x.Type.Equals(JwtRegisteredClaimNames.NameId)); + if (userIdString is null || !Guid.TryParse(userIdString.Value, out var userId)) { throw new UnauthorizedAccessException(); } @@ -83,14 +83,9 @@ public User GetCurrentUser() var staff = _context.Staffs .Include(x => x.User) .Include(x => x.Room) - .FirstOrDefault(x => x.User.Username.Equals(userName)); - - if (staff is null) - { - throw new UnauthorizedAccessException(); - } + .FirstOrDefault(x => x.Id == userId); - return staff.Room!.Id; + return staff?.Room?.Id; } public Guid? GetCurrentDepartmentForStaff() diff --git a/src/Application/Common/Messages/DocumentLogMessages.cs b/src/Application/Common/Messages/DocumentLogMessages.cs index 835c3d48..0c889d28 100644 --- a/src/Application/Common/Messages/DocumentLogMessages.cs +++ b/src/Application/Common/Messages/DocumentLogMessages.cs @@ -7,7 +7,7 @@ public static class Import public const string NewImport = "Imported new document"; public const string NewImportRequest = "Created new import request"; public const string Checkin = "Checked in document"; - public const string Approve = "Approved import request"; + public const string Approve = "Document is approved to be imported"; public const string Reject = "Rejected import request"; public const string Assign = "Assigned to a folder"; } @@ -21,4 +21,6 @@ public static class Borrow public const string Return = "Returned borrow request"; public const string Update = "Updated borrow request"; } + public const string Delete = "Delete document"; + public const string Update = "Updated document information"; } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs b/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs index b8129b60..6d383334 100644 --- a/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/DocumentDto.cs @@ -15,6 +15,7 @@ public class DocumentDto : BaseDto, IMapFrom public UserDto? Importer { get; set; } public FolderDto? Folder { get; set; } public string Status { get; set; } = null!; + public bool IsPrivate { get; set; } public EntryDto? Entry { get; set; } public void Mapping(Profile profile) diff --git a/src/Application/Documents/Commands/ApproveDocument.cs b/src/Application/Documents/Commands/ApproveDocument.cs index 15bb65cd..33d0feaa 100644 --- a/src/Application/Documents/Commands/ApproveDocument.cs +++ b/src/Application/Documents/Commands/ApproveDocument.cs @@ -4,6 +4,7 @@ using Application.Common.Models.Dtos.ImportDocument; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; using Domain.Entities.Logging; using Domain.Statuses; using MediatR; @@ -17,8 +18,9 @@ public class ApproveDocument { public record Command : IRequest { - public Guid PerformingUserId { get; init; } + public User CurrentUser { get; init; } = null!; public Guid DocumentId { get; init; } + public string Decision { get; init; } = null!; public string Reason { get; init; } = null!; } @@ -46,25 +48,33 @@ public async Task Handle(Command request, CancellationToken cancell if (document.Status is not DocumentStatus.Issued) { - throw new ConflictException("Request cannot be approved."); + throw new ConflictException("Request cannot be approved or rejected."); } - document.Status = DocumentStatus.Approved; - var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); + if (IsApproval(request.Decision)) + { + document.Status = DocumentStatus.Approved; + } + + if (IsRejection(request.Decision)) + { + document.Status = DocumentStatus.Rejected; + } + var log = new DocumentLog() { Object = document, Time = LocalDateTime.FromDateTime(DateTime.Now), - User = performingUser!, - UserId = performingUser!.Id, + User = request.CurrentUser, + UserId = request.CurrentUser.Id, Action = DocumentLogMessages.Import.Approve, }; var requestLog = new RequestLog() { Object = document, Time = LocalDateTime.FromDateTime(DateTime.Now), - User = performingUser, - UserId = performingUser.Id, + User = request.CurrentUser, + UserId = request.CurrentUser.Id, Action = RequestLogMessages.ApproveImport, Reason = request.Reason, }; @@ -74,5 +84,11 @@ public async Task Handle(Command request, CancellationToken cancell await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } + + private static bool IsApproval(string decision) + => decision.ToLower().Trim().Equals("approve"); + + private static bool IsRejection(string decision) + => decision.ToLower().Trim().Equals("reject"); } } \ No newline at end of file diff --git a/src/Application/Documents/Commands/DeleteDocument.cs b/src/Application/Documents/Commands/DeleteDocument.cs index 903b7804..494f4f45 100644 --- a/src/Application/Documents/Commands/DeleteDocument.cs +++ b/src/Application/Documents/Commands/DeleteDocument.cs @@ -1,8 +1,12 @@ using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Documents.Commands; @@ -10,6 +14,7 @@ public class DeleteDocument { public record Command : IRequest { + public User CurrentUser { get; init; } = null!; public Guid DocumentId { get; init; } } @@ -17,33 +22,44 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) { var document = await _context.Documents .Include( x => x.Folder) - .FirstOrDefaultAsync(x => x.Id.Equals(request.DocumentId), cancellationToken); + .FirstOrDefaultAsync(x => x.Id == request.DocumentId, cancellationToken); if (document is null) { throw new KeyNotFoundException("Document does not exist."); } - var folder = document.Folder; - var result = _context.Documents.Remove(document); + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); - if (folder is not null) + if (document.Folder is not null) { - folder.NumberOfDocuments -= 1; - _context.Folders.Update(folder); + document.Folder.NumberOfDocuments -= 1; + _context.Folders.Update(document.Folder); } + var log = new DocumentLog() + { + Object = document, + Time = localDateTimeNow, + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + Action = DocumentLogMessages.Delete, + }; + var result = _context.Documents.Remove(document); + await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } diff --git a/src/Application/Documents/Commands/ImportDocument.cs b/src/Application/Documents/Commands/ImportDocument.cs index eaf17236..7f449892 100644 --- a/src/Application/Documents/Commands/ImportDocument.cs +++ b/src/Application/Documents/Commands/ImportDocument.cs @@ -4,6 +4,7 @@ using Application.Common.Models.Dtos.ImportDocument; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; using Domain.Entities.Logging; using Domain.Entities.Physical; using Domain.Statuses; @@ -17,7 +18,7 @@ public class ImportDocument { public record Command : IRequest { - public Guid PerformingUserId { get; init; } + public User CurrentUser { get; init; } = null!; public string Title { get; init; } = null!; public string? Description { get; init; } public string DocumentType { get; init; } = null!; @@ -30,11 +31,13 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -47,6 +50,16 @@ public async Task Handle(Command request, CancellationToken cancell throw new KeyNotFoundException("User does not exist."); } + if (importer.Department is null) + { + throw new ConflictException("User does not have a department."); + } + + if (importer.Department.Id != request.CurrentUser.Department!.Id) + { + throw new ConflictException("User is in another department as staff."); + } + var document = _context.Documents.FirstOrDefault(x => x.Title.Trim().ToLower().Equals(request.Title.Trim().ToLower()) && x.Importer != null @@ -68,7 +81,8 @@ public async Task Handle(Command request, CancellationToken cancell throw new ConflictException("This folder cannot accept more documents."); } - var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + var entity = new Document() { Title = request.Title.Trim(), @@ -79,15 +93,15 @@ public async Task Handle(Command request, CancellationToken cancell Folder = folder, Status = DocumentStatus.Available, IsPrivate = request.IsPrivate, - Created = LocalDateTime.FromDateTime(DateTime.Now), - CreatedBy = performingUser!.Id, + Created = localDateTimeNow, + CreatedBy = request.CurrentUser.Id, }; var log = new DocumentLog() { - User = performingUser, - UserId = performingUser.Id, + User = request.CurrentUser, + UserId = request.CurrentUser.Id, Object = entity, - Time = LocalDateTime.FromDateTime(DateTime.Now), + Time = localDateTimeNow, Action = DocumentLogMessages.Import.NewImport, }; diff --git a/src/Application/Documents/Commands/RequestImportDocument.cs b/src/Application/Documents/Commands/RequestImportDocument.cs index 4efe3ca1..2247891f 100644 --- a/src/Application/Documents/Commands/RequestImportDocument.cs +++ b/src/Application/Documents/Commands/RequestImportDocument.cs @@ -4,6 +4,7 @@ using Application.Common.Models.Dtos.ImportDocument; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; using Domain.Entities.Logging; using Domain.Entities.Physical; using Domain.Statuses; @@ -21,61 +22,57 @@ public record Command : IRequest public string Title { get; init; } = null!; public string? Description { get; init; } public string DocumentType { get; init; } = null!; - public Guid IssuerId { get; init; } - public bool IsPrivate { get; set; } + public User Issuer { get; init; } = null!; + public Guid RoomId { get; init; } + public bool IsPrivate { get; init; } } public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) { - var issuer = await _context.Users - .Include(x => x.Department) - .FirstOrDefaultAsync(x => x.Id == request.IssuerId, cancellationToken); - if (issuer is null) - { - throw new UnauthorizedAccessException(); - } - var document = _context.Documents.FirstOrDefault(x => x.Title.Trim().ToLower().Equals(request.Title.Trim().ToLower()) - && x.Importer != null - && x.Importer.Id == request.IssuerId); + && x.Importer!.Id == request.Issuer.Id); if (document is not null) { - throw new ConflictException($"Document title already exists for user {issuer.LastName}."); + throw new ConflictException($"Document title already exists for user {request.Issuer.LastName}."); } + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + var entity = new Document() { Title = request.Title.Trim(), Description = request.Description?.Trim(), DocumentType = request.DocumentType.Trim(), - Importer = issuer, - Department = issuer.Department, + Importer = request.Issuer, + Department = request.Issuer.Department, Status = DocumentStatus.Issued, IsPrivate = request.IsPrivate, - Created = LocalDateTime.FromDateTime(DateTime.Now), - CreatedBy = issuer.Id, + Created = localDateTimeNow, + CreatedBy = request.Issuer.Id, }; + var log = new DocumentLog() { Object = entity, - Time = LocalDateTime.FromDateTime(DateTime.Now), - User = issuer, - UserId = issuer.Id, + Time = localDateTimeNow, + User = request.Issuer, + UserId = request.Issuer.Id, Action = DocumentLogMessages.Import.NewImportRequest, }; - var result = await _context.Documents.AddAsync(entity, cancellationToken); await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); diff --git a/src/Application/Documents/Commands/UpdateDocument.cs b/src/Application/Documents/Commands/UpdateDocument.cs index 442da5ce..a35031d4 100644 --- a/src/Application/Documents/Commands/UpdateDocument.cs +++ b/src/Application/Documents/Commands/UpdateDocument.cs @@ -1,10 +1,16 @@ using Application.Common.Exceptions; +using Application.Common.Extensions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; +using Domain.Entities.Physical; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Documents.Commands; @@ -31,21 +37,25 @@ public Validator() public record Command : IRequest { + public User CurrentUser { get; init; } = null!; public Guid DocumentId { get; init; } public string Title { get; init; } = null!; public string? Description { get; init; } public string DocumentType { get; init; } = null!; + public bool IsPrivate { get; init; } } public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -53,12 +63,17 @@ public async Task Handle(Command request, CancellationToken cancell var document = await _context.Documents .Include(x => x.Department) .Include( x => x.Importer) - .FirstOrDefaultAsync( x => x.Id.Equals(request.DocumentId), cancellationToken); + .FirstOrDefaultAsync( x => x.Id == request.DocumentId, cancellationToken); if (document is null) { throw new KeyNotFoundException("Document does not exist."); } + + if (ViolateConstraints(request.CurrentUser, document)) + { + throw new UnauthorizedAccessException("Cannot update this document."); + } if (document.Importer is not null) { @@ -67,22 +82,44 @@ public async Task Handle(Command request, CancellationToken cancell .AnyAsync(x => x.Title.Trim().ToLower().Equals(request.Title.Trim().ToLower()) && x.Id != document.Id - && x.Importer!.Id == document.Importer!.Id - , cancellationToken); + && x.Importer!.Id == document.Importer!.Id, cancellationToken); if (titleExisted) { throw new ConflictException("Document name already exists for this importer."); } } + + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); document.Title = request.Title; document.DocumentType = request.DocumentType; document.Description = request.Description; + document.IsPrivate = request.IsPrivate; + document.LastModified = localDateTimeNow; + document.LastModifiedBy = request.CurrentUser.Id; + var log = new DocumentLog() + { + Object = document, + Time = localDateTimeNow, + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + Action = DocumentLogMessages.Update, + }; var result = _context.Documents.Update(document); + await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } + + private static bool ViolateConstraints(User currentUser, Document document) + => (currentUser.Role.IsStaff() + && NotInSameDepartment(currentUser, document)) + || (currentUser.Role.IsEmployee() + && document.ImporterId != currentUser.Id); + + private static bool NotInSameDepartment(User currentUser, Document document) + => currentUser.Department!.Id != document.Department!.Id; } } \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetAllDocumentsForEmployeePaginated.cs b/src/Application/Documents/Queries/GetAllDocumentsForEmployeePaginated.cs index cb9c52cf..0c0edd84 100644 --- a/src/Application/Documents/Queries/GetAllDocumentsForEmployeePaginated.cs +++ b/src/Application/Documents/Queries/GetAllDocumentsForEmployeePaginated.cs @@ -18,7 +18,9 @@ public class GetAllDocumentsForEmployeePaginated { public record Query : IRequest> { - public User CurrentUser { get; init; } = null!; + public Guid CurrentUserId { get; init; } + public Guid CurrentUserDepartmentId { get; init; } + public Guid? UserId { get; init; } public string? SearchTerm { get; init; } public int? Page { get; init; } public int? Size { get; init; } @@ -53,25 +55,30 @@ public async Task> Handle(Query request, if (request.IsPrivate) { var permissions = _context.Permissions.Where(x => - IsSameUser(x.EmployeeId, request.CurrentUser.Id) - && InSameDepartmentAsUser(x.Document, request.CurrentUser) + IsSameUser(x.EmployeeId, request.CurrentUserId) + && InSameDepartmentAsUser(x.Document, request.CurrentUserDepartmentId) && HasReadPermission(x.AllowedOperations)) .Select(x => x.DocumentId); documents = documents.Where(x => - InSameDepartmentAsUser(x, request.CurrentUser) + InSameDepartmentAsUser(x, request.CurrentUserDepartmentId) && x.IsPrivate - && (CanRead(permissions, x.Id) || IsImporter(x, request.CurrentUser))); + && (CanRead(permissions, x.Id) || IsImporter(x, request.CurrentUserId))); } else { documents = documents.Where(x => - InSameDepartmentAsUser(x, request.CurrentUser) + InSameDepartmentAsUser(x, request.CurrentUserDepartmentId) && !x.IsPrivate); } + + if (request.UserId is not null) + { + documents = documents.Where(x => x.Importer!.Id == request.UserId); + } if (request.DocumentStatus is not null - && Enum.TryParse(request.DocumentStatus, true, out DocumentStatus status)) + && Enum.TryParse(request.DocumentStatus, true, out DocumentStatus status)) { documents = documents.Where(x => x.Status == status); } @@ -95,8 +102,8 @@ public async Task> Handle(Query request, private static bool IsSameUser(Guid userId1, Guid userId2) => userId1 == userId2; - private static bool InSameDepartmentAsUser(Document document, User user) - => document.Department!.Id == user.Department!.Id; + private static bool InSameDepartmentAsUser(Document document, Guid userDepartmentId) + => document.Department!.Id == userDepartmentId; private static bool HasReadPermission(string allowedPermissions) => allowedPermissions.Contains(DocumentOperation.Read.ToString()); @@ -104,7 +111,7 @@ private static bool HasReadPermission(string allowedPermissions) private static bool CanRead(IEnumerable documentIds, Guid documentId) => documentIds.Contains(documentId); - private static bool IsImporter(Document document, User currentUser) - => document.ImporterId == currentUser.Id; + private static bool IsImporter(Document document, Guid userId) + => document.ImporterId == userId; } } \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs b/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs index a8af8b3b..aaa60446 100644 --- a/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs +++ b/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs @@ -41,6 +41,7 @@ public record Query : IRequest> public string? SortBy { get; init; } public string? SortOrder { get; init; } public string? DocumentStatus { get; init; } + public string? Role { get; init; } public bool? IsPrivate { get; init; } } @@ -84,6 +85,11 @@ public async Task> Handle(Query request, { documents = documents.Where(x => x.Importer!.Id == request.UserId); } + + if (request.Role is not null) + { + documents = documents.Where(x => x.Importer!.Role.Equals(request.Role)); + } if (folderExists) { diff --git a/src/Application/Documents/Queries/GetDocumentReason.cs b/src/Application/Documents/Queries/GetDocumentReason.cs index fc517fdf..f88e1190 100644 --- a/src/Application/Documents/Queries/GetDocumentReason.cs +++ b/src/Application/Documents/Queries/GetDocumentReason.cs @@ -16,7 +16,7 @@ public class GetDocumentReason { public record Query : IRequest { - public User User { get; set; } + public User CurrentUser { get; init; } public Guid DocumentId { get; init; } public RequestType Type { get; init; } } @@ -25,13 +25,11 @@ public class QueryHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; - private readonly ICurrentUserService _currentUserService; - public QueryHandler(IApplicationDbContext context, IMapper mapper, ICurrentUserService currentUserService) + public QueryHandler(IApplicationDbContext context, IMapper mapper) { _context = context; _mapper = mapper; - _currentUserService = currentUserService; } public async Task Handle(Query request, CancellationToken cancellationToken) @@ -46,10 +44,10 @@ public async Task Handle(Query request, CancellationToken cancellatio if (log is null) { - throw new KeyNotFoundException("Document does not have a request."); + throw new KeyNotFoundException("Import request does not exist."); } - await EnforceRoleConstraintsAsync(_currentUserService.GetCurrentUser(), log); + await EnforceRoleConstraintsAsync(request.CurrentUser, log); return _mapper.Map(log); } diff --git a/src/Application/Folders/Commands/AddFolder.cs b/src/Application/Folders/Commands/AddFolder.cs index 6d47e513..dd65042e 100644 --- a/src/Application/Folders/Commands/AddFolder.cs +++ b/src/Application/Folders/Commands/AddFolder.cs @@ -42,6 +42,7 @@ public Validator() public record Command : IRequest { public User CurrentUser { get; init; } = null!; + public Guid? CurrentStaffRoomId { get; init; } public string Name { get; init; } = null!; public string? Description { get; init; } public int Capacity { get; init; } @@ -78,9 +79,10 @@ public async Task Handle(Command request, CancellationToken cancellat } if (request.CurrentUser.Role.IsStaff() - && !LockerExistsAndInSameDepartment(locker, request.CurrentUser.Department?.Id)) + && (locker.Room.Id != request.CurrentStaffRoomId + || !LockerIsInRoom(locker, request.CurrentStaffRoomId))) { - throw new UnauthorizedAccessException("User cannot add this resource."); + throw new UnauthorizedAccessException("User cannot access this resource."); } if (await DuplicatedNameFolderExistsInSameLockerAsync(request.Name, locker.Id, cancellationToken)) @@ -132,7 +134,7 @@ private static bool EqualsInvariant(string x, string y) private static bool IsSameLocker(Guid lockerId1, Guid lockerId2) => lockerId1 == lockerId2; - private static bool LockerExistsAndInSameDepartment(Locker locker, Guid? departmentId) - => departmentId is not null && locker.Room.DepartmentId == departmentId; + private static bool LockerIsInRoom(Locker locker, Guid? roomId) + => roomId is not null && locker.Room.Id == roomId; } } \ No newline at end of file diff --git a/src/Application/Folders/Commands/RemoveFolder.cs b/src/Application/Folders/Commands/RemoveFolder.cs index dee17b93..11f91fd2 100644 --- a/src/Application/Folders/Commands/RemoveFolder.cs +++ b/src/Application/Folders/Commands/RemoveFolder.cs @@ -14,7 +14,7 @@ public class RemoveFolder public record Command : IRequest { public string CurrentUserRole { get; init; } = null!; - public Guid CurrentUserDepartmentId { get; init; } + public Guid? CurrentStaffRoomId { get; init; } public Guid FolderId { get; init; } } @@ -43,7 +43,7 @@ public async Task Handle(Command request, CancellationToken cancellat } if (request.CurrentUserRole.IsStaff() - && !FolderIsInDepartment(folder, request.CurrentUserDepartmentId)) + && (request.CurrentStaffRoomId is null || !FolderIsInRoom(folder, request.CurrentStaffRoomId.Value))) { throw new UnauthorizedAccessException("User cannot remove this resource."); } @@ -62,7 +62,7 @@ public async Task Handle(Command request, CancellationToken cancellat return _mapper.Map(result.Entity); } - private static bool FolderIsInDepartment(Folder folder, Guid departmentId) - => folder.Locker.Room.DepartmentId == departmentId; + private static bool FolderIsInRoom(Folder folder, Guid roomId) + => folder.Locker.Room.Id == roomId; } } \ No newline at end of file diff --git a/src/Application/Folders/Commands/UpdateFolder.cs b/src/Application/Folders/Commands/UpdateFolder.cs index 19b443bb..a32c569a 100644 --- a/src/Application/Folders/Commands/UpdateFolder.cs +++ b/src/Application/Folders/Commands/UpdateFolder.cs @@ -38,6 +38,7 @@ public Validator() public record Command : IRequest { public User CurrentUser { get; init; } = null!; + public Guid? CurrentStaffRoomId { get; init; } public Guid FolderId { get; init; } public string Name { get; init; } = null!; public string? Description { get; init; } @@ -69,7 +70,7 @@ public async Task Handle(Command request, CancellationToken cancellat } if (request.CurrentUser.Role.IsStaff() - && !FolderIsInDepartment(folder, request.CurrentUser.Department!.Id)) + && (request.CurrentStaffRoomId is null || !FolderIsInRoom(folder, request.CurrentUser.Department!.Id))) { throw new UnauthorizedAccessException("User cannot remove this resource."); } @@ -126,7 +127,7 @@ private static bool IsSameLocker(Guid lockerId1, Guid lockerId2) private static bool IsNotSameFolder(Guid folderId1, Guid folderId2) => folderId1 != folderId2; - private static bool FolderIsInDepartment(Folder folder, Guid departmentId) - => folder.Locker.Room.DepartmentId == departmentId; + private static bool FolderIsInRoom(Folder folder, Guid roomId) + => folder.Locker.Room.Id == roomId; } } \ No newline at end of file diff --git a/src/Application/Folders/Queries/GetAllFoldersPaginated.cs b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs index 3161b4c7..d2d9d77d 100644 --- a/src/Application/Folders/Queries/GetAllFoldersPaginated.cs +++ b/src/Application/Folders/Queries/GetAllFoldersPaginated.cs @@ -29,7 +29,7 @@ public Validator() public record Query : IRequest> { public string CurrentUserRole { get; init; } = null!; - public Guid CurrentUserDepartmentId { get; init; } + public Guid? CurrentStaffRoomId { get; init; } public Guid? RoomId { get; init; } public Guid? LockerId { get; init; } public string? SearchTerm { get; init; } @@ -52,24 +52,11 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task> Handle(Query request, CancellationToken cancellationToken) { - if (request.CurrentUserRole.IsStaff()) + if (request.CurrentUserRole.IsStaff() + && (request.CurrentStaffRoomId is null || request.RoomId is null + || !IsSameRoom(request.RoomId.Value, request.CurrentStaffRoomId.Value))) { - if (request.RoomId is null) - { - throw new UnauthorizedAccessException("User cannot access this resource."); - } - - var currentUserRoom = await GetRoomByDepartmentIdAsync(request.CurrentUserDepartmentId, cancellationToken); - - if (currentUserRoom is null) - { - throw new UnauthorizedAccessException("User cannot access this resource."); - } - - if (!IsSameRoom(currentUserRoom.Id, request.RoomId.Value)) - { - throw new UnauthorizedAccessException("User cannot access this resource."); - } + throw new UnauthorizedAccessException("User cannot access this resource."); } var folders = _context.Folders @@ -127,11 +114,6 @@ public async Task> Handle(Query request, CancellationTo cancellationToken); } - private async Task GetRoomByDepartmentIdAsync(Guid departmentId, CancellationToken cancellationToken) - => await _context.Rooms.FirstOrDefaultAsync( - x => x.DepartmentId == departmentId, - cancellationToken); - private static bool IsSameRoom(Guid roomId1, Guid roomId2) => roomId1 == roomId2; } diff --git a/src/Application/Folders/Queries/GetFolderById.cs b/src/Application/Folders/Queries/GetFolderById.cs index 4a1aa1af..a2183e56 100644 --- a/src/Application/Folders/Queries/GetFolderById.cs +++ b/src/Application/Folders/Queries/GetFolderById.cs @@ -13,7 +13,7 @@ public class GetFolderById public record Query : IRequest { public string CurrentUserRole { get; init; } = null!; - public Guid CurrentUserDepartmentId { get; init; } + public Guid? CurrentStaffRoomId { get; init; } public Guid FolderId { get; init; } } @@ -42,15 +42,15 @@ public async Task Handle(Query request, CancellationToken cancellatio } if (request.CurrentUserRole.IsStaff() - && !FolderInSameDepartment(folder, request.CurrentUserDepartmentId)) + && (request.CurrentStaffRoomId is null || !FolderInSameRoom(folder, request.CurrentStaffRoomId.Value))) { - throw new UnauthorizedAccessException(); + throw new UnauthorizedAccessException("User cannot access this resource."); } return _mapper.Map(folder); } - private static bool FolderInSameDepartment(Folder folder, Guid departmentId) - => folder.Locker.Room.DepartmentId == departmentId; + private static bool FolderInSameRoom(Folder folder, Guid roomId) + => folder.Locker.Room.Id == roomId; } } \ No newline at end of file diff --git a/src/Application/Lockers/Queries/GetLockerById.cs b/src/Application/Lockers/Queries/GetLockerById.cs index 1f962c43..3a21a7a4 100644 --- a/src/Application/Lockers/Queries/GetLockerById.cs +++ b/src/Application/Lockers/Queries/GetLockerById.cs @@ -14,7 +14,7 @@ public class GetLockerById public record Query : IRequest { public string CurrentUserRole { get; init; } = null!; - public Guid CurrentUserDepartmentId { get; init; } + public Guid? CurrentStaffRoomId { get; init; } public Guid LockerId { get; init; } } @@ -40,19 +40,19 @@ public async Task Handle(Query request, CancellationToken cancellatio { throw new KeyNotFoundException("Locker does not exist."); } - + if (request.CurrentUserRole.IsStaff() - && !LockerInSameDepartment(locker, request.CurrentUserDepartmentId)) + && (request.CurrentStaffRoomId is null || !LockerInSameRoom(locker, request.CurrentStaffRoomId.Value))) { - throw new UnauthorizedAccessException(); + throw new UnauthorizedAccessException("User cannot access this resource."); } return _mapper.Map(locker); } - private static bool LockerInSameDepartment( + private static bool LockerInSameRoom( Locker locker, - Guid departmentId) - => locker.Room.DepartmentId == departmentId; + Guid roomId) + => locker.Room.Id == roomId; } } \ No newline at end of file diff --git a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs index b53043c5..a075caed 100644 --- a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs +++ b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs @@ -5,6 +5,7 @@ using Application.Common.Models.Dtos.Physical; using AutoMapper; using AutoMapper.QueryableExtensions; +using Domain.Entities; using Domain.Entities.Physical; using MediatR; using Microsoft.EntityFrameworkCore; @@ -15,6 +16,8 @@ public class GetAllRoomsPaginated { public record Query : IRequest> { + public User CurrentUser { get; init; } = null!; + public Guid? DepartmentId { get; init; } public string? SearchTerm { get; init; } public int? Page { get; init; } public int? Size { get; init; } @@ -40,6 +43,14 @@ public async Task> Handle(Query request, CancellationToke .Include(x => x.Staff) .AsQueryable(); + if (request.CurrentUser.Role.IsEmployee() + && request.CurrentUser.Department?.Id != request.DepartmentId) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + + rooms = rooms.Where(x => x.Department.Id == request.DepartmentId); + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) { rooms = rooms.Where(x => diff --git a/src/Domain/Entities/Physical/ImportRequest.cs b/src/Domain/Entities/Physical/ImportRequest.cs new file mode 100644 index 00000000..236148ee --- /dev/null +++ b/src/Domain/Entities/Physical/ImportRequest.cs @@ -0,0 +1,9 @@ +using Domain.Common; + +namespace Domain.Entities.Physical; + +public class ImportRequest : BaseAuditableEntity +{ + public Room Room { get; set; } + public Document Document { get; set; } +} \ No newline at end of file From 229cd0f76f82c84b757543e41ef9c8455b3f0257 Mon Sep 17 00:00:00 2001 From: Vzart Date: Sun, 18 Jun 2023 12:39:37 +0700 Subject: [PATCH 45/56] i dont know what i do --- src/Api/Controllers/DocumentsController.cs | 6 +- .../Interfaces/IApplicationDbContext.cs | 1 + .../Documents/Commands/ApproveDocument.cs | 20 +- .../Commands/RequestImportDocument.cs | 19 +- src/Domain/Entities/Physical/ImportRequest.cs | 8 +- src/Domain/Statuses/ImportRequestStatus.cs | 9 + .../Persistence/ApplicationDbContext.cs | 1 + .../ImportRequestConfiguration.cs | 32 + ...0230618044742_AddImportRequest.Designer.cs | 1076 ++++++++++++++++ .../20230618044742_AddImportRequest.cs | 63 + ...8051340_AddImportRequestReason.Designer.cs | 1080 +++++++++++++++++ .../20230618051340_AddImportRequestReason.cs | 29 + .../ApplicationDbContextModelSnapshot.cs | 59 + 13 files changed, 2394 insertions(+), 9 deletions(-) create mode 100644 src/Domain/Statuses/ImportRequestStatus.cs create mode 100644 src/Infrastructure/Persistence/Configurations/ImportRequestConfiguration.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230618044742_AddImportRequest.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230618044742_AddImportRequest.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230618051340_AddImportRequestReason.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230618051340_AddImportRequestReason.cs diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index 14f358c8..a0c38704 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -277,7 +277,7 @@ public async Task>> Delete( /// /// Approve a document request /// - /// Id of the document to be approved + /// Id of the document to be approved /// /// A DocumentDto of the approved document [RequiresRole(IdentityData.Roles.Staff)] @@ -286,14 +286,14 @@ public async Task>> Delete( [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> ApproveOrReject( - [FromRoute] Guid documentId, + [FromRoute] Guid importRequestId, [FromBody] ApproveOrRejectImportRequest request) { var currentUser = _currentUserService.GetCurrentUser(); var query = new ApproveDocument.Command() { CurrentUser = currentUser, - DocumentId = documentId, + ImportRequestId = importRequestId, Reason = request.Reason, Decision = request.Decision, }; diff --git a/src/Application/Common/Interfaces/IApplicationDbContext.cs b/src/Application/Common/Interfaces/IApplicationDbContext.cs index 88d61ad1..9addca06 100644 --- a/src/Application/Common/Interfaces/IApplicationDbContext.cs +++ b/src/Application/Common/Interfaces/IApplicationDbContext.cs @@ -16,6 +16,7 @@ public interface IApplicationDbContext public DbSet Lockers { get; } public DbSet Folders { get; } public DbSet Documents { get; } + public DbSet ImportRequests { get; } public DbSet Borrows { get; } public DbSet Permissions { get; } diff --git a/src/Application/Documents/Commands/ApproveDocument.cs b/src/Application/Documents/Commands/ApproveDocument.cs index 33d0feaa..cf39a4cf 100644 --- a/src/Application/Documents/Commands/ApproveDocument.cs +++ b/src/Application/Documents/Commands/ApproveDocument.cs @@ -6,6 +6,7 @@ using AutoMapper; using Domain.Entities; using Domain.Entities.Logging; +using Domain.Entities.Physical; using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; @@ -19,7 +20,7 @@ public class ApproveDocument public record Command : IRequest { public User CurrentUser { get; init; } = null!; - public Guid DocumentId { get; init; } + public Guid ImportRequestId { get; init; } public string Decision { get; init; } = null!; public string Reason { get; init; } = null!; } @@ -37,16 +38,27 @@ public CommandHandler(IApplicationDbContext context, IMapper mapper) public async Task Handle(Command request, CancellationToken cancellationToken) { + var importRequest = await _context.ImportRequests + .Include(x => x.Document) + .Include(x => x.Room) + .FirstOrDefaultAsync(x => x.Id == request.ImportRequestId, cancellationToken); + + if (importRequest is null) + { + throw new KeyNotFoundException("Import request does not exist."); + } + var document = await _context.Documents .Include(x => x.Department) .FirstOrDefaultAsync(x => - x.Id == request.DocumentId, cancellationToken); + x.Id == importRequest.Document.Id, cancellationToken); if (document is null) { throw new ConflictException("Document does not exist."); } - if (document.Status is not DocumentStatus.Issued) + if (document.Status is not DocumentStatus.Issued + && importRequest.Status is not ImportRequestStatus.Issued) { throw new ConflictException("Request cannot be approved or rejected."); } @@ -54,11 +66,13 @@ public async Task Handle(Command request, CancellationToken cancell if (IsApproval(request.Decision)) { document.Status = DocumentStatus.Approved; + importRequest.Status = ImportRequestStatus.Approved; } if (IsRejection(request.Decision)) { document.Status = DocumentStatus.Rejected; + importRequest.Status = ImportRequestStatus.Rejected; } var log = new DocumentLog() diff --git a/src/Application/Documents/Commands/RequestImportDocument.cs b/src/Application/Documents/Commands/RequestImportDocument.cs index 2247891f..84cd1309 100644 --- a/src/Application/Documents/Commands/RequestImportDocument.cs +++ b/src/Application/Documents/Commands/RequestImportDocument.cs @@ -47,7 +47,14 @@ public async Task Handle(Command request, CancellationToken c && x.Importer!.Id == request.Issuer.Id); if (document is not null) { - throw new ConflictException($"Document title already exists for user {request.Issuer.LastName}."); + throw new ConflictException($"Document title already exists for user {request.Issuer.FirstName}."); + } + + var room = await _context.Rooms.FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); + + if (room is null) + { + throw new KeyNotFoundException("Room does not exist."); } var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); @@ -64,6 +71,15 @@ public async Task Handle(Command request, CancellationToken c Created = localDateTimeNow, CreatedBy = request.Issuer.Id, }; + + var importRequest = new ImportRequest() + { + Document = entity, + Status = ImportRequestStatus.Issued, + Room = room, + Created = localDateTimeNow, + CreatedBy = request.Issuer.Id + }; var log = new DocumentLog() { @@ -74,6 +90,7 @@ public async Task Handle(Command request, CancellationToken c Action = DocumentLogMessages.Import.NewImportRequest, }; var result = await _context.Documents.AddAsync(entity, cancellationToken); + await _context.ImportRequests.AddAsync(importRequest, cancellationToken); await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); diff --git a/src/Domain/Entities/Physical/ImportRequest.cs b/src/Domain/Entities/Physical/ImportRequest.cs index 236148ee..bf4c002e 100644 --- a/src/Domain/Entities/Physical/ImportRequest.cs +++ b/src/Domain/Entities/Physical/ImportRequest.cs @@ -1,9 +1,13 @@ using Domain.Common; +using Domain.Statuses; +using NodaTime; namespace Domain.Entities.Physical; public class ImportRequest : BaseAuditableEntity { - public Room Room { get; set; } - public Document Document { get; set; } + public Room Room { get; set; } = null!; + public Document Document { get; set; } = null!; + public string Reason { get; set; } = null!; + public ImportRequestStatus Status { get; set; } } \ No newline at end of file diff --git a/src/Domain/Statuses/ImportRequestStatus.cs b/src/Domain/Statuses/ImportRequestStatus.cs new file mode 100644 index 00000000..067b149c --- /dev/null +++ b/src/Domain/Statuses/ImportRequestStatus.cs @@ -0,0 +1,9 @@ +namespace Domain.Statuses; + +public enum ImportRequestStatus +{ + Issued, + Approved, + Rejected, + CheckedIn, +} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/ApplicationDbContext.cs b/src/Infrastructure/Persistence/ApplicationDbContext.cs index 5ee7171b..237cd38f 100644 --- a/src/Infrastructure/Persistence/ApplicationDbContext.cs +++ b/src/Infrastructure/Persistence/ApplicationDbContext.cs @@ -27,6 +27,7 @@ public ApplicationDbContext( public DbSet Lockers => Set(); public DbSet Folders => Set(); public DbSet Documents => Set(); + public DbSet ImportRequests => Set(); public DbSet Borrows => Set(); public DbSet Permissions => Set(); diff --git a/src/Infrastructure/Persistence/Configurations/ImportRequestConfiguration.cs b/src/Infrastructure/Persistence/Configurations/ImportRequestConfiguration.cs new file mode 100644 index 00000000..6c8fcf7d --- /dev/null +++ b/src/Infrastructure/Persistence/Configurations/ImportRequestConfiguration.cs @@ -0,0 +1,32 @@ +using Domain.Entities.Physical; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Infrastructure.Persistence.Configurations; + +public class ImportRequestConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(x => x.Id); + + builder.Property(x => x.Id) + .ValueGeneratedOnAdd(); + + builder.HasOne(x => x.Document) + .WithMany() + .HasForeignKey("DocumentId") + .IsRequired(); + + builder.HasOne(x => x.Room) + .WithMany() + .HasForeignKey("RoomId") + .IsRequired(); + + builder.Property(x => x.Reason) + .IsRequired(); + + builder.Property(x => x.Status) + .IsRequired(); + } +} \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Migrations/20230618044742_AddImportRequest.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230618044742_AddImportRequest.Designer.cs new file mode 100644 index 00000000..dc0a573d --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230618044742_AddImportRequest.Designer.cs @@ -0,0 +1,1076 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230618044742_AddImportRequest")] + partial class AddImportRequest + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("RoomId"); + + b.ToTable("ImportRequests"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.HasOne("Domain.Entities.User", "Object") + .WithMany() + .HasForeignKey("ObjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Entry"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany() + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany("Rooms") + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230618044742_AddImportRequest.cs b/src/Infrastructure/Persistence/Migrations/20230618044742_AddImportRequest.cs new file mode 100644 index 00000000..67c80471 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230618044742_AddImportRequest.cs @@ -0,0 +1,63 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class AddImportRequest : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ImportRequests", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + RoomId = table.Column(type: "uuid", nullable: false), + DocumentId = table.Column(type: "uuid", nullable: false), + Status = table.Column(type: "integer", nullable: false), + Created = table.Column(type: "timestamp without time zone", nullable: false), + CreatedBy = table.Column(type: "uuid", nullable: true), + LastModified = table.Column(type: "timestamp without time zone", nullable: true), + LastModifiedBy = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ImportRequests", x => x.Id); + table.ForeignKey( + name: "FK_ImportRequests_Documents_DocumentId", + column: x => x.DocumentId, + principalTable: "Documents", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ImportRequests_Rooms_RoomId", + column: x => x.RoomId, + principalTable: "Rooms", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ImportRequests_DocumentId", + table: "ImportRequests", + column: "DocumentId"); + + migrationBuilder.CreateIndex( + name: "IX_ImportRequests_RoomId", + table: "ImportRequests", + column: "RoomId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ImportRequests"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230618051340_AddImportRequestReason.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230618051340_AddImportRequestReason.Designer.cs new file mode 100644 index 00000000..a64e8b12 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230618051340_AddImportRequestReason.Designer.cs @@ -0,0 +1,1080 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230618051340_AddImportRequestReason")] + partial class AddImportRequestReason + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("RoomId"); + + b.ToTable("ImportRequests"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.HasOne("Domain.Entities.User", "Object") + .WithMany() + .HasForeignKey("ObjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Entry"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany() + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany("Rooms") + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230618051340_AddImportRequestReason.cs b/src/Infrastructure/Persistence/Migrations/20230618051340_AddImportRequestReason.cs new file mode 100644 index 00000000..930e4718 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230618051340_AddImportRequestReason.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class AddImportRequestReason : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Reason", + table: "ImportRequests", + type: "text", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Reason", + table: "ImportRequests"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index 7896db84..f2809a40 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -438,6 +438,46 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Folders"); }); + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId"); + + b.HasIndex("RoomId"); + + b.ToTable("ImportRequests"); + }); + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => { b.Property("Id") @@ -887,6 +927,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Locker"); }); + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany() + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Room"); + }); + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => { b.HasOne("Domain.Entities.Physical.Room", "Room") From 3442a377e9d2e00d09941bf0790e7164f537642a Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Mon, 19 Jun 2023 09:56:05 +0700 Subject: [PATCH 46/56] fuck me 5 --- .fleet/settings.json | 3 + src/Api/Controllers/DocumentsController.cs | 170 +-- src/Api/Controllers/FoldersController.cs | 6 +- .../Controllers/ImportRequestsController.cs | 183 +++ src/Api/Controllers/LockersController.cs | 13 +- .../Documents/SharePermissionsRequest.cs | 2 +- .../GetAllLogsPaginatedQueryParameters.cs | 5 + ...lImportRequestsPaginatedQueryParameters.cs | 10 + src/Api/Controllers/UsersController.cs | 1 + src/Api/Services/ExpiryPermissionService.cs | 1 + .../Borrows/Commands/ApproveBorrowRequest.cs | 1 - .../Borrows/Commands/RejectBorrowRequest.cs | 1 - .../Common/Extensions/StringExtensions.cs | 6 + .../Interfaces/IApplicationDbContext.cs | 2 +- .../Common/Messages/DocumentLogMessages.cs | 4 + .../Common/Messages/RequestLogMessages.cs | 1 + .../Dtos/ImportDocument/ImportRequestDto.cs | 21 + .../ImportDocument/IssuedRequestRoomDto.cs | 21 + .../Documents/Commands/AssignDocument.cs | 90 -- .../Documents/Commands/CheckinDocument.cs | 75 -- .../Documents/Commands/RejectDocument.cs | 75 -- .../Documents/Commands/ShareDocument.cs | 118 +- .../Documents/Queries/GetDocumentReason.cs | 87 -- .../Documents/Queries/GetPermissions.cs | 55 +- .../Commands/ApproveOrRejectDocument.cs} | 92 +- .../ImportRequests/Commands/AssignDocument.cs | 104 ++ .../Commands/CheckinDocument.cs | 104 ++ .../Commands/RequestImportDocument.cs | 21 +- .../Queries/GetAllImportRequestsPaginated.cs | 91 ++ .../Queries/GetImportRequestById.cs | 57 + .../Queries/GetAllLockerLogsPaginated.cs | 38 + .../Lockers/Queries/GetLockerLogById.cs | 17 +- .../Rooms/Queries/GetAllRoomLogsPaginated.cs | 8 +- .../Users/Queries/GetAllUserLogsPaginated.cs | 6 + src/Domain/Entities/Logging/DocumentLog.cs | 2 +- src/Domain/Entities/Logging/FolderLog.cs | 1 + src/Domain/Entities/Logging/LockerLog.cs | 1 + src/Domain/Entities/Logging/RequestLog.cs | 1 - src/Domain/Entities/Physical/ImportRequest.cs | 8 +- src/Domain/Statuses/DocumentStatus.cs | 2 - src/Domain/Statuses/ImportRequestStatus.cs | 2 +- .../Persistence/ApplicationDbContext.cs | 2 +- .../ImportRequestConfiguration.cs | 19 +- ...6101721_DepartmentHasManyRooms.Designer.cs | 1021 +++++++++++++++ .../20230616101721_DepartmentHasManyRooms.cs | 37 + ...133410_LoggingNowHasBaseObject.Designer.cs | 1110 +++++++++++++++++ .../20230618133410_LoggingNowHasBaseObject.cs | 139 +++ .../ApplicationDbContextModelSnapshot.cs | 44 +- 48 files changed, 3266 insertions(+), 612 deletions(-) create mode 100644 .fleet/settings.json create mode 100644 src/Api/Controllers/ImportRequestsController.cs create mode 100644 src/Api/Controllers/Payload/Requests/ImportRequests/GetAllImportRequestsPaginatedQueryParameters.cs create mode 100644 src/Application/Common/Models/Dtos/ImportDocument/ImportRequestDto.cs create mode 100644 src/Application/Common/Models/Dtos/ImportDocument/IssuedRequestRoomDto.cs delete mode 100644 src/Application/Documents/Commands/AssignDocument.cs delete mode 100644 src/Application/Documents/Commands/CheckinDocument.cs delete mode 100644 src/Application/Documents/Commands/RejectDocument.cs delete mode 100644 src/Application/Documents/Queries/GetDocumentReason.cs rename src/Application/{Documents/Commands/ApproveDocument.cs => ImportRequests/Commands/ApproveOrRejectDocument.cs} (54%) create mode 100644 src/Application/ImportRequests/Commands/AssignDocument.cs create mode 100644 src/Application/ImportRequests/Commands/CheckinDocument.cs rename src/Application/{Documents => ImportRequests}/Commands/RequestImportDocument.cs (79%) create mode 100644 src/Application/ImportRequests/Queries/GetAllImportRequestsPaginated.cs create mode 100644 src/Application/ImportRequests/Queries/GetImportRequestById.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230616101721_DepartmentHasManyRooms.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230616101721_DepartmentHasManyRooms.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230618133410_LoggingNowHasBaseObject.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230618133410_LoggingNowHasBaseObject.cs diff --git a/.fleet/settings.json b/.fleet/settings.json new file mode 100644 index 00000000..a7858d18 --- /dev/null +++ b/.fleet/settings.json @@ -0,0 +1,3 @@ +{ + "editor.guides": [] +} \ No newline at end of file diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index a0c38704..660f3a95 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -3,14 +3,11 @@ using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Models; -using Application.Common.Models.Dtos; -using Application.Common.Models.Dtos.ImportDocument; using Application.Common.Models.Dtos.Logging; using Application.Common.Models.Dtos.Physical; using Application.Documents.Commands; using Application.Documents.Queries; using Application.Identity; -using Domain.Enums; using Infrastructure.Identity.Authorization; using Microsoft.AspNetCore.Mvc; @@ -157,60 +154,6 @@ public async Task>> Import( var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } - - /// - /// Request to import a document - /// - /// Import document request details - /// A DocumentDto of the imported document - [RequiresRole(IdentityData.Roles.Employee)] - [HttpPost("import-requests")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> RequestImport( - [FromBody] RequestImportDocumentRequest request) - { - var currentUser = _currentUserService.GetCurrentUser(); - var command = new RequestImportDocument.Command() - { - Title = request.Title, - Description = request.Description, - DocumentType = request.DocumentType, - IsPrivate = request.IsPrivate, - Issuer = currentUser, - RoomId = request.RoomId, - }; - var result = await Mediator.Send(command); - return Ok(Result.Succeed(result)); - } - - /// - /// Checkin a document - /// - /// - /// A DocumentDto of the imported document - [RequiresRole(IdentityData.Roles.Staff)] - [HttpPost("checkin{documentId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Checkin( - [FromRoute] Guid documentId) - { - var performingUserId = _currentUserService.GetId(); - var command = new CheckinDocument.Command() - { - PerformingUserId = performingUserId, - DocumentId = documentId, - }; - var result = await Mediator.Send(command); - return Ok(Result.Succeed(result)); - } /// /// Update a document @@ -273,82 +216,28 @@ public async Task>> Delete( var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); } - - /// - /// Approve a document request - /// - /// Id of the document to be approved - /// - /// A DocumentDto of the approved document - [RequiresRole(IdentityData.Roles.Staff)] - [HttpPut("import-requests/{documentId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> ApproveOrReject( - [FromRoute] Guid importRequestId, - [FromBody] ApproveOrRejectImportRequest request) - { - var currentUser = _currentUserService.GetCurrentUser(); - var query = new ApproveDocument.Command() - { - CurrentUser = currentUser, - ImportRequestId = importRequestId, - Reason = request.Reason, - Decision = request.Decision, - }; - var result = await Mediator.Send(query); - return Ok(Result.Succeed(result)); - } - - /// - /// Get a document request reason - /// - /// Id of the document to be rejected - /// A DocumentDto of the rejected document - [RequiresRole(IdentityData.Roles.Staff, IdentityData.Roles.Employee)] - [HttpPost("import-requests/{documentId:guid}/reasons")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> Reason( - [FromRoute] Guid documentId) - { - var currentUser = _currentUserService.GetCurrentUser(); - var query = new GetDocumentReason.Query() - { - CurrentUser = currentUser, - DocumentId = documentId, - Type = RequestType.Import, - }; - var result = await Mediator.Send(query); - return Ok(Result.Succeed(result)); - } /// - /// Assign a document to + /// Get permissions for an employee of a specific document /// - /// Id of the document to be rejected - /// - /// A DocumentDto of the rejected document - [RequiresRole(IdentityData.Roles.Staff)] - [HttpPost("assign/{documentId:guid}")] + /// Id of the document to be getting permissions from + /// A DocumentDto of the rejected document + [RequiresRole(IdentityData.Roles.Employee)] + [HttpGet("{documentId:guid}/permissions")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> Assign( - [FromRoute] Guid documentId, - [FromBody] AssignDocumentToFolderRequest request) + public async Task>> GetPermissions( + [FromRoute] Guid documentId) { - var performingUserId = _currentUserService.GetId(); - var query = new AssignDocument.Command() + var performingUser = _currentUserService.GetCurrentUser(); + var query = new GetPermissions.Query() { - PerformingUserId = performingUserId, + CurrentUser = performingUser, DocumentId = documentId, - FolderId = request.FolderId, }; var result = await Mediator.Send(query); - return Ok(Result.Succeed(result)); + return Ok(Result.Succeed(result)); } /// @@ -362,22 +251,22 @@ public async Task>> Assign( [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> SharePermissions( + public async Task>> SharePermissions( [FromRoute] Guid documentId, [FromBody] SharePermissionsRequest request) { - var performingUserId = _currentUserService.GetId(); + var currentUser = _currentUserService.GetCurrentUser(); var query = new ShareDocument.Command() { - PerformingUserId = performingUserId, + CurrentUser = currentUser, DocumentId = documentId, - UserIds = request.UserIds, + UserId = request.UserId, CanRead = request.CanRead, CanBorrow = request.CanBorrow, ExpiryDate = request.ExpiryDate, }; var result = await Mediator.Send(query); - return Ok(Result.Succeed(result)); + return Ok(Result.Succeed(result)); } /// @@ -385,7 +274,7 @@ public async Task>> SharePermissions( /// /// /// Return a DocumentLogDto - [RequiresRole(IdentityData.Roles.Admin)] + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpGet("log/{logId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -405,7 +294,7 @@ public async Task>> GetLogById([FromRoute] G /// /// /// Paginated list of DocumentLogDto - [RequiresRole(IdentityData.Roles.Admin)] + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpGet("logs")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task>>> GetAllLogsPaginated( @@ -420,27 +309,4 @@ public async Task>>> GetAllLog var result = await Mediator.Send(query); return Ok(Result>.Succeed(result)); } - - /// - /// Get permissions for an employee of a specific document - /// - /// Id of the document to be getting permissions from - /// A DocumentDto of the rejected document - [RequiresRole(IdentityData.Roles.Employee)] - [HttpGet("{documentId:guid}/permissions")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetPermissions( - [FromRoute] Guid documentId) - { - var performingUser = _currentUserService.GetCurrentUser(); - var query = new GetPermissions.Query() - { - PerformingUser = performingUser, - DocumentId = documentId, - }; - var result = await Mediator.Send(query); - return Ok(Result.Succeed(result)); - } } \ No newline at end of file diff --git a/src/Api/Controllers/FoldersController.cs b/src/Api/Controllers/FoldersController.cs index b4460ec3..c2ebeb13 100644 --- a/src/Api/Controllers/FoldersController.cs +++ b/src/Api/Controllers/FoldersController.cs @@ -167,7 +167,7 @@ public async Task>> Update([FromRoute] Guid folde [HttpGet("logs")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task>>> GetAllFolderLogs( + public async Task>>> GetAllLogsPaginated( [FromQuery] GetAllLogsPaginatedQueryParameters queryParameters) { var query = new GetAllFolderLogsPaginated.Query() @@ -185,12 +185,12 @@ public async Task>>> GetAllFolde /// /// /// - [RequiresRole(IdentityData.Roles.Admin)] + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpGet("log/{logId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetFolderLogById([FromRoute] Guid logId) + public async Task>> GetLogById([FromRoute] Guid logId) { var query = new GetFolderLogById.Query() { diff --git a/src/Api/Controllers/ImportRequestsController.cs b/src/Api/Controllers/ImportRequestsController.cs new file mode 100644 index 00000000..9c4ce63d --- /dev/null +++ b/src/Api/Controllers/ImportRequestsController.cs @@ -0,0 +1,183 @@ +using Api.Controllers.Payload.Requests.Documents; +using Api.Controllers.Payload.Requests.ImportRequests; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos; +using Application.Common.Models.Dtos.ImportDocument; +using Application.Common.Models.Dtos.Physical; +using Application.Identity; +using Application.ImportRequests.Commands; +using Application.ImportRequests.Queries; +using Domain.Enums; +using Infrastructure.Identity.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Api.Controllers; + +[Route("api/v1/documents/[controller]")] +public class ImportRequestsController : ApiControllerBase +{ + private readonly ICurrentUserService _currentUserService; + + public ImportRequestsController(ICurrentUserService currentUserService) + { + _currentUserService = currentUserService; + } + + /// + /// Get an import request by id. + /// + /// Id of the request> + /// An ImportRequestDto of the request + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff, IdentityData.Roles.Employee)] + [HttpGet("{importRequestId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> GetImportRequestById( + [FromRoute] Guid importRequestId) + { + var currentUserRole = _currentUserService.GetRole(); + var currentStaffRoomId = _currentUserService.GetCurrentRoomForStaff(); + var query = new GetImportRequestById.Query() + { + CurrentUserRole = currentUserRole, + RequestId = importRequestId, + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } + + /// + /// + /// + /// + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff, IdentityData.Roles.Employee)] + [HttpGet] + public async Task>>> GetAllImportRequestsPaginated( + [FromQuery] GetAllImportRequestsPaginatedQueryParameters queryParameters) + { + var currentUser = _currentUserService.GetCurrentUser(); + var query = new GetAllImportRequestsPaginated.Query() + { + CurrentUser = currentUser, + SearchTerm = queryParameters.SearchTerm, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } + + /// + /// Request to import a document + /// + /// Import document request details + /// A DocumentDto of the imported document + [RequiresRole(IdentityData.Roles.Employee)] + [HttpPost] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> RequestImport( + [FromBody] RequestImportDocumentRequest request) + { + var currentUser = _currentUserService.GetCurrentUser(); + var command = new RequestImportDocument.Command() + { + Title = request.Title, + Description = request.Description, + DocumentType = request.DocumentType, + IsPrivate = request.IsPrivate, + Issuer = currentUser, + RoomId = request.RoomId, + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } + + /// + /// Approve a document request + /// + /// Id of the document to be approved + /// + /// A DocumentDto of the approved document + [RequiresRole(IdentityData.Roles.Staff)] + [HttpPut] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> ApproveOrReject( + [FromRoute] Guid importRequestId, + [FromBody] ApproveOrRejectImportRequest request) + { + var currentUser = _currentUserService.GetCurrentUser(); + var query = new ApproveOrRejectDocument.Command() + { + CurrentUser = currentUser, + ImportRequestId = importRequestId, + Reason = request.Reason, + Decision = request.Decision, + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } + + /// + /// Assign a document to a folder + /// + /// Id of the document to be rejected + /// + /// A DocumentDto of the rejected document + [RequiresRole(IdentityData.Roles.Staff)] + [HttpPut("{importRequestId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> Assign( + [FromRoute] Guid importRequestId, + [FromBody] AssignDocumentToFolderRequest request) + { + var currentUser = _currentUserService.GetCurrentUser(); + var staffRoomId = _currentUserService.GetCurrentRoomForStaff(); + var query = new AssignDocument.Command() + { + CurrentUser = currentUser, + StaffRoomId = staffRoomId, + ImportRequestId = importRequestId, + FolderId = request.FolderId, + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } + + /// + /// Checkin a document + /// + /// + /// A DocumentDto of the imported document + [RequiresRole(IdentityData.Roles.Staff)] + [HttpPut("checkin/{documentId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> Checkin( + [FromRoute] Guid documentId) + { + var currentUser = _currentUserService.GetCurrentUser(); + var command = new CheckinDocument.Command() + { + CurrentUser = currentUser, + DocumentId = documentId, + }; + + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } +} \ No newline at end of file diff --git a/src/Api/Controllers/LockersController.cs b/src/Api/Controllers/LockersController.cs index bc9310eb..8c11bbfd 100644 --- a/src/Api/Controllers/LockersController.cs +++ b/src/Api/Controllers/LockersController.cs @@ -158,18 +158,23 @@ public async Task>> Update( /// /// Query parameters /// A list of LockerLogsDtos - [RequiresRole(IdentityData.Roles.Admin)] + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpGet("logs")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task>>> GetAllLockerLogs( + public async Task>>> GetAllLogsPaginated( [FromQuery] GetAllLogsPaginatedQueryParameters queryParameters) { + var currentUserRole = _currentUserService.GetRole(); + var currentUserDepartmentId = _currentUserService.GetDepartmentId(); var query = new GetAllLockerLogsPaginated.Query() { + CurrentUserRole = currentUserRole, + CurrentUserDepartmentId = currentUserDepartmentId, SearchTerm = queryParameters.SearchTerm, Page = queryParameters.Page, Size = queryParameters.Size, + RoomId = queryParameters.UserId }; var result = await Mediator.Send(query); return Ok(Result>.Succeed(result)); @@ -180,12 +185,12 @@ public async Task>>> GetAllLocke /// /// Id of the requested log /// A LockerLogDto of the requested log. - [RequiresRole(IdentityData.Roles.Admin)] + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpGet("logs/{logId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetLockerLogById([FromRoute] Guid logId) + public async Task>> GetLogById([FromRoute] Guid logId) { var query = new GetLockerLogById.Query() { diff --git a/src/Api/Controllers/Payload/Requests/Documents/SharePermissionsRequest.cs b/src/Api/Controllers/Payload/Requests/Documents/SharePermissionsRequest.cs index be85ccef..6a26ec1b 100644 --- a/src/Api/Controllers/Payload/Requests/Documents/SharePermissionsRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Documents/SharePermissionsRequest.cs @@ -2,7 +2,7 @@ namespace Api.Controllers.Payload.Requests.Documents; public class SharePermissionsRequest { - public Guid[] UserIds { get; set; } + public Guid UserId { get; set; } public bool CanRead { get; set; } public bool CanBorrow { get; set; } public DateTime ExpiryDate { get; set; } diff --git a/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs index 6bbb0f60..94c60331 100644 --- a/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs @@ -17,4 +17,9 @@ public class GetAllLogsPaginatedQueryParameters /// Size number /// public int? Size { get; set; } + + /// + /// User Id + /// + public Guid? UserId { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/ImportRequests/GetAllImportRequestsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/ImportRequests/GetAllImportRequestsPaginatedQueryParameters.cs new file mode 100644 index 00000000..c66c1956 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/ImportRequests/GetAllImportRequestsPaginatedQueryParameters.cs @@ -0,0 +1,10 @@ +namespace Api.Controllers.Payload.Requests.ImportRequests; + +/// +/// +/// +public class GetAllImportRequestsPaginatedQueryParameters : PaginatedQueryParameters +{ + public string? SearchTerm { get; set; } + public Guid? RoomId { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/UsersController.cs b/src/Api/Controllers/UsersController.cs index 7f2e791b..c0fd9a21 100644 --- a/src/Api/Controllers/UsersController.cs +++ b/src/Api/Controllers/UsersController.cs @@ -206,6 +206,7 @@ public async Task>>> GetAllLogsPag SearchTerm = queryParameters.SearchTerm, Page = queryParameters.Page, Size = queryParameters.Size, + UserId = queryParameters.UserId, }; var result = await Mediator.Send(query); return Ok(Result>.Succeed(result)); diff --git a/src/Api/Services/ExpiryPermissionService.cs b/src/Api/Services/ExpiryPermissionService.cs index f1e855ae..360c108e 100644 --- a/src/Api/Services/ExpiryPermissionService.cs +++ b/src/Api/Services/ExpiryPermissionService.cs @@ -1,4 +1,5 @@ using Application.Common.Interfaces; +using Domain.Entities.Physical; using NodaTime; namespace Api.Services; diff --git a/src/Application/Borrows/Commands/ApproveBorrowRequest.cs b/src/Application/Borrows/Commands/ApproveBorrowRequest.cs index d66bda15..4f6ebfde 100644 --- a/src/Application/Borrows/Commands/ApproveBorrowRequest.cs +++ b/src/Application/Borrows/Commands/ApproveBorrowRequest.cs @@ -92,7 +92,6 @@ or BorrowRequestStatus.CheckedOut User = performingUser, Time = LocalDateTime.FromDateTime(DateTime.Now), Action = DocumentLogMessages.Borrow.Approve, - Reason = request.Reason, }; var result = _context.Borrows.Update(borrowRequest); await _context.DocumentLogs.AddAsync(log, cancellationToken); diff --git a/src/Application/Borrows/Commands/RejectBorrowRequest.cs b/src/Application/Borrows/Commands/RejectBorrowRequest.cs index b38e4e02..2fcd0744 100644 --- a/src/Application/Borrows/Commands/RejectBorrowRequest.cs +++ b/src/Application/Borrows/Commands/RejectBorrowRequest.cs @@ -56,7 +56,6 @@ public async Task Handle(Command request, CancellationToken cancellat User = performingUser, Time = LocalDateTime.FromDateTime(DateTime.Now), Action = DocumentLogMessages.Borrow.Reject, - Reason = request.Reason, }; var result = _context.Borrows.Update(borrowRequest); await _context.RequestLogs.AddAsync(requestLog, cancellationToken); diff --git a/src/Application/Common/Extensions/StringExtensions.cs b/src/Application/Common/Extensions/StringExtensions.cs index f0646068..1725822a 100644 --- a/src/Application/Common/Extensions/StringExtensions.cs +++ b/src/Application/Common/Extensions/StringExtensions.cs @@ -21,4 +21,10 @@ public static bool IsStaff(this string role) public static bool IsEmployee(this string role) => role.Equals(IdentityData.Roles.Employee); + + public static bool IsApproval(this string decision) + => decision.ToLower().Trim().Equals("approve"); + + public static bool IsRejection(this string decision) + => decision.ToLower().Trim().Equals("reject"); } \ No newline at end of file diff --git a/src/Application/Common/Interfaces/IApplicationDbContext.cs b/src/Application/Common/Interfaces/IApplicationDbContext.cs index 9addca06..9176b655 100644 --- a/src/Application/Common/Interfaces/IApplicationDbContext.cs +++ b/src/Application/Common/Interfaces/IApplicationDbContext.cs @@ -18,7 +18,7 @@ public interface IApplicationDbContext public DbSet Documents { get; } public DbSet ImportRequests { get; } public DbSet Borrows { get; } - public DbSet Permissions { get; } + public DbSet Permissions { get; } public DbSet UserGroups { get; } public DbSet Files { get; } diff --git a/src/Application/Common/Messages/DocumentLogMessages.cs b/src/Application/Common/Messages/DocumentLogMessages.cs index 0c889d28..e92040f2 100644 --- a/src/Application/Common/Messages/DocumentLogMessages.cs +++ b/src/Application/Common/Messages/DocumentLogMessages.cs @@ -23,4 +23,8 @@ public static class Borrow } public const string Delete = "Delete document"; public const string Update = "Updated document information"; + public static string GrantRead(string userName) => $"Share Read Permission to user {userName}"; + public static string GrantBorrow(string userName) => $"Share Borrow Permission to user {userName}"; + public static string RevokeRead(string userName) => $"Remove Read Permission to user {userName}"; + public static string RevokeBorrow(string userName) => $"Remove Borrow Permission to user {userName}"; } \ No newline at end of file diff --git a/src/Application/Common/Messages/RequestLogMessages.cs b/src/Application/Common/Messages/RequestLogMessages.cs index 69fa8565..a84fbc04 100644 --- a/src/Application/Common/Messages/RequestLogMessages.cs +++ b/src/Application/Common/Messages/RequestLogMessages.cs @@ -6,4 +6,5 @@ public static class RequestLogMessages public const string RejectImport = "Rejected import request"; public const string ApproveBorrow = "Rejected borrow request"; public const string RejectBorrow = "Rejected borrow request"; + public const string CheckInImport = "Checkin import request"; } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/ImportDocument/ImportRequestDto.cs b/src/Application/Common/Models/Dtos/ImportDocument/ImportRequestDto.cs new file mode 100644 index 00000000..c86b3a7e --- /dev/null +++ b/src/Application/Common/Models/Dtos/ImportDocument/ImportRequestDto.cs @@ -0,0 +1,21 @@ +using Application.Common.Mappings; +using AutoMapper; +using Domain.Entities.Physical; + +namespace Application.Common.Models.Dtos.ImportDocument; + +public class ImportRequestDto : BaseDto, IMapFrom +{ + public IssuedRequestRoomDto Room { get; set; } = null!; + public IssuedDocumentDto Document { get; set; } = null!; + public string Reason { get; set; } = null!; + public string Status { get; set; } = null!; + + public void Mapping(Profile profile) + { + profile.CreateMap() + .ForMember(dest => dest.Status, + opt => opt.MapFrom(src => src.Status.ToString())); + + } +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/ImportDocument/IssuedRequestRoomDto.cs b/src/Application/Common/Models/Dtos/ImportDocument/IssuedRequestRoomDto.cs new file mode 100644 index 00000000..6800b193 --- /dev/null +++ b/src/Application/Common/Models/Dtos/ImportDocument/IssuedRequestRoomDto.cs @@ -0,0 +1,21 @@ +using Application.Common.Mappings; +using AutoMapper; +using Domain.Entities.Physical; + +namespace Application.Common.Models.Dtos.ImportDocument; + +public class IssuedRequestRoomDto : BaseDto, IMapFrom +{ + public string Name { get; set; } = null!; + public string? Description { get; set; } + public Guid? StaffId { get; set; } + public DepartmentDto? Department { get; set; } + + public void Mapping(Profile profile) + { + profile.CreateMap() + .ForMember(dest => dest.StaffId, + opt => opt.MapFrom(src => src.Staff!.Id)); + + } +} \ No newline at end of file diff --git a/src/Application/Documents/Commands/AssignDocument.cs b/src/Application/Documents/Commands/AssignDocument.cs deleted file mode 100644 index 80096146..00000000 --- a/src/Application/Documents/Commands/AssignDocument.cs +++ /dev/null @@ -1,90 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Messages; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using Domain.Entities.Logging; -using Domain.Statuses; -using MediatR; -using Microsoft.EntityFrameworkCore; -using NodaTime; - -namespace Application.Documents.Commands; - -public class AssignDocument -{ - public record Command : IRequest - { - public Guid PerformingUserId { get; init; } - public Guid DocumentId { get; init; } - public Guid FolderId { get; init; } - } - - public class CommandHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public CommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - var document = await _context.Documents - .Include(x => x.Folder) - .FirstOrDefaultAsync(x => - x.Id == request.DocumentId, cancellationToken); - if (document is null) - { - throw new ConflictException("Document does not exist."); - } - - if (document.Status is not DocumentStatus.Approved) - { - throw new ConflictException("Document cannot be assigned."); - } - - var folder = await _context.Folders - .FirstOrDefaultAsync(x => x.Id == request.FolderId, cancellationToken); - - if (folder is null) - { - throw new ConflictException("Folder does not exist."); - } - - if (folder.NumberOfDocuments >= folder.Capacity) - { - throw new ConflictException("This folder cannot accept more documents."); - } - - var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); - document.Folder = folder; - document.LastModified = LocalDateTime.FromDateTime(DateTime.Now); - document.LastModifiedBy = performingUser!.Id; - var log = new DocumentLog() - { - Object = document, - Time = LocalDateTime.FromDateTime(DateTime.Now), - User = performingUser!, - UserId = performingUser!.Id, - Action = DocumentLogMessages.Import.Assign, - }; - var folderLog = new FolderLog() - { - Object = folder, - Time = LocalDateTime.FromDateTime(DateTime.Now), - User = performingUser!, - UserId = performingUser!.Id, - Action = FolderLogMessage.AssignDocument, - }; - var result = _context.Documents.Update(document); - await _context.DocumentLogs.AddAsync(log, cancellationToken); - await _context.FolderLogs.AddAsync(folderLog, cancellationToken); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } - } -} \ No newline at end of file diff --git a/src/Application/Documents/Commands/CheckinDocument.cs b/src/Application/Documents/Commands/CheckinDocument.cs deleted file mode 100644 index 73351023..00000000 --- a/src/Application/Documents/Commands/CheckinDocument.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Messages; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using Domain.Entities.Logging; -using Domain.Entities.Physical; -using Domain.Statuses; -using MediatR; -using Microsoft.EntityFrameworkCore; -using NodaTime; - -namespace Application.Documents.Commands; - -public class CheckinDocument -{ - public record Command : IRequest - { - public Guid PerformingUserId { get; init; } - public Guid DocumentId { get; init; } - } - - public class CommandHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public CommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - var document = await _context.Documents - .Include(x => x.Department) - .Include(x => x.Folder) - .FirstOrDefaultAsync(x => x.Id == request.DocumentId, cancellationToken); - - if (document is null) - { - throw new KeyNotFoundException("Document does not exist."); - } - - if (document.Status is not DocumentStatus.Approved) - { - throw new ConflictException("Request cannot be checked in."); - } - - if (document.Folder is null) - { - throw new ConflictException("Request cannot be checked in."); - } - - var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); - document.Status = DocumentStatus.Available; - document.LastModified = LocalDateTime.FromDateTime(DateTime.Now); - document.LastModifiedBy = performingUser!.Id; - var log = new DocumentLog() - { - User = performingUser, - UserId = performingUser.Id, - Object = document, - Time = LocalDateTime.FromDateTime(DateTime.Now), - Action = DocumentLogMessages.Import.Checkin, - }; - - var result = _context.Documents.Update(document); - await _context.DocumentLogs.AddAsync(log, cancellationToken); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } - } -} \ No newline at end of file diff --git a/src/Application/Documents/Commands/RejectDocument.cs b/src/Application/Documents/Commands/RejectDocument.cs deleted file mode 100644 index d58bb281..00000000 --- a/src/Application/Documents/Commands/RejectDocument.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Messages; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using Domain.Entities.Logging; -using Domain.Statuses; -using MediatR; -using Microsoft.EntityFrameworkCore; -using NodaTime; - -namespace Application.Documents.Commands; - -public class RejectDocument -{ - public record Command : IRequest - { - public Guid PerformingUserId { get; init; } - public Guid DocumentId { get; init; } - public string Reason { get; init; } = null!; - } - - public class CommandHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public CommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - var document = await _context.Documents - .Include(x => x.Department) - .FirstOrDefaultAsync(x => - x.Id == request.DocumentId, cancellationToken); - if (document is null) - { - throw new ConflictException("Document does not exist."); - } - - if (document.Status is not DocumentStatus.Issued) - { - throw new ConflictException("Request cannot be rejected."); - } - - document.Status = DocumentStatus.Rejected; - var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); - var log = new DocumentLog() - { - Object = document, - Time = LocalDateTime.FromDateTime(DateTime.Now), - User = performingUser!, - UserId = performingUser!.Id, - Action = DocumentLogMessages.Import.Reject, - }; - var requestLog = new RequestLog() - { - Object = document, - Time = LocalDateTime.FromDateTime(DateTime.Now), - User = performingUser, - UserId = performingUser.Id, - Action = RequestLogMessages.RejectImport, - Reason = request.Reason, - }; - var result = _context.Documents.Update(document); - await _context.DocumentLogs.AddAsync(log, cancellationToken); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } - } -} \ No newline at end of file diff --git a/src/Application/Documents/Commands/ShareDocument.cs b/src/Application/Documents/Commands/ShareDocument.cs index 3dbbfe08..8e01b4e3 100644 --- a/src/Application/Documents/Commands/ShareDocument.cs +++ b/src/Application/Documents/Commands/ShareDocument.cs @@ -1,36 +1,46 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; -using Application.Common.Models; +using Application.Common.Messages; +using Application.Common.Models.Dtos.Physical; using Application.Common.Models.Operations; +using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; +using Domain.Entities.Physical; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Documents.Commands; public class ShareDocument { - public record Command : IRequest + public record Command : IRequest { - public Guid PerformingUserId { get; init; } + public User CurrentUser { get; init; } = null!; public Guid DocumentId { get; init; } - public Guid[] UserIds { get; init; } = null!; + public Guid UserId { get; init; } public bool CanRead { get; init; } public bool CanBorrow { get; init; } public DateTime ExpiryDate { get; init; } } - public class CommandHandler : IRequestHandler + public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _applicationDbContext; + private readonly IMapper _mapper; private readonly IPermissionManager _permissionManager; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext applicationDbContext, IPermissionManager permissionManager) + public CommandHandler(IApplicationDbContext applicationDbContext, IMapper mapper, IPermissionManager permissionManager, IDateTimeProvider dateTimeProvider) { - _permissionManager = permissionManager; _applicationDbContext = applicationDbContext; + _mapper = mapper; + _permissionManager = permissionManager; + _dateTimeProvider = dateTimeProvider; } - public async Task Handle(Command request, CancellationToken cancellationToken) + public async Task Handle(Command request, CancellationToken cancellationToken) { var document = await _applicationDbContext.Documents @@ -41,40 +51,102 @@ public async Task Handle(Command request, CancellationToken cancellationTo throw new KeyNotFoundException("Document does not exist."); } - if (document.Importer!.Id != request.PerformingUserId) + if (document.Importer!.Id != request.CurrentUser.Id) { throw new UnauthorizedAccessException("You are not the owner of the document."); } + var user = await _applicationDbContext.Users.FirstOrDefaultAsync(x => x.Id == request.UserId, cancellationToken); + + if (user is null) + { + throw new KeyNotFoundException("User does not exist."); + } + if (request.ExpiryDate.ToUniversalTime() < DateTime.UtcNow) { throw new ConflictException("Expiry date cannot be in the past."); } - var users = _applicationDbContext.Users - .Where(x => request.UserIds.Contains(x.Id)) - .ToList(); - users.RemoveAll(x => x.Id == request.PerformingUserId); + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); - if (request.CanRead) + var log = new DocumentLog() { - await _permissionManager.GrantAsync(document, DocumentOperation.Read, users.ToArray(), request.ExpiryDate.ToLocalTime(), cancellationToken); - } - else + Object = document, + Time = localDateTimeNow, + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + Action = string.Empty, + }; + + await HandlePermissionGrantOrRevoke(request.CanRead, document, DocumentOperation.Read, user, request.ExpiryDate.ToLocalTime(), cancellationToken, log); + await HandlePermissionGrantOrRevoke(request.CanBorrow, document, DocumentOperation.Borrow, user, request.ExpiryDate.ToLocalTime(), cancellationToken, log); + + if (!string.IsNullOrEmpty(log.Action)) { - await _permissionManager.RevokeAsync(document.Id, DocumentOperation.Read, request.UserIds, cancellationToken); + await _applicationDbContext.DocumentLogs.AddAsync(log, cancellationToken); } + await _applicationDbContext.SaveChangesAsync(cancellationToken); + return _mapper.Map(document); + } + + private async Task HandlePermissionGrantOrRevoke( + bool canPerformAction, + Document document, + DocumentOperation operation, + User user, + DateTime expiryDate, + CancellationToken cancellationToken, + DocumentLog log) + { + var isGranted = _permissionManager.IsGranted(document.Id, operation, user.Id); - if (request.CanBorrow) + if (canPerformAction && !isGranted) { - await _permissionManager.GrantAsync(document, DocumentOperation.Borrow, users.ToArray(), request.ExpiryDate.ToLocalTime(), cancellationToken); + await GrantPermission(document, operation, user, expiryDate, log, cancellationToken); } - else + + if (!canPerformAction && isGranted) { - await _permissionManager.RevokeAsync(document.Id, DocumentOperation.Borrow, request.UserIds, cancellationToken); + await RevokePermission(document, operation, user, log, cancellationToken); } + } - return true; + private async Task GrantPermission( + Document document, + DocumentOperation operation, + User user, + DateTime expiryDate, + DocumentLog log, + CancellationToken cancellationToken) + { + await _permissionManager.GrantAsync(document, operation, new[] { user }, expiryDate, cancellationToken); + + // log + log.Action = operation switch + { + DocumentOperation.Read => DocumentLogMessages.GrantRead(user.Username), + DocumentOperation.Borrow => DocumentLogMessages.GrantBorrow(user.Username), + _ => log.Action + }; + } + + private async Task RevokePermission( + Document document, + DocumentOperation operation, + User user, + DocumentLog log, + CancellationToken cancellationToken) + { + await _permissionManager.RevokeAsync(document.Id, operation, new[] { user.Id }, cancellationToken); + + // log + log.Action = operation switch + { + DocumentOperation.Read => DocumentLogMessages.RevokeRead(user.Username), + DocumentOperation.Borrow => DocumentLogMessages.RevokeBorrow(user.Username), + _ => log.Action + }; } } } \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetDocumentReason.cs b/src/Application/Documents/Queries/GetDocumentReason.cs deleted file mode 100644 index f88e1190..00000000 --- a/src/Application/Documents/Queries/GetDocumentReason.cs +++ /dev/null @@ -1,87 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Models.Dtos; -using Application.Common.Models.Dtos.Physical; -using Application.Identity; -using AutoMapper; -using Domain.Entities; -using Domain.Entities.Logging; -using Domain.Enums; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Documents.Queries; - -public class GetDocumentReason -{ - public record Query : IRequest - { - public User CurrentUser { get; init; } - public Guid DocumentId { get; init; } - public RequestType Type { get; init; } - } - - public class QueryHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public QueryHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - var log = await _context.RequestLogs - .Include(x => x.Object) - .ThenInclude(y => y.Department) - .Include(x => x.Object) - .ThenInclude(y => y.Importer) - .FirstOrDefaultAsync(x => x.Object!.Id == request.DocumentId - && x.Type == request.Type, cancellationToken); - - if (log is null) - { - throw new KeyNotFoundException("Import request does not exist."); - } - - await EnforceRoleConstraintsAsync(request.CurrentUser, log); - - return _mapper.Map(log); - } - - private async Task EnforceRoleConstraintsAsync(User user, RequestLog log) - { - var role = user.Role; - - switch (role) - { - case IdentityData.Roles.Staff when log.Object!.Department!.Id != user.Department!.Id: - throw new ConflictException("Staff cannot access this request."); - case IdentityData.Roles.Employee when log.Type == RequestType.Import: - { - if (log.Object!.Importer!.Id != user.Id) - { - throw new ConflictException("User cannot access this request."); - } - - break; - } - case IdentityData.Roles.Employee: - { - var borrow = await _context.Borrows.FirstOrDefaultAsync(x => - x.Borrower.Id == user.Id && x.Document.Id == log.Object!.Id); - - if (borrow is null) - { - throw new ConflictException("User cannot access this request."); - } - - break; - } - } - } - } -} \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetPermissions.cs b/src/Application/Documents/Queries/GetPermissions.cs index ed9588a2..7413e097 100644 --- a/src/Application/Documents/Queries/GetPermissions.cs +++ b/src/Application/Documents/Queries/GetPermissions.cs @@ -1,8 +1,6 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; -using Application.Identity; -using Application.Users.Queries; using AutoMapper; using Domain.Entities; using Domain.Entities.Physical; @@ -15,7 +13,7 @@ public class GetPermissions { public record Query : IRequest { - public User PerformingUser { get; init; } = null!; + public User CurrentUser { get; init; } = null!; public Guid DocumentId { get; init; } } @@ -32,46 +30,55 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task Handle(Query request, CancellationToken cancellationToken) { - var document = await _context.Documents - .Include(x => x.Importer) - .FirstOrDefaultAsync(x => x.Id == request.DocumentId, cancellationToken); + var document = await GetDocumentWithImporter(request.DocumentId, cancellationToken); if (document is null) { throw new ConflictException("Document does not exist."); } - if (IsOwner(request.PerformingUser.Id, document)) + if (IsOwner(request.CurrentUser.Id, document)) { - return new PermissionDto() - { - DocumentId = document.Id, - EmployeeId = request.PerformingUser.Id, - CanRead = true, - CanBorrow = true, - }; + return CreatePermissionDto(document.Id, request.CurrentUser.Id, true, true); } - var permission = await _context.Permissions.FirstOrDefaultAsync( - x => x.DocumentId == request.DocumentId && x.EmployeeId == request.PerformingUser.Id, - cancellationToken); + var permission = await GetPermission(request.DocumentId, request.CurrentUser.Id, cancellationToken); if (permission is null) { - return new PermissionDto() - { - DocumentId = document.Id, - EmployeeId = request.PerformingUser.Id, - CanRead = false, - CanBorrow = false, - }; + return CreatePermissionDto(document.Id, request.CurrentUser.Id, false, false); } return _mapper.Map(permission); } + private async Task GetDocumentWithImporter(Guid documentId, CancellationToken cancellationToken) + { + return await _context.Documents + .Include(x => x.Importer) + .FirstOrDefaultAsync(x => x.Id == documentId, cancellationToken); + } + private static bool IsOwner(Guid userId, Document document) { return document.Importer!.Id == userId; } + + private async Task GetPermission(Guid documentId, Guid employeeId, CancellationToken cancellationToken) + { + return await _context.Permissions.FirstOrDefaultAsync( + x => x!.DocumentId == documentId && x.EmployeeId == employeeId, + cancellationToken); + } + + private static PermissionDto CreatePermissionDto(Guid documentId, Guid employeeId, bool canRead, bool canBorrow) + { + return new PermissionDto() + { + DocumentId = documentId, + EmployeeId = employeeId, + CanRead = canRead, + CanBorrow = canBorrow, + }; + } } } \ No newline at end of file diff --git a/src/Application/Documents/Commands/ApproveDocument.cs b/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs similarity index 54% rename from src/Application/Documents/Commands/ApproveDocument.cs rename to src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs index cf39a4cf..9e591b0e 100644 --- a/src/Application/Documents/Commands/ApproveDocument.cs +++ b/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs @@ -1,23 +1,33 @@ using Application.Common.Exceptions; +using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Messages; using Application.Common.Models.Dtos.ImportDocument; -using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities; using Domain.Entities.Logging; -using Domain.Entities.Physical; using Domain.Statuses; +using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; using NodaTime; -namespace Application.Documents.Commands; +namespace Application.ImportRequests.Commands; -public class ApproveDocument +public class ApproveOrRejectDocument { - public record Command : IRequest + public class Validator : AbstractValidator + { + public Validator() + { + RuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(x => x.Decision) + .Must(x => x.IsApproval() || x.IsRejection()).WithMessage("Decision is not valid."); + } + } + + public record Command : IRequest { public User CurrentUser { get; init; } = null!; public Guid ImportRequestId { get; init; } @@ -25,84 +35,84 @@ public record Command : IRequest public string Reason { get; init; } = null!; } - public class CommandHandler : IRequestHandler + public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } - public async Task Handle(Command request, CancellationToken cancellationToken) + public async Task Handle(Command request, CancellationToken cancellationToken) { var importRequest = await _context.ImportRequests .Include(x => x.Document) .Include(x => x.Room) - .FirstOrDefaultAsync(x => x.Id == request.ImportRequestId, cancellationToken); + .FirstOrDefaultAsync(x => x.Id == request.ImportRequestId + && x.Status == ImportRequestStatus.Pending, cancellationToken); if (importRequest is null) { throw new KeyNotFoundException("Import request does not exist."); } - + var document = await _context.Documents .Include(x => x.Department) - .FirstOrDefaultAsync(x => - x.Id == importRequest.Document.Id, cancellationToken); + .FirstOrDefaultAsync(x => x.Id == importRequest.Document.Id + && x.Status == DocumentStatus.Issued, cancellationToken); if (document is null) { throw new ConflictException("Document does not exist."); } - if (document.Status is not DocumentStatus.Issued - && importRequest.Status is not ImportRequestStatus.Issued) - { - throw new ConflictException("Request cannot be approved or rejected."); - } + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); - if (IsApproval(request.Decision)) - { - document.Status = DocumentStatus.Approved; - importRequest.Status = ImportRequestStatus.Approved; - } - - if (IsRejection(request.Decision)) - { - document.Status = DocumentStatus.Rejected; - importRequest.Status = ImportRequestStatus.Rejected; - } - var log = new DocumentLog() { Object = document, - Time = LocalDateTime.FromDateTime(DateTime.Now), + Time = localDateTimeNow, User = request.CurrentUser, UserId = request.CurrentUser.Id, Action = DocumentLogMessages.Import.Approve, }; + var requestLog = new RequestLog() { Object = document, - Time = LocalDateTime.FromDateTime(DateTime.Now), + Time = localDateTimeNow, User = request.CurrentUser, UserId = request.CurrentUser.Id, Action = RequestLogMessages.ApproveImport, - Reason = request.Reason, }; - var result = _context.Documents.Update(document); + + if (request.Decision.IsApproval()) + { + importRequest.Status = ImportRequestStatus.Approved; + log.Action = DocumentLogMessages.Import.Approve; + requestLog.Action = RequestLogMessages.ApproveImport; + } + + if (request.Decision.IsRejection()) + { + importRequest.Status = ImportRequestStatus.Rejected; + log.Action = DocumentLogMessages.Import.Reject; + requestLog.Action = RequestLogMessages.RejectImport; + } + + importRequest.Reason = request.Reason; + importRequest.LastModified = localDateTimeNow; + importRequest.LastModifiedBy = request.CurrentUser.Id; + + var result = _context.ImportRequests.Update(importRequest); await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.RequestLogs.AddAsync(requestLog, cancellationToken); await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); + return _mapper.Map(result.Entity); } - - private static bool IsApproval(string decision) - => decision.ToLower().Trim().Equals("approve"); - - private static bool IsRejection(string decision) - => decision.ToLower().Trim().Equals("reject"); } } \ No newline at end of file diff --git a/src/Application/ImportRequests/Commands/AssignDocument.cs b/src/Application/ImportRequests/Commands/AssignDocument.cs new file mode 100644 index 00000000..c32bc2a2 --- /dev/null +++ b/src/Application/ImportRequests/Commands/AssignDocument.cs @@ -0,0 +1,104 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Messages; +using Application.Common.Models.Dtos.ImportDocument; +using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; +using Domain.Statuses; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.ImportRequests.Commands; + +public class AssignDocument +{ + public record Command : IRequest + { + public User CurrentUser { get; init; } = null!; + public Guid? StaffRoomId { get; init; } + public Guid ImportRequestId { get; init; } + public Guid FolderId { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; + + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + { + _context = context; + _mapper = mapper; + _dateTimeProvider = dateTimeProvider; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + + var importRequest = await _context.ImportRequests + .Include(x => x.Document) + .Include(x => x.Room) + .FirstOrDefaultAsync(x => x.Id == request.ImportRequestId, cancellationToken); + + if (importRequest is null) + { + throw new KeyNotFoundException("Import request does not exist."); + } + + if (request.StaffRoomId is null || importRequest.RoomId != request.StaffRoomId.Value) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + + if (importRequest.Status is not ImportRequestStatus.Approved) + { + throw new ConflictException("Request cannot be assigned."); + } + + var folder = await _context.Folders + .FirstOrDefaultAsync(x => x.Id == request.FolderId + && x.Locker.Room.Id == request.StaffRoomId, cancellationToken); + + if (folder is null) + { + throw new ConflictException("Folder does not exist."); + } + + if (folder.NumberOfDocuments >= folder.Capacity) + { + throw new ConflictException("This folder cannot accept more documents."); + } + + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + + importRequest.Document.Folder = folder; + importRequest.Document.LastModified = localDateTimeNow; + importRequest.Document.LastModifiedBy = request.CurrentUser.Id; + + var log = new DocumentLog() + { + Object = importRequest.Document, + Time = localDateTimeNow, + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + Action = DocumentLogMessages.Import.Assign, + }; + var folderLog = new FolderLog() + { + Object = folder, + Time = localDateTimeNow, + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + Action = FolderLogMessage.AssignDocument, + }; + _context.Documents.Update(importRequest.Document); + await _context.DocumentLogs.AddAsync(log, cancellationToken); + await _context.FolderLogs.AddAsync(folderLog, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(importRequest); + } + } +} \ No newline at end of file diff --git a/src/Application/ImportRequests/Commands/CheckinDocument.cs b/src/Application/ImportRequests/Commands/CheckinDocument.cs new file mode 100644 index 00000000..c72dbb16 --- /dev/null +++ b/src/Application/ImportRequests/Commands/CheckinDocument.cs @@ -0,0 +1,104 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Messages; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; +using Domain.Statuses; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.ImportRequests.Commands; + +public class CheckinDocument +{ + public record Command : IRequest + { + public User CurrentUser { get; init; } = null!; + public Guid DocumentId { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; + + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) + { + _context = context; + _mapper = mapper; + _dateTimeProvider = dateTimeProvider; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var document = await _context.Documents + .Include(x => x.Department) + .Include(x => x.Folder) + .FirstOrDefaultAsync(x => x.Id == request.DocumentId, cancellationToken); + + if (document is null) + { + throw new KeyNotFoundException("Document does not exist."); + } + + var importRequest = await _context.ImportRequests + .Include(x => x.Document) + .Include(x => x.Room) + .FirstOrDefaultAsync(x => x.DocumentId == request.DocumentId, cancellationToken); + + if (importRequest is null) + { + throw new ConflictException("This document does not have an import request."); + } + + if (StatusesAreNotValid(document.Status, importRequest.Status)) + { + throw new ConflictException("Request cannot be checked in."); + } + + if (document.Folder is null) + { + throw new ConflictException("Request cannot be checked in."); + } + + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + + document.Status = DocumentStatus.Available; + document.LastModified = localDateTimeNow; + document.LastModifiedBy = request.CurrentUser.Id; + importRequest.Status = ImportRequestStatus.CheckedIn; + importRequest.LastModified = localDateTimeNow; + importRequest.LastModifiedBy = request.CurrentUser.Id; + + var log = new DocumentLog() + { + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + Object = document, + Time = localDateTimeNow, + Action = DocumentLogMessages.Import.Checkin, + }; + var importLog = new RequestLog() + { + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + Object = document, + Time = localDateTimeNow, + Action = RequestLogMessages.CheckInImport, + }; + var result = _context.Documents.Update(document); + _context.ImportRequests.Update(importRequest); + await _context.DocumentLogs.AddAsync(log, cancellationToken); + await _context.RequestLogs.AddAsync(importLog, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + + private static bool StatusesAreNotValid(DocumentStatus documentStatus, ImportRequestStatus importRequestStatus) + => documentStatus is not DocumentStatus.Available || importRequestStatus is not ImportRequestStatus.Approved; + } +} \ No newline at end of file diff --git a/src/Application/Documents/Commands/RequestImportDocument.cs b/src/Application/ImportRequests/Commands/RequestImportDocument.cs similarity index 79% rename from src/Application/Documents/Commands/RequestImportDocument.cs rename to src/Application/ImportRequests/Commands/RequestImportDocument.cs index 84cd1309..f387b2df 100644 --- a/src/Application/Documents/Commands/RequestImportDocument.cs +++ b/src/Application/ImportRequests/Commands/RequestImportDocument.cs @@ -2,7 +2,6 @@ using Application.Common.Interfaces; using Application.Common.Messages; using Application.Common.Models.Dtos.ImportDocument; -using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities; using Domain.Entities.Logging; @@ -10,14 +9,13 @@ using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; using NodaTime; -namespace Application.Documents.Commands; +namespace Application.ImportRequests.Commands; public class RequestImportDocument { - public record Command : IRequest + public record Command : IRequest { public string Title { get; init; } = null!; public string? Description { get; init; } @@ -27,7 +25,7 @@ public record Command : IRequest public bool IsPrivate { get; init; } } - public class CommandHandler : IRequestHandler + public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; @@ -40,7 +38,7 @@ public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimePr _dateTimeProvider = dateTimeProvider; } - public async Task Handle(Command request, CancellationToken cancellationToken) + public async Task Handle(Command request, CancellationToken cancellationToken) { var document = _context.Documents.FirstOrDefault(x => x.Title.Trim().ToLower().Equals(request.Title.Trim().ToLower()) @@ -50,7 +48,8 @@ public async Task Handle(Command request, CancellationToken c throw new ConflictException($"Document title already exists for user {request.Issuer.FirstName}."); } - var room = await _context.Rooms.FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); + var room = await _context.Rooms + .FirstOrDefaultAsync(x => x.Id == request.RoomId && x.IsAvailable, cancellationToken); if (room is null) { @@ -75,7 +74,7 @@ public async Task Handle(Command request, CancellationToken c var importRequest = new ImportRequest() { Document = entity, - Status = ImportRequestStatus.Issued, + Status = ImportRequestStatus.Pending, Room = room, Created = localDateTimeNow, CreatedBy = request.Issuer.Id @@ -89,11 +88,11 @@ public async Task Handle(Command request, CancellationToken c UserId = request.Issuer.Id, Action = DocumentLogMessages.Import.NewImportRequest, }; - var result = await _context.Documents.AddAsync(entity, cancellationToken); - await _context.ImportRequests.AddAsync(importRequest, cancellationToken); + await _context.Documents.AddAsync(entity, cancellationToken); + var result = await _context.ImportRequests.AddAsync(importRequest, cancellationToken); await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); + return _mapper.Map(result.Entity); } } } \ No newline at end of file diff --git a/src/Application/ImportRequests/Queries/GetAllImportRequestsPaginated.cs b/src/Application/ImportRequests/Queries/GetAllImportRequestsPaginated.cs new file mode 100644 index 00000000..acf14faa --- /dev/null +++ b/src/Application/ImportRequests/Queries/GetAllImportRequestsPaginated.cs @@ -0,0 +1,91 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models; +using Application.Common.Models.Dtos.ImportDocument; +using AutoMapper; +using Domain.Entities; +using Domain.Entities.Physical; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.ImportRequests.Queries; + +public class GetAllImportRequestsPaginated +{ + public record Query : IRequest> + { + public User CurrentUser { get; init; } = null!; + public Guid? RoomId { get; init; } + public string? SearchTerm { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } + } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, + CancellationToken cancellationToken) + { + if (request.CurrentUser.Role.IsStaff() + && request.RoomId is null) + { + throw new UnauthorizedAccessException("User can not access this resource."); + } + + if (request.CurrentUser.Role.IsEmployee()) + { + var room = await _context.Rooms + .FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); + var roomDoesNotExist = room is null; + + if (roomDoesNotExist + || RoomIsNotInSameDepartment(request.CurrentUser, room!)) + { + throw new UnauthorizedAccessException("User can not access this resource."); + } + } + + var importRequests = _context.ImportRequests + .Include(x => x.Document) + .Include(x => x.Room) + .ThenInclude(x => x.Department) + .AsQueryable(); + + if (request.RoomId is not null) + { + importRequests = importRequests.Where(x => x.RoomId == request.RoomId); + } + + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + importRequests = importRequests.Where(x => + x.Document.Title.ToLower().Contains(request.SearchTerm.ToLower())); + } + + + return await importRequests + .ListPaginateWithSortAsync( + request.Page, + request.Size, + request.SortBy, + request.SortOrder, + _mapper.ConfigurationProvider, + cancellationToken); + } + + + private static bool RoomIsNotInSameDepartment(User user, Room room) + => user.Department?.Id != room.DepartmentId; + } +} \ No newline at end of file diff --git a/src/Application/ImportRequests/Queries/GetImportRequestById.cs b/src/Application/ImportRequests/Queries/GetImportRequestById.cs new file mode 100644 index 00000000..625400cd --- /dev/null +++ b/src/Application/ImportRequests/Queries/GetImportRequestById.cs @@ -0,0 +1,57 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Models.Dtos.ImportDocument; +using AutoMapper; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.ImportRequests.Queries; + +public class GetImportRequestById { + public record Query : IRequest + { + public Guid CurrentUserId { get; init; } + public string CurrentUserRole { get; init; } = null!; + public Guid? CurrentStaffRoomId { get; init; } + public Guid RequestId { get; init; } + } + + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var importRequest = await _context.ImportRequests + .Include(x => x.Document) + .Include(x => x.Room) + .FirstOrDefaultAsync(x => x.Id.Equals(request.RequestId), cancellationToken); + + if (importRequest is null) + { + throw new KeyNotFoundException("Import request does not exist."); + } + + if (request.CurrentUserRole.IsStaff() + && (request.CurrentStaffRoomId is null || importRequest.Room.Id != request.CurrentStaffRoomId)) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + + if (request.CurrentUserRole.IsEmployee() + && importRequest.Document.ImporterId != request.CurrentUserId) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + + return _mapper.Map(importRequest); + } + } +} \ No newline at end of file diff --git a/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs b/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs index 8d8e4922..eb52e7b6 100644 --- a/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs +++ b/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs @@ -3,6 +3,7 @@ using Application.Common.Models; using Application.Common.Models.Dtos.Logging; using AutoMapper; +using Domain.Entities; using Domain.Entities.Logging; using Domain.Entities.Physical; using MediatR; @@ -14,7 +15,10 @@ public class GetAllLockerLogsPaginated { public record Query : IRequest> { + public string CurrentUserRole { get; init; } = null!; + public Guid CurrentUserDepartmentId { get; init; } public string? SearchTerm { get; init; } + public Guid? RoomId { get; init; } public int? Page { get; init; } public int? Size { get; init; } } @@ -32,12 +36,38 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task> Handle(Query request, CancellationToken cancellationToken) { + + if (request.CurrentUserRole.IsStaff()) + { + if (request.RoomId is null) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + + var currentRoom = await GetRoomByDepartmentIdAsync(request.CurrentUserDepartmentId, cancellationToken); + + if (currentRoom is null) + { + throw new UnauthorizedAccessException("User cannot access this resource"); + } + + if (!IsSameRoom(currentRoom.Id, request.RoomId.Value)) + { + throw new UnauthorizedAccessException("User cannot access this resource"); + } + } + var logs = _context.LockerLogs .Include(x => x.Object) .Include(x => x.User) .ThenInclude(x => x.Department) .AsQueryable(); + if (request.RoomId is not null) + { + logs = logs.Where(x => x.Object == null || x.Object.Room.Id == request.RoomId); + } + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) { logs = logs.Where(x => @@ -51,5 +81,13 @@ public async Task> Handle(Query request, Cancellatio _mapper.ConfigurationProvider, cancellationToken); } + + private async Task GetRoomByDepartmentIdAsync(Guid departmentId, CancellationToken cancellationToken) + => await _context.Rooms.FirstOrDefaultAsync( + x => x.DepartmentId == departmentId, + cancellationToken); + + private static bool IsSameRoom(Guid roomId1, Guid roomId2) + => roomId1 == roomId2; } } diff --git a/src/Application/Lockers/Queries/GetLockerLogById.cs b/src/Application/Lockers/Queries/GetLockerLogById.cs index 33380268..5085797b 100644 --- a/src/Application/Lockers/Queries/GetLockerLogById.cs +++ b/src/Application/Lockers/Queries/GetLockerLogById.cs @@ -1,6 +1,8 @@ -using Application.Common.Interfaces; +using Application.Common.Extensions; +using Application.Common.Interfaces; using Application.Common.Models.Dtos.Logging; using AutoMapper; +using Domain.Entities.Logging; using MediatR; using Microsoft.EntityFrameworkCore; @@ -11,6 +13,8 @@ public class GetLockerLogById public record Query : IRequest { public Guid LogId { get; init; } + public string CurrentUserRole { get; init; } = null!; + public Guid? CurrentStaffRoomId { get; init; } } public class QueryHandler : IRequestHandler @@ -38,7 +42,18 @@ public async Task Handle(Query request, CancellationToken cancella throw new KeyNotFoundException("Log does not exist."); } + if (request.CurrentUserRole.IsStaff() + && (request.CurrentStaffRoomId is null || !LockerInSameRoom(log, request.CurrentStaffRoomId.Value))) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + return _mapper.Map(log); } + + private static bool LockerInSameRoom( + LockerLog log, + Guid roomId) + => log.BaseRoom!.Id == roomId; } } \ No newline at end of file diff --git a/src/Application/Rooms/Queries/GetAllRoomLogsPaginated.cs b/src/Application/Rooms/Queries/GetAllRoomLogsPaginated.cs index c3b7a501..9b3658b8 100644 --- a/src/Application/Rooms/Queries/GetAllRoomLogsPaginated.cs +++ b/src/Application/Rooms/Queries/GetAllRoomLogsPaginated.cs @@ -14,11 +14,10 @@ public class GetAllRoomLogsPaginated { public record Query : IRequest> { + public Guid? RoomId { get; init; } public string? SearchTerm { get; init; } public int? Page { get; init; } public int? Size { get; init; } - public string? SortBy { get; init; } - public string? SortOrder { get; init; } } public class QueryHandler : IRequestHandler> @@ -40,6 +39,11 @@ public async Task> Handle(Query request, CancellationT .ThenInclude(x => x.Department) .AsQueryable(); + if (request.RoomId is not null) + { + logs = logs.Where(x => x.Object!.Id == request.RoomId); + } + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) { logs = logs.Where(x => diff --git a/src/Application/Users/Queries/GetAllUserLogsPaginated.cs b/src/Application/Users/Queries/GetAllUserLogsPaginated.cs index 6d6d037c..6b459cac 100644 --- a/src/Application/Users/Queries/GetAllUserLogsPaginated.cs +++ b/src/Application/Users/Queries/GetAllUserLogsPaginated.cs @@ -14,6 +14,7 @@ public class GetAllUserLogsPaginated { public record Query : IRequest> { + public Guid? UserId { get; init; } public string? SearchTerm { get; init; } public int? Page { get; init; } public int? Size { get; init; } @@ -37,6 +38,11 @@ public async Task> Handle(Query request, CancellationT .Include(x => x.User) .ThenInclude(x => x.Department) .AsQueryable(); + + if (request.UserId is not null) + { + logs = logs.Where(x => x.Object!.Id == request.UserId); + } if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) { diff --git a/src/Domain/Entities/Logging/DocumentLog.cs b/src/Domain/Entities/Logging/DocumentLog.cs index 13425e10..ae9ecced 100644 --- a/src/Domain/Entities/Logging/DocumentLog.cs +++ b/src/Domain/Entities/Logging/DocumentLog.cs @@ -1,9 +1,9 @@ using Domain.Common; using Domain.Entities.Physical; -using NodaTime; namespace Domain.Entities.Logging; public class DocumentLog : BaseLoggingEntity { + public Folder? BaseFolder { get; set; } } \ No newline at end of file diff --git a/src/Domain/Entities/Logging/FolderLog.cs b/src/Domain/Entities/Logging/FolderLog.cs index fcefc9a2..0cfa7f0d 100644 --- a/src/Domain/Entities/Logging/FolderLog.cs +++ b/src/Domain/Entities/Logging/FolderLog.cs @@ -5,4 +5,5 @@ namespace Domain.Entities.Logging; public class FolderLog : BaseLoggingEntity { + public Locker? BaseLocker { get; set; } } \ No newline at end of file diff --git a/src/Domain/Entities/Logging/LockerLog.cs b/src/Domain/Entities/Logging/LockerLog.cs index b1354a29..3a0d309e 100644 --- a/src/Domain/Entities/Logging/LockerLog.cs +++ b/src/Domain/Entities/Logging/LockerLog.cs @@ -5,4 +5,5 @@ namespace Domain.Entities.Logging; public class LockerLog : BaseLoggingEntity { + public Room? BaseRoom { get; set; } } \ No newline at end of file diff --git a/src/Domain/Entities/Logging/RequestLog.cs b/src/Domain/Entities/Logging/RequestLog.cs index e9605ed3..fbf7d568 100644 --- a/src/Domain/Entities/Logging/RequestLog.cs +++ b/src/Domain/Entities/Logging/RequestLog.cs @@ -7,5 +7,4 @@ namespace Domain.Entities.Logging; public class RequestLog : BaseLoggingEntity { public RequestType Type { get; set; } - public string Reason { get; set; } = null!; } \ No newline at end of file diff --git a/src/Domain/Entities/Physical/ImportRequest.cs b/src/Domain/Entities/Physical/ImportRequest.cs index bf4c002e..bf6ad3a5 100644 --- a/src/Domain/Entities/Physical/ImportRequest.cs +++ b/src/Domain/Entities/Physical/ImportRequest.cs @@ -1,13 +1,15 @@ using Domain.Common; using Domain.Statuses; -using NodaTime; namespace Domain.Entities.Physical; public class ImportRequest : BaseAuditableEntity { - public Room Room { get; set; } = null!; - public Document Document { get; set; } = null!; + public Guid RoomId { get; set; } + public Guid DocumentId { get; set; } public string Reason { get; set; } = null!; public ImportRequestStatus Status { get; set; } + + public Room Room { get; set; } = null!; + public Document Document { get; set; } = null!; } \ No newline at end of file diff --git a/src/Domain/Statuses/DocumentStatus.cs b/src/Domain/Statuses/DocumentStatus.cs index 2e6bc23e..133c1cd1 100644 --- a/src/Domain/Statuses/DocumentStatus.cs +++ b/src/Domain/Statuses/DocumentStatus.cs @@ -3,8 +3,6 @@ namespace Domain.Statuses; public enum DocumentStatus { Issued, - Approved, - Rejected, Available, Borrowed, Lost, diff --git a/src/Domain/Statuses/ImportRequestStatus.cs b/src/Domain/Statuses/ImportRequestStatus.cs index 067b149c..65a65ffc 100644 --- a/src/Domain/Statuses/ImportRequestStatus.cs +++ b/src/Domain/Statuses/ImportRequestStatus.cs @@ -2,7 +2,7 @@ public enum ImportRequestStatus { - Issued, + Pending, Approved, Rejected, CheckedIn, diff --git a/src/Infrastructure/Persistence/ApplicationDbContext.cs b/src/Infrastructure/Persistence/ApplicationDbContext.cs index 237cd38f..c842828d 100644 --- a/src/Infrastructure/Persistence/ApplicationDbContext.cs +++ b/src/Infrastructure/Persistence/ApplicationDbContext.cs @@ -29,7 +29,7 @@ public ApplicationDbContext( public DbSet Documents => Set(); public DbSet ImportRequests => Set(); public DbSet Borrows => Set(); - public DbSet Permissions => Set(); + public DbSet Permissions => Set(); public DbSet UserGroups => Set(); public DbSet Files => Set(); diff --git a/src/Infrastructure/Persistence/Configurations/ImportRequestConfiguration.cs b/src/Infrastructure/Persistence/Configurations/ImportRequestConfiguration.cs index 6c8fcf7d..5c53becd 100644 --- a/src/Infrastructure/Persistence/Configurations/ImportRequestConfiguration.cs +++ b/src/Infrastructure/Persistence/Configurations/ImportRequestConfiguration.cs @@ -9,18 +9,17 @@ public class ImportRequestConfiguration : IEntityTypeConfiguration builder) { builder.HasKey(x => x.Id); - builder.Property(x => x.Id) .ValueGeneratedOnAdd(); builder.HasOne(x => x.Document) - .WithMany() - .HasForeignKey("DocumentId") + .WithOne() + .HasForeignKey(x => x.DocumentId) .IsRequired(); builder.HasOne(x => x.Room) .WithMany() - .HasForeignKey("RoomId") + .HasForeignKey(x => x.RoomId) .IsRequired(); builder.Property(x => x.Reason) @@ -28,5 +27,17 @@ public void Configure(EntityTypeBuilder builder) builder.Property(x => x.Status) .IsRequired(); + + builder.Property(x => x.Created) + .IsRequired(); + + builder.Property(x => x.CreatedBy) + .IsRequired(false); + + builder.Property(x => x.LastModified) + .IsRequired(false); + + builder.Property(x => x.LastModifiedBy) + .IsRequired(false); } } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Migrations/20230616101721_DepartmentHasManyRooms.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230616101721_DepartmentHasManyRooms.Designer.cs new file mode 100644 index 00000000..aa6b5101 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230616101721_DepartmentHasManyRooms.Designer.cs @@ -0,0 +1,1021 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230616101721_DepartmentHasManyRooms")] + partial class DepartmentHasManyRooms + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.HasOne("Domain.Entities.User", "Object") + .WithMany() + .HasForeignKey("ObjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Entry"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany("Rooms") + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230616101721_DepartmentHasManyRooms.cs b/src/Infrastructure/Persistence/Migrations/20230616101721_DepartmentHasManyRooms.cs new file mode 100644 index 00000000..4d7f402c --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230616101721_DepartmentHasManyRooms.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class DepartmentHasManyRooms : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Rooms_DepartmentId", + table: "Rooms"); + + migrationBuilder.CreateIndex( + name: "IX_Rooms_DepartmentId", + table: "Rooms", + column: "DepartmentId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Rooms_DepartmentId", + table: "Rooms"); + + migrationBuilder.CreateIndex( + name: "IX_Rooms_DepartmentId", + table: "Rooms", + column: "DepartmentId", + unique: true); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230618133410_LoggingNowHasBaseObject.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230618133410_LoggingNowHasBaseObject.Designer.cs new file mode 100644 index 00000000..531983a1 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230618133410_LoggingNowHasBaseObject.Designer.cs @@ -0,0 +1,1110 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230618133410_LoggingNowHasBaseObject")] + partial class LoggingNowHasBaseObject + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseFolderId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseFolderId"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseLockerId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseLockerId"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseRoomId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseRoomId"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId") + .IsUnique(); + + b.HasIndex("RoomId"); + + b.ToTable("ImportRequests"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "BaseFolder") + .WithMany() + .HasForeignKey("BaseFolderId"); + + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseFolder"); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "BaseLocker") + .WithMany() + .HasForeignKey("BaseLockerId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseLocker"); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "BaseRoom") + .WithMany() + .HasForeignKey("BaseRoomId"); + + b.HasOne("Domain.Entities.Physical.Locker", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseRoom"); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.HasOne("Domain.Entities.User", "Object") + .WithMany() + .HasForeignKey("ObjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Entry"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.ImportRequest", "DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany() + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany("Rooms") + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230618133410_LoggingNowHasBaseObject.cs b/src/Infrastructure/Persistence/Migrations/20230618133410_LoggingNowHasBaseObject.cs new file mode 100644 index 00000000..64028226 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230618133410_LoggingNowHasBaseObject.cs @@ -0,0 +1,139 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class LoggingNowHasBaseObject : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_ImportRequests_DocumentId", + table: "ImportRequests"); + + migrationBuilder.DropColumn( + name: "Reason", + table: "RequestLogs"); + + migrationBuilder.AddColumn( + name: "BaseRoomId", + table: "LockerLogs", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "BaseLockerId", + table: "FolderLogs", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "BaseFolderId", + table: "DocumentLogs", + type: "uuid", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_LockerLogs_BaseRoomId", + table: "LockerLogs", + column: "BaseRoomId"); + + migrationBuilder.CreateIndex( + name: "IX_ImportRequests_DocumentId", + table: "ImportRequests", + column: "DocumentId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_FolderLogs_BaseLockerId", + table: "FolderLogs", + column: "BaseLockerId"); + + migrationBuilder.CreateIndex( + name: "IX_DocumentLogs_BaseFolderId", + table: "DocumentLogs", + column: "BaseFolderId"); + + migrationBuilder.AddForeignKey( + name: "FK_DocumentLogs_Folders_BaseFolderId", + table: "DocumentLogs", + column: "BaseFolderId", + principalTable: "Folders", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_FolderLogs_Lockers_BaseLockerId", + table: "FolderLogs", + column: "BaseLockerId", + principalTable: "Lockers", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_LockerLogs_Rooms_BaseRoomId", + table: "LockerLogs", + column: "BaseRoomId", + principalTable: "Rooms", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_DocumentLogs_Folders_BaseFolderId", + table: "DocumentLogs"); + + migrationBuilder.DropForeignKey( + name: "FK_FolderLogs_Lockers_BaseLockerId", + table: "FolderLogs"); + + migrationBuilder.DropForeignKey( + name: "FK_LockerLogs_Rooms_BaseRoomId", + table: "LockerLogs"); + + migrationBuilder.DropIndex( + name: "IX_LockerLogs_BaseRoomId", + table: "LockerLogs"); + + migrationBuilder.DropIndex( + name: "IX_ImportRequests_DocumentId", + table: "ImportRequests"); + + migrationBuilder.DropIndex( + name: "IX_FolderLogs_BaseLockerId", + table: "FolderLogs"); + + migrationBuilder.DropIndex( + name: "IX_DocumentLogs_BaseFolderId", + table: "DocumentLogs"); + + migrationBuilder.DropColumn( + name: "BaseRoomId", + table: "LockerLogs"); + + migrationBuilder.DropColumn( + name: "BaseLockerId", + table: "FolderLogs"); + + migrationBuilder.DropColumn( + name: "BaseFolderId", + table: "DocumentLogs"); + + migrationBuilder.AddColumn( + name: "Reason", + table: "RequestLogs", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.CreateIndex( + name: "IX_ImportRequests_DocumentId", + table: "ImportRequests", + column: "DocumentId"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index f2809a40..46bffd08 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -114,6 +114,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); + b.Property("BaseFolderId") + .HasColumnType("uuid"); + b.Property("ObjectId") .HasColumnType("uuid"); @@ -125,6 +128,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("BaseFolderId"); + b.HasIndex("ObjectId"); b.HasIndex("UserId"); @@ -142,6 +147,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); + b.Property("BaseLockerId") + .HasColumnType("uuid"); + b.Property("ObjectId") .HasColumnType("uuid"); @@ -153,6 +161,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("BaseLockerId"); + b.HasIndex("ObjectId"); b.HasIndex("UserId"); @@ -170,6 +180,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); + b.Property("BaseRoomId") + .HasColumnType("uuid"); + b.Property("ObjectId") .HasColumnType("uuid"); @@ -181,6 +194,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("BaseRoomId"); + b.HasIndex("ObjectId"); b.HasIndex("UserId"); @@ -201,10 +216,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ObjectId") .HasColumnType("uuid"); - b.Property("Reason") - .IsRequired() - .HasColumnType("text"); - b.Property("Time") .HasColumnType("timestamp without time zone"); @@ -471,7 +482,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.HasIndex("DocumentId"); + b.HasIndex("DocumentId") + .IsUnique(); b.HasIndex("RoomId"); @@ -768,6 +780,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => { + b.HasOne("Domain.Entities.Physical.Folder", "BaseFolder") + .WithMany() + .HasForeignKey("BaseFolderId"); + b.HasOne("Domain.Entities.Physical.Document", "Object") .WithMany() .HasForeignKey("ObjectId"); @@ -778,6 +794,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.Navigation("BaseFolder"); + b.Navigation("Object"); b.Navigation("User"); @@ -785,6 +803,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => { + b.HasOne("Domain.Entities.Physical.Locker", "BaseLocker") + .WithMany() + .HasForeignKey("BaseLockerId"); + b.HasOne("Domain.Entities.Physical.Folder", "Object") .WithMany() .HasForeignKey("ObjectId"); @@ -795,6 +817,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.Navigation("BaseLocker"); + b.Navigation("Object"); b.Navigation("User"); @@ -802,6 +826,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => { + b.HasOne("Domain.Entities.Physical.Room", "BaseRoom") + .WithMany() + .HasForeignKey("BaseRoomId"); + b.HasOne("Domain.Entities.Physical.Locker", "Object") .WithMany() .HasForeignKey("ObjectId"); @@ -812,6 +840,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.Navigation("BaseRoom"); + b.Navigation("Object"); b.Navigation("User"); @@ -930,8 +960,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => { b.HasOne("Domain.Entities.Physical.Document", "Document") - .WithMany() - .HasForeignKey("DocumentId") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.ImportRequest", "DocumentId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); From f1585efcba17454de19fef85e5350cbc23ff216e Mon Sep 17 00:00:00 2001 From: Vzart Date: Tue, 20 Jun 2023 11:14:07 +0700 Subject: [PATCH 47/56] borrow works now --- src/Api/Controllers/BorrowsController.cs | 23 +------ src/Api/Controllers/DocumentsController.cs | 20 ------- src/Api/Controllers/FoldersController.cs | 21 ------- src/Api/Controllers/LockersController.cs | 20 ------- src/Api/Controllers/StaffsController.cs | 2 +- src/Api/Controllers/UsersController.cs | 21 ------- .../Borrows/Commands/ApproveBorrowRequest.cs | 26 ++++---- .../Borrows/Commands/BorrowDocument.cs | 60 +++++++++++-------- .../Borrows/Commands/UpdateBorrow.cs | 27 +++++---- .../Borrows/Queries/GetRequestLogById.cs | 48 --------------- .../Documents/Queries/GetLogOfDocumentById.cs | 43 ------------- src/Application/Folders/Commands/AddFolder.cs | 4 +- .../Folders/Queries/GetFolderLogById.cs | 43 ------------- src/Application/Lockers/Commands/AddLocker.cs | 4 +- .../Lockers/Queries/GetLockerLogById.cs | 59 ------------------ .../Users/Queries/GetUserLogById.cs | 43 ------------- 16 files changed, 68 insertions(+), 396 deletions(-) delete mode 100644 src/Application/Borrows/Queries/GetRequestLogById.cs delete mode 100644 src/Application/Documents/Queries/GetLogOfDocumentById.cs delete mode 100644 src/Application/Folders/Queries/GetFolderLogById.cs delete mode 100644 src/Application/Lockers/Queries/GetLockerLogById.cs delete mode 100644 src/Application/Users/Queries/GetUserLogById.cs diff --git a/src/Api/Controllers/BorrowsController.cs b/src/Api/Controllers/BorrowsController.cs index 9ebb4b9a..fcaddd3f 100644 --- a/src/Api/Controllers/BorrowsController.cs +++ b/src/Api/Controllers/BorrowsController.cs @@ -192,7 +192,7 @@ public async Task>> ApproveRequest([FromRoute] Gu var performingUserId = _currentUserService.GetId(); var command = new ApproveBorrowRequest.Command() { - PerformingUserId = performingUserId, + CurrentUserId = performingUserId, BorrowId = borrowId, Reason = request.Reason, }; @@ -343,25 +343,4 @@ public async Task>>> GetAllRequ var result = await Mediator.Send(query); return Ok(Result>.Succeed(result)); } - - /// - /// Get a log related to request by Id. - /// - /// Id of the requested log - /// A LockerLogDto of the requested log. - [RequiresRole(IdentityData.Roles.Admin)] - [HttpGet("log/{logId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetRequestLogById([FromRoute] Guid logId) - { - var query = new GetRequestLogById.Query() - { - LogId = logId - }; - - var result = await Mediator.Send(query); - return Ok(Result.Succeed(result)); - } } \ No newline at end of file diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index 660f3a95..cf42aea5 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -268,27 +268,7 @@ public async Task>> SharePermissions( var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); } - - /// - /// Get a document log by Id - /// - /// - /// Return a DocumentLogDto - [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] - [HttpGet("log/{logId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetLogById([FromRoute] Guid logId) - { - var query = new GetLogOfDocumentById.Query() - { - LogId = logId - }; - var result = await Mediator.Send(query); - return Ok(Result.Succeed(result)); - } - /// /// Get all log of document /// diff --git a/src/Api/Controllers/FoldersController.cs b/src/Api/Controllers/FoldersController.cs index c2ebeb13..3a12e958 100644 --- a/src/Api/Controllers/FoldersController.cs +++ b/src/Api/Controllers/FoldersController.cs @@ -179,25 +179,4 @@ public async Task>>> GetAllLogsP var result = await Mediator.Send(query); return Ok(Result>.Succeed(result)); } - - /// - /// - /// - /// - /// - [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] - [HttpGet("log/{logId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetLogById([FromRoute] Guid logId) - { - var query = new GetFolderLogById.Query() - { - LogId = logId - }; - - var result = await Mediator.Send(query); - return Ok(Result.Succeed(result)); - } } \ No newline at end of file diff --git a/src/Api/Controllers/LockersController.cs b/src/Api/Controllers/LockersController.cs index 8c11bbfd..5043c5e6 100644 --- a/src/Api/Controllers/LockersController.cs +++ b/src/Api/Controllers/LockersController.cs @@ -179,24 +179,4 @@ public async Task>>> GetAllLogsP var result = await Mediator.Send(query); return Ok(Result>.Succeed(result)); } - - /// - /// Get a log related to locker by Id. - /// - /// Id of the requested log - /// A LockerLogDto of the requested log. - [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] - [HttpGet("logs/{logId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetLogById([FromRoute] Guid logId) - { - var query = new GetLockerLogById.Query() - { - LogId = logId, - }; - var result = await Mediator.Send(query); - return Ok(Result.Succeed(result)); - } } diff --git a/src/Api/Controllers/StaffsController.cs b/src/Api/Controllers/StaffsController.cs index b9d9ea64..7ccda68e 100644 --- a/src/Api/Controllers/StaffsController.cs +++ b/src/Api/Controllers/StaffsController.cs @@ -87,7 +87,7 @@ public async Task>>> GetAllPaginated } /// - /// Add a staff + /// Assign a staff /// /// Add staff details /// A StaffDto of the added staff diff --git a/src/Api/Controllers/UsersController.cs b/src/Api/Controllers/UsersController.cs index c0fd9a21..7e7f01a2 100644 --- a/src/Api/Controllers/UsersController.cs +++ b/src/Api/Controllers/UsersController.cs @@ -211,25 +211,4 @@ public async Task>>> GetAllLogsPag var result = await Mediator.Send(query); return Ok(Result>.Succeed(result)); } - - /// - /// Get user related log by Id - /// - /// Id of the logged user - /// A UserLogDto of the logged user - [RequiresRole(IdentityData.Roles.Admin)] - [HttpGet("logs/{logId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetLogById( - [FromRoute] Guid logId) - { - var query = new GetUserLogById.Query() - { - LogId = logId, - }; - var result = await Mediator.Send(query); - return Ok(Result.Succeed(result)); - } } \ No newline at end of file diff --git a/src/Application/Borrows/Commands/ApproveBorrowRequest.cs b/src/Application/Borrows/Commands/ApproveBorrowRequest.cs index 4f6ebfde..f910fdcb 100644 --- a/src/Application/Borrows/Commands/ApproveBorrowRequest.cs +++ b/src/Application/Borrows/Commands/ApproveBorrowRequest.cs @@ -15,7 +15,7 @@ public class ApproveBorrowRequest { public record Command : IRequest { - public Guid PerformingUserId { get; init; } + public Guid CurrentUserId { get; init; } public Guid BorrowId { get; init; } public string Reason { get; init; } = null!; } @@ -57,39 +57,39 @@ public async Task Handle(Command request, CancellationToken cancellat } var localDateTimeNow = LocalDateTime.FromDateTime(DateTime.Now); - var existedBorrow = await _context.Borrows - .FirstOrDefaultAsync(x => + var existedBorrows = _context.Borrows + .Where(x => x.Document.Id == borrowRequest.Document.Id && x.Id != borrowRequest.Id && ((x.DueTime > localDateTimeNow) - || x.Status == BorrowRequestStatus.Overdue), cancellationToken); + || x.Status == BorrowRequestStatus.Overdue)); - if (existedBorrow is not null) + foreach (var borrow in existedBorrows) { - if (existedBorrow?.Status + if (borrow?.Status is BorrowRequestStatus.Approved or BorrowRequestStatus.CheckedOut - && borrowRequest.BorrowTime < existedBorrow.DueTime) + && borrowRequest.BorrowTime < borrow.DueTime) { throw new ConflictException("This document cannot be borrowed."); } } - - var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); + + var currentUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.CurrentUserId, cancellationToken); borrowRequest.Status = BorrowRequestStatus.Approved; var log = new DocumentLog() { Object = borrowRequest.Document, - UserId = performingUser!.Id, - User = performingUser, + UserId = currentUser!.Id, + User = currentUser, Time = LocalDateTime.FromDateTime(DateTime.Now), Action = DocumentLogMessages.Borrow.Approve, }; var requestLog = new RequestLog() { Object = borrowRequest.Document, - UserId = performingUser.Id, - User = performingUser, + UserId = currentUser.Id, + User = currentUser, Time = LocalDateTime.FromDateTime(DateTime.Now), Action = DocumentLogMessages.Borrow.Approve, }; diff --git a/src/Application/Borrows/Commands/BorrowDocument.cs b/src/Application/Borrows/Commands/BorrowDocument.cs index 7f810868..528ab4c8 100644 --- a/src/Application/Borrows/Commands/BorrowDocument.cs +++ b/src/Application/Borrows/Commands/BorrowDocument.cs @@ -49,12 +49,14 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; private readonly IPermissionManager _permissionManager; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper, IPermissionManager permissionManager) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IPermissionManager permissionManager, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; _permissionManager = permissionManager; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -100,43 +102,51 @@ public async Task Handle(Command request, CancellationToken cancellat // if the request is in time, meaning not overdue, // then check if its due date is less than the borrow request date, if not then check // if it's already been approved, checked out or lost, meaning - var localDateTimeNow = LocalDateTime.FromDateTime(DateTime.Now); - var existedBorrow = await _context.Borrows + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + var borrowFromTime = LocalDateTime.FromDateTime(request.BorrowFrom); + var borrowToTime = LocalDateTime.FromDateTime(request.BorrowTo); + var existedBorrows = _context.Borrows .Include(x => x.Borrower) - .FirstOrDefaultAsync(x => + .Where(x => x.Document.Id == request.DocumentId && ((x.DueTime > localDateTimeNow) - || x.Status == BorrowRequestStatus.Overdue), cancellationToken); - - if (existedBorrow is not null) + || x.Status == BorrowRequestStatus.Overdue)); + + + // Does not make sense if the same person go up and want to borrow the same document again + // even if the borrow day will be after the due day + foreach (var borrow in existedBorrows) { - // Does not make sense if the same person go up and want to borrow the same document again - // even if the borrow day will be after the due day - if (existedBorrow.Borrower.Id == request.BorrowerId - && existedBorrow.Status is BorrowRequestStatus.Pending + if (borrow.Borrower.Id == request.BorrowerId + && borrow.Status is BorrowRequestStatus.Pending or BorrowRequestStatus.Approved) - { - throw new ConflictException("This document is already requested borrow from the same user."); - } + { + throw new ConflictException("This document is already requested borrow from the same user."); + } - if (existedBorrow.Status - is BorrowRequestStatus.Approved - or BorrowRequestStatus.CheckedOut - && LocalDateTime.FromDateTime(request.BorrowFrom) < existedBorrow.DueTime) - { - throw new ConflictException("This document cannot be borrowed."); - } + if (borrowFromTime <= borrow.DueTime && borrowToTime >= borrow.BorrowTime) + { + throw new ConflictException("Overlapping time"); + } + + if (borrow.Status + is BorrowRequestStatus.Approved + or BorrowRequestStatus.CheckedOut + && borrowFromTime < borrow.DueTime) + { + throw new ConflictException("This document cannot be borrowed."); + } } - + var entity = new Borrow() { Borrower = user, Document = document, - BorrowTime = LocalDateTime.FromDateTime(request.BorrowFrom), - DueTime = LocalDateTime.FromDateTime(request.BorrowTo), + BorrowTime = borrowFromTime, + DueTime = borrowToTime, Reason = request.Reason, Status = BorrowRequestStatus.Pending, - Created = LocalDateTime.FromDateTime(DateTime.Now), + Created = localDateTimeNow, CreatedBy = user.Id, }; diff --git a/src/Application/Borrows/Commands/UpdateBorrow.cs b/src/Application/Borrows/Commands/UpdateBorrow.cs index c1b3ba70..8c75b727 100644 --- a/src/Application/Borrows/Commands/UpdateBorrow.cs +++ b/src/Application/Borrows/Commands/UpdateBorrow.cs @@ -73,27 +73,28 @@ public async Task Handle(Command request, CancellationToken cancellat } var localDateTimeNow = LocalDateTime.FromDateTime(DateTime.Now); - var existedBorrow = await _context.Borrows + var existedBorrows = _context.Borrows .Include(x => x.Borrower) - .FirstOrDefaultAsync(x => + .Where(x => x.Document.Id == borrowRequest.Document.Id && x.Id != borrowRequest.Id && ((x.DueTime > localDateTimeNow) - || x.Status == BorrowRequestStatus.Overdue), cancellationToken); - - if (existedBorrow is not null) + || x.Status == BorrowRequestStatus.Overdue)); + + var borrowFromTime = LocalDateTime.FromDateTime(request.BorrowFrom); + var borrowToTime = LocalDateTime.FromDateTime(request.BorrowTo); + foreach (var borrow in existedBorrows) { - if (existedBorrow.Status - is BorrowRequestStatus.Approved - or BorrowRequestStatus.CheckedOut - && LocalDateTime.FromDateTime(request.BorrowFrom) < existedBorrow.DueTime) + if (borrow.Status + is BorrowRequestStatus.Approved + or BorrowRequestStatus.CheckedOut + && (borrowFromTime <= borrow.DueTime && borrowToTime >= borrow.BorrowTime)) { - throw new ConflictException("This document cannot be borrowed."); + throw new ConflictException("This document cannot be updated."); } } - - borrowRequest.BorrowTime = LocalDateTime.FromDateTime(request.BorrowFrom); - borrowRequest.DueTime = LocalDateTime.FromDateTime(request.BorrowTo); + borrowRequest.BorrowTime = borrowFromTime; + borrowRequest.DueTime = borrowToTime; borrowRequest.Reason = request.Reason; var result = _context.Borrows.Update(borrowRequest); diff --git a/src/Application/Borrows/Queries/GetRequestLogById.cs b/src/Application/Borrows/Queries/GetRequestLogById.cs deleted file mode 100644 index 4d53f316..00000000 --- a/src/Application/Borrows/Queries/GetRequestLogById.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Logging; -using AutoMapper; -using Domain.Enums; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Borrows.Queries; - -public class GetRequestLogById -{ - public record Query : IRequest - { - public Guid LogId { get; init; } - } - - public class QueryHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public QueryHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - var log = await _context.RequestLogs - .Include(x => x.Object) - .ThenInclude(x => x!.Importer) - .Include(x => x.Object) - .ThenInclude(x => x!.Folder) - .Include(x => x.User) - .ThenInclude(x => x.Department) - .FirstOrDefaultAsync(x => x.Id.Equals(request.LogId), cancellationToken); - - if (log is null) - { - throw new KeyNotFoundException("Log does not exist."); - } - - return _mapper.Map(log); - } - } -} \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetLogOfDocumentById.cs b/src/Application/Documents/Queries/GetLogOfDocumentById.cs deleted file mode 100644 index 2a6fd6a2..00000000 --- a/src/Application/Documents/Queries/GetLogOfDocumentById.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Logging; -using AutoMapper; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Documents.Queries; - -public class GetLogOfDocumentById -{ - public record Query : IRequest - { - public Guid LogId { get; init; } - } - - public class QueryHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public QueryHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - var log = await _context.DocumentLogs - .Include(x => x.Object) - .Include(x => x.User) - .ThenInclude(x => x.Department) - .FirstOrDefaultAsync(x => x.Id.Equals(request.LogId), cancellationToken); - - if (log is null) - { - throw new KeyNotFoundException("Log does not exist."); - } - - return _mapper.Map(log); - } - } -} \ No newline at end of file diff --git a/src/Application/Folders/Commands/AddFolder.cs b/src/Application/Folders/Commands/AddFolder.cs index dd65042e..8a8c928c 100644 --- a/src/Application/Folders/Commands/AddFolder.cs +++ b/src/Application/Folders/Commands/AddFolder.cs @@ -123,8 +123,8 @@ public async Task Handle(Command request, CancellationToken cancellat private async Task DuplicatedNameFolderExistsInSameLockerAsync(string folderName, Guid lockerId, CancellationToken cancellationToken) { var folder = await _context.Folders.FirstOrDefaultAsync( - x => EqualsInvariant(x.Name, folderName) - && IsSameLocker(x.Locker.Id, lockerId), cancellationToken); + x => x.Name.ToLower().Equals(folderName.ToLower()) + && x.Locker.Id == lockerId, cancellationToken); return folder is not null; } diff --git a/src/Application/Folders/Queries/GetFolderLogById.cs b/src/Application/Folders/Queries/GetFolderLogById.cs deleted file mode 100644 index 340ba6a3..00000000 --- a/src/Application/Folders/Queries/GetFolderLogById.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Logging; -using AutoMapper; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Folders.Queries; - -public class GetFolderLogById -{ - public record Query : IRequest - { - public Guid LogId { get; init; } - } - - public class QueryHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public QueryHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - var log = await _context.FolderLogs - .Include(x => x.Object) - .Include(x => x.User) - .ThenInclude(x => x.Department) - .FirstOrDefaultAsync(x => x.Id.Equals(request.LogId), cancellationToken); - - if (log is null) - { - throw new KeyNotFoundException("Log does not exist."); - } - - return _mapper.Map(log); - } - } -} \ No newline at end of file diff --git a/src/Application/Lockers/Commands/AddLocker.cs b/src/Application/Lockers/Commands/AddLocker.cs index 60803a84..b39e6e82 100644 --- a/src/Application/Lockers/Commands/AddLocker.cs +++ b/src/Application/Lockers/Commands/AddLocker.cs @@ -111,8 +111,8 @@ public async Task Handle(Command request, CancellationToken cancellat private async Task DuplicatedNameLockerExistsInSameRoomAsync(string lockerName, Guid roomId, CancellationToken cancellationToken) { var locker = await _context.Lockers.FirstOrDefaultAsync( - x => EqualsInvariant(x.Name, lockerName) - && IsSameRoom(x.Room.Id, roomId), cancellationToken); + x => x.Name.ToLower().Equals(lockerName.ToLower()) + && x.Room.Id == roomId, cancellationToken); return locker is not null; } diff --git a/src/Application/Lockers/Queries/GetLockerLogById.cs b/src/Application/Lockers/Queries/GetLockerLogById.cs deleted file mode 100644 index 5085797b..00000000 --- a/src/Application/Lockers/Queries/GetLockerLogById.cs +++ /dev/null @@ -1,59 +0,0 @@ -using Application.Common.Extensions; -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Logging; -using AutoMapper; -using Domain.Entities.Logging; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Lockers.Queries; - -public class GetLockerLogById -{ - public record Query : IRequest - { - public Guid LogId { get; init; } - public string CurrentUserRole { get; init; } = null!; - public Guid? CurrentStaffRoomId { get; init; } - } - - public class QueryHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public QueryHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - var log = await _context.LockerLogs - .Include(x => x.Object) - .ThenInclude(x => x!.Room) - .Include(x => x.User) - .ThenInclude(x => x.Department) - .FirstOrDefaultAsync(x => x.Id.Equals(request.LogId), cancellationToken); - - if (log is null) - { - throw new KeyNotFoundException("Log does not exist."); - } - - if (request.CurrentUserRole.IsStaff() - && (request.CurrentStaffRoomId is null || !LockerInSameRoom(log, request.CurrentStaffRoomId.Value))) - { - throw new UnauthorizedAccessException("User cannot access this resource."); - } - - return _mapper.Map(log); - } - - private static bool LockerInSameRoom( - LockerLog log, - Guid roomId) - => log.BaseRoom!.Id == roomId; - } -} \ No newline at end of file diff --git a/src/Application/Users/Queries/GetUserLogById.cs b/src/Application/Users/Queries/GetUserLogById.cs deleted file mode 100644 index 7e561ae7..00000000 --- a/src/Application/Users/Queries/GetUserLogById.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Logging; -using AutoMapper; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Users.Queries; - -public class GetUserLogById -{ - public record Query : IRequest - { - public Guid LogId { get; init; } - } - - public class QueryHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public QueryHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - var log = await _context.UserLogs - .Include(x => x.Object) - .Include(x => x.User) - .ThenInclude(x => x.Department) - .FirstOrDefaultAsync(x => x.Id.Equals(request.LogId), cancellationToken); - - if (log is null) - { - throw new KeyNotFoundException("Log does not exist."); - } - - return _mapper.Map(log); - } - } -} \ No newline at end of file From b52522b7acbf727c19411229960100cb459c6bd5 Mon Sep 17 00:00:00 2001 From: Vzart Date: Tue, 20 Jun 2023 14:40:19 +0700 Subject: [PATCH 48/56] refactor kinda oke i guess --- src/Api/Controllers/BorrowsController.cs | 143 +++--------------- src/Api/Controllers/DocumentsController.cs | 3 +- src/Api/Controllers/FoldersController.cs | 1 + .../Controllers/ImportRequestsController.cs | 7 +- src/Api/Controllers/LockersController.cs | 4 +- ...=> ApproveOrRejectBorrowRequestRequest.cs} | 2 +- ...BorrowRequestsPaginatedQueryParameters.cs} | 6 +- .../GetAllLogsPaginatedQueryParameters.cs | 2 +- src/Api/Controllers/RoomsController.cs | 21 +-- src/Api/Controllers/UsersController.cs | 2 +- src/Api/Services/CurrentUserService.cs | 32 ++-- ...est.cs => ApproveOrRejectBorrowRequest.cs} | 75 ++++++--- .../Borrows/Commands/BorrowDocument.cs | 41 +++-- .../Queries/GetAllBorrowRequestsPaginated.cs | 63 ++++++-- .../Documents/Commands/UpdateDocument.cs | 5 +- .../Queries/GetAllDocumentLogsPaginated.cs | 7 + .../Queries/GetAllFolderLogsPaginated.cs | 9 +- .../Queries/GetAllImportRequestsPaginated.cs | 27 +++- src/Application/Lockers/Commands/AddLocker.cs | 6 - .../Lockers/Commands/UpdateLocker.cs | 15 +- .../Queries/GetAllLockerLogsPaginated.cs | 12 +- src/Application/Rooms/Commands/UpdateRoom.cs | 10 +- .../Rooms/Queries/GetRoomLogById.cs | 43 ------ src/Application/Users/Queries/GetUserById.cs | 5 +- .../Commands/ApproveBorrowRequestTests.cs | 12 +- 25 files changed, 227 insertions(+), 326 deletions(-) rename src/Api/Controllers/Payload/Requests/Borrows/{ApproveRequest.cs => ApproveOrRejectBorrowRequestRequest.cs} (65%) rename src/Api/Controllers/Payload/Requests/Borrows/{GetAllBorrowRequestsPaginatedAsAdminQueryParameters.cs => GetAllBorrowRequestsPaginatedQueryParameters.cs} (59%) rename src/Application/Borrows/Commands/{ApproveBorrowRequest.cs => ApproveOrRejectBorrowRequest.cs} (60%) delete mode 100644 src/Application/Rooms/Queries/GetRoomLogById.cs diff --git a/src/Api/Controllers/BorrowsController.cs b/src/Api/Controllers/BorrowsController.cs index fcaddd3f..48841037 100644 --- a/src/Api/Controllers/BorrowsController.cs +++ b/src/Api/Controllers/BorrowsController.cs @@ -2,6 +2,7 @@ using Api.Controllers.Payload.Requests.Borrows; using Application.Borrows.Commands; using Application.Borrows.Queries; +using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Models; using Application.Common.Models.Dtos.Logging; @@ -12,6 +13,7 @@ namespace Api.Controllers; +[Route("api/v1/documents/[controller]")] public class BorrowsController : ApiControllerBase { private readonly ICurrentUserService _currentUserService; @@ -30,7 +32,8 @@ public BorrowsController(ICurrentUserService currentUserService) [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> BorrowDocument([FromBody] BorrowDocumentRequest request) + public async Task>> BorrowDocument( + [FromBody] BorrowDocumentRequest request) { var borrowerId = _currentUserService.GetCurrentUser().Id; var command = new BorrowDocument.Command() @@ -55,7 +58,8 @@ public async Task>> BorrowDocument([FromBody] Bor [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> GetById([FromRoute] Guid borrowId) + public async Task>> GetById( + [FromRoute] Guid borrowId) { var user = _currentUserService.GetCurrentUser(); var command = new GetBorrowRequestById.Query() @@ -67,47 +71,25 @@ public async Task>> GetById([FromRoute] Guid borr return Ok(Result.Succeed(result)); } - [RequiresRole(IdentityData.Roles.Staff)] - [HttpGet("staffs")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>>> GetAllRequestsAsStaffPaginated( - [FromQuery] GetAllBorrowRequestsPaginatedAsStaffQueryParameters queryParameters) - { - var departmentId = _currentUserService.GetCurrentDepartmentForStaff(); - var command = new GetAllBorrowRequestsPaginated.Query() - { - DepartmentId = departmentId, - EmployeeId = queryParameters.EmployeeId, - DocumentId = queryParameters.DocumentId, - Page = queryParameters.Page, - Size = queryParameters.Size, - SortBy = queryParameters.SortBy, - SortOrder = queryParameters.SortOrder, - }; - var result = await Mediator.Send(command); - return Ok(Result>.Succeed(result)); - } - /// - /// Get all borrow requests as admin paginated + /// /// /// /// - [RequiresRole(IdentityData.Roles.Admin)] + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff, IdentityData.Roles.Employee)] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>>> GetAllRequestsAsAdminPaginated( - [FromQuery] GetAllBorrowRequestsPaginatedAsAdminQueryParameters queryParameters) + public async Task>>> GetAllRequestsPaginated( + [FromQuery] GetAllBorrowRequestsPaginatedQueryParameters queryParameters) { + var currentUser = _currentUserService.GetCurrentUser(); var command = new GetAllBorrowRequestsPaginated.Query() { - DepartmentId = queryParameters.DepartmentId, + CurrentUser = currentUser, + RoomId = queryParameters.RoomId, EmployeeId = queryParameters.EmployeeId, DocumentId = queryParameters.DocumentId, Page = queryParameters.Page, @@ -119,78 +101,24 @@ public async Task>>> GetAllRequests return Ok(Result>.Succeed(result)); } - /// - /// Get all borrow requests as employee paginated - /// - /// - /// - [RequiresRole(IdentityData.Roles.Employee)] - [HttpGet("employees")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>>> GetAllRequestsAsEmployeePaginated( - [FromQuery] GetAllBorrowRequestsPaginatedAsEmployeeQueryParameters queryParameters) - { - var userId = _currentUserService.GetCurrentUser().Id; - var command = new GetAllBorrowRequestsPaginated.Query() - { - EmployeeId = userId, - DocumentId = queryParameters.DocumentId, - Page = queryParameters.Page, - Size = queryParameters.Size, - SortBy = queryParameters.SortBy, - SortOrder = queryParameters.SortOrder, - }; - var result = await Mediator.Send(command); - return Ok(Result>.Succeed(result)); - } - - /// - /// Get all borrow requests for a document paginated - /// - /// - /// - /// - [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] - [HttpGet("documents/{documentId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>>> GetAllRequestsForDocumentPaginated( - [FromRoute] Guid documentId, - [FromQuery] GetAllBorrowRequestsPaginatedForDocumentQueryParameters queryParameters) - { - var command = new GetAllBorrowRequestsPaginated.Query() - { - DocumentId = documentId, - Page = queryParameters.Page, - Size = queryParameters.Size, - SortBy = queryParameters.SortBy, - SortOrder = queryParameters.SortOrder, - Status = queryParameters.Status, - }; - var result = await Mediator.Send(command); - return Ok(Result>.Succeed(result)); - } - /// /// Approve a borrow request /// /// Id of the borrow request to be approved + /// /// A BorrowDto of the approved borrow request [RequiresRole(IdentityData.Roles.Staff)] - [HttpPost("approve/{borrowId:guid}")] + [HttpPut("{borrowId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> ApproveRequest([FromRoute] Guid borrowId, [FromBody] ApproveRequest request) + public async Task>> ApproveOrRejectRequest( + [FromRoute] Guid borrowId, + [FromBody] ApproveOrRejectBorrowRequestRequest request) { var performingUserId = _currentUserService.GetId(); - var command = new ApproveBorrowRequest.Command() + var command = new ApproveOrRejectBorrowRequest.Command() { CurrentUserId = performingUserId, BorrowId = borrowId, @@ -200,30 +128,6 @@ public async Task>> ApproveRequest([FromRoute] Gu return Ok(Result.Succeed(result)); } - /// - /// Reject a borrow request - /// - /// Id of the borrow request to be rejected - /// A BorrowDto of the rejected borrow request - [RequiresRole(IdentityData.Roles.Staff)] - [HttpPost("reject/{borrowId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> RejectRequest([FromRoute] Guid borrowId, [FromBody] RejectRequest request) - { - var performingUserId = _currentUserService.GetId(); - var command = new RejectBorrowRequest.Command() - { - PerformingUserId = performingUserId, - BorrowId = borrowId, - Reason = request.Reason, - }; - var result = await Mediator.Send(command); - return Ok(Result.Succeed(result)); - } - /// /// Check out a borrow request /// @@ -235,7 +139,8 @@ public async Task>> RejectRequest([FromRoute] Gui [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Checkout([FromRoute] Guid borrowId) + public async Task>> Checkout( + [FromRoute] Guid borrowId) { var performingUserId = _currentUserService.GetId(); var command = new CheckoutDocument.Command() @@ -258,7 +163,8 @@ public async Task>> Checkout([FromRoute] Guid bor [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Return([FromRoute] Guid documentId) + public async Task>> Return( + [FromRoute] Guid documentId) { var performingUserId = _currentUserService.GetId(); var command = new ReturnDocument.Command() @@ -310,7 +216,8 @@ public async Task>> Update( [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Cancel([FromRoute] Guid borrowId) + public async Task>> Cancel( + [FromRoute] Guid borrowId) { var performingUserId = _currentUserService.GetId(); var command = new CancelBorrowRequest.Command() diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index cf42aea5..2a70eba2 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -274,7 +274,7 @@ public async Task>> SharePermissions( /// /// /// Paginated list of DocumentLogDto - [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] + [RequiresRole(IdentityData.Roles.Admin)] [HttpGet("logs")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task>>> GetAllLogsPaginated( @@ -282,6 +282,7 @@ public async Task>>> GetAllLog { var query = new GetAllDocumentLogsPaginated.Query() { + DocumentId = queryParameters.ObjectId, SearchTerm = queryParameters.SearchTerm, Page = queryParameters.Page, Size = queryParameters.Size, diff --git a/src/Api/Controllers/FoldersController.cs b/src/Api/Controllers/FoldersController.cs index 3a12e958..76af4934 100644 --- a/src/Api/Controllers/FoldersController.cs +++ b/src/Api/Controllers/FoldersController.cs @@ -172,6 +172,7 @@ public async Task>>> GetAllLogsP { var query = new GetAllFolderLogsPaginated.Query() { + FolderId = queryParameters.ObjectId, SearchTerm = queryParameters.SearchTerm, Page = queryParameters.Page, Size = queryParameters.Size, diff --git a/src/Api/Controllers/ImportRequestsController.cs b/src/Api/Controllers/ImportRequestsController.cs index 9c4ce63d..3f5a5e27 100644 --- a/src/Api/Controllers/ImportRequestsController.cs +++ b/src/Api/Controllers/ImportRequestsController.cs @@ -105,9 +105,9 @@ public async Task>> RequestImport( /// /// Id of the document to be approved /// - /// A DocumentDto of the approved document + /// A DocumentDto of the approved document [RequiresRole(IdentityData.Roles.Staff)] - [HttpPut] + [HttpPut("{importRequestId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -134,7 +134,7 @@ public async Task>> ApproveOrReject( /// /// A DocumentDto of the rejected document [RequiresRole(IdentityData.Roles.Staff)] - [HttpPut("{importRequestId:guid}")] + [HttpPut("assign/{importRequestId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -176,7 +176,6 @@ public async Task>> Checkin( CurrentUser = currentUser, DocumentId = documentId, }; - var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } diff --git a/src/Api/Controllers/LockersController.cs b/src/Api/Controllers/LockersController.cs index 5043c5e6..a02feeed 100644 --- a/src/Api/Controllers/LockersController.cs +++ b/src/Api/Controllers/LockersController.cs @@ -158,7 +158,7 @@ public async Task>> Update( /// /// Query parameters /// A list of LockerLogsDtos - [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] + [RequiresRole(IdentityData.Roles.Admin)] [HttpGet("logs")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -174,7 +174,7 @@ public async Task>>> GetAllLogsP SearchTerm = queryParameters.SearchTerm, Page = queryParameters.Page, Size = queryParameters.Size, - RoomId = queryParameters.UserId + LockerId = queryParameters.ObjectId }; var result = await Mediator.Send(query); return Ok(Result>.Succeed(result)); diff --git a/src/Api/Controllers/Payload/Requests/Borrows/ApproveRequest.cs b/src/Api/Controllers/Payload/Requests/Borrows/ApproveOrRejectBorrowRequestRequest.cs similarity index 65% rename from src/Api/Controllers/Payload/Requests/Borrows/ApproveRequest.cs rename to src/Api/Controllers/Payload/Requests/Borrows/ApproveOrRejectBorrowRequestRequest.cs index f6c0c0d2..16997ef0 100644 --- a/src/Api/Controllers/Payload/Requests/Borrows/ApproveRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Borrows/ApproveOrRejectBorrowRequestRequest.cs @@ -1,6 +1,6 @@ namespace Api.Controllers.Payload.Requests.Borrows; -public class ApproveRequest +public class ApproveOrRejectBorrowRequestRequest { public string Reason { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsAdminQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedQueryParameters.cs similarity index 59% rename from src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsAdminQueryParameters.cs rename to src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedQueryParameters.cs index 8ea8eca1..aa844929 100644 --- a/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedAsAdminQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Borrows/GetAllBorrowRequestsPaginatedQueryParameters.cs @@ -3,12 +3,12 @@ namespace Api.Controllers.Payload.Requests.Borrows; /// /// Query parameters for getting all borrow requests with pagination as admin /// -public class GetAllBorrowRequestsPaginatedAsAdminQueryParameters : PaginatedQueryParameters +public class GetAllBorrowRequestsPaginatedQueryParameters : PaginatedQueryParameters { /// - /// Id of the department to get borrow requests in + /// Id of the room to get borrow requests in /// - public Guid? DepartmentId { get; set; } + public Guid? RoomId { get; set; } public Guid? DocumentId { get; set; } public Guid? EmployeeId { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs index 94c60331..59019d0d 100644 --- a/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/GetAllLogsPaginatedQueryParameters.cs @@ -21,5 +21,5 @@ public class GetAllLogsPaginatedQueryParameters /// /// User Id /// - public Guid? UserId { get; set; } + public Guid? ObjectId { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/RoomsController.cs b/src/Api/Controllers/RoomsController.cs index 0daf20f2..efcd6b40 100644 --- a/src/Api/Controllers/RoomsController.cs +++ b/src/Api/Controllers/RoomsController.cs @@ -212,6 +212,7 @@ public async Task>>> GetAllLogsPag { var query = new GetAllRoomLogsPaginated.Query() { + RoomId = queryParameters.ObjectId, SearchTerm = queryParameters.SearchTerm, Page = queryParameters.Page, Size = queryParameters.Size, @@ -219,24 +220,4 @@ public async Task>>> GetAllLogsPag var result = await Mediator.Send(query); return Ok(Result>.Succeed(result)); } - - /// - /// Get a room log by id - /// - /// - /// A RoomLogDto - [RequiresRole(IdentityData.Roles.Admin)] - [HttpGet("logs/{logId:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetLogById( - [FromRoute] Guid logId) - { - var query = new GetRoomLogById.Query() - { - LogId = logId, - }; - var result = await Mediator.Send(query); - return Ok(Result.Succeed(result)); - } } \ No newline at end of file diff --git a/src/Api/Controllers/UsersController.cs b/src/Api/Controllers/UsersController.cs index 7e7f01a2..12ca4695 100644 --- a/src/Api/Controllers/UsersController.cs +++ b/src/Api/Controllers/UsersController.cs @@ -206,7 +206,7 @@ public async Task>>> GetAllLogsPag SearchTerm = queryParameters.SearchTerm, Page = queryParameters.Page, Size = queryParameters.Size, - UserId = queryParameters.UserId, + UserId = queryParameters.ObjectId, }; var result = await Mediator.Send(query); return Ok(Result>.Succeed(result)); diff --git a/src/Api/Services/CurrentUserService.cs b/src/Api/Services/CurrentUserService.cs index 6eb40f38..143498a9 100644 --- a/src/Api/Services/CurrentUserService.cs +++ b/src/Api/Services/CurrentUserService.cs @@ -7,13 +7,13 @@ namespace Api.Services; public class CurrentUserService : ICurrentUserService { - private readonly IApplicationDbContext _context; + private readonly IApplicationDbContext _dbContext; private readonly IHttpContextAccessor _httpContextAccessor; public CurrentUserService(IHttpContextAccessor httpContextAccessor, IApplicationDbContext context) { _httpContextAccessor = httpContextAccessor; - _context = context; + _dbContext = context; } public Guid GetId() @@ -32,7 +32,7 @@ public string GetRole() throw new UnauthorizedAccessException(); } - var user = _context.Users.FirstOrDefault(x => x.Username.Equals(userName)); + var user = _dbContext.Users.FirstOrDefault(x => x.Username.Equals(userName)); if (user is null) { @@ -59,7 +59,7 @@ public User GetCurrentUser() throw new UnauthorizedAccessException(); } - var user = _context.Users + var user = _dbContext.Users .Include(x => x.Department) .FirstOrDefault(x => x.Username.Equals(userName)); @@ -80,7 +80,7 @@ public User GetCurrentUser() throw new UnauthorizedAccessException(); } - var staff = _context.Staffs + var staff = _dbContext.Staffs .Include(x => x.User) .Include(x => x.Room) .FirstOrDefault(x => x.Id == userId); @@ -90,28 +90,18 @@ public User GetCurrentUser() public Guid? GetCurrentDepartmentForStaff() { - var userName = _httpContextAccessor.HttpContext!.User.Claims - .FirstOrDefault(x => x.Type.Equals(JwtRegisteredClaimNames.Sub))!.Value; - if (userName is null) + var userIdString = _httpContextAccessor.HttpContext!.User.Claims + .FirstOrDefault(x => x.Type.Equals(JwtRegisteredClaimNames.NameId)); + if (userIdString is null || !Guid.TryParse(userIdString.Value, out var userId)) { throw new UnauthorizedAccessException(); } - var staff = _context.Staffs + var staff = _dbContext.Staffs .Include(x => x.User) .Include(x => x.Room) - .FirstOrDefault(x => x.User.Username.Equals(userName)); - - if (staff is null) - { - throw new UnauthorizedAccessException(); - } - - if (staff.Room is null) - { - throw new UnauthorizedAccessException(); - } + .FirstOrDefault(x => x.Id == userId); - return staff.Room!.DepartmentId; + return staff?.Room?.DepartmentId; } } \ No newline at end of file diff --git a/src/Application/Borrows/Commands/ApproveBorrowRequest.cs b/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs similarity index 60% rename from src/Application/Borrows/Commands/ApproveBorrowRequest.cs rename to src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs index f910fdcb..8b6fcbc9 100644 --- a/src/Application/Borrows/Commands/ApproveBorrowRequest.cs +++ b/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs @@ -1,9 +1,11 @@ using Application.Common.Exceptions; +using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities.Logging; +using Domain.Enums; using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; @@ -11,12 +13,13 @@ namespace Application.Borrows.Commands; -public class ApproveBorrowRequest +public class ApproveOrRejectBorrowRequest { public record Command : IRequest { public Guid CurrentUserId { get; init; } public Guid BorrowId { get; init; } + public string Decision { get; init; } = null!; public string Reason { get; init; } = null!; } @@ -24,11 +27,13 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -50,49 +55,75 @@ public async Task Handle(Command request, CancellationToken cancellat throw new ConflictException("Document is lost. Request is unprocessable."); } - if (borrowRequest.Status is not BorrowRequestStatus.Pending - && borrowRequest.Status is not BorrowRequestStatus.Rejected) + if (borrowRequest.Status is not (BorrowRequestStatus.Pending or BorrowRequestStatus.Rejected) + && request.Decision.IsApproval()) { throw new ConflictException("Request cannot be approved."); } - var localDateTimeNow = LocalDateTime.FromDateTime(DateTime.Now); + if (borrowRequest.Status is not BorrowRequestStatus.Pending + && request.Decision.IsRejection()) + { + throw new ConflictException("Request cannot be rejected."); + } + + var currentUser = await _context.Users + .FirstOrDefaultAsync(x => x.Id == request.CurrentUserId, cancellationToken); + + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + var existedBorrows = _context.Borrows .Where(x => x.Document.Id == borrowRequest.Document.Id && x.Id != borrowRequest.Id - && ((x.DueTime > localDateTimeNow) + && (x.DueTime > localDateTimeNow || x.Status == BorrowRequestStatus.Overdue)); - foreach (var borrow in existedBorrows) - { - if (borrow?.Status - is BorrowRequestStatus.Approved - or BorrowRequestStatus.CheckedOut - && borrowRequest.BorrowTime < borrow.DueTime) - { - throw new ConflictException("This document cannot be borrowed."); - } - } - - var currentUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.CurrentUserId, cancellationToken); - borrowRequest.Status = BorrowRequestStatus.Approved; var log = new DocumentLog() { Object = borrowRequest.Document, UserId = currentUser!.Id, User = currentUser, - Time = LocalDateTime.FromDateTime(DateTime.Now), + Time = localDateTimeNow, Action = DocumentLogMessages.Borrow.Approve, }; var requestLog = new RequestLog() { Object = borrowRequest.Document, + Type = RequestType.Borrow, UserId = currentUser.Id, User = currentUser, - Time = LocalDateTime.FromDateTime(DateTime.Now), - Action = DocumentLogMessages.Borrow.Approve, + Time = localDateTimeNow, + Action = RequestLogMessages.ApproveBorrow, }; + + if (request.Decision.IsApproval()) + { + foreach (var existedBorrow in existedBorrows) + { + if (existedBorrow.Status + is BorrowRequestStatus.Approved + or BorrowRequestStatus.CheckedOut + && borrowRequest.BorrowTime <= existedBorrow.DueTime && borrowRequest.DueTime >= existedBorrow.BorrowTime) + { + throw new ConflictException("Request cannot be approved."); + } + } + + borrowRequest.Status = BorrowRequestStatus.Approved; + } + + if (request.Decision.IsRejection()) + { + borrowRequest.Status = BorrowRequestStatus.Rejected; + log.Action = DocumentLogMessages.Borrow.Reject; + requestLog.Action = RequestLogMessages.RejectBorrow; + } + + borrowRequest.Reason = request.Reason; + borrowRequest.LastModified = localDateTimeNow; + borrowRequest.LastModifiedBy = currentUser.Id; + var result = _context.Borrows.Update(borrowRequest); await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.RequestLogs.AddAsync(requestLog, cancellationToken); diff --git a/src/Application/Borrows/Commands/BorrowDocument.cs b/src/Application/Borrows/Commands/BorrowDocument.cs index 528ab4c8..ae13862c 100644 --- a/src/Application/Borrows/Commands/BorrowDocument.cs +++ b/src/Application/Borrows/Commands/BorrowDocument.cs @@ -1,3 +1,4 @@ +using System.Runtime.InteropServices; using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Messages; @@ -109,33 +110,27 @@ public async Task Handle(Command request, CancellationToken cancellat .Include(x => x.Borrower) .Where(x => x.Document.Id == request.DocumentId - && ((x.DueTime > localDateTimeNow) + && (x.DueTime > localDateTimeNow || x.Status == BorrowRequestStatus.Overdue)); - - // Does not make sense if the same person go up and want to borrow the same document again - // even if the borrow day will be after the due day foreach (var borrow in existedBorrows) { - if (borrow.Borrower.Id == request.BorrowerId - && borrow.Status is BorrowRequestStatus.Pending - or BorrowRequestStatus.Approved) - { - throw new ConflictException("This document is already requested borrow from the same user."); - } - - if (borrowFromTime <= borrow.DueTime && borrowToTime >= borrow.BorrowTime) - { - throw new ConflictException("Overlapping time"); - } - - if (borrow.Status - is BorrowRequestStatus.Approved - or BorrowRequestStatus.CheckedOut - && borrowFromTime < borrow.DueTime) - { - throw new ConflictException("This document cannot be borrowed."); - } + // Does not make sense if the same person go up and want to borrow the same document again + // even if the borrow day will be after the due day + if (borrow.Borrower.Id == request.BorrowerId + && borrow.Status is BorrowRequestStatus.Pending + or BorrowRequestStatus.Approved) + { + throw new ConflictException("This document is already requested borrow from the same user."); + } + + if (borrow.Status + is BorrowRequestStatus.Approved + or BorrowRequestStatus.CheckedOut + && borrowFromTime <= borrow.DueTime && borrowToTime >= borrow.BorrowTime) + { + throw new ConflictException("This document cannot be borrowed."); + } } var entity = new Borrow() diff --git a/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs b/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs index c78f674a..ee822637 100644 --- a/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs +++ b/src/Application/Borrows/Queries/GetAllBorrowRequestsPaginated.cs @@ -1,11 +1,11 @@ -using Application.Common.Exceptions; using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Models; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; +using Domain.Entities.Physical; using Domain.Statuses; -using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; @@ -13,17 +13,10 @@ namespace Application.Borrows.Queries; public class GetAllBorrowRequestsPaginated { - public class Validator : AbstractValidator - { - public Validator() - { - RuleLevelCascadeMode = CascadeMode.Stop; - } - } - public record Query : IRequest> { - public Guid? DepartmentId { get; init; } + public User CurrentUser { get; init; } = null!; + public Guid? RoomId { get; init; } public Guid? DocumentId { get; init; } public Guid? EmployeeId { get; init; } public int? Page { get; init; } @@ -47,6 +40,47 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task> Handle(Query request, CancellationToken cancellationToken) { + if (request.CurrentUser.Role.IsStaff()) + { + if (request.RoomId is null) + { + throw new UnauthorizedAccessException("User can not access this resource."); + } + + var room = await _context.Rooms + .FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); + var roomDoesNotExist = room is null; + + if (roomDoesNotExist + || RoomIsNotInSameDepartment(request.CurrentUser, room!)) + { + throw new UnauthorizedAccessException("User can not access this resource."); + } + } + + if (request.CurrentUser.Role.IsEmployee()) + { + if (request.RoomId is null) + { + throw new UnauthorizedAccessException("User can not access this resource."); + } + + if (request.EmployeeId != request.CurrentUser.Id) + { + throw new UnauthorizedAccessException("User can not access this resource"); + } + + var room = await _context.Rooms + .FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); + var roomDoesNotExist = room is null; + + if (roomDoesNotExist + || RoomIsNotInSameDepartment(request.CurrentUser, room!)) + { + throw new UnauthorizedAccessException("User can not access this resource."); + } + } + var borrows = _context.Borrows.AsQueryable(); borrows = borrows @@ -59,9 +93,9 @@ public async Task> Handle(Query request, .ThenInclude(t => t.Room) .ThenInclude(s => s.Department); - if (request.DepartmentId is not null) + if (request.RoomId is not null) { - borrows = borrows.Where(x => x.Document.Department!.Id == request.DepartmentId); + borrows = borrows.Where(x => x.Document.Folder!.Locker.Room.Id == request.RoomId); } if (request.EmployeeId is not null) @@ -108,5 +142,8 @@ public async Task> Handle(Query request, return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); } + + private static bool RoomIsNotInSameDepartment(User user, Room room) + => user.Department?.Id != room.DepartmentId; } } \ No newline at end of file diff --git a/src/Application/Documents/Commands/UpdateDocument.cs b/src/Application/Documents/Commands/UpdateDocument.cs index a35031d4..6219a0a4 100644 --- a/src/Application/Documents/Commands/UpdateDocument.cs +++ b/src/Application/Documents/Commands/UpdateDocument.cs @@ -115,11 +115,8 @@ public async Task Handle(Command request, CancellationToken cancell private static bool ViolateConstraints(User currentUser, Document document) => (currentUser.Role.IsStaff() - && NotInSameDepartment(currentUser, document)) + && currentUser.Department!.Id != document.Department!.Id) || (currentUser.Role.IsEmployee() && document.ImporterId != currentUser.Id); - - private static bool NotInSameDepartment(User currentUser, Document document) - => currentUser.Department!.Id != document.Department!.Id; } } \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetAllDocumentLogsPaginated.cs b/src/Application/Documents/Queries/GetAllDocumentLogsPaginated.cs index fa3767f8..1dea3495 100644 --- a/src/Application/Documents/Queries/GetAllDocumentLogsPaginated.cs +++ b/src/Application/Documents/Queries/GetAllDocumentLogsPaginated.cs @@ -14,6 +14,7 @@ public class GetAllDocumentLogsPaginated { public record Query : IRequest> { + public Guid? DocumentId { get; set; } public string? SearchTerm { get; init; } public int? Page { get; init; } public int? Size { get; init; } @@ -38,12 +39,18 @@ public async Task> Handle(Query request, Cancellat .ThenInclude(x => x.Department) .AsQueryable(); + if (request.DocumentId is not null) + { + logs = logs.Where(x => x.Object!.Id == request.DocumentId); + } + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) { logs = logs.Where(x => x.Action.ToLower().Contains(request.SearchTerm.ToLower())); } + return await logs .LoggingListPaginateAsync( request.Page, diff --git a/src/Application/Folders/Queries/GetAllFolderLogsPaginated.cs b/src/Application/Folders/Queries/GetAllFolderLogsPaginated.cs index 41708a51..6be59683 100644 --- a/src/Application/Folders/Queries/GetAllFolderLogsPaginated.cs +++ b/src/Application/Folders/Queries/GetAllFolderLogsPaginated.cs @@ -14,6 +14,7 @@ public class GetAllFolderLogsPaginated { public record Query : IRequest> { + public Guid? FolderId { get; init; } public string? SearchTerm { get; init; } public int? Page { get; init; } public int? Size { get; init; } @@ -36,12 +37,18 @@ public async Task> Handle(Query request, Cancellatio .Include(x => x.Object) .AsQueryable(); + if (request.FolderId is not null) + { + logs = logs.Where(x => x.Object!.Id == request.FolderId); + } + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) { logs = logs.Where(x => x.Action.Trim().ToLower().Contains(request.SearchTerm.Trim().ToLower())); } - + + return await logs .LoggingListPaginateAsync( request.Page, diff --git a/src/Application/ImportRequests/Queries/GetAllImportRequestsPaginated.cs b/src/Application/ImportRequests/Queries/GetAllImportRequestsPaginated.cs index acf14faa..f90d4991 100644 --- a/src/Application/ImportRequests/Queries/GetAllImportRequestsPaginated.cs +++ b/src/Application/ImportRequests/Queries/GetAllImportRequestsPaginated.cs @@ -34,17 +34,33 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) _mapper = mapper; } - public async Task> Handle(Query request, - CancellationToken cancellationToken) + public async Task> Handle(Query request, CancellationToken cancellationToken) { - if (request.CurrentUser.Role.IsStaff() - && request.RoomId is null) + if (request.CurrentUser.Role.IsStaff()) { - throw new UnauthorizedAccessException("User can not access this resource."); + if (request.RoomId is null) + { + throw new UnauthorizedAccessException("User can not access this resource."); + } + + var room = await _context.Rooms + .FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); + var roomDoesNotExist = room is null; + + if (roomDoesNotExist + || RoomIsNotInSameDepartment(request.CurrentUser, room!)) + { + throw new UnauthorizedAccessException("User can not access this resource."); + } } if (request.CurrentUser.Role.IsEmployee()) { + if (request.RoomId is null) + { + throw new UnauthorizedAccessException("User can not access this resource."); + } + var room = await _context.Rooms .FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); var roomDoesNotExist = room is null; @@ -84,7 +100,6 @@ public async Task> Handle(Query request, cancellationToken); } - private static bool RoomIsNotInSameDepartment(User user, Room room) => user.Department?.Id != room.DepartmentId; } diff --git a/src/Application/Lockers/Commands/AddLocker.cs b/src/Application/Lockers/Commands/AddLocker.cs index b39e6e82..15ee1535 100644 --- a/src/Application/Lockers/Commands/AddLocker.cs +++ b/src/Application/Lockers/Commands/AddLocker.cs @@ -115,11 +115,5 @@ private async Task DuplicatedNameLockerExistsInSameRoomAsync(string locker && x.Room.Id == roomId, cancellationToken); return locker is not null; } - - private static bool EqualsInvariant(string x, string y) - => x.Trim().ToLower().Equals(y.Trim().ToLower()); - - private static bool IsSameRoom(Guid roomId1, Guid roomId2) - => roomId1 == roomId2; } } \ No newline at end of file diff --git a/src/Application/Lockers/Commands/UpdateLocker.cs b/src/Application/Lockers/Commands/UpdateLocker.cs index a670703d..c1d0a472 100644 --- a/src/Application/Lockers/Commands/UpdateLocker.cs +++ b/src/Application/Lockers/Commands/UpdateLocker.cs @@ -107,20 +107,11 @@ private async Task DuplicatedNameLockerExistsInSameRoomAsync( CancellationToken cancellationToken) { var locker = await _context.Lockers.FirstOrDefaultAsync( - x => EqualsInvariant(x.Name, lockerName) - && IsNotSameLocker(x.Id, lockerId) - && IsSameRoom(x.Room.Id, roomId), + x => x.Name.Trim().ToLower().Equals(lockerName.ToLower()) + && x.Id != lockerId + && x.Room.Id == roomId, cancellationToken); return locker is not null; } - - private static bool EqualsInvariant(string x, string y) - => x.Trim().ToLower().Equals(y.Trim().ToLower()); - - private static bool IsSameRoom(Guid roomId1, Guid roomId2) - => roomId1 == roomId2; - - private static bool IsNotSameLocker(Guid lockerId1, Guid lockerId2) - => lockerId1 != lockerId2; } } \ No newline at end of file diff --git a/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs b/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs index eb52e7b6..64f949ca 100644 --- a/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs +++ b/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs @@ -1,4 +1,4 @@ -using Application.Common.Extensions; +using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Models; using Application.Common.Models.Dtos.Logging; @@ -18,7 +18,7 @@ public record Query : IRequest> public string CurrentUserRole { get; init; } = null!; public Guid CurrentUserDepartmentId { get; init; } public string? SearchTerm { get; init; } - public Guid? RoomId { get; init; } + public Guid? LockerId { get; init; } public int? Page { get; init; } public int? Size { get; init; } } @@ -39,7 +39,7 @@ public async Task> Handle(Query request, Cancellatio if (request.CurrentUserRole.IsStaff()) { - if (request.RoomId is null) + if (request.LockerId is null) { throw new UnauthorizedAccessException("User cannot access this resource."); } @@ -51,7 +51,7 @@ public async Task> Handle(Query request, Cancellatio throw new UnauthorizedAccessException("User cannot access this resource"); } - if (!IsSameRoom(currentRoom.Id, request.RoomId.Value)) + if (!IsSameRoom(currentRoom.Id, request.LockerId.Value)) { throw new UnauthorizedAccessException("User cannot access this resource"); } @@ -63,9 +63,9 @@ public async Task> Handle(Query request, Cancellatio .ThenInclude(x => x.Department) .AsQueryable(); - if (request.RoomId is not null) + if (request.LockerId is not null) { - logs = logs.Where(x => x.Object == null || x.Object.Room.Id == request.RoomId); + logs = logs.Where(x => x.Object!.Id == request.LockerId); } if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) diff --git a/src/Application/Rooms/Commands/UpdateRoom.cs b/src/Application/Rooms/Commands/UpdateRoom.cs index aa14ad82..b6ed3727 100644 --- a/src/Application/Rooms/Commands/UpdateRoom.cs +++ b/src/Application/Rooms/Commands/UpdateRoom.cs @@ -107,16 +107,10 @@ private async Task DuplicatedNameRoomExistsAsync( CancellationToken cancellationToken) { var room = await _context.Rooms.FirstOrDefaultAsync( - x => EqualsInvariant(x.Name, roomName) - && IsNotSameRoom(x.Id, roomId), + x => x.Name.Trim().ToLower().Equals(roomName.Trim().ToLower()) + && x.Id != roomId, cancellationToken); return room is not null; } - - private static bool EqualsInvariant(string x, string y) - => x.Trim().ToLower().Equals(y.Trim().ToLower()); - - private static bool IsNotSameRoom(Guid roomId1, Guid roomId2) - => roomId1 != roomId2; } } \ No newline at end of file diff --git a/src/Application/Rooms/Queries/GetRoomLogById.cs b/src/Application/Rooms/Queries/GetRoomLogById.cs deleted file mode 100644 index df0f4d45..00000000 --- a/src/Application/Rooms/Queries/GetRoomLogById.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Application.Common.Interfaces; -using Application.Common.Models.Dtos.Logging; -using AutoMapper; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Rooms.Queries; - -public class GetRoomLogById -{ - public record Query : IRequest - { - public Guid LogId { get; init; } - } - - public class QueryHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public QueryHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Query request, CancellationToken cancellationToken) - { - var log = await _context.RoomLogs - .Include(x => x.Object) - .Include(x => x.User) - .ThenInclude(x => x.Department) - .FirstOrDefaultAsync(x => x.Id == request.LogId, cancellationToken); - - if (log is null) - { - throw new KeyNotFoundException("Log does not exist."); - } - - return _mapper.Map(log); - } - } -} \ No newline at end of file diff --git a/src/Application/Users/Queries/GetUserById.cs b/src/Application/Users/Queries/GetUserById.cs index 45f20c2e..d49ddb29 100644 --- a/src/Application/Users/Queries/GetUserById.cs +++ b/src/Application/Users/Queries/GetUserById.cs @@ -49,9 +49,6 @@ public async Task Handle(Query request, CancellationToken cancellationT private static bool ViolateConstraints(string userRole, Guid userDepartmentId, User foundUser) => (userRole.IsStaff() || userRole.IsEmployee()) - && GetUserInOtherDepartment(userDepartmentId, foundUser); - - private static bool GetUserInOtherDepartment(Guid userDepartmentId, User foundUser) - => foundUser.Department?.Id != userDepartmentId; + && userDepartmentId != foundUser.Department?.Id; } } \ No newline at end of file diff --git a/tests/Application.Tests.Integration/Borrows/Commands/ApproveBorrowRequestTests.cs b/tests/Application.Tests.Integration/Borrows/Commands/ApproveBorrowRequestTests.cs index 87a40fd6..09f8d7bd 100644 --- a/tests/Application.Tests.Integration/Borrows/Commands/ApproveBorrowRequestTests.cs +++ b/tests/Application.Tests.Integration/Borrows/Commands/ApproveBorrowRequestTests.cs @@ -1,4 +1,4 @@ -using Application.Borrows.Commands; +using Application.Borrows.Commands; using Application.Common.Exceptions; using Application.Identity; using Domain.Entities.Physical; @@ -30,7 +30,7 @@ public async Task ShouldApproveRequest_WhenRequestIsValid() await AddAsync(request); - var command = new ApproveBorrowRequest.Command() + var command = new ApproveOrRejectBorrowRequest.Command() { BorrowId = request.Id, }; @@ -51,7 +51,7 @@ public async Task ShouldApproveRequest_WhenRequestIsValid() public async Task ShouldThrowKeyNotFoundException_WhenRequestDoesNotExist() { // Arrange - var command = new ApproveBorrowRequest.Command() + var command = new ApproveOrRejectBorrowRequest.Command() { BorrowId = Guid.NewGuid(), }; @@ -78,7 +78,7 @@ public async Task ShouldThrowConflictException_WhenDocumentIsLost() await AddAsync(request); - var command = new ApproveBorrowRequest.Command() + var command = new ApproveOrRejectBorrowRequest.Command() { BorrowId = request.Id, }; @@ -108,7 +108,7 @@ public async Task ShouldThrowConflictException_WhenRequestStatusIsNotPendingAndR await AddAsync(request); - var command = new ApproveBorrowRequest.Command() + var command = new ApproveOrRejectBorrowRequest.Command() { BorrowId = request.Id, }; @@ -146,7 +146,7 @@ public async Task ShouldThrowConflictException_WhenRequestTimespanOverlapAnAppro await context.AddAsync(request2); await context.SaveChangesAsync(); - var command = new ApproveBorrowRequest.Command() + var command = new ApproveOrRejectBorrowRequest.Command() { BorrowId = request2.Id, }; From 7b43de087148f160a529ef6f37ff478cfd28b640 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Tue, 20 Jun 2023 17:55:01 +0700 Subject: [PATCH 49/56] fuck --- src/Api/Controllers/BorrowsController.cs | 2 +- src/Api/Controllers/DepartmentsController.cs | 2 +- src/Api/Controllers/DocumentsController.cs | 16 +++++- src/Api/Controllers/FoldersController.cs | 4 +- .../Controllers/ImportRequestsController.cs | 3 ++ .../GetAllUsersPaginatedQueryParameters.cs | 2 +- src/Api/Controllers/RoomsController.cs | 8 ++- src/Api/Controllers/StaffsController.cs | 6 +-- src/Api/Middlewares/ExceptionMiddleware.cs | 2 +- .../Commands/ApproveOrRejectBorrowRequest.cs | 4 +- .../Borrows/Commands/BorrowDocument.cs | 2 +- .../Borrows/Commands/CancelBorrowRequest.cs | 2 +- .../Borrows/Commands/CheckoutDocument.cs | 2 +- .../Borrows/Commands/RejectBorrowRequest.cs | 2 +- .../Queries/GetAllRequestLogsPaginated.cs | 2 +- .../Common/Extensions/QueryableExtensions.cs | 2 +- .../Common/Extensions/StringExtensions.cs | 1 + .../Models/Dtos/Logging/DocumentLogDto.cs | 2 +- .../Models/Dtos/Logging/FolderLogDto.cs | 2 +- .../Models/Dtos/Logging/LockerLogDto.cs | 2 +- .../Models/Dtos/Logging/RequestLogDto.cs | 2 +- .../Common/Models/Dtos/Logging/RoomLogDto.cs | 2 +- .../Common/Models/Dtos/Logging/UserLogDto.cs | 2 +- .../Documents/Commands/DeleteDocument.cs | 2 +- .../Documents/Commands/ImportDocument.cs | 15 +++++- .../Documents/Commands/ShareDocument.cs | 2 +- .../Documents/Commands/UpdateDocument.cs | 2 +- .../Queries/GetAllDocumentLogsPaginated.cs | 4 +- .../GetAllDocumentsForEmployeePaginated.cs | 27 +++------- .../Queries/GetAllDocumentsPaginated.cs | 23 ++++++-- .../Documents/Queries/GetDocumentById.cs | 1 + src/Application/Folders/Commands/AddFolder.cs | 8 +-- .../Folders/Commands/UpdateFolder.cs | 33 +++++------- .../Queries/GetAllFolderLogsPaginated.cs | 4 +- .../Commands/ApproveOrRejectDocument.cs | 4 +- .../ImportRequests/Commands/AssignDocument.cs | 4 +- .../Commands/CheckinDocument.cs | 4 +- .../Commands/RequestImportDocument.cs | 2 +- src/Application/Lockers/Commands/AddLocker.cs | 2 +- .../Lockers/Commands/UpdateLocker.cs | 2 +- .../Queries/GetAllLockerLogsPaginated.cs | 4 +- src/Application/Rooms/Commands/AddRoom.cs | 2 +- src/Application/Rooms/Commands/RemoveRoom.cs | 12 +++-- src/Application/Rooms/Commands/UpdateRoom.cs | 7 +-- .../Rooms/Queries/GetAllRoomLogsPaginated.cs | 4 +- .../Rooms/Queries/GetAllRoomsPaginated.cs | 9 ++-- src/Application/Rooms/Queries/GetRoomById.cs | 2 +- .../Rooms/Queries/GetRoomByStaffId.cs | 8 +++ .../Commands/{AddStaff.cs => AssignStaff.cs} | 27 +++++++--- .../Staffs/Commands/RemoveStaff.cs | 2 +- .../Staffs/Commands/RemoveStaffFromRoom.cs | 2 +- .../Staffs/Queries/GetStaffByRoomId.cs | 9 ++++ src/Application/Users/Commands/AddUser.cs | 2 +- src/Application/Users/Commands/UpdateUser.cs | 2 +- .../Users/Queries/GetAllUserLogsPaginated.cs | 4 +- .../Users/Queries/GetAllUsersPaginated.cs | 2 +- src/Domain/Common/BaseLoggingEntity.cs | 5 +- src/Domain/Entities/Logging/DocumentLog.cs | 2 +- src/Domain/Entities/Logging/FolderLog.cs | 2 +- src/Domain/Entities/Logging/LockerLog.cs | 2 +- src/Domain/Entities/Logging/RequestLog.cs | 2 +- src/Domain/Entities/Logging/RoomLog.cs | 2 +- src/Domain/Entities/Logging/UserLog.cs | 2 +- src/Infrastructure/ConfigureServices.cs | 1 + .../Identity/IdentityService.cs | 2 +- .../Persistence/ApplicationDbContextSeed.cs | 20 +++---- .../Configurations/UserLogConfiguration.cs | 5 -- .../ApplicationDbContextModelSnapshot.cs | 52 +------------------ .../Services/PermissionManager.cs | 7 +-- 69 files changed, 213 insertions(+), 202 deletions(-) rename src/Application/Staffs/Commands/{AddStaff.cs => AssignStaff.cs} (73%) diff --git a/src/Api/Controllers/BorrowsController.cs b/src/Api/Controllers/BorrowsController.cs index 48841037..fedd6726 100644 --- a/src/Api/Controllers/BorrowsController.cs +++ b/src/Api/Controllers/BorrowsController.cs @@ -108,7 +108,7 @@ public async Task>>> GetAllRequests /// /// A BorrowDto of the approved borrow request [RequiresRole(IdentityData.Roles.Staff)] - [HttpPut("{borrowId:guid}")] + [HttpPut("staffs/{borrowId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] diff --git a/src/Api/Controllers/DepartmentsController.cs b/src/Api/Controllers/DepartmentsController.cs index d66d3456..1292c8e1 100644 --- a/src/Api/Controllers/DepartmentsController.cs +++ b/src/Api/Controllers/DepartmentsController.cs @@ -23,7 +23,7 @@ public DepartmentsController(ICurrentUserService currentUserService) } /// - /// Get back a department based on its id + /// Get back a room based on its department id /// /// id of the department to be retrieved /// A DepartmentDto of the retrieved department diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index 2a70eba2..2d500422 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -27,11 +27,13 @@ public DocumentsController(ICurrentUserService currentUserService) /// /// Id of the document to be retrieved /// A DocumentDto of the retrieved document + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff, IdentityData.Roles.Staff)] [HttpGet("{documentId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetById([FromRoute] Guid documentId) + public async Task>> GetById( + [FromRoute] Guid documentId) { var currentUser = _currentUserService.GetCurrentUser(); var query = new GetDocumentById.Query() @@ -42,7 +44,7 @@ public async Task>> GetById([FromRoute] Guid do var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); } - + /// /// Get all documents paginated /// @@ -57,8 +59,16 @@ public async Task>> GetById([FromRoute] Guid do public async Task>>> GetAllPaginated( [FromQuery] GetAllDocumentsPaginatedQueryParameters queryParameters) { + var currentUser = _currentUserService.GetCurrentUser(); + Guid? currentStaffRoomId = null; + if (currentUser.Role.IsStaff()) + { + currentStaffRoomId = _currentUserService.GetCurrentRoomForStaff(); + } var query = new GetAllDocumentsPaginated.Query() { + CurrentUser = currentUser, + CurrentStaffRoomId = currentStaffRoomId, UserId = queryParameters.UserId, RoomId = queryParameters.RoomId, LockerId = queryParameters.LockerId, @@ -138,6 +148,7 @@ public async Task>> Import( [FromBody] ImportDocumentRequest request) { var currentUser = _currentUserService.GetCurrentUser(); + var currentStaffRoomId = _currentUserService.GetCurrentRoomForStaff(); if (currentUser.Department is null) { return Forbid(); @@ -145,6 +156,7 @@ public async Task>> Import( var command = new ImportDocument.Command() { CurrentUser = currentUser, + CurrentStaffRoomId = currentStaffRoomId, Title = request.Title, Description = request.Description, DocumentType = request.DocumentType, diff --git a/src/Api/Controllers/FoldersController.cs b/src/Api/Controllers/FoldersController.cs index 76af4934..e62602ca 100644 --- a/src/Api/Controllers/FoldersController.cs +++ b/src/Api/Controllers/FoldersController.cs @@ -141,7 +141,9 @@ public async Task>> RemoveFolder([FromRoute] Guid [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Update([FromRoute] Guid folderId, [FromBody] UpdateFolderRequest request) + public async Task>> Update( + [FromRoute] Guid folderId, + [FromBody] UpdateFolderRequest request) { var currentUser = _currentUserService.GetCurrentUser(); var staffRoomId = _currentUserService.GetCurrentRoomForStaff(); diff --git a/src/Api/Controllers/ImportRequestsController.cs b/src/Api/Controllers/ImportRequestsController.cs index 3f5a5e27..2ff8d081 100644 --- a/src/Api/Controllers/ImportRequestsController.cs +++ b/src/Api/Controllers/ImportRequestsController.cs @@ -37,11 +37,14 @@ public ImportRequestsController(ICurrentUserService currentUserService) public async Task>> GetImportRequestById( [FromRoute] Guid importRequestId) { + var currentUserId = _currentUserService.GetId(); var currentUserRole = _currentUserService.GetRole(); var currentStaffRoomId = _currentUserService.GetCurrentRoomForStaff(); var query = new GetImportRequestById.Query() { + CurrentUserId = currentUserId, CurrentUserRole = currentUserRole, + CurrentStaffRoomId = currentStaffRoomId, RequestId = importRequestId, }; var result = await Mediator.Send(query); diff --git a/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs index 7175d8c1..738a1017 100644 --- a/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs +++ b/src/Api/Controllers/Payload/Requests/Users/GetAllUsersPaginatedQueryParameters.cs @@ -9,7 +9,7 @@ public class GetAllUsersPaginatedQueryParameters : PaginatedQueryParameters /// Id of the department to find users in /// public Guid[]? DepartmentIds { get; set; } - public string Role { get; set; } + public string? Role { get; set; } /// /// Search term /// diff --git a/src/Api/Controllers/RoomsController.cs b/src/Api/Controllers/RoomsController.cs index efcd6b40..f0603cae 100644 --- a/src/Api/Controllers/RoomsController.cs +++ b/src/Api/Controllers/RoomsController.cs @@ -28,7 +28,7 @@ public RoomsController(ICurrentUserService currentUserService) /// /// Id of the room to be retrieved /// A RoomDto of the retrieved room - [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff, IdentityData.Roles.Employee)] [HttpGet("{roomId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -105,15 +105,18 @@ public async Task>> GetEmptyContainer /// /// Id of the room to retrieve staff /// A StaffDto of the retrieved staff + [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpGet("{roomId:guid}/staffs")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> GetByRoom( + public async Task>> GetStaffByRoom( [FromRoute] Guid roomId) { + var currentUser = _currentUserService.GetCurrentUser(); var query = new GetStaffByRoomId.Query() { + CurrentUser = currentUser, RoomId = roomId, }; var result = await Mediator.Send(query); @@ -176,6 +179,7 @@ public async Task>> RemoveRoom( /// Id of the room to be updated /// Update room details /// A RoomDto of the updated room + [RequiresRole(IdentityData.Roles.Admin)] [HttpPut("{roomId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] diff --git a/src/Api/Controllers/StaffsController.cs b/src/Api/Controllers/StaffsController.cs index 7ccda68e..12452352 100644 --- a/src/Api/Controllers/StaffsController.cs +++ b/src/Api/Controllers/StaffsController.cs @@ -44,8 +44,8 @@ public async Task>> GetById( /// /// Get room by staff id /// - /// Id of the room to retrieve staff - /// A StaffDto of the retrieved staff + /// Id of the staff to retrieve room + /// A RoomDto of the retrieved room [RequiresRole(IdentityData.Roles.Admin, IdentityData.Roles.Staff)] [HttpGet("{staffId:guid}/rooms")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -100,7 +100,7 @@ public async Task>> Assign( [FromBody] AddStaffRequest request) { var currentUser = _currentUserService.GetCurrentUser(); - var command = new AddStaff.Command() + var command = new AssignStaff.Command() { CurrentUser = currentUser, RoomId = request.RoomId, diff --git a/src/Api/Middlewares/ExceptionMiddleware.cs b/src/Api/Middlewares/ExceptionMiddleware.cs index c08e16ed..3a52aae5 100644 --- a/src/Api/Middlewares/ExceptionMiddleware.cs +++ b/src/Api/Middlewares/ExceptionMiddleware.cs @@ -100,7 +100,7 @@ private static async void HandleAuthenticationException(HttpContext context, Exc private static async void HandleUnauthorizedAccessException(HttpContext context, Exception ex) { - context.Response.StatusCode = StatusCodes.Status401Unauthorized; + context.Response.StatusCode = StatusCodes.Status403Forbidden; await WriteExceptionMessageAsync(context, ex); } diff --git a/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs b/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs index 8b6fcbc9..880c614d 100644 --- a/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs +++ b/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs @@ -81,7 +81,7 @@ public async Task Handle(Command request, CancellationToken cancellat var log = new DocumentLog() { - Object = borrowRequest.Document, + ObjectId = borrowRequest.Document.Id, UserId = currentUser!.Id, User = currentUser, Time = localDateTimeNow, @@ -89,7 +89,7 @@ public async Task Handle(Command request, CancellationToken cancellat }; var requestLog = new RequestLog() { - Object = borrowRequest.Document, + ObjectId = borrowRequest.Document.Id, Type = RequestType.Borrow, UserId = currentUser.Id, User = currentUser, diff --git a/src/Application/Borrows/Commands/BorrowDocument.cs b/src/Application/Borrows/Commands/BorrowDocument.cs index ae13862c..d2642d43 100644 --- a/src/Application/Borrows/Commands/BorrowDocument.cs +++ b/src/Application/Borrows/Commands/BorrowDocument.cs @@ -159,7 +159,7 @@ or BorrowRequestStatus.CheckedOut { UserId = user.Id, User = user, - Object = document, + ObjectId = document.Id, Time = localDateTimeNow, Action = DocumentLogMessages.Borrow.NewBorrowRequest, }; diff --git a/src/Application/Borrows/Commands/CancelBorrowRequest.cs b/src/Application/Borrows/Commands/CancelBorrowRequest.cs index 669bafbb..f75b9837 100644 --- a/src/Application/Borrows/Commands/CancelBorrowRequest.cs +++ b/src/Application/Borrows/Commands/CancelBorrowRequest.cs @@ -49,7 +49,7 @@ public async Task Handle(Command request, CancellationToken cancellat var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); var log = new DocumentLog() { - Object = borrowRequest.Document, + ObjectId = borrowRequest.Document.Id, UserId = performingUser!.Id, User = performingUser, Time = LocalDateTime.FromDateTime(DateTime.Now), diff --git a/src/Application/Borrows/Commands/CheckoutDocument.cs b/src/Application/Borrows/Commands/CheckoutDocument.cs index a3fbea79..9d30014b 100644 --- a/src/Application/Borrows/Commands/CheckoutDocument.cs +++ b/src/Application/Borrows/Commands/CheckoutDocument.cs @@ -58,7 +58,7 @@ public async Task Handle(Command request, CancellationToken cancellat borrowRequest.Document.LastModifiedBy = performingUser!.Id; var log = new DocumentLog() { - Object = borrowRequest.Document, + ObjectId = borrowRequest.Document.Id, UserId = performingUser.Id, User = performingUser, Time = LocalDateTime.FromDateTime(DateTime.Now), diff --git a/src/Application/Borrows/Commands/RejectBorrowRequest.cs b/src/Application/Borrows/Commands/RejectBorrowRequest.cs index 2fcd0744..fe72f987 100644 --- a/src/Application/Borrows/Commands/RejectBorrowRequest.cs +++ b/src/Application/Borrows/Commands/RejectBorrowRequest.cs @@ -51,7 +51,7 @@ public async Task Handle(Command request, CancellationToken cancellat borrowRequest.Status = BorrowRequestStatus.Rejected; var requestLog = new RequestLog() { - Object = borrowRequest.Document, + ObjectId = borrowRequest.Document.Id, UserId = performingUser!.Id, User = performingUser, Time = LocalDateTime.FromDateTime(DateTime.Now), diff --git a/src/Application/Borrows/Queries/GetAllRequestLogsPaginated.cs b/src/Application/Borrows/Queries/GetAllRequestLogsPaginated.cs index 56dc9173..dc3176e8 100644 --- a/src/Application/Borrows/Queries/GetAllRequestLogsPaginated.cs +++ b/src/Application/Borrows/Queries/GetAllRequestLogsPaginated.cs @@ -32,7 +32,7 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task> Handle(Query request, CancellationToken cancellationToken) { var logs = _context.RequestLogs - .Include(x => x.Object) + .Include(x => x.ObjectId) .AsQueryable(); if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) diff --git a/src/Application/Common/Extensions/QueryableExtensions.cs b/src/Application/Common/Extensions/QueryableExtensions.cs index c3d0afc7..71587465 100644 --- a/src/Application/Common/Extensions/QueryableExtensions.cs +++ b/src/Application/Common/Extensions/QueryableExtensions.cs @@ -39,7 +39,7 @@ public static async Task> LoggingListPaginateAsync - where TLoggingEntity : BaseLoggingEntity + where TLoggingEntity : BaseLoggingEntity where TEntity : BaseEntity { var pageNumber = page is null or <= 0 ? 1 : page; diff --git a/src/Application/Common/Extensions/StringExtensions.cs b/src/Application/Common/Extensions/StringExtensions.cs index 1725822a..07f9a6e5 100644 --- a/src/Application/Common/Extensions/StringExtensions.cs +++ b/src/Application/Common/Extensions/StringExtensions.cs @@ -27,4 +27,5 @@ public static bool IsApproval(this string decision) public static bool IsRejection(this string decision) => decision.ToLower().Trim().Equals("reject"); + } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Logging/DocumentLogDto.cs b/src/Application/Common/Models/Dtos/Logging/DocumentLogDto.cs index c8ad9fdd..7e68116e 100644 --- a/src/Application/Common/Models/Dtos/Logging/DocumentLogDto.cs +++ b/src/Application/Common/Models/Dtos/Logging/DocumentLogDto.cs @@ -21,7 +21,7 @@ public void Mapping(Profile profile) .ForMember(dest => dest.Time, opt => opt.MapFrom(src => src.Time.ToDateTimeUnspecified())) .ForMember(dest => dest.Object, - opt => opt.MapFrom(src => src.Object)); + opt => opt.MapFrom(src => src.ObjectId)); } } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Logging/FolderLogDto.cs b/src/Application/Common/Models/Dtos/Logging/FolderLogDto.cs index e31d63bc..2717f224 100644 --- a/src/Application/Common/Models/Dtos/Logging/FolderLogDto.cs +++ b/src/Application/Common/Models/Dtos/Logging/FolderLogDto.cs @@ -19,6 +19,6 @@ public void Mapping(Profile profile) .ForMember( dest => dest.Time, opt => opt.MapFrom( src => src.Time.ToDateTimeUnspecified())) .ForMember(dest => dest.Object, - opt => opt.MapFrom( src => src.Object)); + opt => opt.MapFrom( src => src.ObjectId)); } } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Logging/LockerLogDto.cs b/src/Application/Common/Models/Dtos/Logging/LockerLogDto.cs index 53e64a29..8ce8516a 100644 --- a/src/Application/Common/Models/Dtos/Logging/LockerLogDto.cs +++ b/src/Application/Common/Models/Dtos/Logging/LockerLogDto.cs @@ -19,6 +19,6 @@ public void Mapping(Profile profile) .ForMember( dest => dest.Time, opt => opt.MapFrom( src => src.Time.ToDateTimeUnspecified())) .ForMember(dest => dest.Object, - opt => opt.MapFrom( src => src.Object)); + opt => opt.MapFrom( src => src.ObjectId)); } } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Logging/RequestLogDto.cs b/src/Application/Common/Models/Dtos/Logging/RequestLogDto.cs index ecf32e14..e0613a22 100644 --- a/src/Application/Common/Models/Dtos/Logging/RequestLogDto.cs +++ b/src/Application/Common/Models/Dtos/Logging/RequestLogDto.cs @@ -21,7 +21,7 @@ public void Mapping(Profile profile) .ForMember(dest => dest.Time, opt => opt.MapFrom(src => src.Time.ToDateTimeUnspecified())) .ForMember(dest => dest.Object, - opt => opt.MapFrom(src => src.Object)) + opt => opt.MapFrom(src => src.ObjectId)) .ForMember(dest => dest.Type, opt => opt.MapFrom(src => src.Type.ToString())); } diff --git a/src/Application/Common/Models/Dtos/Logging/RoomLogDto.cs b/src/Application/Common/Models/Dtos/Logging/RoomLogDto.cs index 833c14eb..7c2e832e 100644 --- a/src/Application/Common/Models/Dtos/Logging/RoomLogDto.cs +++ b/src/Application/Common/Models/Dtos/Logging/RoomLogDto.cs @@ -21,7 +21,7 @@ public void Mapping(Profile profile) .ForMember(dest => dest.Time, opt => opt.MapFrom(src => src.Time.ToDateTimeUnspecified())) .ForMember(dest => dest.Object, - opt => opt.MapFrom(src => src.Object)); + opt => opt.MapFrom(src => src.ObjectId)); } } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Logging/UserLogDto.cs b/src/Application/Common/Models/Dtos/Logging/UserLogDto.cs index 7929f13d..316cf930 100644 --- a/src/Application/Common/Models/Dtos/Logging/UserLogDto.cs +++ b/src/Application/Common/Models/Dtos/Logging/UserLogDto.cs @@ -18,7 +18,7 @@ public void Mapping(Profile profile) .ForMember( dest => dest.Time, opt => opt.MapFrom( src => src.Time.ToDateTimeUnspecified())) .ForMember(dest => dest.Object, - opt => opt.MapFrom( src => src.Object)); + opt => opt.MapFrom( src => src.ObjectId)); } } \ No newline at end of file diff --git a/src/Application/Documents/Commands/DeleteDocument.cs b/src/Application/Documents/Commands/DeleteDocument.cs index 494f4f45..b5455f63 100644 --- a/src/Application/Documents/Commands/DeleteDocument.cs +++ b/src/Application/Documents/Commands/DeleteDocument.cs @@ -52,7 +52,7 @@ public async Task Handle(Command request, CancellationToken cancell var log = new DocumentLog() { - Object = document, + ObjectId = document.Id, Time = localDateTimeNow, User = request.CurrentUser, UserId = request.CurrentUser.Id, diff --git a/src/Application/Documents/Commands/ImportDocument.cs b/src/Application/Documents/Commands/ImportDocument.cs index 7f449892..a60f64f2 100644 --- a/src/Application/Documents/Commands/ImportDocument.cs +++ b/src/Application/Documents/Commands/ImportDocument.cs @@ -19,6 +19,7 @@ public class ImportDocument public record Command : IRequest { public User CurrentUser { get; init; } = null!; + public Guid? CurrentStaffRoomId { get; init; } public string Title { get; init; } = null!; public string? Description { get; init; } public string DocumentType { get; init; } = null!; @@ -42,6 +43,11 @@ public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimePr public async Task Handle(Command request, CancellationToken cancellationToken) { + if (request.CurrentStaffRoomId is null) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + var importer = await _context.Users .Include(x => x.Department) .FirstOrDefaultAsync(x => x.Id == request.ImporterId, cancellationToken); @@ -70,6 +76,8 @@ public async Task Handle(Command request, CancellationToken cancell } var folder = await _context.Folders + .Include(x => x.Locker) + .ThenInclude(y => y.Room) .FirstOrDefaultAsync(x => x.Id == request.FolderId, cancellationToken); if (folder is null) { @@ -81,6 +89,11 @@ public async Task Handle(Command request, CancellationToken cancell throw new ConflictException("This folder cannot accept more documents."); } + if (folder.Locker.Room.Id != request.CurrentStaffRoomId) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); var entity = new Document() @@ -100,7 +113,7 @@ public async Task Handle(Command request, CancellationToken cancell { User = request.CurrentUser, UserId = request.CurrentUser.Id, - Object = entity, + ObjectId = entity.Id, Time = localDateTimeNow, Action = DocumentLogMessages.Import.NewImport, }; diff --git a/src/Application/Documents/Commands/ShareDocument.cs b/src/Application/Documents/Commands/ShareDocument.cs index 8e01b4e3..7b40ad74 100644 --- a/src/Application/Documents/Commands/ShareDocument.cs +++ b/src/Application/Documents/Commands/ShareDocument.cs @@ -72,7 +72,7 @@ public async Task Handle(Command request, CancellationToken cancell var log = new DocumentLog() { - Object = document, + ObjectId = document.Id, Time = localDateTimeNow, User = request.CurrentUser, UserId = request.CurrentUser.Id, diff --git a/src/Application/Documents/Commands/UpdateDocument.cs b/src/Application/Documents/Commands/UpdateDocument.cs index 6219a0a4..195cc593 100644 --- a/src/Application/Documents/Commands/UpdateDocument.cs +++ b/src/Application/Documents/Commands/UpdateDocument.cs @@ -101,7 +101,7 @@ public async Task Handle(Command request, CancellationToken cancell var log = new DocumentLog() { - Object = document, + ObjectId = document.Id, Time = localDateTimeNow, User = request.CurrentUser, UserId = request.CurrentUser.Id, diff --git a/src/Application/Documents/Queries/GetAllDocumentLogsPaginated.cs b/src/Application/Documents/Queries/GetAllDocumentLogsPaginated.cs index 1dea3495..db36ee66 100644 --- a/src/Application/Documents/Queries/GetAllDocumentLogsPaginated.cs +++ b/src/Application/Documents/Queries/GetAllDocumentLogsPaginated.cs @@ -34,14 +34,14 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task> Handle(Query request, CancellationToken cancellationToken) { var logs = _context.DocumentLogs - .Include(x => x.Object) + .Include(x => x.ObjectId) .Include(x => x.User) .ThenInclude(x => x.Department) .AsQueryable(); if (request.DocumentId is not null) { - logs = logs.Where(x => x.Object!.Id == request.DocumentId); + logs = logs.Where(x => x.ObjectId! == request.DocumentId); } if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) diff --git a/src/Application/Documents/Queries/GetAllDocumentsForEmployeePaginated.cs b/src/Application/Documents/Queries/GetAllDocumentsForEmployeePaginated.cs index 0c0edd84..ef5a6991 100644 --- a/src/Application/Documents/Queries/GetAllDocumentsForEmployeePaginated.cs +++ b/src/Application/Documents/Queries/GetAllDocumentsForEmployeePaginated.cs @@ -55,20 +55,20 @@ public async Task> Handle(Query request, if (request.IsPrivate) { var permissions = _context.Permissions.Where(x => - IsSameUser(x.EmployeeId, request.CurrentUserId) - && InSameDepartmentAsUser(x.Document, request.CurrentUserDepartmentId) - && HasReadPermission(x.AllowedOperations)) + x!.EmployeeId == request.CurrentUserId + && x.Document.Department!.Id == request.CurrentUserDepartmentId + && x.AllowedOperations.Contains(DocumentOperation.Read.ToString())) .Select(x => x.DocumentId); documents = documents.Where(x => - InSameDepartmentAsUser(x, request.CurrentUserDepartmentId) + x.Department!.Id == request.CurrentUserDepartmentId && x.IsPrivate - && (CanRead(permissions, x.Id) || IsImporter(x, request.CurrentUserId))); + && (permissions.Contains(x.Id) || x.ImporterId == request.CurrentUserId)); } else { documents = documents.Where(x => - InSameDepartmentAsUser(x, request.CurrentUserDepartmentId) + x.Department!.Id == request.CurrentUserDepartmentId && !x.IsPrivate); } @@ -98,20 +98,5 @@ public async Task> Handle(Query request, _mapper.ConfigurationProvider, cancellationToken); } - - private static bool IsSameUser(Guid userId1, Guid userId2) - => userId1 == userId2; - - private static bool InSameDepartmentAsUser(Document document, Guid userDepartmentId) - => document.Department!.Id == userDepartmentId; - - private static bool HasReadPermission(string allowedPermissions) - => allowedPermissions.Contains(DocumentOperation.Read.ToString()); - - private static bool CanRead(IEnumerable documentIds, Guid documentId) - => documentIds.Contains(documentId); - - private static bool IsImporter(Document document, Guid userId) - => document.ImporterId == userId; } } \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs b/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs index aaa60446..40ae1e6d 100644 --- a/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs +++ b/src/Application/Documents/Queries/GetAllDocumentsPaginated.cs @@ -6,6 +6,7 @@ using Application.Common.Models.Dtos.Physical; using AutoMapper; using AutoMapper.QueryableExtensions; +using Domain.Entities; using Domain.Entities.Physical; using Domain.Statuses; using FluentValidation; @@ -31,6 +32,8 @@ public Validator() public record Query : IRequest> { + public User CurrentUser { get; init; } = null!; + public Guid? CurrentStaffRoomId { get; init; } public Guid? UserId { get; init; } public Guid? RoomId { get; init; } public Guid? LockerId { get; init; } @@ -56,9 +59,21 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) _mapper = mapper; } - public async Task> Handle(Query request, - CancellationToken cancellationToken) + public async Task> Handle(Query request, CancellationToken cancellationToken) { + if (request.CurrentUser.Role.IsStaff()) + { + if (request.RoomId is null) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + + if (request.RoomId != request.CurrentStaffRoomId) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + } + var documents = _context.Documents.AsQueryable(); var roomExists = request.RoomId is not null; var lockerExists = request.LockerId is not null; @@ -67,7 +82,7 @@ public async Task> Handle(Query request, documents = documents .Include(x => x.Department) .Include(x => x.Folder) - .ThenInclude(y => y.Locker) + .ThenInclude(y => y!.Locker) .ThenInclude(z => z.Room); if (request.DocumentStatus is not null @@ -88,7 +103,7 @@ public async Task> Handle(Query request, if (request.Role is not null) { - documents = documents.Where(x => x.Importer!.Role.Equals(request.Role)); + documents = documents.Where(x => x.Importer!.Role.ToLower().Equals(request.Role.Trim().ToLower())); } if (folderExists) diff --git a/src/Application/Documents/Queries/GetDocumentById.cs b/src/Application/Documents/Queries/GetDocumentById.cs index 5300072a..cfaa653d 100644 --- a/src/Application/Documents/Queries/GetDocumentById.cs +++ b/src/Application/Documents/Queries/GetDocumentById.cs @@ -66,6 +66,7 @@ private static bool IsStaffAndNotInSameDepartment(User user, Document document) private bool IsEmployeeAndDoesNotHasReadPermission(User user, Document document) => user.Role.IsEmployee() + && document.ImporterId != user.Id && !_permissionManager.IsGranted(document.Id, DocumentOperation.Read, user.Id); } } \ No newline at end of file diff --git a/src/Application/Folders/Commands/AddFolder.cs b/src/Application/Folders/Commands/AddFolder.cs index 8a8c928c..9eff4671 100644 --- a/src/Application/Folders/Commands/AddFolder.cs +++ b/src/Application/Folders/Commands/AddFolder.cs @@ -108,7 +108,7 @@ public async Task Handle(Command request, CancellationToken cancellat { User = request.CurrentUser, UserId = request.CurrentUser.Id, - Object = entity, + ObjectId = entity.Id, Time = localDateTimeNow, Action = FolderLogMessage.Add, }; @@ -128,12 +128,6 @@ private async Task DuplicatedNameFolderExistsInSameLockerAsync(string fold return folder is not null; } - private static bool EqualsInvariant(string x, string y) - => x.Trim().ToLower().Equals(y.Trim().ToLower()); - - private static bool IsSameLocker(Guid lockerId1, Guid lockerId2) - => lockerId1 == lockerId2; - private static bool LockerIsInRoom(Locker locker, Guid? roomId) => roomId is not null && locker.Room.Id == roomId; } diff --git a/src/Application/Folders/Commands/UpdateFolder.cs b/src/Application/Folders/Commands/UpdateFolder.cs index a32c569a..cec1d243 100644 --- a/src/Application/Folders/Commands/UpdateFolder.cs +++ b/src/Application/Folders/Commands/UpdateFolder.cs @@ -49,11 +49,13 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -70,9 +72,9 @@ public async Task Handle(Command request, CancellationToken cancellat } if (request.CurrentUser.Role.IsStaff() - && (request.CurrentStaffRoomId is null || !FolderIsInRoom(folder, request.CurrentUser.Department!.Id))) + && (request.CurrentStaffRoomId is null || !FolderIsInRoom(folder, request.CurrentStaffRoomId!.Value))) { - throw new UnauthorizedAccessException("User cannot remove this resource."); + throw new UnauthorizedAccessException("User cannot access this resource."); } if (await DuplicatedNameFolderExistsInSameLockerAsync(request.Name, folder.Id, folder.Locker.Id, cancellationToken)) @@ -85,18 +87,20 @@ public async Task Handle(Command request, CancellationToken cancellat throw new ConflictException("New capacity cannot be less than current number of documents."); } + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + folder.Name = request.Name; folder.Description = request.Description; folder.Capacity = request.Capacity; - folder.LastModified = LocalDateTime.FromDateTime(DateTime.Now); + folder.LastModified = localDateTimeNow; folder.LastModifiedBy = request.CurrentUser.Id; var log = new FolderLog() { User = request.CurrentUser, UserId = request.CurrentUser.Id, - Object = folder, - Time = LocalDateTime.FromDateTime(DateTime.Now), + ObjectId = folder.Id, + Time = localDateTimeNow, Action = FolderLogMessage.Update, }; var result = _context.Folders.Update(folder); @@ -110,23 +114,14 @@ private async Task DuplicatedNameFolderExistsInSameLockerAsync( Guid folderId, CancellationToken cancellationToken) { - var folder = await _context.Lockers.FirstOrDefaultAsync( - x => EqualsInvariant(x.Name, folderName) - && IsNotSameFolder(x.Id, folderId) - && IsSameLocker(x.Room.Id, lockerId), + var folder = await _context.Folders.FirstOrDefaultAsync( + x => x.Name.Trim().ToLower().Equals(folderName.Trim().ToLower()) + && x.Id != folderId + && x.Locker.Id == lockerId, cancellationToken); return folder is not null; } - private static bool EqualsInvariant(string x, string y) - => x.Trim().ToLower().Equals(y.Trim().ToLower()); - - private static bool IsSameLocker(Guid lockerId1, Guid lockerId2) - => lockerId1 == lockerId2; - - private static bool IsNotSameFolder(Guid folderId1, Guid folderId2) - => folderId1 != folderId2; - private static bool FolderIsInRoom(Folder folder, Guid roomId) => folder.Locker.Room.Id == roomId; } diff --git a/src/Application/Folders/Queries/GetAllFolderLogsPaginated.cs b/src/Application/Folders/Queries/GetAllFolderLogsPaginated.cs index 6be59683..537c2dc0 100644 --- a/src/Application/Folders/Queries/GetAllFolderLogsPaginated.cs +++ b/src/Application/Folders/Queries/GetAllFolderLogsPaginated.cs @@ -34,12 +34,12 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task> Handle(Query request, CancellationToken cancellationToken) { var logs = _context.FolderLogs - .Include(x => x.Object) + .Include(x => x.ObjectId) .AsQueryable(); if (request.FolderId is not null) { - logs = logs.Where(x => x.Object!.Id == request.FolderId); + logs = logs.Where(x => x.ObjectId! == request.FolderId); } if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) diff --git a/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs b/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs index 9e591b0e..7590c5b6 100644 --- a/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs +++ b/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs @@ -74,7 +74,7 @@ public async Task Handle(Command request, CancellationToken ca var log = new DocumentLog() { - Object = document, + ObjectId = document.Id, Time = localDateTimeNow, User = request.CurrentUser, UserId = request.CurrentUser.Id, @@ -83,7 +83,7 @@ public async Task Handle(Command request, CancellationToken ca var requestLog = new RequestLog() { - Object = document, + ObjectId = document.Id, Time = localDateTimeNow, User = request.CurrentUser, UserId = request.CurrentUser.Id, diff --git a/src/Application/ImportRequests/Commands/AssignDocument.cs b/src/Application/ImportRequests/Commands/AssignDocument.cs index c32bc2a2..8b8f1fb0 100644 --- a/src/Application/ImportRequests/Commands/AssignDocument.cs +++ b/src/Application/ImportRequests/Commands/AssignDocument.cs @@ -80,7 +80,7 @@ public async Task Handle(Command request, CancellationToken ca var log = new DocumentLog() { - Object = importRequest.Document, + ObjectId = importRequest.Document.Id, Time = localDateTimeNow, User = request.CurrentUser, UserId = request.CurrentUser.Id, @@ -88,7 +88,7 @@ public async Task Handle(Command request, CancellationToken ca }; var folderLog = new FolderLog() { - Object = folder, + ObjectId = folder.Id, Time = localDateTimeNow, User = request.CurrentUser, UserId = request.CurrentUser.Id, diff --git a/src/Application/ImportRequests/Commands/CheckinDocument.cs b/src/Application/ImportRequests/Commands/CheckinDocument.cs index c72dbb16..c1e9d25f 100644 --- a/src/Application/ImportRequests/Commands/CheckinDocument.cs +++ b/src/Application/ImportRequests/Commands/CheckinDocument.cs @@ -78,7 +78,7 @@ public async Task Handle(Command request, CancellationToken cancell { User = request.CurrentUser, UserId = request.CurrentUser.Id, - Object = document, + ObjectId = document.Id, Time = localDateTimeNow, Action = DocumentLogMessages.Import.Checkin, }; @@ -86,7 +86,7 @@ public async Task Handle(Command request, CancellationToken cancell { User = request.CurrentUser, UserId = request.CurrentUser.Id, - Object = document, + ObjectId = document.Id, Time = localDateTimeNow, Action = RequestLogMessages.CheckInImport, }; diff --git a/src/Application/ImportRequests/Commands/RequestImportDocument.cs b/src/Application/ImportRequests/Commands/RequestImportDocument.cs index f387b2df..74f5b3c0 100644 --- a/src/Application/ImportRequests/Commands/RequestImportDocument.cs +++ b/src/Application/ImportRequests/Commands/RequestImportDocument.cs @@ -82,7 +82,7 @@ public async Task Handle(Command request, CancellationToken ca var log = new DocumentLog() { - Object = entity, + ObjectId = entity.Id, Time = localDateTimeNow, User = request.Issuer, UserId = request.Issuer.Id, diff --git a/src/Application/Lockers/Commands/AddLocker.cs b/src/Application/Lockers/Commands/AddLocker.cs index 15ee1535..f1a9097c 100644 --- a/src/Application/Lockers/Commands/AddLocker.cs +++ b/src/Application/Lockers/Commands/AddLocker.cs @@ -96,7 +96,7 @@ public async Task Handle(Command request, CancellationToken cancellat { User = request.CurrentUser, UserId = request.CurrentUser.Id, - Object = entity, + ObjectId = entity.Id, Time = localDateTimeNow, Action = LockerLogMessage.Add, }; diff --git a/src/Application/Lockers/Commands/UpdateLocker.cs b/src/Application/Lockers/Commands/UpdateLocker.cs index c1d0a472..e618129a 100644 --- a/src/Application/Lockers/Commands/UpdateLocker.cs +++ b/src/Application/Lockers/Commands/UpdateLocker.cs @@ -90,7 +90,7 @@ public async Task Handle(Command request, CancellationToken cancellat { User = request.CurrentUser, UserId = request.CurrentUser.Id, - Object = locker, + ObjectId = locker.Id, Time = localDateTimeNow, Action = LockerLogMessage.Update, }; diff --git a/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs b/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs index 64f949ca..6adb27be 100644 --- a/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs +++ b/src/Application/Lockers/Queries/GetAllLockerLogsPaginated.cs @@ -58,14 +58,14 @@ public async Task> Handle(Query request, Cancellatio } var logs = _context.LockerLogs - .Include(x => x.Object) + .Include(x => x.ObjectId) .Include(x => x.User) .ThenInclude(x => x.Department) .AsQueryable(); if (request.LockerId is not null) { - logs = logs.Where(x => x.Object!.Id == request.LockerId); + logs = logs.Where(x => x.ObjectId! == request.LockerId); } if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) diff --git a/src/Application/Rooms/Commands/AddRoom.cs b/src/Application/Rooms/Commands/AddRoom.cs index e62bfb75..031b1131 100644 --- a/src/Application/Rooms/Commands/AddRoom.cs +++ b/src/Application/Rooms/Commands/AddRoom.cs @@ -100,7 +100,7 @@ public async Task Handle(Command request, CancellationToken cancellatio { User = request.CurrentUser, UserId = request.CurrentUser.Id, - Object = entity, + ObjectId = entity.Id, Time = localDateTimeNow, Action = RoomLogMessage.Add, }; diff --git a/src/Application/Rooms/Commands/RemoveRoom.cs b/src/Application/Rooms/Commands/RemoveRoom.cs index 740da920..37307454 100644 --- a/src/Application/Rooms/Commands/RemoveRoom.cs +++ b/src/Application/Rooms/Commands/RemoveRoom.cs @@ -48,11 +48,15 @@ public async Task Handle(Command request, CancellationToken cancellatio throw new KeyNotFoundException("Room does not exist."); } - var canNotRemove = await _context.Documents - .AnyAsync(x => x.Department!.Id == room.DepartmentId, cancellationToken); - if (canNotRemove) + var containsDocuments = await _context.Documents + .AnyAsync(x => x.Folder!.Locker.Room.Id == room.Id, cancellationToken); + var containsFolders = await _context.Folders + .AnyAsync(x => x.Locker.Room.Id == room.Id, cancellationToken); + var containsLockers = await _context.Lockers + .AnyAsync(x => x.Room.Id == room.Id, cancellationToken); + if (containsDocuments || containsFolders || containsLockers) { - throw new ConflictException("Room cannot be removed because it contains documents."); + throw new ConflictException("Room cannot be removed because it contains something."); } var result = _context.Rooms.Remove(room); diff --git a/src/Application/Rooms/Commands/UpdateRoom.cs b/src/Application/Rooms/Commands/UpdateRoom.cs index b6ed3727..d585ae1d 100644 --- a/src/Application/Rooms/Commands/UpdateRoom.cs +++ b/src/Application/Rooms/Commands/UpdateRoom.cs @@ -60,6 +60,7 @@ public async Task Handle(Command request, CancellationToken cancellatio var room = await _context.Rooms .Include(x => x.Department) .Include(x => x.Staff) + .AsNoTracking() .FirstOrDefaultAsync(x => x.Id.Equals(request.RoomId), cancellationToken: cancellationToken); if (room is null) @@ -91,14 +92,14 @@ public async Task Handle(Command request, CancellationToken cancellatio { User = request.CurrentUser, UserId = request.CurrentUser.Id, - Object = room, + ObjectId = room.Id, Time = localDateTimeNow, Action = RoomLogMessage.Update, }; - var result = _context.Rooms.Update(room); + _context.Rooms.Entry(room).State = EntityState.Modified; await _context.RoomLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); + return _mapper.Map(room); } private async Task DuplicatedNameRoomExistsAsync( diff --git a/src/Application/Rooms/Queries/GetAllRoomLogsPaginated.cs b/src/Application/Rooms/Queries/GetAllRoomLogsPaginated.cs index 9b3658b8..37b9f7ff 100644 --- a/src/Application/Rooms/Queries/GetAllRoomLogsPaginated.cs +++ b/src/Application/Rooms/Queries/GetAllRoomLogsPaginated.cs @@ -34,14 +34,14 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task> Handle(Query request, CancellationToken cancellationToken) { var logs = _context.RoomLogs - .Include(x => x.Object) + .Include(x => x.ObjectId) .Include(x => x.User) .ThenInclude(x => x.Department) .AsQueryable(); if (request.RoomId is not null) { - logs = logs.Where(x => x.Object!.Id == request.RoomId); + logs = logs.Where(x => x.ObjectId! == request.RoomId); } if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) diff --git a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs index a075caed..b2961b8b 100644 --- a/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs +++ b/src/Application/Rooms/Queries/GetAllRoomsPaginated.cs @@ -43,13 +43,16 @@ public async Task> Handle(Query request, CancellationToke .Include(x => x.Staff) .AsQueryable(); - if (request.CurrentUser.Role.IsEmployee() + if ((request.CurrentUser.Role.IsStaff() || request.CurrentUser.Role.IsEmployee()) && request.CurrentUser.Department?.Id != request.DepartmentId) { throw new UnauthorizedAccessException("User cannot access this resource."); } - - rooms = rooms.Where(x => x.Department.Id == request.DepartmentId); + + if (request.DepartmentId is not null) + { + rooms = rooms.Where(x => x.Department.Id == request.DepartmentId); + } if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) { diff --git a/src/Application/Rooms/Queries/GetRoomById.cs b/src/Application/Rooms/Queries/GetRoomById.cs index 61108259..76532fbe 100644 --- a/src/Application/Rooms/Queries/GetRoomById.cs +++ b/src/Application/Rooms/Queries/GetRoomById.cs @@ -40,7 +40,7 @@ public async Task Handle(Query request, CancellationToken cancellationT throw new KeyNotFoundException("Room does not exist."); } - if (request.CurrentUserRole.IsStaff() + if ((request.CurrentUserRole.IsStaff() || request.CurrentUserRole.IsEmployee()) && !IsSameDepartment(request.CurrentUserDepartmentId, room.DepartmentId)) { throw new UnauthorizedAccessException("User cannot update this resource."); diff --git a/src/Application/Rooms/Queries/GetRoomByStaffId.cs b/src/Application/Rooms/Queries/GetRoomByStaffId.cs index 12bf6d1c..7a0c4f90 100644 --- a/src/Application/Rooms/Queries/GetRoomByStaffId.cs +++ b/src/Application/Rooms/Queries/GetRoomByStaffId.cs @@ -26,6 +26,14 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task Handle(Query request, CancellationToken cancellationToken) { + var staff = await _context.Staffs + .FirstOrDefaultAsync(x => x.Id == request.StaffId, cancellationToken); + + if (staff is null) + { + throw new KeyNotFoundException("Staff does not exists."); + } + var room = await _context.Rooms .Include(x => x.Staff) .ThenInclude(y => y!.User) diff --git a/src/Application/Staffs/Commands/AddStaff.cs b/src/Application/Staffs/Commands/AssignStaff.cs similarity index 73% rename from src/Application/Staffs/Commands/AddStaff.cs rename to src/Application/Staffs/Commands/AssignStaff.cs index 01b3ad50..3e569701 100644 --- a/src/Application/Staffs/Commands/AddStaff.cs +++ b/src/Application/Staffs/Commands/AssignStaff.cs @@ -12,7 +12,7 @@ namespace Application.Staffs.Commands; -public class AddStaff +public class AssignStaff { public record Command : IRequest { @@ -25,16 +25,19 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) { var staff = await _context.Staffs + .Include(x => x.User) .FirstOrDefaultAsync(x => x.Id == request.StaffId, cancellationToken); if (staff is null) @@ -51,21 +54,33 @@ public async Task Handle(Command request, CancellationToken cancellati throw new KeyNotFoundException("Room does not exist."); } + if (!room.IsAvailable) + { + throw new ConflictException("Room is not available."); + } + if (room.Staff is not null) { throw new ConflictException("Room already has a staff."); } + + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + + staff.Room = room; + room.Staff = staff; + room.LastModified = localDateTimeNow; + room.LastModifiedBy = request.CurrentUser.Id; var log = new UserLog() { User = request.CurrentUser, UserId = request.CurrentUser.Id, - Object = staff.User, - Time = LocalDateTime.FromDateTime(DateTime.Now), + ObjectId = staff.User.Id, + Time = localDateTimeNow, Action = UserLogMessages.Staff.AssignStaff(room.Id.ToString()), }; - - var result = await _context.Staffs.AddAsync(staff, cancellationToken); + _context.Rooms.Update(room); + var result = _context.Staffs.Update(staff); await _context.UserLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); diff --git a/src/Application/Staffs/Commands/RemoveStaff.cs b/src/Application/Staffs/Commands/RemoveStaff.cs index d3d42d3c..f70c1f34 100644 --- a/src/Application/Staffs/Commands/RemoveStaff.cs +++ b/src/Application/Staffs/Commands/RemoveStaff.cs @@ -46,7 +46,7 @@ public async Task Handle(Command request, CancellationToken cancellati { User = performingUser!, UserId = performingUser!.Id, - Object = staff.User, + ObjectId = staff.User.Id, Time = LocalDateTime.FromDateTime(DateTime.Now), Action = UserLogMessages.Staff.Remove, }; diff --git a/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs b/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs index 54a9ed99..448c2093 100644 --- a/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs +++ b/src/Application/Staffs/Commands/RemoveStaffFromRoom.cs @@ -59,7 +59,7 @@ public async Task Handle(Command request, CancellationToken cancellati { User = request.CurrentUser, UserId = request.CurrentUser.Id, - Object = staff.User, + ObjectId = staff.User.Id, Time = localDateTimeNow, Action = UserLogMessages.Staff.RemoveFromRoom, }; diff --git a/src/Application/Staffs/Queries/GetStaffByRoomId.cs b/src/Application/Staffs/Queries/GetStaffByRoomId.cs index 4b75b7f1..119b96f1 100644 --- a/src/Application/Staffs/Queries/GetStaffByRoomId.cs +++ b/src/Application/Staffs/Queries/GetStaffByRoomId.cs @@ -1,6 +1,8 @@ +using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; using MediatR; using Microsoft.EntityFrameworkCore; @@ -10,6 +12,7 @@ public class GetStaffByRoomId { public record Query : IRequest { + public User CurrentUser { get; init; } = null!; public Guid RoomId { get; init; } } @@ -37,6 +40,12 @@ public async Task Handle(Query request, CancellationToken cancellation throw new KeyNotFoundException("Room does not exist."); } + if (request.CurrentUser.Role.IsStaff() + && room.DepartmentId != request.CurrentUser.Department!.Id) + { + throw new UnauthorizedAccessException("User cannot access this resource."); + } + if (room.Staff is null) { throw new KeyNotFoundException("Staff does not exist."); diff --git a/src/Application/Users/Commands/AddUser.cs b/src/Application/Users/Commands/AddUser.cs index 2437ea20..35eedace 100644 --- a/src/Application/Users/Commands/AddUser.cs +++ b/src/Application/Users/Commands/AddUser.cs @@ -130,7 +130,7 @@ public async Task Handle(Command request, CancellationToken cancellatio { User = request.CurrentUser, UserId = request.CurrentUser.Id, - Object = entity, + ObjectId = entity.Id, Time = localDateTimeNow, Action = UserLogMessages.Add(entity.Role), }; diff --git a/src/Application/Users/Commands/UpdateUser.cs b/src/Application/Users/Commands/UpdateUser.cs index f28a8a66..8cb22bcd 100644 --- a/src/Application/Users/Commands/UpdateUser.cs +++ b/src/Application/Users/Commands/UpdateUser.cs @@ -85,7 +85,7 @@ public async Task Handle(Command request, CancellationToken cancellatio { User = request.CurrentUser, UserId = request.CurrentUser.Id, - Object = user, + ObjectId = user.Id, Time = localDateTimeNow, Action = UserLogMessages.Update, }; diff --git a/src/Application/Users/Queries/GetAllUserLogsPaginated.cs b/src/Application/Users/Queries/GetAllUserLogsPaginated.cs index 6b459cac..ee1c3e7c 100644 --- a/src/Application/Users/Queries/GetAllUserLogsPaginated.cs +++ b/src/Application/Users/Queries/GetAllUserLogsPaginated.cs @@ -34,14 +34,14 @@ public QueryHandler(IApplicationDbContext context, IMapper mapper) public async Task> Handle(Query request, CancellationToken cancellationToken) { var logs = _context.UserLogs - .Include(x => x.Object) + .Include(x => x.ObjectId) .Include(x => x.User) .ThenInclude(x => x.Department) .AsQueryable(); if (request.UserId is not null) { - logs = logs.Where(x => x.Object!.Id == request.UserId); + logs = logs.Where(x => x.ObjectId! == request.UserId); } if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) diff --git a/src/Application/Users/Queries/GetAllUsersPaginated.cs b/src/Application/Users/Queries/GetAllUsersPaginated.cs index f9a0a167..27d57746 100644 --- a/src/Application/Users/Queries/GetAllUsersPaginated.cs +++ b/src/Application/Users/Queries/GetAllUsersPaginated.cs @@ -46,7 +46,7 @@ public async Task> Handle(Query request, CancellationToke // Filter by role if (request.Role is not null) { - users = users.Where(x => x.Role.Equals(request.Role)); + users = users.Where(x => x.Role.ToLower().Equals(request.Role.Trim().ToLower())); } // Search diff --git a/src/Domain/Common/BaseLoggingEntity.cs b/src/Domain/Common/BaseLoggingEntity.cs index f6dfe0df..d7379232 100644 --- a/src/Domain/Common/BaseLoggingEntity.cs +++ b/src/Domain/Common/BaseLoggingEntity.cs @@ -3,12 +3,11 @@ namespace Domain.Common; -public class BaseLoggingEntity : BaseEntity - where T : BaseEntity +public class BaseLoggingEntity : BaseEntity { public string Action { get; set; } = null!; public Guid UserId { get; set; } - public T? Object { get; set; } + public Guid? ObjectId { get; set; } public LocalDateTime Time { get; set; } public User User { get; set; } = null!; diff --git a/src/Domain/Entities/Logging/DocumentLog.cs b/src/Domain/Entities/Logging/DocumentLog.cs index ae9ecced..e52fae0c 100644 --- a/src/Domain/Entities/Logging/DocumentLog.cs +++ b/src/Domain/Entities/Logging/DocumentLog.cs @@ -3,7 +3,7 @@ namespace Domain.Entities.Logging; -public class DocumentLog : BaseLoggingEntity +public class DocumentLog : BaseLoggingEntity { public Folder? BaseFolder { get; set; } } \ No newline at end of file diff --git a/src/Domain/Entities/Logging/FolderLog.cs b/src/Domain/Entities/Logging/FolderLog.cs index 0cfa7f0d..e41cbdf2 100644 --- a/src/Domain/Entities/Logging/FolderLog.cs +++ b/src/Domain/Entities/Logging/FolderLog.cs @@ -3,7 +3,7 @@ namespace Domain.Entities.Logging; -public class FolderLog : BaseLoggingEntity +public class FolderLog : BaseLoggingEntity { public Locker? BaseLocker { get; set; } } \ No newline at end of file diff --git a/src/Domain/Entities/Logging/LockerLog.cs b/src/Domain/Entities/Logging/LockerLog.cs index 3a0d309e..96c8ea44 100644 --- a/src/Domain/Entities/Logging/LockerLog.cs +++ b/src/Domain/Entities/Logging/LockerLog.cs @@ -3,7 +3,7 @@ namespace Domain.Entities.Logging; -public class LockerLog : BaseLoggingEntity +public class LockerLog : BaseLoggingEntity { public Room? BaseRoom { get; set; } } \ No newline at end of file diff --git a/src/Domain/Entities/Logging/RequestLog.cs b/src/Domain/Entities/Logging/RequestLog.cs index fbf7d568..5e801092 100644 --- a/src/Domain/Entities/Logging/RequestLog.cs +++ b/src/Domain/Entities/Logging/RequestLog.cs @@ -4,7 +4,7 @@ namespace Domain.Entities.Logging; -public class RequestLog : BaseLoggingEntity +public class RequestLog : BaseLoggingEntity { public RequestType Type { get; set; } } \ No newline at end of file diff --git a/src/Domain/Entities/Logging/RoomLog.cs b/src/Domain/Entities/Logging/RoomLog.cs index 666c0733..b662a46f 100644 --- a/src/Domain/Entities/Logging/RoomLog.cs +++ b/src/Domain/Entities/Logging/RoomLog.cs @@ -3,6 +3,6 @@ namespace Domain.Entities.Logging; -public class RoomLog : BaseLoggingEntity +public class RoomLog : BaseLoggingEntity { } \ No newline at end of file diff --git a/src/Domain/Entities/Logging/UserLog.cs b/src/Domain/Entities/Logging/UserLog.cs index 2310fb75..32735697 100644 --- a/src/Domain/Entities/Logging/UserLog.cs +++ b/src/Domain/Entities/Logging/UserLog.cs @@ -2,6 +2,6 @@ namespace Domain.Entities.Logging; -public class UserLog : BaseLoggingEntity +public class UserLog : BaseLoggingEntity { } \ No newline at end of file diff --git a/src/Infrastructure/ConfigureServices.cs b/src/Infrastructure/ConfigureServices.cs index 1523448f..710b935b 100644 --- a/src/Infrastructure/ConfigureServices.cs +++ b/src/Infrastructure/ConfigureServices.cs @@ -7,6 +7,7 @@ using Infrastructure.Persistence; using Infrastructure.Services; using Infrastructure.Shared; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; diff --git a/src/Infrastructure/Identity/IdentityService.cs b/src/Infrastructure/Identity/IdentityService.cs index eab0be8b..ddf84780 100644 --- a/src/Infrastructure/Identity/IdentityService.cs +++ b/src/Infrastructure/Identity/IdentityService.cs @@ -195,7 +195,7 @@ public async Task RefreshTokenAsync(string token, string r { var user = _applicationDbContext.Users .Include(x => x.Department) - .FirstOrDefault(x => x.Email!.Equals(email)); + .FirstOrDefault(x => x.Email.ToLower().Equals(email.Trim().ToLower())); if (user is null || !user.PasswordHash.Equals(password.HashPasswordWith(user.PasswordSalt, _securitySettings.Pepper))) { diff --git a/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs b/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs index f788d804..394444b2 100644 --- a/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs +++ b/src/Infrastructure/Persistence/ApplicationDbContextSeed.cs @@ -86,11 +86,6 @@ private static async Task TrySeedAsync(ApplicationDbContext context, string pepp admin.Department = department; await context.Users.AddAsync(admin); } - if (context.Users.All(u => u.Username != staff.Username)) - { - staff.Department = department; - await context.Users.AddAsync(staff); - } } else { @@ -100,11 +95,6 @@ private static async Task TrySeedAsync(ApplicationDbContext context, string pepp admin.Department = departmentEntity; await context.Users.AddAsync(admin); } - if (context.Users.All(u => u.Username != staff.Username)) - { - staff.Department = departmentEntity; - await context.Users.AddAsync(staff); - } } if (context.Departments.All(u => u.Name != itDepartment.Name)) @@ -115,6 +105,11 @@ private static async Task TrySeedAsync(ApplicationDbContext context, string pepp employee.Department = itDepartment; await context.Users.AddAsync(employee); } + if (context.Users.All(u => u.Username != staff.Username)) + { + staff.Department = department; + await context.Users.AddAsync(staff); + } } else { @@ -124,6 +119,11 @@ private static async Task TrySeedAsync(ApplicationDbContext context, string pepp employee.Department = departmentEntity; await context.Users.AddAsync(employee); } + if (context.Users.All(u => u.Username != staff.Username)) + { + staff.Department = departmentEntity; + await context.Users.AddAsync(staff); + } } await context.SaveChangesAsync(); diff --git a/src/Infrastructure/Persistence/Configurations/UserLogConfiguration.cs b/src/Infrastructure/Persistence/Configurations/UserLogConfiguration.cs index 92f2fe16..09f6db4c 100644 --- a/src/Infrastructure/Persistence/Configurations/UserLogConfiguration.cs +++ b/src/Infrastructure/Persistence/Configurations/UserLogConfiguration.cs @@ -16,10 +16,5 @@ public void Configure(EntityTypeBuilder builder) .WithMany() .HasForeignKey(x => x.UserId) .IsRequired(); - - builder.HasOne(x => x.Object) - .WithMany() - .HasForeignKey("ObjectId") - .IsRequired(); } } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index 46bffd08..70a25d80 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -130,8 +130,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("BaseFolderId"); - b.HasIndex("ObjectId"); - b.HasIndex("UserId"); b.ToTable("DocumentLogs"); @@ -163,8 +161,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("BaseLockerId"); - b.HasIndex("ObjectId"); - b.HasIndex("UserId"); b.ToTable("FolderLogs"); @@ -196,8 +192,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("BaseRoomId"); - b.HasIndex("ObjectId"); - b.HasIndex("UserId"); b.ToTable("LockerLogs"); @@ -227,8 +221,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.HasIndex("ObjectId"); - b.HasIndex("UserId"); b.ToTable("RequestLogs"); @@ -255,8 +247,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.HasIndex("ObjectId"); - b.HasIndex("UserId"); b.ToTable("RoomLogs"); @@ -272,7 +262,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("ObjectId") + b.Property("ObjectId") .HasColumnType("uuid"); b.Property("Time") @@ -283,8 +273,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.HasIndex("ObjectId"); - b.HasIndex("UserId"); b.ToTable("UserLogs"); @@ -784,10 +772,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .WithMany() .HasForeignKey("BaseFolderId"); - b.HasOne("Domain.Entities.Physical.Document", "Object") - .WithMany() - .HasForeignKey("ObjectId"); - b.HasOne("Domain.Entities.User", "User") .WithMany() .HasForeignKey("UserId") @@ -796,8 +780,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("BaseFolder"); - b.Navigation("Object"); - b.Navigation("User"); }); @@ -807,10 +789,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .WithMany() .HasForeignKey("BaseLockerId"); - b.HasOne("Domain.Entities.Physical.Folder", "Object") - .WithMany() - .HasForeignKey("ObjectId"); - b.HasOne("Domain.Entities.User", "User") .WithMany() .HasForeignKey("UserId") @@ -819,8 +797,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("BaseLocker"); - b.Navigation("Object"); - b.Navigation("User"); }); @@ -830,10 +806,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .WithMany() .HasForeignKey("BaseRoomId"); - b.HasOne("Domain.Entities.Physical.Locker", "Object") - .WithMany() - .HasForeignKey("ObjectId"); - b.HasOne("Domain.Entities.User", "User") .WithMany() .HasForeignKey("UserId") @@ -842,61 +814,39 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("BaseRoom"); - b.Navigation("Object"); - b.Navigation("User"); }); modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => { - b.HasOne("Domain.Entities.Physical.Document", "Object") - .WithMany() - .HasForeignKey("ObjectId"); - b.HasOne("Domain.Entities.User", "User") .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("Object"); - b.Navigation("User"); }); modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => { - b.HasOne("Domain.Entities.Physical.Room", "Object") - .WithMany() - .HasForeignKey("ObjectId"); - b.HasOne("Domain.Entities.User", "User") .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("Object"); - b.Navigation("User"); }); modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => { - b.HasOne("Domain.Entities.User", "Object") - .WithMany() - .HasForeignKey("ObjectId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - b.HasOne("Domain.Entities.User", "User") .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("Object"); - b.Navigation("User"); }); diff --git a/src/Infrastructure/Services/PermissionManager.cs b/src/Infrastructure/Services/PermissionManager.cs index 8fb62dc0..81137b75 100644 --- a/src/Infrastructure/Services/PermissionManager.cs +++ b/src/Infrastructure/Services/PermissionManager.cs @@ -18,9 +18,10 @@ public PermissionManager(IApplicationDbContext context) public bool IsGranted(Guid documentId, DocumentOperation operation, params Guid[] userIds) { - return Array.TrueForAll(userIds, id => !_context.Permissions.Any(x => - x.DocumentId == documentId && x.EmployeeId == id && - !x.AllowedOperations.Contains(operation.ToString()))); + return Array.TrueForAll(userIds, id => _context.Permissions.Any(x => + x.DocumentId == documentId + && x.EmployeeId == id + && x.AllowedOperations.Contains(operation.ToString()))); } public async Task GrantAsync(Document document, DocumentOperation operation, User[] users, DateTime expiryDate, CancellationToken cancellationToken) From 2e0e0d54f5f7157192e7d8afccd85bbc94daf9e2 Mon Sep 17 00:00:00 2001 From: Vzart Date: Tue, 20 Jun 2023 19:29:02 +0700 Subject: [PATCH 50/56] add: migration for Logging --- src/Application/Users/Commands/AddUser.cs | 14 +- ...20122747_LoggingNowHasObjectId.Designer.cs | 1060 +++++++++++++++++ .../20230620122747_LoggingNowHasObjectId.cs | 22 + 3 files changed, 1090 insertions(+), 6 deletions(-) create mode 100644 src/Infrastructure/Persistence/Migrations/20230620122747_LoggingNowHasObjectId.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230620122747_LoggingNowHasObjectId.cs diff --git a/src/Application/Users/Commands/AddUser.cs b/src/Application/Users/Commands/AddUser.cs index 35eedace..a3ad9461 100644 --- a/src/Application/Users/Commands/AddUser.cs +++ b/src/Application/Users/Commands/AddUser.cs @@ -126,6 +126,14 @@ public async Task Handle(Command request, CancellationToken cancellatio CreatedBy = request.CurrentUser.Id, }; + + entity.AddDomainEvent(new UserCreatedEvent(entity, password)); + if (request.Role.IsStaff()) + { + entity.AddDomainEvent(new StaffCreatedEvent(entity, request.CurrentUser)); + } + var result = await _context.Users.AddAsync(entity, cancellationToken); + var log = new UserLog() { User = request.CurrentUser, @@ -134,12 +142,6 @@ public async Task Handle(Command request, CancellationToken cancellatio Time = localDateTimeNow, Action = UserLogMessages.Add(entity.Role), }; - entity.AddDomainEvent(new UserCreatedEvent(entity, password)); - if (request.Role.IsStaff()) - { - entity.AddDomainEvent(new StaffCreatedEvent(entity, request.CurrentUser)); - } - var result = await _context.Users.AddAsync(entity, cancellationToken); await _context.UserLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); diff --git a/src/Infrastructure/Persistence/Migrations/20230620122747_LoggingNowHasObjectId.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230620122747_LoggingNowHasObjectId.Designer.cs new file mode 100644 index 00000000..0b6ea03b --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230620122747_LoggingNowHasObjectId.Designer.cs @@ -0,0 +1,1060 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230620122747_LoggingNowHasObjectId")] + partial class LoggingNowHasObjectId + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseFolderId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseFolderId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseLockerId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseLockerId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseRoomId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseRoomId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId") + .IsUnique(); + + b.HasIndex("RoomId"); + + b.ToTable("ImportRequests"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "BaseFolder") + .WithMany() + .HasForeignKey("BaseFolderId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseFolder"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "BaseLocker") + .WithMany() + .HasForeignKey("BaseLockerId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseLocker"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "BaseRoom") + .WithMany() + .HasForeignKey("BaseRoomId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseRoom"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Entry"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.ImportRequest", "DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany() + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany("Rooms") + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230620122747_LoggingNowHasObjectId.cs b/src/Infrastructure/Persistence/Migrations/20230620122747_LoggingNowHasObjectId.cs new file mode 100644 index 00000000..dfefb433 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230620122747_LoggingNowHasObjectId.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class LoggingNowHasObjectId : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} From 7da027f6cd72c4a8c0da29c228a77335d9e14dae Mon Sep 17 00:00:00 2001 From: Vzart Date: Tue, 20 Jun 2023 20:15:25 +0700 Subject: [PATCH 51/56] fix: migrations work now --- .../20230620122747_LoggingNowHasObjectId.cs | 22 --- ...0131154_LoggingNowHasObjectId.Designer.cs} | 2 +- .../20230620131154_LoggingNowHasObjectId.cs | 158 ++++++++++++++++++ 3 files changed, 159 insertions(+), 23 deletions(-) delete mode 100644 src/Infrastructure/Persistence/Migrations/20230620122747_LoggingNowHasObjectId.cs rename src/Infrastructure/Persistence/Migrations/{20230620122747_LoggingNowHasObjectId.Designer.cs => 20230620131154_LoggingNowHasObjectId.Designer.cs} (99%) create mode 100644 src/Infrastructure/Persistence/Migrations/20230620131154_LoggingNowHasObjectId.cs diff --git a/src/Infrastructure/Persistence/Migrations/20230620122747_LoggingNowHasObjectId.cs b/src/Infrastructure/Persistence/Migrations/20230620122747_LoggingNowHasObjectId.cs deleted file mode 100644 index dfefb433..00000000 --- a/src/Infrastructure/Persistence/Migrations/20230620122747_LoggingNowHasObjectId.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Infrastructure.Persistence.Migrations -{ - /// - public partial class LoggingNowHasObjectId : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - - } - } -} diff --git a/src/Infrastructure/Persistence/Migrations/20230620122747_LoggingNowHasObjectId.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230620131154_LoggingNowHasObjectId.Designer.cs similarity index 99% rename from src/Infrastructure/Persistence/Migrations/20230620122747_LoggingNowHasObjectId.Designer.cs rename to src/Infrastructure/Persistence/Migrations/20230620131154_LoggingNowHasObjectId.Designer.cs index 0b6ea03b..77f26586 100644 --- a/src/Infrastructure/Persistence/Migrations/20230620122747_LoggingNowHasObjectId.Designer.cs +++ b/src/Infrastructure/Persistence/Migrations/20230620131154_LoggingNowHasObjectId.Designer.cs @@ -13,7 +13,7 @@ namespace Infrastructure.Persistence.Migrations { [DbContext(typeof(ApplicationDbContext))] - [Migration("20230620122747_LoggingNowHasObjectId")] + [Migration("20230620131154_LoggingNowHasObjectId")] partial class LoggingNowHasObjectId { /// diff --git a/src/Infrastructure/Persistence/Migrations/20230620131154_LoggingNowHasObjectId.cs b/src/Infrastructure/Persistence/Migrations/20230620131154_LoggingNowHasObjectId.cs new file mode 100644 index 00000000..28d5c38d --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230620131154_LoggingNowHasObjectId.cs @@ -0,0 +1,158 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class LoggingNowHasObjectId : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_DocumentLogs_Documents_ObjectId", + table: "DocumentLogs"); + + migrationBuilder.DropForeignKey( + name: "FK_FolderLogs_Folders_ObjectId", + table: "FolderLogs"); + + migrationBuilder.DropForeignKey( + name: "FK_LockerLogs_Lockers_ObjectId", + table: "LockerLogs"); + + migrationBuilder.DropForeignKey( + name: "FK_RequestLogs_Documents_ObjectId", + table: "RequestLogs"); + + migrationBuilder.DropForeignKey( + name: "FK_RoomLogs_Rooms_ObjectId", + table: "RoomLogs"); + + migrationBuilder.DropForeignKey( + name: "FK_UserLogs_Users_ObjectId", + table: "UserLogs"); + + migrationBuilder.DropIndex( + name: "IX_UserLogs_ObjectId", + table: "UserLogs"); + + migrationBuilder.DropIndex( + name: "IX_RoomLogs_ObjectId", + table: "RoomLogs"); + + migrationBuilder.DropIndex( + name: "IX_RequestLogs_ObjectId", + table: "RequestLogs"); + + migrationBuilder.DropIndex( + name: "IX_LockerLogs_ObjectId", + table: "LockerLogs"); + + migrationBuilder.DropIndex( + name: "IX_FolderLogs_ObjectId", + table: "FolderLogs"); + + migrationBuilder.DropIndex( + name: "IX_DocumentLogs_ObjectId", + table: "DocumentLogs"); + + migrationBuilder.AlterColumn( + name: "ObjectId", + table: "UserLogs", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "ObjectId", + table: "UserLogs", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.CreateIndex( + name: "IX_UserLogs_ObjectId", + table: "UserLogs", + column: "ObjectId"); + + migrationBuilder.CreateIndex( + name: "IX_RoomLogs_ObjectId", + table: "RoomLogs", + column: "ObjectId"); + + migrationBuilder.CreateIndex( + name: "IX_RequestLogs_ObjectId", + table: "RequestLogs", + column: "ObjectId"); + + migrationBuilder.CreateIndex( + name: "IX_LockerLogs_ObjectId", + table: "LockerLogs", + column: "ObjectId"); + + migrationBuilder.CreateIndex( + name: "IX_FolderLogs_ObjectId", + table: "FolderLogs", + column: "ObjectId"); + + migrationBuilder.CreateIndex( + name: "IX_DocumentLogs_ObjectId", + table: "DocumentLogs", + column: "ObjectId"); + + migrationBuilder.AddForeignKey( + name: "FK_DocumentLogs_Documents_ObjectId", + table: "DocumentLogs", + column: "ObjectId", + principalTable: "Documents", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_FolderLogs_Folders_ObjectId", + table: "FolderLogs", + column: "ObjectId", + principalTable: "Folders", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_LockerLogs_Lockers_ObjectId", + table: "LockerLogs", + column: "ObjectId", + principalTable: "Lockers", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_RequestLogs_Documents_ObjectId", + table: "RequestLogs", + column: "ObjectId", + principalTable: "Documents", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_RoomLogs_Rooms_ObjectId", + table: "RoomLogs", + column: "ObjectId", + principalTable: "Rooms", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_UserLogs_Users_ObjectId", + table: "UserLogs", + column: "ObjectId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} From 133bb470f198602bf132ef9879abb73da6810b7c Mon Sep 17 00:00:00 2001 From: Vzart Date: Wed, 21 Jun 2023 10:21:54 +0700 Subject: [PATCH 52/56] test: borrow is end to end test ok I think, suppose, nothing gonna go wrong rite? --- src/Api/Controllers/BorrowsController.cs | 17 ++--- .../ApproveOrRejectBorrowRequestRequest.cs | 1 + .../Commands/ApproveOrRejectBorrowRequest.cs | 28 +++++++- .../Borrows/Commands/BorrowDocument.cs | 6 +- .../Borrows/Commands/CancelBorrowRequest.cs | 16 +++-- .../Borrows/Commands/CheckoutDocument.cs | 42 +++++++++--- .../Borrows/Commands/RejectBorrowRequest.cs | 66 ------------------- .../Borrows/Commands/ReturnDocument.cs | 24 ++++++- .../Borrows/Commands/UpdateBorrow.cs | 14 ++-- 9 files changed, 116 insertions(+), 98 deletions(-) delete mode 100644 src/Application/Borrows/Commands/RejectBorrowRequest.cs diff --git a/src/Api/Controllers/BorrowsController.cs b/src/Api/Controllers/BorrowsController.cs index fedd6726..b809a100 100644 --- a/src/Api/Controllers/BorrowsController.cs +++ b/src/Api/Controllers/BorrowsController.cs @@ -102,7 +102,7 @@ public async Task>>> GetAllRequests } /// - /// Approve a borrow request + /// Approve or Reject a borrow request /// /// Id of the borrow request to be approved /// @@ -123,6 +123,7 @@ public async Task>> ApproveOrRejectRequest( CurrentUserId = performingUserId, BorrowId = borrowId, Reason = request.Reason, + Decision = request.Decision }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); @@ -142,10 +143,10 @@ public async Task>> ApproveOrRejectRequest( public async Task>> Checkout( [FromRoute] Guid borrowId) { - var performingUserId = _currentUserService.GetId(); + var currentStaff = _currentUserService.GetCurrentUser(); var command = new CheckoutDocument.Command() { - PerformingUserId = performingUserId, + CurrentStaff = currentStaff, BorrowId = borrowId, }; var result = await Mediator.Send(command); @@ -169,7 +170,7 @@ public async Task>> Return( var performingUserId = _currentUserService.GetId(); var command = new ReturnDocument.Command() { - PerformingUserId = performingUserId, + CurrentUserId = performingUserId, DocumentId = documentId, }; var result = await Mediator.Send(command); @@ -192,10 +193,10 @@ public async Task>> Update( [FromRoute] Guid borrowId, [FromBody] UpdateBorrowRequest request) { - var performingUserId = _currentUserService.GetId(); + var currentUserId = _currentUserService.GetId(); var command = new UpdateBorrow.Command() { - PerformingUserId = performingUserId, + CurrentUserId = currentUserId, BorrowId = borrowId, BorrowFrom = request.BorrowFrom, BorrowTo = request.BorrowTo, @@ -219,10 +220,10 @@ public async Task>> Update( public async Task>> Cancel( [FromRoute] Guid borrowId) { - var performingUserId = _currentUserService.GetId(); + var currentUserId = _currentUserService.GetId(); var command = new CancelBorrowRequest.Command() { - PerformingUserId = performingUserId, + CurrentUserId = currentUserId, BorrowId = borrowId, }; var result = await Mediator.Send(command); diff --git a/src/Api/Controllers/Payload/Requests/Borrows/ApproveOrRejectBorrowRequestRequest.cs b/src/Api/Controllers/Payload/Requests/Borrows/ApproveOrRejectBorrowRequestRequest.cs index 16997ef0..d946630c 100644 --- a/src/Api/Controllers/Payload/Requests/Borrows/ApproveOrRejectBorrowRequestRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Borrows/ApproveOrRejectBorrowRequestRequest.cs @@ -3,4 +3,5 @@ namespace Api.Controllers.Payload.Requests.Borrows; public class ApproveOrRejectBorrowRequestRequest { public string Reason { get; set; } + public string Decision { get; set; } } \ No newline at end of file diff --git a/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs b/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs index 880c614d..bcedf9bb 100644 --- a/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs +++ b/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs @@ -41,6 +41,9 @@ public async Task Handle(Command request, CancellationToken cancellat var borrowRequest = await _context.Borrows .Include(x => x.Borrower) .Include(x => x.Document) + .ThenInclude(x => x.Folder!) + .ThenInclude(x => x.Locker) + .ThenInclude(x => x.Room) .FirstOrDefaultAsync(x => x.Id == request.BorrowId, cancellationToken); if (borrowRequest is null) { @@ -70,6 +73,25 @@ public async Task Handle(Command request, CancellationToken cancellat var currentUser = await _context.Users .FirstOrDefaultAsync(x => x.Id == request.CurrentUserId, cancellationToken); + var staff = await _context.Staffs + .Include(x => x.Room) + .FirstOrDefaultAsync(x => x.Id == request.CurrentUserId, cancellationToken); + + if (staff is null) + { + throw new KeyNotFoundException("Staff does not exist."); + } + + if (staff.Room is null) + { + throw new ConflictException("Staff does not manage a room."); + } + + if (staff.Room.Id != borrowRequest.Document.Folder!.Locker.Room.Id) + { + throw new ConflictException("Request cannot be checked out due to different room."); + } + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); var existedBorrows = _context.Borrows @@ -101,10 +123,10 @@ public async Task Handle(Command request, CancellationToken cancellat { foreach (var existedBorrow in existedBorrows) { - if (existedBorrow.Status + if ((existedBorrow.Status is BorrowRequestStatus.Approved - or BorrowRequestStatus.CheckedOut - && borrowRequest.BorrowTime <= existedBorrow.DueTime && borrowRequest.DueTime >= existedBorrow.BorrowTime) + or BorrowRequestStatus.CheckedOut) + || borrowRequest.BorrowTime <= existedBorrow.DueTime && borrowRequest.DueTime >= existedBorrow.BorrowTime) { throw new ConflictException("Request cannot be approved."); } diff --git a/src/Application/Borrows/Commands/BorrowDocument.cs b/src/Application/Borrows/Commands/BorrowDocument.cs index d2642d43..293aecfd 100644 --- a/src/Application/Borrows/Commands/BorrowDocument.cs +++ b/src/Application/Borrows/Commands/BorrowDocument.cs @@ -124,10 +124,10 @@ public async Task Handle(Command request, CancellationToken cancellat throw new ConflictException("This document is already requested borrow from the same user."); } - if (borrow.Status + if ((borrow.Status is BorrowRequestStatus.Approved - or BorrowRequestStatus.CheckedOut - && borrowFromTime <= borrow.DueTime && borrowToTime >= borrow.BorrowTime) + or BorrowRequestStatus.CheckedOut) + || (borrowFromTime <= borrow.DueTime && borrowToTime >= borrow.BorrowTime)) { throw new ConflictException("This document cannot be borrowed."); } diff --git a/src/Application/Borrows/Commands/CancelBorrowRequest.cs b/src/Application/Borrows/Commands/CancelBorrowRequest.cs index f75b9837..c3bc2de1 100644 --- a/src/Application/Borrows/Commands/CancelBorrowRequest.cs +++ b/src/Application/Borrows/Commands/CancelBorrowRequest.cs @@ -15,7 +15,7 @@ public class CancelBorrowRequest { public record Command : IRequest { - public Guid PerformingUserId { get; init; } + public Guid CurrentUserId { get; init; } public Guid BorrowId { get; init; } } @@ -41,17 +41,23 @@ public async Task Handle(Command request, CancellationToken cancellat throw new KeyNotFoundException("Borrow request does not exist."); } - if (borrowRequest.Status is not (BorrowRequestStatus.Approved or BorrowRequestStatus.Pending)) + if (borrowRequest.Status is not BorrowRequestStatus.Pending) { throw new ConflictException("Request cannot be cancelled."); } - var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); + if (borrowRequest.Borrower.Id != request.CurrentUserId) + { + throw new ConflictException("Can not cancel other borrow request"); + } + + var currentUser = await _context.Users + .FirstOrDefaultAsync(x => x.Id == request.CurrentUserId, cancellationToken); var log = new DocumentLog() { ObjectId = borrowRequest.Document.Id, - UserId = performingUser!.Id, - User = performingUser, + UserId = currentUser!.Id, + User = currentUser, Time = LocalDateTime.FromDateTime(DateTime.Now), Action = DocumentLogMessages.Borrow.CanCel, }; diff --git a/src/Application/Borrows/Commands/CheckoutDocument.cs b/src/Application/Borrows/Commands/CheckoutDocument.cs index 9d30014b..9ac4d77b 100644 --- a/src/Application/Borrows/Commands/CheckoutDocument.cs +++ b/src/Application/Borrows/Commands/CheckoutDocument.cs @@ -3,6 +3,7 @@ using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; using Domain.Entities.Logging; using Domain.Statuses; using MediatR; @@ -15,7 +16,7 @@ public class CheckoutDocument { public record Command : IRequest { - public Guid PerformingUserId { get; init; } + public User CurrentStaff { get; init; } = null!; public Guid BorrowId { get; init; } } @@ -23,11 +24,13 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -35,7 +38,11 @@ public async Task Handle(Command request, CancellationToken cancellat var borrowRequest = await _context.Borrows .Include(x => x.Borrower) .Include(x => x.Document) + .ThenInclude(x => x.Folder!) + .ThenInclude(x => x.Locker) + .ThenInclude(x => x.Room) .FirstOrDefaultAsync(x => x.Id == request.BorrowId, cancellationToken); + if (borrowRequest is null) { throw new KeyNotFoundException("Borrow request does not exist."); @@ -51,17 +58,36 @@ public async Task Handle(Command request, CancellationToken cancellat throw new ConflictException("Request cannot be checked out."); } - var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); + var staff = await _context.Staffs + .Include(x => x.Room) + .FirstOrDefaultAsync(x => x.Id == request.CurrentStaff.Id, cancellationToken); + + if (staff is null) + { + throw new KeyNotFoundException("Staff does not exist."); + } + + if (staff.Room is null) + { + throw new ConflictException("Staff does not have a room."); + } + + if (staff.Room.Id != borrowRequest.Document.Folder!.Locker.Room.Id) + { + throw new ConflictException("Request cannot be checked out due to different room."); + } + + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); borrowRequest.Status = BorrowRequestStatus.CheckedOut; borrowRequest.Document.Status = DocumentStatus.Borrowed; - borrowRequest.Document.LastModified = LocalDateTime.FromDateTime(DateTime.Now); - borrowRequest.Document.LastModifiedBy = performingUser!.Id; + borrowRequest.Document.LastModified = localDateTimeNow; + borrowRequest.Document.LastModifiedBy = request.CurrentStaff.Id; var log = new DocumentLog() { ObjectId = borrowRequest.Document.Id, - UserId = performingUser.Id, - User = performingUser, - Time = LocalDateTime.FromDateTime(DateTime.Now), + UserId = request.CurrentStaff.Id, + User = request.CurrentStaff, + Time = localDateTimeNow, Action = DocumentLogMessages.Borrow.Checkout, }; var result = _context.Borrows.Update(borrowRequest); diff --git a/src/Application/Borrows/Commands/RejectBorrowRequest.cs b/src/Application/Borrows/Commands/RejectBorrowRequest.cs deleted file mode 100644 index fe72f987..00000000 --- a/src/Application/Borrows/Commands/RejectBorrowRequest.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Application.Common.Exceptions; -using Application.Common.Interfaces; -using Application.Common.Messages; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using Domain.Entities.Logging; -using Domain.Statuses; -using MediatR; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using NodaTime; - -namespace Application.Borrows.Commands; - -public class RejectBorrowRequest -{ - public record Command : IRequest - { - public Guid PerformingUserId { get; init; } - public Guid BorrowId { get; init; } - public string Reason { get; init; } = null!; - } - - public class CommandHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public CommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - public async Task Handle(Command request, CancellationToken cancellationToken) - { - var borrowRequest = await _context.Borrows - .Include(x => x.Borrower) - .Include(x => x.Document) - .FirstOrDefaultAsync(x => x.Id == request.BorrowId, cancellationToken); - if (borrowRequest is null) - { - throw new KeyNotFoundException("Borrow request does not exist."); - } - - if (borrowRequest.Status is not BorrowRequestStatus.Pending) - { - throw new ConflictException("Request cannot be rejected."); - } - - var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); - borrowRequest.Status = BorrowRequestStatus.Rejected; - var requestLog = new RequestLog() - { - ObjectId = borrowRequest.Document.Id, - UserId = performingUser!.Id, - User = performingUser, - Time = LocalDateTime.FromDateTime(DateTime.Now), - Action = DocumentLogMessages.Borrow.Reject, - }; - var result = _context.Borrows.Update(borrowRequest); - await _context.RequestLogs.AddAsync(requestLog, cancellationToken); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } - } -} \ No newline at end of file diff --git a/src/Application/Borrows/Commands/ReturnDocument.cs b/src/Application/Borrows/Commands/ReturnDocument.cs index 37af412e..451147f6 100644 --- a/src/Application/Borrows/Commands/ReturnDocument.cs +++ b/src/Application/Borrows/Commands/ReturnDocument.cs @@ -13,7 +13,7 @@ public class ReturnDocument { public record Command : IRequest { - public Guid PerformingUserId { get; init; } + public Guid CurrentUserId { get; init; } public Guid DocumentId { get; init; } } @@ -33,6 +33,9 @@ public async Task Handle(Command request, CancellationToken cancellat var borrowRequest = await _context.Borrows .Include(x => x.Borrower) .Include(x => x.Document) + .ThenInclude(x => x.Folder!) + .ThenInclude(x => x.Locker) + .ThenInclude(x => x.Room) .FirstOrDefaultAsync(x => x.Document.Id == request.DocumentId && x.Status == BorrowRequestStatus.CheckedOut, cancellationToken); if (borrowRequest is null) @@ -49,6 +52,25 @@ public async Task Handle(Command request, CancellationToken cancellat { throw new ConflictException("Request cannot be made."); } + + var staff = await _context.Staffs + .Include(x => x.Room) + .FirstOrDefaultAsync(x => x.Id == request.CurrentUserId, cancellationToken); + + if (staff is null) + { + throw new KeyNotFoundException("Staff does not exist."); + } + + if (staff.Room is null) + { + throw new ConflictException("Staff does not have a room."); + } + + if (staff.Room.Id != borrowRequest.Document.Folder!.Locker.Room.Id) + { + throw new ConflictException("Request cannot be checked out due to different room."); + } borrowRequest.Status = BorrowRequestStatus.Returned; borrowRequest.Document.Status = DocumentStatus.Available; diff --git a/src/Application/Borrows/Commands/UpdateBorrow.cs b/src/Application/Borrows/Commands/UpdateBorrow.cs index 8c75b727..f8680ca7 100644 --- a/src/Application/Borrows/Commands/UpdateBorrow.cs +++ b/src/Application/Borrows/Commands/UpdateBorrow.cs @@ -2,6 +2,7 @@ using Application.Common.Interfaces; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; using Domain.Entities.Physical; using Domain.Statuses; using FluentValidation; @@ -33,7 +34,7 @@ public Validator() public record Command : IRequest { - public Guid PerformingUserId { get; init; } + public Guid CurrentUserId { get; init; } public Guid BorrowId { get; init; } public DateTime BorrowFrom { get; init; } public DateTime BorrowTo { get; init; } @@ -72,6 +73,11 @@ public async Task Handle(Command request, CancellationToken cancellat throw new ConflictException("Document is lost."); } + if (borrowRequest.Borrower.Id != request.CurrentUserId) + { + throw new ConflictException("Can not update other borrow request."); + } + var localDateTimeNow = LocalDateTime.FromDateTime(DateTime.Now); var existedBorrows = _context.Borrows .Include(x => x.Borrower) @@ -85,10 +91,10 @@ public async Task Handle(Command request, CancellationToken cancellat var borrowToTime = LocalDateTime.FromDateTime(request.BorrowTo); foreach (var borrow in existedBorrows) { - if (borrow.Status + if ((borrow.Status is BorrowRequestStatus.Approved - or BorrowRequestStatus.CheckedOut - && (borrowFromTime <= borrow.DueTime && borrowToTime >= borrow.BorrowTime)) + or BorrowRequestStatus.CheckedOut) + || (borrowFromTime <= borrow.DueTime && borrowToTime >= borrow.BorrowTime)) { throw new ConflictException("This document cannot be updated."); } From fbd8e8bdadfbe594d5763ffc36a4a5746f3d881c Mon Sep 17 00:00:00 2001 From: Vzart Date: Wed, 21 Jun 2023 14:16:53 +0700 Subject: [PATCH 53/56] test: import request are done --- .../Controllers/ImportRequestsController.cs | 4 +++- .../Documents/RequestImportDocumentRequest.cs | 2 ++ .../Commands/ApproveOrRejectDocument.cs | 19 +++++++++++++++++++ .../Commands/CheckinDocument.cs | 2 +- .../Commands/RequestImportDocument.cs | 8 +++++--- 5 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/Api/Controllers/ImportRequestsController.cs b/src/Api/Controllers/ImportRequestsController.cs index 2ff8d081..4e14c85c 100644 --- a/src/Api/Controllers/ImportRequestsController.cs +++ b/src/Api/Controllers/ImportRequestsController.cs @@ -65,6 +65,7 @@ public async Task>>> GetAllI { CurrentUser = currentUser, SearchTerm = queryParameters.SearchTerm, + RoomId = queryParameters.RoomId, Page = queryParameters.Page, Size = queryParameters.Size, SortBy = queryParameters.SortBy, @@ -98,6 +99,7 @@ public async Task>> RequestImport( IsPrivate = request.IsPrivate, Issuer = currentUser, RoomId = request.RoomId, + Reason = request.Reason }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); @@ -133,7 +135,7 @@ public async Task>> ApproveOrReject( /// /// Assign a document to a folder /// - /// Id of the document to be rejected + /// Id of the import request to be rejected or approved /// /// A DocumentDto of the rejected document [RequiresRole(IdentityData.Roles.Staff)] diff --git a/src/Api/Controllers/Payload/Requests/Documents/RequestImportDocumentRequest.cs b/src/Api/Controllers/Payload/Requests/Documents/RequestImportDocumentRequest.cs index 4f2e99d6..ea5bb0e4 100644 --- a/src/Api/Controllers/Payload/Requests/Documents/RequestImportDocumentRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Documents/RequestImportDocumentRequest.cs @@ -17,6 +17,8 @@ public class RequestImportDocumentRequest /// Document type of the document to be imported /// public string DocumentType { get; set; } = null!; + + public string Reason { get; set; } = null!; public Guid RoomId { get; set; } public bool IsPrivate { get; set; } } \ No newline at end of file diff --git a/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs b/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs index 7590c5b6..6f658ae4 100644 --- a/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs +++ b/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs @@ -70,6 +70,25 @@ public async Task Handle(Command request, CancellationToken ca throw new ConflictException("Document does not exist."); } + var staff = await _context.Staffs + .Include(x => x.Room) + .FirstOrDefaultAsync(x => x.Id == request.CurrentUser.Id, cancellationToken); + + if (staff is null) + { + throw new KeyNotFoundException("Staff does not exist."); + } + + if (staff.Room is null) + { + throw new ConflictException("Staff does not assign to a room."); + } + + if (staff.Room.Id != importRequest.RoomId) + { + throw new KeyNotFoundException("Can not approve request from different room"); + } + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); var log = new DocumentLog() diff --git a/src/Application/ImportRequests/Commands/CheckinDocument.cs b/src/Application/ImportRequests/Commands/CheckinDocument.cs index c1e9d25f..70c0c34a 100644 --- a/src/Application/ImportRequests/Commands/CheckinDocument.cs +++ b/src/Application/ImportRequests/Commands/CheckinDocument.cs @@ -99,6 +99,6 @@ public async Task Handle(Command request, CancellationToken cancell } private static bool StatusesAreNotValid(DocumentStatus documentStatus, ImportRequestStatus importRequestStatus) - => documentStatus is not DocumentStatus.Available || importRequestStatus is not ImportRequestStatus.Approved; + => documentStatus is not DocumentStatus.Issued || importRequestStatus is not ImportRequestStatus.Approved; } } \ No newline at end of file diff --git a/src/Application/ImportRequests/Commands/RequestImportDocument.cs b/src/Application/ImportRequests/Commands/RequestImportDocument.cs index 74f5b3c0..1b96f13a 100644 --- a/src/Application/ImportRequests/Commands/RequestImportDocument.cs +++ b/src/Application/ImportRequests/Commands/RequestImportDocument.cs @@ -23,6 +23,7 @@ public record Command : IRequest public User Issuer { get; init; } = null!; public Guid RoomId { get; init; } public bool IsPrivate { get; init; } + public string Reason { get; set; } = null!; } public class CommandHandler : IRequestHandler @@ -70,14 +71,16 @@ public async Task Handle(Command request, CancellationToken ca Created = localDateTimeNow, CreatedBy = request.Issuer.Id, }; - + await _context.Documents.AddAsync(entity, cancellationToken); + var importRequest = new ImportRequest() { Document = entity, Status = ImportRequestStatus.Pending, Room = room, Created = localDateTimeNow, - CreatedBy = request.Issuer.Id + CreatedBy = request.Issuer.Id, + Reason = request.Reason }; var log = new DocumentLog() @@ -88,7 +91,6 @@ public async Task Handle(Command request, CancellationToken ca UserId = request.Issuer.Id, Action = DocumentLogMessages.Import.NewImportRequest, }; - await _context.Documents.AddAsync(entity, cancellationToken); var result = await _context.ImportRequests.AddAsync(importRequest, cancellationToken); await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); From 187dc5344f88c2a8f138f32cd397c2267c2d781f Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Wed, 21 Jun 2023 20:15:23 +0700 Subject: [PATCH 54/56] final --- src/Api/Controllers/BorrowsController.cs | 8 +-- src/Api/Controllers/FoldersController.cs | 11 +++- src/Api/Controllers/LockersController.cs | 2 + src/Api/Controllers/RoomsController.cs | 2 + .../Commands/ApproveOrRejectBorrowRequest.cs | 2 +- .../Borrows/Commands/BorrowDocument.cs | 2 +- .../Borrows/Commands/CancelBorrowRequest.cs | 10 +++- .../Borrows/Commands/ReturnDocument.cs | 27 +++++++-- .../Borrows/Commands/UpdateBorrow.cs | 27 +++++++-- .../Common/Messages/FolderLogMessage.cs | 1 + .../Common/Messages/LockerLogMessage.cs | 1 + .../Common/Messages/RoomLogMessage.cs | 1 + .../Folders/Commands/RemoveFolder.cs | 24 +++++++- .../Commands/RequestImportDocument.cs | 2 +- .../Lockers/Commands/RemoveLocker.cs | 22 ++++++- src/Application/Rooms/Commands/RemoveRoom.cs | 20 ++++++- .../Staffs/Commands/RemoveStaff.cs | 59 ------------------ src/Application/Users/Commands/AddUser.cs | 2 +- .../Staffs/Commands/RemoveStaffTests.cs | 60 ------------------- 19 files changed, 133 insertions(+), 150 deletions(-) delete mode 100644 src/Application/Staffs/Commands/RemoveStaff.cs delete mode 100644 tests/Application.Tests.Integration/Staffs/Commands/RemoveStaffTests.cs diff --git a/src/Api/Controllers/BorrowsController.cs b/src/Api/Controllers/BorrowsController.cs index b809a100..430735ba 100644 --- a/src/Api/Controllers/BorrowsController.cs +++ b/src/Api/Controllers/BorrowsController.cs @@ -167,10 +167,10 @@ public async Task>> Checkout( public async Task>> Return( [FromRoute] Guid documentId) { - var performingUserId = _currentUserService.GetId(); + var currentUser = _currentUserService.GetCurrentUser(); var command = new ReturnDocument.Command() { - CurrentUserId = performingUserId, + CurrentUser = currentUser, DocumentId = documentId, }; var result = await Mediator.Send(command); @@ -193,10 +193,10 @@ public async Task>> Update( [FromRoute] Guid borrowId, [FromBody] UpdateBorrowRequest request) { - var currentUserId = _currentUserService.GetId(); + var currentUser = _currentUserService.GetCurrentUser(); var command = new UpdateBorrow.Command() { - CurrentUserId = currentUserId, + CurrentUser = currentUser, BorrowId = borrowId, BorrowFrom = request.BorrowFrom, BorrowTo = request.BorrowTo, diff --git a/src/Api/Controllers/FoldersController.cs b/src/Api/Controllers/FoldersController.cs index e62602ca..153367ab 100644 --- a/src/Api/Controllers/FoldersController.cs +++ b/src/Api/Controllers/FoldersController.cs @@ -1,5 +1,6 @@ using Api.Controllers.Payload.Requests; using Api.Controllers.Payload.Requests.Folders; +using Application.Common.Extensions; using Application.Common.Interfaces; using Application.Common.Models; using Application.Common.Models.Dtos.Logging; @@ -118,11 +119,15 @@ public async Task>> AddFolder([FromBody] AddFolde [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> RemoveFolder([FromRoute] Guid folderId) { - var currentUserRole = _currentUserService.GetRole(); - var staffRoomId = _currentUserService.GetCurrentRoomForStaff(); + var currentUser = _currentUserService.GetCurrentUser(); + Guid? staffRoomId = null; + if (currentUser.Role.IsStaff()) + { + staffRoomId = _currentUserService.GetCurrentRoomForStaff(); + } var command = new RemoveFolder.Command() { - CurrentUserRole = currentUserRole, + CurrentUser = currentUser, CurrentStaffRoomId = staffRoomId, FolderId = folderId, }; diff --git a/src/Api/Controllers/LockersController.cs b/src/Api/Controllers/LockersController.cs index a02feeed..fb87d0ee 100644 --- a/src/Api/Controllers/LockersController.cs +++ b/src/Api/Controllers/LockersController.cs @@ -116,8 +116,10 @@ public async Task>> Add( [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task>> Remove([FromRoute] Guid lockerId) { + var currentUser = _currentUserService.GetCurrentUser(); var command = new RemoveLocker.Command() { + CurrentUser = currentUser, LockerId = lockerId, }; var result = await Mediator.Send(command); diff --git a/src/Api/Controllers/RoomsController.cs b/src/Api/Controllers/RoomsController.cs index f0603cae..1f1dadca 100644 --- a/src/Api/Controllers/RoomsController.cs +++ b/src/Api/Controllers/RoomsController.cs @@ -165,8 +165,10 @@ public async Task>> AddRoom( public async Task>> RemoveRoom( [FromRoute] Guid roomId) { + var currentUser = _currentUserService.GetCurrentUser(); var command = new RemoveRoom.Command() { + CurrentUser = currentUser, RoomId = roomId, }; var result = await Mediator.Send(command); diff --git a/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs b/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs index bcedf9bb..91a974e9 100644 --- a/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs +++ b/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs @@ -126,7 +126,7 @@ public async Task Handle(Command request, CancellationToken cancellat if ((existedBorrow.Status is BorrowRequestStatus.Approved or BorrowRequestStatus.CheckedOut) - || borrowRequest.BorrowTime <= existedBorrow.DueTime && borrowRequest.DueTime >= existedBorrow.BorrowTime) + && (borrowRequest.BorrowTime <= existedBorrow.DueTime && borrowRequest.DueTime >= existedBorrow.BorrowTime)) { throw new ConflictException("Request cannot be approved."); } diff --git a/src/Application/Borrows/Commands/BorrowDocument.cs b/src/Application/Borrows/Commands/BorrowDocument.cs index 293aecfd..becbe90b 100644 --- a/src/Application/Borrows/Commands/BorrowDocument.cs +++ b/src/Application/Borrows/Commands/BorrowDocument.cs @@ -127,7 +127,7 @@ public async Task Handle(Command request, CancellationToken cancellat if ((borrow.Status is BorrowRequestStatus.Approved or BorrowRequestStatus.CheckedOut) - || (borrowFromTime <= borrow.DueTime && borrowToTime >= borrow.BorrowTime)) + && (borrowFromTime <= borrow.DueTime && borrowToTime >= borrow.BorrowTime)) { throw new ConflictException("This document cannot be borrowed."); } diff --git a/src/Application/Borrows/Commands/CancelBorrowRequest.cs b/src/Application/Borrows/Commands/CancelBorrowRequest.cs index c3bc2de1..b4015ffd 100644 --- a/src/Application/Borrows/Commands/CancelBorrowRequest.cs +++ b/src/Application/Borrows/Commands/CancelBorrowRequest.cs @@ -23,11 +23,13 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; - - public CommandHandler(IApplicationDbContext context, IMapper mapper) + private readonly IDateTimeProvider _dateTimeProvider; + + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -50,6 +52,8 @@ public async Task Handle(Command request, CancellationToken cancellat { throw new ConflictException("Can not cancel other borrow request"); } + + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); var currentUser = await _context.Users .FirstOrDefaultAsync(x => x.Id == request.CurrentUserId, cancellationToken); @@ -58,7 +62,7 @@ public async Task Handle(Command request, CancellationToken cancellat ObjectId = borrowRequest.Document.Id, UserId = currentUser!.Id, User = currentUser, - Time = LocalDateTime.FromDateTime(DateTime.Now), + Time = localDateTimeNow, Action = DocumentLogMessages.Borrow.CanCel, }; diff --git a/src/Application/Borrows/Commands/ReturnDocument.cs b/src/Application/Borrows/Commands/ReturnDocument.cs index 451147f6..e18b5c89 100644 --- a/src/Application/Borrows/Commands/ReturnDocument.cs +++ b/src/Application/Borrows/Commands/ReturnDocument.cs @@ -1,7 +1,10 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; using Domain.Statuses; using MediatR; using Microsoft.EntityFrameworkCore; @@ -13,7 +16,7 @@ public class ReturnDocument { public record Command : IRequest { - public Guid CurrentUserId { get; init; } + public User CurrentUser { get; init; } = null!; public Guid DocumentId { get; init; } } @@ -21,11 +24,13 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -37,7 +42,7 @@ public async Task Handle(Command request, CancellationToken cancellat .ThenInclude(x => x.Locker) .ThenInclude(x => x.Room) .FirstOrDefaultAsync(x => x.Document.Id == request.DocumentId - && x.Status == BorrowRequestStatus.CheckedOut, cancellationToken); + && x.Status == BorrowRequestStatus.CheckedOut, cancellationToken); if (borrowRequest is null) { throw new KeyNotFoundException("Borrow request does not exist."); @@ -55,7 +60,7 @@ public async Task Handle(Command request, CancellationToken cancellat var staff = await _context.Staffs .Include(x => x.Room) - .FirstOrDefaultAsync(x => x.Id == request.CurrentUserId, cancellationToken); + .FirstOrDefaultAsync(x => x.Id == request.CurrentUser.Id, cancellationToken); if (staff is null) { @@ -72,10 +77,22 @@ public async Task Handle(Command request, CancellationToken cancellat throw new ConflictException("Request cannot be checked out due to different room."); } + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + borrowRequest.Status = BorrowRequestStatus.Returned; borrowRequest.Document.Status = DocumentStatus.Available; - borrowRequest.ActualReturnTime = LocalDateTime.FromDateTime(DateTime.Now); + borrowRequest.ActualReturnTime = localDateTimeNow; + + var log = new DocumentLog() + { + ObjectId = borrowRequest.Document.Id, + UserId = request.CurrentUser.Id, + User = request.CurrentUser, + Time = localDateTimeNow, + Action = DocumentLogMessages.Borrow.Checkout, + }; var result = _context.Borrows.Update(borrowRequest); + await _context.DocumentLogs.AddAsync(log, cancellationToken); _context.Documents.Update(borrowRequest.Document); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); diff --git a/src/Application/Borrows/Commands/UpdateBorrow.cs b/src/Application/Borrows/Commands/UpdateBorrow.cs index f8680ca7..bd12086d 100644 --- a/src/Application/Borrows/Commands/UpdateBorrow.cs +++ b/src/Application/Borrows/Commands/UpdateBorrow.cs @@ -1,8 +1,10 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities; +using Domain.Entities.Logging; using Domain.Entities.Physical; using Domain.Statuses; using FluentValidation; @@ -34,7 +36,7 @@ public Validator() public record Command : IRequest { - public Guid CurrentUserId { get; init; } + public User CurrentUser { get; init; } = null!; public Guid BorrowId { get; init; } public DateTime BorrowFrom { get; init; } public DateTime BorrowTo { get; init; } @@ -45,11 +47,13 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -73,12 +77,12 @@ public async Task Handle(Command request, CancellationToken cancellat throw new ConflictException("Document is lost."); } - if (borrowRequest.Borrower.Id != request.CurrentUserId) + if (borrowRequest.Borrower.Id != request.CurrentUser.Id) { throw new ConflictException("Can not update other borrow request."); } - var localDateTimeNow = LocalDateTime.FromDateTime(DateTime.Now); + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); var existedBorrows = _context.Borrows .Include(x => x.Borrower) .Where(x => @@ -94,7 +98,7 @@ public async Task Handle(Command request, CancellationToken cancellat if ((borrow.Status is BorrowRequestStatus.Approved or BorrowRequestStatus.CheckedOut) - || (borrowFromTime <= borrow.DueTime && borrowToTime >= borrow.BorrowTime)) + && (borrowFromTime <= borrow.DueTime && borrowToTime >= borrow.BorrowTime)) { throw new ConflictException("This document cannot be updated."); } @@ -102,8 +106,19 @@ is BorrowRequestStatus.Approved borrowRequest.BorrowTime = borrowFromTime; borrowRequest.DueTime = borrowToTime; borrowRequest.Reason = request.Reason; - + borrowRequest.LastModified = localDateTimeNow; + borrowRequest.LastModifiedBy = request.CurrentUser.Id; + + var log = new DocumentLog() + { + UserId = request.CurrentUser.Id, + User = request.CurrentUser, + ObjectId = borrowRequest.Document.Id, + Time = localDateTimeNow, + Action = DocumentLogMessages.Borrow.Update, + }; var result = _context.Borrows.Update(borrowRequest); + await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); diff --git a/src/Application/Common/Messages/FolderLogMessage.cs b/src/Application/Common/Messages/FolderLogMessage.cs index 97f97818..c2bffb4d 100644 --- a/src/Application/Common/Messages/FolderLogMessage.cs +++ b/src/Application/Common/Messages/FolderLogMessage.cs @@ -4,5 +4,6 @@ public static class FolderLogMessage { public const string Add = "Added folder"; public const string Update = "Updated folder"; + public const string Remove = "Removed folder"; public const string AssignDocument = "Assigned document to folder"; } \ No newline at end of file diff --git a/src/Application/Common/Messages/LockerLogMessage.cs b/src/Application/Common/Messages/LockerLogMessage.cs index 20d1018f..291e4510 100644 --- a/src/Application/Common/Messages/LockerLogMessage.cs +++ b/src/Application/Common/Messages/LockerLogMessage.cs @@ -4,4 +4,5 @@ public static class LockerLogMessage { public const string Add = "Added locker"; public const string Update = "Updated locker"; + public const string Remove = "Removed locker"; } \ No newline at end of file diff --git a/src/Application/Common/Messages/RoomLogMessage.cs b/src/Application/Common/Messages/RoomLogMessage.cs index cd105a65..90125821 100644 --- a/src/Application/Common/Messages/RoomLogMessage.cs +++ b/src/Application/Common/Messages/RoomLogMessage.cs @@ -4,4 +4,5 @@ public static class RoomLogMessage { public const string Add = "Added room"; public const string Update = "Updated room"; + public const string Remove = "Removed room"; } \ No newline at end of file diff --git a/src/Application/Folders/Commands/RemoveFolder.cs b/src/Application/Folders/Commands/RemoveFolder.cs index 11f91fd2..c6a0c6c7 100644 --- a/src/Application/Folders/Commands/RemoveFolder.cs +++ b/src/Application/Folders/Commands/RemoveFolder.cs @@ -1,11 +1,15 @@ using Application.Common.Exceptions; using Application.Common.Extensions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; using Domain.Entities.Physical; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Folders.Commands; @@ -13,7 +17,7 @@ public class RemoveFolder { public record Command : IRequest { - public string CurrentUserRole { get; init; } = null!; + public User CurrentUser { get; init; } = null!; public Guid? CurrentStaffRoomId { get; init; } public Guid FolderId { get; init; } } @@ -22,11 +26,13 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -42,7 +48,7 @@ public async Task Handle(Command request, CancellationToken cancellat throw new KeyNotFoundException("Folder does not exist."); } - if (request.CurrentUserRole.IsStaff() + if (request.CurrentUser.Role.IsStaff() && (request.CurrentStaffRoomId is null || !FolderIsInRoom(folder, request.CurrentStaffRoomId.Value))) { throw new UnauthorizedAccessException("User cannot remove this resource."); @@ -55,9 +61,21 @@ public async Task Handle(Command request, CancellationToken cancellat throw new ConflictException("Folder cannot be removed because it contains documents."); } + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + var locker = folder.Locker; + + var log = new FolderLog() + { + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + ObjectId = folder.Id, + Time = localDateTimeNow, + Action = FolderLogMessage.Remove, + }; var result = _context.Folders.Remove(folder); locker.NumberOfFolders -= 1; + await _context.FolderLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } diff --git a/src/Application/ImportRequests/Commands/RequestImportDocument.cs b/src/Application/ImportRequests/Commands/RequestImportDocument.cs index 1b96f13a..15a83b8f 100644 --- a/src/Application/ImportRequests/Commands/RequestImportDocument.cs +++ b/src/Application/ImportRequests/Commands/RequestImportDocument.cs @@ -23,7 +23,7 @@ public record Command : IRequest public User Issuer { get; init; } = null!; public Guid RoomId { get; init; } public bool IsPrivate { get; init; } - public string Reason { get; set; } = null!; + public string Reason { get; init; } = null!; } public class CommandHandler : IRequestHandler diff --git a/src/Application/Lockers/Commands/RemoveLocker.cs b/src/Application/Lockers/Commands/RemoveLocker.cs index 6aa7a79f..c58f35ab 100644 --- a/src/Application/Lockers/Commands/RemoveLocker.cs +++ b/src/Application/Lockers/Commands/RemoveLocker.cs @@ -1,10 +1,14 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Lockers.Commands; @@ -23,6 +27,7 @@ public Validator() public record Command : IRequest { + public User CurrentUser { get; init; } = null!; public Guid LockerId { get; init; } } @@ -30,11 +35,13 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; - - public CommandHandler(IApplicationDbContext context, IMapper mapper) + private readonly IDateTimeProvider _dateTimeProvider; + + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -56,10 +63,21 @@ public async Task Handle(Command request, CancellationToken cancellat throw new ConflictException("Locker cannot be removed because it contains documents."); } + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + + var log = new LockerLog() + { + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + ObjectId = locker.Id, + Time = localDateTimeNow, + Action = LockerLogMessage.Remove, + }; var room = locker.Room; var result = _context.Lockers.Remove(locker); room.NumberOfLockers -= 1; _context.Rooms.Update(room); + await _context.LockerLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } diff --git a/src/Application/Rooms/Commands/RemoveRoom.cs b/src/Application/Rooms/Commands/RemoveRoom.cs index 37307454..6f56a584 100644 --- a/src/Application/Rooms/Commands/RemoveRoom.cs +++ b/src/Application/Rooms/Commands/RemoveRoom.cs @@ -1,10 +1,14 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; +using Application.Common.Messages; using Application.Common.Models.Dtos.Physical; using AutoMapper; +using Domain.Entities; +using Domain.Entities.Logging; using FluentValidation; using MediatR; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Application.Rooms.Commands; @@ -23,6 +27,7 @@ public Validator() public record Command : IRequest { + public User CurrentUser { get; init; } = null!; public Guid RoomId { get; init; } } @@ -30,11 +35,13 @@ public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IDateTimeProvider _dateTimeProvider; - public CommandHandler(IApplicationDbContext context, IMapper mapper) + public CommandHandler(IApplicationDbContext context, IMapper mapper, IDateTimeProvider dateTimeProvider) { _context = context; _mapper = mapper; + _dateTimeProvider = dateTimeProvider; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -59,7 +66,18 @@ public async Task Handle(Command request, CancellationToken cancellatio throw new ConflictException("Room cannot be removed because it contains something."); } + var localDateTimeNow = LocalDateTime.FromDateTime(_dateTimeProvider.DateTimeNow); + + var log = new RoomLog() + { + User = request.CurrentUser, + UserId = request.CurrentUser.Id, + ObjectId = room.Id, + Time = localDateTimeNow, + Action = RoomLogMessage.Remove, + }; var result = _context.Rooms.Remove(room); + await _context.RoomLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } diff --git a/src/Application/Staffs/Commands/RemoveStaff.cs b/src/Application/Staffs/Commands/RemoveStaff.cs deleted file mode 100644 index f70c1f34..00000000 --- a/src/Application/Staffs/Commands/RemoveStaff.cs +++ /dev/null @@ -1,59 +0,0 @@ -using Application.Common.Interfaces; -using Application.Common.Messages; -using Application.Common.Models.Dtos.Physical; -using AutoMapper; -using Domain.Entities.Logging; -using FluentValidation; -using MediatR; -using Microsoft.EntityFrameworkCore; -using NodaTime; - -namespace Application.Staffs.Commands; - -public class RemoveStaff -{ - public record Command : IRequest - { - public Guid PerformingUserId { get; init; } - public Guid StaffId { get; init; } - } - - public class CommandHandler : IRequestHandler - { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - - public CommandHandler(IApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } - - public async Task Handle(Command request, CancellationToken cancellationToken) - { - var staff = await _context.Staffs - .Include(x => x.User) - .Include(x => x.Room) - .FirstOrDefaultAsync(x => x.User.Id.Equals(request.StaffId), cancellationToken: cancellationToken); - - if (staff is null) - { - throw new KeyNotFoundException("Staff does not exist."); - } - - var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); - var log = new UserLog() - { - User = performingUser!, - UserId = performingUser!.Id, - ObjectId = staff.User.Id, - Time = LocalDateTime.FromDateTime(DateTime.Now), - Action = UserLogMessages.Staff.Remove, - }; - var result = _context.Staffs.Remove(staff); - await _context.UserLogs.AddAsync(log, cancellationToken); - await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); - } - } -} \ No newline at end of file diff --git a/src/Application/Users/Commands/AddUser.cs b/src/Application/Users/Commands/AddUser.cs index a3ad9461..38c94551 100644 --- a/src/Application/Users/Commands/AddUser.cs +++ b/src/Application/Users/Commands/AddUser.cs @@ -133,7 +133,7 @@ public async Task Handle(Command request, CancellationToken cancellatio entity.AddDomainEvent(new StaffCreatedEvent(entity, request.CurrentUser)); } var result = await _context.Users.AddAsync(entity, cancellationToken); - + var log = new UserLog() { User = request.CurrentUser, diff --git a/tests/Application.Tests.Integration/Staffs/Commands/RemoveStaffTests.cs b/tests/Application.Tests.Integration/Staffs/Commands/RemoveStaffTests.cs deleted file mode 100644 index 92df91b8..00000000 --- a/tests/Application.Tests.Integration/Staffs/Commands/RemoveStaffTests.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Application.Identity; -using Application.Staffs.Commands; -using Domain.Entities; -using Domain.Entities.Physical; -using FluentAssertions; -using Xunit; - -namespace Application.Tests.Integration.Staffs.Commands; - -public class RemoveStaffTests : BaseClassFixture -{ - public RemoveStaffTests(CustomApiFactory apiFactory) : base(apiFactory) - { - } - - [Fact] - public async Task ShouldRemoveStaff_WhenStaffIdIsValid() - { - // Arrange - var department = CreateDepartment(); - var user = CreateUser(IdentityData.Roles.Admin, "123456"); - var room = CreateRoom(department); - var staff = CreateStaff(user, room); - await AddAsync(staff); - - var command = new RemoveStaff.Command() - { - StaffId = staff.Id - }; - - // Act - await SendAsync(command); - - // Assert - var result = await FindAsync(staff.Id); - result.Should().BeNull(); - - // Cleanup - Remove(await FindAsync(room.Id)); - Remove(user); - Remove(await FindAsync(department.Id)); - } - - [Fact] - public async Task ShouldThrowKeyNotFoundException_WhenStaffDoesNotExist() - { - // Arrange - var command = new RemoveStaff.Command() - { - StaffId = Guid.NewGuid() - }; - - // Act - var action = async () => await SendAsync(command); - - // Assert - await action.Should().ThrowAsync() - .WithMessage("Staff does not exist."); - } -} \ No newline at end of file From 5bf9210a1be601e78883f8996a6527b7149895c6 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien <87883163+ChienNQuang@users.noreply.github.com> Date: Wed, 21 Jun 2023 20:16:22 +0700 Subject: [PATCH 55/56] Delete .fleet directory --- .fleet/settings.json | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .fleet/settings.json diff --git a/.fleet/settings.json b/.fleet/settings.json deleted file mode 100644 index a7858d18..00000000 --- a/.fleet/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "editor.guides": [] -} \ No newline at end of file From 86ee182de3f85977e2715175bd73530a8e81fb24 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Wed, 21 Jun 2023 20:55:12 +0700 Subject: [PATCH 56/56] final 2 --- src/Api/Controllers/BorrowsController.cs | 6 +- .../Controllers/ImportRequestsController.cs | 6 +- .../ApproveOrRejectBorrowRequestRequest.cs | 2 +- .../Documents/ApproveOrRejectImportRequest.cs | 2 +- .../Documents/RequestImportDocumentRequest.cs | 2 +- .../Commands/ApproveOrRejectBorrowRequest.cs | 4 +- .../Borrows/Commands/BorrowDocument.cs | 7 +- .../Borrows/Commands/UpdateBorrow.cs | 6 +- .../Dtos/ImportDocument/ImportRequestDto.cs | 3 +- .../Models/Dtos/Logging/DocumentLogDto.cs | 6 +- .../Models/Dtos/Logging/FolderLogDto.cs | 8 +- .../Models/Dtos/Logging/LockerLogDto.cs | 6 +- .../Models/Dtos/Logging/RequestLogDto.cs | 5 +- .../Common/Models/Dtos/Logging/RoomLogDto.cs | 6 +- .../Common/Models/Dtos/Logging/UserLogDto.cs | 6 +- .../Common/Models/Dtos/Physical/BorrowDto.cs | 3 +- .../Common/Models/Dtos/ReasonDto.cs | 11 - .../Commands/ApproveOrRejectDocument.cs | 4 +- .../Commands/RequestImportDocument.cs | 5 +- src/Domain/Entities/Physical/Borrow.cs | 3 +- src/Domain/Entities/Physical/ImportRequest.cs | 3 +- .../Configurations/BorrowConfiguration.cs | 2 +- .../ImportRequestConfiguration.cs | 2 +- .../20230621135501_AddMoreReason.Designer.cs | 1068 +++++++++++++++++ .../20230621135501_AddMoreReason.cs | 60 + .../ApplicationDbContextModelSnapshot.cs | 18 +- .../BaseClassFixture.cs | 2 +- .../Borrows/Commands/BorrowDocumentTests.cs | 20 +- .../Borrows/Commands/UpdateBorrowTests.cs | 12 +- .../Common/Mappings/MappingTests.cs | 4 + 30 files changed, 1207 insertions(+), 85 deletions(-) delete mode 100644 src/Application/Common/Models/Dtos/ReasonDto.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230621135501_AddMoreReason.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230621135501_AddMoreReason.cs diff --git a/src/Api/Controllers/BorrowsController.cs b/src/Api/Controllers/BorrowsController.cs index 430735ba..0388c57a 100644 --- a/src/Api/Controllers/BorrowsController.cs +++ b/src/Api/Controllers/BorrowsController.cs @@ -42,7 +42,7 @@ public async Task>> BorrowDocument( DocumentId = request.DocumentId, BorrowFrom = request.BorrowFrom, BorrowTo = request.BorrowTo, - Reason = request.Reason, + BorrowReason = request.Reason, }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); @@ -122,7 +122,7 @@ public async Task>> ApproveOrRejectRequest( { CurrentUserId = performingUserId, BorrowId = borrowId, - Reason = request.Reason, + StaffReason = request.StaffReason, Decision = request.Decision }; var result = await Mediator.Send(command); @@ -200,7 +200,7 @@ public async Task>> Update( BorrowId = borrowId, BorrowFrom = request.BorrowFrom, BorrowTo = request.BorrowTo, - Reason = request.Reason, + BorrowReason = request.Reason, }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); diff --git a/src/Api/Controllers/ImportRequestsController.cs b/src/Api/Controllers/ImportRequestsController.cs index 4e14c85c..dc7f0bfc 100644 --- a/src/Api/Controllers/ImportRequestsController.cs +++ b/src/Api/Controllers/ImportRequestsController.cs @@ -96,17 +96,17 @@ public async Task>> RequestImport( Title = request.Title, Description = request.Description, DocumentType = request.DocumentType, + ImportReason = request.ImportReason, IsPrivate = request.IsPrivate, Issuer = currentUser, RoomId = request.RoomId, - Reason = request.Reason }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); } /// - /// Approve a document request + /// Approve or reject a document request /// /// Id of the document to be approved /// @@ -125,7 +125,7 @@ public async Task>> ApproveOrReject( { CurrentUser = currentUser, ImportRequestId = importRequestId, - Reason = request.Reason, + StaffReason = request.StaffReason, Decision = request.Decision, }; var result = await Mediator.Send(query); diff --git a/src/Api/Controllers/Payload/Requests/Borrows/ApproveOrRejectBorrowRequestRequest.cs b/src/Api/Controllers/Payload/Requests/Borrows/ApproveOrRejectBorrowRequestRequest.cs index d946630c..7cd10e4c 100644 --- a/src/Api/Controllers/Payload/Requests/Borrows/ApproveOrRejectBorrowRequestRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Borrows/ApproveOrRejectBorrowRequestRequest.cs @@ -2,6 +2,6 @@ namespace Api.Controllers.Payload.Requests.Borrows; public class ApproveOrRejectBorrowRequestRequest { - public string Reason { get; set; } + public string StaffReason { get; set; } public string Decision { get; set; } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/ApproveOrRejectImportRequest.cs b/src/Api/Controllers/Payload/Requests/Documents/ApproveOrRejectImportRequest.cs index 83e90fe3..5281b1e5 100644 --- a/src/Api/Controllers/Payload/Requests/Documents/ApproveOrRejectImportRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Documents/ApproveOrRejectImportRequest.cs @@ -3,5 +3,5 @@ namespace Api.Controllers.Payload.Requests.Documents; public class ApproveOrRejectImportRequest { public string Decision { get; set; } = null!; - public string Reason { get; set; } = null!; + public string StaffReason { get; set; } = null!; } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/RequestImportDocumentRequest.cs b/src/Api/Controllers/Payload/Requests/Documents/RequestImportDocumentRequest.cs index ea5bb0e4..c673896d 100644 --- a/src/Api/Controllers/Payload/Requests/Documents/RequestImportDocumentRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Documents/RequestImportDocumentRequest.cs @@ -17,8 +17,8 @@ public class RequestImportDocumentRequest /// Document type of the document to be imported /// public string DocumentType { get; set; } = null!; + public string ImportReason { get; set; } = null!; - public string Reason { get; set; } = null!; public Guid RoomId { get; set; } public bool IsPrivate { get; set; } } \ No newline at end of file diff --git a/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs b/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs index 91a974e9..45120059 100644 --- a/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs +++ b/src/Application/Borrows/Commands/ApproveOrRejectBorrowRequest.cs @@ -20,7 +20,7 @@ public record Command : IRequest public Guid CurrentUserId { get; init; } public Guid BorrowId { get; init; } public string Decision { get; init; } = null!; - public string Reason { get; init; } = null!; + public string StaffReason { get; init; } = null!; } public class CommandHandler : IRequestHandler @@ -142,7 +142,7 @@ is BorrowRequestStatus.Approved requestLog.Action = RequestLogMessages.RejectBorrow; } - borrowRequest.Reason = request.Reason; + borrowRequest.StaffReason = request.StaffReason; borrowRequest.LastModified = localDateTimeNow; borrowRequest.LastModifiedBy = currentUser.Id; diff --git a/src/Application/Borrows/Commands/BorrowDocument.cs b/src/Application/Borrows/Commands/BorrowDocument.cs index becbe90b..7fbf04d7 100644 --- a/src/Application/Borrows/Commands/BorrowDocument.cs +++ b/src/Application/Borrows/Commands/BorrowDocument.cs @@ -24,7 +24,7 @@ public Validator() { RuleLevelCascadeMode = CascadeMode.Stop; - RuleFor(x => x.Reason) + RuleFor(x => x.BorrowReason) .MaximumLength(512).WithMessage("Reason cannot exceed 512 characters."); RuleFor(x => x.BorrowFrom) @@ -42,7 +42,7 @@ public record Command : IRequest public Guid BorrowerId { get; init; } public DateTime BorrowFrom { get; init; } public DateTime BorrowTo { get; init; } - public string Reason { get; init; } = null!; + public string BorrowReason { get; init; } = null!; } public class CommandHandler : IRequestHandler @@ -139,7 +139,8 @@ is BorrowRequestStatus.Approved Document = document, BorrowTime = borrowFromTime, DueTime = borrowToTime, - Reason = request.Reason, + BorrowReason = request.BorrowReason, + StaffReason = string.Empty, Status = BorrowRequestStatus.Pending, Created = localDateTimeNow, CreatedBy = user.Id, diff --git a/src/Application/Borrows/Commands/UpdateBorrow.cs b/src/Application/Borrows/Commands/UpdateBorrow.cs index bd12086d..c8cd900f 100644 --- a/src/Application/Borrows/Commands/UpdateBorrow.cs +++ b/src/Application/Borrows/Commands/UpdateBorrow.cs @@ -22,7 +22,7 @@ public Validator() { RuleLevelCascadeMode = CascadeMode.Stop; - RuleFor(x => x.Reason) + RuleFor(x => x.BorrowReason) .MaximumLength(512).WithMessage("Reason cannot exceed 512 characters."); RuleFor(x => x.BorrowFrom) @@ -40,7 +40,7 @@ public record Command : IRequest public Guid BorrowId { get; init; } public DateTime BorrowFrom { get; init; } public DateTime BorrowTo { get; init; } - public string Reason { get; init; } = null!; + public string BorrowReason { get; init; } = null!; } public class CommandHandler : IRequestHandler @@ -105,7 +105,7 @@ is BorrowRequestStatus.Approved } borrowRequest.BorrowTime = borrowFromTime; borrowRequest.DueTime = borrowToTime; - borrowRequest.Reason = request.Reason; + borrowRequest.BorrowReason = request.BorrowReason; borrowRequest.LastModified = localDateTimeNow; borrowRequest.LastModifiedBy = request.CurrentUser.Id; diff --git a/src/Application/Common/Models/Dtos/ImportDocument/ImportRequestDto.cs b/src/Application/Common/Models/Dtos/ImportDocument/ImportRequestDto.cs index c86b3a7e..d05b3774 100644 --- a/src/Application/Common/Models/Dtos/ImportDocument/ImportRequestDto.cs +++ b/src/Application/Common/Models/Dtos/ImportDocument/ImportRequestDto.cs @@ -8,7 +8,8 @@ public class ImportRequestDto : BaseDto, IMapFrom { public IssuedRequestRoomDto Room { get; set; } = null!; public IssuedDocumentDto Document { get; set; } = null!; - public string Reason { get; set; } = null!; + public string ImportReason { get; set; } = null!; + public string StaffReason { get; set; } = null!; public string Status { get; set; } = null!; public void Mapping(Profile profile) diff --git a/src/Application/Common/Models/Dtos/Logging/DocumentLogDto.cs b/src/Application/Common/Models/Dtos/Logging/DocumentLogDto.cs index 7e68116e..c7098277 100644 --- a/src/Application/Common/Models/Dtos/Logging/DocumentLogDto.cs +++ b/src/Application/Common/Models/Dtos/Logging/DocumentLogDto.cs @@ -10,7 +10,7 @@ public class DocumentLogDto : BaseDto, IMapFrom { public Guid UserId { get; set; } public string Action { get; set; } - public DocumentDto? Object { get; set; } + public Guid? ObjectId { get; set; } public DateTime Time { get; set; } public UserDto User { get; set; } @@ -19,9 +19,7 @@ public void Mapping(Profile profile) profile.CreateMap() .ForMember(dest => dest.Time, - opt => opt.MapFrom(src => src.Time.ToDateTimeUnspecified())) - .ForMember(dest => dest.Object, - opt => opt.MapFrom(src => src.ObjectId)); + opt => opt.MapFrom(src => src.Time.ToDateTimeUnspecified())); } } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Logging/FolderLogDto.cs b/src/Application/Common/Models/Dtos/Logging/FolderLogDto.cs index 2717f224..f5287aea 100644 --- a/src/Application/Common/Models/Dtos/Logging/FolderLogDto.cs +++ b/src/Application/Common/Models/Dtos/Logging/FolderLogDto.cs @@ -9,16 +9,14 @@ namespace Application.Common.Models.Dtos.Logging; public class FolderLogDto : BaseDto, IMapFrom { public string Action { get; set; } = null!; - public FolderDto? Object { get; set; } + public Guid? ObjectId { get; set; } public DateTime Time { get; set; } public UserDto User { get; set; } = null!; public void Mapping(Profile profile) { profile.CreateMap() - .ForMember( dest => dest.Time, - opt => opt.MapFrom( src => src.Time.ToDateTimeUnspecified())) - .ForMember(dest => dest.Object, - opt => opt.MapFrom( src => src.ObjectId)); + .ForMember(dest => dest.Time, + opt => opt.MapFrom(src => src.Time.ToDateTimeUnspecified())); } } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Logging/LockerLogDto.cs b/src/Application/Common/Models/Dtos/Logging/LockerLogDto.cs index 8ce8516a..dc532aa7 100644 --- a/src/Application/Common/Models/Dtos/Logging/LockerLogDto.cs +++ b/src/Application/Common/Models/Dtos/Logging/LockerLogDto.cs @@ -9,7 +9,7 @@ namespace Application.Common.Models.Dtos.Logging; public class LockerLogDto : BaseDto, IMapFrom { public string Action { get; set; } = null!; - public LockerDto? Object { get; set; } + public Guid? ObjectId { get; set; } public DateTime Time { get; set; } public UserDto User { get; set; } = null!; @@ -17,8 +17,6 @@ public void Mapping(Profile profile) { profile.CreateMap() .ForMember( dest => dest.Time, - opt => opt.MapFrom( src => src.Time.ToDateTimeUnspecified())) - .ForMember(dest => dest.Object, - opt => opt.MapFrom( src => src.ObjectId)); + opt => opt.MapFrom( src => src.Time.ToDateTimeUnspecified())); } } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Logging/RequestLogDto.cs b/src/Application/Common/Models/Dtos/Logging/RequestLogDto.cs index e0613a22..a874bd7a 100644 --- a/src/Application/Common/Models/Dtos/Logging/RequestLogDto.cs +++ b/src/Application/Common/Models/Dtos/Logging/RequestLogDto.cs @@ -9,10 +9,9 @@ namespace Application.Common.Models.Dtos.Logging; public class RequestLogDto : BaseDto, IMapFrom { public string Action { get; set; } = null!; - public DocumentDto? Object { get; set; } + public Guid? ObjectId { get; set; } public DateTime Time { get; set; } public UserDto User { get; set; } = null!; - public string Reason { get; set; } = null!; public string Type { get; set; } = null!; public void Mapping(Profile profile) @@ -20,8 +19,6 @@ public void Mapping(Profile profile) profile.CreateMap() .ForMember(dest => dest.Time, opt => opt.MapFrom(src => src.Time.ToDateTimeUnspecified())) - .ForMember(dest => dest.Object, - opt => opt.MapFrom(src => src.ObjectId)) .ForMember(dest => dest.Type, opt => opt.MapFrom(src => src.Type.ToString())); } diff --git a/src/Application/Common/Models/Dtos/Logging/RoomLogDto.cs b/src/Application/Common/Models/Dtos/Logging/RoomLogDto.cs index 7c2e832e..8d05216a 100644 --- a/src/Application/Common/Models/Dtos/Logging/RoomLogDto.cs +++ b/src/Application/Common/Models/Dtos/Logging/RoomLogDto.cs @@ -10,7 +10,7 @@ public class RoomLogDto : BaseDto, IMapFrom { public Guid UserId { get; set; } public string Action { get; set; } - public RoomDto? Object { get; set; } + public Guid? ObjectId { get; set; } public DateTime Time { get; set; } public UserDto User { get; set; } @@ -19,9 +19,7 @@ public void Mapping(Profile profile) profile.CreateMap() .ForMember(dest => dest.Time, - opt => opt.MapFrom(src => src.Time.ToDateTimeUnspecified())) - .ForMember(dest => dest.Object, - opt => opt.MapFrom(src => src.ObjectId)); + opt => opt.MapFrom(src => src.Time.ToDateTimeUnspecified())); } } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Logging/UserLogDto.cs b/src/Application/Common/Models/Dtos/Logging/UserLogDto.cs index 316cf930..83a1d38a 100644 --- a/src/Application/Common/Models/Dtos/Logging/UserLogDto.cs +++ b/src/Application/Common/Models/Dtos/Logging/UserLogDto.cs @@ -8,7 +8,7 @@ namespace Application.Common.Models.Dtos.Logging; public class UserLogDto : BaseDto, IMapFrom { public string Action { get; set; } = null!; - public UserDto? Object { get; set; } + public Guid? ObjectId { get; set; } public DateTime Time { get; set; } public UserDto User { get; set; } = null!; @@ -16,9 +16,7 @@ public void Mapping(Profile profile) { profile.CreateMap() .ForMember( dest => dest.Time, - opt => opt.MapFrom( src => src.Time.ToDateTimeUnspecified())) - .ForMember(dest => dest.Object, - opt => opt.MapFrom( src => src.ObjectId)); + opt => opt.MapFrom( src => src.Time.ToDateTimeUnspecified())); } } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/Physical/BorrowDto.cs b/src/Application/Common/Models/Dtos/Physical/BorrowDto.cs index 1401cb19..8199fa36 100644 --- a/src/Application/Common/Models/Dtos/Physical/BorrowDto.cs +++ b/src/Application/Common/Models/Dtos/Physical/BorrowDto.cs @@ -12,7 +12,8 @@ public class BorrowDto : BaseDto, IMapFrom public DateTime BorrowTime { get; set; } public DateTime DueTime { get; set; } public DateTime ActualReturnTime { get; set; } - public string Reason { get; set; } = null!; + public string BorrowReason { get; set; } = null!; + public string StaffReason { get; set; } = null!; public string Status { get; set; } = null!; public void Mapping(Profile profile) diff --git a/src/Application/Common/Models/Dtos/ReasonDto.cs b/src/Application/Common/Models/Dtos/ReasonDto.cs deleted file mode 100644 index 8e82c755..00000000 --- a/src/Application/Common/Models/Dtos/ReasonDto.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Application.Common.Mappings; -using Domain.Entities.Logging; -using Domain.Enums; - -namespace Application.Common.Models.Dtos; - -public class ReasonDto : IMapFrom -{ - public RequestType Type { get; set; } - public string Reason { get; set; } = null!; -} \ No newline at end of file diff --git a/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs b/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs index 6f658ae4..e428ad20 100644 --- a/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs +++ b/src/Application/ImportRequests/Commands/ApproveOrRejectDocument.cs @@ -32,7 +32,7 @@ public record Command : IRequest public User CurrentUser { get; init; } = null!; public Guid ImportRequestId { get; init; } public string Decision { get; init; } = null!; - public string Reason { get; init; } = null!; + public string StaffReason { get; init; } = null!; } public class CommandHandler : IRequestHandler @@ -123,7 +123,7 @@ public async Task Handle(Command request, CancellationToken ca requestLog.Action = RequestLogMessages.RejectImport; } - importRequest.Reason = request.Reason; + importRequest.StaffReason = request.StaffReason; importRequest.LastModified = localDateTimeNow; importRequest.LastModifiedBy = request.CurrentUser.Id; diff --git a/src/Application/ImportRequests/Commands/RequestImportDocument.cs b/src/Application/ImportRequests/Commands/RequestImportDocument.cs index 15a83b8f..d27cbeef 100644 --- a/src/Application/ImportRequests/Commands/RequestImportDocument.cs +++ b/src/Application/ImportRequests/Commands/RequestImportDocument.cs @@ -20,10 +20,10 @@ public record Command : IRequest public string Title { get; init; } = null!; public string? Description { get; init; } public string DocumentType { get; init; } = null!; + public string ImportReason { get; init; } = null!; public User Issuer { get; init; } = null!; public Guid RoomId { get; init; } public bool IsPrivate { get; init; } - public string Reason { get; init; } = null!; } public class CommandHandler : IRequestHandler @@ -80,7 +80,8 @@ public async Task Handle(Command request, CancellationToken ca Room = room, Created = localDateTimeNow, CreatedBy = request.Issuer.Id, - Reason = request.Reason + ImportReason = request.ImportReason, + StaffReason = string.Empty, }; var log = new DocumentLog() diff --git a/src/Domain/Entities/Physical/Borrow.cs b/src/Domain/Entities/Physical/Borrow.cs index e5ece11f..bd05231a 100644 --- a/src/Domain/Entities/Physical/Borrow.cs +++ b/src/Domain/Entities/Physical/Borrow.cs @@ -11,6 +11,7 @@ public class Borrow : BaseAuditableEntity public LocalDateTime BorrowTime { get; set; } public LocalDateTime DueTime { get; set; } public LocalDateTime ActualReturnTime { get; set; } - public string Reason { get; set; } = null!; + public string BorrowReason { get; set; } = null!; + public string StaffReason { get; set; } = null!; public BorrowRequestStatus Status { get; set; } } \ No newline at end of file diff --git a/src/Domain/Entities/Physical/ImportRequest.cs b/src/Domain/Entities/Physical/ImportRequest.cs index bf6ad3a5..b5060d88 100644 --- a/src/Domain/Entities/Physical/ImportRequest.cs +++ b/src/Domain/Entities/Physical/ImportRequest.cs @@ -7,7 +7,8 @@ public class ImportRequest : BaseAuditableEntity { public Guid RoomId { get; set; } public Guid DocumentId { get; set; } - public string Reason { get; set; } = null!; + public string ImportReason { get; set; } = null!; + public string StaffReason { get; set; } = null!; public ImportRequestStatus Status { get; set; } public Room Room { get; set; } = null!; diff --git a/src/Infrastructure/Persistence/Configurations/BorrowConfiguration.cs b/src/Infrastructure/Persistence/Configurations/BorrowConfiguration.cs index 78697f26..7b855437 100644 --- a/src/Infrastructure/Persistence/Configurations/BorrowConfiguration.cs +++ b/src/Infrastructure/Persistence/Configurations/BorrowConfiguration.cs @@ -28,7 +28,7 @@ public void Configure(EntityTypeBuilder builder) builder.Property(x => x.DueTime) .IsRequired(); - builder.Property(x => x.Reason) + builder.Property(x => x.BorrowReason) .IsRequired(); builder.Property(x => x.Status) diff --git a/src/Infrastructure/Persistence/Configurations/ImportRequestConfiguration.cs b/src/Infrastructure/Persistence/Configurations/ImportRequestConfiguration.cs index 5c53becd..c9e4e1b6 100644 --- a/src/Infrastructure/Persistence/Configurations/ImportRequestConfiguration.cs +++ b/src/Infrastructure/Persistence/Configurations/ImportRequestConfiguration.cs @@ -22,7 +22,7 @@ public void Configure(EntityTypeBuilder builder) .HasForeignKey(x => x.RoomId) .IsRequired(); - builder.Property(x => x.Reason) + builder.Property(x => x.ImportReason) .IsRequired(); builder.Property(x => x.Status) diff --git a/src/Infrastructure/Persistence/Migrations/20230621135501_AddMoreReason.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230621135501_AddMoreReason.Designer.cs new file mode 100644 index 00000000..938aeb1a --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230621135501_AddMoreReason.Designer.cs @@ -0,0 +1,1068 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230621135501_AddMoreReason")] + partial class AddMoreReason + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseFolderId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseFolderId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseLockerId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseLockerId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("BaseRoomId") + .HasColumnType("uuid"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("BaseRoomId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("StaffReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("ImportReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.Property("StaffReason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DocumentId") + .IsUnique(); + + b.HasIndex("RoomId"); + + b.ToTable("ImportRequests"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "BaseFolder") + .WithMany() + .HasForeignKey("BaseFolderId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseFolder"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "BaseLocker") + .WithMany() + .HasForeignKey("BaseLockerId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseLocker"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "BaseRoom") + .WithMany() + .HasForeignKey("BaseRoomId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BaseRoom"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Entry"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.ImportRequest", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.ImportRequest", "DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany() + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany("Rooms") + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230621135501_AddMoreReason.cs b/src/Infrastructure/Persistence/Migrations/20230621135501_AddMoreReason.cs new file mode 100644 index 00000000..04890a1d --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230621135501_AddMoreReason.cs @@ -0,0 +1,60 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class AddMoreReason : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Reason", + table: "ImportRequests", + newName: "StaffReason"); + + migrationBuilder.RenameColumn( + name: "Reason", + table: "Borrows", + newName: "StaffReason"); + + migrationBuilder.AddColumn( + name: "ImportReason", + table: "ImportRequests", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "BorrowReason", + table: "Borrows", + type: "text", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ImportReason", + table: "ImportRequests"); + + migrationBuilder.DropColumn( + name: "BorrowReason", + table: "Borrows"); + + migrationBuilder.RenameColumn( + name: "StaffReason", + table: "ImportRequests", + newName: "Reason"); + + migrationBuilder.RenameColumn( + name: "StaffReason", + table: "Borrows", + newName: "Reason"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index 70a25d80..ba5d9a63 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -287,6 +287,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ActualReturnTime") .HasColumnType("timestamp without time zone"); + b.Property("BorrowReason") + .IsRequired() + .HasColumnType("text"); + b.Property("BorrowTime") .HasColumnType("timestamp without time zone"); @@ -311,7 +315,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("LastModifiedBy") .HasColumnType("uuid"); - b.Property("Reason") + b.Property("StaffReason") .IsRequired() .HasColumnType("text"); @@ -452,19 +456,23 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DocumentId") .HasColumnType("uuid"); + b.Property("ImportReason") + .IsRequired() + .HasColumnType("text"); + b.Property("LastModified") .HasColumnType("timestamp without time zone"); b.Property("LastModifiedBy") .HasColumnType("uuid"); - b.Property("Reason") - .IsRequired() - .HasColumnType("text"); - b.Property("RoomId") .HasColumnType("uuid"); + b.Property("StaffReason") + .IsRequired() + .HasColumnType("text"); + b.Property("Status") .HasColumnType("integer"); diff --git a/tests/Application.Tests.Integration/BaseClassFixture.cs b/tests/Application.Tests.Integration/BaseClassFixture.cs index eaa0afa5..f4644e5e 100644 --- a/tests/Application.Tests.Integration/BaseClassFixture.cs +++ b/tests/Application.Tests.Integration/BaseClassFixture.cs @@ -242,7 +242,7 @@ protected static Borrow CreateBorrowRequest(User borrower, Document document, Bo Id = Guid.NewGuid(), Borrower = borrower, Document = document, - Reason = "something something", + BorrowReason = "something something", Status = status, BorrowTime = LocalDateTime.FromDateTime(DateTime.Now), DueTime = LocalDateTime.FromDateTime(DateTime.Now + TimeSpan.FromDays(1)) diff --git a/tests/Application.Tests.Integration/Borrows/Commands/BorrowDocumentTests.cs b/tests/Application.Tests.Integration/Borrows/Commands/BorrowDocumentTests.cs index b595d20f..ee5a8708 100644 --- a/tests/Application.Tests.Integration/Borrows/Commands/BorrowDocumentTests.cs +++ b/tests/Application.Tests.Integration/Borrows/Commands/BorrowDocumentTests.cs @@ -42,7 +42,7 @@ public async Task ShouldCreateBorrowRequest_WhenDetailsAreValid() { BorrowerId = user.Id, DocumentId = document.Id, - Reason = "Example", + BorrowReason = "Example", BorrowFrom = DateTime.Now.Add(TimeSpan.FromHours(1)), BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(1)), }; @@ -53,7 +53,7 @@ public async Task ShouldCreateBorrowRequest_WhenDetailsAreValid() // Assert result.DocumentId.Should().Be(command.DocumentId); result.BorrowerId.Should().Be(command.BorrowerId); - result.Reason.Should().Be(command.Reason); + result.BorrowReason.Should().Be(command.BorrowReason); result.BorrowTime.Should().Be(command.BorrowFrom); result.DueTime.Should().Be(command.BorrowTo); result.Status.Should().Be(BorrowRequestStatus.Pending.ToString()); @@ -79,7 +79,7 @@ public async Task ShouldThrowKeyNotFoundException_WhenUserDoesNotExist() DocumentId = document.Id, BorrowFrom = DateTime.Now.Add(TimeSpan.FromDays(1)), BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), - Reason = "Example", + BorrowReason = "Example", }; // Act @@ -111,7 +111,7 @@ public async Task ShouldThrowConflictException_WhenUserIsNotActive() DocumentId = document.Id, BorrowFrom = DateTime.Now.Add(TimeSpan.FromDays(1)), BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), - Reason = "Example", + BorrowReason = "Example", }; // Act @@ -144,7 +144,7 @@ public async Task ShouldThrowConflictException_WhenUserIsNotActivated() DocumentId = document.Id, BorrowFrom = DateTime.Now.Add(TimeSpan.FromDays(1)), BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), - Reason = "Example", + BorrowReason = "Example", }; // Act @@ -172,7 +172,7 @@ public async Task ShouldThrowKeyNotFoundException_WhenDocumentDoesNotExist() DocumentId = Guid.NewGuid(), BorrowFrom = DateTime.Now.Add(TimeSpan.FromDays(1)), BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), - Reason = "Example", + BorrowReason = "Example", }; // Act @@ -203,7 +203,7 @@ public async Task ShouldConflictException_WhenDocumentIsNotAvailable() DocumentId = document.Id, BorrowFrom = DateTime.Now.Add(TimeSpan.FromDays(1)), BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), - Reason = "Example", + BorrowReason = "Example", }; // Act @@ -244,7 +244,7 @@ public async Task ShouldConflictException_WhenUserAndDocumentDoesNotBelongToTheS DocumentId = document.Id, BorrowFrom = DateTime.Now.Add(TimeSpan.FromDays(1)), BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), - Reason = "Example", + BorrowReason = "Example", }; // Act @@ -287,7 +287,7 @@ public async Task ShouldThrowConflictException_WhenRequestWithSameUserAndDocumen DocumentId = document.Id, BorrowFrom = DateTime.Now.Add(TimeSpan.FromDays(1)), BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), - Reason = "Example", + BorrowReason = "Example", }; // Act @@ -334,7 +334,7 @@ public async Task ShouldThrowConflictException_WhenARequestIsMadeWhileDocumentIs DocumentId = document.Id, BorrowFrom = DateTime.Now.AddHours(1), BorrowTo = DateTime.Now.Add(TimeSpan.FromDays(2)), - Reason = "Example", + BorrowReason = "Example", }; // Act diff --git a/tests/Application.Tests.Integration/Borrows/Commands/UpdateBorrowTests.cs b/tests/Application.Tests.Integration/Borrows/Commands/UpdateBorrowTests.cs index f00d3e25..841a1f04 100644 --- a/tests/Application.Tests.Integration/Borrows/Commands/UpdateBorrowTests.cs +++ b/tests/Application.Tests.Integration/Borrows/Commands/UpdateBorrowTests.cs @@ -32,7 +32,7 @@ public async Task ShouldUpdateBorrow_WhenDetailsAreValid() var command = new UpdateBorrow.Command() { - Reason = "Example Update", + BorrowReason = "Example Update", BorrowFrom = DateTime.Now.AddDays(3), BorrowTo = DateTime.Now.AddDays(12), BorrowId = borrow.Id, @@ -42,7 +42,7 @@ public async Task ShouldUpdateBorrow_WhenDetailsAreValid() var result = await SendAsync(command); // Assert - result.Reason.Should().Be(command.Reason); + result.BorrowReason.Should().Be(command.BorrowReason); result.BorrowTime.Should().Be(command.BorrowFrom); result.DueTime.Should().Be(command.BorrowTo); @@ -58,7 +58,7 @@ public async Task ShouldThrowKeyNotFoundException_WhenRequestDoesNotExist() // Arrange var command = new UpdateBorrow.Command() { - Reason = "adsda", + BorrowReason = "adsda", BorrowFrom = DateTime.Now.AddHours(1), BorrowTo = DateTime.Now.AddHours(2), BorrowId = Guid.NewGuid(), @@ -86,7 +86,7 @@ public async Task ShouldThrowConflictException_WhenRequestStatusIsNotPending() var command = new UpdateBorrow.Command() { - Reason = "Example Update", + BorrowReason = "Example Update", BorrowFrom = DateTime.Now.AddDays(3), BorrowTo = DateTime.Now.AddDays(12), BorrowId = borrow.Id, @@ -120,7 +120,7 @@ public async Task ShouldThrowConflictException_WhenDocumentIsLost() var command = new UpdateBorrow.Command() { - Reason = "Example Update", + BorrowReason = "Example Update", BorrowFrom = DateTime.Now.AddDays(3), BorrowTo = DateTime.Now.AddDays(12), BorrowId = borrow.Id, @@ -163,7 +163,7 @@ public async Task ShouldThrowConflictException_WhenRequestTimespanOverlapAnAppro var command = new UpdateBorrow.Command() { - Reason = "Example Update", + BorrowReason = "Example Update", BorrowFrom = DateTime.Now.AddDays(5), BorrowTo = DateTime.Now.AddDays(13), BorrowId = borrow1.Id, diff --git a/tests/Application.Tests.Unit/Common/Mappings/MappingTests.cs b/tests/Application.Tests.Unit/Common/Mappings/MappingTests.cs index 49db0eca..7f6a4ac8 100644 --- a/tests/Application.Tests.Unit/Common/Mappings/MappingTests.cs +++ b/tests/Application.Tests.Unit/Common/Mappings/MappingTests.cs @@ -2,6 +2,7 @@ using Application.Common.Mappings; using Application.Common.Models.Dtos; using Application.Common.Models.Dtos.Digital; +using Application.Common.Models.Dtos.ImportDocument; using Application.Common.Models.Dtos.Physical; using Application.Users.Queries; using AutoMapper; @@ -48,6 +49,9 @@ public void ShouldHaveValidConfiguration() [InlineData(typeof(FileEntity), typeof(FileDto))] [InlineData(typeof(Entry), typeof(EntryDto))] [InlineData(typeof(UserGroup), typeof(UserGroupDto))] + [InlineData(typeof(User), typeof(IssuerDto))] + [InlineData(typeof(Document), typeof(IssuedDocumentDto))] + [InlineData(typeof(ImportRequest), typeof(ImportRequestDto))] public void ShouldSupportMappingFromSourceToDestination(Type source, Type destination) { // Arrange