Add Users code

This commit is contained in:
CodeLiturgy 2022-09-09 22:33:17 +01:00
parent f46d20c1ea
commit 2a404c3560
52 changed files with 1850 additions and 185 deletions

View File

@ -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" />

View File

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

View File

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

View File

@ -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;
@ -16,6 +17,7 @@ namespace BlueWest.WebApi.EF
/// </summary> /// </summary>
public DbSet<User> Users { get; set; } public DbSet<User> Users { get; set; }
/// <summary> /// <summary>
/// App configuration. /// App configuration.
/// </summary> /// </summary>

View File

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

View File

@ -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)]

View File

@ -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 =>
{ {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
namespace BlueWest.WebApi.Context.Users;
public class AuthSettings
{
public string SecretKey { get; set; }
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
using System.Threading.Tasks;
namespace BlueWest.WebApi.Context.Users;
public interface IJwtFactory
{
Task<AccessToken> GenerateEncodedToken(string id, string userName);
}

View File

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

View File

@ -0,0 +1,6 @@
namespace BlueWest.WebApi.Context.Users;
public interface ITokenFactory
{
string GenerateToken(int size= 32);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Identity;
namespace BlueWest.WebApi.Context.Users
{
/// <inheritdoc />
public class ApplicationRole : IdentityRole<string>
{ }
}

View File

@ -0,0 +1,10 @@
using System;
using Microsoft.AspNetCore.Identity;
namespace BlueWest.WebApi.Context.Users;
/// <inheritdoc />
public class ApplicationRoleClaim : IdentityRoleClaim<string>
{
}

View File

@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Identity;
namespace BlueWest.WebApi.Context.Users;
/// <inheritdoc />
public class ApplicationUserClaim : IdentityUserClaim<string>
{
}

View File

@ -0,0 +1,6 @@
using Microsoft.AspNetCore.Identity;
namespace BlueWest.WebApi.Context.Users;
/// <inheritdoc />
public class ApplicationUserLogin : IdentityUserLogin<string> { }

View File

@ -0,0 +1,7 @@
using System;
using Microsoft.AspNetCore.Identity;
namespace BlueWest.WebApi.Context.Users;
/// <inheritdoc />
public class ApplicationUserRole : IdentityUserRole<string> { }

View File

@ -0,0 +1,6 @@
using Microsoft.AspNetCore.Identity;
namespace BlueWest.WebApi.Context.Users;
/// <inheritdoc />
public class ApplicationUserToken : IdentityUserToken<string> { }

View File

@ -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)
{
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)
{ {

View File

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

View File

@ -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)
{ {

View File

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

View File

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

View File

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