From 52667d913d5de0f9cfdad49d0f379fe8dec35918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rich=C3=A1rd=20Kunkli?= Date: Sat, 24 Oct 2020 23:23:22 +0200 Subject: [PATCH] Separated app into three layers, cleaned up leftovers --- Birdmap.BLL/Birdmap.BLL.csproj | 11 +++++ .../Exceptions/AuthenticationException.cs | 5 +-- .../Interfaces/IAuthService.cs | 4 +- .../Services/AuthService.cs | 30 ++++++++------ Birdmap.BLL/Startup.cs | 17 ++++++++ Birdmap.DAL/Birdmap.DAL.csproj | 13 ++++++ Birdmap.DAL/BirdmapContext.cs | 27 ++++++++++++ .../Configurations/UserConfiguration.cs | 23 +++++++++++ .../Models => Birdmap.DAL/Entities}/User.cs | 16 +++++--- Birdmap.DAL/Startup.cs | 23 +++++++++++ Birdmap.sln | 14 ++++++- .../{Birdmap.csproj => Birdmap.API.csproj} | 5 +++ Birdmap/Controllers/AuthController.cs | 29 +++++++------ .../Controllers/WeatherForecastController.cs | 41 ------------------- .../{Models => DTOs}/AuthenticateRequest.cs | 0 Birdmap/DTOs/AuthenticateResponse.cs | 19 +++++++++ Birdmap/MapperProfiles/BirdmapProfile.cs | 13 ++++++ Birdmap/Startup.cs | 14 +++---- Birdmap/WeatherForecast.cs | 15 ------- Birdmap/appsettings.json | 24 +++++++++-- 20 files changed, 237 insertions(+), 106 deletions(-) create mode 100644 Birdmap.BLL/Birdmap.BLL.csproj rename {Birdmap => Birdmap.BLL}/Exceptions/AuthenticationException.cs (64%) rename {Birdmap/Services => Birdmap.BLL}/Interfaces/IAuthService.cs (71%) rename {Birdmap => Birdmap.BLL}/Services/AuthService.cs (72%) create mode 100644 Birdmap.BLL/Startup.cs create mode 100644 Birdmap.DAL/Birdmap.DAL.csproj create mode 100644 Birdmap.DAL/BirdmapContext.cs create mode 100644 Birdmap.DAL/Entities/Configurations/UserConfiguration.cs rename {Birdmap/Models => Birdmap.DAL/Entities}/User.cs (50%) create mode 100644 Birdmap.DAL/Startup.cs rename Birdmap/{Birdmap.csproj => Birdmap.API.csproj} (95%) delete mode 100644 Birdmap/Controllers/WeatherForecastController.cs rename Birdmap/{Models => DTOs}/AuthenticateRequest.cs (100%) create mode 100644 Birdmap/DTOs/AuthenticateResponse.cs create mode 100644 Birdmap/MapperProfiles/BirdmapProfile.cs delete mode 100644 Birdmap/WeatherForecast.cs diff --git a/Birdmap.BLL/Birdmap.BLL.csproj b/Birdmap.BLL/Birdmap.BLL.csproj new file mode 100644 index 0000000..8191252 --- /dev/null +++ b/Birdmap.BLL/Birdmap.BLL.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.1 + + + + + + + diff --git a/Birdmap/Exceptions/AuthenticationException.cs b/Birdmap.BLL/Exceptions/AuthenticationException.cs similarity index 64% rename from Birdmap/Exceptions/AuthenticationException.cs rename to Birdmap.BLL/Exceptions/AuthenticationException.cs index a1a1a23..04ccb33 100644 --- a/Birdmap/Exceptions/AuthenticationException.cs +++ b/Birdmap.BLL/Exceptions/AuthenticationException.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -namespace Birdmap.Exceptions +namespace Birdmap.BLL.Exceptions { public class AuthenticationException : Exception { diff --git a/Birdmap/Services/Interfaces/IAuthService.cs b/Birdmap.BLL/Interfaces/IAuthService.cs similarity index 71% rename from Birdmap/Services/Interfaces/IAuthService.cs rename to Birdmap.BLL/Interfaces/IAuthService.cs index 11d4b6f..757b2ca 100644 --- a/Birdmap/Services/Interfaces/IAuthService.cs +++ b/Birdmap.BLL/Interfaces/IAuthService.cs @@ -1,7 +1,7 @@ -using Birdmap.Models; +using Birdmap.DAL.Entities; using System.Threading.Tasks; -namespace Birdmap.Services.Interfaces +namespace Birdmap.BLL.Interfaces { public interface IAuthService { diff --git a/Birdmap/Services/AuthService.cs b/Birdmap.BLL/Services/AuthService.cs similarity index 72% rename from Birdmap/Services/AuthService.cs rename to Birdmap.BLL/Services/AuthService.cs index 82b77e5..7e73b51 100644 --- a/Birdmap/Services/AuthService.cs +++ b/Birdmap.BLL/Services/AuthService.cs @@ -1,5 +1,5 @@ -using Birdmap.Models; -using Birdmap.Services.Interfaces; +using Birdmap.DAL.Entities; +using Birdmap.BLL.Interfaces; using Microsoft.Extensions.Configuration; using System; using System.Collections.Generic; @@ -7,25 +7,31 @@ using System.Linq; using System.Security.Authentication; using System.Text; using System.Threading.Tasks; +using Birdmap.DAL; +using Microsoft.EntityFrameworkCore; -namespace Birdmap.Services +namespace Birdmap.BLL.Services { public class AuthService : IAuthService { - private readonly IConfiguration _configuration; + private readonly BirdmapContext _context; - public AuthService(IConfiguration configuration) + public AuthService(BirdmapContext context) { - _configuration = configuration; + _context = context; } - public async Task AuthenticateUserAsync(string username, string password) + public Task AuthenticateUserAsync(string username, string password) { if (string.IsNullOrWhiteSpace(username) || string.IsNullOrEmpty(password)) throw new ArgumentException("Username or password cannot be null or empty."); - //var user = await _context.Users.SingleOrDefaultAsync(u => u.Name == username) - var user = await Temp_GetUserAsync(_configuration) + return AuthenticateUserInternalAsync(username, password); + } + + private async Task AuthenticateUserInternalAsync(string username, string password) + { + var user = await _context.Users.SingleOrDefaultAsync(u => u.Name == username) ?? throw new AuthenticationException(); if (!VerifyPasswordHash(password, user.PasswordHash, user.PasswordSalt)) @@ -60,9 +66,9 @@ namespace Birdmap.Services private static bool VerifyPasswordHash(string password, byte[] storedHash, byte[] storedSalt) { - if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Value cannot be null or empty.", "password"); - if (storedHash.Length != 64) throw new ArgumentException("Invalid length of password hash (64 bytes expected).", "passwordHash"); - if (storedSalt.Length != 128) throw new ArgumentException("Invalid length of password salt (128 bytes expected).", "passwordHash"); + if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Value cannot be null or empty.", nameof(password)); + if (storedHash.Length != 64) throw new ArgumentException("Invalid length of password hash (64 bytes expected).", nameof(storedHash)); + if (storedSalt.Length != 128) throw new ArgumentException("Invalid length of password salt (128 bytes expected).", nameof(storedSalt)); using var hmac = new System.Security.Cryptography.HMACSHA512(storedSalt); diff --git a/Birdmap.BLL/Startup.cs b/Birdmap.BLL/Startup.cs new file mode 100644 index 0000000..253554a --- /dev/null +++ b/Birdmap.BLL/Startup.cs @@ -0,0 +1,17 @@ +using Birdmap.BLL.Interfaces; +using Birdmap.BLL.Services; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Birdmap.BLL +{ + public static class Startup + { + public static IServiceCollection ConfigureBLL(this IServiceCollection services, IConfiguration configuration) + { + services.AddTransient(); + + return services; + } + } +} diff --git a/Birdmap.DAL/Birdmap.DAL.csproj b/Birdmap.DAL/Birdmap.DAL.csproj new file mode 100644 index 0000000..2dfa7bf --- /dev/null +++ b/Birdmap.DAL/Birdmap.DAL.csproj @@ -0,0 +1,13 @@ + + + + netcoreapp3.1 + + + + + + + + + diff --git a/Birdmap.DAL/BirdmapContext.cs b/Birdmap.DAL/BirdmapContext.cs new file mode 100644 index 0000000..e4cfce3 --- /dev/null +++ b/Birdmap.DAL/BirdmapContext.cs @@ -0,0 +1,27 @@ +using Birdmap.DAL.Entities; +using Microsoft.EntityFrameworkCore; +using System.Diagnostics.CodeAnalysis; + +namespace Birdmap.DAL +{ + public class BirdmapContext : DbContext + { + public DbSet Users { get; set; } + + public BirdmapContext([NotNull] DbContextOptions options) : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfigurationsFromAssembly(typeof(BirdmapContext).Assembly); + + SeedDatabase(modelBuilder); + } + + private void SeedDatabase(ModelBuilder modelBuilder) + { + + } + } +} diff --git a/Birdmap.DAL/Entities/Configurations/UserConfiguration.cs b/Birdmap.DAL/Entities/Configurations/UserConfiguration.cs new file mode 100644 index 0000000..fb2b6df --- /dev/null +++ b/Birdmap.DAL/Entities/Configurations/UserConfiguration.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Birdmap.DAL.Entities.Configurations +{ + public class UserConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.Property(u => u.Name) + .IsRequired(); + + builder.Property(u => u.PasswordHash) + .IsRequired(); + + builder.Property(u => u.PasswordSalt) + .IsRequired(); + + builder.Property(u => u.Role) + .IsRequired(); + } + } +} diff --git a/Birdmap/Models/User.cs b/Birdmap.DAL/Entities/User.cs similarity index 50% rename from Birdmap/Models/User.cs rename to Birdmap.DAL/Entities/User.cs index 3ac4951..7d07766 100644 --- a/Birdmap/Models/User.cs +++ b/Birdmap.DAL/Entities/User.cs @@ -1,14 +1,18 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Birdmap.Models +namespace Birdmap.DAL.Entities { + public enum Roles + { + User, + Admin, + } + public class User { + public int Id { get; set; } public string Name { get; set; } public byte[] PasswordHash { get; set; } public byte[] PasswordSalt { get; set; } + + public Roles Role { get; set; } } } diff --git a/Birdmap.DAL/Startup.cs b/Birdmap.DAL/Startup.cs new file mode 100644 index 0000000..31e52fa --- /dev/null +++ b/Birdmap.DAL/Startup.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Birdmap.DAL +{ + public static class Startup + { + public static IServiceCollection ConfigureDAL(this IServiceCollection services, IConfiguration configuration) + { + services.AddDbContext(o => + o.UseSqlServer(configuration["LocalDbConnectionString"])); + + services.AddHealthChecks() + .AddDbContextCheck(); + + return services; + } + } +} diff --git a/Birdmap.sln b/Birdmap.sln index 0237101..2c40501 100644 --- a/Birdmap.sln +++ b/Birdmap.sln @@ -3,7 +3,11 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.30611.23 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Birdmap", "Birdmap\Birdmap.csproj", "{88855E5F-9555-49E5-92F2-4E8C1194F60B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Birdmap.API", "Birdmap\Birdmap.API.csproj", "{88855E5F-9555-49E5-92F2-4E8C1194F60B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Birdmap.BLL", "Birdmap.BLL\Birdmap.BLL.csproj", "{879D7B8D-6865-4EBE-B346-E0CA37D3C06A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Birdmap.DAL", "Birdmap.DAL\Birdmap.DAL.csproj", "{543FAB06-B960-41A9-8865-1624A2ED2170}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -15,6 +19,14 @@ Global {88855E5F-9555-49E5-92F2-4E8C1194F60B}.Debug|Any CPU.Build.0 = Debug|Any CPU {88855E5F-9555-49E5-92F2-4E8C1194F60B}.Release|Any CPU.ActiveCfg = Release|Any CPU {88855E5F-9555-49E5-92F2-4E8C1194F60B}.Release|Any CPU.Build.0 = Release|Any CPU + {879D7B8D-6865-4EBE-B346-E0CA37D3C06A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {879D7B8D-6865-4EBE-B346-E0CA37D3C06A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {879D7B8D-6865-4EBE-B346-E0CA37D3C06A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {879D7B8D-6865-4EBE-B346-E0CA37D3C06A}.Release|Any CPU.Build.0 = Release|Any CPU + {543FAB06-B960-41A9-8865-1624A2ED2170}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {543FAB06-B960-41A9-8865-1624A2ED2170}.Debug|Any CPU.Build.0 = Debug|Any CPU + {543FAB06-B960-41A9-8865-1624A2ED2170}.Release|Any CPU.ActiveCfg = Release|Any CPU + {543FAB06-B960-41A9-8865-1624A2ED2170}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Birdmap/Birdmap.csproj b/Birdmap/Birdmap.API.csproj similarity index 95% rename from Birdmap/Birdmap.csproj rename to Birdmap/Birdmap.API.csproj index fe937b2..2ff7ec8 100644 --- a/Birdmap/Birdmap.csproj +++ b/Birdmap/Birdmap.API.csproj @@ -10,6 +10,7 @@ + @@ -44,6 +45,10 @@ + + + + diff --git a/Birdmap/Controllers/AuthController.cs b/Birdmap/Controllers/AuthController.cs index 3df0d3c..48aee62 100644 --- a/Birdmap/Controllers/AuthController.cs +++ b/Birdmap/Controllers/AuthController.cs @@ -1,5 +1,7 @@ -using Birdmap.Models; -using Birdmap.Services.Interfaces; +using AutoMapper; +using Birdmap.API.DTOs; +using Birdmap.BLL.Interfaces; +using Birdmap.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -20,11 +22,13 @@ namespace Birdmap.Controllers { private readonly IAuthService _service; private readonly IConfiguration _configuration; + private readonly IMapper _mapper; - public AuthController(IAuthService service, IConfiguration configuration) + public AuthController(IAuthService service, IConfiguration configuration, IMapper mapper) { _service = service; _configuration = configuration; + _mapper = mapper; } [AllowAnonymous] @@ -35,12 +39,13 @@ namespace Birdmap.Controllers var user = await _service.AuthenticateUserAsync(model.Username, model.Password); var expiresInSeconds = TimeSpan.FromHours(2).TotalSeconds; var tokenHandler = new JwtSecurityTokenHandler(); - var key = Encoding.ASCII.GetBytes(_configuration["BasicAuth:Secret"]); + var key = Encoding.ASCII.GetBytes(_configuration["Secret"]); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(new Claim[] { - new Claim(ClaimTypes.Name, user.Name) + new Claim(ClaimTypes.Name, user.Name), + new Claim(ClaimTypes.Role, user.Role.ToString()), }), Expires = DateTime.UtcNow.AddHours(expiresInSeconds), SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) @@ -48,14 +53,12 @@ namespace Birdmap.Controllers var token = tokenHandler.CreateToken(tokenDescriptor); var tokenString = tokenHandler.WriteToken(token); - return Ok( - new - { - user_name = user.Name, - access_token = tokenString, - token_type = "Bearer", - expires_in = expiresInSeconds, - }); + var response = _mapper.Map(user); + response.AccessToken = tokenString; + response.TokenType = "Bearer"; + response.ExpiresIn = expiresInSeconds; + + return Ok(response); } } } diff --git a/Birdmap/Controllers/WeatherForecastController.cs b/Birdmap/Controllers/WeatherForecastController.cs deleted file mode 100644 index 0572181..0000000 --- a/Birdmap/Controllers/WeatherForecastController.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; - -namespace Birdmap.Controllers -{ - [Authorize] - [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/Birdmap/Models/AuthenticateRequest.cs b/Birdmap/DTOs/AuthenticateRequest.cs similarity index 100% rename from Birdmap/Models/AuthenticateRequest.cs rename to Birdmap/DTOs/AuthenticateRequest.cs diff --git a/Birdmap/DTOs/AuthenticateResponse.cs b/Birdmap/DTOs/AuthenticateResponse.cs new file mode 100644 index 0000000..a475791 --- /dev/null +++ b/Birdmap/DTOs/AuthenticateResponse.cs @@ -0,0 +1,19 @@ +using Birdmap.DAL.Entities; +using Newtonsoft.Json; + +namespace Birdmap.API.DTOs +{ + public class AuthenticateResponse + { + [JsonProperty("user_name")] + public string Username { get; set; } + [JsonProperty("user_role")] + public Roles UserRole { get; set; } + [JsonProperty("access_token")] + public string AccessToken { get; set; } + [JsonProperty("token_type")] + public string TokenType { get; set; } + [JsonProperty("expires_in")] + public double ExpiresIn { get; set; } + } +} diff --git a/Birdmap/MapperProfiles/BirdmapProfile.cs b/Birdmap/MapperProfiles/BirdmapProfile.cs new file mode 100644 index 0000000..ba57fbf --- /dev/null +++ b/Birdmap/MapperProfiles/BirdmapProfile.cs @@ -0,0 +1,13 @@ +using AutoMapper; +using Birdmap.DAL.Entities; + +namespace Birdmap.API.MapperProfiles +{ + public class BirdmapProfile : Profile + { + public BirdmapProfile() + { + CreateMap().ReverseMap(); + } + } +} diff --git a/Birdmap/Startup.cs b/Birdmap/Startup.cs index e95c385..7683c3f 100644 --- a/Birdmap/Startup.cs +++ b/Birdmap/Startup.cs @@ -1,18 +1,15 @@ -using Birdmap.Services; -using Birdmap.Services.Interfaces; +using Birdmap.BLL; +using Birdmap.BLL.Interfaces; +using Birdmap.DAL; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.HttpsPolicy; -using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Tokens; -using System; using System.Text; -using System.Text.Json; namespace Birdmap { @@ -34,9 +31,10 @@ namespace Birdmap //opt.JsonSerializerOptions.PropertyNamingPolicy = new JsonNamingPolicy() }); - services.AddTransient(); + services.ConfigureBLL(Configuration); + services.ConfigureDAL(Configuration); - var key = Encoding.ASCII.GetBytes(Configuration["BasicAuth:Secret"]); + var key = Encoding.ASCII.GetBytes(Configuration["Secret"]); services.AddAuthentication(opt => { opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; diff --git a/Birdmap/WeatherForecast.cs b/Birdmap/WeatherForecast.cs deleted file mode 100644 index 2c04d64..0000000 --- a/Birdmap/WeatherForecast.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Birdmap -{ - 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; } - } -} diff --git a/Birdmap/appsettings.json b/Birdmap/appsettings.json index c60ae0a..f9d3958 100644 --- a/Birdmap/appsettings.json +++ b/Birdmap/appsettings.json @@ -7,9 +7,25 @@ } }, "AllowedHosts": "*", - "BasicAuth": { - "Username": "user", - "Password": "pass", - "Secret": "7vj.3KW.hYE!}4u6" + "Secret": "7vj.3KW.hYE!}4u6", + "LocalDbConnectionString": null, + "Default": { + "Users": [ + { + "Username": "user", + "Password": "pass", + "Role": "User" + }, + { + "Username": "admin", + "Password": "pass", + "Role": "Admin" + } + ], + "Endpoints": [ + "", + "", + "" + ] } }