Improve flow with separate creating step, minor tweaks (#306)

This commit is contained in:
Leendert de Borst
2024-10-18 14:28:50 +02:00
parent 862f013bda
commit 658d03bc02
10 changed files with 210 additions and 184 deletions

View File

@@ -1,4 +1,3 @@
@inherits LayoutComponentBase
<GlobalNotificationDisplay />
@Body

View File

@@ -0,0 +1,113 @@
@inherits AliasVault.Client.Auth.Pages.Base.LoginBase
@layout Auth.Layout.EmptyLayout
@attribute [AllowAnonymous]
@inject IConfiguration Configuration
@using System.Text.Json
@using AliasVault.Client.Utilities
@using AliasVault.Cryptography.Client
@using AliasVault.Shared.Models.WebApi.Auth
@using SecureRemotePassword
<div class="w-full mx-auto">
<div class="relative inset-0 mt-10 z-10">
<GlobalNotificationDisplay />
@if (IsLoading)
{
<div class="flex justify-center items-center">
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-primary-500"></div>
</div>
}
</div>
</div>
@code {
private bool IsLoading { get; set; } = true;
/// <summary>
/// The username to use for the new account.
/// </summary>
[Parameter]
public string Username { get; set; } = string.Empty;
/// <summary>
/// The password to use for the new account.
/// </summary>
[Parameter]
public string Password { get; set; } = string.Empty;
/// <inheritdoc />
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
await CompleteSetup();
}
}
private async Task CompleteSetup()
{
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(Password, salt, encryptionType, encryptionSettings);
var passwordHashString = BitConverter.ToString(passwordHash).Replace("-", string.Empty);
var srpSignup = Srp.PasswordChangeAsync(client, salt, 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, true);
}
IsLoading = false;
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();
NavigationManager.NavigateTo("/");
}
else
{
IsLoading = false;
GlobalNotificationService.AddErrorMessage("An error occurred during registration.", true);
StateHasChanged();
}
}
catch (Exception ex)
{
GlobalNotificationService.AddErrorMessage($"An error occurred: {ex.Message}", true);
}
finally
{
IsLoading = false;
StateHasChanged();
}
}
}

View File

@@ -11,7 +11,7 @@
</div>
}
<div class="@(isLoading ? "invisible opacity-0" : "opacity-100") transition-opacity duration-300 w-full">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg lg:shadow-none p-6 mb-6">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg lg:shadow-none p-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">
@@ -20,21 +20,21 @@
<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="p-4 mb-6 bg-gray-100 dark:bg-gray-900 rounded-lg text-gray-900 dark:text-gray-100">
<p class="text-sm 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 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 class="space-y-4">
<div>
<div class="">
@@ -127,6 +127,7 @@
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
await Task.Delay(100); // Give time for the DOM to update

View File

@@ -1,60 +0,0 @@
@using AliasVault.Client.Auth.Pages.Setup.Components
<div class="w-full max-w-md mx-auto">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg lg:shadow-none 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 choice! Here are some tips for using AliasVault for @Purpose:
</p>
<ul class="list-disc list-inside text-sm text-gray-900 dark:text-white mt-3">
@foreach (var instruction in GetInstructions())
{
<li>@instruction</li>
}
</ul>
</div>
</div>
</div>
</div>
@code {
[Parameter]
public string Purpose { get; set; } = string.Empty;
[Parameter]
public EventCallback OnNext { get; set; }
[Parameter]
public EventCallback OnBack { get; set; }
private List<string> GetInstructions()
{
return Purpose switch
{
"Personal Security" => new List<string>
{
"Create unique aliases for each of your online accounts",
"Use the password generator to create strong, unique passwords",
"Enable two-factor authentication for added security",
"Regularly review your aliases and remove unused ones"
},
"Privacy Protection" => new List<string>
{
"Use different aliases for different types of online activities",
"Take advantage of the disposable email feature for temporary signups",
"Use the notes feature to keep track of which alias is used where",
"Regularly check the email forwarding settings for each alias"
},
_ => new List<string>
{
"Explore the different features of AliasVault",
"Use the help section for detailed guides on each feature",
"Contact support if you need any assistance"
}
};
}
}

View File

@@ -9,7 +9,6 @@
}
<div class="@(isLoading ? "invisible opacity-0" : "opacity-100") transition-opacity duration-300 w-full">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg lg:shadow-none p-6">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Using AliasVault</h2>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
Please read and agree to the following terms and conditions before proceeding.
</p>
@@ -30,7 +29,7 @@
</p>
</div>
<div class="flex items-center">
<input type="checkbox" id="agreeTerms" @bind="agreedToTerms" @bind:after="OnAgreedToTerms" class="mr-2">
<input type="checkbox" id="agreeTerms" @bind="AgreedToTerms" @bind:after="OnAgreedToTerms" class="mr-2">
<label for="agreeTerms" class="text-sm font-bold text-gray-600 dark:text-gray-400">
I have read and agree to the Terms and Conditions
</label>
@@ -40,12 +39,17 @@
</div>
@code {
/// <summary>
/// Gets or sets a value indicating whether the user has agreed to the terms and conditions.
/// </summary>
[Parameter]
public bool AgreedToTerms { get; set; }
[Parameter]
public EventCallback<bool> OnAgreedToTermsChanged { get; set; }
private bool isLoading = true;
private Timer? loadingTimer;
private bool agreedToTerms = false;
protected override void OnInitialized()
{
@@ -68,6 +72,6 @@
private async Task OnAgreedToTerms()
{
await OnAgreedToTermsChanged.InvokeAsync(agreedToTerms);
await OnAgreedToTermsChanged.InvokeAsync(AgreedToTerms);
}
}

View File

@@ -111,6 +111,7 @@
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
await Task.Delay(100); // Give time for the DOM to update

View File

@@ -1,6 +1,5 @@
<div class="w-full max-w-md mx-auto">
<h2 class="mt-16 text-2xl font-bold text-gray-900 dark:text-white mb-4 text-center">Welcome to AliasVault</h2>
<p class="text-gray-600 dark:text-gray-400 mb-8 text-center">
<p class="mt-16 text-gray-600 dark:text-gray-400 mb-8 text-center">
AliasVault is a secure app which help you manage your online identities and passwords.
Let's get you set up with your new vault.
</p>

View File

@@ -3,12 +3,6 @@
@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="min-h-screen bg-gray-100 dark:bg-gray-900 flex items-center justify-center">
<div class="w-full mx-auto lg:max-w-xl lg:bg-white lg:dark:bg-gray-800 lg:shadow-xl lg:rounded-lg lg:overflow-hidden">
@@ -22,25 +16,33 @@
</svg>
</button>
</div>
<div class="flex-grow text-center">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">@GetStepTitle(currentStep)</h2>
</div>
<button @onclick="CancelSetup" class="text-gray-500 -mt-1 hover:text-gray-800 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>
@if (GetProgressPercentage() > 0)
{
<div class="w-full bg-gray-200 rounded-full h-2.5 mb-4 dark:bg-gray-700 mt-4">
<div class="bg-primary-600 h-2.5 rounded-full" style="width: @(GetProgressPercentage())%"></div>
</div>
}
@switch (currentStep)
{
case SetupStep.Welcome:
<WelcomeStep />
break;
case SetupStep.Purpose:
<TermsAndConditionsStep OnAgreedToTermsChanged="@HandleAgreedToTermsChanged" />
case SetupStep.TermsAndConditions:
<TermsAndConditionsStep
AgreedToTerms="@setupData.AgreedToTerms"
OnAgreedToTermsChanged="@HandleAgreedToTermsChanged" />
break;
case SetupStep.PurposeInstructions:
<PurposeInstructionsStep />
break;
case SetupStep.UserInfo:
case SetupStep.Username:
<UsernameStep
DefaultUsername="@setupData.Username"
OnUsernameChange="@((string username) => { setupData.Username = username; StateHasChanged(); })" />
@@ -50,17 +52,20 @@
Username="@setupData.Username"
OnPasswordChange="@((string pwd) => { setupData.Password = pwd; StateHasChanged(); })" />
break;
case SetupStep.Creating:
<CreatingStep Username="@setupData.Username" Password="@setupData.Password" />
break;
}
</div>
<div class="p-8 bg-gray-100 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 lg:bg-transparent lg:dark:bg-transparent lg:border-0">
@if (currentStep == SetupStep.Password && !string.IsNullOrWhiteSpace(setupData.Password))
{
<button @onclick="CompleteSetup"
<button @onclick="GoNext"
class="w-full 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
else if (currentStep != SetupStep.Creating)
{
<button @onclick="GoNext"
class="w-full 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")"
@@ -73,18 +78,14 @@
</div>
</div>
<FullScreenLoadingIndicator @ref="LoadingIndicator"/>
@code {
private FullScreenLoadingIndicator LoadingIndicator = new();
private SetupStep currentStep = SetupStep.Welcome;
private SetupData setupData = new();
private bool isNextEnabled => currentStep switch
{
SetupStep.Welcome => true,
SetupStep.Purpose => setupData.AgreedToTerms,
SetupStep.PurposeInstructions => true,
SetupStep.UserInfo => !string.IsNullOrWhiteSpace(setupData.Username),
SetupStep.TermsAndConditions => setupData.AgreedToTerms,
SetupStep.Username => !string.IsNullOrWhiteSpace(setupData.Username),
SetupStep.Password => !string.IsNullOrWhiteSpace(setupData.Password),
_ => false
};
@@ -93,17 +94,17 @@
{
switch (currentStep)
{
case SetupStep.Purpose:
case SetupStep.TermsAndConditions:
currentStep = SetupStep.Welcome;
break;
case SetupStep.PurposeInstructions:
currentStep = SetupStep.Purpose;
break;
case SetupStep.UserInfo:
currentStep = SetupStep.PurposeInstructions;
case SetupStep.Username:
currentStep = SetupStep.TermsAndConditions;
break;
case SetupStep.Password:
currentStep = SetupStep.UserInfo;
currentStep = SetupStep.Username;
break;
case SetupStep.Creating:
currentStep = SetupStep.Password;
break;
}
}
@@ -112,79 +113,14 @@
{
currentStep = currentStep switch
{
SetupStep.Welcome => SetupStep.Purpose,
SetupStep.Purpose => SetupStep.PurposeInstructions,
SetupStep.PurposeInstructions => SetupStep.UserInfo,
SetupStep.UserInfo => SetupStep.Password,
SetupStep.Welcome => SetupStep.TermsAndConditions,
SetupStep.TermsAndConditions => SetupStep.Username,
SetupStep.Username => SetupStep.Password,
SetupStep.Password => SetupStep.Creating,
_ => currentStep
};
}
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()
{
NavigationManager.NavigateTo("/");
@@ -199,10 +135,10 @@
private enum SetupStep
{
Welcome,
Purpose,
PurposeInstructions,
UserInfo,
Password
TermsAndConditions,
Username,
Password,
Creating
}
private class SetupData
@@ -216,11 +152,11 @@
{
return step switch
{
SetupStep.Welcome => "Welcome",
SetupStep.Purpose => "Terms and Conditions",
SetupStep.PurposeInstructions => "Purpose Instructions",
SetupStep.UserInfo => "Choose Username",
SetupStep.Welcome => "Welcome to AliasVault",
SetupStep.TermsAndConditions => "Using AliasVault",
SetupStep.Username => "Choose Username",
SetupStep.Password => "Set Password",
SetupStep.Creating => "Creating Vault",
_ => "Setup"
};
}

View File

@@ -7,7 +7,7 @@
return;
}
<div class="messages-container grid px-4 pt-6 lg:gap-4 dark:bg-gray-900">
<div class="messages-container grid px-4 pt-6 lg:gap-4">
@foreach (var message in Messages)
{
if (message.Key == "success")

View File

@@ -849,6 +849,14 @@ video {
margin-top: 4rem;
}
.mb-10 {
margin-bottom: 2.5rem;
}
.mt-10 {
margin-top: 2.5rem;
}
.line-clamp-2 {
overflow: hidden;
display: -webkit-box;
@@ -1361,6 +1369,11 @@ video {
border-color: rgb(214 131 56 / var(--tw-border-opacity));
}
.border-yellow-500 {
--tw-border-opacity: 1;
border-color: rgb(234 179 8 / var(--tw-border-opacity));
}
.bg-blue-100 {
--tw-bg-opacity: 1;
background-color: rgb(219 234 254 / var(--tw-bg-opacity));
@@ -1506,6 +1519,11 @@ video {
background-color: rgb(254 252 232 / var(--tw-bg-opacity));
}
.bg-yellow-100 {
--tw-bg-opacity: 1;
background-color: rgb(254 249 195 / var(--tw-bg-opacity));
}
.bg-opacity-50 {
--tw-bg-opacity: 0.5;
}
@@ -1913,6 +1931,11 @@ video {
color: rgb(133 77 14 / var(--tw-text-opacity));
}
.text-yellow-700 {
--tw-text-opacity: 1;
color: rgb(161 98 7 / var(--tw-text-opacity));
}
.opacity-0 {
opacity: 0;
}
@@ -2383,6 +2406,11 @@ video {
background-color: rgb(133 77 14 / var(--tw-bg-opacity));
}
.dark\:bg-yellow-900:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(113 63 18 / var(--tw-bg-opacity));
}
.dark\:bg-opacity-80:is(.dark *) {
--tw-bg-opacity: 0.8;
}
@@ -2462,6 +2490,11 @@ video {
color: rgb(250 204 21 / var(--tw-text-opacity));
}
.dark\:text-yellow-200:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(254 240 138 / 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));