From 1e3e542f923ccf020264e832e26fb201cac4cb37 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Mon, 14 Jul 2025 13:26:32 +0200 Subject: [PATCH] Localize form model validations (#1006) --- .../Auth/Models/RegisterFormModel.cs | 60 ++++++++ .../Auth/Pages/Register.razor | 4 +- .../Main/Models/TotpCodeEdit.cs | 3 +- .../Validation/PasswordChangeFormModel.cs | 52 +++++++ .../Settings/Security/ChangePassword.razor | 51 +++--- .../Settings/Security/DeleteAccount.razor | 53 ++++--- .../Settings/Security/ChangePassword.en.resx | 119 ++++++++++++++ .../Settings/Security/DeleteAccount.en.resx | 145 ++++++++++++++++++ .../Resources/SharedResources.en.resx | 34 ++++ .../Resources/ValidationMessages.cs | 80 ++++++++++ .../Resources/ValidationMessages.en.resx | 99 ++++++++++++ .../Services/Auth/UserRegistrationService.cs | 8 +- .../Models/WebApi/Auth/RegisterModel.cs | 9 -- .../PasswordChange/PasswordChangeModel.cs | 7 - 14 files changed, 656 insertions(+), 68 deletions(-) create mode 100644 apps/server/AliasVault.Client/Auth/Models/RegisterFormModel.cs create mode 100644 apps/server/AliasVault.Client/Main/Models/Validation/PasswordChangeFormModel.cs create mode 100644 apps/server/AliasVault.Client/Resources/Components/Main/Pages/Settings/Security/ChangePassword.en.resx create mode 100644 apps/server/AliasVault.Client/Resources/Components/Main/Pages/Settings/Security/DeleteAccount.en.resx create mode 100644 apps/server/AliasVault.Client/Resources/ValidationMessages.cs create mode 100644 apps/server/AliasVault.Client/Resources/ValidationMessages.en.resx diff --git a/apps/server/AliasVault.Client/Auth/Models/RegisterFormModel.cs b/apps/server/AliasVault.Client/Auth/Models/RegisterFormModel.cs new file mode 100644 index 000000000..ba57b649d --- /dev/null +++ b/apps/server/AliasVault.Client/Auth/Models/RegisterFormModel.cs @@ -0,0 +1,60 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + +namespace AliasVault.Client.Auth.Models; + +using System.ComponentModel.DataAnnotations; +using AliasVault.Client.Resources; +using AliasVault.Shared.Models.Validation; +using AliasVault.Shared.Models.WebApi.Auth; + +/// +/// Register form model with validation. +/// +public class RegisterFormModel : RegisterModel +{ + /// + /// Gets or sets the username. + /// + [Required] + public new string Username { get; set; } = null!; + + /// + /// Gets or sets the password. + /// + [Required] + [MinLength(10, ErrorMessageResourceType = typeof(ValidationMessages), ErrorMessageResourceName = nameof(ValidationMessages.PasswordMinLengthGeneric))] + public new string Password { get; set; } = null!; + + /// + /// Gets or sets the password confirmation. + /// + [Required] + [Compare("Password", ErrorMessageResourceType = typeof(ValidationMessages), ErrorMessageResourceName = nameof(ValidationMessages.PasswordsDoNotMatchGeneric))] + public new string PasswordConfirm { get; set; } = null!; + + /// + /// Gets or sets a value indicating whether the terms and conditions are accepted or not. + /// + [MustBeTrue(ErrorMessageResourceType = typeof(ValidationMessages), ErrorMessageResourceName = nameof(ValidationMessages.MustAcceptTerms))] + public new bool AcceptTerms { get; set; } = false; + + /// + /// Converts the form model to the base model. + /// + /// The base RegisterModel. + public RegisterModel ToBaseModel() + { + return new RegisterModel + { + Username = Username, + Password = Password, + PasswordConfirm = PasswordConfirm, + AcceptTerms = AcceptTerms, + }; + } +} diff --git a/apps/server/AliasVault.Client/Auth/Pages/Register.razor b/apps/server/AliasVault.Client/Auth/Pages/Register.razor index 939c5825d..00b0fcafa 100644 --- a/apps/server/AliasVault.Client/Auth/Pages/Register.razor +++ b/apps/server/AliasVault.Client/Auth/Pages/Register.razor @@ -2,7 +2,7 @@ @inherits AliasVault.Client.Auth.Pages.Base.LoginBase @attribute [AllowAnonymous] @layout Auth.Layout.MainLayout -@using AliasVault.Shared.Models.WebApi.Auth +@using AliasVault.Client.Auth.Models @using AliasVault.Client.Auth.Components @using Microsoft.Extensions.Localization @@ -48,7 +48,7 @@ @code { - private readonly RegisterModel _registerModel = new(); + private readonly RegisterFormModel _registerModel = new(); private FullScreenLoadingIndicator _loadingIndicator = new(); private ServerValidationErrors _serverValidationErrors = new(); diff --git a/apps/server/AliasVault.Client/Main/Models/TotpCodeEdit.cs b/apps/server/AliasVault.Client/Main/Models/TotpCodeEdit.cs index 521bf5378..d1138db7a 100644 --- a/apps/server/AliasVault.Client/Main/Models/TotpCodeEdit.cs +++ b/apps/server/AliasVault.Client/Main/Models/TotpCodeEdit.cs @@ -10,6 +10,7 @@ namespace AliasVault.Client.Main.Models; using System; using System.ComponentModel.DataAnnotations; using AliasClientDb; +using AliasVault.Client.Resources; /// /// Credential edit model. @@ -29,7 +30,7 @@ public sealed class TotpCodeEdit /// /// Gets or sets the secret key of the TOTP code. /// - [Required(ErrorMessage = "Secret key is required")] + [Required(ErrorMessageResourceType = typeof(ValidationMessages), ErrorMessageResourceName = nameof(ValidationMessages.SecretKeyRequired))] public string SecretKey { get; set; } = string.Empty; /// diff --git a/apps/server/AliasVault.Client/Main/Models/Validation/PasswordChangeFormModel.cs b/apps/server/AliasVault.Client/Main/Models/Validation/PasswordChangeFormModel.cs new file mode 100644 index 000000000..6d85f1b31 --- /dev/null +++ b/apps/server/AliasVault.Client/Main/Models/Validation/PasswordChangeFormModel.cs @@ -0,0 +1,52 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + +namespace AliasVault.Client.Main.Models.Validation; + +using System.ComponentModel.DataAnnotations; +using AliasVault.Client.Resources; +using AliasVault.Shared.Models.WebApi.PasswordChange; + +/// +/// Password change form model with validation. +/// +public class PasswordChangeFormModel : PasswordChangeModel +{ + /// + /// Gets or sets the current password. + /// + [Required] + public new string CurrentPassword { get; set; } = null!; + + /// + /// Gets or sets the new password. + /// + [Required] + [MinLength(10, ErrorMessageResourceType = typeof(ValidationMessages), ErrorMessageResourceName = nameof(ValidationMessages.PasswordMinLength))] + public new string NewPassword { get; set; } = null!; + + /// + /// Gets or sets the password confirmation. + /// + [Required] + [Compare("NewPassword", ErrorMessageResourceType = typeof(ValidationMessages), ErrorMessageResourceName = nameof(ValidationMessages.PasswordsDoNotMatch))] + public new string NewPasswordConfirm { get; set; } = null!; + + /// + /// Converts the form model to the base model. + /// + /// The base PasswordChangeModel. + public PasswordChangeModel ToBaseModel() + { + return new PasswordChangeModel + { + CurrentPassword = CurrentPassword, + NewPassword = NewPassword, + NewPasswordConfirm = NewPasswordConfirm, + }; + } +} diff --git a/apps/server/AliasVault.Client/Main/Pages/Settings/Security/ChangePassword.razor b/apps/server/AliasVault.Client/Main/Pages/Settings/Security/ChangePassword.razor index b9298f859..9e0a642cf 100644 --- a/apps/server/AliasVault.Client/Main/Pages/Settings/Security/ChangePassword.razor +++ b/apps/server/AliasVault.Client/Main/Pages/Settings/Security/ChangePassword.razor @@ -1,19 +1,21 @@ @page "/settings/security/change-password" @using AliasVault.Client.Utilities +@using AliasVault.Client.Main.Models.Validation @using AliasVault.Shared.Models.WebApi.PasswordChange @using AliasVault.Shared.Models.WebApi.Vault; @using AliasVault.Cryptography.Client @using SecureRemotePassword +@using Microsoft.Extensions.Localization @inherits MainBase @inject HttpClient Http -Change password +@Localizer["PageTitle"]
-

Change password

-

Changing your master password also changes the vault encryption keys. It is advised to periodically change your master password to keep your vaults secure.

+

@Localizer["PageTitle"]

+

@Localizer["PageDescription"]

@@ -24,34 +26,34 @@ else {
- +
- - @Localizer["CurrentPasswordLabel"] +
- - @Localizer["NewPasswordLabel"] +
- - @Localizer["ConfirmNewPasswordLabel"] +
@@ -64,9 +66,12 @@ else private bool IsLoading { get; set; } = true; /// - /// Gets or sets the password change model. + /// Gets or sets the password change form model. /// - private PasswordChangeModel PasswordChangeModel { get; set; } = new(); + private PasswordChangeFormModel PasswordChangeFormModel { get; set; } = new(); + + private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Main.Pages.Settings.Security.ChangePassword", "AliasVault.Client"); + private IStringLocalizer ApiErrorLocalizer => LocalizerFactory.Create("ApiErrors", "AliasVault.Client"); /// /// Gets or sets the current user's password salt. @@ -96,8 +101,8 @@ else { await base.OnInitializedAsync(); - BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Security settings", Url = "/settings/security" }); - BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Change password" }); + BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["BreadcrumbSecuritySettings"], Url = "/settings/security" }); + BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["BreadcrumbChangePassword"] }); } /// @@ -126,7 +131,7 @@ else if (response == null) { - GlobalNotificationService.AddErrorMessage("Failed to initiate the password change process.", true); + GlobalNotificationService.AddErrorMessage(Localizer["FailedToInitiatePasswordChange"], true); IsLoading = false; StateHasChanged(); return; @@ -143,12 +148,12 @@ else /// private async Task InitiatePasswordChange() { - GlobalLoadingSpinner.Show("Changing password..."); + GlobalLoadingSpinner.Show(Localizer["ChangingPasswordMessage"]); GlobalNotificationService.ClearMessages(); StateHasChanged(); // Generate ephemeral for current password to verify it. - var currentPasswordHash = await Encryption.DeriveKeyFromPasswordAsync(PasswordChangeModel.CurrentPassword, CurrentSalt, CurrentEncryptionType, CurrentEncryptionSettings); + var currentPasswordHash = await Encryption.DeriveKeyFromPasswordAsync(PasswordChangeFormModel.CurrentPassword, CurrentSalt, CurrentEncryptionType, CurrentEncryptionSettings); var currentPasswordHashString = BitConverter.ToString(currentPasswordHash).Replace("-", string.Empty); ClientEphemeral = Srp.GenerateEphemeralClient(); @@ -165,7 +170,7 @@ else var client = new SrpClient(); var newSalt = client.GenerateSalt(); - byte[] newPasswordHash = await Encryption.DeriveKeyFromPasswordAsync(PasswordChangeModel.NewPassword, newSalt); + byte[] newPasswordHash = await Encryption.DeriveKeyFromPasswordAsync(PasswordChangeFormModel.NewPassword, newSalt); var newPasswordHashString = BitConverter.ToString(newPasswordHash).Replace("-", string.Empty); // Backup current password hash in case of failure. @@ -201,7 +206,7 @@ else }; // Clear form. - PasswordChangeModel = new PasswordChangeModel(); + PasswordChangeFormModel = new PasswordChangeFormModel(); // 4. Client sends proof of session key to server. try { @@ -210,7 +215,7 @@ else if (!response.IsSuccessStatusCode) { - foreach (var error in ApiResponseUtility.ParseErrorResponse(responseContent)) + foreach (var error in ApiResponseUtility.ParseErrorResponse(responseContent, ApiErrorLocalizer)) { GlobalNotificationService.AddErrorMessage(error, true); } @@ -233,7 +238,7 @@ else } catch { - GlobalNotificationService.AddErrorMessage("Failed to change password. Please refresh the page and try again.", true); + GlobalNotificationService.AddErrorMessage(Localizer["FailedToChangePassword"], true); // Set currentPasswordHash back to original, so we're back to the original state. await AuthService.StoreEncryptionKeyAsync(backupPasswordHash); @@ -244,7 +249,7 @@ else } // Set success message. - GlobalNotificationService.AddSuccessMessage("Password changed successfully.", true); + GlobalNotificationService.AddSuccessMessage(Localizer["PasswordChangedSuccessfully"], true); // Get the new password ephemeral and salt from the server, which is required if the usre // wants to change the password again. diff --git a/apps/server/AliasVault.Client/Main/Pages/Settings/Security/DeleteAccount.razor b/apps/server/AliasVault.Client/Main/Pages/Settings/Security/DeleteAccount.razor index afc768dbf..c349141cf 100644 --- a/apps/server/AliasVault.Client/Main/Pages/Settings/Security/DeleteAccount.razor +++ b/apps/server/AliasVault.Client/Main/Pages/Settings/Security/DeleteAccount.razor @@ -6,14 +6,16 @@ @using AliasVault.Cryptography.Client @using SecureRemotePassword @using System.ComponentModel.DataAnnotations +@using Microsoft.Extensions.Localization +@using AliasVault.Client.Resources @inject HttpClient Http -Delete Account +@Localizer["PageTitle"]
-

Delete Account

+

@Localizer["PageTitle"]

@@ -21,25 +23,25 @@ @if (!_showPasswordConfirm) {
- +
-

Please note:

+

@Localizer["PleaseNote"]

    -
  • All encrypted vaults which includes all of your credentials will be permanently deleted
  • -
  • Your email aliases will be orphaned and cannot be claimed by other users
  • -
  • Your account cannot be recovered after deletion
  • +
  • @Localizer["VaultsDeletedNote"]
  • +
  • @Localizer["EmailAliasesOrphanedNote"]
  • +
  • @Localizer["AccountCannotBeRecoveredNote"]
- +
- +
@@ -48,12 +50,12 @@ else {
- +
-

Please note:

+

@Localizer["PleaseNote"]

    -
  • Account deletion is irreversible and cannot be undone. Pressing the button below will delete your account immmediately and permanently.
  • +
  • @Localizer["DeletionIrreversibleNote"]
@@ -62,12 +64,12 @@
- +
- +
@@ -86,6 +88,9 @@ ///
private readonly DeleteAccountPasswordModel _passwordModel = new(); + private IStringLocalizer Localizer => LocalizerFactory.Create("Components.Main.Pages.Settings.Security.DeleteAccount", "AliasVault.Client"); + private IStringLocalizer ApiErrorLocalizer => LocalizerFactory.Create("ApiErrors", "AliasVault.Client"); + /// /// Whether to show the password confirmation step. /// @@ -106,8 +111,8 @@ { await base.OnInitializedAsync(); - BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Security settings", Url = "/settings/security" }); - BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Delete Account" }); + BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["BreadcrumbSecuritySettings"], Url = "/settings/security" }); + BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = Localizer["BreadcrumbDeleteAccount"] }); } /// @@ -119,7 +124,7 @@ if (string.IsNullOrEmpty(_usernameModel.Username)) { - GlobalNotificationService.AddErrorMessage("Username is required", true); + GlobalNotificationService.AddErrorMessage(Localizer["UsernameRequired"], true); return; } @@ -127,7 +132,7 @@ var usernameMatches = string.Equals(_usernameModel.Username.Trim(), username.Trim(), StringComparison.OrdinalIgnoreCase); if (!usernameMatches) { - GlobalNotificationService.AddErrorMessage("The username you entered does not match your current username. Please try again.", true); + GlobalNotificationService.AddErrorMessage(Localizer["UsernameDoesNotMatch"], true); return; } @@ -140,7 +145,7 @@ /// private async Task DeleteAccountConfirmed() { - GlobalLoadingSpinner.Show("Deleting account..."); + GlobalLoadingSpinner.Show(Localizer["DeletingAccountMessage"]); GlobalNotificationService.ClearMessages(); try @@ -152,7 +157,7 @@ if (!result.IsSuccessStatusCode) { - foreach (var error in ApiResponseUtility.ParseErrorResponse(responseContent)) + foreach (var error in ApiResponseUtility.ParseErrorResponse(responseContent, ApiErrorLocalizer)) { GlobalNotificationService.AddErrorMessage(error, true); } @@ -162,7 +167,7 @@ var loginResponse = JsonSerializer.Deserialize(responseContent); if (loginResponse == null) { - GlobalNotificationService.AddErrorMessage("An error occurred while processing the request.", true); + GlobalNotificationService.AddErrorMessage(Localizer["ErrorProcessingRequest"], true); return; } @@ -185,7 +190,7 @@ if (!result.IsSuccessStatusCode) { - foreach (var error in ApiResponseUtility.ParseErrorResponse(responseContent)) + foreach (var error in ApiResponseUtility.ParseErrorResponse(responseContent, ApiErrorLocalizer)) { GlobalNotificationService.AddErrorMessage(error, true); } @@ -217,7 +222,7 @@ /// /// Gets or sets the username. /// - [Required(ErrorMessage = "Username is required")] + [Required(ErrorMessageResourceType = typeof(ValidationMessages), ErrorMessageResourceName = nameof(ValidationMessages.UsernameRequired))] public string Username { get; set; } = string.Empty; } @@ -229,7 +234,7 @@ /// /// Gets or sets the password. /// - [Required(ErrorMessage = "Password is required")] + [Required(ErrorMessageResourceType = typeof(ValidationMessages), ErrorMessageResourceName = nameof(ValidationMessages.PasswordRequired))] public string Password { get; set; } = string.Empty; } } diff --git a/apps/server/AliasVault.Client/Resources/Components/Main/Pages/Settings/Security/ChangePassword.en.resx b/apps/server/AliasVault.Client/Resources/Components/Main/Pages/Settings/Security/ChangePassword.en.resx new file mode 100644 index 000000000..4a9c21136 --- /dev/null +++ b/apps/server/AliasVault.Client/Resources/Components/Main/Pages/Settings/Security/ChangePassword.en.resx @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + + Change password + Page title for the change password page + + + Security settings + Breadcrumb text for security settings + + + Change password + Breadcrumb text for change password + + + + + Changing your master password also changes the vault encryption keys. It is advised to periodically change your master password to keep your vaults secure. + Description text explaining the password change process + + + + + Current Password + Label for current password input field + + + New Password + Label for new password input field + + + Confirm New Password + Label for confirm new password input field + + + + + Change Password + Button text for changing password + + + + + Changing password... + Loading message displayed while changing password + + + Password changed successfully. + Success message after password change + + + Failed to initiate the password change process. + Error message when password change initiation fails + + + Failed to change password. Please refresh the page and try again. + Error message when password change fails + + \ No newline at end of file diff --git a/apps/server/AliasVault.Client/Resources/Components/Main/Pages/Settings/Security/DeleteAccount.en.resx b/apps/server/AliasVault.Client/Resources/Components/Main/Pages/Settings/Security/DeleteAccount.en.resx new file mode 100644 index 000000000..2283283d9 --- /dev/null +++ b/apps/server/AliasVault.Client/Resources/Components/Main/Pages/Settings/Security/DeleteAccount.en.resx @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + + Delete Account + Page title for the delete account page + + + Security settings + Breadcrumb text for security settings + + + Delete Account + Breadcrumb text for delete account + + + + + Warning: This action is permanent and cannot be undone. All your data will be permanently deleted. + Warning message about permanent deletion + + + Final warning: Enter your password to permanently delete your account. + Final warning message before account deletion + + + + + Please note: + Header for note section + + + All encrypted vaults which includes all of your credentials will be permanently deleted + Note about vault deletion + + + Your email aliases will be orphaned and cannot be claimed by other users + Note about email aliases being orphaned + + + Your account cannot be recovered after deletion + Note about account recovery + + + Account deletion is irreversible and cannot be undone. Pressing the button below will delete your account immmediately and permanently. + Note about deletion being irreversible + + + + + Confirm your username + Label for username confirmation input field + + + Enter your password + Label for password input field + + + + + Continue with Account Deletion + Button text to continue with account deletion + + + Delete My Account + Button text to delete account + + + + + Deleting account... + Loading message displayed while deleting account + + + Username is required + Error message when username is not provided + + + The username you entered does not match your current username. Please try again. + Error message when username doesn't match + + + An error occurred while processing the request. + Generic error message for request processing + + \ No newline at end of file diff --git a/apps/server/AliasVault.Client/Resources/SharedResources.en.resx b/apps/server/AliasVault.Client/Resources/SharedResources.en.resx index 53b738de7..f0d3ae151 100644 --- a/apps/server/AliasVault.Client/Resources/SharedResources.en.resx +++ b/apps/server/AliasVault.Client/Resources/SharedResources.en.resx @@ -259,4 +259,38 @@ If loading seems stuck, you can click the button below to refresh the page. Text shown above refresh button on loading screen + + + + The new password must be at least 10 characters long. + Error message for password minimum length validation + + + The new passwords do not match. + Error message when password confirmation doesn't match + + + Password must be at least 10 characters long. + Generic error message for password minimum length validation + + + Passwords do not match. + Generic error message when passwords don't match + + + You must accept the terms and conditions. + Error message for terms and conditions acceptance + + + Secret key is required + Error message when secret key is required + + + Username is required + Error message when username is required + + + Password is required + Error message when password is required + \ No newline at end of file diff --git a/apps/server/AliasVault.Client/Resources/ValidationMessages.cs b/apps/server/AliasVault.Client/Resources/ValidationMessages.cs new file mode 100644 index 000000000..e21bdcc55 --- /dev/null +++ b/apps/server/AliasVault.Client/Resources/ValidationMessages.cs @@ -0,0 +1,80 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + +namespace AliasVault.Client.Resources; + +using System.Globalization; +using System.Resources; + +/// +/// Provides access to validation message resources. +/// +public static class ValidationMessages +{ + /// + /// The resource manager for accessing validation messages. + /// + private static readonly ResourceManager ResourceManager = new("AliasVault.Client.Resources.ValidationMessages", typeof(ValidationMessages).Assembly); + + /// + /// Gets the error message for password minimum length validation. + /// + public static string PasswordMinLength => GetResourceValue("PasswordMinLength"); + + /// + /// Gets the error message when password confirmation doesn't match. + /// + public static string PasswordsDoNotMatch => GetResourceValue("PasswordsDoNotMatch"); + + /// + /// Gets the generic error message for password minimum length validation. + /// + public static string PasswordMinLengthGeneric => GetResourceValue("PasswordMinLengthGeneric"); + + /// + /// Gets the generic error message when passwords don't match. + /// + public static string PasswordsDoNotMatchGeneric => GetResourceValue("PasswordsDoNotMatchGeneric"); + + /// + /// Gets the error message when username is required. + /// + public static string UsernameRequired => GetResourceValue("UsernameRequired"); + + /// + /// Gets the error message when password is required. + /// + public static string PasswordRequired => GetResourceValue("PasswordRequired"); + + /// + /// Gets the error message when secret key is required. + /// + public static string SecretKeyRequired => GetResourceValue("SecretKeyRequired"); + + /// + /// Gets the error message for terms and conditions acceptance. + /// + public static string MustAcceptTerms => GetResourceValue("MustAcceptTerms"); + + /// + /// Gets the resource value for the specified key. + /// + /// The resource key. + /// The localized resource value. + private static string GetResourceValue(string key) + { + try + { + return ResourceManager.GetString(key, CultureInfo.CurrentUICulture) ?? key; + } + catch + { + // Return the key as fallback if resource loading fails + return key; + } + } +} diff --git a/apps/server/AliasVault.Client/Resources/ValidationMessages.en.resx b/apps/server/AliasVault.Client/Resources/ValidationMessages.en.resx new file mode 100644 index 000000000..b6309c8f3 --- /dev/null +++ b/apps/server/AliasVault.Client/Resources/ValidationMessages.en.resx @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + + The new password must be at least 10 characters long. + Error message for password minimum length validation + + + The new passwords do not match. + Error message when password confirmation doesn't match + + + Password must be at least 10 characters long. + Generic error message for password minimum length validation + + + Passwords do not match. + Generic error message when passwords don't match + + + + + Username is required + Error message when username is required + + + Password is required + Error message when password is required + + + Secret key is required + Error message when secret key is required + + + + + You must accept the terms and conditions. + Error message for terms and conditions acceptance + + \ No newline at end of file diff --git a/apps/server/AliasVault.Client/Services/Auth/UserRegistrationService.cs b/apps/server/AliasVault.Client/Services/Auth/UserRegistrationService.cs index 7d9fa92e1..46df25f4f 100644 --- a/apps/server/AliasVault.Client/Services/Auth/UserRegistrationService.cs +++ b/apps/server/AliasVault.Client/Services/Auth/UserRegistrationService.cs @@ -13,6 +13,7 @@ using AliasVault.Client.Utilities; using AliasVault.Cryptography.Client; using AliasVault.Shared.Models.WebApi.Auth; using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.Extensions.Localization; using SecureRemotePassword; /// @@ -22,8 +23,11 @@ using SecureRemotePassword; /// The provider that manages authentication state. /// The service handling authentication operations. /// The application configuration. -public class UserRegistrationService(HttpClient httpClient, AuthenticationStateProvider authStateProvider, AuthService authService, Config config) +/// The string localizer factory for localization. +public class UserRegistrationService(HttpClient httpClient, AuthenticationStateProvider authStateProvider, AuthService authService, Config config, IStringLocalizerFactory localizerFactory) { + private readonly IStringLocalizer _apiErrorLocalizer = localizerFactory.Create("ApiErrors", "AliasVault.Client"); + /// /// Registers a new user asynchronously. /// @@ -55,7 +59,7 @@ public class UserRegistrationService(HttpClient httpClient, AuthenticationStateP if (!result.IsSuccessStatusCode) { - var errors = ApiResponseUtility.ParseErrorResponse(responseContent); + var errors = ApiResponseUtility.ParseErrorResponse(responseContent, _apiErrorLocalizer); return (false, string.Join(", ", errors)); } diff --git a/apps/server/Shared/AliasVault.Shared/Models/WebApi/Auth/RegisterModel.cs b/apps/server/Shared/AliasVault.Shared/Models/WebApi/Auth/RegisterModel.cs index 4c55230ac..253e5f7de 100644 --- a/apps/server/Shared/AliasVault.Shared/Models/WebApi/Auth/RegisterModel.cs +++ b/apps/server/Shared/AliasVault.Shared/Models/WebApi/Auth/RegisterModel.cs @@ -7,9 +7,6 @@ namespace AliasVault.Shared.Models.WebApi.Auth; -using System.ComponentModel.DataAnnotations; -using AliasVault.Shared.Models.Validation; - /// /// Register model. /// @@ -18,26 +15,20 @@ public class RegisterModel /// /// Gets or sets the username. /// - [Required] public string Username { get; set; } = null!; /// /// Gets or sets the password. /// - [Required] - [MinLength(8, ErrorMessage = "Password must be at least 8 characters long.")] public string Password { get; set; } = null!; /// /// Gets or sets the password confirmation. /// - [Required] - [Compare("Password", ErrorMessage = "Passwords do not match.")] public string PasswordConfirm { get; set; } = null!; /// /// Gets or sets a value indicating whether the terms and conditions are accepted or not. /// - [MustBeTrue(ErrorMessage = "You must accept the terms and conditions.")] public bool AcceptTerms { get; set; } = false; } diff --git a/apps/server/Shared/AliasVault.Shared/Models/WebApi/PasswordChange/PasswordChangeModel.cs b/apps/server/Shared/AliasVault.Shared/Models/WebApi/PasswordChange/PasswordChangeModel.cs index 300a2aa59..800c5fe7c 100644 --- a/apps/server/Shared/AliasVault.Shared/Models/WebApi/PasswordChange/PasswordChangeModel.cs +++ b/apps/server/Shared/AliasVault.Shared/Models/WebApi/PasswordChange/PasswordChangeModel.cs @@ -7,8 +7,6 @@ namespace AliasVault.Shared.Models.WebApi.PasswordChange; -using System.ComponentModel.DataAnnotations; - /// /// Password change model. /// @@ -17,20 +15,15 @@ public class PasswordChangeModel /// /// Gets or sets the current password. /// - [Required] public string CurrentPassword { get; set; } = null!; /// /// Gets or sets the password. /// - [Required] - [MinLength(8, ErrorMessage = "The new password must be at least 8 characters long.")] public string NewPassword { get; set; } = null!; /// /// Gets or sets the password confirmation. /// - [Required] - [Compare("NewPassword", ErrorMessage = "The new passwords do not match.")] public string NewPasswordConfirm { get; set; } = null!; }