From d5eed4e85d789c7f0f8bf1858be022fa332875ff Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Thu, 17 Dec 2020 11:27:19 -0600 Subject: [PATCH 01/11] Lots of changes to get code ready to add library. --- API/Controllers/LibraryController.cs | 65 +++++++++ API/Controllers/UsersController.cs | 44 +++++- API/DTOs/CreateLibraryDto.cs | 19 +++ API/DTOs/LibraryDto.cs | 13 ++ API/DTOs/MemberDto.cs | 4 + API/Data/DataContext.cs | 1 + API/Data/LibraryRepository.cs | 45 ++++++ .../20201215195007_AddedLibrary.Designer.cs | 128 ++++++++++++++++++ .../Migrations/20201215195007_AddedLibrary.cs | 71 ++++++++++ .../Migrations/DataContextModelSnapshot.cs | 72 ++++++++++ API/Data/UserRepository.cs | 6 +- API/Entities/AppUser.cs | 3 + API/Entities/FolderPath.cs | 10 ++ API/Entities/Library.cs | 16 +++ API/Entities/LibraryType.cs | 16 +++ .../ApplicationServiceExtensions.cs | 2 + API/Helpers/AutoMapperProfiles.cs | 7 +- API/Interfaces/IDirectoryService.cs | 10 ++ API/Interfaces/ILibraryRepository.cs | 20 +++ API/Services/DirectoryService.cs | 21 +++ 20 files changed, 570 insertions(+), 3 deletions(-) create mode 100644 API/Controllers/LibraryController.cs create mode 100644 API/DTOs/CreateLibraryDto.cs create mode 100644 API/DTOs/LibraryDto.cs create mode 100644 API/Data/LibraryRepository.cs create mode 100644 API/Data/Migrations/20201215195007_AddedLibrary.Designer.cs create mode 100644 API/Data/Migrations/20201215195007_AddedLibrary.cs create mode 100644 API/Entities/FolderPath.cs create mode 100644 API/Entities/Library.cs create mode 100644 API/Entities/LibraryType.cs create mode 100644 API/Interfaces/IDirectoryService.cs create mode 100644 API/Interfaces/ILibraryRepository.cs create mode 100644 API/Services/DirectoryService.cs diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs new file mode 100644 index 000000000..45d51894f --- /dev/null +++ b/API/Controllers/LibraryController.cs @@ -0,0 +1,65 @@ +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.Mvc; +using Microsoft.Extensions.Logging; + +namespace API.Controllers +{ + public class LibraryController : BaseApiController + { + private readonly DataContext _context; + private readonly IDirectoryService _directoryService; + private readonly ILibraryRepository _libraryRepository; + private readonly ILogger _logger; + private readonly IUserRepository _userRepository; + + public LibraryController(DataContext context, IDirectoryService directoryService, + ILibraryRepository libraryRepository, ILogger logger, IUserRepository userRepository) + { + _context = context; + _directoryService = directoryService; + _libraryRepository = libraryRepository; + _logger = logger; + _userRepository = userRepository; + } + + /// + /// 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()); + } + + + + } +} \ No newline at end of file diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index dd27d3f15..71f1677d7 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -1,10 +1,14 @@ using System.Collections; 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.Mvc; +using Microsoft.Extensions.Logging; namespace API.Controllers { @@ -12,11 +16,13 @@ namespace API.Controllers { 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; } [HttpGet] @@ -24,5 +30,41 @@ namespace API.Controllers { return Ok(await _userRepository.GetMembersAsync()); } + + [HttpPost("add-library")] + public async Task AddLibrary(CreateLibraryDto createLibraryDto) + { + //_logger.Log(LogLevel.Debug, "Creating a new " + createLibraryDto.Type + " library"); + var user = await _userRepository.GetUserByUsernameAsync(User.GetUsername()); + + + 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, + Type = createLibraryDto.Type, + //Folders = createLibraryDto.Folders + }; + + + user.Libraries.Add(library); + _userRepository.Update(user); + + if (await _userRepository.SaveAllAsync()) + { + return Ok(); + } + + + + + + return BadRequest("Not implemented"); + } } } \ No newline at end of file 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..7c42553a0 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 { @@ -12,5 +15,6 @@ namespace API.DTOs public DateTime Created { get; set; } public DateTime LastActive { get; set; } public bool IsAdmin { get; set; } + //public IEnumerable Libraries { get; set; } } } \ No newline at end of file diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index bca4e24be..f5ed3afbe 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -11,5 +11,6 @@ namespace API.Data } public DbSet Users { get; set; } + public DbSet Library { get; set; } } } \ No newline at end of file diff --git a/API/Data/LibraryRepository.cs b/API/Data/LibraryRepository.cs new file mode 100644 index 000000000..d63b588e9 --- /dev/null +++ b/API/Data/LibraryRepository.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +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); + } + } +} \ 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/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index 9ed038826..b341bb481 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -48,6 +48,78 @@ namespace API.Data.Migrations 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/UserRepository.cs b/API/Data/UserRepository.cs index 2308525a4..bb69ccaa8 100644 --- a/API/Data/UserRepository.cs +++ b/API/Data/UserRepository.cs @@ -48,12 +48,16 @@ namespace API.Data public async Task> GetMembersAsync() { - return await _context.Users.ProjectTo(_mapper.ConfigurationProvider).ToListAsync(); + return await _context.Users.Include(x => x.Libraries) + .Include(x => x.Libraries) + .ProjectTo(_mapper.ConfigurationProvider) + .ToListAsync(); } public async Task GetMemberAsync(string username) { return await _context.Users.Where(x => x.UserName == username) + .Include(x => x.Libraries) .ProjectTo(_mapper.ConfigurationProvider) .SingleOrDefaultAsync(); } diff --git a/API/Entities/AppUser.cs b/API/Entities/AppUser.cs index a15e894c4..a67272ccd 100644 --- a/API/Entities/AppUser.cs +++ b/API/Entities/AppUser.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using API.Entities.Interfaces; @@ -14,6 +16,7 @@ namespace API.Entities 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 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..06291ede0 --- /dev/null +++ b/API/Entities/Library.cs @@ -0,0 +1,16 @@ +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 AppUser AppUser { get; set; } + public int AppUserId { 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/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs index 49da92f21..9e98f2ee0 100644 --- a/API/Helpers/AutoMapperProfiles.cs +++ b/API/Helpers/AutoMapperProfiles.cs @@ -1,4 +1,5 @@ -using API.DTOs; +using System.Linq; +using API.DTOs; using API.Entities; using AutoMapper; @@ -9,6 +10,10 @@ namespace API.Helpers public AutoMapperProfiles() { CreateMap(); + CreateMap() + .ForMember(dest => dest.Folders, + opt => + opt.MapFrom(src => src.Folders.Select(x => x.Path).ToList())); } } } \ 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..625fba35e --- /dev/null +++ b/API/Interfaces/ILibraryRepository.cs @@ -0,0 +1,20 @@ +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); + } +} \ No newline at end of file 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 From b6e0e0520501e308a31bb589ae6fa3f3c315484a Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Fri, 18 Dec 2020 12:01:45 -0600 Subject: [PATCH 02/11] Added new many to many migration for libraries and users. Add Library now works and you can get Library (entity) objects from a user. LibraryDto project is upcoming. --- API/Controllers/LibraryController.cs | 2 + API/Controllers/UsersController.cs | 16 +- API/DTOs/MemberDto.cs | 2 +- API/Data/DataContext.cs | 3 +- ...1218173135_ManyToManyLibraries.Designer.cs | 141 ++++++++++++++++++ .../20201218173135_ManyToManyLibraries.cs | 119 +++++++++++++++ .../Migrations/DataContextModelSnapshot.cs | 49 +++--- API/Data/UserRepository.cs | 3 +- API/Entities/Library.cs | 4 +- 9 files changed, 308 insertions(+), 31 deletions(-) create mode 100644 API/Data/Migrations/20201218173135_ManyToManyLibraries.Designer.cs create mode 100644 API/Data/Migrations/20201218173135_ManyToManyLibraries.cs diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index 45d51894f..5ceaf1690 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -9,11 +9,13 @@ 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; diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index 71f1677d7..b0ac10ff2 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -7,11 +7,13 @@ using API.DTOs; using API.Entities; using API.Extensions; using API.Interfaces; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace API.Controllers { + [Authorize] public class UsersController : BaseApiController { private readonly DataContext _context; @@ -36,6 +38,8 @@ namespace API.Controllers { //_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)) @@ -44,26 +48,24 @@ namespace API.Controllers } // TODO: We probably need to clean the folders before we insert - var library = new Library() + var library = new Library { Name = createLibraryDto.Name, Type = createLibraryDto.Type, //Folders = createLibraryDto.Folders + AppUsers = new List() { user } }; - + user.Libraries ??= new List(); // If user is null, then set it + user.Libraries.Add(library); - _userRepository.Update(user); + //_userRepository.Update(user); if (await _userRepository.SaveAllAsync()) { return Ok(); } - - - - return BadRequest("Not implemented"); } } diff --git a/API/DTOs/MemberDto.cs b/API/DTOs/MemberDto.cs index 7c42553a0..26e299a7d 100644 --- a/API/DTOs/MemberDto.cs +++ b/API/DTOs/MemberDto.cs @@ -15,6 +15,6 @@ namespace API.DTOs public DateTime Created { get; set; } public DateTime LastActive { get; set; } public bool IsAdmin { get; set; } - //public IEnumerable Libraries { get; set; } + public IEnumerable Libraries { get; set; } } } \ No newline at end of file diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index f5ed3afbe..92ce19955 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -1,4 +1,5 @@ -using API.Entities; +using System; +using API.Entities; using Microsoft.EntityFrameworkCore; namespace API.Data 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/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index b341bb481..c19ace39f 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -55,7 +55,7 @@ namespace API.Data.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("LibraryId") + b.Property("LibraryId") .HasColumnType("INTEGER"); b.Property("Path") @@ -74,9 +74,6 @@ namespace API.Data.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("AppUserId") - .HasColumnType("INTEGER"); - b.Property("CoverImage") .HasColumnType("TEXT"); @@ -88,32 +85,48 @@ namespace API.Data.Migrations b.HasKey("Id"); - b.HasIndex("AppUserId"); - 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", null) + b.HasOne("API.Entities.Library", "Library") .WithMany("Folders") - .HasForeignKey("LibraryId"); - }); - - modelBuilder.Entity("API.Entities.Library", b => - { - b.HasOne("API.Entities.AppUser", "AppUser") - .WithMany("Libraries") - .HasForeignKey("AppUserId") + .HasForeignKey("LibraryId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("AppUser"); + b.Navigation("Library"); }); - modelBuilder.Entity("API.Entities.AppUser", b => + modelBuilder.Entity("AppUserLibrary", b => { - b.Navigation("Libraries"); + 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 => diff --git a/API/Data/UserRepository.cs b/API/Data/UserRepository.cs index bb69ccaa8..1a8692c97 100644 --- a/API/Data/UserRepository.cs +++ b/API/Data/UserRepository.cs @@ -43,7 +43,8 @@ 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() diff --git a/API/Entities/Library.cs b/API/Entities/Library.cs index 06291ede0..279498210 100644 --- a/API/Entities/Library.cs +++ b/API/Entities/Library.cs @@ -9,8 +9,6 @@ namespace API.Entities public string CoverImage { get; set; } public LibraryType Type { get; set; } public ICollection Folders { get; set; } - - public AppUser AppUser { get; set; } - public int AppUserId { get; set; } + public ICollection AppUsers { get; set; } } } \ No newline at end of file From 8156aeb495221db4877711f53f48657131cba6d6 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Sun, 20 Dec 2020 17:47:18 -0600 Subject: [PATCH 03/11] Some api work --- API/Controllers/LibraryController.cs | 42 +++++++++++++++++++- API/Controllers/UsersController.cs | 18 ++++++--- API/Controllers/WeatherForecastController.cs | 39 ------------------ API/DTOs/MemberDto.cs | 2 +- API/DTOs/UpdateLibraryDto.cs | 11 +++++ API/Data/LibraryRepository.cs | 8 ++++ API/Helpers/AutoMapperProfiles.cs | 9 ++++- API/Interfaces/ILibraryRepository.cs | 2 + API/WeatherForecast.cs | 15 ------- 9 files changed, 82 insertions(+), 64 deletions(-) delete mode 100644 API/Controllers/WeatherForecastController.cs create mode 100644 API/DTOs/UpdateLibraryDto.cs delete mode 100644 API/WeatherForecast.cs diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index 5ceaf1690..2ff43ec52 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -23,15 +23,18 @@ namespace API.Controllers 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) + ILibraryRepository libraryRepository, ILogger logger, IUserRepository userRepository, + IMapper mapper) { _context = context; _directoryService = directoryService; _libraryRepository = libraryRepository; _logger = logger; _userRepository = userRepository; + _mapper = mapper; } /// @@ -60,6 +63,43 @@ namespace API.Controllers { 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)); + // } + + [HttpPut("update-for")] + public async Task> UpdateLibrary(UpdateLibraryDto updateLibraryDto) + { + // TODO: Only admins can do this + var user = await _userRepository.GetUserByUsernameAsync(updateLibraryDto.Username); + + if (user == null) return BadRequest("Could not validate user"); + if (!user.IsAdmin) return Unauthorized("Only admins are permitted"); + + 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"); + } diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index b0ac10ff2..67039cce2 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -1,5 +1,4 @@ -using System.Collections; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using API.Data; @@ -9,7 +8,6 @@ using API.Extensions; using API.Interfaces; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; namespace API.Controllers { @@ -36,6 +34,8 @@ namespace API.Controllers [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()); @@ -50,16 +50,20 @@ namespace API.Controllers // TODO: We probably need to clean the folders before we insert var library = new Library { - Name = createLibraryDto.Name, + Name = createLibraryDto.Name, // TODO: Ensure code handles Library name always being lowercase Type = createLibraryDto.Type, - //Folders = createLibraryDto.Folders 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); - //_userRepository.Update(user); if (await _userRepository.SaveAllAsync()) { @@ -68,5 +72,7 @@ namespace API.Controllers return BadRequest("Not implemented"); } + + } } \ No newline at end of file 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/MemberDto.cs b/API/DTOs/MemberDto.cs index 26e299a7d..38ecbfcc3 100644 --- a/API/DTOs/MemberDto.cs +++ b/API/DTOs/MemberDto.cs @@ -15,6 +15,6 @@ namespace API.DTOs public DateTime Created { get; set; } public DateTime LastActive { get; set; } public bool IsAdmin { get; set; } - public IEnumerable Libraries { get; set; } + public IEnumerable Libraries { 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/Data/LibraryRepository.cs b/API/Data/LibraryRepository.cs index d63b588e9..68e6371bd 100644 --- a/API/Data/LibraryRepository.cs +++ b/API/Data/LibraryRepository.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using API.DTOs; using API.Entities; @@ -41,5 +42,12 @@ namespace API.Data { 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/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs index 9e98f2ee0..4e2f2a100 100644 --- a/API/Helpers/AutoMapperProfiles.cs +++ b/API/Helpers/AutoMapperProfiles.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using API.DTOs; using API.Entities; using AutoMapper; @@ -9,11 +10,15 @@ 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)); } } } \ No newline at end of file diff --git a/API/Interfaces/ILibraryRepository.cs b/API/Interfaces/ILibraryRepository.cs index 625fba35e..3f929efda 100644 --- a/API/Interfaces/ILibraryRepository.cs +++ b/API/Interfaces/ILibraryRepository.cs @@ -16,5 +16,7 @@ namespace API.Interfaces /// /// Task LibraryExists(string libraryName); + + Task> GetLibrariesForUserAsync(AppUser user); } } \ No newline at end of file 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; } - } -} From f8d7581a1287df4faf4dcd4928a471a87493968d Mon Sep 17 00:00:00 2001 From: Andrew Song Date: Sun, 20 Dec 2020 18:32:24 -0600 Subject: [PATCH 04/11] adding admin exists api --- API/Controllers/AdminController.cs | 24 ++++++++++++++++++++++++ API/Data/UserRepository.cs | 6 ++++++ API/Interfaces/IUserRepository.cs | 1 + 3 files changed, 31 insertions(+) create mode 100644 API/Controllers/AdminController.cs diff --git a/API/Controllers/AdminController.cs b/API/Controllers/AdminController.cs new file mode 100644 index 000000000..155a53bfa --- /dev/null +++ b/API/Controllers/AdminController.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; +using API.Interfaces; +using Microsoft.AspNetCore.Mvc; + +namespace API.Controllers +{ + public class AdminController : BaseApiController + { + private readonly IUserRepository _userRepository; + + public AdminController(IUserRepository userRepository) + { + _userRepository = userRepository; + } + + [HttpGet] + public async Task> AdminExists() + { + return await _userRepository.AdminExists(); + } + + + } +} \ No newline at end of file diff --git a/API/Data/UserRepository.cs b/API/Data/UserRepository.cs index 1a8692c97..3ecf5578e 100644 --- a/API/Data/UserRepository.cs +++ b/API/Data/UserRepository.cs @@ -62,5 +62,11 @@ namespace API.Data .ProjectTo(_mapper.ConfigurationProvider) .SingleOrDefaultAsync(); } + + public async Task AdminExists() + { + return await _context.Users.AnyAsync(x => x.IsAdmin); + + } } } \ No newline at end of file diff --git a/API/Interfaces/IUserRepository.cs b/API/Interfaces/IUserRepository.cs index 69b872821..4ed10236f 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); + Task AdminExists(); } } \ No newline at end of file From 8f7df85d496be3f6166c8286f9b8ebae64152c07 Mon Sep 17 00:00:00 2001 From: Andrew Song Date: Mon, 21 Dec 2020 09:24:21 -0600 Subject: [PATCH 05/11] Refractor token auth stuff to use identiycore framework --- API/API.csproj | 1 + API/Controllers/AccountController.cs | 61 ++--- API/Controllers/LibraryController.cs | 3 - API/Data/DataContext.cs | 25 +- .../Migrations/DataContextModelSnapshot.cs | 241 +++++++++++++++++- API/Data/Seed.cs | 24 ++ API/Entities/AppRole.cs | 10 + API/Entities/AppUser.cs | 11 +- API/Entities/AppUserRole.cs | 10 + API/Extensions/IdentityServiceExtensions.cs | 17 +- API/Helpers/AutoMapperProfiles.cs | 2 + API/Interfaces/ITokenService.cs | 5 +- API/Program.cs | 4 + API/Services/TokenService.cs | 13 +- 14 files changed, 377 insertions(+), 50 deletions(-) create mode 100644 API/Data/Seed.cs create mode 100644 API/Entities/AppRole.cs create mode 100644 API/Entities/AppUserRole.cs 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/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index a621c1086..a007bfc6c 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -1,11 +1,10 @@ using System; -using System.Security.Cryptography; -using System.Text; using System.Threading.Tasks; -using API.Data; 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,17 +13,25 @@ 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")] @@ -35,24 +42,21 @@ namespace API.Controllers { 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); + + var roleResult = await _userManager.AddToRoleAsync(user, "Pleb"); + + if (!roleResult.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(); - return new UserDto() { Username = user.UserName, - Token = _tokenService.CreateToken(user), + Token = await _tokenService.CreateToken(user), IsAdmin = user.IsAdmin }; } @@ -60,18 +64,15 @@ namespace API.Controllers [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; @@ -81,14 +82,14 @@ namespace API.Controllers return new UserDto() { Username = user.UserName, - Token = _tokenService.CreateToken(user), + Token = await _tokenService.CreateToken(user), IsAdmin = user.IsAdmin }; } 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/LibraryController.cs b/API/Controllers/LibraryController.cs index 2ff43ec52..93e3cdf06 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -100,8 +100,5 @@ namespace API.Controllers return BadRequest("Not Implemented"); } - - - } } \ No newline at end of file diff --git a/API/Data/DataContext.cs b/API/Data/DataContext.cs index 92ce19955..e29f5758a 100644 --- a/API/Data/DataContext.cs +++ b/API/Data/DataContext.cs @@ -1,17 +1,36 @@ 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/Migrations/DataContextModelSnapshot.cs b/API/Data/Migrations/DataContextModelSnapshot.cs index c19ace39f..fd6f137ac 100644 --- a/API/Data/Migrations/DataContextModelSnapshot.cs +++ b/API/Data/Migrations/DataContextModelSnapshot.cs @@ -16,37 +16,127 @@ 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 => @@ -103,6 +193,109 @@ namespace API.Data.Migrations 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") @@ -129,6 +322,52 @@ namespace API.Data.Migrations .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"); diff --git a/API/Data/Seed.cs b/API/Data/Seed.cs new file mode 100644 index 000000000..4bc4ebbc6 --- /dev/null +++ b/API/Data/Seed.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +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 = "Admin"}, + new AppRole {Name = "Pleb"} + }; + + foreach (var role in roles) + { + await roleManager.CreateAsync(role); + } + } + } +} \ 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 a67272ccd..ea7ad0245 100644 --- a/API/Entities/AppUser.cs +++ b/API/Entities/AppUser.cs @@ -1,18 +1,13 @@ using System; -using System.Collections; 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; } @@ -20,6 +15,8 @@ namespace API.Entities [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/Extensions/IdentityServiceExtensions.cs b/API/Extensions/IdentityServiceExtensions.cs index 386efc0f2..9669c6822 100644 --- a/API/Extensions/IdentityServiceExtensions.cs +++ b/API/Extensions/IdentityServiceExtensions.cs @@ -1,6 +1,8 @@ -using System.Collections; -using System.Text; +using System.Text; +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 +13,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 => { diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs index 4e2f2a100..4e203ab04 100644 --- a/API/Helpers/AutoMapperProfiles.cs +++ b/API/Helpers/AutoMapperProfiles.cs @@ -19,6 +19,8 @@ namespace API.Helpers CreateMap() .AfterMap((ps, pst, context) => context.Mapper.Map(ps.Libraries, pst.Libraries)); + + CreateMap(); } } } \ 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/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/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); From 259b954f2669d651f3c1db09f01c82da8b01a569 Mon Sep 17 00:00:00 2001 From: Andrew Song Date: Mon, 21 Dec 2020 11:18:51 -0600 Subject: [PATCH 06/11] Forgot to add migrations --- .../20201221141047_IdentityAdded.Designer.cs | 380 ++++++++++++++++++ .../20201221141047_IdentityAdded.cs | 376 +++++++++++++++++ 2 files changed, 756 insertions(+) create mode 100644 API/Data/Migrations/20201221141047_IdentityAdded.Designer.cs create mode 100644 API/Data/Migrations/20201221141047_IdentityAdded.cs 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); + } + } +} From bb276a5984c32d197e4699e6d9d2b345d03a955d Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Tue, 22 Dec 2020 17:28:38 -0600 Subject: [PATCH 07/11] Some changes to get register/login flow working smoothly with Admin role. --- API/Controllers/AccountController.cs | 14 +++++++------- API/Controllers/AdminController.cs | 19 ++++++++++++++++--- API/DTOs/MemberDto.cs | 1 - API/DTOs/UserDto.cs | 5 ++--- API/Data/UserRepository.cs | 7 +------ API/Interfaces/IUserRepository.cs | 1 - 6 files changed, 26 insertions(+), 21 deletions(-) diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index a007bfc6c..6c8c74a5b 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -37,7 +37,6 @@ namespace API.Controllers [HttpPost("register")] public async Task> Register(RegisterDto registerDto) { - _logger.LogInformation("Username: " + registerDto.Password); if (await UserExists(registerDto.Username)) { return BadRequest("Username is taken."); @@ -48,16 +47,18 @@ namespace API.Controllers var result = await _userManager.CreateAsync(user, registerDto.Password); if (!result.Succeeded) return BadRequest(result.Errors); + - var roleResult = await _userManager.AddToRoleAsync(user, "Pleb"); + // TODO: Need a way to store Roles in enum and configure from there + var role = registerDto.IsAdmin ? "Admin" : "Pleb"; + var roleResult = await _userManager.AddToRoleAsync(user, role); if (!roleResult.Succeeded) return BadRequest(result.Errors); - return new UserDto() + return new UserDto { Username = user.UserName, Token = await _tokenService.CreateToken(user), - IsAdmin = user.IsAdmin }; } @@ -79,11 +80,10 @@ namespace API.Controllers _userRepository.Update(user); await _userRepository.SaveAllAsync(); - return new UserDto() + return new UserDto { Username = user.UserName, - Token = await _tokenService.CreateToken(user), - IsAdmin = user.IsAdmin + Token = await _tokenService.CreateToken(user) }; } diff --git a/API/Controllers/AdminController.cs b/API/Controllers/AdminController.cs index 155a53bfa..17f3aa12c 100644 --- a/API/Controllers/AdminController.cs +++ b/API/Controllers/AdminController.cs @@ -1,5 +1,8 @@ using System.Threading.Tasks; +using API.Entities; using API.Interfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; namespace API.Controllers @@ -7,16 +10,26 @@ namespace API.Controllers public class AdminController : BaseApiController { private readonly IUserRepository _userRepository; + private readonly UserManager _userManager; - public AdminController(IUserRepository userRepository) + public AdminController(IUserRepository userRepository, UserManager userManager) { _userRepository = userRepository; + _userManager = userManager; } - [HttpGet] + [HttpGet("exists")] public async Task> AdminExists() { - return await _userRepository.AdminExists(); + var users = await _userManager.GetUsersInRoleAsync("Admin"); + return users.Count > 0; + } + + [Authorize(Policy = "RequireAdminRole")] + [HttpDelete] + public async Task DeleteUser(string username) + { + return BadRequest("Not Implemented"); } diff --git a/API/DTOs/MemberDto.cs b/API/DTOs/MemberDto.cs index 38ecbfcc3..7d881d53e 100644 --- a/API/DTOs/MemberDto.cs +++ b/API/DTOs/MemberDto.cs @@ -14,7 +14,6 @@ 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; } } } \ 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/UserRepository.cs b/API/Data/UserRepository.cs index 3ecf5578e..681cdd85c 100644 --- a/API/Data/UserRepository.cs +++ b/API/Data/UserRepository.cs @@ -62,11 +62,6 @@ namespace API.Data .ProjectTo(_mapper.ConfigurationProvider) .SingleOrDefaultAsync(); } - - public async Task AdminExists() - { - return await _context.Users.AnyAsync(x => x.IsAdmin); - - } + } } \ No newline at end of file diff --git a/API/Interfaces/IUserRepository.cs b/API/Interfaces/IUserRepository.cs index 4ed10236f..69b872821 100644 --- a/API/Interfaces/IUserRepository.cs +++ b/API/Interfaces/IUserRepository.cs @@ -15,6 +15,5 @@ namespace API.Interfaces Task GetUserByUsernameAsync(string username); Task> GetMembersAsync(); Task GetMemberAsync(string username); - Task AdminExists(); } } \ No newline at end of file From f0919042b06abd6a267ece2771f4331d7ee6857c Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Thu, 24 Dec 2020 08:13:58 -0600 Subject: [PATCH 08/11] Added a new policy to require being an admin. Implemented ability to delete a user. --- API/Controllers/AdminController.cs | 12 +++++++-- API/Controllers/LibraryController.cs | 3 +-- API/DTOs/MemberDto.cs | 1 + API/Data/UserRepository.cs | 27 ++++++++++++++++++++- API/Extensions/IdentityServiceExtensions.cs | 5 ++++ API/Interfaces/IUserRepository.cs | 1 + 6 files changed, 44 insertions(+), 5 deletions(-) diff --git a/API/Controllers/AdminController.cs b/API/Controllers/AdminController.cs index 17f3aa12c..6d427de89 100644 --- a/API/Controllers/AdminController.cs +++ b/API/Controllers/AdminController.cs @@ -26,10 +26,18 @@ namespace API.Controllers } [Authorize(Policy = "RequireAdminRole")] - [HttpDelete] + [HttpDelete("delete-user")] public async Task DeleteUser(string username) { - return BadRequest("Not Implemented"); + var user = await _userRepository.GetUserByUsernameAsync(username); + _userRepository.Delete(user); + + if (await _userRepository.SaveAllAsync()) + { + return Ok(); + } + + return BadRequest("Could not delete the user."); } diff --git a/API/Controllers/LibraryController.cs b/API/Controllers/LibraryController.cs index 93e3cdf06..0c6e45d99 100644 --- a/API/Controllers/LibraryController.cs +++ b/API/Controllers/LibraryController.cs @@ -77,14 +77,13 @@ namespace API.Controllers // return Ok(await _libraryRepository.GetLibrariesForUserAsync(user)); // } + [Authorize(Policy = "RequireAdminRole")] [HttpPut("update-for")] public async Task> UpdateLibrary(UpdateLibraryDto updateLibraryDto) { - // TODO: Only admins can do this var user = await _userRepository.GetUserByUsernameAsync(updateLibraryDto.Username); if (user == null) return BadRequest("Could not validate user"); - if (!user.IsAdmin) return Unauthorized("Only admins are permitted"); user.Libraries = new List(); diff --git a/API/DTOs/MemberDto.cs b/API/DTOs/MemberDto.cs index 7d881d53e..6f09f1fc3 100644 --- a/API/DTOs/MemberDto.cs +++ b/API/DTOs/MemberDto.cs @@ -15,5 +15,6 @@ namespace API.DTOs public DateTime Created { get; set; } public DateTime LastActive { get; set; } public IEnumerable Libraries { get; set; } + public IEnumerable Roles { get; set; } } } \ No newline at end of file diff --git a/API/Data/UserRepository.cs b/API/Data/UserRepository.cs index 681cdd85c..55a382654 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; @@ -49,6 +57,23 @@ namespace API.Data public async Task> GetMembersAsync() { + 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() + }) + .ToListAsync(); + + //return await _context.Users.Include(x => x.Libraries) + return await _context.Users.Include(x => x.Libraries) .Include(x => x.Libraries) .ProjectTo(_mapper.ConfigurationProvider) diff --git a/API/Extensions/IdentityServiceExtensions.cs b/API/Extensions/IdentityServiceExtensions.cs index 9669c6822..9138ffbb8 100644 --- a/API/Extensions/IdentityServiceExtensions.cs +++ b/API/Extensions/IdentityServiceExtensions.cs @@ -35,6 +35,11 @@ namespace API.Extensions ValidateAudience = false }; }); + services.AddAuthorization(opt => + { + opt.AddPolicy("RequireAdminRole", policy => policy.RequireRole("Admin")); + }); + return services; } } 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 From fbe2daac6a93830012ef8ab22a0fba0539ec937f Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Thu, 24 Dec 2020 08:38:55 -0600 Subject: [PATCH 09/11] Fixed code for getting members with roles and libraries --- API/Data/UserRepository.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/API/Data/UserRepository.cs b/API/Data/UserRepository.cs index 55a382654..6c63d31b9 100644 --- a/API/Data/UserRepository.cs +++ b/API/Data/UserRepository.cs @@ -68,16 +68,16 @@ namespace API.Data Username = u.UserName, Created = u.Created, LastActive = u.LastActive, - Roles = u.UserRoles.Select(r => r.Role.Name).ToList() + 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(); - - //return await _context.Users.Include(x => x.Libraries) - - return await _context.Users.Include(x => x.Libraries) - .Include(x => x.Libraries) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(); } public async Task GetMemberAsync(string username) From a40bc9e9f7423d58f927e9bbfec569c999a37ede Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Thu, 24 Dec 2020 10:13:22 -0600 Subject: [PATCH 10/11] Added Policy for getUsers and moved some APIs. --- API/Controllers/AdminController.cs | 19 +- API/Controllers/UsersController.cs | 28 +- .../20201224155621_MiscCleanup.Designer.cs | 377 ++++++++++++++++++ .../Migrations/20201224155621_MiscCleanup.cs | 42 ++ 4 files changed, 444 insertions(+), 22 deletions(-) create mode 100644 API/Data/Migrations/20201224155621_MiscCleanup.Designer.cs create mode 100644 API/Data/Migrations/20201224155621_MiscCleanup.cs diff --git a/API/Controllers/AdminController.cs b/API/Controllers/AdminController.cs index 6d427de89..173961a48 100644 --- a/API/Controllers/AdminController.cs +++ b/API/Controllers/AdminController.cs @@ -1,4 +1,6 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Threading.Tasks; +using API.DTOs; using API.Entities; using API.Interfaces; using Microsoft.AspNetCore.Authorization; @@ -25,20 +27,7 @@ namespace API.Controllers return users.Count > 0; } - [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."); - } + } diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index 67039cce2..3b546b21f 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -24,12 +24,6 @@ namespace API.Controllers _userRepository = userRepository; _libraryRepository = libraryRepository; } - - [HttpGet] - public async Task>> GetUsers() - { - return Ok(await _userRepository.GetMembersAsync()); - } [HttpPost("add-library")] public async Task AddLibrary(CreateLibraryDto createLibraryDto) @@ -72,7 +66,27 @@ namespace API.Controllers 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() + { + return Ok(await _userRepository.GetMembersAsync()); + } } } \ No newline at end of file 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); + } + } +} From 793c13e1e63c6e98da83dcdc9088ff84f287e3fe Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Thu, 24 Dec 2020 10:21:59 -0600 Subject: [PATCH 11/11] Refactored all Policy strings into their own constant file. --- API/Constants/PolicyConstants.cs | 8 ++++++++ API/Controllers/AccountController.cs | 3 ++- API/Data/Seed.cs | 5 +++-- API/Extensions/IdentityServiceExtensions.cs | 3 ++- 4 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 API/Constants/PolicyConstants.cs 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 6c8c74a5b..3610925e9 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using API.Constants; using API.DTOs; using API.Entities; using API.Interfaces; @@ -50,7 +51,7 @@ namespace API.Controllers // TODO: Need a way to store Roles in enum and configure from there - var role = registerDto.IsAdmin ? "Admin" : "Pleb"; + var role = registerDto.IsAdmin ? PolicyConstants.AdminRole : PolicyConstants.PlebRole; var roleResult = await _userManager.AddToRoleAsync(user, role); if (!roleResult.Succeeded) return BadRequest(result.Errors); diff --git a/API/Data/Seed.cs b/API/Data/Seed.cs index 4bc4ebbc6..c03407e95 100644 --- a/API/Data/Seed.cs +++ b/API/Data/Seed.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; +using API.Constants; using API.Entities; using Microsoft.AspNetCore.Identity; @@ -11,8 +12,8 @@ namespace API.Data { var roles = new List { - new AppRole {Name = "Admin"}, - new AppRole {Name = "Pleb"} + new AppRole {Name = PolicyConstants.AdminRole}, + new AppRole {Name = PolicyConstants.PlebRole} }; foreach (var role in roles) diff --git a/API/Extensions/IdentityServiceExtensions.cs b/API/Extensions/IdentityServiceExtensions.cs index 9138ffbb8..2d2a235f5 100644 --- a/API/Extensions/IdentityServiceExtensions.cs +++ b/API/Extensions/IdentityServiceExtensions.cs @@ -1,4 +1,5 @@ using System.Text; +using API.Constants; using API.Data; using API.Entities; using Microsoft.AspNetCore.Authentication.JwtBearer; @@ -37,7 +38,7 @@ namespace API.Extensions }); services.AddAuthorization(opt => { - opt.AddPolicy("RequireAdminRole", policy => policy.RequireRole("Admin")); + opt.AddPolicy("RequireAdminRole", policy => policy.RequireRole(PolicyConstants.AdminRole)); }); return services;