From 55a44000fcc2cee2ae3ff012ab48b64e87b01e5b Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sat, 2 Jan 2021 09:25:45 -0600 Subject: [PATCH 1/6] Changed scan to POST and added new API for route gurads on UI. --- API/Controllers/LibraryController.cs | 2 +- API/Controllers/UsersController.cs | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index e06cba530..b87e75cbf 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -85,7 +85,7 @@ namespace API.Controllers } [Authorize(Policy = "RequireAdminRole")] - [HttpGet("scan")] + [HttpPost("scan")] public async Task ScanLibrary(int libraryId) { var library = await _libraryRepository.GetLibraryDtoForIdAsync(libraryId); diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index db81af51d..a332c88c9 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -84,5 +84,17 @@ namespace API.Controllers { return Ok(await _userRepository.GetMembersAsync()); } + + [HttpGet("has-library-access")] + public async Task> HasLibraryAccess(int libraryId) + { + var user = await _userRepository.GetUserByUsernameAsync(User.GetUsername()); + + if (user == null) return BadRequest("Could not validate user"); + + var libs = await _libraryRepository.GetLibrariesForUsernameAysnc(user.UserName); + + return Ok(libs.Any(x => x.Id == libraryId)); + } } } \ No newline at end of file From d632e53f18c00889a74a29782fdea640318cca4d Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sat, 2 Jan 2021 10:59:52 -0600 Subject: [PATCH 2/6] Added ability to automatically track last modified and created timestamps for entities via an interface. DBContext will automatically update for us. --- API/Data/DataContext.cs | 27 +- ...0210102165536_EntityTimestamps.Designer.cs | 509 ++++++++++++++++++ .../20210102165536_EntityTimestamps.cs | 80 +++ .../Migrations/DataContextModelSnapshot.cs | 18 + API/Entities/FolderPath.cs | 3 +- API/Entities/Interfaces/IEntityDate.cs | 10 + API/Entities/Library.cs | 9 +- API/Entities/MangaFile.cs | 3 +- API/Entities/Series.cs | 8 +- API/Entities/Volume.cs | 8 +- API/Services/DirectoryService.cs | 2 +- 11 files changed, 660 insertions(+), 17 deletions(-) create mode 100644 API/Data/Migrations/20210102165536_EntityTimestamps.Designer.cs create mode 100644 API/Data/Migrations/20210102165536_EntityTimestamps.cs create mode 100644 API/Entities/Interfaces/IEntityDate.cs diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index a83a76e9e..7a75ad138 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -1,16 +1,21 @@ -using API.Entities; +using System; +using API.Entities; +using API.Entities.Interfaces; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; namespace API.Data { - public class DataContext : IdentityDbContext, AppUserRole, IdentityUserLogin, IdentityRoleClaim, IdentityUserToken> { public DataContext(DbContextOptions options) : base(options) { + ChangeTracker.Tracked += OnEntityTracked; + ChangeTracker.StateChanged += OnEntityStateChanged; } public DbSet Library { get; set; } @@ -33,12 +38,18 @@ namespace API.Data .WithOne(u => u.Role) .HasForeignKey(ur => ur.RoleId) .IsRequired(); - - // builder.Entity() - // .HasMany(s => s.Series) - // .WithOne(l => l.Library) - // .HasForeignKey(x => x.Id) - + } + + void OnEntityTracked(object sender, EntityTrackedEventArgs e) + { + if (!e.FromQuery && e.Entry.State == EntityState.Added && e.Entry.Entity is IEntityDate entity) + entity.Created = DateTime.Now; + } + + void OnEntityStateChanged(object sender, EntityStateChangedEventArgs e) + { + if (e.NewState == EntityState.Modified && e.Entry.Entity is IEntityDate entity) + entity.LastModified = DateTime.Now; } } } \ No newline at end of file diff --git a/API/Data/Migrations/20210102165536_EntityTimestamps.Designer.cs b/API/Data/Migrations/20210102165536_EntityTimestamps.Designer.cs new file mode 100644 index 000000000..de4910b51 --- /dev/null +++ b/API/Data/Migrations/20210102165536_EntityTimestamps.Designer.cs @@ -0,0 +1,509 @@ +// +using System; +using API.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace API.Data.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20210102165536_EntityTimestamps")] + partial class EntityTimestamps + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.1"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("IsAdmin") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Folders") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Files") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Series") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Volumes") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Navigation("Volumes"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Navigation("Files"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20210102165536_EntityTimestamps.cs b/API/Data/Migrations/20210102165536_EntityTimestamps.cs new file mode 100644 index 000000000..2ed6041f0 --- /dev/null +++ b/API/Data/Migrations/20210102165536_EntityTimestamps.cs @@ -0,0 +1,80 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace API.Data.Migrations +{ + public partial class EntityTimestamps : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Created", + table: "Volume", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "LastModified", + table: "Volume", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "Created", + table: "Series", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "LastModified", + table: "Series", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "Created", + table: "Library", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "LastModified", + table: "Library", + type: "TEXT", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Created", + table: "Volume"); + + migrationBuilder.DropColumn( + name: "LastModified", + table: "Volume"); + + migrationBuilder.DropColumn( + name: "Created", + table: "Series"); + + migrationBuilder.DropColumn( + name: "LastModified", + table: "Series"); + + migrationBuilder.DropColumn( + name: "Created", + table: "Library"); + + migrationBuilder.DropColumn( + name: "LastModified", + table: "Library"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index 110c60774..f840250c9 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -164,6 +164,12 @@ namespace API.Data.Migrations b.Property("CoverImage") .HasColumnType("TEXT"); + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + b.Property("Name") .HasColumnType("TEXT"); @@ -203,6 +209,12 @@ namespace API.Data.Migrations b.Property("CoverImage") .HasColumnType("TEXT"); + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + b.Property("LibraryId") .HasColumnType("INTEGER"); @@ -231,6 +243,12 @@ namespace API.Data.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + b.Property("Number") .HasColumnType("TEXT"); diff --git a/API/Entities/FolderPath.cs b/API/Entities/FolderPath.cs index d1e49180f..84d3ea798 100644 --- a/API/Entities/FolderPath.cs +++ b/API/Entities/FolderPath.cs @@ -1,4 +1,5 @@ -namespace API.Entities + +namespace API.Entities { public class FolderPath { diff --git a/API/Entities/Interfaces/IEntityDate.cs b/API/Entities/Interfaces/IEntityDate.cs new file mode 100644 index 000000000..79330546e --- /dev/null +++ b/API/Entities/Interfaces/IEntityDate.cs @@ -0,0 +1,10 @@ +using System; + +namespace API.Entities.Interfaces +{ + public interface IEntityDate + { + DateTime Created { get; set; } + DateTime LastModified { get; set; } + } +} \ No newline at end of file diff --git a/API/Entities/Library.cs b/API/Entities/Library.cs index 53b3073a7..3c2129c37 100644 --- a/API/Entities/Library.cs +++ b/API/Entities/Library.cs @@ -1,15 +1,20 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using API.Entities.Interfaces; namespace API.Entities { - public class Library + public class Library : IEntityDate { public int Id { get; set; } public string Name { get; set; } public string CoverImage { get; set; } public LibraryType Type { get; set; } + public DateTime Created { get; set; } + public DateTime LastModified { get; set; } public ICollection Folders { get; set; } public ICollection AppUsers { get; set; } public ICollection Series { get; set; } + } } \ No newline at end of file diff --git a/API/Entities/MangaFile.cs b/API/Entities/MangaFile.cs index 685d399e9..06e132193 100644 --- a/API/Entities/MangaFile.cs +++ b/API/Entities/MangaFile.cs @@ -1,4 +1,5 @@ -namespace API.Entities + +namespace API.Entities { public class MangaFile { diff --git a/API/Entities/Series.cs b/API/Entities/Series.cs index dd47e528d..f20fc8658 100644 --- a/API/Entities/Series.cs +++ b/API/Entities/Series.cs @@ -1,8 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using API.Entities.Interfaces; namespace API.Entities { - public class Series + public class Series : IEntityDate { public int Id { get; set; } /// @@ -22,6 +24,8 @@ namespace API.Entities /// public string Summary { get; set; } public string CoverImage { get; set; } + public DateTime Created { get; set; } + public DateTime LastModified { get; set; } public ICollection Volumes { get; set; } public Library Library { get; set; } diff --git a/API/Entities/Volume.cs b/API/Entities/Volume.cs index bb3638323..80d1f6573 100644 --- a/API/Entities/Volume.cs +++ b/API/Entities/Volume.cs @@ -1,12 +1,16 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using API.Entities.Interfaces; namespace API.Entities { - public class Volume + public class Volume : IEntityDate { public int Id { get; set; } public string Number { get; set; } public ICollection Files { get; set; } + public DateTime Created { get; set; } + public DateTime LastModified { get; set; } // Many-to-Many relationships public Series Series { get; set; } diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index de1cf994b..f8ee643ce 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -223,7 +223,7 @@ namespace API.Services foreach (var seriesKey in series.Keys) { var s = UpdateSeries(seriesKey, series[seriesKey].ToArray()); - Console.WriteLine($"Created/Updated series {s.Name}"); + _logger.LogInformation($"Created/Updated series {s.Name}"); libraryEntity.Series.Add(s); } From 9168e1248397259eb425b70d7c967b8448c80dae Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sat, 2 Jan 2021 12:21:36 -0600 Subject: [PATCH 3/6] Refactored Volume to have Name and Number (int) so that we can properly sort and still handle possible split volumes. Refactored ScanLibrary into Library controller and updated it so it adds the new library to all admins. --- API/Controllers/AccountController.cs | 2 + API/Controllers/LibraryController.cs | 44 +- API/Controllers/UsersController.cs | 41 -- API/DTOs/VolumeDto.cs | 3 +- ...102173326_VolumeNumberRefactor.Designer.cs | 512 ++++++++++++++++++ .../20210102173326_VolumeNumberRefactor.cs | 41 ++ .../Migrations/DataContextModelSnapshot.cs | 5 +- API/Data/SeriesRepository.cs | 4 + API/Data/UserRepository.cs | 6 + API/Entities/AppUser.cs | 4 +- API/Entities/Volume.cs | 3 +- API/Interfaces/IUserRepository.cs | 1 + API/Services/DirectoryService.cs | 5 +- 13 files changed, 622 insertions(+), 49 deletions(-) create mode 100644 API/Data/Migrations/20210102173326_VolumeNumberRefactor.Designer.cs create mode 100644 API/Data/Migrations/20210102173326_VolumeNumberRefactor.cs diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index 3c25974ef..54d304dd6 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -78,6 +78,8 @@ namespace API.Controllers user.LastActive = DateTime.Now; _userRepository.Update(user); await _userRepository.SaveAllAsync(); + + _logger.LogInformation($"{user.UserName} logged in at {user.LastActive}"); return new UserDto { diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index b87e75cbf..64a259503 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using API.DTOs; using API.Entities; @@ -35,6 +36,47 @@ namespace API.Controllers _taskScheduler = taskScheduler; _seriesRepository = seriesRepository; } + + /// + /// Creates a new Library. Upon library creation, adds new library to all Admin accounts. + /// + /// + /// + [Authorize(Policy = "RequireAdminRole")] + [HttpPost("create")] + public async Task AddLibrary(CreateLibraryDto createLibraryDto) + { + if (await _libraryRepository.LibraryExists(createLibraryDto.Name)) + { + return BadRequest("Library name already exists. Please choose a unique name to the server."); + } + + var admins = (await _userRepository.GetAdminUsersAsync()).ToList(); + + var library = new Library + { + Name = createLibraryDto.Name, + Type = createLibraryDto.Type, + AppUsers = admins, + Folders = createLibraryDto.Folders.Select(x => new FolderPath {Path = x}).ToList() + }; + + foreach (var admin in admins) + { + // If user is null, then set it + admin.Libraries ??= new List(); + admin.Libraries.Add(library); + } + + + if (await _userRepository.SaveAllAsync()) + { + //TODO: Enqueue scan library task + return Ok(); + } + + return BadRequest("There was a critical issue. Please try again."); + } /// /// Returns a list of directories for a given path. If path is empty, returns root drives. @@ -92,6 +134,7 @@ namespace API.Controllers // We have to send a json encoded Library (aka a DTO) to the Background Job thread. // Because we use EF, we have circular dependencies back to Library and it will crap out + // TODO: Refactor this to use libraryId and move Library call in method. BackgroundJob.Enqueue(() => _directoryService.ScanLibrary(library)); return Ok(); } @@ -106,7 +149,6 @@ namespace API.Controllers public async Task>> GetSeriesForLibrary(int libraryId) { return Ok(await _seriesRepository.GetSeriesForLibraryIdAsync(libraryId)); - } } } \ No newline at end of file diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index a332c88c9..b860316df 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -22,47 +22,6 @@ namespace API.Controllers _libraryRepository = libraryRepository; } - [Authorize(Policy = "RequireAdminRole")] - [HttpPost("add-library")] - public async Task AddLibrary(CreateLibraryDto createLibraryDto) - { - // NOTE: I think we should move this into library controller because it gets added to all admins - - var user = await _userRepository.GetUserByUsernameAsync(User.GetUsername()); - - if (user == null) return BadRequest("Could not validate user"); - - - if (await _libraryRepository.LibraryExists(createLibraryDto.Name)) - { - return BadRequest("Library name already exists. Please choose a unique name to the server."); - } - - var library = new Library - { - Name = createLibraryDto.Name.ToLower(), - Type = createLibraryDto.Type, - AppUsers = new List() { user } - }; - - library.Folders = createLibraryDto.Folders.Select(x => new FolderPath - { - Path = x, - Library = library - }).ToList(); - - user.Libraries ??= new List(); // If user is null, then set it - - user.Libraries.Add(library); - - if (await _userRepository.SaveAllAsync()) - { - return Ok(); - } - - return BadRequest("Not implemented"); - } - [Authorize(Policy = "RequireAdminRole")] [HttpDelete("delete-user")] public async Task DeleteUser(string username) diff --git a/API/DTOs/VolumeDto.cs b/API/DTOs/VolumeDto.cs index 1753d4679..b3dab2834 100644 --- a/API/DTOs/VolumeDto.cs +++ b/API/DTOs/VolumeDto.cs @@ -5,7 +5,8 @@ namespace API.DTOs public class VolumeDto { public int Id { get; set; } - public string Number { get; set; } + public int Number { get; set; } + public string Name { get; set; } public string CoverImage { get; set; } public ICollection Files { get; set; } } diff --git a/API/Data/Migrations/20210102173326_VolumeNumberRefactor.Designer.cs b/API/Data/Migrations/20210102173326_VolumeNumberRefactor.Designer.cs new file mode 100644 index 000000000..1102111fc --- /dev/null +++ b/API/Data/Migrations/20210102173326_VolumeNumberRefactor.Designer.cs @@ -0,0 +1,512 @@ +// +using System; +using API.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace API.Data.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20210102173326_VolumeNumberRefactor")] + partial class VolumeNumberRefactor + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.1"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("IsAdmin") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Folders") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Files") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Series") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Volumes") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Navigation("Volumes"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Navigation("Files"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20210102173326_VolumeNumberRefactor.cs b/API/Data/Migrations/20210102173326_VolumeNumberRefactor.cs new file mode 100644 index 000000000..21cc8d42c --- /dev/null +++ b/API/Data/Migrations/20210102173326_VolumeNumberRefactor.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace API.Data.Migrations +{ + public partial class VolumeNumberRefactor : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Number", + table: "Volume", + type: "INTEGER", + nullable: false, + defaultValue: 0, + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AddColumn( + name: "Name", + table: "Volume", + type: "TEXT", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Name", + table: "Volume"); + + migrationBuilder.AlterColumn( + name: "Number", + table: "Volume", + type: "TEXT", + nullable: true, + oldClrType: typeof(int), + oldType: "INTEGER"); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index f840250c9..5b22f751d 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -249,9 +249,12 @@ namespace API.Data.Migrations b.Property("LastModified") .HasColumnType("TEXT"); - b.Property("Number") + b.Property("Name") .HasColumnType("TEXT"); + b.Property("Number") + .HasColumnType("INTEGER"); + b.Property("SeriesId") .HasColumnType("INTEGER"); diff --git a/API/Data/SeriesRepository.cs b/API/Data/SeriesRepository.cs index fd109f45f..a9623275b 100644 --- a/API/Data/SeriesRepository.cs +++ b/API/Data/SeriesRepository.cs @@ -50,6 +50,7 @@ namespace API.Data { return await _context.Series .Where(series => series.LibraryId == libraryId) + .OrderBy(s => s.SortName) .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); } @@ -57,6 +58,7 @@ namespace API.Data { return await _context.Volume .Where(vol => vol.SeriesId == seriesId) + .OrderBy(volume => volume.Number) .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); } @@ -64,6 +66,7 @@ namespace API.Data { return _context.Volume .Where(vol => vol.SeriesId == seriesId) + .OrderBy(vol => vol.Number) .ProjectTo(_mapper.ConfigurationProvider).ToList(); } @@ -71,6 +74,7 @@ namespace API.Data { return _context.Volume .Where(vol => vol.SeriesId == seriesId) + .OrderBy(vol => vol.Number) .ToList(); } diff --git a/API/Data/UserRepository.cs b/API/Data/UserRepository.cs index 6c63d31b9..5098b156c 100644 --- a/API/Data/UserRepository.cs +++ b/API/Data/UserRepository.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using API.Constants; using API.DTOs; using API.Entities; using API.Interfaces; @@ -55,6 +56,11 @@ namespace API.Data .SingleOrDefaultAsync(x => x.UserName == username); } + public async Task> GetAdminUsersAsync() + { + return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole); + } + public async Task> GetMembersAsync() { return await _userManager.Users diff --git a/API/Entities/AppUser.cs b/API/Entities/AppUser.cs index ea7ad0245..f5b16890d 100644 --- a/API/Entities/AppUser.cs +++ b/API/Entities/AppUser.cs @@ -1,16 +1,16 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using API.Entities.Interfaces; using Microsoft.AspNetCore.Identity; namespace API.Entities { - public class AppUser : IdentityUser + public class AppUser : IdentityUser, IHasConcurrencyToken { public DateTime Created { get; set; } = DateTime.Now; public DateTime LastActive { get; set; } - public bool IsAdmin { get; set; } public ICollection Libraries { get; set; } [ConcurrencyCheck] diff --git a/API/Entities/Volume.cs b/API/Entities/Volume.cs index 80d1f6573..18cadee8d 100644 --- a/API/Entities/Volume.cs +++ b/API/Entities/Volume.cs @@ -7,7 +7,8 @@ namespace API.Entities public class Volume : IEntityDate { public int Id { get; set; } - public string Number { get; set; } + public string Name { get; set; } + public int Number { get; set; } public ICollection Files { get; set; } public DateTime Created { get; set; } public DateTime LastModified { get; set; } diff --git a/API/Interfaces/IUserRepository.cs b/API/Interfaces/IUserRepository.cs index 31be6e52e..26e3b5a11 100644 --- a/API/Interfaces/IUserRepository.cs +++ b/API/Interfaces/IUserRepository.cs @@ -15,5 +15,6 @@ namespace API.Interfaces Task> GetMembersAsync(); Task GetMemberAsync(string username); public void Delete(AppUser user); + Task> GetAdminUsersAsync(); } } \ No newline at end of file diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index f8ee643ce..9d40069e7 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -151,7 +151,7 @@ namespace API.Services IList existingVolumes = _seriesRepository.GetVolumes(series.Id).ToList(); foreach (var info in infos) { - var existingVolume = existingVolumes.SingleOrDefault(v => v.Number == info.Volumes); + var existingVolume = existingVolumes.SingleOrDefault(v => v.Name == info.Volumes); if (existingVolume != null) { // Temp let's overwrite all files (we need to enhance to update files) @@ -168,7 +168,8 @@ namespace API.Services { var vol = new Volume() { - Number = info.Volumes, + Name = info.Volumes, + Number = Int32.Parse(info.Volumes), Files = new List() { new MangaFile() From 7b1714349d0cc7b81f6e20f14bf0b7ddc7c68353 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sat, 2 Jan 2021 12:48:48 -0600 Subject: [PATCH 4/6] Refactored ScanLibrary to accept and library id rather than DTO. Refactored ScanLibrary to use Task.Run() rather than having synchronous repo methods. --- API/Controllers/LibraryController.cs | 18 +++++++---------- API/Controllers/SeriesController.cs | 4 ++-- API/Controllers/UsersController.cs | 3 +-- API/Data/LibraryRepository.cs | 30 ++++++++++------------------ API/Data/SeriesRepository.cs | 16 ++++----------- API/Interfaces/IDirectoryService.cs | 3 +-- API/Interfaces/ILibraryRepository.cs | 10 ++-------- API/Interfaces/ISeriesRepository.cs | 7 +++---- API/Services/DirectoryService.cs | 28 ++++++++++++-------------- 9 files changed, 44 insertions(+), 75 deletions(-) diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index 64a259503..40e9eb1f1 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -70,8 +70,9 @@ namespace API.Controllers if (await _userRepository.SaveAllAsync()) - { - //TODO: Enqueue scan library task + { + var createdLibrary = await _libraryRepository.GetLibraryForNameAsync(library.Name); + BackgroundJob.Enqueue(() => _directoryService.ScanLibrary(createdLibrary.Id)); return Ok(); } @@ -128,27 +129,22 @@ namespace API.Controllers [Authorize(Policy = "RequireAdminRole")] [HttpPost("scan")] - public async Task ScanLibrary(int libraryId) + public ActionResult ScanLibrary(int libraryId) { - var library = await _libraryRepository.GetLibraryDtoForIdAsync(libraryId); - - // We have to send a json encoded Library (aka a DTO) to the Background Job thread. - // Because we use EF, we have circular dependencies back to Library and it will crap out - // TODO: Refactor this to use libraryId and move Library call in method. - BackgroundJob.Enqueue(() => _directoryService.ScanLibrary(library)); + BackgroundJob.Enqueue(() => _directoryService.ScanLibrary(libraryId)); return Ok(); } [HttpGet("libraries-for")] public async Task>> GetLibrariesForUser(string username) { - return Ok(await _libraryRepository.GetLibrariesForUsernameAysnc(username)); + return Ok(await _libraryRepository.GetLibrariesDtoForUsernameAsync(username)); } [HttpGet("series")] public async Task>> GetSeriesForLibrary(int libraryId) { - return Ok(await _seriesRepository.GetSeriesForLibraryIdAsync(libraryId)); + return Ok(await _seriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId)); } } } \ No newline at end of file diff --git a/API/Controllers/SeriesController.cs b/API/Controllers/SeriesController.cs index dbdfaf93e..4c5e66a41 100644 --- a/API/Controllers/SeriesController.cs +++ b/API/Controllers/SeriesController.cs @@ -27,13 +27,13 @@ namespace API.Controllers [HttpGet("{seriesId}")] public async Task> GetSeries(int seriesId) { - return Ok(await _seriesRepository.GetSeriesByIdAsync(seriesId)); + return Ok(await _seriesRepository.GetSeriesDtoByIdAsync(seriesId)); } [HttpGet("volumes")] public async Task>> GetVolumes(int seriesId) { - return Ok(await _seriesRepository.GetVolumesAsync(seriesId)); + return Ok(await _seriesRepository.GetVolumesDtoAsync(seriesId)); } } } \ No newline at end of file diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index b860316df..535345b75 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -2,7 +2,6 @@ using System.Linq; using System.Threading.Tasks; using API.DTOs; -using API.Entities; using API.Extensions; using API.Interfaces; using Microsoft.AspNetCore.Authorization; @@ -51,7 +50,7 @@ namespace API.Controllers if (user == null) return BadRequest("Could not validate user"); - var libs = await _libraryRepository.GetLibrariesForUsernameAysnc(user.UserName); + var libs = await _libraryRepository.GetLibrariesDtoForUsernameAsync(user.UserName); return Ok(libs.Any(x => x.Id == libraryId)); } diff --git a/API/Data/LibraryRepository.cs b/API/Data/LibraryRepository.cs index ce986279f..ef8b416d3 100644 --- a/API/Data/LibraryRepository.cs +++ b/API/Data/LibraryRepository.cs @@ -36,16 +36,7 @@ namespace API.Data return _context.SaveChanges() > 0; } - public Library GetLibraryForName(string libraryName) - { - return _context.Library - .Where(x => x.Name == libraryName) - .Include(f => f.Folders) - .Include(s => s.Series) - .Single(); - } - - public async Task> GetLibrariesForUsernameAysnc(string userName) + public async Task> GetLibrariesDtoForUsernameAsync(string userName) { return await _context.Library .Include(l => l.AppUsers) @@ -53,21 +44,22 @@ namespace API.Data .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); } + public async Task GetLibraryForNameAsync(string libraryName) + { + return await _context.Library + .Where(x => x.Name == libraryName) + .Include(f => f.Folders) + .Include(s => s.Series) + .SingleAsync(); + } + public async Task> GetLibrariesAsync() { return await _context.Library .Include(f => f.Folders) .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); } - - public async Task GetLibraryDtoForIdAsync(int libraryId) - { - return await _context.Library - .Where(x => x.Id == libraryId) - .Include(f => f.Folders) - .ProjectTo(_mapper.ConfigurationProvider).SingleAsync(); - } - + public async Task GetLibraryForIdAsync(int libraryId) { return await _context.Library diff --git a/API/Data/SeriesRepository.cs b/API/Data/SeriesRepository.cs index a9623275b..011550348 100644 --- a/API/Data/SeriesRepository.cs +++ b/API/Data/SeriesRepository.cs @@ -46,7 +46,7 @@ namespace API.Data return _context.Series.SingleOrDefault(x => x.Name == name); } - public async Task> GetSeriesForLibraryIdAsync(int libraryId) + public async Task> GetSeriesDtoForLibraryIdAsync(int libraryId) { return await _context.Series .Where(series => series.LibraryId == libraryId) @@ -54,22 +54,14 @@ namespace API.Data .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); } - public async Task> GetVolumesAsync(int seriesId) + public async Task> GetVolumesDtoAsync(int seriesId) { return await _context.Volume .Where(vol => vol.SeriesId == seriesId) .OrderBy(volume => volume.Number) .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); } - - public IEnumerable GetVolumesDto(int seriesId) - { - return _context.Volume - .Where(vol => vol.SeriesId == seriesId) - .OrderBy(vol => vol.Number) - .ProjectTo(_mapper.ConfigurationProvider).ToList(); - } - + public IEnumerable GetVolumes(int seriesId) { return _context.Volume @@ -78,7 +70,7 @@ namespace API.Data .ToList(); } - public async Task GetSeriesByIdAsync(int seriesId) + public async Task GetSeriesDtoByIdAsync(int seriesId) { return await _context.Series.Where(x => x.Id == seriesId) .ProjectTo(_mapper.ConfigurationProvider).SingleAsync(); diff --git a/API/Interfaces/IDirectoryService.cs b/API/Interfaces/IDirectoryService.cs index 818aa9451..351e67a68 100644 --- a/API/Interfaces/IDirectoryService.cs +++ b/API/Interfaces/IDirectoryService.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using API.DTOs; namespace API.Interfaces { @@ -7,6 +6,6 @@ namespace API.Interfaces { IEnumerable ListDirectory(string rootPath); - void ScanLibrary(LibraryDto library); + void ScanLibrary(int libraryId); } } \ No newline at end of file diff --git a/API/Interfaces/ILibraryRepository.cs b/API/Interfaces/ILibraryRepository.cs index 409068fc3..93a0832ae 100644 --- a/API/Interfaces/ILibraryRepository.cs +++ b/API/Interfaces/ILibraryRepository.cs @@ -10,16 +10,10 @@ namespace API.Interfaces void Update(Library library); Task SaveAllAsync(); Task> GetLibrariesAsync(); - /// - /// Checks to see if a library of the same name exists. We only allow unique library names, no duplicates per LibraryType. - /// - /// - /// Task LibraryExists(string libraryName); - Task GetLibraryDtoForIdAsync(int libraryId); Task GetLibraryForIdAsync(int libraryId); bool SaveAll(); - Library GetLibraryForName(string libraryName); - Task> GetLibrariesForUsernameAysnc(string userName); + Task> GetLibrariesDtoForUsernameAsync(string userName); + Task GetLibraryForNameAsync(string libraryName); } } \ No newline at end of file diff --git a/API/Interfaces/ISeriesRepository.cs b/API/Interfaces/ISeriesRepository.cs index ddd085cec..fe2a6b6b3 100644 --- a/API/Interfaces/ISeriesRepository.cs +++ b/API/Interfaces/ISeriesRepository.cs @@ -12,11 +12,10 @@ namespace API.Interfaces Task GetSeriesByNameAsync(string name); Series GetSeriesByName(string name); bool SaveAll(); - Task> GetSeriesForLibraryIdAsync(int libraryId); - Task> GetVolumesAsync(int seriesId); - IEnumerable GetVolumesDto(int seriesId); + Task> GetSeriesDtoForLibraryIdAsync(int libraryId); + Task> GetVolumesDtoAsync(int seriesId); IEnumerable GetVolumes(int seriesId); - Task GetSeriesByIdAsync(int seriesId); + Task GetSeriesDtoByIdAsync(int seriesId); } } \ No newline at end of file diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs index 9d40069e7..706967bc1 100644 --- a/API/Services/DirectoryService.cs +++ b/API/Services/DirectoryService.cs @@ -5,11 +5,9 @@ using System.Collections.Immutable; using System.Diagnostics; using System.IO; using System.Linq; -using System.Security; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using API.DTOs; using API.Entities; using API.Interfaces; using API.Parser; @@ -149,6 +147,7 @@ namespace API.Services // BUG: This is creating new volume entries and not resetting each run. IList existingVolumes = _seriesRepository.GetVolumes(series.Id).ToList(); + //IList existingVolumes = Task.Run(() => _seriesRepository.GetVolumesAsync(series.Id)).Result.ToList(); foreach (var info in infos) { var existingVolume = existingVolumes.SingleOrDefault(v => v.Name == info.Volumes); @@ -189,46 +188,45 @@ namespace API.Services return series; } - public void ScanLibrary(LibraryDto library) + public void ScanLibrary(int libraryId) { + var library = Task.Run(() => _libraryRepository.GetLibraryForIdAsync(libraryId)).Result; _scannedSeries = new ConcurrentDictionary>(); _logger.LogInformation($"Beginning scan on {library.Name}"); foreach (var folderPath in library.Folders) { try { - TraverseTreeParallelForEach(folderPath, (f) => + TraverseTreeParallelForEach(folderPath.Path, (f) => { - // Exceptions are no-ops. try { Process(f); } - catch (FileNotFoundException) {} - catch (IOException) {} - catch (UnauthorizedAccessException) {} - catch (SecurityException) {} + catch (FileNotFoundException exception) + { + _logger.LogError(exception, "The file could not be found"); + } }); } catch (ArgumentException ex) { - _logger.LogError(ex, "The directory '{folderPath}' does not exist"); + _logger.LogError(ex, $"The directory '{folderPath}' does not exist"); } } var filtered = _scannedSeries.Where(kvp => !kvp.Value.IsEmpty); var series = filtered.ToImmutableDictionary(v => v.Key, v => v.Value); - // Perform DB activities on ImmutableDictionary - var libraryEntity = _libraryRepository.GetLibraryForName(library.Name); - libraryEntity.Series = new List(); // Temp delete everything for testing + // Perform DB activities + library.Series = new List(); // Temp delete everything until we can mark items Unavailable foreach (var seriesKey in series.Keys) { var s = UpdateSeries(seriesKey, series[seriesKey].ToArray()); _logger.LogInformation($"Created/Updated series {s.Name}"); - libraryEntity.Series.Add(s); + library.Series.Add(s); } - _libraryRepository.Update(libraryEntity); + _libraryRepository.Update(library); if (_libraryRepository.SaveAll()) { From 5e18c1bf3a5262a08895a4678329b3608aeb3408 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sat, 2 Jan 2021 12:59:04 -0600 Subject: [PATCH 5/6] Implemented the ability to delete a Library. --- API/Controllers/LibraryController.cs | 7 +++++++ API/Data/LibraryRepository.cs | 7 +++++++ API/Interfaces/ILibraryRepository.cs | 2 ++ 3 files changed, 16 insertions(+) diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index 40e9eb1f1..b72094f17 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -146,5 +146,12 @@ namespace API.Controllers { return Ok(await _seriesRepository.GetSeriesDtoForLibraryIdAsync(libraryId)); } + + [Authorize(Policy = "RequireAdminRole")] + [HttpDelete("delete")] + public async Task> DeleteLibrary(int libraryId) + { + return Ok(await _libraryRepository.DeleteLibrary(libraryId)); + } } } \ No newline at end of file diff --git a/API/Data/LibraryRepository.cs b/API/Data/LibraryRepository.cs index ef8b416d3..9c2c74b29 100644 --- a/API/Data/LibraryRepository.cs +++ b/API/Data/LibraryRepository.cs @@ -53,6 +53,13 @@ namespace API.Data .SingleAsync(); } + public async Task DeleteLibrary(int libraryId) + { + var library = await GetLibraryForIdAsync(libraryId); + _context.Library.Remove(library); + return await _context.SaveChangesAsync() > 0; + } + public async Task> GetLibrariesAsync() { return await _context.Library diff --git a/API/Interfaces/ILibraryRepository.cs b/API/Interfaces/ILibraryRepository.cs index 93a0832ae..5a919770c 100644 --- a/API/Interfaces/ILibraryRepository.cs +++ b/API/Interfaces/ILibraryRepository.cs @@ -15,5 +15,7 @@ namespace API.Interfaces bool SaveAll(); Task> GetLibrariesDtoForUsernameAsync(string userName); Task GetLibraryForNameAsync(string libraryName); + + Task DeleteLibrary(int libraryId); } } \ No newline at end of file From ac2b40aba60e85f2f771d5128c471a244c758eaa Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sun, 3 Jan 2021 14:16:06 -0600 Subject: [PATCH 6/6] Added migration for removing IsAdmin column since we use IdentityService Roles instead. --- ...210103201043_RemoveUserIsAdmin.Designer.cs | 509 ++++++++++++++++++ .../20210103201043_RemoveUserIsAdmin.cs | 24 + .../Migrations/DataContextModelSnapshot.cs | 3 - API/Entities/AppUser.cs | 1 - 4 files changed, 533 insertions(+), 4 deletions(-) create mode 100644 API/Data/Migrations/20210103201043_RemoveUserIsAdmin.Designer.cs create mode 100644 API/Data/Migrations/20210103201043_RemoveUserIsAdmin.cs diff --git a/API/Data/Migrations/20210103201043_RemoveUserIsAdmin.Designer.cs b/API/Data/Migrations/20210103201043_RemoveUserIsAdmin.Designer.cs new file mode 100644 index 000000000..4288a9878 --- /dev/null +++ b/API/Data/Migrations/20210103201043_RemoveUserIsAdmin.Designer.cs @@ -0,0 +1,509 @@ +// +using System; +using API.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace API.Data.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20210103201043_RemoveUserIsAdmin")] + partial class RemoveUserIsAdmin + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.1"); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("FolderPath"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FilePath") + .HasColumnType("TEXT"); + + b.Property("VolumeId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("VolumeId"); + + b.ToTable("MangaFile"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("LibraryId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OriginalName") + .HasColumnType("TEXT"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("Summary") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("LibraryId"); + + b.ToTable("Series"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("SeriesId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeriesId"); + + b.ToTable("Volume"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.Property("AppUsersId") + .HasColumnType("INTEGER"); + + b.Property("LibrariesId") + .HasColumnType("INTEGER"); + + b.HasKey("AppUsersId", "LibrariesId"); + + b.HasIndex("LibrariesId"); + + b.ToTable("AppUserLibrary"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("API.Entities.AppUserRole", b => + { + b.HasOne("API.Entities.AppRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.AppUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Folders") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.MangaFile", b => + { + b.HasOne("API.Entities.Volume", "Volume") + .WithMany("Files") + .HasForeignKey("VolumeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Volume"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Series") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.HasOne("API.Entities.Series", "Series") + .WithMany("Volumes") + .HasForeignKey("SeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("AppUserLibrary", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("AppUsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("API.Entities.Library", null) + .WithMany() + .HasForeignKey("LibrariesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("API.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("API.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("API.Entities.AppRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("API.Entities.Series", b => + { + b.Navigation("Volumes"); + }); + + modelBuilder.Entity("API.Entities.Volume", b => + { + b.Navigation("Files"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20210103201043_RemoveUserIsAdmin.cs b/API/Data/Migrations/20210103201043_RemoveUserIsAdmin.cs new file mode 100644 index 000000000..826159fbb --- /dev/null +++ b/API/Data/Migrations/20210103201043_RemoveUserIsAdmin.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace API.Data.Migrations +{ + public partial class RemoveUserIsAdmin : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsAdmin", + table: "AspNetUsers"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsAdmin", + table: "AspNetUsers", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index 5b22f751d..6e0a0fae0 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -66,9 +66,6 @@ namespace API.Data.Migrations b.Property("EmailConfirmed") .HasColumnType("INTEGER"); - b.Property("IsAdmin") - .HasColumnType("INTEGER"); - b.Property("LastActive") .HasColumnType("TEXT"); diff --git a/API/Entities/AppUser.cs b/API/Entities/AppUser.cs index f5b16890d..7b7dfe8f4 100644 --- a/API/Entities/AppUser.cs +++ b/API/Entities/AppUser.cs @@ -12,7 +12,6 @@ namespace API.Entities public DateTime Created { get; set; } = DateTime.Now; public DateTime LastActive { get; set; } public ICollection Libraries { get; set; } - [ConcurrencyCheck] public uint RowVersion { get; set; }