From 658d03bc021cb9222c52aefffd86b3ca081950be Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Fri, 18 Oct 2024 14:28:50 +0200 Subject: [PATCH] Improve flow with separate creating step, minor tweaks (#306) --- .../Auth/Layout/EmptyLayout.razor | 1 - .../Pages/Setup/Components/CreatingStep.razor | 113 ++++++++++++++ .../Pages/Setup/Components/PasswordStep.razor | 25 +-- .../Components/PurposeInstructionsStep.razor | 60 -------- .../Components/TermsAndConditionsStep.razor | 12 +- .../Pages/Setup/Components/UsernameStep.razor | 1 + .../Pages/Setup/Components/WelcomeStep.razor | 3 +- .../Auth/Pages/Setup/Setup.razor | 144 +++++------------- .../Alerts/GlobalNotificationDisplay.razor | 2 +- .../wwwroot/css/tailwind.css | 33 ++++ 10 files changed, 210 insertions(+), 184 deletions(-) create mode 100644 src/AliasVault.Client/Auth/Pages/Setup/Components/CreatingStep.razor delete mode 100644 src/AliasVault.Client/Auth/Pages/Setup/Components/PurposeInstructionsStep.razor diff --git a/src/AliasVault.Client/Auth/Layout/EmptyLayout.razor b/src/AliasVault.Client/Auth/Layout/EmptyLayout.razor index 7c9a87f1b..e1a9a7567 100644 --- a/src/AliasVault.Client/Auth/Layout/EmptyLayout.razor +++ b/src/AliasVault.Client/Auth/Layout/EmptyLayout.razor @@ -1,4 +1,3 @@ @inherits LayoutComponentBase - @Body diff --git a/src/AliasVault.Client/Auth/Pages/Setup/Components/CreatingStep.razor b/src/AliasVault.Client/Auth/Pages/Setup/Components/CreatingStep.razor new file mode 100644 index 000000000..37623d9b3 --- /dev/null +++ b/src/AliasVault.Client/Auth/Pages/Setup/Components/CreatingStep.razor @@ -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 + +
+
+ + @if (IsLoading) + { +
+
+
+ } +
+
+ +@code { + private bool IsLoading { get; set; } = true; + + /// + /// The username to use for the new account. + /// + [Parameter] + public string Username { get; set; } = string.Empty; + + /// + /// The password to use for the new account. + /// + [Parameter] + public string Password { get; set; } = string.Empty; + + /// + 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(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(); + } + } +} diff --git a/src/AliasVault.Client/Auth/Pages/Setup/Components/PasswordStep.razor b/src/AliasVault.Client/Auth/Pages/Setup/Components/PasswordStep.razor index 260c65169..594e3f1b6 100644 --- a/src/AliasVault.Client/Auth/Pages/Setup/Components/PasswordStep.razor +++ b/src/AliasVault.Client/Auth/Pages/Setup/Components/PasswordStep.razor @@ -11,7 +11,7 @@ }
-
+
AliasVault Assistant @@ -20,21 +20,21 @@

Great! Now, let's set up your master password for AliasVault.

-

- Please enter a strong master password for your account. Your username is: @Username -

-

- 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. -

-
    -
  • Your master password never leaves your device
  • -
  • The server has no access to your unencrypted data
  • -
  • Even the server admin cannot restore your access if you forget this password
  • -
+
+

+ 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. +

+
    +
  • Your master password never leaves your device
  • +
  • The server has no access to your unencrypted data
  • +
  • Even the server admin cannot restore your access if you forget this password
  • +
+
+
@@ -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 diff --git a/src/AliasVault.Client/Auth/Pages/Setup/Components/PurposeInstructionsStep.razor b/src/AliasVault.Client/Auth/Pages/Setup/Components/PurposeInstructionsStep.razor deleted file mode 100644 index 23a69e271..000000000 --- a/src/AliasVault.Client/Auth/Pages/Setup/Components/PurposeInstructionsStep.razor +++ /dev/null @@ -1,60 +0,0 @@ -@using AliasVault.Client.Auth.Pages.Setup.Components - -
-
-
-
- AliasVault Assistant -
-
-

- Great choice! Here are some tips for using AliasVault for @Purpose: -

-
    - @foreach (var instruction in GetInstructions()) - { -
  • @instruction
  • - } -
-
-
-
-
- -@code { - [Parameter] - public string Purpose { get; set; } = string.Empty; - - [Parameter] - public EventCallback OnNext { get; set; } - - [Parameter] - public EventCallback OnBack { get; set; } - - private List GetInstructions() - { - return Purpose switch - { - "Personal Security" => new List - { - "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 - { - "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 - { - "Explore the different features of AliasVault", - "Use the help section for detailed guides on each feature", - "Contact support if you need any assistance" - } - }; - } -} diff --git a/src/AliasVault.Client/Auth/Pages/Setup/Components/TermsAndConditionsStep.razor b/src/AliasVault.Client/Auth/Pages/Setup/Components/TermsAndConditionsStep.razor index 7b6180670..23ab06e35 100644 --- a/src/AliasVault.Client/Auth/Pages/Setup/Components/TermsAndConditionsStep.razor +++ b/src/AliasVault.Client/Auth/Pages/Setup/Components/TermsAndConditionsStep.razor @@ -9,7 +9,6 @@ }
-

Using AliasVault

Please read and agree to the following terms and conditions before proceeding.

@@ -30,7 +29,7 @@

- + @@ -40,12 +39,17 @@
@code { + /// + /// Gets or sets a value indicating whether the user has agreed to the terms and conditions. + /// + [Parameter] + public bool AgreedToTerms { get; set; } + [Parameter] public EventCallback 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); } } diff --git a/src/AliasVault.Client/Auth/Pages/Setup/Components/UsernameStep.razor b/src/AliasVault.Client/Auth/Pages/Setup/Components/UsernameStep.razor index ffa010ab0..d28401d09 100644 --- a/src/AliasVault.Client/Auth/Pages/Setup/Components/UsernameStep.razor +++ b/src/AliasVault.Client/Auth/Pages/Setup/Components/UsernameStep.razor @@ -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 diff --git a/src/AliasVault.Client/Auth/Pages/Setup/Components/WelcomeStep.razor b/src/AliasVault.Client/Auth/Pages/Setup/Components/WelcomeStep.razor index 4bc6c427d..dd2b662ed 100644 --- a/src/AliasVault.Client/Auth/Pages/Setup/Components/WelcomeStep.razor +++ b/src/AliasVault.Client/Auth/Pages/Setup/Components/WelcomeStep.razor @@ -1,6 +1,5 @@
-

Welcome to AliasVault

-

+

AliasVault is a secure app which help you manage your online identities and passwords. Let's get you set up with your new vault.

diff --git a/src/AliasVault.Client/Auth/Pages/Setup/Setup.razor b/src/AliasVault.Client/Auth/Pages/Setup/Setup.razor index 723630ef8..6cc2a2bf9 100644 --- a/src/AliasVault.Client/Auth/Pages/Setup/Setup.razor +++ b/src/AliasVault.Client/Auth/Pages/Setup/Setup.razor @@ -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
@@ -22,25 +16,33 @@
+
+

@GetStepTitle(currentStep)

+
+ @if (GetProgressPercentage() > 0) + { +
+
+
+ } @switch (currentStep) { case SetupStep.Welcome: break; - case SetupStep.Purpose: - + case SetupStep.TermsAndConditions: + break; - case SetupStep.PurposeInstructions: - - break; - case SetupStep.UserInfo: + case SetupStep.Username: @@ -50,17 +52,20 @@ Username="@setupData.Username" OnPasswordChange="@((string pwd) => { setupData.Password = pwd; StateHasChanged(); })" /> break; + case SetupStep.Creating: + + break; }
@if (currentStep == SetupStep.Password && !string.IsNullOrWhiteSpace(setupData.Password)) { - } - else + else if (currentStep != SetupStep.Creating) {
- - @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(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" }; } diff --git a/src/AliasVault.Client/Main/Components/Alerts/GlobalNotificationDisplay.razor b/src/AliasVault.Client/Main/Components/Alerts/GlobalNotificationDisplay.razor index 05a687c65..b4d820301 100644 --- a/src/AliasVault.Client/Main/Components/Alerts/GlobalNotificationDisplay.razor +++ b/src/AliasVault.Client/Main/Components/Alerts/GlobalNotificationDisplay.razor @@ -7,7 +7,7 @@ return; } -
+
@foreach (var message in Messages) { if (message.Key == "success") diff --git a/src/AliasVault.Client/wwwroot/css/tailwind.css b/src/AliasVault.Client/wwwroot/css/tailwind.css index 18496de2d..ab332048d 100644 --- a/src/AliasVault.Client/wwwroot/css/tailwind.css +++ b/src/AliasVault.Client/wwwroot/css/tailwind.css @@ -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));