diff --git a/API/API.csproj b/API/API.csproj index 784cff6b4..52664396f 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -8,6 +8,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/API/Constants/PolicyConstants.cs b/API/Constants/PolicyConstants.cs new file mode 100644 index 000000000..d64a2bab6 --- /dev/null +++ b/API/Constants/PolicyConstants.cs @@ -0,0 +1,8 @@ +namespace API.Constants +{ + public static class PolicyConstants + { + public static readonly string AdminRole = "Admin"; + public static readonly string PlebRole = "Pleb"; + } +} \ No newline at end of file diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index a621c1086..3610925e9 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -1,11 +1,11 @@ using System; -using System.Security.Cryptography; -using System.Text; using System.Threading.Tasks; -using API.Data; +using API.Constants; using API.DTOs; using API.Entities; using API.Interfaces; +using AutoMapper; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; @@ -14,81 +14,83 @@ namespace API.Controllers { public class AccountController : BaseApiController { - private readonly DataContext _context; + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; private readonly ITokenService _tokenService; private readonly IUserRepository _userRepository; private readonly ILogger _logger; + private readonly IMapper _mapper; - public AccountController(DataContext context, ITokenService tokenService, IUserRepository userRepository, ILogger logger) + public AccountController(UserManager userManager, + SignInManager signInManager, + ITokenService tokenService, IUserRepository userRepository, + ILogger logger, + IMapper mapper) { - _context = context; + _userManager = userManager; + _signInManager = signInManager; _tokenService = tokenService; _userRepository = userRepository; _logger = logger; + _mapper = mapper; } [HttpPost("register")] public async Task> Register(RegisterDto registerDto) { - _logger.LogInformation("Username: " + registerDto.Password); if (await UserExists(registerDto.Username)) { return BadRequest("Username is taken."); } + + var user = _mapper.Map(registerDto); + + var result = await _userManager.CreateAsync(user, registerDto.Password); + + if (!result.Succeeded) return BadRequest(result.Errors); - using var hmac = new HMACSHA512(); - var user = new AppUser - { - UserName = registerDto.Username.ToLower(), - PasswordHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(registerDto.Password)), - PasswordSalt = hmac.Key, - IsAdmin = registerDto.IsAdmin, - LastActive = DateTime.Now - }; - _context.Users.Add(user); - await _context.SaveChangesAsync(); + // TODO: Need a way to store Roles in enum and configure from there + var role = registerDto.IsAdmin ? PolicyConstants.AdminRole : PolicyConstants.PlebRole; + var roleResult = await _userManager.AddToRoleAsync(user, role); - return new UserDto() + if (!roleResult.Succeeded) return BadRequest(result.Errors); + + return new UserDto { Username = user.UserName, - Token = _tokenService.CreateToken(user), - IsAdmin = user.IsAdmin + Token = await _tokenService.CreateToken(user), }; } [HttpPost("login")] public async Task> Login(LoginDto loginDto) { - var user = await _userRepository.GetUserByUsernameAsync(loginDto.Username); + var user = await _userManager.Users + .SingleOrDefaultAsync(x => x.UserName == loginDto.Username.ToLower()); if (user == null) return Unauthorized("Invalid username"); - - using var hmac = new HMACSHA512(user.PasswordSalt); - var computedHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(loginDto.Password)); + var result = await _signInManager + .CheckPasswordSignInAsync(user, loginDto.Password, false); - for (int i = 0; i < computedHash.Length; i++) - { - if (computedHash[i] != user.PasswordHash[i]) return Unauthorized("Invalid password"); - } + if (!result.Succeeded) return Unauthorized(); // Update LastActive on account user.LastActive = DateTime.Now; _userRepository.Update(user); await _userRepository.SaveAllAsync(); - return new UserDto() + return new UserDto { Username = user.UserName, - Token = _tokenService.CreateToken(user), - IsAdmin = user.IsAdmin + Token = await _tokenService.CreateToken(user) }; } private async Task UserExists(string username) { - return await _context.Users.AnyAsync(user => user.UserName == username.ToLower()); + return await _userManager.Users.AnyAsync(user => user.UserName == username.ToLower()); } } } \ No newline at end of file diff --git a/API/Controllers/AdminController.cs b/API/Controllers/AdminController.cs new file mode 100644 index 000000000..173961a48 --- /dev/null +++ b/API/Controllers/AdminController.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using API.DTOs; +using API.Entities; +using API.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; + +namespace API.Controllers +{ + public class AdminController : BaseApiController + { + private readonly IUserRepository _userRepository; + private readonly UserManager _userManager; + + public AdminController(IUserRepository userRepository, UserManager userManager) + { + _userRepository = userRepository; + _userManager = userManager; + } + + [HttpGet("exists")] + public async Task> AdminExists() + { + var users = await _userManager.GetUsersInRoleAsync("Admin"); + return users.Count > 0; + } + + + + + } +} \ No newline at end of file diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs new file mode 100644 index 000000000..0c6e45d99 --- /dev/null +++ b/API/Controllers/LibraryController.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using API.Data; +using API.DTOs; +using API.Entities; +using API.Extensions; +using API.Interfaces; +using AutoMapper; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace API.Controllers +{ + [Authorize] + public class LibraryController : BaseApiController + { + private readonly DataContext _context; + private readonly IDirectoryService _directoryService; + private readonly ILibraryRepository _libraryRepository; + private readonly ILogger _logger; + private readonly IUserRepository _userRepository; + private readonly IMapper _mapper; + + public LibraryController(DataContext context, IDirectoryService directoryService, + ILibraryRepository libraryRepository, ILogger logger, IUserRepository userRepository, + IMapper mapper) + { + _context = context; + _directoryService = directoryService; + _libraryRepository = libraryRepository; + _logger = logger; + _userRepository = userRepository; + _mapper = mapper; + } + + /// + /// Returns a list of directories for a given path. If path is empty, returns root drives. + /// + /// + /// + [HttpGet("list")] + public ActionResult> GetDirectories(string path) + { + // TODO: We need some sort of validation other than our auth layer + _logger.Log(LogLevel.Debug, "Listing Directories for " + path); + + if (string.IsNullOrEmpty(path)) + { + return Ok(Directory.GetLogicalDrives()); + } + + if (!Directory.Exists(@path)) return BadRequest("This is not a valid path"); + + return Ok(_directoryService.ListDirectory(path)); + } + + [HttpGet] + public async Task>> GetLibraries() + { + return Ok(await _libraryRepository.GetLibrariesAsync()); + } + + + // Do I need this method? + // [HttpGet("library/{username}")] + // public async Task>> GetLibrariesForUser(string username) + // { + // _logger.LogDebug("Method hit"); + // var user = await _userRepository.GetUserByUsernameAsync(User.GetUsername()); + // + // if (user == null) return BadRequest("Could not validate user"); + // + // return Ok(await _libraryRepository.GetLibrariesForUserAsync(user)); + // } + + [Authorize(Policy = "RequireAdminRole")] + [HttpPut("update-for")] + public async Task> UpdateLibrary(UpdateLibraryDto updateLibraryDto) + { + var user = await _userRepository.GetUserByUsernameAsync(updateLibraryDto.Username); + + if (user == null) return BadRequest("Could not validate user"); + + user.Libraries = new List(); + + foreach (var selectedLibrary in updateLibraryDto.SelectedLibraries) + { + user.Libraries.Add(_mapper.Map(selectedLibrary)); + } + + if (await _userRepository.SaveAllAsync()) + { + return Ok(user); + } + + return BadRequest("Not Implemented"); + } + } +} \ No newline at end of file diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index dd27d3f15..3b546b21f 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -1,24 +1,88 @@ -using System.Collections; -using System.Collections.Generic; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using API.Data; using API.DTOs; +using API.Entities; +using API.Extensions; using API.Interfaces; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace API.Controllers { + [Authorize] public class UsersController : BaseApiController { private readonly DataContext _context; private readonly IUserRepository _userRepository; + private readonly ILibraryRepository _libraryRepository; - public UsersController(DataContext context, IUserRepository userRepository) + public UsersController(DataContext context, IUserRepository userRepository, ILibraryRepository libraryRepository) { _context = context; _userRepository = userRepository; + _libraryRepository = libraryRepository; } + + [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 + + //_logger.Log(LogLevel.Debug, "Creating a new " + createLibraryDto.Type + " library"); + 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."); + } + + // TODO: We probably need to clean the folders before we insert + var library = new Library + { + Name = createLibraryDto.Name, // TODO: Ensure code handles Library name always being lowercase + 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) + { + var user = await _userRepository.GetUserByUsernameAsync(username); + _userRepository.Delete(user); + + if (await _userRepository.SaveAllAsync()) + { + return Ok(); + } + + return BadRequest("Could not delete the user."); + } + + [Authorize(Policy = "RequireAdminRole")] [HttpGet] public async Task>> GetUsers() { diff --git a/API/Controllers/WeatherForecastController.cs b/API/Controllers/WeatherForecastController.cs deleted file mode 100644 index 28c1c42a8..000000000 --- a/API/Controllers/WeatherForecastController.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; - -namespace API.Controllers -{ - [ApiController] - [Route("[controller]")] - public class WeatherForecastController : ControllerBase - { - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; - - private readonly ILogger _logger; - - public WeatherForecastController(ILogger logger) - { - _logger = logger; - } - - [HttpGet] - public IEnumerable Get() - { - var rng = new Random(); - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateTime.Now.AddDays(index), - TemperatureC = rng.Next(-20, 55), - Summary = Summaries[rng.Next(Summaries.Length)] - }) - .ToArray(); - } - } -} diff --git a/API/DTOs/CreateLibraryDto.cs b/API/DTOs/CreateLibraryDto.cs new file mode 100644 index 000000000..3e3263d48 --- /dev/null +++ b/API/DTOs/CreateLibraryDto.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; +using API.Entities; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion.Internal; + +namespace API.DTOs +{ + public class CreateLibraryDto + { + [Required] + public string Name { get; set; } + [Required] + public LibraryType Type { get; set; } + [Required] + [MinLength(1)] + public IEnumerable Folders { get; set; } + } +} \ No newline at end of file diff --git a/API/DTOs/LibraryDto.cs b/API/DTOs/LibraryDto.cs new file mode 100644 index 000000000..31e51e173 --- /dev/null +++ b/API/DTOs/LibraryDto.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using API.Entities; + +namespace API.DTOs +{ + public class LibraryDto + { + public string Name { get; set; } + public string CoverImage { get; set; } + public LibraryType Type { get; set; } + public ICollection Folders { get; set; } + } +} \ No newline at end of file diff --git a/API/DTOs/MemberDto.cs b/API/DTOs/MemberDto.cs index a1f8b377b..6f09f1fc3 100644 --- a/API/DTOs/MemberDto.cs +++ b/API/DTOs/MemberDto.cs @@ -1,4 +1,7 @@ using System; +using System.Collections; +using System.Collections.Generic; +using API.Entities; namespace API.DTOs { @@ -11,6 +14,7 @@ namespace API.DTOs public string Username { get; set; } public DateTime Created { get; set; } public DateTime LastActive { get; set; } - public bool IsAdmin { get; set; } + public IEnumerable Libraries { get; set; } + public IEnumerable Roles { get; set; } } } \ No newline at end of file diff --git a/API/DTOs/UpdateLibraryDto.cs b/API/DTOs/UpdateLibraryDto.cs new file mode 100644 index 000000000..c664b1df6 --- /dev/null +++ b/API/DTOs/UpdateLibraryDto.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace API.DTOs +{ + // NOTE: Should this be a Record? https://www.youtube.com/watch?v=9Byvwa9yF-I + public class UpdateLibraryDto + { + public string Username { get; init; } + public IEnumerable SelectedLibraries { get; init; } + } +} \ No newline at end of file diff --git a/API/DTOs/UserDto.cs b/API/DTOs/UserDto.cs index c8b97abb6..eb0bf6bf2 100644 --- a/API/DTOs/UserDto.cs +++ b/API/DTOs/UserDto.cs @@ -2,8 +2,7 @@ { public class UserDto { - public string Username { get; set; } - public string Token { get; set; } - public bool IsAdmin { get; set; } + public string Username { get; init; } + public string Token { get; init; } } } \ No newline at end of file diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index bca4e24be..e29f5758a 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -1,15 +1,36 @@ -using API.Entities; +using System; +using API.Entities; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; namespace API.Data { - public class DataContext : DbContext + public class DataContext : IdentityDbContext, AppUserRole, IdentityUserLogin, + IdentityRoleClaim, IdentityUserToken> { public DataContext(DbContextOptions options) : base(options) { } - - public DbSet Users { get; set; } + public DbSet Library { get; set; } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + builder.Entity() + .HasMany(ur => ur.UserRoles) + .WithOne(u => u.User) + .HasForeignKey(ur => ur.UserId) + .IsRequired(); + + builder.Entity() + .HasMany(ur => ur.UserRoles) + .WithOne(u => u.Role) + .HasForeignKey(ur => ur.RoleId) + .IsRequired(); + } } } \ No newline at end of file diff --git a/API/Data/LibraryRepository.cs b/API/Data/LibraryRepository.cs new file mode 100644 index 000000000..68e6371bd --- /dev/null +++ b/API/Data/LibraryRepository.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using API.DTOs; +using API.Entities; +using API.Interfaces; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Microsoft.EntityFrameworkCore; + +namespace API.Data +{ + public class LibraryRepository : ILibraryRepository + { + private readonly DataContext _context; + private readonly IMapper _mapper; + + public LibraryRepository(DataContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public void Update(Library library) + { + _context.Entry(library).State = EntityState.Modified; + } + + public async Task SaveAllAsync() + { + return await _context.SaveChangesAsync() > 0; + } + + public async Task> GetLibrariesAsync() + { + return await _context.Library + .Include(f => f.Folders) + .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + } + + public async Task LibraryExists(string libraryName) + { + return await _context.Library.AnyAsync(x => x.Name == libraryName); + } + + public async Task> GetLibrariesForUserAsync(AppUser user) + { + return await _context.Library.Where(library => library.AppUsers.Contains(user)) + .Include(l => l.Folders) + .ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + } + } +} \ No newline at end of file diff --git a/API/Data/Migrations/20201215195007_AddedLibrary.Designer.cs b/API/Data/Migrations/20201215195007_AddedLibrary.Designer.cs new file mode 100644 index 000000000..4a657771e --- /dev/null +++ b/API/Data/Migrations/20201215195007_AddedLibrary.Designer.cs @@ -0,0 +1,128 @@ +// +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("20201215195007_AddedLibrary")] + partial class AddedLibrary + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.1"); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("IsAdmin") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("BLOB"); + + b.Property("PasswordSalt") + .HasColumnType("BLOB"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + 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("AppUserId") + .HasColumnType("INTEGER"); + + b.Property("CoverImage") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AppUserId"); + + b.ToTable("Library"); + }); + + modelBuilder.Entity("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", null) + .WithMany("Folders") + .HasForeignKey("LibraryId"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.HasOne("API.Entities.AppUser", "AppUser") + .WithMany("Libraries") + .HasForeignKey("AppUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AppUser"); + }); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Navigation("Libraries"); + }); + + modelBuilder.Entity("API.Entities.Library", b => + { + b.Navigation("Folders"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20201215195007_AddedLibrary.cs b/API/Data/Migrations/20201215195007_AddedLibrary.cs new file mode 100644 index 000000000..f1c4adf56 --- /dev/null +++ b/API/Data/Migrations/20201215195007_AddedLibrary.cs @@ -0,0 +1,71 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace API.Data.Migrations +{ + public partial class AddedLibrary : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Library", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", nullable: true), + CoverImage = table.Column(type: "TEXT", nullable: true), + Type = table.Column(type: "INTEGER", nullable: false), + AppUserId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Library", x => x.Id); + table.ForeignKey( + name: "FK_Library_Users_AppUserId", + column: x => x.AppUserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "FolderPath", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Path = table.Column(type: "TEXT", nullable: true), + LibraryId = table.Column(type: "INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_FolderPath", x => x.Id); + table.ForeignKey( + name: "FK_FolderPath_Library_LibraryId", + column: x => x.LibraryId, + principalTable: "Library", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_FolderPath_LibraryId", + table: "FolderPath", + column: "LibraryId"); + + migrationBuilder.CreateIndex( + name: "IX_Library_AppUserId", + table: "Library", + column: "AppUserId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "FolderPath"); + + migrationBuilder.DropTable( + name: "Library"); + } + } +} diff --git a/API/Data/Migrations/20201218173135_ManyToManyLibraries.Designer.cs b/API/Data/Migrations/20201218173135_ManyToManyLibraries.Designer.cs new file mode 100644 index 000000000..98af0c730 --- /dev/null +++ b/API/Data/Migrations/20201218173135_ManyToManyLibraries.Designer.cs @@ -0,0 +1,141 @@ +// +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("20201218173135_ManyToManyLibraries")] + partial class ManyToManyLibraries + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.1"); + + modelBuilder.Entity("API.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("IsAdmin") + .HasColumnType("INTEGER"); + + b.Property("LastActive") + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("BLOB"); + + b.Property("PasswordSalt") + .HasColumnType("BLOB"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + 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("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + 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("API.Entities.FolderPath", b => + { + b.HasOne("API.Entities.Library", "Library") + .WithMany("Folders") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + 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("API.Entities.Library", b => + { + b.Navigation("Folders"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20201218173135_ManyToManyLibraries.cs b/API/Data/Migrations/20201218173135_ManyToManyLibraries.cs new file mode 100644 index 000000000..e7d2cb39b --- /dev/null +++ b/API/Data/Migrations/20201218173135_ManyToManyLibraries.cs @@ -0,0 +1,119 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace API.Data.Migrations +{ + public partial class ManyToManyLibraries : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_FolderPath_Library_LibraryId", + table: "FolderPath"); + + migrationBuilder.DropForeignKey( + name: "FK_Library_Users_AppUserId", + table: "Library"); + + migrationBuilder.DropIndex( + name: "IX_Library_AppUserId", + table: "Library"); + + migrationBuilder.DropColumn( + name: "AppUserId", + table: "Library"); + + migrationBuilder.AlterColumn( + name: "LibraryId", + table: "FolderPath", + type: "INTEGER", + nullable: false, + defaultValue: 0, + oldClrType: typeof(int), + oldType: "INTEGER", + oldNullable: true); + + migrationBuilder.CreateTable( + name: "AppUserLibrary", + columns: table => new + { + AppUsersId = table.Column(type: "INTEGER", nullable: false), + LibrariesId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AppUserLibrary", x => new { x.AppUsersId, x.LibrariesId }); + table.ForeignKey( + name: "FK_AppUserLibrary_Library_LibrariesId", + column: x => x.LibrariesId, + principalTable: "Library", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AppUserLibrary_Users_AppUsersId", + column: x => x.AppUsersId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AppUserLibrary_LibrariesId", + table: "AppUserLibrary", + column: "LibrariesId"); + + migrationBuilder.AddForeignKey( + name: "FK_FolderPath_Library_LibraryId", + table: "FolderPath", + column: "LibraryId", + principalTable: "Library", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_FolderPath_Library_LibraryId", + table: "FolderPath"); + + migrationBuilder.DropTable( + name: "AppUserLibrary"); + + migrationBuilder.AddColumn( + name: "AppUserId", + table: "Library", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AlterColumn( + name: "LibraryId", + table: "FolderPath", + type: "INTEGER", + nullable: true, + oldClrType: typeof(int), + oldType: "INTEGER"); + + migrationBuilder.CreateIndex( + name: "IX_Library_AppUserId", + table: "Library", + column: "AppUserId"); + + migrationBuilder.AddForeignKey( + name: "FK_FolderPath_Library_LibraryId", + table: "FolderPath", + column: "LibraryId", + principalTable: "Library", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_Library_Users_AppUserId", + table: "Library", + column: "AppUserId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/API/Data/Migrations/20201221141047_IdentityAdded.Designer.cs b/API/Data/Migrations/20201221141047_IdentityAdded.Designer.cs new file mode 100644 index 000000000..0836f6f4a --- /dev/null +++ b/API/Data/Migrations/20201221141047_IdentityAdded.Designer.cs @@ -0,0 +1,380 @@ +// +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("20201221141047_IdentityAdded")] + partial class IdentityAdded + { + 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("BLOB"); + + b.Property("PasswordSalt") + .HasColumnType("BLOB"); + + 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("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + 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("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"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20201221141047_IdentityAdded.cs b/API/Data/Migrations/20201221141047_IdentityAdded.cs new file mode 100644 index 000000000..ee9dd15b2 --- /dev/null +++ b/API/Data/Migrations/20201221141047_IdentityAdded.cs @@ -0,0 +1,376 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace API.Data.Migrations +{ + public partial class IdentityAdded : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_AppUserLibrary_Users_AppUsersId", + table: "AppUserLibrary"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Users", + table: "Users"); + + migrationBuilder.RenameTable( + name: "Users", + newName: "AspNetUsers"); + + migrationBuilder.AddColumn( + name: "AccessFailedCount", + table: "AspNetUsers", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "ConcurrencyStamp", + table: "AspNetUsers", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "Email", + table: "AspNetUsers", + type: "TEXT", + maxLength: 256, + nullable: true); + + migrationBuilder.AddColumn( + name: "EmailConfirmed", + table: "AspNetUsers", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "LockoutEnabled", + table: "AspNetUsers", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "LockoutEnd", + table: "AspNetUsers", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "NormalizedEmail", + table: "AspNetUsers", + type: "TEXT", + maxLength: 256, + nullable: true); + + migrationBuilder.AddColumn( + name: "NormalizedUserName", + table: "AspNetUsers", + type: "TEXT", + maxLength: 256, + nullable: true); + + migrationBuilder.AddColumn( + name: "PhoneNumber", + table: "AspNetUsers", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "PhoneNumberConfirmed", + table: "AspNetUsers", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "SecurityStamp", + table: "AspNetUsers", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "TwoFactorEnabled", + table: "AspNetUsers", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddPrimaryKey( + name: "PK_AspNetUsers", + table: "AspNetUsers", + column: "Id"); + + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + UserId = table.Column(type: "INTEGER", nullable: false), + ClaimType = table.Column(type: "TEXT", nullable: true), + ClaimValue = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "TEXT", nullable: false), + ProviderKey = table.Column(type: "TEXT", nullable: false), + ProviderDisplayName = table.Column(type: "TEXT", nullable: true), + UserId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "INTEGER", nullable: false), + LoginProvider = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + Value = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + RoleId = table.Column(type: "INTEGER", nullable: false), + ClaimType = table.Column(type: "TEXT", nullable: true), + ClaimValue = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(type: "INTEGER", nullable: false), + RoleId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.AddForeignKey( + name: "FK_AppUserLibrary_AspNetUsers_AppUsersId", + table: "AppUserLibrary", + column: "AppUsersId", + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_AppUserLibrary_AspNetUsers_AppUsersId", + table: "AppUserLibrary"); + + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropPrimaryKey( + name: "PK_AspNetUsers", + table: "AspNetUsers"); + + migrationBuilder.DropIndex( + name: "EmailIndex", + table: "AspNetUsers"); + + migrationBuilder.DropIndex( + name: "UserNameIndex", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "AccessFailedCount", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "ConcurrencyStamp", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "Email", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "EmailConfirmed", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "LockoutEnabled", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "LockoutEnd", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "NormalizedEmail", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "NormalizedUserName", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "PhoneNumber", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "PhoneNumberConfirmed", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "SecurityStamp", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "TwoFactorEnabled", + table: "AspNetUsers"); + + migrationBuilder.RenameTable( + name: "AspNetUsers", + newName: "Users"); + + migrationBuilder.AddPrimaryKey( + name: "PK_Users", + table: "Users", + column: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_AppUserLibrary_Users_AppUsersId", + table: "AppUserLibrary", + column: "AppUsersId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/API/Data/Migrations/20201224155621_MiscCleanup.Designer.cs b/API/Data/Migrations/20201224155621_MiscCleanup.Designer.cs new file mode 100644 index 000000000..8ae8c597a --- /dev/null +++ b/API/Data/Migrations/20201224155621_MiscCleanup.Designer.cs @@ -0,0 +1,377 @@ +// +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("20201224155621_MiscCleanup")] + partial class MiscCleanup + { + 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("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + 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("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"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Data/Migrations/20201224155621_MiscCleanup.cs b/API/Data/Migrations/20201224155621_MiscCleanup.cs new file mode 100644 index 000000000..20e0a4dc9 --- /dev/null +++ b/API/Data/Migrations/20201224155621_MiscCleanup.cs @@ -0,0 +1,42 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace API.Data.Migrations +{ + public partial class MiscCleanup : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "PasswordSalt", + table: "AspNetUsers"); + + migrationBuilder.AlterColumn( + name: "PasswordHash", + table: "AspNetUsers", + type: "TEXT", + nullable: true, + oldClrType: typeof(byte[]), + oldType: "BLOB", + oldNullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "PasswordHash", + table: "AspNetUsers", + type: "BLOB", + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AddColumn( + name: "PasswordSalt", + table: "AspNetUsers", + type: "BLOB", + nullable: true); + } + } +} diff --git a/API/Data/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index 9ed038826..fd6f137ac 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -16,37 +16,361 @@ namespace API.Data.Migrations 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("BLOB"); b.Property("PasswordSalt") .HasColumnType("BLOB"); + 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.ToTable("Users"); + 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("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Library"); + }); + + 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("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"); }); #pragma warning restore 612, 618 } diff --git a/API/Data/Seed.cs b/API/Data/Seed.cs new file mode 100644 index 000000000..c03407e95 --- /dev/null +++ b/API/Data/Seed.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using API.Constants; +using API.Entities; +using Microsoft.AspNetCore.Identity; + +namespace API.Data +{ + public class Seed + { + public static async Task SeedRoles(RoleManager roleManager) + { + var roles = new List + { + new AppRole {Name = PolicyConstants.AdminRole}, + new AppRole {Name = PolicyConstants.PlebRole} + }; + + foreach (var role in roles) + { + await roleManager.CreateAsync(role); + } + } + } +} \ No newline at end of file diff --git a/API/Data/UserRepository.cs b/API/Data/UserRepository.cs index 2308525a4..6c63d31b9 100644 --- a/API/Data/UserRepository.cs +++ b/API/Data/UserRepository.cs @@ -6,6 +6,7 @@ using API.Entities; using API.Interfaces; using AutoMapper; using AutoMapper.QueryableExtensions; +using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; namespace API.Data @@ -14,11 +15,13 @@ namespace API.Data { private readonly DataContext _context; private readonly IMapper _mapper; + private readonly UserManager _userManager; - public UserRepository(DataContext context, IMapper mapper) + public UserRepository(DataContext context, IMapper mapper, UserManager userManager) { _context = context; _mapper = mapper; + _userManager = userManager; } public void Update(AppUser user) @@ -26,6 +29,11 @@ namespace API.Data _context.Entry(user).State = EntityState.Modified; } + public void Delete(AppUser user) + { + _context.Users.Remove(user); + } + public async Task SaveAllAsync() { return await _context.SaveChangesAsync() > 0; @@ -43,19 +51,42 @@ namespace API.Data public async Task GetUserByUsernameAsync(string username) { - return await _context.Users.SingleOrDefaultAsync(x => x.UserName == username); + return await _context.Users + .SingleOrDefaultAsync(x => x.UserName == username); } public async Task> GetMembersAsync() { - return await _context.Users.ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + return await _userManager.Users + .Include(x => x.Libraries) + .Include(r => r.UserRoles) + .ThenInclude(r => r.Role) + .OrderBy(u => u.UserName) + .Select(u => new MemberDto + { + Id = u.Id, + Username = u.UserName, + Created = u.Created, + LastActive = u.LastActive, + Roles = u.UserRoles.Select(r => r.Role.Name).ToList(), + Libraries = u.Libraries.Select(l => new LibraryDto + { + Name = l.Name, + CoverImage = l.CoverImage, + Type = l.Type, + Folders = l.Folders.Select(x => x.Path).ToList() + }).ToList() + }) + .ToListAsync(); } public async Task GetMemberAsync(string username) { return await _context.Users.Where(x => x.UserName == username) + .Include(x => x.Libraries) .ProjectTo(_mapper.ConfigurationProvider) .SingleOrDefaultAsync(); } + } } \ No newline at end of file diff --git a/API/Entities/AppRole.cs b/API/Entities/AppRole.cs new file mode 100644 index 000000000..8c0d07f96 --- /dev/null +++ b/API/Entities/AppRole.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Identity; + +namespace API.Entities +{ + public class AppRole : IdentityRole + { + public ICollection UserRoles { get; set; } + } +} \ No newline at end of file diff --git a/API/Entities/AppUser.cs b/API/Entities/AppUser.cs index a15e894c4..ea7ad0245 100644 --- a/API/Entities/AppUser.cs +++ b/API/Entities/AppUser.cs @@ -1,22 +1,22 @@ using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using API.Entities.Interfaces; +using Microsoft.AspNetCore.Identity; namespace API.Entities { - public class AppUser : IHasConcurrencyToken + public class AppUser : IdentityUser { - public int Id { get; set; } - public string UserName { get; set; } - public byte[] PasswordHash { get; set; } - public byte[] PasswordSalt { get; set; } 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; } + + public ICollection UserRoles { get; set; } public void OnSavingChanges() { diff --git a/API/Entities/AppUserRole.cs b/API/Entities/AppUserRole.cs new file mode 100644 index 000000000..b4c73f87e --- /dev/null +++ b/API/Entities/AppUserRole.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Identity; + +namespace API.Entities +{ + public class AppUserRole : IdentityUserRole + { + public AppUser User { get; set; } + public AppRole Role { get; set; } + } +} \ No newline at end of file diff --git a/API/Entities/FolderPath.cs b/API/Entities/FolderPath.cs new file mode 100644 index 000000000..d1e49180f --- /dev/null +++ b/API/Entities/FolderPath.cs @@ -0,0 +1,10 @@ +namespace API.Entities +{ + public class FolderPath + { + public int Id { get; set; } + public string Path { get; set; } + public Library Library { get; set; } + public int LibraryId { get; set; } + } +} \ No newline at end of file diff --git a/API/Entities/Library.cs b/API/Entities/Library.cs new file mode 100644 index 000000000..279498210 --- /dev/null +++ b/API/Entities/Library.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace API.Entities +{ + public class Library + { + public int Id { get; set; } + public string Name { get; set; } + public string CoverImage { get; set; } + public LibraryType Type { get; set; } + public ICollection Folders { get; set; } + public ICollection AppUsers { get; set; } + } +} \ No newline at end of file diff --git a/API/Entities/LibraryType.cs b/API/Entities/LibraryType.cs new file mode 100644 index 000000000..6136042b0 --- /dev/null +++ b/API/Entities/LibraryType.cs @@ -0,0 +1,16 @@ +using System.ComponentModel; + +namespace API.Entities +{ + public enum LibraryType + { + [Description("Manga")] + Manga = 0, + [Description("Comic")] + Comic = 1, + [Description("Book")] + Book = 2, + [Description("Raw")] + Raw = 3 + } +} \ No newline at end of file diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index 07b096c95..454c7c2f2 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -16,6 +16,8 @@ namespace API.Extensions services.AddAutoMapper(typeof(AutoMapperProfiles).Assembly); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddDbContext(options => { options.UseSqlite(config.GetConnectionString("DefaultConnection")); diff --git a/API/Extensions/IdentityServiceExtensions.cs b/API/Extensions/IdentityServiceExtensions.cs index 386efc0f2..2d2a235f5 100644 --- a/API/Extensions/IdentityServiceExtensions.cs +++ b/API/Extensions/IdentityServiceExtensions.cs @@ -1,6 +1,9 @@ -using System.Collections; -using System.Text; +using System.Text; +using API.Constants; +using API.Data; +using API.Entities; using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; @@ -11,6 +14,17 @@ namespace API.Extensions { public static IServiceCollection AddIdentityServices(this IServiceCollection services, IConfiguration config) { + services.AddIdentityCore(opt => + { + // Change password / signin requirements here + opt.Password.RequireNonAlphanumeric = false; + }) + .AddRoles() + .AddRoleManager>() + .AddSignInManager>() + .AddRoleValidator>() + .AddEntityFrameworkStores(); + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { @@ -22,6 +36,11 @@ namespace API.Extensions ValidateAudience = false }; }); + services.AddAuthorization(opt => + { + opt.AddPolicy("RequireAdminRole", policy => policy.RequireRole(PolicyConstants.AdminRole)); + }); + return services; } } diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs index 49da92f21..4e203ab04 100644 --- a/API/Helpers/AutoMapperProfiles.cs +++ b/API/Helpers/AutoMapperProfiles.cs @@ -1,4 +1,6 @@ -using API.DTOs; +using System; +using System.Linq; +using API.DTOs; using API.Entities; using AutoMapper; @@ -8,7 +10,17 @@ namespace API.Helpers { public AutoMapperProfiles() { - CreateMap(); + CreateMap(); + + CreateMap() + .ForMember(dest => dest.Folders, + opt => + opt.MapFrom(src => src.Folders.Select(x => x.Path).ToList())); + + CreateMap() + .AfterMap((ps, pst, context) => context.Mapper.Map(ps.Libraries, pst.Libraries)); + + CreateMap(); } } } \ No newline at end of file diff --git a/API/Interfaces/IDirectoryService.cs b/API/Interfaces/IDirectoryService.cs new file mode 100644 index 000000000..87a2b9f98 --- /dev/null +++ b/API/Interfaces/IDirectoryService.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace API.Interfaces +{ + public interface IDirectoryService + { + IEnumerable ListDirectory(string rootPath); + } +} \ No newline at end of file diff --git a/API/Interfaces/ILibraryRepository.cs b/API/Interfaces/ILibraryRepository.cs new file mode 100644 index 000000000..3f929efda --- /dev/null +++ b/API/Interfaces/ILibraryRepository.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using API.DTOs; +using API.Entities; + +namespace API.Interfaces +{ + public interface ILibraryRepository + { + 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> GetLibrariesForUserAsync(AppUser user); + } +} \ No newline at end of file diff --git a/API/Interfaces/ITokenService.cs b/API/Interfaces/ITokenService.cs index e721d9ade..042426964 100644 --- a/API/Interfaces/ITokenService.cs +++ b/API/Interfaces/ITokenService.cs @@ -1,9 +1,10 @@ -using API.Entities; +using System.Threading.Tasks; +using API.Entities; namespace API.Interfaces { public interface ITokenService { - string CreateToken(AppUser user); + Task CreateToken(AppUser user); } } \ No newline at end of file diff --git a/API/Interfaces/IUserRepository.cs b/API/Interfaces/IUserRepository.cs index 69b872821..601913c89 100644 --- a/API/Interfaces/IUserRepository.cs +++ b/API/Interfaces/IUserRepository.cs @@ -15,5 +15,6 @@ namespace API.Interfaces Task GetUserByUsernameAsync(string username); Task> GetMembersAsync(); Task GetMemberAsync(string username); + public void Delete(AppUser user); } } \ No newline at end of file diff --git a/API/Program.cs b/API/Program.cs index f3a23a6c1..7ed8e7175 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -1,7 +1,9 @@ using System; using System.Threading.Tasks; using API.Data; +using API.Entities; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -21,8 +23,10 @@ namespace API try { var context = services.GetRequiredService(); + var roleManager = services.GetRequiredService>(); // Apply all migrations on startup await context.Database.MigrateAsync(); + await Seed.SeedRoles(roleManager); } catch (Exception ex) { diff --git a/API/Services/DirectoryService.cs b/API/Services/DirectoryService.cs new file mode 100644 index 000000000..1b333b74c --- /dev/null +++ b/API/Services/DirectoryService.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using API.Interfaces; + +namespace API.Services +{ + public class DirectoryService : IDirectoryService + { + public IEnumerable ListDirectory(string rootPath) + { + // TODO: Filter out Hidden and System folders + // DirectoryInfo di = new DirectoryInfo(@path); + // var dirs = di.GetDirectories() + // .Where(dir => (dir.Attributes & FileAttributes.Hidden & FileAttributes.System) == 0).ToImmutableList(); + // + + return Directory.GetDirectories(@rootPath); + } + } +} \ No newline at end of file diff --git a/API/Services/TokenService.cs b/API/Services/TokenService.cs index 195af02d2..45673dae8 100644 --- a/API/Services/TokenService.cs +++ b/API/Services/TokenService.cs @@ -1,10 +1,13 @@ using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; +using System.Linq; using System.Security.Claims; using System.Text; +using System.Threading.Tasks; using API.Entities; using API.Interfaces; +using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; using JwtRegisteredClaimNames = Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames; @@ -14,19 +17,25 @@ namespace API.Services { public class TokenService : ITokenService { + private readonly UserManager _userManager; private readonly SymmetricSecurityKey _key; - public TokenService(IConfiguration config) + public TokenService(IConfiguration config, UserManager userManager) { + _userManager = userManager; _key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["TokenKey"])); } - public string CreateToken(AppUser user) + public async Task CreateToken(AppUser user) { var claims = new List { new Claim(JwtRegisteredClaimNames.NameId, user.UserName) }; + + var roles = await _userManager.GetRolesAsync(user); + + claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role))); var creds = new SigningCredentials(_key, SecurityAlgorithms.HmacSha512Signature); diff --git a/API/WeatherForecast.cs b/API/WeatherForecast.cs deleted file mode 100644 index 6f8943b65..000000000 --- a/API/WeatherForecast.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace API -{ - public class WeatherForecast - { - public DateTime Date { get; set; } - - public int TemperatureC { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - - public string Summary { get; set; } - } -}