mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-20 15:41:40 -04:00
Add password step and working account creation (#306)
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
@using System.Timers
|
||||
@using System.Net.Http.Json
|
||||
@implements IDisposable
|
||||
|
||||
<div class="w-full max-w-md mx-auto">
|
||||
@if (isLoading)
|
||||
{
|
||||
<div class="flex justify-center items-center h-64">
|
||||
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-primary-500"></div>
|
||||
</div>
|
||||
}
|
||||
<div class="@(isLoading ? "opacity-0" : "opacity-100 transition-opacity duration-300") w-full">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 mb-6">
|
||||
<div class="flex items-start mb-4">
|
||||
<div class="flex-shrink-0">
|
||||
<img class="h-10 w-10 rounded-full" src="/img/avatar.webp" alt="AliasVault Assistant">
|
||||
</div>
|
||||
<div class="ml-3 bg-blue-100 dark:bg-blue-900 rounded-lg p-3">
|
||||
<p class="text-sm text-gray-900 dark:text-white">
|
||||
Great! Now, let's set up your master password for AliasVault.
|
||||
</p>
|
||||
<p class="text-sm text-gray-900 dark:text-white mt-3">
|
||||
Please enter a strong master password for your account. Your username is: <strong>@Username</strong>
|
||||
</p>
|
||||
<p class="text-sm text-gray-900 dark:text-white mt-3 font-semibold">
|
||||
Important: This master password will be used to encrypt your vault. It should be a long, complex string that you can remember. If you forget this password, your data will be permanently inaccessible.
|
||||
</p>
|
||||
<ul class="text-sm text-gray-900 dark:text-white mt-3 list-disc list-inside">
|
||||
<li>Your master password never leaves your device</li>
|
||||
<li>The server has no access to your unencrypted data</li>
|
||||
<li>Even the server admin cannot restore your access if you forget this password</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<div class="">
|
||||
<EditFormRow Id="password" Label="Master Password" @bind-Value="Password" Type="password" Placeholder="Enter your master password" OnFocus="@OnPasswordInputFocus"/>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<EditFormRow Id="confirmPassword" Label="Confirm Master Password" @bind-Value="ConfirmPassword" Type="password" Placeholder="Confirm your master password" OnFocus="@OnPasswordInputFocus" />
|
||||
</div>
|
||||
@if (isValidating)
|
||||
{
|
||||
<div class="mt-2 text-sm text-gray-600 dark:text-gray-400">Validating password...</div>
|
||||
}
|
||||
else if (isValid)
|
||||
{
|
||||
@if (!string.IsNullOrEmpty(errorMessage))
|
||||
{
|
||||
<div class="mt-2 text-sm text-yellow-600 dark:text-yellow-400">@errorMessage</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="mt-2 text-sm text-green-600 dark:text-green-400">Password is valid and strong!</div>
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(errorMessage))
|
||||
{
|
||||
<div class="mt-2 text-sm text-red-600 dark:text-red-400">@errorMessage</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string _password = string.Empty;
|
||||
private string _confirmPassword = string.Empty;
|
||||
private bool isValid = false;
|
||||
private bool isValidating = false;
|
||||
private string errorMessage = string.Empty;
|
||||
private Timer? debounceTimer;
|
||||
|
||||
[Inject]
|
||||
private HttpClient Http { get; set; } = null!;
|
||||
|
||||
[Parameter]
|
||||
public string Username { get; set; } = string.Empty;
|
||||
|
||||
public string Password
|
||||
{
|
||||
get => _password;
|
||||
set
|
||||
{
|
||||
if (_password != value)
|
||||
{
|
||||
_password = value;
|
||||
ValidatePassword();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string ConfirmPassword
|
||||
{
|
||||
get => _confirmPassword;
|
||||
set
|
||||
{
|
||||
if (_confirmPassword != value)
|
||||
{
|
||||
_confirmPassword = value;
|
||||
ValidatePassword();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<string> OnPasswordChange { get; set; }
|
||||
|
||||
private bool isLoading = true;
|
||||
private Timer? loadingTimer;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
loadingTimer = new Timer(300);
|
||||
loadingTimer.Elapsed += (sender, e) => FinishLoading();
|
||||
loadingTimer.AutoReset = false;
|
||||
loadingTimer.Start();
|
||||
|
||||
debounceTimer = new Timer(300);
|
||||
debounceTimer.Elapsed += async (sender, e) => await ValidatePasswordDebounced();
|
||||
debounceTimer.AutoReset = false;
|
||||
}
|
||||
|
||||
private void FinishLoading()
|
||||
{
|
||||
isLoading = false;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private void ValidatePassword()
|
||||
{
|
||||
isValidating = true;
|
||||
isValid = false;
|
||||
errorMessage = string.Empty;
|
||||
StateHasChanged();
|
||||
|
||||
debounceTimer?.Stop();
|
||||
debounceTimer?.Start();
|
||||
}
|
||||
|
||||
private async Task ValidatePasswordDebounced()
|
||||
{
|
||||
await InvokeAsync(async () =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Password) || string.IsNullOrWhiteSpace(ConfirmPassword))
|
||||
{
|
||||
isValidating = false;
|
||||
isValid = false;
|
||||
errorMessage = "Both password fields are required.";
|
||||
await OnPasswordChange.InvokeAsync(string.Empty);
|
||||
StateHasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Password != ConfirmPassword)
|
||||
{
|
||||
isValidating = false;
|
||||
isValid = false;
|
||||
errorMessage = "Passwords do not match.";
|
||||
await OnPasswordChange.InvokeAsync(string.Empty);
|
||||
StateHasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Password.Length < 10)
|
||||
{
|
||||
isValidating = false;
|
||||
isValid = false;
|
||||
errorMessage = "Master password must be at least 10 characters long.";
|
||||
await OnPasswordChange.InvokeAsync(string.Empty);
|
||||
StateHasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
// If password is valid
|
||||
isValid = true;
|
||||
errorMessage = string.Empty;
|
||||
|
||||
// Show warning for passwords between 10 and 13 characters
|
||||
if (Password.Length < 14)
|
||||
{
|
||||
errorMessage = "Password is acceptable, but can be improved when longer.";
|
||||
}
|
||||
|
||||
await OnPasswordChange.InvokeAsync(Password);
|
||||
|
||||
isValidating = false;
|
||||
StateHasChanged();
|
||||
});
|
||||
}
|
||||
|
||||
private void OnPasswordInputFocus(FocusEventArgs args)
|
||||
{
|
||||
// Reset validation state when the input is focused
|
||||
isValid = false;
|
||||
isValidating = false;
|
||||
errorMessage = string.Empty;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
loadingTimer?.Dispose();
|
||||
debounceTimer?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,12 @@
|
||||
@inherits AliasVault.Client.Auth.Pages.Base.LoginBase
|
||||
@layout Auth.Layout.EmptyLayout
|
||||
@attribute [AllowAnonymous]
|
||||
@inject IConfiguration Configuration
|
||||
@using System.Text.Json
|
||||
@using AliasVault.Shared.Models.WebApi.Auth
|
||||
@using AliasVault.Client.Utilities
|
||||
@using AliasVault.Cryptography.Client
|
||||
@using SecureRemotePassword
|
||||
|
||||
<div class="flex min-h-screen bg-gray-100 dark:bg-gray-900">
|
||||
<div class="w-full max-w-md mx-auto flex flex-col @(currentStep == SetupStep.Welcome ? "justify-center" : "pt-8")">
|
||||
@@ -25,11 +31,11 @@
|
||||
<UsernameStep
|
||||
OnUsernameChange="@((string username) => { setupData.Username = username; StateHasChanged(); })" />
|
||||
break;
|
||||
/*case SetupStep.Password:
|
||||
case SetupStep.Password:
|
||||
<PasswordStep
|
||||
Password="@setupData.Password"
|
||||
Username="@setupData.Username"
|
||||
OnPasswordChange="@((string pwd) => { setupData.Password = pwd; StateHasChanged(); })" />
|
||||
break; */
|
||||
break;
|
||||
}
|
||||
</div>
|
||||
<div class="fixed bottom-0 left-0 right-0 p-8 bg-gray-100 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700">
|
||||
@@ -43,7 +49,14 @@
|
||||
}
|
||||
</div>
|
||||
<div class="lg:flex-shrink-0">
|
||||
@if (currentStep != SetupStep.Password)
|
||||
@if (currentStep == SetupStep.Password && !string.IsNullOrWhiteSpace(setupData.Password))
|
||||
{
|
||||
<button @onclick="CompleteSetup"
|
||||
class="w-full lg:w-auto py-3 px-4 bg-green-600 hover:bg-green-700 text-white font-semibold rounded-lg transition duration-300 ease-in-out">
|
||||
Create Account
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button @onclick="GoNext"
|
||||
class="w-full lg:w-auto py-3 px-4 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition duration-300 ease-in-out @(isNextEnabled ? "" : "opacity-50 cursor-not-allowed")"
|
||||
@@ -54,10 +67,18 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button @onclick="CancelSetup" class="absolute top-4 right-4 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FullScreenLoadingIndicator @ref="LoadingIndicator"/>
|
||||
|
||||
@code {
|
||||
private FullScreenLoadingIndicator LoadingIndicator = new();
|
||||
private SetupStep currentStep = SetupStep.Welcome;
|
||||
private SetupData setupData = new();
|
||||
private bool isNextEnabled => currentStep switch
|
||||
@@ -101,10 +122,73 @@
|
||||
};
|
||||
}
|
||||
|
||||
private void CompleteSetup()
|
||||
private async Task CompleteSetup()
|
||||
{
|
||||
LoadingIndicator.Show();
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
var client = new SrpClient();
|
||||
var salt = client.GenerateSalt();
|
||||
|
||||
string encryptionType = Defaults.EncryptionType;
|
||||
string encryptionSettings = Defaults.EncryptionSettings;
|
||||
if (Configuration["CryptographyOverrideType"] is not null && Configuration["CryptographyOverrideSettings"] is not null)
|
||||
{
|
||||
encryptionType = Configuration["CryptographyOverrideType"]!;
|
||||
encryptionSettings = Configuration["CryptographyOverrideSettings"]!;
|
||||
}
|
||||
|
||||
var passwordHash = await Encryption.DeriveKeyFromPasswordAsync(setupData.Password, salt, encryptionType, encryptionSettings);
|
||||
var passwordHashString = BitConverter.ToString(passwordHash).Replace("-", string.Empty);
|
||||
var srpSignup = Srp.PasswordChangeAsync(client, salt, setupData.Username, passwordHashString);
|
||||
|
||||
var registerRequest = new RegisterRequest(srpSignup.Username, srpSignup.Salt, srpSignup.Verifier, encryptionType, encryptionSettings);
|
||||
var result = await Http.PostAsJsonAsync("api/v1/Auth/register", registerRequest);
|
||||
var responseContent = await result.Content.ReadAsStringAsync();
|
||||
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
foreach (var error in ApiResponseUtility.ParseErrorResponse(responseContent))
|
||||
{
|
||||
GlobalNotificationService.AddErrorMessage(error);
|
||||
}
|
||||
StateHasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
var tokenObject = JsonSerializer.Deserialize<TokenModel>(responseContent);
|
||||
|
||||
if (tokenObject != null)
|
||||
{
|
||||
await AuthService.StoreEncryptionKeyAsync(passwordHash);
|
||||
await AuthService.StoreAccessTokenAsync(tokenObject.Token);
|
||||
await AuthService.StoreRefreshTokenAsync(tokenObject.RefreshToken);
|
||||
await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
|
||||
GlobalNotificationService.AddSuccessMessage("Account created successfully!");
|
||||
NavigationManager.NavigateTo("/");
|
||||
}
|
||||
else
|
||||
{
|
||||
GlobalNotificationService.AddErrorMessage("An error occurred during registration.");
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
GlobalNotificationService.AddErrorMessage($"An error occurred: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
LoadingIndicator.Hide();
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelSetup()
|
||||
{
|
||||
// TODO: Implement the logic to save user information and create the account
|
||||
// For example: await UserService.CreateUser(setupData.UserInfo, setupData.Password);
|
||||
NavigationManager.NavigateTo("/");
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<input type="text" id="@Id" autocomplete="off" @onfocus="OnFocusEvent" class="outline-0 shadow-sm bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg block w-full p-2.5 pr-10 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" value="@Value" @oninput="OnInputChanged" placeholder="@Placeholder">
|
||||
<input type="@Type" id="@Id" autocomplete="off" @onfocus="OnFocusEvent" class="outline-0 shadow-sm bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg block w-full p-2.5 pr-10 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" value="@Value" @oninput="OnInputChanged" placeholder="@Placeholder">
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -678,6 +678,14 @@ video {
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
.right-4 {
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.top-4 {
|
||||
top: 1rem;
|
||||
}
|
||||
|
||||
.z-10 {
|
||||
z-index: 10;
|
||||
}
|
||||
@@ -1923,6 +1931,11 @@ video {
|
||||
color: rgb(133 77 14 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-yellow-600 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(202 138 4 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.opacity-25 {
|
||||
opacity: 0.25;
|
||||
}
|
||||
@@ -2184,6 +2197,11 @@ video {
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:text-gray-700:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(55 65 81 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.hover\:underline:hover {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
@@ -2517,6 +2535,11 @@ video {
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-yellow-400:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(250 204 21 / 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));
|
||||
@@ -2601,6 +2624,11 @@ video {
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:text-gray-200:hover:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(229 231 235 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:focus\:border-blue-500:focus:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(59 130 246 / var(--tw-border-opacity));
|
||||
|
||||
Reference in New Issue
Block a user