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 e06cba530..b72094f17 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,48 @@ 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()) + { + var createdLibrary = await _libraryRepository.GetLibraryForNameAsync(library.Name); + BackgroundJob.Enqueue(() => _directoryService.ScanLibrary(createdLibrary.Id)); + 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. @@ -85,28 +128,30 @@ namespace API.Controllers } [Authorize(Policy = "RequireAdminRole")] - [HttpGet("scan")] - public async Task ScanLibrary(int libraryId) + [HttpPost("scan")] + 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 - 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)); } + + [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/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 db81af51d..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; @@ -22,47 +21,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) @@ -84,5 +42,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.GetLibrariesDtoForUsernameAsync(user.UserName); + + return Ok(libs.Any(x => x.Id == libraryId)); + } } } \ No newline at end of file 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/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/LibraryRepository.cs b/API/Data/LibraryRepository.cs index ce986279f..9c2c74b29 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,29 @@ 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 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 .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/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/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/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 110c60774..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"); @@ -164,6 +161,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 +206,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,9 +240,18 @@ namespace API.Data.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("Number") + 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"); diff --git a/API/Data/SeriesRepository.cs b/API/Data/SeriesRepository.cs index fd109f45f..011550348 100644 --- a/API/Data/SeriesRepository.cs +++ b/API/Data/SeriesRepository.cs @@ -46,35 +46,31 @@ 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) + .OrderBy(s => s.SortName) .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) - .ProjectTo(_mapper.ConfigurationProvider).ToList(); - } - + public IEnumerable GetVolumes(int seriesId) { return _context.Volume .Where(vol => vol.SeriesId == seriesId) + .OrderBy(vol => vol.Number) .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/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..7b7dfe8f4 100644 --- a/API/Entities/AppUser.cs +++ b/API/Entities/AppUser.cs @@ -1,18 +1,17 @@ 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] public uint RowVersion { get; set; } 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..18cadee8d 100644 --- a/API/Entities/Volume.cs +++ b/API/Entities/Volume.cs @@ -1,12 +1,17 @@ -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 string Name { get; set; } + public int 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/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..5a919770c 100644 --- a/API/Interfaces/ILibraryRepository.cs +++ b/API/Interfaces/ILibraryRepository.cs @@ -10,16 +10,12 @@ 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); + + Task DeleteLibrary(int libraryId); } } \ 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/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 de1cf994b..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,9 +147,10 @@ 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.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 +167,8 @@ namespace API.Services { var vol = new Volume() { - Number = info.Volumes, + Name = info.Volumes, + Number = Int32.Parse(info.Volumes), Files = new List() { new MangaFile() @@ -188,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()); - Console.WriteLine($"Created/Updated series {s.Name}"); - libraryEntity.Series.Add(s); + _logger.LogInformation($"Created/Updated series {s.Name}"); + library.Series.Add(s); } - _libraryRepository.Update(libraryEntity); + _libraryRepository.Update(library); if (_libraryRepository.SaveAll()) {