Separated app into three layers, cleaned up leftovers

This commit is contained in:
Richárd Kunkli
2020-10-24 23:23:22 +02:00
parent 96003c21dd
commit 52667d913d
20 changed files with 237 additions and 106 deletions

View File

@ -10,6 +10,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="10.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.9" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="3.1.9" />
<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="4.0.3">
@ -44,6 +45,10 @@
<Folder Include="ClientApp\src\components\heatmap\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Birdmap.BLL\Birdmap.BLL.csproj" />
</ItemGroup>
<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
<!-- Ensure Node.js is installed -->
<Exec Command="node --version" ContinueOnError="true">

View File

@ -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<AuthenticateResponse>(user);
response.AccessToken = tokenString;
response.TokenType = "Bearer";
response.ExpiresIn = expiresInSeconds;
return Ok(response);
}
}
}

View File

@ -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<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet]
public IEnumerable<WeatherForecast> 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();
}
}
}

View File

@ -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; }
}
}

View File

@ -1,15 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Birdmap.Exceptions
{
public class AuthenticationException : Exception
{
public AuthenticationException()
: base("Username or password is incorrect.")
{
}
}
}

View File

@ -0,0 +1,13 @@
using AutoMapper;
using Birdmap.DAL.Entities;
namespace Birdmap.API.MapperProfiles
{
public class BirdmapProfile : Profile
{
public BirdmapProfile()
{
CreateMap<User, DTOs.AuthenticateResponse>().ReverseMap();
}
}
}

View File

@ -1,14 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Birdmap.Models
{
public class User
{
public string Name { get; set; }
public byte[] PasswordHash { get; set; }
public byte[] PasswordSalt { get; set; }
}
}

View File

@ -1,78 +0,0 @@
using Birdmap.Models;
using Birdmap.Services.Interfaces;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Authentication;
using System.Text;
using System.Threading.Tasks;
namespace Birdmap.Services
{
public class AuthService : IAuthService
{
private readonly IConfiguration _configuration;
public AuthService(IConfiguration configuration)
{
_configuration = configuration;
}
public async Task<User> 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)
?? throw new AuthenticationException();
if (!VerifyPasswordHash(password, user.PasswordHash, user.PasswordSalt))
throw new AuthenticationException();
return user;
}
private Task<User> Temp_GetUserAsync(IConfiguration configuration)
{
var name = configuration["BasicAuth:Username"];
var pass = configuration["BasicAuth:Password"];
CreatePasswordHash(pass, out var hash, out var salt);
return Task.FromResult(new User
{
Name = name,
PasswordHash = hash,
PasswordSalt = salt,
});
}
private static void CreatePasswordHash(string password, out byte[] passwordHash, out byte[] passwordSalt)
{
if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Value cannot be null or empty.", "password");
using var hmac = new System.Security.Cryptography.HMACSHA512();
passwordSalt = hmac.Key;
passwordHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(password));
}
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");
using var hmac = new System.Security.Cryptography.HMACSHA512(storedSalt);
var computedHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(password));
for (int i = 0; i < computedHash.Length; i++)
{
if (computedHash[i] != storedHash[i]) return false;
}
return true;
}
}
}

View File

@ -1,10 +0,0 @@
using Birdmap.Models;
using System.Threading.Tasks;
namespace Birdmap.Services.Interfaces
{
public interface IAuthService
{
Task<User> AuthenticateUserAsync(string username, string password);
}
}

View File

@ -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<IAuthService, AuthService>();
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;

View File

@ -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; }
}
}

View File

@ -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": [
"",
"",
""
]
}
}