Localize form model validations (#1006)

This commit is contained in:
Leendert de Borst
2025-07-14 13:26:32 +02:00
committed by Leendert de Borst
parent c90c5a9f2f
commit 1e3e542f92
14 changed files with 656 additions and 68 deletions

View File

@@ -0,0 +1,60 @@
//-----------------------------------------------------------------------
// <copyright file="RegisterFormModel.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.Client.Auth.Models;
using System.ComponentModel.DataAnnotations;
using AliasVault.Client.Resources;
using AliasVault.Shared.Models.Validation;
using AliasVault.Shared.Models.WebApi.Auth;
/// <summary>
/// Register form model with validation.
/// </summary>
public class RegisterFormModel : RegisterModel
{
/// <summary>
/// Gets or sets the username.
/// </summary>
[Required]
public new string Username { get; set; } = null!;
/// <summary>
/// Gets or sets the password.
/// </summary>
[Required]
[MinLength(10, ErrorMessageResourceType = typeof(ValidationMessages), ErrorMessageResourceName = nameof(ValidationMessages.PasswordMinLengthGeneric))]
public new string Password { get; set; } = null!;
/// <summary>
/// Gets or sets the password confirmation.
/// </summary>
[Required]
[Compare("Password", ErrorMessageResourceType = typeof(ValidationMessages), ErrorMessageResourceName = nameof(ValidationMessages.PasswordsDoNotMatchGeneric))]
public new string PasswordConfirm { get; set; } = null!;
/// <summary>
/// Gets or sets a value indicating whether the terms and conditions are accepted or not.
/// </summary>
[MustBeTrue(ErrorMessageResourceType = typeof(ValidationMessages), ErrorMessageResourceName = nameof(ValidationMessages.MustAcceptTerms))]
public new bool AcceptTerms { get; set; } = false;
/// <summary>
/// Converts the form model to the base model.
/// </summary>
/// <returns>The base RegisterModel.</returns>
public RegisterModel ToBaseModel()
{
return new RegisterModel
{
Username = Username,
Password = Password,
PasswordConfirm = PasswordConfirm,
AcceptTerms = AcceptTerms,
};
}
}

View File

@@ -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 @@
</EditForm>
@code {
private readonly RegisterModel _registerModel = new();
private readonly RegisterFormModel _registerModel = new();
private FullScreenLoadingIndicator _loadingIndicator = new();
private ServerValidationErrors _serverValidationErrors = new();

View File

@@ -10,6 +10,7 @@ namespace AliasVault.Client.Main.Models;
using System;
using System.ComponentModel.DataAnnotations;
using AliasClientDb;
using AliasVault.Client.Resources;
/// <summary>
/// Credential edit model.
@@ -29,7 +30,7 @@ public sealed class TotpCodeEdit
/// <summary>
/// Gets or sets the secret key of the TOTP code.
/// </summary>
[Required(ErrorMessage = "Secret key is required")]
[Required(ErrorMessageResourceType = typeof(ValidationMessages), ErrorMessageResourceName = nameof(ValidationMessages.SecretKeyRequired))]
public string SecretKey { get; set; } = string.Empty;
/// <summary>

View File

@@ -0,0 +1,52 @@
//-----------------------------------------------------------------------
// <copyright file="PasswordChangeFormModel.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.Client.Main.Models.Validation;
using System.ComponentModel.DataAnnotations;
using AliasVault.Client.Resources;
using AliasVault.Shared.Models.WebApi.PasswordChange;
/// <summary>
/// Password change form model with validation.
/// </summary>
public class PasswordChangeFormModel : PasswordChangeModel
{
/// <summary>
/// Gets or sets the current password.
/// </summary>
[Required]
public new string CurrentPassword { get; set; } = null!;
/// <summary>
/// Gets or sets the new password.
/// </summary>
[Required]
[MinLength(10, ErrorMessageResourceType = typeof(ValidationMessages), ErrorMessageResourceName = nameof(ValidationMessages.PasswordMinLength))]
public new string NewPassword { get; set; } = null!;
/// <summary>
/// Gets or sets the password confirmation.
/// </summary>
[Required]
[Compare("NewPassword", ErrorMessageResourceType = typeof(ValidationMessages), ErrorMessageResourceName = nameof(ValidationMessages.PasswordsDoNotMatch))]
public new string NewPasswordConfirm { get; set; } = null!;
/// <summary>
/// Converts the form model to the base model.
/// </summary>
/// <returns>The base PasswordChangeModel.</returns>
public PasswordChangeModel ToBaseModel()
{
return new PasswordChangeModel
{
CurrentPassword = CurrentPassword,
NewPassword = NewPassword,
NewPasswordConfirm = NewPasswordConfirm,
};
}
}

View File

@@ -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
<LayoutPageTitle>Change password</LayoutPageTitle>
<LayoutPageTitle>@Localizer["PageTitle"]</LayoutPageTitle>
<div class="grid grid-cols-1 px-4 pt-6 xl:grid-cols-3 xl:gap-4 dark:bg-gray-900">
<div class="mb-4 col-span-full xl:mb-2">
<Breadcrumb BreadcrumbItems="BreadcrumbItems"/>
<H1>Change password</H1>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">Changing your master password also changes the vault encryption keys. It is advised to periodically change your master password to keep your vaults secure.</p>
<H1>@Localizer["PageTitle"]</H1>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">@Localizer["PageDescription"]</p>
</div>
</div>
@@ -24,34 +26,34 @@
else
{
<div class="p-4 mb-4 mx-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<EditForm Model="@PasswordChangeModel" OnValidSubmit="@InitiatePasswordChange" class="space-y-4">
<EditForm Model="@PasswordChangeFormModel" OnValidSubmit="@InitiatePasswordChange" class="space-y-4">
<DataAnnotationsValidator />
<ValidationSummary />
<div>
<label for="currentPassword" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Current Password</label>
<InputText type="password" id="currentPassword" @bind-Value="PasswordChangeModel.CurrentPassword"
<label for="currentPassword" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["CurrentPasswordLabel"]</label>
<InputText type="password" id="currentPassword" @bind-Value="PasswordChangeFormModel.CurrentPassword"
class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
required />
</div>
<div>
<label for="newPassword" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">New Password</label>
<InputText type="password" id="newPassword" @bind-Value="PasswordChangeModel.NewPassword"
<label for="newPassword" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["NewPasswordLabel"]</label>
<InputText type="password" id="newPassword" @bind-Value="PasswordChangeFormModel.NewPassword"
class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
required />
</div>
<div>
<label for="newPasswordConfirm" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Confirm New Password</label>
<InputText type="password" id="newPasswordConfirm" @bind-Value="PasswordChangeModel.NewPasswordConfirm"
<label for="newPasswordConfirm" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["ConfirmNewPasswordLabel"]</label>
<InputText type="password" id="newPasswordConfirm" @bind-Value="PasswordChangeFormModel.NewPasswordConfirm"
class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500"
required />
</div>
<button type="submit"
class="w-full bg-primary-500 text-white py-2 px-4 rounded-md hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 transition duration-150 ease-in-out">
Change Password
@Localizer["ChangePasswordButton"]
</button>
</EditForm>
</div>
@@ -64,9 +66,12 @@ else
private bool IsLoading { get; set; } = true;
/// <summary>
/// Gets or sets the password change model.
/// Gets or sets the password change form model.
/// </summary>
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");
/// <summary>
/// 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"] });
}
/// <inheritdoc />
@@ -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
/// </summary>
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.

View File

@@ -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
<LayoutPageTitle>Delete Account</LayoutPageTitle>
<LayoutPageTitle>@Localizer["PageTitle"]</LayoutPageTitle>
<div class="grid grid-cols-1 px-4 pt-6 xl:grid-cols-3 xl:gap-4 dark:bg-gray-900">
<div class="mb-4 col-span-full xl:mb-2">
<Breadcrumb BreadcrumbItems="BreadcrumbItems"/>
<H1>Delete Account</H1>
<H1>@Localizer["PageTitle"]</H1>
</div>
</div>
@@ -21,25 +23,25 @@
@if (!_showPasswordConfirm)
{
<div class="mb-6">
<MessageWarning Message="Warning: This action is permanent and cannot be undone. All your data will be permanently deleted." />
<MessageWarning Message="@Localizer["PermanentActionWarning"]" />
<div class="mt-4 mb-6 text-gray-600 dark:text-gray-400">
<p class="mb-2">Please note:</p>
<p class="mb-2">@Localizer["PleaseNote"]</p>
<ul class="list-disc list-inside space-y-2">
<li>All encrypted vaults which includes all of your credentials will be permanently deleted</li>
<li>Your email aliases will be orphaned and cannot be claimed by other users</li>
<li>Your account cannot be recovered after deletion</li>
<li>@Localizer["VaultsDeletedNote"]</li>
<li>@Localizer["EmailAliasesOrphanedNote"]</li>
<li>@Localizer["AccountCannotBeRecoveredNote"]</li>
</ul>
</div>
<EditForm Model="@_usernameModel" OnSubmit="@ConfirmUsername">
<div class="mb-4">
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Confirm your username</label>
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["ConfirmUsernameLabel"]</label>
<InputText id="username" @bind-Value="_usernameModel.Username" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" />
</div>
<div class="flex space-x-3">
<Button Type="submit" Color="danger">Continue with Account Deletion</Button>
<Button Type="submit" Color="danger">@Localizer["ContinueWithAccountDeletion"]</Button>
<Button Type="button" Color="secondary" OnClick="Cancel">Cancel</Button>
</div>
</EditForm>
@@ -48,12 +50,12 @@
else
{
<div class="mb-6">
<MessageWarning Message="Final warning: Enter your password to permanently delete your account." />
<MessageWarning Message="@Localizer["FinalWarning"]" />
<div class="mt-4 mb-6 text-gray-600 dark:text-gray-400">
<p class="mb-2">Please note:</p>
<p class="mb-2">@Localizer["PleaseNote"]</p>
<ul class="list-disc list-inside space-y-2">
<li>Account deletion is irreversible and cannot be undone. Pressing the button below will delete your account immmediately and permanently.</li>
<li>@Localizer["DeletionIrreversibleNote"]</li>
</ul>
</div>
@@ -62,12 +64,12 @@
<ValidationSummary />
<div class="mb-4">
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Enter your password</label>
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Localizer["EnterPasswordLabel"]</label>
<InputText id="password" type="password" @bind-Value="_passwordModel.Password" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500" />
</div>
<div class="flex space-x-3">
<Button Type="submit" Color="danger">Delete My Account</Button>
<Button Type="submit" Color="danger">@Localizer["DeleteMyAccount"]</Button>
<Button Type="button" Color="secondary" OnClick="Cancel">Cancel</Button>
</div>
</EditForm>
@@ -86,6 +88,9 @@
/// </summary>
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");
/// <summary>
/// Whether to show the password confirmation step.
/// </summary>
@@ -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"] });
}
/// <summary>
@@ -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 @@
/// </summary>
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<LoginInitiateResponse>(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 @@
/// <summary>
/// Gets or sets the username.
/// </summary>
[Required(ErrorMessage = "Username is required")]
[Required(ErrorMessageResourceType = typeof(ValidationMessages), ErrorMessageResourceName = nameof(ValidationMessages.UsernameRequired))]
public string Username { get; set; } = string.Empty;
}
@@ -229,7 +234,7 @@
/// <summary>
/// Gets or sets the password.
/// </summary>
[Required(ErrorMessage = "Password is required")]
[Required(ErrorMessageResourceType = typeof(ValidationMessages), ErrorMessageResourceName = nameof(ValidationMessages.PasswordRequired))]
public string Password { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<!-- Page title and breadcrumbs -->
<data name="PageTitle" xml:space="preserve">
<value>Change password</value>
<comment>Page title for the change password page</comment>
</data>
<data name="BreadcrumbSecuritySettings" xml:space="preserve">
<value>Security settings</value>
<comment>Breadcrumb text for security settings</comment>
</data>
<data name="BreadcrumbChangePassword" xml:space="preserve">
<value>Change password</value>
<comment>Breadcrumb text for change password</comment>
</data>
<!-- Page description -->
<data name="PageDescription" xml:space="preserve">
<value>Changing your master password also changes the vault encryption keys. It is advised to periodically change your master password to keep your vaults secure.</value>
<comment>Description text explaining the password change process</comment>
</data>
<!-- Form labels -->
<data name="CurrentPasswordLabel" xml:space="preserve">
<value>Current Password</value>
<comment>Label for current password input field</comment>
</data>
<data name="NewPasswordLabel" xml:space="preserve">
<value>New Password</value>
<comment>Label for new password input field</comment>
</data>
<data name="ConfirmNewPasswordLabel" xml:space="preserve">
<value>Confirm New Password</value>
<comment>Label for confirm new password input field</comment>
</data>
<!-- Button text -->
<data name="ChangePasswordButton" xml:space="preserve">
<value>Change Password</value>
<comment>Button text for changing password</comment>
</data>
<!-- Loading and status messages -->
<data name="ChangingPasswordMessage" xml:space="preserve">
<value>Changing password...</value>
<comment>Loading message displayed while changing password</comment>
</data>
<data name="PasswordChangedSuccessfully" xml:space="preserve">
<value>Password changed successfully.</value>
<comment>Success message after password change</comment>
</data>
<data name="FailedToInitiatePasswordChange" xml:space="preserve">
<value>Failed to initiate the password change process.</value>
<comment>Error message when password change initiation fails</comment>
</data>
<data name="FailedToChangePassword" xml:space="preserve">
<value>Failed to change password. Please refresh the page and try again.</value>
<comment>Error message when password change fails</comment>
</data>
</root>

View File

@@ -0,0 +1,145 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<!-- Page title and breadcrumbs -->
<data name="PageTitle" xml:space="preserve">
<value>Delete Account</value>
<comment>Page title for the delete account page</comment>
</data>
<data name="BreadcrumbSecuritySettings" xml:space="preserve">
<value>Security settings</value>
<comment>Breadcrumb text for security settings</comment>
</data>
<data name="BreadcrumbDeleteAccount" xml:space="preserve">
<value>Delete Account</value>
<comment>Breadcrumb text for delete account</comment>
</data>
<!-- Warning messages -->
<data name="PermanentActionWarning" xml:space="preserve">
<value>Warning: This action is permanent and cannot be undone. All your data will be permanently deleted.</value>
<comment>Warning message about permanent deletion</comment>
</data>
<data name="FinalWarning" xml:space="preserve">
<value>Final warning: Enter your password to permanently delete your account.</value>
<comment>Final warning message before account deletion</comment>
</data>
<!-- Note sections -->
<data name="PleaseNote" xml:space="preserve">
<value>Please note:</value>
<comment>Header for note section</comment>
</data>
<data name="VaultsDeletedNote" xml:space="preserve">
<value>All encrypted vaults which includes all of your credentials will be permanently deleted</value>
<comment>Note about vault deletion</comment>
</data>
<data name="EmailAliasesOrphanedNote" xml:space="preserve">
<value>Your email aliases will be orphaned and cannot be claimed by other users</value>
<comment>Note about email aliases being orphaned</comment>
</data>
<data name="AccountCannotBeRecoveredNote" xml:space="preserve">
<value>Your account cannot be recovered after deletion</value>
<comment>Note about account recovery</comment>
</data>
<data name="DeletionIrreversibleNote" xml:space="preserve">
<value>Account deletion is irreversible and cannot be undone. Pressing the button below will delete your account immmediately and permanently.</value>
<comment>Note about deletion being irreversible</comment>
</data>
<!-- Form labels -->
<data name="ConfirmUsernameLabel" xml:space="preserve">
<value>Confirm your username</value>
<comment>Label for username confirmation input field</comment>
</data>
<data name="EnterPasswordLabel" xml:space="preserve">
<value>Enter your password</value>
<comment>Label for password input field</comment>
</data>
<!-- Button text -->
<data name="ContinueWithAccountDeletion" xml:space="preserve">
<value>Continue with Account Deletion</value>
<comment>Button text to continue with account deletion</comment>
</data>
<data name="DeleteMyAccount" xml:space="preserve">
<value>Delete My Account</value>
<comment>Button text to delete account</comment>
</data>
<!-- Loading and status messages -->
<data name="DeletingAccountMessage" xml:space="preserve">
<value>Deleting account...</value>
<comment>Loading message displayed while deleting account</comment>
</data>
<data name="UsernameRequired" xml:space="preserve">
<value>Username is required</value>
<comment>Error message when username is not provided</comment>
</data>
<data name="UsernameDoesNotMatch" xml:space="preserve">
<value>The username you entered does not match your current username. Please try again.</value>
<comment>Error message when username doesn't match</comment>
</data>
<data name="ErrorProcessingRequest" xml:space="preserve">
<value>An error occurred while processing the request.</value>
<comment>Generic error message for request processing</comment>
</data>
</root>

View File

@@ -259,4 +259,38 @@
<value>If loading seems stuck, you can click the button below to refresh the page.</value>
<comment>Text shown above refresh button on loading screen</comment>
</data>
<!-- Validation error messages for forms -->
<data name="ErrorPasswordMinLength" xml:space="preserve">
<value>The new password must be at least 10 characters long.</value>
<comment>Error message for password minimum length validation</comment>
</data>
<data name="ErrorPasswordsDoNotMatch" xml:space="preserve">
<value>The new passwords do not match.</value>
<comment>Error message when password confirmation doesn't match</comment>
</data>
<data name="ErrorPasswordMinLengthGeneric" xml:space="preserve">
<value>Password must be at least 10 characters long.</value>
<comment>Generic error message for password minimum length validation</comment>
</data>
<data name="ErrorPasswordsDoNotMatchGeneric" xml:space="preserve">
<value>Passwords do not match.</value>
<comment>Generic error message when passwords don't match</comment>
</data>
<data name="ErrorMustAcceptTerms" xml:space="preserve">
<value>You must accept the terms and conditions.</value>
<comment>Error message for terms and conditions acceptance</comment>
</data>
<data name="ErrorSecretKeyRequired" xml:space="preserve">
<value>Secret key is required</value>
<comment>Error message when secret key is required</comment>
</data>
<data name="ErrorUsernameRequired" xml:space="preserve">
<value>Username is required</value>
<comment>Error message when username is required</comment>
</data>
<data name="ErrorPasswordRequired" xml:space="preserve">
<value>Password is required</value>
<comment>Error message when password is required</comment>
</data>
</root>

View File

@@ -0,0 +1,80 @@
//-----------------------------------------------------------------------
// <copyright file="ValidationMessages.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.Client.Resources;
using System.Globalization;
using System.Resources;
/// <summary>
/// Provides access to validation message resources.
/// </summary>
public static class ValidationMessages
{
/// <summary>
/// The resource manager for accessing validation messages.
/// </summary>
private static readonly ResourceManager ResourceManager = new("AliasVault.Client.Resources.ValidationMessages", typeof(ValidationMessages).Assembly);
/// <summary>
/// Gets the error message for password minimum length validation.
/// </summary>
public static string PasswordMinLength => GetResourceValue("PasswordMinLength");
/// <summary>
/// Gets the error message when password confirmation doesn't match.
/// </summary>
public static string PasswordsDoNotMatch => GetResourceValue("PasswordsDoNotMatch");
/// <summary>
/// Gets the generic error message for password minimum length validation.
/// </summary>
public static string PasswordMinLengthGeneric => GetResourceValue("PasswordMinLengthGeneric");
/// <summary>
/// Gets the generic error message when passwords don't match.
/// </summary>
public static string PasswordsDoNotMatchGeneric => GetResourceValue("PasswordsDoNotMatchGeneric");
/// <summary>
/// Gets the error message when username is required.
/// </summary>
public static string UsernameRequired => GetResourceValue("UsernameRequired");
/// <summary>
/// Gets the error message when password is required.
/// </summary>
public static string PasswordRequired => GetResourceValue("PasswordRequired");
/// <summary>
/// Gets the error message when secret key is required.
/// </summary>
public static string SecretKeyRequired => GetResourceValue("SecretKeyRequired");
/// <summary>
/// Gets the error message for terms and conditions acceptance.
/// </summary>
public static string MustAcceptTerms => GetResourceValue("MustAcceptTerms");
/// <summary>
/// Gets the resource value for the specified key.
/// </summary>
/// <param name="key">The resource key.</param>
/// <returns>The localized resource value.</returns>
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;
}
}
}

View File

@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<!-- Password validation messages -->
<data name="PasswordMinLength" xml:space="preserve">
<value>The new password must be at least 10 characters long.</value>
<comment>Error message for password minimum length validation</comment>
</data>
<data name="PasswordsDoNotMatch" xml:space="preserve">
<value>The new passwords do not match.</value>
<comment>Error message when password confirmation doesn't match</comment>
</data>
<data name="PasswordMinLengthGeneric" xml:space="preserve">
<value>Password must be at least 10 characters long.</value>
<comment>Generic error message for password minimum length validation</comment>
</data>
<data name="PasswordsDoNotMatchGeneric" xml:space="preserve">
<value>Passwords do not match.</value>
<comment>Generic error message when passwords don't match</comment>
</data>
<!-- Required field validation messages -->
<data name="UsernameRequired" xml:space="preserve">
<value>Username is required</value>
<comment>Error message when username is required</comment>
</data>
<data name="PasswordRequired" xml:space="preserve">
<value>Password is required</value>
<comment>Error message when password is required</comment>
</data>
<data name="SecretKeyRequired" xml:space="preserve">
<value>Secret key is required</value>
<comment>Error message when secret key is required</comment>
</data>
<!-- Terms and conditions -->
<data name="MustAcceptTerms" xml:space="preserve">
<value>You must accept the terms and conditions.</value>
<comment>Error message for terms and conditions acceptance</comment>
</data>
</root>

View File

@@ -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;
/// <summary>
@@ -22,8 +23,11 @@ using SecureRemotePassword;
/// <param name="authStateProvider">The provider that manages authentication state.</param>
/// <param name="authService">The service handling authentication operations.</param>
/// <param name="config">The application configuration.</param>
public class UserRegistrationService(HttpClient httpClient, AuthenticationStateProvider authStateProvider, AuthService authService, Config config)
/// <param name="localizerFactory">The string localizer factory for localization.</param>
public class UserRegistrationService(HttpClient httpClient, AuthenticationStateProvider authStateProvider, AuthService authService, Config config, IStringLocalizerFactory localizerFactory)
{
private readonly IStringLocalizer _apiErrorLocalizer = localizerFactory.Create("ApiErrors", "AliasVault.Client");
/// <summary>
/// Registers a new user asynchronously.
/// </summary>
@@ -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));
}

View File

@@ -7,9 +7,6 @@
namespace AliasVault.Shared.Models.WebApi.Auth;
using System.ComponentModel.DataAnnotations;
using AliasVault.Shared.Models.Validation;
/// <summary>
/// Register model.
/// </summary>
@@ -18,26 +15,20 @@ public class RegisterModel
/// <summary>
/// Gets or sets the username.
/// </summary>
[Required]
public string Username { get; set; } = null!;
/// <summary>
/// Gets or sets the password.
/// </summary>
[Required]
[MinLength(8, ErrorMessage = "Password must be at least 8 characters long.")]
public string Password { get; set; } = null!;
/// <summary>
/// Gets or sets the password confirmation.
/// </summary>
[Required]
[Compare("Password", ErrorMessage = "Passwords do not match.")]
public string PasswordConfirm { get; set; } = null!;
/// <summary>
/// Gets or sets a value indicating whether the terms and conditions are accepted or not.
/// </summary>
[MustBeTrue(ErrorMessage = "You must accept the terms and conditions.")]
public bool AcceptTerms { get; set; } = false;
}

View File

@@ -7,8 +7,6 @@
namespace AliasVault.Shared.Models.WebApi.PasswordChange;
using System.ComponentModel.DataAnnotations;
/// <summary>
/// Password change model.
/// </summary>
@@ -17,20 +15,15 @@ public class PasswordChangeModel
/// <summary>
/// Gets or sets the current password.
/// </summary>
[Required]
public string CurrentPassword { get; set; } = null!;
/// <summary>
/// Gets or sets the password.
/// </summary>
[Required]
[MinLength(8, ErrorMessage = "The new password must be at least 8 characters long.")]
public string NewPassword { get; set; } = null!;
/// <summary>
/// Gets or sets the password confirmation.
/// </summary>
[Required]
[Compare("NewPassword", ErrorMessage = "The new passwords do not match.")]
public string NewPasswordConfirm { get; set; } = null!;
}