Add Users code
This commit is contained in:
parent
f46d20c1ea
commit
2a404c3560
|
@ -12,8 +12,10 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.ApiAuthorization.IdentityServer" Version="6.0.2" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.8" />
|
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.8" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authorization.Policy" Version="2.2.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Authorization.Policy" Version="2.2.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.7" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.2">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.2">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
@ -41,6 +43,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\BlueWest.Data.Capital\BlueWest.Data.Capital.csproj" />
|
<ProjectReference Include="..\BlueWest.Data.Capital\BlueWest.Data.Capital.csproj" />
|
||||||
|
<ProjectReference Include="..\BlueWest.Data.Geography\BlueWest.Data.Geography.csproj" />
|
||||||
<ProjectReference Include="..\BlueWest\BlueWest.csproj" />
|
<ProjectReference Include="..\BlueWest\BlueWest.csproj" />
|
||||||
<ProjectReference Include="..\include\BlueWest.EfMethods\src\BlueWest.EfMethods\BlueWest.EfMethods.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
|
<ProjectReference Include="..\include\BlueWest.EfMethods\src\BlueWest.EfMethods\BlueWest.EfMethods.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,9 @@ using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace BlueWest.WebApi.Context.Extensions
|
namespace BlueWest.WebApi.Context.Extensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Code first model builder
|
||||||
|
/// </summary>
|
||||||
public static class ModelBuilderCountryDbContextExtensions
|
public static class ModelBuilderCountryDbContextExtensions
|
||||||
{ /// <summary>
|
{ /// <summary>
|
||||||
/// Setup the database model
|
/// Setup the database model
|
||||||
|
|
|
@ -16,7 +16,7 @@ public static (bool, {returnTypeFullName}[]) Get{propertyName}(this {contextFull
|
||||||
|
|
||||||
var query = dbContext
|
var query = dbContext
|
||||||
.{propertyName}
|
.{propertyName}
|
||||||
.Select(x => new {returnTypeFullName}(x));
|
.Select(x => new {returnTypeFullName}(x))
|
||||||
.Skip(skip)
|
.Skip(skip)
|
||||||
.Take(take);
|
.Take(take);
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using BlueWest.Data;
|
using BlueWest.Data;
|
||||||
|
using BlueWest.WebApi.Context.Users;
|
||||||
using BlueWest.WebApi.EF.Model;
|
using BlueWest.WebApi.EF.Model;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
@ -15,6 +16,7 @@ namespace BlueWest.WebApi.EF
|
||||||
/// Users entity.
|
/// Users entity.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DbSet<User> Users { get; set; }
|
public DbSet<User> Users { get; set; }
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// App configuration.
|
/// App configuration.
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AutoMapper;
|
||||||
|
using BlueWest.WebApi.Context.Users;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Controllers;
|
||||||
|
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
|
||||||
|
[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
|
||||||
|
[ApiController]
|
||||||
|
public class AuthController : Controller
|
||||||
|
{
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
private readonly IAuthManager _authManager;
|
||||||
|
private readonly IUserManager _userManager;
|
||||||
|
|
||||||
|
public AuthController( IMapper mapper, IAuthManager authManager, IUserManager userManager)
|
||||||
|
{
|
||||||
|
_mapper = mapper;
|
||||||
|
_authManager = authManager;
|
||||||
|
_userManager = userManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpPost("register")]
|
||||||
|
public async Task<ActionResult<IdentityResult>> SignupUserAsync(RegisterViewModel registerViewModel)
|
||||||
|
{
|
||||||
|
return await _authManager.CreateUserAsync(registerViewModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpPost("login")]
|
||||||
|
public async Task<ActionResult<IdentityResult>> GetTokenAsync(LoginViewModel loginViewModel)
|
||||||
|
{
|
||||||
|
var loginResultSucceded = await _authManager.GetToken(loginViewModel);
|
||||||
|
|
||||||
|
if (loginResultSucceded != null)
|
||||||
|
{
|
||||||
|
return Ok(_mapper.Map<AccessToken>(loginResultSucceded));
|
||||||
|
|
||||||
|
}
|
||||||
|
return Problem();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpPost("login2")]
|
||||||
|
public async Task<ActionResult<IdentityResult>> DoLoginAsync(LoginViewModel loginDto)
|
||||||
|
{
|
||||||
|
var user = await _userManager.FindByEmailAsync(loginDto.Email);
|
||||||
|
|
||||||
|
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
if(await _userManager.CheckPasswordAsync(user, loginDto.Password))
|
||||||
|
{
|
||||||
|
var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
|
identity.AddClaim(new Claim(ClaimTypes.Email, user.Email));
|
||||||
|
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity));
|
||||||
|
return Json(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Json(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpPost("logout")]
|
||||||
|
public async Task<ActionResult<IdentityResult>> DoLogoutAsync(LoginViewModel loginDto)
|
||||||
|
{
|
||||||
|
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
|
|
||||||
|
|
||||||
|
return Json(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet("test")]
|
||||||
|
public ActionResult TestRequest()
|
||||||
|
{
|
||||||
|
return Ok(new {Message = "Test"});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -77,9 +77,9 @@ namespace BlueWest.WebApi.Controllers
|
||||||
#region GetCurrencyWithCode
|
#region GetCurrencyWithCode
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a currency by the currency number (id)
|
/// Gets a currency by code.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="currencyId">The id of the currency to get</param>
|
/// <param name="currencyCode">The currency Code </param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
@ -179,7 +179,6 @@ namespace BlueWest.WebApi.Controllers
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add Currency to the table of currencies
|
/// Add Currency to the table of currencies
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="currencyId"></param>
|
|
||||||
/// <param name="countryToCreate"></param>
|
/// <param name="countryToCreate"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||||
|
|
|
@ -7,8 +7,12 @@ using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using BlueWest.Tools;
|
using BlueWest.Tools;
|
||||||
|
using BlueWest.WebApi.Context.Users;
|
||||||
using BlueWest.WebApi.Interfaces;
|
using BlueWest.WebApi.Interfaces;
|
||||||
using BlueWest.WebApi.Tools;
|
using BlueWest.WebApi.Tools;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.Extensions.FileProviders;
|
||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi.Models;
|
||||||
|
|
||||||
namespace BlueWest.WebApi
|
namespace BlueWest.WebApi
|
||||||
|
@ -70,6 +74,38 @@ namespace BlueWest.WebApi
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
services.Configure<IdentityOptions>(options =>
|
||||||
|
{
|
||||||
|
// Password settings.
|
||||||
|
options.Password.RequireDigit = true;
|
||||||
|
options.Password.RequireLowercase = true;
|
||||||
|
options.Password.RequireNonAlphanumeric = true;
|
||||||
|
options.Password.RequireUppercase = true;
|
||||||
|
options.Password.RequiredLength = 6;
|
||||||
|
options.Password.RequiredUniqueChars = 1;
|
||||||
|
|
||||||
|
// Lockout settings.
|
||||||
|
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
|
||||||
|
options.Lockout.MaxFailedAccessAttempts = 5;
|
||||||
|
options.Lockout.AllowedForNewUsers = true;
|
||||||
|
|
||||||
|
// User settings.
|
||||||
|
options.User.AllowedUserNameCharacters =
|
||||||
|
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
|
||||||
|
options.User.RequireUniqueEmail = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
services.AddScoped<SignInManager>();
|
||||||
|
|
||||||
|
services.AddScoped<RoleManager>();
|
||||||
|
|
||||||
|
|
||||||
|
services.AddSingleton<IFileProvider>(
|
||||||
|
new PhysicalFileProvider(
|
||||||
|
Path.Combine(Directory.GetCurrentDirectory(), "wwwroot/ImageFiles")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
IConfigurationRoot configuration = new ConfigurationBuilder()
|
IConfigurationRoot configuration = new ConfigurationBuilder()
|
||||||
.AddJsonFile("config.json")
|
.AddJsonFile("config.json")
|
||||||
.Build();
|
.Build();
|
||||||
|
@ -118,11 +154,12 @@ namespace BlueWest.WebApi
|
||||||
c.RoutePrefix = "swagger";
|
c.RoutePrefix = "swagger";
|
||||||
c.SwaggerEndpoint("/swagger/v1/swagger.json", "BlueWest.Api v1");
|
c.SwaggerEndpoint("/swagger/v1/swagger.json", "BlueWest.Api v1");
|
||||||
});
|
});
|
||||||
|
app.UseStaticFiles();
|
||||||
//app.UseHttpsRedirection();
|
//app.UseHttpsRedirection();
|
||||||
|
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
app.UseCors(MyAllowSpecificOrigins);
|
app.UseCors(MyAllowSpecificOrigins);
|
||||||
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
app.UseEndpoints(endpoints =>
|
app.UseEndpoints(endpoints =>
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,12 +1,24 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BlueWest.Cryptography;
|
||||||
|
using BlueWest.Data;
|
||||||
|
using BlueWest.WebApi.Context.Users;
|
||||||
using BlueWest.WebApi.EF;
|
using BlueWest.WebApi.EF;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
|
||||||
namespace BlueWest.WebApi
|
namespace BlueWest.WebApi
|
||||||
{
|
{
|
||||||
|
@ -89,5 +101,139 @@ namespace BlueWest.WebApi
|
||||||
.AddDbContextPool<CompanyDbContext>(options => options.UseSqlite(sqliteConString));
|
.AddDbContextPool<CompanyDbContext>(options => options.UseSqlite(sqliteConString));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void AddAuthServerServices(this IServiceCollection services, string origins, IConfiguration _configuration)
|
||||||
|
{
|
||||||
|
services.AddScoped<IJwtTokenHandler, JwtTokenHandler>();
|
||||||
|
services.AddScoped<IJwtFactory, JwtFactory>();
|
||||||
|
|
||||||
|
// User management
|
||||||
|
services
|
||||||
|
.AddIdentityCore<ApplicationUser>(opt => { opt.User.RequireUniqueEmail = true; })
|
||||||
|
.AddEntityFrameworkStores<UserDbContext>()
|
||||||
|
.AddUserManager<UserManager>()
|
||||||
|
.AddUserStore<UserRepository>();
|
||||||
|
// Database Context and Swagger
|
||||||
|
|
||||||
|
services.TryAddSingleton<ISystemClock, SystemClock>();
|
||||||
|
// Registering 'services' and Authentication, Cookies, JWT
|
||||||
|
services
|
||||||
|
.AddScoped<IUsersRepo, UserRepository>()
|
||||||
|
.AddScoped<IUserManager, UserManager>() // So it gets successfully registered in UserManager
|
||||||
|
.AddScoped<IAuthManager, AuthManager>()
|
||||||
|
.AddScoped<IHasher, Hasher>();
|
||||||
|
|
||||||
|
|
||||||
|
// Register the ConfigurationBuilder instance of AuthSettings
|
||||||
|
var authSettings = _configuration.GetSection(nameof(AuthSettings));
|
||||||
|
services.Configure<AuthSettings>(authSettings);
|
||||||
|
var signingKey = new SymmetricSecurityKey
|
||||||
|
(Encoding.ASCII.GetBytes(authSettings[nameof(AuthSettings.SecretKey)]));
|
||||||
|
|
||||||
|
// jwt wire up
|
||||||
|
// Get options from app settings
|
||||||
|
var jwtAppSettingOptions = _configuration
|
||||||
|
.GetSection(nameof(JwtIssuerOptions));
|
||||||
|
|
||||||
|
// Configure JwtIssuerOptions
|
||||||
|
services.Configure<JwtIssuerOptions>(options =>
|
||||||
|
{
|
||||||
|
options.Issuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)];
|
||||||
|
options.Audience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)];
|
||||||
|
options.SigningCredentials = new SigningCredentials
|
||||||
|
(signingKey, SecurityAlgorithms.HmacSha256);
|
||||||
|
});
|
||||||
|
|
||||||
|
var tokenValidationParameters = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuer = true,
|
||||||
|
ValidIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)],
|
||||||
|
|
||||||
|
ValidateAudience = true,
|
||||||
|
ValidAudience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)],
|
||||||
|
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
IssuerSigningKey = signingKey,
|
||||||
|
|
||||||
|
RequireExpirationTime = false,
|
||||||
|
ValidateLifetime = true,
|
||||||
|
ClockSkew = TimeSpan.Zero
|
||||||
|
};
|
||||||
|
|
||||||
|
services.AddAuthentication(options =>
|
||||||
|
{
|
||||||
|
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
|
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
|
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
|
||||||
|
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
|
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
|
})
|
||||||
|
.AddCookie(options =>
|
||||||
|
{
|
||||||
|
options.LoginPath = "/api/auth/login2";
|
||||||
|
options.LogoutPath = "/api/auth/logout";
|
||||||
|
})
|
||||||
|
.AddJwtBearer(configureOptions =>
|
||||||
|
{
|
||||||
|
configureOptions.ClaimsIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)];
|
||||||
|
configureOptions.TokenValidationParameters = tokenValidationParameters;
|
||||||
|
configureOptions.SaveToken = true;
|
||||||
|
|
||||||
|
configureOptions.Events = new JwtBearerEvents
|
||||||
|
{
|
||||||
|
OnAuthenticationFailed = context =>
|
||||||
|
{
|
||||||
|
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
|
||||||
|
{
|
||||||
|
context.Response.Headers.Add("Token-Expired", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// api user claim policy
|
||||||
|
services.AddAuthorization(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy("ApiUser",
|
||||||
|
policy => policy.RequireClaim(Constants.JwtClaimIdentifiers.Rol,
|
||||||
|
Constants.JwtClaims.ApiAccess));
|
||||||
|
});
|
||||||
|
|
||||||
|
// add identity
|
||||||
|
var identityBuilder = services.AddIdentityCore<User>(o =>
|
||||||
|
{
|
||||||
|
// configure identity options
|
||||||
|
o.Password.RequireDigit = false;
|
||||||
|
o.Password.RequireLowercase = false;
|
||||||
|
o.Password.RequireUppercase = false;
|
||||||
|
o.Password.RequireNonAlphanumeric = false;
|
||||||
|
o.Password.RequiredLength = 6;
|
||||||
|
});
|
||||||
|
|
||||||
|
identityBuilder = new IdentityBuilder(identityBuilder.UserType, typeof(IdentityRole), identityBuilder.Services);
|
||||||
|
identityBuilder.AddEntityFrameworkStores<UserDbContext>().AddDefaultTokenProviders();
|
||||||
|
}
|
||||||
|
public static void ConfigureApiWithUsers(this IApplicationBuilder app, IWebHostEnvironment env, string origins)
|
||||||
|
{
|
||||||
|
if (env.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseDeveloperExceptionPage();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
app.UseHsts();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseSwagger()
|
||||||
|
.UseSwaggerUI(config => { config.SwaggerEndpoint("/swagger/v1/swagger.json", "Commands And Snippets API"); })
|
||||||
|
.UseRouting()
|
||||||
|
.UseAuthentication()
|
||||||
|
.UseAuthorization()
|
||||||
|
.UseCors(origins)
|
||||||
|
.UseEndpoints(endpoints => endpoints.MapControllers());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
using System;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Application User in the Identity System.
|
||||||
|
/// </summary>
|
||||||
|
public class ApplicationUser : IdentityUser<string>
|
||||||
|
{
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the primary key for this user.
|
||||||
|
/// </summary>
|
||||||
|
[PersonalData]
|
||||||
|
public new Guid Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the user name for this user.
|
||||||
|
/// </summary>
|
||||||
|
[ProtectedPersonalData]
|
||||||
|
public override string UserName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the normalized user name for this user.
|
||||||
|
/// </summary>
|
||||||
|
public override string NormalizedUserName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the email address for this user.
|
||||||
|
/// </summary>
|
||||||
|
[ProtectedPersonalData]
|
||||||
|
public override string Email { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the normalized email address for this user.
|
||||||
|
/// </summary>
|
||||||
|
public override string NormalizedEmail { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a flag indicating if a user has confirmed their email address.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>True if the email address has been confirmed, otherwise false.</value>
|
||||||
|
[PersonalData]
|
||||||
|
public override bool EmailConfirmed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a salted and hashed representation of the password for this user.
|
||||||
|
/// </summary>
|
||||||
|
public override string PasswordHash { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A random value that must change whenever a users credentials change (password changed, login removed)
|
||||||
|
/// </summary>
|
||||||
|
public override string SecurityStamp { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A random value that must change whenever a user is persisted to the store
|
||||||
|
/// </summary>
|
||||||
|
public override string ConcurrencyStamp { get; set; } = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a telephone number for the user.
|
||||||
|
/// </summary>
|
||||||
|
[ProtectedPersonalData]
|
||||||
|
public override string PhoneNumber { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a flag indicating if a user has confirmed their telephone address.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>True if the telephone number has been confirmed, otherwise false.</value>
|
||||||
|
[PersonalData]
|
||||||
|
public override bool PhoneNumberConfirmed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a flag indicating if two factor authentication is enabled for this user.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>True if 2fa is enabled, otherwise false.</value>
|
||||||
|
[PersonalData]
|
||||||
|
public override bool TwoFactorEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the date and time, in UTC, when any user lockout ends.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A value in the past means the user is not locked out.
|
||||||
|
/// </remarks>
|
||||||
|
public override DateTimeOffset? LockoutEnd { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a flag indicating if the user could be locked out.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>True if the user could be locked out, otherwise false.</value>
|
||||||
|
public override bool LockoutEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the number of failed login attempts for the current user.
|
||||||
|
/// </summary>
|
||||||
|
public override int AccessFailedCount { get; set; }
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BlueWest.Data;
|
||||||
|
using BlueWest.WebApi.EF;
|
||||||
|
using BlueWest.WebApi.EF.Model;
|
||||||
|
using Duende.IdentityServer.EntityFramework.Entities;
|
||||||
|
using Duende.IdentityServer.EntityFramework.Interfaces;
|
||||||
|
using Duende.IdentityServer.Stores.Serialization;
|
||||||
|
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Application User Db Context
|
||||||
|
/// </summary>
|
||||||
|
public class ApplicationUserDbContext : IdentityDbContext<
|
||||||
|
ApplicationUser,
|
||||||
|
ApplicationRole,
|
||||||
|
string,
|
||||||
|
ApplicationUserClaim,
|
||||||
|
ApplicationUserRole,
|
||||||
|
ApplicationUserLogin,
|
||||||
|
ApplicationRoleClaim,
|
||||||
|
ApplicationUserToken>, IPersistedGrantDbContext
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the <see cref="DbSet{TEntity}"/> of User roles.
|
||||||
|
/// </summary>
|
||||||
|
public override DbSet<ApplicationUserRole> UserRoles { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the <see cref="DbSet{TEntity}"/> of roles.
|
||||||
|
/// </summary>
|
||||||
|
public override DbSet<ApplicationRole> Roles { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the <see cref="DbSet{TEntity}"/> of role claims.
|
||||||
|
/// </summary>
|
||||||
|
public override DbSet<ApplicationRoleClaim> RoleClaims { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures the schema needed for the identity framework.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="builder">
|
||||||
|
/// The builder being used to construct the model for this context.
|
||||||
|
/// </param>
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Database for the context of database users
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options"></param>
|
||||||
|
public ApplicationUserDbContext(DbContextOptions<UserDbContext> options) : base(options)
|
||||||
|
{
|
||||||
|
Database.EnsureCreated();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures the schema needed for the identity framework.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="builder">
|
||||||
|
/// The builder being used to construct the model for this context.
|
||||||
|
/// </param>
|
||||||
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
|
{
|
||||||
|
base.OnModelCreating(builder);
|
||||||
|
builder.ConfigureCurrentDbModel();
|
||||||
|
base.OnModelCreating(builder);
|
||||||
|
builder.Entity<ApplicationUser>(b =>
|
||||||
|
{
|
||||||
|
b.HasMany<ApplicationUserRole>().WithOne().HasForeignKey(ur => ur.UserId).IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Entity<ApplicationRole>(b =>
|
||||||
|
{
|
||||||
|
b.HasKey(r => r.Id);
|
||||||
|
b.HasIndex(r => r.NormalizedName).HasDatabaseName("RoleNameIndex").IsUnique();
|
||||||
|
b.ToTable("Roles");
|
||||||
|
b.Property(r => r.ConcurrencyStamp).IsConcurrencyToken();
|
||||||
|
|
||||||
|
b.Property(u => u.Name).HasMaxLength(256);
|
||||||
|
b.Property(u => u.NormalizedName).HasMaxLength(256);
|
||||||
|
|
||||||
|
b.HasMany<ApplicationUserRole>().WithOne().HasForeignKey(ur => ur.RoleId).IsRequired();
|
||||||
|
b.HasMany<ApplicationRoleClaim>().WithOne().HasForeignKey(rc => rc.RoleId).IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Entity<ApplicationRoleClaim>(b =>
|
||||||
|
{
|
||||||
|
b.HasKey(rc => rc.Id);
|
||||||
|
b.ToTable("RoleClaims");
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Entity<ApplicationUserRole>(b =>
|
||||||
|
{
|
||||||
|
b.HasKey(r => new { r.UserId, r.RoleId });
|
||||||
|
b.ToTable("UserRoles");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<int> SaveChangesAsync()
|
||||||
|
{
|
||||||
|
return SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public DbSet<PersistedGrant> PersistedGrants { get; set; }
|
||||||
|
public DbSet<DeviceFlowCodes> DeviceFlowCodes { get; set; }
|
||||||
|
public DbSet<Key> Keys { get; set; }
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
namespace BlueWest.WebApi.Context.Users
|
||||||
|
{
|
||||||
|
public class AccessToken
|
||||||
|
{
|
||||||
|
public string Token { get; }
|
||||||
|
public int ExpiresIn { get; }
|
||||||
|
|
||||||
|
public AccessToken(string token, int expiresIn)
|
||||||
|
{
|
||||||
|
Token = token;
|
||||||
|
ExpiresIn = expiresIn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using AutoMapper;
|
||||||
|
using BlueWest.Cryptography;
|
||||||
|
using BlueWest.Data;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
public class AuthManager : IAuthManager
|
||||||
|
{
|
||||||
|
private readonly IUserManager _userManager;
|
||||||
|
private readonly IUsersRepo _usersRepo;
|
||||||
|
private readonly IHasher _hasher;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
private readonly IJwtFactory _jwtFactory;
|
||||||
|
|
||||||
|
public AuthManager(IUserManager userManager, IHasher hasher, IMapper mapper
|
||||||
|
, IUsersRepo usersRepo, IJwtFactory jwtFactory)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_hasher = hasher;
|
||||||
|
_mapper = mapper;
|
||||||
|
_usersRepo = usersRepo;
|
||||||
|
_jwtFactory = jwtFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AccessToken> GetToken(LoginViewModel loginViewModel)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(loginViewModel.Email) && !string.IsNullOrEmpty(loginViewModel.Password))
|
||||||
|
{
|
||||||
|
var user = await _userManager.FindByEmailAsync(loginViewModel.Email);
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
if (await VerifyLoginAsync(loginViewModel.Email,loginViewModel.Password))
|
||||||
|
{
|
||||||
|
// Todo generate refresh token
|
||||||
|
// Todo Add refresh token
|
||||||
|
await _usersRepo.UpdateAsync(user, CancellationToken.None);
|
||||||
|
var token = await _jwtFactory.GenerateEncodedToken(user.Id.ToString(), user.UserName);
|
||||||
|
// await _userManager.SetAuthenticationTokenAsync(user, "Income", "ApiUser", token.Token);
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> VerifyLoginAsync(string email, string password)
|
||||||
|
{
|
||||||
|
var user = await _userManager.FindByEmailAsync(email);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return false; // return error user doesn't exist
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _userManager.CheckPasswordAsync(user, password);
|
||||||
|
|
||||||
|
// return await GenerateAuthenticationResultForUserAsync(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RegisterViewModel FromSignupToUser(RegisterViewModel signupDto)
|
||||||
|
{
|
||||||
|
var pwd = signupDto.Password;
|
||||||
|
var hash = _hasher.CreateHash(pwd, BaseCryptoItem.HashAlgorithm.SHA3_512);
|
||||||
|
signupDto.Password = hash;
|
||||||
|
signupDto.ConfirmPassword = hash;
|
||||||
|
|
||||||
|
return signupDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IdentityResult> CreateUserAsync(RegisterViewModel userSignupDto)
|
||||||
|
{
|
||||||
|
|
||||||
|
return await _userManager.CreateAsync(userSignupDto.ToUser());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
public class AuthSettings
|
||||||
|
{
|
||||||
|
public string SecretKey { get; set; }
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BlueWest.Data;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
public interface IAuthManager
|
||||||
|
{
|
||||||
|
Task<IdentityResult> CreateUserAsync(RegisterViewModel registerViewModel);
|
||||||
|
|
||||||
|
Task<bool> VerifyLoginAsync(string email, string password);
|
||||||
|
|
||||||
|
Task<AccessToken> GetToken(LoginViewModel loginViewModel);
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SignInManager
|
||||||
|
/// </summary>
|
||||||
|
internal class SignInManager : SignInManager<ApplicationUser>
|
||||||
|
{
|
||||||
|
public SignInManager(
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
IHttpContextAccessor contextAccessor,
|
||||||
|
IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory,
|
||||||
|
IOptions<IdentityOptions> optionsAccessor,
|
||||||
|
ILogger<SignInManager<ApplicationUser>> logger,
|
||||||
|
IAuthenticationSchemeProvider schemes,
|
||||||
|
IUserConfirmation<ApplicationUser> confirmation) :
|
||||||
|
base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<ClaimsPrincipal> CreateUserPrincipalAsync(ApplicationUser user) => await ClaimsFactory.CreateAsync(user);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
public static class Constants
|
||||||
|
{
|
||||||
|
public const string AdminRoleName = "Admin";
|
||||||
|
public const string UserRoleName = "User";
|
||||||
|
public const string ExpectatorRoleName = "Expectator";
|
||||||
|
public static class JwtClaimIdentifiers
|
||||||
|
{
|
||||||
|
public const string Rol = "rol", Id = "id";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class JwtClaims
|
||||||
|
{
|
||||||
|
public const string ApiAccess = "api_access";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace BlueWest.Cryptography
|
||||||
|
{
|
||||||
|
public abstract class BaseCryptoItem
|
||||||
|
{
|
||||||
|
public enum HashAlgorithm
|
||||||
|
{
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
SHA2_512 = 1,
|
||||||
|
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
PBKDF2_SHA512 = 2,
|
||||||
|
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
SHA3_512 = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
protected byte[] HexStringToByteArray(string stringInHexFormat)
|
||||||
|
{
|
||||||
|
var converted = Enumerable.Range(0, stringInHexFormat.Length)
|
||||||
|
.Where(x => x % 2 == 0)
|
||||||
|
.Select(x => Convert.ToByte(stringInHexFormat.Substring(x, 2), 16))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return converted;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected string ByteArrayToString(byte[] bytes)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
|
for (int i = 0; i < bytes.Length; i++)
|
||||||
|
{
|
||||||
|
sb.Append(bytes[i].ToString("X2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates a random string
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="length">The length of the random string</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected string CreateRandomString(int length)
|
||||||
|
{
|
||||||
|
var rng = RandomNumberGenerator.Create();
|
||||||
|
var buffer = new byte[length / 2];
|
||||||
|
rng.GetBytes(buffer);
|
||||||
|
var randomString = BitConverter.ToString(buffer).Replace("-", "");
|
||||||
|
return randomString;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Refactor me
|
||||||
|
/// <summary>
|
||||||
|
/// Get Cryptographic algorithm
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cipherText"></param>
|
||||||
|
/// <param name="algorithm"></param>
|
||||||
|
/// <param name="keyIndex"></param>
|
||||||
|
/// <param name="trimmedCipherText"></param>
|
||||||
|
protected void GetAlgorithm(string cipherText, out int? algorithm, out int? keyIndex,
|
||||||
|
out string trimmedCipherText)
|
||||||
|
{
|
||||||
|
algorithm = null;
|
||||||
|
keyIndex = null;
|
||||||
|
trimmedCipherText = cipherText;
|
||||||
|
|
||||||
|
if (cipherText.Length <= 5 || cipherText[0] != '[') return;
|
||||||
|
|
||||||
|
var cipherInfo = cipherText.Substring(1, cipherText.IndexOf(']') - 1).Split(",");
|
||||||
|
|
||||||
|
if (int.TryParse(cipherInfo[0], out var foundAlgorithm))
|
||||||
|
{
|
||||||
|
algorithm = foundAlgorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cipherInfo.Length == 2 && int.TryParse(cipherInfo[1], out var foundKeyIndex))
|
||||||
|
keyIndex = foundKeyIndex;
|
||||||
|
trimmedCipherText = cipherText.Substring(cipherText.IndexOf(']') + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using BlueWest.Data;
|
||||||
|
using BlueWest.WebApi.Context.Users;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
|
||||||
|
namespace BlueWest.Cryptography
|
||||||
|
{
|
||||||
|
|
||||||
|
public class Hasher : BaseCryptoItem, IHasher
|
||||||
|
{
|
||||||
|
private const int SaltLength = 64;
|
||||||
|
|
||||||
|
public string CreateHash(string text, BaseCryptoItem.HashAlgorithm algorithm)
|
||||||
|
{
|
||||||
|
var salt = CreateRandomString(SaltLength);
|
||||||
|
return CreateHash(text, salt, algorithm, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string CreateHash(string text, string saltName, BaseCryptoItem.HashAlgorithm algorithm)
|
||||||
|
{
|
||||||
|
var salt = "TODOFIXME";
|
||||||
|
return CreateHash(text, salt, algorithm, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string CreateHash(string text, string salt, HashAlgorithm algorithm, bool storeSalt)
|
||||||
|
{
|
||||||
|
string hash;
|
||||||
|
|
||||||
|
switch (algorithm)
|
||||||
|
{
|
||||||
|
case HashAlgorithm.SHA2_512:
|
||||||
|
var sha2 = new SHA2_512();
|
||||||
|
hash = sha2.Hash(text, salt, storeSalt);
|
||||||
|
break;
|
||||||
|
case HashAlgorithm.SHA3_512:
|
||||||
|
var sha3 = new SHA2_512();
|
||||||
|
hash = sha3.Hash(text, salt, storeSalt);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool MatchesHash(string text, string hash)
|
||||||
|
{
|
||||||
|
string salt = "";
|
||||||
|
|
||||||
|
GetAlgorithm(hash, out var algoAsInt, out _, out _, out salt);
|
||||||
|
|
||||||
|
if (!algoAsInt.HasValue) return false;
|
||||||
|
|
||||||
|
var hashAlgorithm = (HashAlgorithm) algoAsInt.Value;
|
||||||
|
var hashed = CreateHash(text, salt, hashAlgorithm, true);
|
||||||
|
return hashed == hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string HashPassword(ApplicationUser ApplicationUser, string password)
|
||||||
|
{
|
||||||
|
return CreateHash(password, HashAlgorithm.SHA3_512);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PasswordVerificationResult VerifyHashedPassword(ApplicationUser ApplicationUser, string hashedPassword,
|
||||||
|
string providedPassword)
|
||||||
|
{
|
||||||
|
var match = MatchesHash(providedPassword, hashedPassword);
|
||||||
|
return match ? PasswordVerificationResult.Success : PasswordVerificationResult.Failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GetAlgorithm(string cipherText, out int? algorithm, out int? keyIndex,
|
||||||
|
out string trimmedCipherText, out string salt)
|
||||||
|
{
|
||||||
|
GetAlgorithm(cipherText, out algorithm, out keyIndex, out trimmedCipherText);
|
||||||
|
if (algorithm.HasValue && trimmedCipherText.Length > SaltLength)
|
||||||
|
{
|
||||||
|
salt = trimmedCipherText.Substring(0, SaltLength);
|
||||||
|
trimmedCipherText = trimmedCipherText.Substring(SaltLength);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
salt = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
using BlueWest.Data;
|
||||||
|
using BlueWest.WebApi.Context.Users;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace BlueWest.Cryptography;
|
||||||
|
|
||||||
|
public interface IHasher : IPasswordHasher<ApplicationUser>
|
||||||
|
{
|
||||||
|
string CreateHash(string text, BaseCryptoItem.HashAlgorithm algorithm);
|
||||||
|
string CreateHash(string text, string salt, BaseCryptoItem.HashAlgorithm algorithm);
|
||||||
|
bool MatchesHash(string text, string hash);
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
|
||||||
|
namespace BlueWest.Cryptography
|
||||||
|
{
|
||||||
|
public class SHA2_512 : BaseCryptoItem
|
||||||
|
{
|
||||||
|
public string Hash(string text, string salt, bool storeSalt)
|
||||||
|
{
|
||||||
|
var fullText = string.Concat(text, salt);
|
||||||
|
var data = Encoding.UTF8.GetBytes(fullText);
|
||||||
|
string hash;
|
||||||
|
using SHA512 sha = new SHA512Managed();
|
||||||
|
var hashBytes = sha.ComputeHash(data);
|
||||||
|
var asString = ByteArrayToString(hashBytes);
|
||||||
|
|
||||||
|
if (storeSalt)
|
||||||
|
{
|
||||||
|
hash = $"[{(int)HashAlgorithm.SHA3_512}]{salt}{asString}";
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
hash = $"[{(int)HashAlgorithm.SHA3_512}]{asString}";
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public string Hash_PBKDF2(string plainText, string salt, bool saveSaltInResult)
|
||||||
|
{
|
||||||
|
var saltAsBytes = Encoding.ASCII.GetBytes(salt);
|
||||||
|
|
||||||
|
string hashed = ByteArrayToString(KeyDerivation.Pbkdf2(
|
||||||
|
password: plainText,
|
||||||
|
salt: saltAsBytes,
|
||||||
|
prf: KeyDerivationPrf.HMACSHA512, //.NET 3.1 uses HMACSHA256 here
|
||||||
|
iterationCount: 100000, //.NET 3.1 uses 10,000 iterations here
|
||||||
|
numBytesRequested: 64)); //.NET 3.1 uses 32 bytes here
|
||||||
|
|
||||||
|
if (saveSaltInResult)
|
||||||
|
return string.Format("[{0}]{1}{2}", (int)HashAlgorithm.PBKDF2_SHA512, salt, hashed);
|
||||||
|
else
|
||||||
|
return string.Format("[{0}]{1}", (int)HashAlgorithm.PBKDF2_SHA512, hashed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BlueWest.Data;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users
|
||||||
|
{
|
||||||
|
public interface IUserManager
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Create user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<IdentityResult> CreateAsync(ApplicationUser user);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks for user password
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user"></param>
|
||||||
|
/// <param name="password"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<bool> CheckPasswordAsync(ApplicationUser user, string password);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Find by email
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="email"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<ApplicationUser> FindByEmailAsync(string email);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BlueWest.Data;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
/// <summary>
|
||||||
|
/// This is our Users repository.
|
||||||
|
/// Since this is a simple app we'll have the following roles
|
||||||
|
/// Admin and APIClient
|
||||||
|
/// </summary>
|
||||||
|
public interface IUsersRepo : IUserStore<ApplicationUser>
|
||||||
|
{
|
||||||
|
public Task<IEnumerable<ApplicationUser>> GetUsers();
|
||||||
|
public Task CreateUser(ApplicationUser user);
|
||||||
|
public Task SaveChanges();
|
||||||
|
|
||||||
|
public Task<ApplicationUser> GetUserById(string id);
|
||||||
|
|
||||||
|
Task<ApplicationUser> FindByEmailAsync(string email, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
public interface IJwtFactory
|
||||||
|
{
|
||||||
|
Task<AccessToken> GenerateEncodedToken(string id, string userName);
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users
|
||||||
|
{
|
||||||
|
public interface IJwtTokenHandler
|
||||||
|
{
|
||||||
|
string WriteToken(JwtSecurityToken jwt);
|
||||||
|
ClaimsPrincipal ValidateToken(string token, TokenValidationParameters tokenValidationParameters);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
public interface ITokenFactory
|
||||||
|
{
|
||||||
|
string GenerateToken(int size= 32);
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
using System;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Security.Principal;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using static BlueWest.WebApi.Context.Users.Constants;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
public class JwtFactory : IJwtFactory
|
||||||
|
{
|
||||||
|
private readonly IJwtTokenHandler _jwtTokenHandler;
|
||||||
|
private readonly JwtIssuerOptions _jwtOptions;
|
||||||
|
|
||||||
|
public JwtFactory(IJwtTokenHandler jwtTokenHandler, IOptions<JwtIssuerOptions> jwtOptions)
|
||||||
|
{
|
||||||
|
_jwtTokenHandler = jwtTokenHandler;
|
||||||
|
_jwtOptions = jwtOptions.Value;
|
||||||
|
ThrowIfInvalidOptions(_jwtOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AccessToken> GenerateEncodedToken(string id, string userName)
|
||||||
|
{
|
||||||
|
var identity = GenerateClaimsIdentity(id, userName);
|
||||||
|
|
||||||
|
var claims = new[]
|
||||||
|
{
|
||||||
|
new Claim(JwtRegisteredClaimNames.Sub, userName),
|
||||||
|
new Claim(JwtRegisteredClaimNames.Jti, await _jwtOptions.JtiGenerator()),
|
||||||
|
new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(_jwtOptions.IssuedAt).ToString(),
|
||||||
|
ClaimValueTypes.Integer64),
|
||||||
|
identity.FindFirst(JwtClaimIdentifiers.Rol),
|
||||||
|
identity.FindFirst(JwtClaimIdentifiers.Id)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the JWT security token and encode it.
|
||||||
|
var jwt = new JwtSecurityToken(
|
||||||
|
_jwtOptions.Issuer,
|
||||||
|
_jwtOptions.Audience,
|
||||||
|
claims,
|
||||||
|
_jwtOptions.NotBefore,
|
||||||
|
_jwtOptions.Expiration,
|
||||||
|
_jwtOptions.SigningCredentials);
|
||||||
|
|
||||||
|
return new AccessToken(_jwtTokenHandler.WriteToken(jwt), (int)_jwtOptions.ValidFor.TotalSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ClaimsIdentity GenerateClaimsIdentity(string id, string userName)
|
||||||
|
{
|
||||||
|
return new ClaimsIdentity(new GenericIdentity(userName, "Token"), new[]
|
||||||
|
{
|
||||||
|
new Claim(JwtClaimIdentifiers.Id, id),
|
||||||
|
new Claim(JwtClaimIdentifiers.Rol, JwtClaims.ApiAccess)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <returns>Date converted to seconds since Unix epoch (Jan 1, 1970, midnight UTC).</returns>
|
||||||
|
private static long ToUnixEpochDate(DateTime date)
|
||||||
|
=> (long)Math.Round((date.ToUniversalTime() -
|
||||||
|
new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero))
|
||||||
|
.TotalSeconds);
|
||||||
|
|
||||||
|
private static void ThrowIfInvalidOptions(JwtIssuerOptions options)
|
||||||
|
{
|
||||||
|
if (options == null) throw new ArgumentNullException(nameof(options));
|
||||||
|
|
||||||
|
if (options.ValidFor <= TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Must be a non-zero TimeSpan.", nameof(JwtIssuerOptions.ValidFor));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.SigningCredentials == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(JwtIssuerOptions.SigningCredentials));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.JtiGenerator == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(JwtIssuerOptions.JtiGenerator));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
public class JwtIssuerOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 4.1.1. "iss" (Issuer) Claim - The "iss" (issuer) claim identifies the principal that issued the JWT.
|
||||||
|
/// </summary>
|
||||||
|
public string Issuer { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 4.1.2. "sub" (Subject) Claim - The "sub" (subject) claim identifies the principal that is the subject of the JWT.
|
||||||
|
/// </summary>
|
||||||
|
public string Subject { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 4.1.3. "aud" (Audience) Claim - The "aud" (audience) claim identifies the recipients that the JWT is intended for.
|
||||||
|
/// </summary>
|
||||||
|
public string Audience { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 4.1.4. "exp" (Expiration Time) Claim - The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime Expiration => IssuedAt.Add(ValidFor);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 4.1.5. "nbf" (Not Before) Claim - The "nbf" (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime NotBefore => DateTime.UtcNow;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 4.1.6. "iat" (Issued At) Claim - The "iat" (issued at) claim identifies the time at which the JWT was issued.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime IssuedAt => DateTime.UtcNow;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the timespan the token will be valid for (default is 120 min)
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan ValidFor { get; set; } = TimeSpan.FromMinutes(120);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "jti" (JWT ID) Claim (default ID is a GUID)
|
||||||
|
/// </summary>
|
||||||
|
public Func<Task<string>> JtiGenerator =>
|
||||||
|
() => Task.FromResult(Guid.NewGuid().ToString());
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The signing key to use when generating tokens.
|
||||||
|
/// </summary>
|
||||||
|
public SigningCredentials SigningCredentials { get; set; }
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
using System;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
public class JwtTokenHandler : IJwtTokenHandler
|
||||||
|
{
|
||||||
|
private readonly JwtSecurityTokenHandler _jwtSecurityTokenHandler;
|
||||||
|
|
||||||
|
public JwtTokenHandler()
|
||||||
|
{
|
||||||
|
if (_jwtSecurityTokenHandler == null)
|
||||||
|
_jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string WriteToken(JwtSecurityToken jwt)
|
||||||
|
{
|
||||||
|
return _jwtSecurityTokenHandler.WriteToken(jwt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClaimsPrincipal ValidateToken(string token, TokenValidationParameters tokenValidationParameters)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var principal = _jwtSecurityTokenHandler.ValidateToken(token, tokenValidationParameters, out var securityToken);
|
||||||
|
|
||||||
|
if (!(securityToken is JwtSecurityToken jwtSecurityToken) || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
throw new SecurityTokenException("Invalid token");
|
||||||
|
|
||||||
|
return principal;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users
|
||||||
|
{
|
||||||
|
// from: https://github.com/dotnet/aspnetcore/tree/main/src/Identity/samples/IdentitySample.Mvc/Models/AccountViewModels
|
||||||
|
/// <summary>
|
||||||
|
/// Login View Model
|
||||||
|
/// </summary>
|
||||||
|
public class LoginViewModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Email
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[EmailAddress]
|
||||||
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Password
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[DataType(DataType.Password)]
|
||||||
|
public string Password { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RememberMe
|
||||||
|
/// </summary>
|
||||||
|
[Display(Name = "Remember me?")]
|
||||||
|
public bool RememberMe { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public class RegisterViewModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Email
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[EmailAddress]
|
||||||
|
[Display(Name = "Email")]
|
||||||
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Password
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
|
||||||
|
[DataType(DataType.Password)]
|
||||||
|
[Display(Name = "Password")]
|
||||||
|
public string Password { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ConfirmPassword
|
||||||
|
/// </summary>
|
||||||
|
[DataType(DataType.Password)]
|
||||||
|
[Display(Name = "Confirm password")]
|
||||||
|
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
|
||||||
|
public string ConfirmPassword { get; set; }
|
||||||
|
|
||||||
|
public ApplicationUser ToUser()
|
||||||
|
{
|
||||||
|
var newUser = new ApplicationUser();
|
||||||
|
newUser.Email = Email;
|
||||||
|
newUser.PasswordHash = Password;
|
||||||
|
return newUser;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
public class ResetPasswordViewModel
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[EmailAddress]
|
||||||
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
|
||||||
|
[DataType(DataType.Password)]
|
||||||
|
public string Password { get; set; }
|
||||||
|
|
||||||
|
[DataType(DataType.Password)]
|
||||||
|
[Display(Name = "Confirm password")]
|
||||||
|
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
|
||||||
|
public string ConfirmPassword { get; set; }
|
||||||
|
|
||||||
|
public string Code { get; set; }
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public class ApplicationRole : IdentityRole<string>
|
||||||
|
{ }
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
using System;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public class ApplicationRoleClaim : IdentityRoleClaim<string>
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public class ApplicationUserClaim : IdentityUserClaim<string>
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public class ApplicationUserLogin : IdentityUserLogin<string> { }
|
|
@ -0,0 +1,7 @@
|
||||||
|
using System;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public class ApplicationUserRole : IdentityUserRole<string> { }
|
|
@ -0,0 +1,6 @@
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public class ApplicationUserToken : IdentityUserToken<string> { }
|
|
@ -0,0 +1,18 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
public class RoleManager : RoleManager<ApplicationRole>
|
||||||
|
{
|
||||||
|
public RoleManager(
|
||||||
|
IRoleStore<ApplicationRole> store,
|
||||||
|
IEnumerable<IRoleValidator<ApplicationRole>> roleValidators,
|
||||||
|
ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors,
|
||||||
|
ILogger<RoleManager<ApplicationRole>> logger) : base(store, roleValidators, keyNormalizer, errors, logger)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Role storage management
|
||||||
|
/// </summary>
|
||||||
|
public class RoleStore : IRoleStore<ApplicationRole>
|
||||||
|
{
|
||||||
|
private ApplicationUserDbContext _dbContext;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Role Store constructor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dbContext"></param>
|
||||||
|
public RoleStore(ApplicationUserDbContext dbContext)
|
||||||
|
{
|
||||||
|
_dbContext = dbContext;
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="NotImplementedException"></exception>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_dbContext = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get role name
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="role"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="NotImplementedException"></exception>
|
||||||
|
public async Task<string> GetRoleNameAsync(ApplicationUserRole role, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var foundRole = await _dbContext.Roles
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == role.RoleId, cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
if (foundRole != null)
|
||||||
|
{
|
||||||
|
return foundRole.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<IdentityResult> CreateAsync(ApplicationRole role, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_dbContext.Roles.Add(role);
|
||||||
|
return await _dbContext.SaveChangesAsync(cancellationToken) >= 0 ? IdentityResult.Success : IdentityResult.Failed();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IdentityResult> UpdateAsync(ApplicationRole role, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_dbContext.Roles.Update(role);
|
||||||
|
return await _dbContext.SaveChangesAsync(cancellationToken) >= 0 ? IdentityResult.Success : IdentityResult.Failed();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IdentityResult> DeleteAsync(ApplicationRole role, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_dbContext.Roles.Remove(role);
|
||||||
|
return await _dbContext.SaveChangesAsync(cancellationToken) >= 0 ? IdentityResult.Success : IdentityResult.Failed();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetRoleIdAsync(ApplicationRole role, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var x = await _dbContext.Roles.FirstOrDefaultAsync(x => x.Id == role.Id, cancellationToken: cancellationToken);
|
||||||
|
if (x != null)
|
||||||
|
{
|
||||||
|
return x.Id;
|
||||||
|
}
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<string> GetRoleNameAsync(ApplicationRole role, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task SetRoleNameAsync(ApplicationRole role, string roleName, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<string> GetNormalizedRoleNameAsync(ApplicationRole role, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task SetNormalizedRoleNameAsync(ApplicationRole role, string normalizedName, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<ApplicationRole> FindByIdAsync(string roleId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<ApplicationRole> FindByNameAsync(string normalizedRoleName, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,155 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BlueWest.Cryptography;
|
||||||
|
using BlueWest.Data;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
public class UserManager : UserManager<ApplicationUser>, IUserManager
|
||||||
|
{
|
||||||
|
private readonly IHasher _hasher;
|
||||||
|
private readonly IUsersRepo _usersRepo;
|
||||||
|
public UserManager(IUsersRepo store, IOptions<IdentityOptions> optionsAccessor,
|
||||||
|
IHasher passwordHasher, IEnumerable<IUserValidator<ApplicationUser>> userValidators,
|
||||||
|
IEnumerable<IPasswordValidator<ApplicationUser>> passwordValidators, ILookupNormalizer keyNormalizer,
|
||||||
|
IdentityErrorDescriber errors, IServiceProvider services, ILogger<UserManager<ApplicationUser>> logger) : base(store,
|
||||||
|
optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services,
|
||||||
|
logger)
|
||||||
|
{
|
||||||
|
_hasher = passwordHasher;
|
||||||
|
_usersRepo = store;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<IdentityResult> CreateAsync(ApplicationUser user)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
var result = await ValidateUserAsync(user);
|
||||||
|
if (!result.Succeeded)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (Options.Lockout.AllowedForNewUsers && SupportsUserLockout)
|
||||||
|
{
|
||||||
|
// await GetUserLockoutStore().SetLockoutEnabledAsync(user, true, CancellationToken);
|
||||||
|
}
|
||||||
|
await UpdateNormalizedUserNameAsync(user);
|
||||||
|
await UpdateNormalizedEmailAsync(user);
|
||||||
|
|
||||||
|
return await _usersRepo.CreateAsync(user, CancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<bool> CheckPasswordAsync(ApplicationUser user, string password)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
var passwordStore = GetPasswordStore();
|
||||||
|
|
||||||
|
var result = await VerifyPasswordAsync(passwordStore, user, password);
|
||||||
|
if (result == PasswordVerificationResult.SuccessRehashNeeded)
|
||||||
|
{
|
||||||
|
//Remove the IPasswordStore parameter so we can call the protected, not private, method
|
||||||
|
await UpdatePasswordHash(user, password, validatePassword: false);
|
||||||
|
await UpdateUserAsync(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
var success = result != PasswordVerificationResult.Failed;
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
var userId = user != null ? GetUserIdAsync(user).Result : "(null)";
|
||||||
|
Logger.LogWarning(0, "Invalid password for user {userId}.", userId);
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<PasswordVerificationResult> VerifyPasswordAsync(IUserPasswordStore<ApplicationUser> store, ApplicationUser user, string password)
|
||||||
|
{
|
||||||
|
string existingHash;
|
||||||
|
|
||||||
|
if (user != null)
|
||||||
|
existingHash = await store.GetPasswordHashAsync(user, CancellationToken);
|
||||||
|
else
|
||||||
|
existingHash = "not a real hash";
|
||||||
|
|
||||||
|
if (existingHash == null)
|
||||||
|
{
|
||||||
|
return PasswordVerificationResult.Failed;
|
||||||
|
}
|
||||||
|
return PasswordHasher.VerifyHashedPassword(user, existingHash, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<ApplicationUser> FindByNameAsync(string userName)
|
||||||
|
{
|
||||||
|
if (userName == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(userName));
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplicationUser user;
|
||||||
|
|
||||||
|
if (Store is IUsersRepo repo)
|
||||||
|
{
|
||||||
|
user = await repo.FindByNameAsync(userName, CancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
userName = NormalizeName(userName);
|
||||||
|
user = await Store.FindByNameAsync(userName, CancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public override async Task<IdentityResult> ChangePasswordAsync(ApplicationUser user, string currentPassword, string newPassword)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
var passwordStore = GetPasswordStore();
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await VerifyPasswordAsync(passwordStore, user, currentPassword) != PasswordVerificationResult.Failed)
|
||||||
|
{
|
||||||
|
var result = await UpdatePasswordHash(user, newPassword, validatePassword: false);
|
||||||
|
if (!result.Succeeded)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await UpdateUserAsync(user);
|
||||||
|
}
|
||||||
|
Logger.LogWarning(2, "Change password failed for user {userId}.", await GetUserIdAsync(user));
|
||||||
|
return IdentityResult.Failed(ErrorDescriber.PasswordMismatch());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private IUserPasswordStore<ApplicationUser> GetPasswordStore()
|
||||||
|
{
|
||||||
|
if (Store is IUserPasswordStore<ApplicationUser> passwordStore)
|
||||||
|
{
|
||||||
|
return passwordStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<ApplicationUser> FindByEmailAsync(string email)
|
||||||
|
{
|
||||||
|
ApplicationUser user = null;
|
||||||
|
|
||||||
|
if (Store is IUsersRepo repo)
|
||||||
|
{
|
||||||
|
user = await repo.FindByEmailAsync(email, CancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
user = await Store.FindByNameAsync(email, CancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,225 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BlueWest.Data;
|
||||||
|
using BlueWest.WebApi.EF;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace BlueWest.WebApi.Context.Users;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Users Repository
|
||||||
|
/// </summary>
|
||||||
|
public class UserRepository : UserStore<ApplicationUser, ApplicationRole, ApplicationUserDbContext>, IUsersRepo
|
||||||
|
{
|
||||||
|
private readonly ApplicationUserDbContext _context;
|
||||||
|
|
||||||
|
public UserRepository(ApplicationUserDbContext context, IdentityErrorDescriber describer = null) : base(context, describer)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get Application Users
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<IEnumerable<ApplicationUser>> GetUsers()
|
||||||
|
{
|
||||||
|
var users = await _context.Users.ToListAsync();
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create Application User
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user"></param>
|
||||||
|
public async Task CreateUser(ApplicationUser user)
|
||||||
|
{
|
||||||
|
await CreateAsync(user, CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Save Changes
|
||||||
|
/// </summary>
|
||||||
|
public async Task SaveChanges()
|
||||||
|
{
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> SaveChanges(ApplicationUser user)
|
||||||
|
{
|
||||||
|
_context.Users.Update(user);
|
||||||
|
return await _context.SaveChangesAsync() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dispose repository
|
||||||
|
/// </summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_context.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<string> GetUserIdAsync(ApplicationUser user, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return Task.FromCanceled<string>(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(user.Id.ToString());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<string> GetUserNameAsync(ApplicationUser user, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return Task.FromCanceled<string>(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(user.UserName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override async Task SetUserNameAsync(ApplicationUser user, string userName, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var foundUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == user.Id, cancellationToken: cancellationToken);
|
||||||
|
if (foundUser == null) return;
|
||||||
|
foundUser.UserName = userName;
|
||||||
|
await SaveChanges(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<string> GetNormalizedUserNameAsync(ApplicationUser user, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return Task.FromCanceled<string>(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(user.NormalizedUserName);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override async Task<IdentityResult> CreateAsync(ApplicationUser user, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var u = await _context.AddAsync(user, cancellationToken);
|
||||||
|
|
||||||
|
if(u.State == EntityState.Added)
|
||||||
|
{
|
||||||
|
await SaveChanges();
|
||||||
|
return IdentityResult.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
return IdentityResult.Failed();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override async Task<IdentityResult> UpdateAsync(ApplicationUser user, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_context.Users.Update(user);
|
||||||
|
|
||||||
|
var success = await _context.SaveChangesAsync(cancellationToken) > 0;
|
||||||
|
|
||||||
|
if (success) return IdentityResult.Success;
|
||||||
|
|
||||||
|
return IdentityResult.Failed();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override async Task<IdentityResult> DeleteAsync(ApplicationUser user, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var foundUser = await _context.Users.FirstOrDefaultAsync(x=> x.Id == user.Id, cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
var error = new IdentityError {Description = "ApplicationUser Not found"};
|
||||||
|
|
||||||
|
if (foundUser == null) return IdentityResult.Failed(error);
|
||||||
|
|
||||||
|
_context.Users.Remove(foundUser);
|
||||||
|
|
||||||
|
return IdentityResult.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<ApplicationUser> GetUserById(string id)
|
||||||
|
{
|
||||||
|
var db = _context.Users;
|
||||||
|
var user = await db.FirstOrDefaultAsync(u => u.Id.ToString() == id);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<string> GetPasswordHashAsync(ApplicationUser user, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return Task.FromCanceled<string>(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(user.PasswordHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<bool> HasPasswordAsync(ApplicationUser user, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return Task.FromCanceled<bool>(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(!string.IsNullOrEmpty(user.PasswordHash));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<string> GetEmailAsync(ApplicationUser user, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return Task.FromCanceled<string>(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(user.Email);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<bool> GetEmailConfirmedAsync(ApplicationUser user, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return Task.FromCanceled<bool>(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(user.EmailConfirmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override async Task<ApplicationUser> FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ApplicationUser user = null;
|
||||||
|
var db = _context.Users;
|
||||||
|
user = await db.FirstOrDefaultAsync(u => u.NormalizedEmail == normalizedEmail, cancellationToken: cancellationToken);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<string> GetNormalizedEmailAsync(ApplicationUser user, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
base.GetNormalizedEmailAsync(user, cancellationToken);
|
||||||
|
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return Task.FromCanceled<string>(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(user.NormalizedEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,2 +1,2 @@
|
||||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
<s:Boolean x:Key="/Default/CodeEditing/SuppressNullableWarningFix/Enabled/@EntryValue">False</s:Boolean></wpf:ResourceDictionary>
|
<s:Boolean x:Key="/Default/CodeEditing/SuppressUninitializedWarningFix/Enabled/@EntryValue">False</s:Boolean></wpf:ResourceDictionary>
|
|
@ -103,8 +103,8 @@ namespace BlueWest.Core
|
||||||
|
|
||||||
private static void MkdirCommand(string? input)
|
private static void MkdirCommand(string? input)
|
||||||
{
|
{
|
||||||
var argument = input.Split(" ");
|
var argument = input?.Split(" ");
|
||||||
if (argument.Length > 1 && argument[0] != "")
|
if (argument != null && argument.Length > 1 && argument[0] != "")
|
||||||
{
|
{
|
||||||
PathUtils.CreateDirectory(argument[1]);
|
PathUtils.CreateDirectory(argument[1]);
|
||||||
return;
|
return;
|
||||||
|
@ -127,17 +127,18 @@ namespace BlueWest.Core
|
||||||
|
|
||||||
private static void LsCommand(string? input)
|
private static void LsCommand(string? input)
|
||||||
{
|
{
|
||||||
var split = input.Split(" ");
|
var split = input?.Split(" ");
|
||||||
|
|
||||||
foreach (var name in split)
|
if (split != null)
|
||||||
{
|
foreach (var name in split)
|
||||||
if (name == "ls") continue;
|
{
|
||||||
if (string.IsNullOrWhiteSpace(name)) continue;
|
if (name == "ls") continue;
|
||||||
var assPath = AssemblyUtils.GetAssemblyPath();
|
if (string.IsNullOrWhiteSpace(name)) continue;
|
||||||
var fp = Path.GetFullPath(name);
|
var assPath = AssemblyUtils.GetAssemblyPath();
|
||||||
InternalLog(PathUtils.ListAll(fp));
|
var fp = Path.GetFullPath(name);
|
||||||
return;
|
InternalLog(PathUtils.ListAll(fp));
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var pathFiles = PathUtils.ListAssemblyPathFiles();
|
var pathFiles = PathUtils.ListAssemblyPathFiles();
|
||||||
|
|
||||||
|
|
|
@ -88,7 +88,9 @@ namespace BlueWest.Tools
|
||||||
{
|
{
|
||||||
var eventListener = list[i];
|
var eventListener = list[i];
|
||||||
var casted = eventListener as EventListener<TEvent>;
|
var casted = eventListener as EventListener<TEvent>;
|
||||||
|
#pragma warning disable CS8602
|
||||||
casted.OnEvent( newEvent );
|
casted.OnEvent( newEvent );
|
||||||
|
#pragma warning restore CS8602
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,9 +102,7 @@ namespace BlueWest.Tools
|
||||||
/// <param name="receiver">Receiver.</param>
|
/// <param name="receiver">Receiver.</param>
|
||||||
private bool SubscriptionExists( Type type, EventListenerBase receiver )
|
private bool SubscriptionExists( Type type, EventListenerBase receiver )
|
||||||
{
|
{
|
||||||
List<EventListenerBase> receivers;
|
if( !_subscribersList.TryGetValue( type, out var receivers ) ) return false;
|
||||||
|
|
||||||
if( !_subscribersList.TryGetValue( type, out receivers ) ) return false;
|
|
||||||
|
|
||||||
bool exists = false;
|
bool exists = false;
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace BlueWest.Tools
|
namespace BlueWest.Tools
|
||||||
{
|
{
|
||||||
public struct EventManagerAsync
|
public struct EventManagerAsync
|
||||||
{
|
{
|
||||||
private static readonly Dictionary<Type, List<EventListenerBaseAsync>> _subscribersList;
|
private static readonly ConcurrentDictionary<Type, List<EventListenerBaseAsync>> _subscribersList;
|
||||||
|
|
||||||
static EventManagerAsync()
|
static EventManagerAsync()
|
||||||
{
|
{
|
||||||
_subscribersList = new Dictionary<Type, List<EventListenerBaseAsync>>(12412);
|
_subscribersList = new ConcurrentDictionary<Type, List<EventListenerBaseAsync>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <suary>
|
/// <suary>
|
||||||
|
@ -58,7 +59,7 @@ namespace BlueWest.Tools
|
||||||
subscriberList.Remove(subscriberList[i]);
|
subscriberList.Remove(subscriberList[i]);
|
||||||
|
|
||||||
if (subscriberList.Count == 0)
|
if (subscriberList.Count == 0)
|
||||||
_subscribersList.Remove(eventType);
|
_subscribersList.Remove(eventType, out subscriberList);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,10 +116,12 @@ namespace BlueWest.Core.ComponentSystem
|
||||||
}
|
}
|
||||||
public void AddComponent<T>() where T : Component
|
public void AddComponent<T>() where T : Component
|
||||||
{
|
{
|
||||||
var component = Activator.CreateInstance(typeof(T), _eventManager) as Component;
|
if (Activator.CreateInstance(typeof(T), _eventManager) is Component component)
|
||||||
_components.Add(component);
|
{
|
||||||
_componentsCount += 1;
|
_components.Add(component);
|
||||||
component.Start();
|
_componentsCount += 1;
|
||||||
|
component.Start();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ public class DisabledArtefact
|
||||||
{
|
{
|
||||||
|
|
||||||
protected internal ArtefactFrequency Frequency;
|
protected internal ArtefactFrequency Frequency;
|
||||||
protected EventManager _eventManager;
|
protected EventManager _eventManager = null!;
|
||||||
|
|
||||||
protected virtual void Update(double delta)
|
protected virtual void Update(double delta)
|
||||||
{
|
{
|
||||||
|
|
|
@ -59,9 +59,15 @@ namespace BlueWest.Core
|
||||||
{
|
{
|
||||||
SpawnThread(() =>
|
SpawnThread(() =>
|
||||||
{
|
{
|
||||||
object instantiatiable = Activator.CreateInstance(type);
|
#pragma warning disable CS8600
|
||||||
Artefact behavior = instantiatiable as Artefact;
|
object instantiate = Activator.CreateInstance(type);
|
||||||
|
#pragma warning restore CS8600
|
||||||
|
#pragma warning disable CS8600
|
||||||
|
Artefact behavior = instantiate as Artefact;
|
||||||
|
#pragma warning restore CS8600
|
||||||
|
#pragma warning disable CS8602
|
||||||
behavior.SetupEntity(_eventManager);
|
behavior.SetupEntity(_eventManager);
|
||||||
|
#pragma warning restore CS8602
|
||||||
behavior.RunLoop();
|
behavior.RunLoop();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ namespace BlueWest.DataAgent
|
||||||
|
|
||||||
foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(source))
|
foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(source))
|
||||||
{
|
{
|
||||||
object value = property.GetValue(source).ToString();
|
object? value = property.GetValue(source)?.ToString();
|
||||||
|
|
||||||
if (value is Dictionary<string, string>.KeyCollection keyCollection)
|
if (value is Dictionary<string, string>.KeyCollection keyCollection)
|
||||||
{
|
{
|
||||||
|
|
|
@ -9,8 +9,6 @@ namespace BlueWest.Core
|
||||||
{
|
{
|
||||||
public static class MappingExtensions
|
public static class MappingExtensions
|
||||||
{
|
{
|
||||||
private static Type _currentType = null;
|
|
||||||
|
|
||||||
private static bool IsMatchingProperty(ref Type type, string propertyName)
|
private static bool IsMatchingProperty(ref Type type, string propertyName)
|
||||||
{
|
{
|
||||||
var typeProperties = type.GetProperties();
|
var typeProperties = type.GetProperties();
|
||||||
|
|
|
@ -7,7 +7,7 @@ namespace PerformanceSolution.Tools
|
||||||
{
|
{
|
||||||
public static class JsonTools
|
public static class JsonTools
|
||||||
{
|
{
|
||||||
public static Dictionary<string, object> ConvertFromObjectToDictionary(object arg)
|
public static Dictionary<string, object?> ConvertFromObjectToDictionary(object arg)
|
||||||
{
|
{
|
||||||
var properties = arg.GetType().GetProperties();
|
var properties = arg.GetType().GetProperties();
|
||||||
|
|
||||||
|
|
|
@ -1,149 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections;
|
|
||||||
using System.IO;
|
|
||||||
using System.Runtime.Serialization.Formatters.Binary;
|
|
||||||
using Directory = System.IO.Directory;
|
|
||||||
using File = System.IO.File;
|
|
||||||
|
|
||||||
|
|
||||||
namespace PerformanceSolution.Tools
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Allows the save and load of objects in a specific folder and file.
|
|
||||||
/// </summary>
|
|
||||||
public static class SaveLoadManager
|
|
||||||
{
|
|
||||||
private static readonly string _baseFolderName = Path.DirectorySeparatorChar + "BMData";
|
|
||||||
private const string _defaultFolderName = "BlueWest";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determines the save path to use when loading and saving a file based on a folder name.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The save path.</returns>
|
|
||||||
/// <param name="folderName">Folder name.</param>
|
|
||||||
static string DetermineSavePath(string folderName = _defaultFolderName)
|
|
||||||
{
|
|
||||||
string savePath;
|
|
||||||
// depending on the device we're on, we assemble the path
|
|
||||||
|
|
||||||
//savePath = OS.GetUserDataDir() + "/";
|
|
||||||
|
|
||||||
savePath = Path.Combine(Environment.GetFolderPath(
|
|
||||||
Environment.SpecialFolder.ApplicationData));
|
|
||||||
|
|
||||||
// #if UNITY_EDITOR
|
|
||||||
// savePath = Application.dataPath + _baseFolderName;
|
|
||||||
// #endif
|
|
||||||
|
|
||||||
var pathSeparator = Path.DirectorySeparatorChar;
|
|
||||||
|
|
||||||
savePath = savePath + pathSeparator + folderName + pathSeparator;
|
|
||||||
return savePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determines the name of the file to save
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The save file name.</returns>
|
|
||||||
/// <param name="fileName">File name.</param>
|
|
||||||
static string DetermineSaveFileName(string fileName)
|
|
||||||
{
|
|
||||||
return fileName + ".binary";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Save the specified saveObject, fileName and foldername into a file on disk.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="saveObject">Save object.</param>
|
|
||||||
/// <param name="fileName">File name.</param>
|
|
||||||
/// <param name="foldername">Foldername.</param>
|
|
||||||
public static void Save<T>(T saveObject, string fileName, string foldername = _defaultFolderName)
|
|
||||||
where T : class
|
|
||||||
{
|
|
||||||
string savePath = DetermineSavePath(foldername);
|
|
||||||
string saveFileName = DetermineSaveFileName(fileName);
|
|
||||||
// if the directory doesn't already exist, we create it
|
|
||||||
if (!Directory.Exists(savePath))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(savePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// we serialize and write our object into a file on disk
|
|
||||||
//var byteData = MessagePackSerializer.Serialize(saveObject);
|
|
||||||
|
|
||||||
|
|
||||||
File.WriteAllText(savePath + saveFileName + ".json", saveObject.ToString());
|
|
||||||
|
|
||||||
//File.WriteAllBytes(savePath + saveFileName, byteData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Load the specified file based on a file name into a specified folder
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="fileName">File name.</param>
|
|
||||||
/// <param name="foldername">Foldername.</param>
|
|
||||||
public static T Load<T>(string fileName, string foldername = _defaultFolderName) where T : class
|
|
||||||
{
|
|
||||||
string savePath = DetermineSavePath(foldername);
|
|
||||||
string saveFileName = savePath + DetermineSaveFileName(fileName);
|
|
||||||
|
|
||||||
object returnObject;
|
|
||||||
|
|
||||||
// if the MMSaves directory or the save file doesn't exist, there's nothing to load, we do nothing and exit
|
|
||||||
if (!Directory.Exists(savePath) || !File.Exists(saveFileName))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] readByte = File.ReadAllBytes(saveFileName);
|
|
||||||
|
|
||||||
//var finalObject = MessagePackSerializer.Deserialize<T>(readByte);
|
|
||||||
|
|
||||||
//return finalObject;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes a save from disk
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="fileName">File name.</param>
|
|
||||||
/// <param name="folderName">Folder name.</param>
|
|
||||||
public static void DeleteSave(string fileName, string folderName = _defaultFolderName)
|
|
||||||
{
|
|
||||||
string savePath = DetermineSavePath(folderName);
|
|
||||||
string saveFileName = DetermineSaveFileName(fileName);
|
|
||||||
if (File.Exists(savePath + saveFileName))
|
|
||||||
{
|
|
||||||
File.Delete(savePath + saveFileName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void DeleteSaveFolder(string folderName = _defaultFolderName)
|
|
||||||
{
|
|
||||||
string savePath = DetermineSavePath(folderName);
|
|
||||||
if (Directory.Exists(savePath))
|
|
||||||
{
|
|
||||||
DeleteDirectory(savePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void DeleteDirectory(string target_dir)
|
|
||||||
{
|
|
||||||
string[] files = Directory.GetFiles(target_dir);
|
|
||||||
string[] dirs = Directory.GetDirectories(target_dir);
|
|
||||||
|
|
||||||
foreach (string file in files)
|
|
||||||
{
|
|
||||||
File.SetAttributes(file, FileAttributes.Normal);
|
|
||||||
File.Delete(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (string dir in dirs)
|
|
||||||
{
|
|
||||||
DeleteDirectory(dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
Directory.Delete(target_dir, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue