Files
aliasvault/src/AliasVault.Client/Services/SettingsService.cs
2024-08-05 17:24:51 +02:00

252 lines
8.6 KiB
C#

//-----------------------------------------------------------------------
// <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;
using Microsoft.EntityFrameworkCore;
/// <summary>
/// Service class for accessing and mutating general settings stored in database.
/// </summary>
/// <remarks>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.</remarks>
public class SettingsService
{
private readonly Dictionary<string, string?> _settings = new();
private DbService? _dbService;
private bool _initialized;
/// <summary>
/// Gets the DefaultEmailDomain setting.
/// </summary>
/// <returns>Default email domain as string.</returns>
public string DefaultEmailDomain => GetSetting("DefaultEmailDomain");
/// <summary>
/// Gets a value indicating whether email refresh should be done automatically on the credentials page.
/// </summary>
/// <returns>AutoEmailRefresh setting as string.</returns>
public bool AutoEmailRefresh => GetSetting<bool>("AutoEmailRefresh", true);
/// <summary>
/// Gets the DefaultIdentityLanguage setting.
/// </summary>
/// <returns>Default identity language as two-letter code.</returns>
public string DefaultIdentityLanguage => GetSetting<string>("DefaultIdentityLanguage", "en")!;
/// <summary>
/// Sets the DefaultEmailDomain setting.
/// </summary>
/// <param name="value">The new DefaultEmailDomain setting.</param>
/// <returns>Task.</returns>
public Task SetDefaultEmailDomain(string value) => SetSettingAsync("DefaultEmailDomain", value);
/// <summary>
/// Sets the AutoEmailRefresh setting as a string.
/// </summary>
/// <param name="value">The new value.</param>
/// <returns>Task.</returns>
public Task SetAutoEmailRefresh(bool value) => SetSettingAsync<bool>("AutoEmailRefresh", value);
/// <summary>
/// Sets the DefaultIdentityLanguage setting.
/// </summary>
/// <param name="value">The new value.</param>
/// <returns>Task.</returns>
public Task SetDefaultIdentityLanguage(string value) => SetSettingAsync("DefaultIdentityLanguage", value);
/// <summary>
/// Initializes the settings service asynchronously.
/// </summary>
/// <param name="dbService">DbService instance.</param>
/// <returns>Task.</returns>
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;
}
/// <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 static T? CastSetting<T>(string value)
{
if (string.IsNullOrEmpty(value))
{
if (default(T) is null)
{
return default;
}
throw new InvalidOperationException($"Setting value is null or empty for 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 static 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);
}
/// <summary>
/// Get setting value from database.
/// </summary>
/// <param name="key">Key of setting to retrieve.</param>
/// <returns>Setting as string value.</returns>
private string GetSetting(string key)
{
var setting = _settings.GetValueOrDefault(key);
return setting ?? string.Empty;
}
/// <summary>
/// Gets a setting 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>
/// <param name="defaultValue">The default value to use if no setting is set in database.</param>
/// <returns>The setting value cast to type T.</returns>
private T? GetSetting<T>(string key, T? defaultValue = default)
{
string value = GetSetting(key);
try
{
return CastSetting<T>(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);
}
}
/// <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);
}
// Also update the setting in the local dictionary so the new value
// is returned by subsequent local reads.
_settings[key] = value;
await _dbService.SaveDatabaseAsync();
}
}