diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index ca04949..c3fc553 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -1,7 +1,7 @@ name: Build and run unit tests env: - DOTNET_VERSION: '9.x' + DOTNET_VERSION: '10.x' on: push: @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up .NET Core uses: actions/setup-dotnet@v5 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7364687..505743f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,7 +3,7 @@ name: Release CI/CD env: AZURE_WEBAPP_NAME: app-open5etools AZURE_WEBAPP_PACKAGE_PATH: '.' - DOTNET_VERSION: '9.x' + DOTNET_VERSION: '10.x' on: push: @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up .NET Core uses: actions/setup-dotnet@v5 diff --git a/Directory.Build.props b/Directory.Build.props index 48051fb..c4d6973 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - net9.0 + net10.0 latest true enable diff --git a/Directory.Packages.props b/Directory.Packages.props index c13fcff..23000a3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,24 +7,23 @@ - - - - - - - - - + + + + + + + + + - - - + + - + diff --git a/README.md b/README.md index 9bd0814..a8da0a9 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This repo is for playing around with DnD5E SRD materials, trying out some techno ## Prerequisites -You have .NET 9 SDK installed. +You have .NET 10 SDK installed. ## Get the code diff --git a/src/Open5ETools.Core/Common/Exceptions/ServiceException.cs b/src/Open5ETools.Core/Common/Exceptions/ServiceException.cs index 9f05c4b..1401045 100644 --- a/src/Open5ETools.Core/Common/Exceptions/ServiceException.cs +++ b/src/Open5ETools.Core/Common/Exceptions/ServiceException.cs @@ -2,6 +2,7 @@ public class ServiceException : Exception { + public const string GeneralError = "GeneralError"; public const string GeneralAggregateError = "GeneralAggregateError"; public const string EntityNotFoundException = "EntityNotFoundException"; public const string RequiredValidation = "RequiredValidation"; @@ -9,11 +10,11 @@ public class ServiceException : Exception public object[]? Args { get; } - public string? Field { get; } + public string Field { get; } public ServiceException(string message, params object[] args) : base(message) { - Field = null; + Field = string.Empty; Args = args; } @@ -26,7 +27,7 @@ public ServiceException(string message, string field, params object[] args) : ba public ServiceException(string message, Exception innerException, params object[] args) : base(message, innerException) { - Field = null; + Field = string.Empty; Args = args; } diff --git a/src/Open5ETools.Core/Common/Helpers/PasswordHelper.cs b/src/Open5ETools.Core/Common/Helpers/PasswordHelper.cs index 7f5f69c..f17838f 100644 --- a/src/Open5ETools.Core/Common/Helpers/PasswordHelper.cs +++ b/src/Open5ETools.Core/Common/Helpers/PasswordHelper.cs @@ -4,16 +4,20 @@ namespace Open5ETools.Core.Common.Helpers; public static class PasswordHelper { + private const int Iterations = 10000; + private const int HashLength = 20; + private const int SaltLength = 16; + private static readonly HashAlgorithmName HashAlgorithmName = HashAlgorithmName.SHA512; + public static string EncryptPassword(string password) { using var randomNumberGenerator = RandomNumberGenerator.Create(); - var salt = new byte[16]; + var salt = new byte[SaltLength]; randomNumberGenerator.GetBytes(salt); - using var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 10000, HashAlgorithmName.SHA512); - var hash = pbkdf2.GetBytes(20); - var hashBytes = new byte[36]; - Array.Copy(salt, 0, hashBytes, 0, 16); - Array.Copy(hash, 0, hashBytes, 16, 20); + var hash = Rfc2898DeriveBytes.Pbkdf2(password, salt, Iterations, HashAlgorithmName, HashLength); + var hashBytes = new byte[SaltLength + HashLength]; + Array.Copy(salt, 0, hashBytes, 0, SaltLength); + Array.Copy(hash, 0, hashBytes, SaltLength, HashLength); return Convert.ToBase64String(hashBytes); } @@ -21,14 +25,13 @@ public static bool CheckPassword(string savedPasswordHash, string password) { var result = true; var hashBytes = Convert.FromBase64String(savedPasswordHash); - var salt = new byte[16]; - Array.Copy(hashBytes, 0, salt, 0, 16); - using var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 10000, HashAlgorithmName.SHA512); - var hash = pbkdf2.GetBytes(20); + var salt = new byte[SaltLength]; + Array.Copy(hashBytes, 0, salt, 0, SaltLength); + var hash = Rfc2898DeriveBytes.Pbkdf2(password, salt, Iterations, HashAlgorithmName, HashLength); - for (var i = 0; i < 20; i++) + for (var i = 0; i < HashLength; i++) { - if (hashBytes[i + 16] == hash[i]) + if (hashBytes[i + SaltLength] == hash[i]) continue; result = false; break; diff --git a/src/Open5ETools.Core/Open5ETools.Core.csproj b/src/Open5ETools.Core/Open5ETools.Core.csproj index 8364213..68ba417 100644 --- a/src/Open5ETools.Core/Open5ETools.Core.csproj +++ b/src/Open5ETools.Core/Open5ETools.Core.csproj @@ -3,7 +3,6 @@ - diff --git a/src/Open5ETools.Web/Controllers/Web/DungeonController.cs b/src/Open5ETools.Web/Controllers/Web/DungeonController.cs index 408cc58..b995528 100644 --- a/src/Open5ETools.Web/Controllers/Web/DungeonController.cs +++ b/src/Open5ETools.Web/Controllers/Web/DungeonController.cs @@ -11,6 +11,7 @@ using System.Text.Json; using Open5ETools.Core.Common.Exceptions; using Open5ETools.Resources; +using Open5ETools.Web.Extensions; namespace Open5ETools.Web.Controllers.Web; @@ -70,7 +71,7 @@ public async Task Delete(int id, CancellationToken cancellationTo } catch (Exception ex) { - _logger.LogError(ex, "Error deleting dungeon."); + this.HandleException(ex, _logger, "Error deleting dungeon."); } return RedirectToAction(nameof(Index)); @@ -109,7 +110,7 @@ await _dungeonService.RenameDungeonAsync(model.Id, model.UserId, model.NewDungeo } catch (Exception ex) { - _logger.LogError(ex, "Error renaming dungeon."); + this.HandleException(ex, _logger, "Error renaming dungeon."); } return View(model); @@ -125,7 +126,7 @@ public async Task DeleteOption(int id, CancellationToken cancella } catch (Exception ex) { - _logger.LogError(ex, "Error deleting dungeon option."); + this.HandleException(ex, _logger, "Error deleting dungeon option."); } return RedirectToAction(nameof(Index)); @@ -168,7 +169,7 @@ public async Task Create(DungeonOptionCreateViewModel model, Canc } catch (Exception ex) { - _logger.LogError(ex, "Error creating dungeon."); + this.HandleException(ex, _logger, "Error creating dungeon."); } } diff --git a/src/Open5ETools.Web/Controllers/Web/ProfileController.cs b/src/Open5ETools.Web/Controllers/Web/ProfileController.cs index cea604c..aee4621 100644 --- a/src/Open5ETools.Web/Controllers/Web/ProfileController.cs +++ b/src/Open5ETools.Web/Controllers/Web/ProfileController.cs @@ -3,12 +3,14 @@ using Microsoft.AspNetCore.Mvc; using Open5ETools.Core.Common.Interfaces.Services; using Open5ETools.Core.Common.Models.Services; +using Open5ETools.Web.Extensions; using Open5ETools.Web.Models.Profile; namespace Open5ETools.Web.Controllers.Web; [Authorize] -public class ProfileController(IUserService userService, +public class ProfileController( + IUserService userService, ICurrentUserService currentUserService, IMapper mapper, ILogger logger) : Controller @@ -34,7 +36,8 @@ public IActionResult ChangePassword() [HttpPost] [ValidateAntiForgeryToken] - public async Task ChangePassword(ProfileChangePasswordModel model, CancellationToken cancellationToken) + public async Task ChangePassword(ProfileChangePasswordModel model, + CancellationToken cancellationToken) { if (ModelState.IsValid) { @@ -45,10 +48,10 @@ public async Task ChangePassword(ProfileChangePasswordModel model } catch (Exception ex) { - _logger.LogError(ex, "Error changing password."); - ModelState.AddModelError("", ex.Message); + this.HandleException(ex, _logger, "Error changing password."); } } + return View(model); } } \ No newline at end of file diff --git a/src/Open5ETools.Web/Controllers/Web/UserController.cs b/src/Open5ETools.Web/Controllers/Web/UserController.cs index 726d569..533b8f9 100644 --- a/src/Open5ETools.Web/Controllers/Web/UserController.cs +++ b/src/Open5ETools.Web/Controllers/Web/UserController.cs @@ -4,6 +4,7 @@ using Open5ETools.Core.Common.Interfaces.Services; using Open5ETools.Core.Common.Models.Services; using Open5ETools.Core.Domain; +using Open5ETools.Web.Extensions; using Open5ETools.Web.Models.User; namespace Open5ETools.Web.Controllers.Web; @@ -44,10 +45,10 @@ public async Task Create(UserCreateViewModel model, CancellationT } catch (Exception ex) { - _logger.LogError(ex, "Error creating user."); - ModelState.AddModelError("", ex.Message); + this.HandleException(ex, _logger, "Error creating user."); } } + return View(model); } @@ -73,10 +74,10 @@ public async Task Edit(UserEditViewModel model, CancellationToken } catch (Exception ex) { - _logger.LogError(ex, "Error editing user."); - ModelState.AddModelError("", ex.Message); + this.HandleException(ex, _logger, "Error editing user."); } } + return View(model); } @@ -90,8 +91,9 @@ public async Task Delete(int id, CancellationToken cancellationTo } catch (Exception ex) { - _logger.LogError(ex, "Error deleting user."); + this.HandleException(ex, _logger, "Error deleting user."); } + return RedirectToAction("Index"); } @@ -105,8 +107,9 @@ public async Task Restore(int id, CancellationToken cancellationT } catch (Exception ex) { - _logger.LogError(ex, "Error restoring user."); + this.HandleException(ex, _logger, "Error restoring user."); } + return RedirectToAction("Index"); } } \ No newline at end of file diff --git a/src/Open5ETools.Web/Dockerfile b/src/Open5ETools.Web/Dockerfile index e7d8592..96867dc 100644 --- a/src/Open5ETools.Web/Dockerfile +++ b/src/Open5ETools.Web/Dockerfile @@ -1,11 +1,11 @@ -FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /app COPY . ./ RUN dotnet restore ./src/Open5ETools.Web/Open5ETools.Web.csproj RUN dotnet publish "src/Open5ETools.Web/Open5ETools.Web.csproj" -c Release -o out -FROM mcr.microsoft.com/dotnet/aspnet:9.0 +FROM mcr.microsoft.com/dotnet/aspnet:10.0 WORKDIR /app COPY --from=build /app/out ./ diff --git a/src/Open5ETools.Web/Extensions/ControllerExtensions.cs b/src/Open5ETools.Web/Extensions/ControllerExtensions.cs new file mode 100644 index 0000000..d550ffd --- /dev/null +++ b/src/Open5ETools.Web/Extensions/ControllerExtensions.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Mvc; +using Open5ETools.Core.Common.Exceptions; + +namespace Open5ETools.Web.Extensions; + +public static class ControllerExtensions +{ + public static void HandleException(this Controller controller, Exception ex, ILogger logger, + string? defaultError = null) + { + switch (ex) + { + case ServiceAggregateException ae: + { + foreach (var exception in ae.GetInnerExceptions()) + { + controller.ModelState.AddModelError(exception.Field, exception.Message); + } + + break; + } + case ServiceException e: + controller.ModelState.AddModelError(e.Field, e.Message); + break; + default: + logger.LogError(ex, "{GetDisplayUrl}: {DefaultError}", controller.Request.GetDisplayUrl(), + defaultError ?? ServiceException.GeneralError); + controller.ModelState.AddModelError(string.Empty, defaultError ?? ServiceException.GeneralError); + break; + } + } +} \ No newline at end of file diff --git a/src/Open5ETools.Web/Open5ETools.Web.csproj b/src/Open5ETools.Web/Open5ETools.Web.csproj index aab67f2..4406d08 100644 --- a/src/Open5ETools.Web/Open5ETools.Web.csproj +++ b/src/Open5ETools.Web/Open5ETools.Web.csproj @@ -2,7 +2,7 @@ true true - 1.0.3.2 + 1.0.4.0