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));