From 2a404c3560237bd5653b407b65e5619ffad7671a Mon Sep 17 00:00:00 2001 From: CodeLiturgy Date: Fri, 9 Sep 2022 22:33:17 +0100 Subject: [PATCH] Add Users code --- BlueWest.Api/BlueWest.Api.csproj | 3 + .../ModelBuilderCountryDbContextExtensions.cs | 3 + .../Context/Templates/GetManyTemplate.csx | 2 +- BlueWest.Api/Context/UserDbContext.cs | 2 + BlueWest.Api/Controllers/AuthController.cs | 94 ++++++++ .../Controllers/CurrencyController.cs | 5 +- BlueWest.Api/Startup.cs | 39 ++- BlueWest.Api/StartupExtensions.cs | 148 +++++++++++- BlueWest.Api/Users/ApplicationUser.cs | 101 ++++++++ .../Users/ApplicationUserDbContext.cs | 109 +++++++++ BlueWest.Api/Users/Auth/AccessToken.cs | 15 ++ BlueWest.Api/Users/Auth/AuthManager.cs | 80 +++++++ BlueWest.Api/Users/Auth/AuthSettings.cs | 6 + BlueWest.Api/Users/Auth/IAuthManager.cs | 15 ++ BlueWest.Api/Users/Auth/SignInManager.cs | 36 +++ BlueWest.Api/Users/Constants.cs | 17 ++ BlueWest.Api/Users/Crypto/BaseCryptoItem.cs | 87 +++++++ BlueWest.Api/Users/Crypto/Hasher.cs | 88 +++++++ BlueWest.Api/Users/Crypto/IHasher.cs | 12 + BlueWest.Api/Users/Crypto/SHA_512.cs | 45 ++++ BlueWest.Api/Users/IUserManager.cs | 32 +++ BlueWest.Api/Users/IUsersRepo.cs | 23 ++ BlueWest.Api/Users/Jwt/IJwtFactory.cs | 8 + BlueWest.Api/Users/Jwt/IJwtTokenHandler.cs | 12 + BlueWest.Api/Users/Jwt/ITokenFactory.cs | 6 + BlueWest.Api/Users/Jwt/JwtFactory.cs | 83 +++++++ BlueWest.Api/Users/Jwt/JwtIssuerOptions.cs | 54 +++++ BlueWest.Api/Users/Jwt/JwtTokenHandler.cs | 39 +++ BlueWest.Api/Users/Models/LoginViewModel.cs | 32 +++ .../Users/Models/RegisterViewModel.cs | 42 ++++ .../Users/Models/ResetPasswordViewModel.cs | 22 ++ BlueWest.Api/Users/Roles/ApplicationRole.cs | 9 + .../Users/Roles/ApplicationRoleClaim.cs | 10 + .../Users/Roles/ApplicationUserClaim.cs | 9 + .../Users/Roles/ApplicationUserLogin.cs | 6 + .../Users/Roles/ApplicationUserRole.cs | 7 + .../Users/Roles/ApplicationUserToken.cs | 6 + BlueWest.Api/Users/Roles/RoleManager.cs | 18 ++ BlueWest.Api/Users/Roles/RoleStore.cs | 113 +++++++++ BlueWest.Api/Users/UserManager.cs | 155 ++++++++++++ BlueWest.Api/Users/UserRepository.cs | 225 ++++++++++++++++++ BlueWest.sln.DotSettings | 2 +- BlueWest/Artefacts/BlueConsole.cs | 25 +- BlueWest/Core/Events/EventManager.cs | 6 +- BlueWest/Core/Events/EventManagerAsync.cs | 7 +- BlueWest/Core/System/Artefact.cs | 10 +- BlueWest/Core/System/DisabledArtefact.cs | 2 +- BlueWest/Core/System/ThreadServer.cs | 10 +- BlueWest/Core/Tests/DataExtensions.cs | 2 +- BlueWest/Core/Tests/MappingExtensions.cs | 2 - BlueWest/Tools/JsonTools.cs | 2 +- BlueWest/Tools/SaveLoader.cs | 149 ------------ 52 files changed, 1850 insertions(+), 185 deletions(-) create mode 100644 BlueWest.Api/Controllers/AuthController.cs create mode 100644 BlueWest.Api/Users/ApplicationUser.cs create mode 100644 BlueWest.Api/Users/ApplicationUserDbContext.cs create mode 100644 BlueWest.Api/Users/Auth/AccessToken.cs create mode 100644 BlueWest.Api/Users/Auth/AuthManager.cs create mode 100644 BlueWest.Api/Users/Auth/AuthSettings.cs create mode 100644 BlueWest.Api/Users/Auth/IAuthManager.cs create mode 100644 BlueWest.Api/Users/Auth/SignInManager.cs create mode 100644 BlueWest.Api/Users/Constants.cs create mode 100644 BlueWest.Api/Users/Crypto/BaseCryptoItem.cs create mode 100644 BlueWest.Api/Users/Crypto/Hasher.cs create mode 100644 BlueWest.Api/Users/Crypto/IHasher.cs create mode 100644 BlueWest.Api/Users/Crypto/SHA_512.cs create mode 100644 BlueWest.Api/Users/IUserManager.cs create mode 100644 BlueWest.Api/Users/IUsersRepo.cs create mode 100644 BlueWest.Api/Users/Jwt/IJwtFactory.cs create mode 100644 BlueWest.Api/Users/Jwt/IJwtTokenHandler.cs create mode 100644 BlueWest.Api/Users/Jwt/ITokenFactory.cs create mode 100644 BlueWest.Api/Users/Jwt/JwtFactory.cs create mode 100644 BlueWest.Api/Users/Jwt/JwtIssuerOptions.cs create mode 100644 BlueWest.Api/Users/Jwt/JwtTokenHandler.cs create mode 100644 BlueWest.Api/Users/Models/LoginViewModel.cs create mode 100644 BlueWest.Api/Users/Models/RegisterViewModel.cs create mode 100644 BlueWest.Api/Users/Models/ResetPasswordViewModel.cs create mode 100644 BlueWest.Api/Users/Roles/ApplicationRole.cs create mode 100644 BlueWest.Api/Users/Roles/ApplicationRoleClaim.cs create mode 100644 BlueWest.Api/Users/Roles/ApplicationUserClaim.cs create mode 100644 BlueWest.Api/Users/Roles/ApplicationUserLogin.cs create mode 100644 BlueWest.Api/Users/Roles/ApplicationUserRole.cs create mode 100644 BlueWest.Api/Users/Roles/ApplicationUserToken.cs create mode 100644 BlueWest.Api/Users/Roles/RoleManager.cs create mode 100644 BlueWest.Api/Users/Roles/RoleStore.cs create mode 100644 BlueWest.Api/Users/UserManager.cs create mode 100644 BlueWest.Api/Users/UserRepository.cs delete mode 100644 BlueWest/Tools/SaveLoader.cs diff --git a/BlueWest.Api/BlueWest.Api.csproj b/BlueWest.Api/BlueWest.Api.csproj index 9fa919f..3c34cf8 100644 --- a/BlueWest.Api/BlueWest.Api.csproj +++ b/BlueWest.Api/BlueWest.Api.csproj @@ -12,8 +12,10 @@ + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -41,6 +43,7 @@ + diff --git a/BlueWest.Api/Context/Extensions/ModelBuilderCountryDbContextExtensions.cs b/BlueWest.Api/Context/Extensions/ModelBuilderCountryDbContextExtensions.cs index 87ac2d3..66bd784 100644 --- a/BlueWest.Api/Context/Extensions/ModelBuilderCountryDbContextExtensions.cs +++ b/BlueWest.Api/Context/Extensions/ModelBuilderCountryDbContextExtensions.cs @@ -4,6 +4,9 @@ using Microsoft.EntityFrameworkCore; namespace BlueWest.WebApi.Context.Extensions { + /// + /// Code first model builder + /// public static class ModelBuilderCountryDbContextExtensions { /// /// Setup the database model diff --git a/BlueWest.Api/Context/Templates/GetManyTemplate.csx b/BlueWest.Api/Context/Templates/GetManyTemplate.csx index a617c50..008d2ca 100644 --- a/BlueWest.Api/Context/Templates/GetManyTemplate.csx +++ b/BlueWest.Api/Context/Templates/GetManyTemplate.csx @@ -16,7 +16,7 @@ public static (bool, {returnTypeFullName}[]) Get{propertyName}(this {contextFull var query = dbContext .{propertyName} - .Select(x => new {returnTypeFullName}(x)); + .Select(x => new {returnTypeFullName}(x)) .Skip(skip) .Take(take); diff --git a/BlueWest.Api/Context/UserDbContext.cs b/BlueWest.Api/Context/UserDbContext.cs index 80a0f6f..fb806df 100644 --- a/BlueWest.Api/Context/UserDbContext.cs +++ b/BlueWest.Api/Context/UserDbContext.cs @@ -1,4 +1,5 @@ using BlueWest.Data; +using BlueWest.WebApi.Context.Users; using BlueWest.WebApi.EF.Model; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; @@ -15,6 +16,7 @@ namespace BlueWest.WebApi.EF /// Users entity. /// public DbSet Users { get; set; } + /// /// App configuration. diff --git a/BlueWest.Api/Controllers/AuthController.cs b/BlueWest.Api/Controllers/AuthController.cs new file mode 100644 index 0000000..c4b1f36 --- /dev/null +++ b/BlueWest.Api/Controllers/AuthController.cs @@ -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> SignupUserAsync(RegisterViewModel registerViewModel) + { + return await _authManager.CreateUserAsync(registerViewModel); + } + + + [AllowAnonymous] + [HttpPost("login")] + public async Task> GetTokenAsync(LoginViewModel loginViewModel) + { + var loginResultSucceded = await _authManager.GetToken(loginViewModel); + + if (loginResultSucceded != null) + { + return Ok(_mapper.Map(loginResultSucceded)); + + } + return Problem(); + } + + + [AllowAnonymous] + [HttpPost("login2")] + public async Task> 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> DoLogoutAsync(LoginViewModel loginDto) + { + await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + + + return Json(true); + } + + + [HttpGet("test")] + public ActionResult TestRequest() + { + return Ok(new {Message = "Test"}); + } + + + } \ No newline at end of file diff --git a/BlueWest.Api/Controllers/CurrencyController.cs b/BlueWest.Api/Controllers/CurrencyController.cs index f811c5d..a759f3e 100644 --- a/BlueWest.Api/Controllers/CurrencyController.cs +++ b/BlueWest.Api/Controllers/CurrencyController.cs @@ -77,9 +77,9 @@ namespace BlueWest.WebApi.Controllers #region GetCurrencyWithCode /// - /// Gets a currency by the currency number (id) + /// Gets a currency by code. /// - /// The id of the currency to get + /// The currency Code /// [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -179,7 +179,6 @@ namespace BlueWest.WebApi.Controllers /// /// Add Currency to the table of currencies /// - /// /// /// [ProducesResponseType(StatusCodes.Status201Created)] diff --git a/BlueWest.Api/Startup.cs b/BlueWest.Api/Startup.cs index 99e5c4d..2fb17a7 100644 --- a/BlueWest.Api/Startup.cs +++ b/BlueWest.Api/Startup.cs @@ -7,8 +7,12 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using System.Text.Json.Serialization; using BlueWest.Tools; +using BlueWest.WebApi.Context.Users; using BlueWest.WebApi.Interfaces; using BlueWest.WebApi.Tools; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.FileProviders; using Microsoft.OpenApi.Models; namespace BlueWest.WebApi @@ -70,6 +74,38 @@ namespace BlueWest.WebApi }); + services.Configure(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(); + + services.AddScoped(); + + + services.AddSingleton( + new PhysicalFileProvider( + Path.Combine(Directory.GetCurrentDirectory(), "wwwroot/ImageFiles") + ) + ); + IConfigurationRoot configuration = new ConfigurationBuilder() .AddJsonFile("config.json") .Build(); @@ -118,11 +154,12 @@ namespace BlueWest.WebApi c.RoutePrefix = "swagger"; c.SwaggerEndpoint("/swagger/v1/swagger.json", "BlueWest.Api v1"); }); - + app.UseStaticFiles(); //app.UseHttpsRedirection(); app.UseRouting(); app.UseCors(MyAllowSpecificOrigins); + app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { diff --git a/BlueWest.Api/StartupExtensions.cs b/BlueWest.Api/StartupExtensions.cs index 65b43b7..469e8b6 100644 --- a/BlueWest.Api/StartupExtensions.cs +++ b/BlueWest.Api/StartupExtensions.cs @@ -1,12 +1,24 @@ 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 Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; namespace BlueWest.WebApi { @@ -89,5 +101,139 @@ namespace BlueWest.WebApi .AddDbContextPool(options => options.UseSqlite(sqliteConString)); } + public static void AddAuthServerServices(this IServiceCollection services, string origins, IConfiguration _configuration) + { + services.AddScoped(); + services.AddScoped(); + + // User management + services + .AddIdentityCore(opt => { opt.User.RequireUniqueEmail = true; }) + .AddEntityFrameworkStores() + .AddUserManager() + .AddUserStore(); + // Database Context and Swagger + + services.TryAddSingleton(); + // Registering 'services' and Authentication, Cookies, JWT + services + .AddScoped() + .AddScoped() // So it gets successfully registered in UserManager + .AddScoped() + .AddScoped(); + + + // Register the ConfigurationBuilder instance of AuthSettings + var authSettings = _configuration.GetSection(nameof(AuthSettings)); + services.Configure(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(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(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().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()); + } + } } \ No newline at end of file diff --git a/BlueWest.Api/Users/ApplicationUser.cs b/BlueWest.Api/Users/ApplicationUser.cs new file mode 100644 index 0000000..c04d44e --- /dev/null +++ b/BlueWest.Api/Users/ApplicationUser.cs @@ -0,0 +1,101 @@ +using System; +using Microsoft.AspNetCore.Identity; + +namespace BlueWest.WebApi.Context.Users; + +/// +/// Application User in the Identity System. +/// +public class ApplicationUser : IdentityUser +{ + + /// + /// Gets or sets the primary key for this user. + /// + [PersonalData] + public new Guid Id { get; set; } + + /// + /// Gets or sets the user name for this user. + /// + [ProtectedPersonalData] + public override string UserName { get; set; } + + /// + /// Gets or sets the normalized user name for this user. + /// + public override string NormalizedUserName { get; set; } + + /// + /// Gets or sets the email address for this user. + /// + [ProtectedPersonalData] + public override string Email { get; set; } + + /// + /// Gets or sets the normalized email address for this user. + /// + public override string NormalizedEmail { get; set; } + + /// + /// Gets or sets a flag indicating if a user has confirmed their email address. + /// + /// True if the email address has been confirmed, otherwise false. + [PersonalData] + public override bool EmailConfirmed { get; set; } + + /// + /// Gets or sets a salted and hashed representation of the password for this user. + /// + public override string PasswordHash { get; set; } + + /// + /// A random value that must change whenever a users credentials change (password changed, login removed) + /// + public override string SecurityStamp { get; set; } + + /// + /// A random value that must change whenever a user is persisted to the store + /// + public override string ConcurrencyStamp { get; set; } = Guid.NewGuid().ToString(); + + /// + /// Gets or sets a telephone number for the user. + /// + [ProtectedPersonalData] + public override string PhoneNumber { get; set; } + + /// + /// Gets or sets a flag indicating if a user has confirmed their telephone address. + /// + /// True if the telephone number has been confirmed, otherwise false. + [PersonalData] + public override bool PhoneNumberConfirmed { get; set; } + + /// + /// Gets or sets a flag indicating if two factor authentication is enabled for this user. + /// + /// True if 2fa is enabled, otherwise false. + [PersonalData] + public override bool TwoFactorEnabled { get; set; } + + /// + /// Gets or sets the date and time, in UTC, when any user lockout ends. + /// + /// + /// A value in the past means the user is not locked out. + /// + public override DateTimeOffset? LockoutEnd { get; set; } + + /// + /// Gets or sets a flag indicating if the user could be locked out. + /// + /// True if the user could be locked out, otherwise false. + public override bool LockoutEnabled { get; set; } + + /// + /// Gets or sets the number of failed login attempts for the current user. + /// + public override int AccessFailedCount { get; set; } + +} \ No newline at end of file diff --git a/BlueWest.Api/Users/ApplicationUserDbContext.cs b/BlueWest.Api/Users/ApplicationUserDbContext.cs new file mode 100644 index 0000000..cfaaa93 --- /dev/null +++ b/BlueWest.Api/Users/ApplicationUserDbContext.cs @@ -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; + +/// +/// Application User Db Context +/// +public class ApplicationUserDbContext : IdentityDbContext< + ApplicationUser, + ApplicationRole, + string, + ApplicationUserClaim, + ApplicationUserRole, + ApplicationUserLogin, + ApplicationRoleClaim, + ApplicationUserToken>, IPersistedGrantDbContext +{ + /// + /// Gets or sets the of User roles. + /// + public override DbSet UserRoles { get; set; } + + /// + /// Gets or sets the of roles. + /// + public override DbSet Roles { get; set; } + + /// + /// Gets or sets the of role claims. + /// + public override DbSet RoleClaims { get; set; } + + /// + /// Configures the schema needed for the identity framework. + /// + /// + /// The builder being used to construct the model for this context. + /// + + /// + /// Database for the context of database users + /// + /// + public ApplicationUserDbContext(DbContextOptions options) : base(options) + { + Database.EnsureCreated(); + } + + /// + /// Configures the schema needed for the identity framework. + /// + /// + /// The builder being used to construct the model for this context. + /// + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + builder.ConfigureCurrentDbModel(); + base.OnModelCreating(builder); + builder.Entity(b => + { + b.HasMany().WithOne().HasForeignKey(ur => ur.UserId).IsRequired(); + }); + + builder.Entity(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().WithOne().HasForeignKey(ur => ur.RoleId).IsRequired(); + b.HasMany().WithOne().HasForeignKey(rc => rc.RoleId).IsRequired(); + }); + + builder.Entity(b => + { + b.HasKey(rc => rc.Id); + b.ToTable("RoleClaims"); + }); + + builder.Entity(b => + { + b.HasKey(r => new { r.UserId, r.RoleId }); + b.ToTable("UserRoles"); + }); + } + + public Task SaveChangesAsync() + { + return SaveChangesAsync(); + } + + public DbSet PersistedGrants { get; set; } + public DbSet DeviceFlowCodes { get; set; } + public DbSet Keys { get; set; } +} \ No newline at end of file diff --git a/BlueWest.Api/Users/Auth/AccessToken.cs b/BlueWest.Api/Users/Auth/AccessToken.cs new file mode 100644 index 0000000..3a19ad1 --- /dev/null +++ b/BlueWest.Api/Users/Auth/AccessToken.cs @@ -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; + } + } +} + diff --git a/BlueWest.Api/Users/Auth/AuthManager.cs b/BlueWest.Api/Users/Auth/AuthManager.cs new file mode 100644 index 0000000..e40f03b --- /dev/null +++ b/BlueWest.Api/Users/Auth/AuthManager.cs @@ -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 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 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 CreateUserAsync(RegisterViewModel userSignupDto) + { + + return await _userManager.CreateAsync(userSignupDto.ToUser()); + } +} \ No newline at end of file diff --git a/BlueWest.Api/Users/Auth/AuthSettings.cs b/BlueWest.Api/Users/Auth/AuthSettings.cs new file mode 100644 index 0000000..bce65bc --- /dev/null +++ b/BlueWest.Api/Users/Auth/AuthSettings.cs @@ -0,0 +1,6 @@ +namespace BlueWest.WebApi.Context.Users; + +public class AuthSettings +{ + public string SecretKey { get; set; } +} \ No newline at end of file diff --git a/BlueWest.Api/Users/Auth/IAuthManager.cs b/BlueWest.Api/Users/Auth/IAuthManager.cs new file mode 100644 index 0000000..1429ffc --- /dev/null +++ b/BlueWest.Api/Users/Auth/IAuthManager.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; +using BlueWest.Data; +using Microsoft.AspNetCore.Identity; + +namespace BlueWest.WebApi.Context.Users; + +public interface IAuthManager +{ + Task CreateUserAsync(RegisterViewModel registerViewModel); + + Task VerifyLoginAsync(string email, string password); + + Task GetToken(LoginViewModel loginViewModel); + +} \ No newline at end of file diff --git a/BlueWest.Api/Users/Auth/SignInManager.cs b/BlueWest.Api/Users/Auth/SignInManager.cs new file mode 100644 index 0000000..0ae9b7a --- /dev/null +++ b/BlueWest.Api/Users/Auth/SignInManager.cs @@ -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; + +/// +/// SignInManager +/// +internal class SignInManager : SignInManager +{ + public SignInManager( + UserManager userManager, + IHttpContextAccessor contextAccessor, + IUserClaimsPrincipalFactory claimsFactory, + IOptions optionsAccessor, + ILogger> logger, + IAuthenticationSchemeProvider schemes, + IUserConfirmation confirmation) : + base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) + { + + } + + public override async Task CreateUserPrincipalAsync(ApplicationUser user) => await ClaimsFactory.CreateAsync(user); + + + + +} \ No newline at end of file diff --git a/BlueWest.Api/Users/Constants.cs b/BlueWest.Api/Users/Constants.cs new file mode 100644 index 0000000..b0bae0b --- /dev/null +++ b/BlueWest.Api/Users/Constants.cs @@ -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"; + } +} \ No newline at end of file diff --git a/BlueWest.Api/Users/Crypto/BaseCryptoItem.cs b/BlueWest.Api/Users/Crypto/BaseCryptoItem.cs new file mode 100644 index 0000000..7d103bc --- /dev/null +++ b/BlueWest.Api/Users/Crypto/BaseCryptoItem.cs @@ -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(); + } + + /// + /// Generates a random string + /// + /// The length of the random string + /// + 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 + /// + /// Get Cryptographic algorithm + /// + /// + /// + /// + /// + 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); + } + } +} \ No newline at end of file diff --git a/BlueWest.Api/Users/Crypto/Hasher.cs b/BlueWest.Api/Users/Crypto/Hasher.cs new file mode 100644 index 0000000..93edf71 --- /dev/null +++ b/BlueWest.Api/Users/Crypto/Hasher.cs @@ -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; + } + } + } +} \ No newline at end of file diff --git a/BlueWest.Api/Users/Crypto/IHasher.cs b/BlueWest.Api/Users/Crypto/IHasher.cs new file mode 100644 index 0000000..258badc --- /dev/null +++ b/BlueWest.Api/Users/Crypto/IHasher.cs @@ -0,0 +1,12 @@ +using BlueWest.Data; +using BlueWest.WebApi.Context.Users; +using Microsoft.AspNetCore.Identity; + +namespace BlueWest.Cryptography; + +public interface IHasher : IPasswordHasher +{ + string CreateHash(string text, BaseCryptoItem.HashAlgorithm algorithm); + string CreateHash(string text, string salt, BaseCryptoItem.HashAlgorithm algorithm); + bool MatchesHash(string text, string hash); +} \ No newline at end of file diff --git a/BlueWest.Api/Users/Crypto/SHA_512.cs b/BlueWest.Api/Users/Crypto/SHA_512.cs new file mode 100644 index 0000000..a7f0a99 --- /dev/null +++ b/BlueWest.Api/Users/Crypto/SHA_512.cs @@ -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); + } + } +} \ No newline at end of file diff --git a/BlueWest.Api/Users/IUserManager.cs b/BlueWest.Api/Users/IUserManager.cs new file mode 100644 index 0000000..78d2bfa --- /dev/null +++ b/BlueWest.Api/Users/IUserManager.cs @@ -0,0 +1,32 @@ +using System.Threading.Tasks; +using BlueWest.Data; +using Microsoft.AspNetCore.Identity; + +namespace BlueWest.WebApi.Context.Users +{ + public interface IUserManager + { + /// + /// Create user. + /// + /// + /// + Task CreateAsync(ApplicationUser user); + + /// + /// Checks for user password + /// + /// + /// + /// + Task CheckPasswordAsync(ApplicationUser user, string password); + + /// + /// Find by email + /// + /// + /// + Task FindByEmailAsync(string email); + } +} + diff --git a/BlueWest.Api/Users/IUsersRepo.cs b/BlueWest.Api/Users/IUsersRepo.cs new file mode 100644 index 0000000..d941c2e --- /dev/null +++ b/BlueWest.Api/Users/IUsersRepo.cs @@ -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; +/// +/// This is our Users repository. +/// Since this is a simple app we'll have the following roles +/// Admin and APIClient +/// +public interface IUsersRepo : IUserStore +{ + public Task> GetUsers(); + public Task CreateUser(ApplicationUser user); + public Task SaveChanges(); + + public Task GetUserById(string id); + + Task FindByEmailAsync(string email, CancellationToken cancellationToken); + +} \ No newline at end of file diff --git a/BlueWest.Api/Users/Jwt/IJwtFactory.cs b/BlueWest.Api/Users/Jwt/IJwtFactory.cs new file mode 100644 index 0000000..84f3aeb --- /dev/null +++ b/BlueWest.Api/Users/Jwt/IJwtFactory.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace BlueWest.WebApi.Context.Users; + +public interface IJwtFactory +{ + Task GenerateEncodedToken(string id, string userName); +} diff --git a/BlueWest.Api/Users/Jwt/IJwtTokenHandler.cs b/BlueWest.Api/Users/Jwt/IJwtTokenHandler.cs new file mode 100644 index 0000000..a88b355 --- /dev/null +++ b/BlueWest.Api/Users/Jwt/IJwtTokenHandler.cs @@ -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); + } +} \ No newline at end of file diff --git a/BlueWest.Api/Users/Jwt/ITokenFactory.cs b/BlueWest.Api/Users/Jwt/ITokenFactory.cs new file mode 100644 index 0000000..1bdfeda --- /dev/null +++ b/BlueWest.Api/Users/Jwt/ITokenFactory.cs @@ -0,0 +1,6 @@ +namespace BlueWest.WebApi.Context.Users; + +public interface ITokenFactory +{ + string GenerateToken(int size= 32); +} \ No newline at end of file diff --git a/BlueWest.Api/Users/Jwt/JwtFactory.cs b/BlueWest.Api/Users/Jwt/JwtFactory.cs new file mode 100644 index 0000000..1d1be6c --- /dev/null +++ b/BlueWest.Api/Users/Jwt/JwtFactory.cs @@ -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 jwtOptions) + { + _jwtTokenHandler = jwtTokenHandler; + _jwtOptions = jwtOptions.Value; + ThrowIfInvalidOptions(_jwtOptions); + } + + public async Task 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) + }); + } + + /// Date converted to seconds since Unix epoch (Jan 1, 1970, midnight UTC). + 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)); + } + } +} \ No newline at end of file diff --git a/BlueWest.Api/Users/Jwt/JwtIssuerOptions.cs b/BlueWest.Api/Users/Jwt/JwtIssuerOptions.cs new file mode 100644 index 0000000..6a00e15 --- /dev/null +++ b/BlueWest.Api/Users/Jwt/JwtIssuerOptions.cs @@ -0,0 +1,54 @@ +using System; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Tokens; + +namespace BlueWest.WebApi.Context.Users; + +public class JwtIssuerOptions +{ + /// + /// 4.1.1. "iss" (Issuer) Claim - The "iss" (issuer) claim identifies the principal that issued the JWT. + /// + public string Issuer { get; set; } + + /// + /// 4.1.2. "sub" (Subject) Claim - The "sub" (subject) claim identifies the principal that is the subject of the JWT. + /// + public string Subject { get; set; } + + /// + /// 4.1.3. "aud" (Audience) Claim - The "aud" (audience) claim identifies the recipients that the JWT is intended for. + /// + public string Audience { get; set; } + + /// + /// 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. + /// + public DateTime Expiration => IssuedAt.Add(ValidFor); + + /// + /// 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. + /// + public DateTime NotBefore => DateTime.UtcNow; + + /// + /// 4.1.6. "iat" (Issued At) Claim - The "iat" (issued at) claim identifies the time at which the JWT was issued. + /// + public DateTime IssuedAt => DateTime.UtcNow; + + /// + /// Set the timespan the token will be valid for (default is 120 min) + /// + public TimeSpan ValidFor { get; set; } = TimeSpan.FromMinutes(120); + + /// + /// "jti" (JWT ID) Claim (default ID is a GUID) + /// + public Func> JtiGenerator => + () => Task.FromResult(Guid.NewGuid().ToString()); + + /// + /// The signing key to use when generating tokens. + /// + public SigningCredentials SigningCredentials { get; set; } +} \ No newline at end of file diff --git a/BlueWest.Api/Users/Jwt/JwtTokenHandler.cs b/BlueWest.Api/Users/Jwt/JwtTokenHandler.cs new file mode 100644 index 0000000..956c079 --- /dev/null +++ b/BlueWest.Api/Users/Jwt/JwtTokenHandler.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/BlueWest.Api/Users/Models/LoginViewModel.cs b/BlueWest.Api/Users/Models/LoginViewModel.cs new file mode 100644 index 0000000..2e5cfa4 --- /dev/null +++ b/BlueWest.Api/Users/Models/LoginViewModel.cs @@ -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 + /// + /// Login View Model + /// + public class LoginViewModel + { + /// + /// Email + /// + [Required] + [EmailAddress] + public string Email { get; set; } + + /// + /// Password + /// + [Required] + [DataType(DataType.Password)] + public string Password { get; set; } + + /// + /// RememberMe + /// + [Display(Name = "Remember me?")] + public bool RememberMe { get; set; } + } +} + diff --git a/BlueWest.Api/Users/Models/RegisterViewModel.cs b/BlueWest.Api/Users/Models/RegisterViewModel.cs new file mode 100644 index 0000000..a05518f --- /dev/null +++ b/BlueWest.Api/Users/Models/RegisterViewModel.cs @@ -0,0 +1,42 @@ +using System.ComponentModel.DataAnnotations; + +namespace BlueWest.WebApi.Context.Users; + +/// +/// +/// +public class RegisterViewModel +{ + /// + /// Email + /// + [Required] + [EmailAddress] + [Display(Name = "Email")] + public string Email { get; set; } + + /// + /// Password + /// + [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; } + + /// + /// ConfirmPassword + /// + [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; + } +} \ No newline at end of file diff --git a/BlueWest.Api/Users/Models/ResetPasswordViewModel.cs b/BlueWest.Api/Users/Models/ResetPasswordViewModel.cs new file mode 100644 index 0000000..94a7674 --- /dev/null +++ b/BlueWest.Api/Users/Models/ResetPasswordViewModel.cs @@ -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; } +} \ No newline at end of file diff --git a/BlueWest.Api/Users/Roles/ApplicationRole.cs b/BlueWest.Api/Users/Roles/ApplicationRole.cs new file mode 100644 index 0000000..8b00c88 --- /dev/null +++ b/BlueWest.Api/Users/Roles/ApplicationRole.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Identity; + +namespace BlueWest.WebApi.Context.Users +{ + /// + public class ApplicationRole : IdentityRole + { } +} + diff --git a/BlueWest.Api/Users/Roles/ApplicationRoleClaim.cs b/BlueWest.Api/Users/Roles/ApplicationRoleClaim.cs new file mode 100644 index 0000000..2e88895 --- /dev/null +++ b/BlueWest.Api/Users/Roles/ApplicationRoleClaim.cs @@ -0,0 +1,10 @@ +using System; +using Microsoft.AspNetCore.Identity; + +namespace BlueWest.WebApi.Context.Users; + +/// +public class ApplicationRoleClaim : IdentityRoleClaim +{ + +} \ No newline at end of file diff --git a/BlueWest.Api/Users/Roles/ApplicationUserClaim.cs b/BlueWest.Api/Users/Roles/ApplicationUserClaim.cs new file mode 100644 index 0000000..493de3c --- /dev/null +++ b/BlueWest.Api/Users/Roles/ApplicationUserClaim.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Identity; + +namespace BlueWest.WebApi.Context.Users; + +/// +public class ApplicationUserClaim : IdentityUserClaim +{ + +} \ No newline at end of file diff --git a/BlueWest.Api/Users/Roles/ApplicationUserLogin.cs b/BlueWest.Api/Users/Roles/ApplicationUserLogin.cs new file mode 100644 index 0000000..50596e5 --- /dev/null +++ b/BlueWest.Api/Users/Roles/ApplicationUserLogin.cs @@ -0,0 +1,6 @@ +using Microsoft.AspNetCore.Identity; + +namespace BlueWest.WebApi.Context.Users; + +/// +public class ApplicationUserLogin : IdentityUserLogin { } \ No newline at end of file diff --git a/BlueWest.Api/Users/Roles/ApplicationUserRole.cs b/BlueWest.Api/Users/Roles/ApplicationUserRole.cs new file mode 100644 index 0000000..41f2c5a --- /dev/null +++ b/BlueWest.Api/Users/Roles/ApplicationUserRole.cs @@ -0,0 +1,7 @@ +using System; +using Microsoft.AspNetCore.Identity; + +namespace BlueWest.WebApi.Context.Users; + +/// +public class ApplicationUserRole : IdentityUserRole { } \ No newline at end of file diff --git a/BlueWest.Api/Users/Roles/ApplicationUserToken.cs b/BlueWest.Api/Users/Roles/ApplicationUserToken.cs new file mode 100644 index 0000000..8d88574 --- /dev/null +++ b/BlueWest.Api/Users/Roles/ApplicationUserToken.cs @@ -0,0 +1,6 @@ +using Microsoft.AspNetCore.Identity; + +namespace BlueWest.WebApi.Context.Users; + +/// +public class ApplicationUserToken : IdentityUserToken { } \ No newline at end of file diff --git a/BlueWest.Api/Users/Roles/RoleManager.cs b/BlueWest.Api/Users/Roles/RoleManager.cs new file mode 100644 index 0000000..213c000 --- /dev/null +++ b/BlueWest.Api/Users/Roles/RoleManager.cs @@ -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 +{ + public RoleManager( + IRoleStore store, + IEnumerable> roleValidators, + ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, + ILogger> logger) : base(store, roleValidators, keyNormalizer, errors, logger) + { + } + + +} \ No newline at end of file diff --git a/BlueWest.Api/Users/Roles/RoleStore.cs b/BlueWest.Api/Users/Roles/RoleStore.cs new file mode 100644 index 0000000..3d9c5a7 --- /dev/null +++ b/BlueWest.Api/Users/Roles/RoleStore.cs @@ -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; + +/// +/// Role storage management +/// +public class RoleStore : IRoleStore +{ + private ApplicationUserDbContext _dbContext; + + /// + /// Role Store constructor + /// + /// + public RoleStore(ApplicationUserDbContext dbContext) + { + _dbContext = dbContext; + } + /// + /// + /// + /// + public void Dispose() + { + _dbContext = null; + } + + + /// + /// Get role name + /// + /// + /// + /// + /// + public async Task 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 CreateAsync(ApplicationRole role, CancellationToken cancellationToken) + { + _dbContext.Roles.Add(role); + return await _dbContext.SaveChangesAsync(cancellationToken) >= 0 ? IdentityResult.Success : IdentityResult.Failed(); + } + + public async Task UpdateAsync(ApplicationRole role, CancellationToken cancellationToken) + { + _dbContext.Roles.Update(role); + return await _dbContext.SaveChangesAsync(cancellationToken) >= 0 ? IdentityResult.Success : IdentityResult.Failed(); + } + + public async Task DeleteAsync(ApplicationRole role, CancellationToken cancellationToken) + { + _dbContext.Roles.Remove(role); + return await _dbContext.SaveChangesAsync(cancellationToken) >= 0 ? IdentityResult.Success : IdentityResult.Failed(); + } + + public async Task 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 GetRoleNameAsync(ApplicationRole role, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task SetRoleNameAsync(ApplicationRole role, string roleName, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task GetNormalizedRoleNameAsync(ApplicationRole role, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task SetNormalizedRoleNameAsync(ApplicationRole role, string normalizedName, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task FindByIdAsync(string roleId, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task FindByNameAsync(string normalizedRoleName, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/BlueWest.Api/Users/UserManager.cs b/BlueWest.Api/Users/UserManager.cs new file mode 100644 index 0000000..622a930 --- /dev/null +++ b/BlueWest.Api/Users/UserManager.cs @@ -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, IUserManager +{ + private readonly IHasher _hasher; + private readonly IUsersRepo _usersRepo; + public UserManager(IUsersRepo store, IOptions optionsAccessor, + IHasher passwordHasher, IEnumerable> userValidators, + IEnumerable> passwordValidators, ILookupNormalizer keyNormalizer, + IdentityErrorDescriber errors, IServiceProvider services, ILogger> logger) : base(store, + optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, + logger) + { + _hasher = passwordHasher; + _usersRepo = store; + } + + public override async Task 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 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 VerifyPasswordAsync(IUserPasswordStore 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 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 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 GetPasswordStore() + { + if (Store is IUserPasswordStore passwordStore) + { + return passwordStore; + } + + return null; + } + + public override async Task 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; + } +} \ No newline at end of file diff --git a/BlueWest.Api/Users/UserRepository.cs b/BlueWest.Api/Users/UserRepository.cs new file mode 100644 index 0000000..322e28b --- /dev/null +++ b/BlueWest.Api/Users/UserRepository.cs @@ -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; + +/// +/// Users Repository +/// +public class UserRepository : UserStore, IUsersRepo +{ + private readonly ApplicationUserDbContext _context; + + public UserRepository(ApplicationUserDbContext context, IdentityErrorDescriber describer = null) : base(context, describer) + { + _context = context; + } + + /// + /// Get Application Users + /// + /// + public async Task> GetUsers() + { + var users = await _context.Users.ToListAsync(); + return users; + } + + /// + /// Create Application User + /// + /// + public async Task CreateUser(ApplicationUser user) + { + await CreateAsync(user, CancellationToken.None); + } + + /// + /// Save Changes + /// + public async Task SaveChanges() + { + await _context.SaveChangesAsync(); + } + + private async Task SaveChanges(ApplicationUser user) + { + _context.Users.Update(user); + return await _context.SaveChangesAsync() > 0; + } + + + /// + /// Dispose repository + /// + public void Dispose() + { + _context.Dispose(); + } + + /// + public override Task GetUserIdAsync(ApplicationUser user, CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + return Task.FromResult(user.Id.ToString()); + + } + + /// + public override Task GetUserNameAsync(ApplicationUser user, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + return Task.FromResult(user.UserName); + } + + /// + 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); + } + + /// + public override Task GetNormalizedUserNameAsync(ApplicationUser user, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + return Task.FromResult(user.NormalizedUserName); + + } + + /// + public override async Task 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(); + } + + /// + public override async Task UpdateAsync(ApplicationUser user, CancellationToken cancellationToken) + { + _context.Users.Update(user); + + var success = await _context.SaveChangesAsync(cancellationToken) > 0; + + if (success) return IdentityResult.Success; + + return IdentityResult.Failed(); + } + + /// + public override async Task 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; + } + + /// + public async Task GetUserById(string id) + { + var db = _context.Users; + var user = await db.FirstOrDefaultAsync(u => u.Id.ToString() == id); + return user; + } + + + /// + public override Task GetPasswordHashAsync(ApplicationUser user, CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + return Task.FromResult(user.PasswordHash); + } + + /// + public override Task HasPasswordAsync(ApplicationUser user, CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + return Task.FromResult(!string.IsNullOrEmpty(user.PasswordHash)); + } + + /// + public override Task GetEmailAsync(ApplicationUser user, CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + return Task.FromResult(user.Email); + } + + /// + public override Task GetEmailConfirmedAsync(ApplicationUser user, CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + return Task.FromResult(user.EmailConfirmed); + } + + /// + public override async Task 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; + } + + /// + public override Task GetNormalizedEmailAsync(ApplicationUser user, CancellationToken cancellationToken = default) + { + base.GetNormalizedEmailAsync(user, cancellationToken); + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + return Task.FromResult(user.NormalizedEmail); + } + +} \ No newline at end of file diff --git a/BlueWest.sln.DotSettings b/BlueWest.sln.DotSettings index 3d8771c..06b71d5 100644 --- a/BlueWest.sln.DotSettings +++ b/BlueWest.sln.DotSettings @@ -1,2 +1,2 @@  - False \ No newline at end of file + False \ No newline at end of file diff --git a/BlueWest/Artefacts/BlueConsole.cs b/BlueWest/Artefacts/BlueConsole.cs index 4a8f0b9..fccc601 100644 --- a/BlueWest/Artefacts/BlueConsole.cs +++ b/BlueWest/Artefacts/BlueConsole.cs @@ -103,8 +103,8 @@ namespace BlueWest.Core private static void MkdirCommand(string? input) { - var argument = input.Split(" "); - if (argument.Length > 1 && argument[0] != "") + var argument = input?.Split(" "); + if (argument != null && argument.Length > 1 && argument[0] != "") { PathUtils.CreateDirectory(argument[1]); return; @@ -127,17 +127,18 @@ namespace BlueWest.Core private static void LsCommand(string? input) { - var split = input.Split(" "); + var split = input?.Split(" "); - foreach (var name in split) - { - if (name == "ls") continue; - if (string.IsNullOrWhiteSpace(name)) continue; - var assPath = AssemblyUtils.GetAssemblyPath(); - var fp = Path.GetFullPath(name); - InternalLog(PathUtils.ListAll(fp)); - return; - } + if (split != null) + foreach (var name in split) + { + if (name == "ls") continue; + if (string.IsNullOrWhiteSpace(name)) continue; + var assPath = AssemblyUtils.GetAssemblyPath(); + var fp = Path.GetFullPath(name); + InternalLog(PathUtils.ListAll(fp)); + return; + } var pathFiles = PathUtils.ListAssemblyPathFiles(); diff --git a/BlueWest/Core/Events/EventManager.cs b/BlueWest/Core/Events/EventManager.cs index fb91693..b7059c1 100644 --- a/BlueWest/Core/Events/EventManager.cs +++ b/BlueWest/Core/Events/EventManager.cs @@ -88,7 +88,9 @@ namespace BlueWest.Tools { var eventListener = list[i]; var casted = eventListener as EventListener; +#pragma warning disable CS8602 casted.OnEvent( newEvent ); +#pragma warning restore CS8602 } } @@ -100,9 +102,7 @@ namespace BlueWest.Tools /// Receiver. private bool SubscriptionExists( Type type, EventListenerBase receiver ) { - List receivers; - - if( !_subscribersList.TryGetValue( type, out receivers ) ) return false; + if( !_subscribersList.TryGetValue( type, out var receivers ) ) return false; bool exists = false; diff --git a/BlueWest/Core/Events/EventManagerAsync.cs b/BlueWest/Core/Events/EventManagerAsync.cs index 9de306e..f86af0a 100644 --- a/BlueWest/Core/Events/EventManagerAsync.cs +++ b/BlueWest/Core/Events/EventManagerAsync.cs @@ -1,15 +1,16 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; namespace BlueWest.Tools { public struct EventManagerAsync { - private static readonly Dictionary> _subscribersList; + private static readonly ConcurrentDictionary> _subscribersList; static EventManagerAsync() { - _subscribersList = new Dictionary>(12412); + _subscribersList = new ConcurrentDictionary>(); } /// @@ -58,7 +59,7 @@ namespace BlueWest.Tools subscriberList.Remove(subscriberList[i]); if (subscriberList.Count == 0) - _subscribersList.Remove(eventType); + _subscribersList.Remove(eventType, out subscriberList); return; } diff --git a/BlueWest/Core/System/Artefact.cs b/BlueWest/Core/System/Artefact.cs index 0c8d431..38c068f 100644 --- a/BlueWest/Core/System/Artefact.cs +++ b/BlueWest/Core/System/Artefact.cs @@ -116,10 +116,12 @@ namespace BlueWest.Core.ComponentSystem } public void AddComponent() where T : Component { - var component = Activator.CreateInstance(typeof(T), _eventManager) as Component; - _components.Add(component); - _componentsCount += 1; - component.Start(); + if (Activator.CreateInstance(typeof(T), _eventManager) is Component component) + { + _components.Add(component); + _componentsCount += 1; + component.Start(); + } } diff --git a/BlueWest/Core/System/DisabledArtefact.cs b/BlueWest/Core/System/DisabledArtefact.cs index e559e33..cfa4327 100644 --- a/BlueWest/Core/System/DisabledArtefact.cs +++ b/BlueWest/Core/System/DisabledArtefact.cs @@ -6,7 +6,7 @@ public class DisabledArtefact { protected internal ArtefactFrequency Frequency; - protected EventManager _eventManager; + protected EventManager _eventManager = null!; protected virtual void Update(double delta) { diff --git a/BlueWest/Core/System/ThreadServer.cs b/BlueWest/Core/System/ThreadServer.cs index d25b1a8..fa9b8e5 100644 --- a/BlueWest/Core/System/ThreadServer.cs +++ b/BlueWest/Core/System/ThreadServer.cs @@ -59,9 +59,15 @@ namespace BlueWest.Core { SpawnThread(() => { - object instantiatiable = Activator.CreateInstance(type); - Artefact behavior = instantiatiable as Artefact; +#pragma warning disable CS8600 + 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); +#pragma warning restore CS8602 behavior.RunLoop(); }); } diff --git a/BlueWest/Core/Tests/DataExtensions.cs b/BlueWest/Core/Tests/DataExtensions.cs index 807a6d5..85516a6 100644 --- a/BlueWest/Core/Tests/DataExtensions.cs +++ b/BlueWest/Core/Tests/DataExtensions.cs @@ -23,7 +23,7 @@ namespace BlueWest.DataAgent foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(source)) { - object value = property.GetValue(source).ToString(); + object? value = property.GetValue(source)?.ToString(); if (value is Dictionary.KeyCollection keyCollection) { diff --git a/BlueWest/Core/Tests/MappingExtensions.cs b/BlueWest/Core/Tests/MappingExtensions.cs index 57cac46..18e0931 100644 --- a/BlueWest/Core/Tests/MappingExtensions.cs +++ b/BlueWest/Core/Tests/MappingExtensions.cs @@ -9,8 +9,6 @@ namespace BlueWest.Core { public static class MappingExtensions { - private static Type _currentType = null; - private static bool IsMatchingProperty(ref Type type, string propertyName) { var typeProperties = type.GetProperties(); diff --git a/BlueWest/Tools/JsonTools.cs b/BlueWest/Tools/JsonTools.cs index 1c3134f..a24980f 100644 --- a/BlueWest/Tools/JsonTools.cs +++ b/BlueWest/Tools/JsonTools.cs @@ -7,7 +7,7 @@ namespace PerformanceSolution.Tools { public static class JsonTools { - public static Dictionary ConvertFromObjectToDictionary(object arg) + public static Dictionary ConvertFromObjectToDictionary(object arg) { var properties = arg.GetType().GetProperties(); diff --git a/BlueWest/Tools/SaveLoader.cs b/BlueWest/Tools/SaveLoader.cs deleted file mode 100644 index 8ea1fdf..0000000 --- a/BlueWest/Tools/SaveLoader.cs +++ /dev/null @@ -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 -{ - /// - /// Allows the save and load of objects in a specific folder and file. - /// - public static class SaveLoadManager - { - private static readonly string _baseFolderName = Path.DirectorySeparatorChar + "BMData"; - private const string _defaultFolderName = "BlueWest"; - - /// - /// Determines the save path to use when loading and saving a file based on a folder name. - /// - /// The save path. - /// Folder name. - 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; - } - - /// - /// Determines the name of the file to save - /// - /// The save file name. - /// File name. - static string DetermineSaveFileName(string fileName) - { - return fileName + ".binary"; - } - - /// - /// Save the specified saveObject, fileName and foldername into a file on disk. - /// - /// Save object. - /// File name. - /// Foldername. - public static void Save(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); - } - - /// - /// Load the specified file based on a file name into a specified folder - /// - /// File name. - /// Foldername. - public static T Load(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(readByte); - - //return finalObject; - return null; - } - - /// - /// Removes a save from disk - /// - /// File name. - /// Folder name. - 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); - } - } -} \ No newline at end of file