diff --git a/apps/server/AliasVault.Admin/Main/Pages/Account/Manage/ChangePassword.razor b/apps/server/AliasVault.Admin/Main/Pages/Account/Manage/ChangePassword.razor
index 11ac83cfe..8352add53 100644
--- a/apps/server/AliasVault.Admin/Main/Pages/Account/Manage/ChangePassword.razor
+++ b/apps/server/AliasVault.Admin/Main/Pages/Account/Manage/ChangePassword.razor
@@ -19,16 +19,18 @@
@if (_isLoading)
@@ -158,7 +159,7 @@
{
await InvokeAsync(async () =>
{
- if (Password.Length < 10)
+ if (Password.Length < PasswordStrengthConstants.MinimumGoodPasswordLength)
{
_errorMessage = Localizer["PasswordTooShortError"];
await OnPasswordChange.InvokeAsync(string.Empty);
diff --git a/apps/server/AliasVault.Client/Main/Components/Shared/PasswordStrengthIndicator.razor b/apps/server/AliasVault.Client/Main/Components/Shared/PasswordStrengthIndicator.razor
index 0a9c717cd..be764ac37 100644
--- a/apps/server/AliasVault.Client/Main/Components/Shared/PasswordStrengthIndicator.razor
+++ b/apps/server/AliasVault.Client/Main/Components/Shared/PasswordStrengthIndicator.razor
@@ -1,4 +1,5 @@
@using Microsoft.Extensions.Localization
+@using AliasVault.Client.Main.Constants
@inject IStringLocalizerFactory LocalizerFactory
@if (!string.IsNullOrEmpty(Password))
@@ -88,10 +89,10 @@
return _passwordStrength switch
{
0 => "bg-orange-400 dark:bg-orange-500",
- 1 => "bg-orange-500 dark:bg-orange-600",
- 2 => "bg-yellow-500 dark:bg-yellow-600",
- 3 => "bg-blue-500 dark:bg-blue-600",
- 4 => "bg-green-500 dark:bg-green-600",
+ 1 => "bg-yellow-500 dark:bg-yellow-600",
+ 2 => "bg-green-500 dark:bg-green-600",
+ 3 => "bg-green-600 dark:bg-green-700",
+ 4 => "bg-green-700 dark:bg-green-800",
_ => "bg-gray-300 dark:bg-gray-600"
};
}
@@ -101,10 +102,10 @@
return _passwordStrength switch
{
0 => "text-orange-600 dark:text-orange-400",
- 1 => "text-orange-600 dark:text-orange-400",
- 2 => "text-yellow-600 dark:text-yellow-400",
- 3 => "text-blue-600 dark:text-blue-400",
- 4 => "text-green-600 dark:text-green-400",
+ 1 => "text-yellow-600 dark:text-yellow-400",
+ 2 => "text-green-600 dark:text-green-400",
+ 3 => "text-green-700 dark:text-green-300",
+ 4 => "text-green-800 dark:text-green-200",
_ => "text-gray-500 dark:text-gray-400"
};
}
@@ -113,4 +114,22 @@
{
return (_passwordStrength + 1) * 20;
}
+
+ ///
+ /// Gets the current password strength level.
+ ///
+ ///
The password strength level (0-4).
+ public int GetPasswordStrength()
+ {
+ return _passwordStrength;
+ }
+
+ ///
+ /// Checks if the current password meets the minimum required strength.
+ ///
+ ///
True if the password meets minimum requirements, false otherwise.
+ public bool MeetsMinimumRequirement()
+ {
+ return _passwordStrength >= PasswordStrengthConstants.MinimumRequiredStrength;
+ }
}
diff --git a/apps/server/AliasVault.Client/Main/Constants/PasswordStrengthConstants.cs b/apps/server/AliasVault.Client/Main/Constants/PasswordStrengthConstants.cs
new file mode 100644
index 000000000..0d78fb0b7
--- /dev/null
+++ b/apps/server/AliasVault.Client/Main/Constants/PasswordStrengthConstants.cs
@@ -0,0 +1,25 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) aliasvault. All rights reserved.
+// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
+//
+//-----------------------------------------------------------------------
+
+namespace AliasVault.Client.Main.Constants;
+
+///
+/// Constants for password strength validation and requirements.
+///
+public static class PasswordStrengthConstants
+{
+ ///
+ /// Minimum password strength level required for account creation and password changes.
+ /// Level 2 corresponds to "Good" (12-15 characters).
+ ///
+ public const int MinimumRequiredStrength = 2;
+
+ ///
+ /// Minimum password length for "Good" strength level.
+ ///
+ public const int MinimumGoodPasswordLength = 12;
+}
diff --git a/apps/server/AliasVault.Client/Main/Models/Validation/PasswordChangeFormModel.cs b/apps/server/AliasVault.Client/Main/Models/Validation/PasswordChangeFormModel.cs
index cfbcc6177..7df3f8e92 100644
--- a/apps/server/AliasVault.Client/Main/Models/Validation/PasswordChangeFormModel.cs
+++ b/apps/server/AliasVault.Client/Main/Models/Validation/PasswordChangeFormModel.cs
@@ -8,6 +8,7 @@
namespace AliasVault.Client.Main.Models.Validation;
using System.ComponentModel.DataAnnotations;
+using AliasVault.Client.Main.Constants;
using AliasVault.Client.Resources;
using AliasVault.Shared.Models.WebApi.PasswordChange;
@@ -26,7 +27,7 @@ public class PasswordChangeFormModel : PasswordChangeModel
/// Gets or sets the new password.
///
[Required(ErrorMessageResourceType = typeof(ValidationMessages), ErrorMessageResourceName = nameof(ValidationMessages.PasswordRequired))]
- [MinLength(10, ErrorMessageResourceType = typeof(ValidationMessages), ErrorMessageResourceName = nameof(ValidationMessages.PasswordMinLength))]
+ [MinLength(PasswordStrengthConstants.MinimumGoodPasswordLength, ErrorMessageResourceType = typeof(ValidationMessages), ErrorMessageResourceName = nameof(ValidationMessages.PasswordMinLength))]
public new string NewPassword { get; set; } = null!;
///
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 e71e0d8c1..a9eb1657d 100644
--- a/apps/server/AliasVault.Client/Main/Pages/Settings/Security/ChangePassword.razor
+++ b/apps/server/AliasVault.Client/Main/Pages/Settings/Security/ChangePassword.razor
@@ -5,6 +5,8 @@
@using AliasVault.Shared.Models.WebApi.Vault;
@using AliasVault.Cryptography.Client
@using AliasVault.Client.Services.JsInterop.RustCore
+@using AliasVault.Client.Main.Components.Shared
+@using AliasVault.Client.Main.Constants
@using Microsoft.Extensions.Localization
@inherits MainBase
@inject HttpClient Http
@@ -40,20 +42,29 @@ else
-
+
+
-
+
+ @if (!string.IsNullOrEmpty(PasswordStrengthError))
+ {
+
+ @PasswordStrengthError
+
+ }
+
@@ -104,6 +115,46 @@ else
private SrpSession ClientSession = new();
private string PrivateKey = string.Empty;
+ ///
+ /// Reference to the password strength indicator component.
+ ///
+ private PasswordStrengthIndicator? PasswordStrengthIndicatorRef { get; set; }
+
+ ///
+ /// Error message for password strength validation.
+ ///
+ private string PasswordStrengthError { get; set; } = string.Empty;
+
+ ///
+ /// Gets a value indicating whether the submit button should be enabled.
+ ///
+ private bool IsSubmitEnabled =>
+ !string.IsNullOrWhiteSpace(PasswordChangeFormModel.CurrentPassword) &&
+ !string.IsNullOrWhiteSpace(PasswordChangeFormModel.NewPassword) &&
+ !string.IsNullOrWhiteSpace(PasswordChangeFormModel.NewPasswordConfirm) &&
+ PasswordChangeFormModel.NewPassword == PasswordChangeFormModel.NewPasswordConfirm &&
+ PasswordStrengthIndicatorRef != null &&
+ PasswordStrengthIndicatorRef.MeetsMinimumRequirement();
+
+ ///
+ /// Handles real-time password input to update the strength indicator.
+ ///
+ private void OnPasswordInput(ChangeEventArgs e)
+ {
+ PasswordChangeFormModel.NewPassword = e.Value?.ToString() ?? string.Empty;
+ PasswordStrengthError = string.Empty;
+ StateHasChanged();
+ }
+
+ ///
+ /// Handles real-time confirm password input to update button state.
+ ///
+ private void OnConfirmPasswordInput(ChangeEventArgs e)
+ {
+ PasswordChangeFormModel.NewPasswordConfirm = e.Value?.ToString() ?? string.Empty;
+ StateHasChanged();
+ }
+
///
protected override async Task OnInitializedAsync()
{
@@ -157,6 +208,17 @@ else
///
private async Task InitiatePasswordChange()
{
+ // Clear any previous errors
+ PasswordStrengthError = string.Empty;
+
+ // Validate password strength before proceeding
+ if (PasswordStrengthIndicatorRef == null || !PasswordStrengthIndicatorRef.MeetsMinimumRequirement())
+ {
+ PasswordStrengthError = Localizer["PasswordStrengthTooWeakError"];
+ StateHasChanged();
+ return;
+ }
+
GlobalLoadingSpinner.Show(Localizer["ChangingPasswordMessage"]);
GlobalNotificationService.ClearMessages();
StateHasChanged();
diff --git a/apps/server/AliasVault.Client/Resources/Components/Auth/Setup/PasswordStep.en.resx b/apps/server/AliasVault.Client/Resources/Components/Auth/Setup/PasswordStep.en.resx
index c6caa6359..37af213c8 100644
--- a/apps/server/AliasVault.Client/Resources/Components/Auth/Setup/PasswordStep.en.resx
+++ b/apps/server/AliasVault.Client/Resources/Components/Auth/Setup/PasswordStep.en.resx
@@ -65,7 +65,7 @@
Success message for valid password
- Master password must be at least 10 characters long.
+ Master password must be at least 12 characters long (Good strength or higher).
Error message for password too short
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
index 4a9c21136..89b229d8e 100644
--- 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
@@ -116,4 +116,8 @@
Failed to change password. Please refresh the page and try again.
Error message when password change fails
+
+ Your new password must be at least 12 characters long (Good strength or higher).
+ Error message when password strength is too weak
+
\ No newline at end of file
diff --git a/apps/server/AliasVault.Client/Resources/ValidationMessages.en.resx b/apps/server/AliasVault.Client/Resources/ValidationMessages.en.resx
index d813bd1e6..8d27c046e 100644
--- a/apps/server/AliasVault.Client/Resources/ValidationMessages.en.resx
+++ b/apps/server/AliasVault.Client/Resources/ValidationMessages.en.resx
@@ -61,7 +61,7 @@
- The new password must be at least 10 characters long.
+ The new password must be at least 12 characters long (Good strength or higher).
Error message for password minimum length validation
@@ -69,7 +69,7 @@
Error message when password confirmation doesn't match
- Password must be at least 10 characters long.
+ Password must be at least 12 characters long (Good strength or higher).
Generic error message for password minimum length validation
diff --git a/apps/server/AliasVault.Client/wwwroot/css/tailwind.css b/apps/server/AliasVault.Client/wwwroot/css/tailwind.css
index ed57844e3..7d4920cc4 100644
--- a/apps/server/AliasVault.Client/wwwroot/css/tailwind.css
+++ b/apps/server/AliasVault.Client/wwwroot/css/tailwind.css
@@ -1539,6 +1539,12 @@ video {
margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
}
+.space-y-1\.5 > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-y-reverse: 0;
+ margin-top: calc(0.375rem * calc(1 - var(--tw-space-y-reverse)));
+ margin-bottom: calc(0.375rem * var(--tw-space-y-reverse));
+}
+
.divide-y > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0;
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
@@ -1715,6 +1721,11 @@ video {
border-color: rgb(251 191 36 / var(--tw-border-opacity));
}
+.border-blue-200 {
+ --tw-border-opacity: 1;
+ border-color: rgb(191 219 254 / var(--tw-border-opacity));
+}
+
.border-blue-700 {
--tw-border-opacity: 1;
border-color: rgb(29 78 216 / var(--tw-border-opacity));
@@ -1894,6 +1905,11 @@ video {
background-color: rgb(240 253 244 / var(--tw-bg-opacity));
}
+.bg-green-500 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(34 197 94 / var(--tw-bg-opacity));
+}
+
.bg-green-600 {
--tw-bg-opacity: 1;
background-color: rgb(22 163 74 / var(--tw-bg-opacity));
@@ -2008,6 +2024,11 @@ video {
background-color: rgb(234 179 8 / var(--tw-bg-opacity));
}
+.bg-orange-400 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(251 146 60 / var(--tw-bg-opacity));
+}
+
.bg-opacity-50 {
--tw-bg-opacity: 0.5;
}
@@ -2584,6 +2605,12 @@ video {
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
+.shadow-inner {
+ --tw-shadow: inset 0 2px 4px 0 rgb(0 0 0 / 0.05);
+ --tw-shadow-colored: inset 0 2px 4px 0 var(--tw-shadow-color);
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
+}
+
.outline-0 {
outline-width: 0px;
}
@@ -2664,6 +2691,10 @@ video {
transition-duration: 300ms;
}
+.duration-500 {
+ transition-duration: 500ms;
+}
+
.ease-in-out {
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
@@ -2672,6 +2703,10 @@ video {
transition-timing-function: linear;
}
+.ease-out {
+ transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
+}
+
.file\:mr-4::file-selector-button {
margin-right: 1rem;
}
@@ -3154,6 +3189,11 @@ video {
cursor: not-allowed;
}
+.disabled\:bg-gray-400:disabled {
+ --tw-bg-opacity: 1;
+ background-color: rgb(156 163 175 / var(--tw-bg-opacity));
+}
+
.disabled\:opacity-50:disabled {
opacity: 0.5;
}
@@ -3181,6 +3221,11 @@ video {
border-color: rgb(59 130 246 / var(--tw-border-opacity));
}
+.dark\:border-blue-800:is(.dark *) {
+ --tw-border-opacity: 1;
+ border-color: rgb(30 64 175 / var(--tw-border-opacity));
+}
+
.dark\:border-gray-400:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(156 163 175 / var(--tw-border-opacity));
@@ -3285,6 +3330,10 @@ video {
background-color: rgb(30 58 138 / var(--tw-bg-opacity));
}
+.dark\:bg-blue-900\/20:is(.dark *) {
+ background-color: rgb(30 58 138 / 0.2);
+}
+
.dark\:bg-gray-500:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(107 114 128 / var(--tw-bg-opacity));
@@ -3428,6 +3477,26 @@ video {
background-color: rgb(113 63 18 / 0.2);
}
+.dark\:bg-orange-600:is(.dark *) {
+ --tw-bg-opacity: 1;
+ background-color: rgb(234 88 12 / var(--tw-bg-opacity));
+}
+
+.dark\:bg-yellow-600:is(.dark *) {
+ --tw-bg-opacity: 1;
+ background-color: rgb(202 138 4 / var(--tw-bg-opacity));
+}
+
+.dark\:bg-orange-500:is(.dark *) {
+ --tw-bg-opacity: 1;
+ background-color: rgb(249 115 22 / var(--tw-bg-opacity));
+}
+
+.dark\:bg-green-700:is(.dark *) {
+ --tw-bg-opacity: 1;
+ background-color: rgb(21 128 61 / var(--tw-bg-opacity));
+}
+
.dark\:bg-opacity-75:is(.dark *) {
--tw-bg-opacity: 0.75;
}
@@ -3456,6 +3525,11 @@ video {
color: rgb(251 191 36 / var(--tw-text-opacity));
}
+.dark\:text-blue-200:is(.dark *) {
+ --tw-text-opacity: 1;
+ color: rgb(191 219 254 / var(--tw-text-opacity));
+}
+
.dark\:text-blue-400:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(96 165 250 / var(--tw-text-opacity));
@@ -3599,6 +3673,16 @@ video {
color: rgb(250 204 21 / var(--tw-text-opacity));
}
+.dark\:text-green-300:is(.dark *) {
+ --tw-text-opacity: 1;
+ color: rgb(134 239 172 / var(--tw-text-opacity));
+}
+
+.dark\:text-green-200:is(.dark *) {
+ --tw-text-opacity: 1;
+ color: rgb(187 247 208 / var(--tw-text-opacity));
+}
+
.dark\:placeholder-gray-400:is(.dark *)::-moz-placeholder {
--tw-placeholder-opacity: 1;
color: rgb(156 163 175 / var(--tw-placeholder-opacity));