From eacfee78cc848ad53eed532b7e8e60cf2ea18b02 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Mon, 5 Aug 2024 11:05:51 +0200 Subject: [PATCH] Refactor SettingsService structure so it initializes when the DbService itself is ready (#145) --- .../Main/Pages/Settings/General.razor | 164 +++++------------- src/AliasVault.Client/Program.cs | 1 - .../Services/Database/DbService.cs | 13 +- .../Services/SettingsService.cs | 89 +++++++--- .../wwwroot/css/tailwind.css | 38 ++++ 5 files changed, 162 insertions(+), 143 deletions(-) diff --git a/src/AliasVault.Client/Main/Pages/Settings/General.razor b/src/AliasVault.Client/Main/Pages/Settings/General.razor index 83efb19bb..de65dc663 100644 --- a/src/AliasVault.Client/Main/Pages/Settings/General.razor +++ b/src/AliasVault.Client/Main/Pages/Settings/General.razor @@ -1,62 +1,63 @@ @page "/settings/general" @inherits MainBase -@inject CredentialService CredentialService General settings
-
-

General settings

-
-

On this page you can configure general AliasVault settings.

+

General settings

+

Configure general AliasVault settings.

-
-

Export vault

+
+

Email Settings

+
-
- -
-
- -
+ + +
+ +
+ +
-
-

Import vault

-
-
- Import unencrypted CSV file: - -
-
-
- -@if (IsImporting) -{ -

Loading...

-} -else if (!string.IsNullOrEmpty(ImportErrorMessage)) -{ -

@ImportErrorMessage

-} -else if (!string.IsNullOrEmpty(ImportSuccessMessage)) -{ -

@ImportSuccessMessage

-} @code { - private bool IsImporting = false; - private string ImportErrorMessage = string.Empty; - private string ImportSuccessMessage = string.Empty; + private List PrivateDomains => Config.PrivateEmailDomains; + private List PublicDomains => Config.PublicEmailDomains; + private bool ShowPrivateDomains => PrivateDomains.Count > 0 && !(PrivateDomains.Count == 1 && PrivateDomains[0] == "DISABLED.TLD"); + + private string DefaultEmailDomain + { + get => DbService.Settings.DefaultEmailDomain; + set => DbService.Settings.SetDefaultEmailDomain(value).Wait(); + } + + private bool AutoEmailRefresh + { + get => DbService.Settings.AutoEmailRefresh; + set => DbService.Settings.SetAutoEmailRefreshAsync(value).Wait(); + } /// protected override async Task OnInitializedAsync() @@ -64,83 +65,4 @@ else if (!string.IsNullOrEmpty(ImportSuccessMessage)) await base.OnInitializedAsync(); BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Vault settings" }); } - - private async Task ExportVaultSqlite() - { - try - { - // Decode the base64 string to a byte array - byte[] fileBytes = Convert.FromBase64String(await DbService.ExportSqliteToBase64Async()); - - // Create a memory stream from the byte array - using (MemoryStream memoryStream = new MemoryStream(fileBytes)) - { - // Invoke JavaScript to initiate the download - await JsInteropService.DownloadFileFromStream("aliasvault-client.sqlite", memoryStream.ToArray()); - } - } - catch (Exception ex) - { - Console.WriteLine($"Error downloading file: {ex.Message}"); - } - } - - private async Task ExportVaultCsv() - { - try - { - var credentials = await CredentialService.LoadAllAsync(); - - var csvBytes = CsvImportExport.CredentialCsvService.ExportCredentialsToCsv(credentials); - - // Create a memory stream from the byte array - using (MemoryStream memoryStream = new MemoryStream(csvBytes)) - { - // Invoke JavaScript to initiate the download - await JsInteropService.DownloadFileFromStream("aliasvault-client.csv", memoryStream.ToArray()); - } - } - catch (Exception ex) - { - Console.WriteLine($"Error downloading file: {ex.Message}"); - } - } - - private async Task LoadFiles(InputFileChangeEventArgs e) - { - IsImporting = true; - StateHasChanged(); - ImportErrorMessage = string.Empty; - ImportSuccessMessage = string.Empty; - - try - { - var file = e.File; - var buffer = new byte[file.Size]; - await file.OpenReadStream().ReadAsync(buffer); - var fileContent = System.Text.Encoding.UTF8.GetString(buffer); - - var importedCredentials = CsvImportExport.CredentialCsvService.ImportCredentialsFromCsv(fileContent); - - // Loop through the imported credentials and actually add them to the database - foreach (var importedCredential in importedCredentials) - { - await CredentialService.InsertEntryAsync(importedCredential, false); - } - - // Save the database. - await DbService.SaveDatabaseAsync(); - - ImportSuccessMessage = $"Succesfully imported {importedCredentials.Count} credentials."; - } - catch (Exception ex) - { - ImportErrorMessage = $"Error importing file: {ex.Message}"; - } - finally - { - IsImporting = false; - StateHasChanged(); - } - } } diff --git a/src/AliasVault.Client/Program.cs b/src/AliasVault.Client/Program.cs index c06708327..576c3be60 100644 --- a/src/AliasVault.Client/Program.cs +++ b/src/AliasVault.Client/Program.cs @@ -68,7 +68,6 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddAuthorizationCore(); diff --git a/src/AliasVault.Client/Services/Database/DbService.cs b/src/AliasVault.Client/Services/Database/DbService.cs index 5a70c3841..b859d5d94 100644 --- a/src/AliasVault.Client/Services/Database/DbService.cs +++ b/src/AliasVault.Client/Services/Database/DbService.cs @@ -27,6 +27,7 @@ public class DbService : IDisposable private readonly HttpClient _httpClient; private readonly DbServiceState _state = new(); private readonly Config _config; + private SettingsService _settingsService = new(); private SqliteConnection _sqlConnection; private AliasClientDbContext _dbContext; private bool _isSuccessfullyInitialized; @@ -54,6 +55,12 @@ public class DbService : IDisposable (_sqlConnection, _dbContext) = InitializeEmptyDatabase(); } + /// + /// Gets the settings service instance which can be used to interact with general settings stored in the database. + /// + /// SettingsService. + public SettingsService Settings => _settingsService; + /// /// Gets database service state object which can be subscribed to. /// @@ -416,7 +423,7 @@ public class DbService : IDisposable string decryptedBase64String = await _jsInteropService.SymmetricDecrypt(vault.Blob, _authService.GetEncryptionKeyAsBase64Async()); await ImportDbContextFromBase64Async(decryptedBase64String); - // Check if database is up to date with migrations. + // Check if database is up-to-date with migrations. var pendingMigrations = await _dbContext.Database.GetPendingMigrationsAsync(); if (pendingMigrations.Any()) { @@ -425,6 +432,10 @@ public class DbService : IDisposable } _isSuccessfullyInitialized = true; + + // Initialize child settings service if it's not already. + await _settingsService.InitializeAsync(this); + _state.UpdateState(DbServiceState.DatabaseStatus.Ready); return true; } diff --git a/src/AliasVault.Client/Services/SettingsService.cs b/src/AliasVault.Client/Services/SettingsService.cs index c4683bc37..514f62aab 100644 --- a/src/AliasVault.Client/Services/SettingsService.cs +++ b/src/AliasVault.Client/Services/SettingsService.cs @@ -11,30 +11,39 @@ using System; using System.Text.Json; using System.Threading.Tasks; using AliasClientDb; +using Microsoft.EntityFrameworkCore; /// /// Service class for accessing and mutating general settings stored in database. /// -public class SettingsService(DbService dbService) +/// Note: this service does not use DI but instead is initialized by and can be accessed through the DbService. +/// This is done because the SettingsService requires a DbContext during initialization and the context is not yet +/// available during application boot because of encryption/decryption of remote database file. When accessing the +/// settings through the DbService we can ensure proper data flow. +public class SettingsService { + private readonly Dictionary _settings = new(); + private DbService? _dbService; + private bool _initialized; + /// /// Gets the DefaultEmailDomain setting asynchronously. /// /// Default email domain as string. - public Task GetDefaultEmailDomainAsync() => GetSettingAsync("DefaultEmailDomain"); + public string DefaultEmailDomain => GetSetting("DefaultEmailDomain"); + + /// + /// Gets a value indicating whether email refresh should be done automatically on the credentials page. + /// + /// AutoEmailRefresh setting as string. + public bool AutoEmailRefresh => GetSetting("AutoEmailRefresh", true); /// /// Sets the DefaultEmailDomain setting asynchronously. /// /// The new DeafultEmailDomain setting. /// Task. - public Task SetDefaultEmailDomainAsync(string value) => SetSettingAsync("DefaultEmailDomain", value); - - /// - /// Gets the AutoEmailRefresh setting asynchronously as a string. - /// - /// AutoEmailRefresh setting as string. - public Task GetAutoEmailRefreshAsync() => GetSettingAsync("AutoEmailRefresh"); + public Task SetDefaultEmailDomain(string value) => SetSettingAsync("DefaultEmailDomain", value); /// /// Sets the AutoEmailRefresh setting asynchronously as a string. @@ -43,28 +52,68 @@ public class SettingsService(DbService dbService) /// Task. public Task SetAutoEmailRefreshAsync(bool value) => SetSettingAsync("AutoEmailRefresh", value); + /// + /// Initializes the settings service asynchronously. + /// + /// DbService instance. + /// Task. + public async Task InitializeAsync(DbService dbService) + { + if (_initialized) + { + return; + } + + // Store the DbService instance for later use. + _dbService = dbService; + + var db = await _dbService.GetDbContextAsync(); + var settings = await db.Settings.ToListAsync(); + foreach (var setting in settings) + { + _settings[setting.Key] = setting.Value; + } + + _initialized = true; + } + /// /// Get setting value from database. /// /// Key of setting to retrieve. /// Setting as string value. - private async Task GetSettingAsync(string key) + private string GetSetting(string key) { - var db = await dbService.GetDbContextAsync(); - var setting = await db.Settings.FindAsync(key); - return setting?.Value ?? string.Empty; + var setting = _settings.GetValueOrDefault(key); + return setting ?? string.Empty; } /// - /// Gets a setting asynchronously and casts it to the specified type. + /// Gets a setting and casts it to the specified type. /// /// The type to cast the setting to. /// The key of the setting. + /// The default value to use if no setting is set in database. /// The setting value cast to type T. - private async Task GetSettingAsync(string key) + private T? GetSetting(string key, T? defaultValue = default) { - string value = await GetSettingAsync(key); - return CastSetting(value); + string value = GetSetting(key); + + try + { + return CastSetting(value); + } + catch (InvalidOperationException ex) + { + // If no value is available in database but default value is set, return default value. + if (defaultValue is not null) + { + return defaultValue; + } + + // No value in database and no default value set, throw exception. + throw new InvalidOperationException($"Failed to cast setting {key} to type {typeof(T)}", ex); + } } /// @@ -88,7 +137,7 @@ public class SettingsService(DbService dbService) /// Task. private async Task SetSettingAsync(string key, string value) { - var db = await dbService.GetDbContextAsync(); + var db = await _dbService!.GetDbContextAsync(); var setting = await db.Settings.FindAsync(key); if (setting == null) { @@ -108,7 +157,7 @@ public class SettingsService(DbService dbService) db.Settings.Update(setting); } - await dbService.SaveDatabaseAsync(); + await _dbService.SaveDatabaseAsync(); } /// @@ -126,7 +175,7 @@ public class SettingsService(DbService dbService) return default; } - throw new ArgumentException($"Cannot cast null or empty string to non-nullable type {typeof(T)}"); + throw new InvalidOperationException($"Setting value is null or empty for non-nullable type {typeof(T)}"); } if (typeof(T) == typeof(bool)) diff --git a/src/AliasVault.Client/wwwroot/css/tailwind.css b/src/AliasVault.Client/wwwroot/css/tailwind.css index 717a56931..1237e7713 100644 --- a/src/AliasVault.Client/wwwroot/css/tailwind.css +++ b/src/AliasVault.Client/wwwroot/css/tailwind.css @@ -772,6 +772,10 @@ video { margin-right: 0.25rem; } +.ml-2 { + margin-left: 0.5rem; +} + .block { display: block; } @@ -1608,6 +1612,11 @@ video { color: rgb(255 255 255 / var(--tw-text-opacity)); } +.text-red-600 { + --tw-text-opacity: 1; + color: rgb(220 38 38 / var(--tw-text-opacity)); +} + .opacity-0 { opacity: 0; } @@ -1777,6 +1786,11 @@ video { border-color: rgb(244 149 65 / var(--tw-border-opacity)); } +.focus\:border-blue-500:focus { + --tw-border-opacity: 1; + border-color: rgb(59 130 246 / var(--tw-border-opacity)); +} + .focus\:outline-none:focus { outline: 2px solid transparent; outline-offset: 2px; @@ -1839,6 +1853,11 @@ video { --tw-ring-color: rgb(156 163 175 / var(--tw-ring-opacity)); } +.focus\:ring-blue-500:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity)); +} + .focus\:ring-offset-2:focus { --tw-ring-offset-width: 2px; } @@ -2041,6 +2060,11 @@ video { border-color: rgb(244 149 65 / var(--tw-border-opacity)); } +.dark\:focus\:border-blue-500:focus:is(.dark *) { + --tw-border-opacity: 1; + border-color: rgb(59 130 246 / var(--tw-border-opacity)); +} + .dark\:focus\:ring-blue-800:focus:is(.dark *) { --tw-ring-opacity: 1; --tw-ring-color: rgb(30 64 175 / var(--tw-ring-opacity)); @@ -2091,6 +2115,16 @@ video { --tw-ring-color: rgb(127 29 29 / var(--tw-ring-opacity)); } +.dark\:focus\:ring-blue-500:focus:is(.dark *) { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity)); +} + +.dark\:focus\:ring-blue-600:focus:is(.dark *) { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(37 99 235 / var(--tw-ring-opacity)); +} + @media (min-width: 640px) { .sm\:col-span-3 { grid-column: span 3 / span 3; @@ -2108,6 +2142,10 @@ video { width: auto; } + .sm\:flex-row { + flex-direction: row; + } + .sm\:space-x-4 > :not([hidden]) ~ :not([hidden]) { --tw-space-x-reverse: 0; margin-right: calc(1rem * var(--tw-space-x-reverse));