Add settings table and service to client project (#145)

This commit is contained in:
Leendert de Borst
2024-08-05 09:57:33 +02:00
parent 540124cabf
commit d4a773fc2c
9 changed files with 764 additions and 1 deletions

View File

@@ -43,7 +43,10 @@
</div>
<ul class="py-1 font-light text-gray-500 dark:text-gray-400" aria-labelledby="userMenuDropdownButton">
<li>
<a href="settings/vault" class="block py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400 dark:hover:text-white">Vault settings</a>
<a href="/settings/general" class="block py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400 dark:hover:text-white">General settings</a>
</li>
<li>
<a href="/settings/vault" class="block py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400 dark:hover:text-white">Vault settings</a>
</li>
</ul>
<ul class="py-1 font-light text-gray-500 dark:text-gray-400" aria-labelledby="dropdown">

View File

@@ -0,0 +1,146 @@
@page "/settings/general"
@inherits MainBase
@inject CredentialService CredentialService
<LayoutPageTitle>General settings</LayoutPageTitle>
<div class="grid grid-cols-1 px-4 pt-6 xl:grid-cols-3 xl:gap-4 dark:bg-gray-900">
<div class="mb-4 col-span-full xl:mb-2">
<Breadcrumb BreadcrumbItems="BreadcrumbItems" />
<div class="flex items-center justify-between">
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">General settings</h1>
</div>
<p>On this page you can configure general AliasVault settings.</p>
</div>
</div>
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h3 class="mb-4 text-xl font-semibold dark:text-white">Export vault</h3>
<div class="mb-4">
<div>
<button @onclick="ExportVaultSqlite" type="button" class="px-4 py-2 text-white bg-primary-600 rounded-lg hover:bg-primary-700 focus:ring-4 focus:ring-primary-300 dark:bg-primary-500 dark:hover:bg-primary-600 dark:focus:ring-primary-800">
Export vault to unencrypted SQLite file
</button>
</div>
<div class="mt-6">
<button @onclick="ExportVaultCsv" type="button" class="px-4 py-2 text-white bg-primary-600 rounded-lg hover:bg-primary-700 focus:ring-4 focus:ring-primary-300 dark:bg-primary-500 dark:hover:bg-primary-600 dark:focus:ring-primary-800">
Export vault to unencrypted CSV file
</button>
</div>
</div>
</div>
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h3 class="mb-4 text-xl font-semibold dark:text-white">Import vault</h3>
<div class="mb-4">
<div>
Import unencrypted CSV file:
<InputFile OnChange="@LoadFiles" />
</div>
</div>
</div>
@if (IsImporting)
{
<p>Loading...</p>
}
else if (!string.IsNullOrEmpty(ImportErrorMessage))
{
<p class="text-danger">@ImportErrorMessage</p>
}
else if (!string.IsNullOrEmpty(ImportSuccessMessage))
{
<p class="text-success">@ImportSuccessMessage</p>
}
@code {
private bool IsImporting = false;
private string ImportErrorMessage = string.Empty;
private string ImportSuccessMessage = string.Empty;
/// <inheritdoc />
protected override async Task OnInitializedAsync()
{
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();
}
}
}

View File

@@ -68,6 +68,7 @@ builder.Services.AddScoped<DbService>();
builder.Services.AddScoped<GlobalNotificationService>();
builder.Services.AddScoped<GlobalLoadingService>();
builder.Services.AddScoped<JsInteropService>();
builder.Services.AddScoped<SettingsService>();
builder.Services.AddSingleton<ClipboardCopyService>();
builder.Services.AddAuthorizationCore();

View File

@@ -0,0 +1,185 @@
//-----------------------------------------------------------------------
// <copyright file="SettingsService.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.Client.Services;
using System;
using System.Text.Json;
using System.Threading.Tasks;
using AliasClientDb;
/// <summary>
/// Service class for accessing and mutating general settings stored in database.
/// </summary>
public class SettingsService(DbService dbService)
{
/// <summary>
/// Gets the DefaultEmailDomain setting asynchronously.
/// </summary>
/// <returns>Default email domain as string.</returns>
public Task<string> GetDefaultEmailDomainAsync() => GetSettingAsync("DefaultEmailDomain");
/// <summary>
/// Sets the DefaultEmailDomain setting asynchronously.
/// </summary>
/// <param name="value">The new DeafultEmailDomain setting.</param>
/// <returns>Task.</returns>
public Task SetDefaultEmailDomainAsync(string value) => SetSettingAsync("DefaultEmailDomain", value);
/// <summary>
/// Gets the AutoEmailRefresh setting asynchronously as a string.
/// </summary>
/// <returns>AutoEmailRefresh setting as string.</returns>
public Task<bool> GetAutoEmailRefreshAsync() => GetSettingAsync<bool>("AutoEmailRefresh");
/// <summary>
/// Sets the AutoEmailRefresh setting asynchronously as a string.
/// </summary>
/// <param name="value">The new value.</param>
/// <returns>Task.</returns>
public Task SetAutoEmailRefreshAsync(bool value) => SetSettingAsync<bool>("AutoEmailRefresh", value);
/// <summary>
/// Get setting value from database.
/// </summary>
/// <param name="key">Key of setting to retrieve.</param>
/// <returns>Setting as string value.</returns>
private async Task<string> GetSettingAsync(string key)
{
var db = await dbService.GetDbContextAsync();
var setting = await db.Settings.FindAsync(key);
return setting?.Value ?? string.Empty;
}
/// <summary>
/// Gets a setting asynchronously and casts it to the specified type.
/// </summary>
/// <typeparam name="T">The type to cast the setting to.</typeparam>
/// <param name="key">The key of the setting.</param>
/// <returns>The setting value cast to type T.</returns>
private async Task<T?> GetSettingAsync<T>(string key)
{
string value = await GetSettingAsync(key);
return CastSetting<T>(value);
}
/// <summary>
/// Sets a setting asynchronously, converting the value to a string so its compatible with the database field.
/// </summary>
/// <typeparam name="T">The type of the value being set.</typeparam>
/// <param name="key">The key of the setting.</param>
/// <param name="value">The value to set.</param>
/// <returns>Task.</returns>
private Task SetSettingAsync<T>(string key, T value)
{
string stringValue = ConvertToString(value);
return SetSettingAsync(key, stringValue);
}
/// <summary>
/// Set setting value in database.
/// </summary>
/// <param name="key">Key of setting to set.</param>
/// <param name="value">Value of setting to set.</param>
/// <returns>Task.</returns>
private async Task SetSettingAsync(string key, string value)
{
var db = await dbService.GetDbContextAsync();
var setting = await db.Settings.FindAsync(key);
if (setting == null)
{
setting = new Setting
{
Key = key,
Value = value,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
};
db.Settings.Add(setting);
}
else
{
setting.Value = value;
setting.UpdatedAt = DateTime.UtcNow;
db.Settings.Update(setting);
}
await dbService.SaveDatabaseAsync();
}
/// <summary>
/// Casts a setting value from the database string type to the specified requested type.
/// </summary>
/// <param name="value">Value (string) to cast.</param>
/// <typeparam name="T">Type to cast it to.</typeparam>
/// <returns>The value casted to the requested type.</returns>
private T? CastSetting<T>(string value)
{
if (string.IsNullOrEmpty(value))
{
if (default(T) is null)
{
return default;
}
throw new ArgumentException($"Cannot cast null or empty string to non-nullable type {typeof(T)}");
}
if (typeof(T) == typeof(bool))
{
return (T)(object)(bool.TryParse(value, out bool result) && result);
}
if (typeof(T) == typeof(int))
{
return (T)(object)int.Parse(value);
}
if (typeof(T) == typeof(double))
{
return (T)(object)double.Parse(value);
}
if (typeof(T) == typeof(string))
{
return (T)(object)value;
}
// For complex types, attempt JSON deserialization
try
{
var result = JsonSerializer.Deserialize<T>(value);
if (result is null && default(T) is not null)
{
throw new InvalidOperationException($"Deserialization resulted in null for non-nullable type {typeof(T)}");
}
return result;
}
catch (JsonException ex)
{
throw new InvalidOperationException($"Failed to deserialize value to type {typeof(T)}", ex);
}
}
/// <summary>
/// Converts a value of any type to a string.
/// </summary>
/// <param name="value">The value to convert.</param>
/// <typeparam name="T">The type of the existing value.</typeparam>
/// <returns>Value converted to string.</returns>
private string ConvertToString<T>(T value)
{
if (value is bool || value is int || value is double || value is string)
{
return value.ToString() ?? string.Empty;
}
// For complex types, use JSON serialization
return JsonSerializer.Serialize(value);
}
}

View File

@@ -73,6 +73,11 @@ public class AliasClientDbContext : DbContext
/// </summary>
public DbSet<EncryptionKey> EncryptionKeys { get; set; } = null!;
/// <summary>
/// Gets or sets the Settings DbSet.
/// </summary>
public DbSet<Setting> Settings { get; set; } = null!;
/// <summary>
/// The OnModelCreating method.
/// </summary>

View File

@@ -0,0 +1,328 @@
// <auto-generated />
using System;
using AliasClientDb;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace AliasClientDb.Migrations
{
[DbContext(typeof(AliasClientDbContext))]
[Migration("20240805073413_1.2.0-AddSettingsTable")]
partial class _120AddSettingsTable
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.7")
.HasAnnotation("Proxies:ChangeTracking", false)
.HasAnnotation("Proxies:CheckEquality", false)
.HasAnnotation("Proxies:LazyLoading", true);
modelBuilder.Entity("AliasClientDb.Alias", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("AddressCity")
.HasMaxLength(255)
.HasColumnType("VARCHAR");
b.Property<string>("AddressCountry")
.HasMaxLength(255)
.HasColumnType("VARCHAR");
b.Property<string>("AddressState")
.HasMaxLength(255)
.HasColumnType("VARCHAR");
b.Property<string>("AddressStreet")
.HasMaxLength(255)
.HasColumnType("VARCHAR");
b.Property<string>("AddressZipCode")
.HasMaxLength(255)
.HasColumnType("VARCHAR");
b.Property<string>("BankAccountIBAN")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<DateTime>("BirthDate")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("FirstName")
.HasMaxLength(255)
.HasColumnType("VARCHAR");
b.Property<string>("Gender")
.HasMaxLength(255)
.HasColumnType("VARCHAR");
b.Property<string>("Hobbies")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("LastName")
.HasMaxLength(255)
.HasColumnType("VARCHAR");
b.Property<string>("NickName")
.HasMaxLength(255)
.HasColumnType("VARCHAR");
b.Property<string>("PhoneMobile")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Aliases");
});
modelBuilder.Entity("AliasClientDb.Attachment", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<byte[]>("Blob")
.IsRequired()
.HasColumnType("BLOB");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("CredentialId")
.HasColumnType("TEXT");
b.Property<string>("Filename")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("CredentialId");
b.ToTable("Attachment");
});
modelBuilder.Entity("AliasClientDb.Credential", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("AliasId")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Notes")
.HasColumnType("TEXT");
b.Property<Guid>("ServiceId")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AliasId");
b.HasIndex("ServiceId");
b.ToTable("Credentials");
});
modelBuilder.Entity("AliasClientDb.EncryptionKey", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<bool>("IsPrimary")
.HasColumnType("INTEGER");
b.Property<string>("PrivateKey")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("TEXT");
b.Property<string>("PublicKey")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("EncryptionKeys");
});
modelBuilder.Entity("AliasClientDb.Password", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<Guid>("CredentialId")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("CredentialId");
b.ToTable("Passwords");
});
modelBuilder.Entity("AliasClientDb.Service", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<byte[]>("Logo")
.HasColumnType("BLOB");
b.Property<string>("Name")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("Url")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Services");
});
modelBuilder.Entity("AliasClientDb.Setting", b =>
{
b.Property<string>("Key")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Key");
b.ToTable("Settings");
});
modelBuilder.Entity("AliasClientDb.Attachment", b =>
{
b.HasOne("AliasClientDb.Credential", "Credential")
.WithMany("Attachments")
.HasForeignKey("CredentialId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Credential");
});
modelBuilder.Entity("AliasClientDb.Credential", b =>
{
b.HasOne("AliasClientDb.Alias", "Alias")
.WithMany("Credentials")
.HasForeignKey("AliasId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("AliasClientDb.Service", "Service")
.WithMany("Credentials")
.HasForeignKey("ServiceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Alias");
b.Navigation("Service");
});
modelBuilder.Entity("AliasClientDb.Password", b =>
{
b.HasOne("AliasClientDb.Credential", "Credential")
.WithMany("Passwords")
.HasForeignKey("CredentialId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Credential");
});
modelBuilder.Entity("AliasClientDb.Alias", b =>
{
b.Navigation("Credentials");
});
modelBuilder.Entity("AliasClientDb.Credential", b =>
{
b.Navigation("Attachments");
b.Navigation("Passwords");
});
modelBuilder.Entity("AliasClientDb.Service", b =>
{
b.Navigation("Credentials");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,37 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace AliasClientDb.Migrations
{
/// <inheritdoc />
public partial class _120AddSettingsTable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Settings",
columns: table => new
{
Key = table.Column<string>(type: "TEXT", maxLength: 255, nullable: false),
Value = table.Column<string>(type: "TEXT", nullable: true),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Settings", x => x.Key);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Settings");
}
}
}

View File

@@ -242,6 +242,26 @@ namespace AliasClientDb.Migrations
b.ToTable("Services");
});
modelBuilder.Entity("AliasClientDb.Setting", b =>
{
b.Property<string>("Key")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Key");
b.ToTable("Settings");
});
modelBuilder.Entity("AliasClientDb.Attachment", b =>
{
b.HasOne("AliasClientDb.Credential", "Credential")

View File

@@ -0,0 +1,38 @@
//-----------------------------------------------------------------------
// <copyright file="Setting.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasClientDb;
using System.ComponentModel.DataAnnotations;
/// <summary>
/// The service entity.
/// </summary>
public class Setting
{
/// <summary>
/// Gets or sets the setting key which is also the primary unique key.
/// </summary>
[Key]
[StringLength(255)]
public string Key { get; set; } = null!;
/// <summary>
/// Gets or sets the setting value. The field type is a string, but it can be used to store any type of data
/// via serialization.
/// </summary>
public string? Value { get; set; }
/// <summary>
/// Gets or sets the created timestamp.
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// Gets or sets the updated timestamp.
/// </summary>
public DateTime UpdatedAt { get; set; }
}