mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-07-29 16:36:00 -04:00
Add Important settings UI for encrypted token storage
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
x:DataType="vm:ImportantSettingsVM"
|
||||
x:Class="LibationAvalonia.Controls.Settings.Important">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,Auto,*">
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,*">
|
||||
<controls:GroupBox
|
||||
Grid.Row="0"
|
||||
Margin="5"
|
||||
@@ -85,8 +85,41 @@
|
||||
</StackPanel>
|
||||
</CheckBox>
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Row="2"
|
||||
Margin="5"
|
||||
Label="Authentication tokens">
|
||||
<StackPanel>
|
||||
<CheckBox
|
||||
IsEnabled="{Binding CanEditEncryptTokens}"
|
||||
IsChecked="{Binding EncryptTokens, Mode=TwoWay}">
|
||||
<TextBlock Text="{Binding EncryptTokensText}" TextWrapping="Wrap" />
|
||||
</CheckBox>
|
||||
<TextBlock
|
||||
Margin="24,4,0,0"
|
||||
Foreground="DarkRed"
|
||||
IsVisible="{Binding PlaintextWarningVisible}"
|
||||
Text="{Binding PlaintextWarningText}"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
Margin="24,4,0,0"
|
||||
Foreground="DarkRed"
|
||||
IsVisible="{Binding OsStoreUnavailableVisible}"
|
||||
Text="{Binding OsStoreUnavailableMessage}"
|
||||
TextWrapping="Wrap" />
|
||||
<Button
|
||||
Margin="0,10,0,0"
|
||||
HorizontalAlignment="Right"
|
||||
Padding="20,0"
|
||||
Content="{Binding ConvertButtonText}"
|
||||
ToolTip.Tip="{Binding ConvertButtonToolTip}"
|
||||
IsEnabled="{Binding ConvertButtonEnabled}"
|
||||
Command="{Binding ConvertExistingTokensCommand}" />
|
||||
</StackPanel>
|
||||
</controls:GroupBox>
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="2" Margin="5"
|
||||
Grid.Row="3" Margin="5"
|
||||
Orientation="Horizontal">
|
||||
|
||||
<TextBlock
|
||||
@@ -111,7 +144,7 @@
|
||||
</StackPanel>
|
||||
|
||||
<controls:GroupBox
|
||||
Grid.Row="3"
|
||||
Grid.Row="4"
|
||||
Margin="5"
|
||||
Label="Display Settings">
|
||||
<Grid
|
||||
@@ -166,7 +199,7 @@
|
||||
</controls:GroupBox>
|
||||
|
||||
<Grid
|
||||
Grid.Row="4"
|
||||
Grid.Row="5"
|
||||
ColumnDefinitions="Auto,Auto,*"
|
||||
Margin="10"
|
||||
VerticalAlignment="Bottom">
|
||||
|
||||
@@ -2,6 +2,7 @@ using Avalonia.Controls;
|
||||
using FileManager;
|
||||
using LibationAvalonia.ViewModels.Settings;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase;
|
||||
using LibationUiBase.Forms;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
@@ -32,8 +33,19 @@ public partial class SettingsDialog : DialogWindow
|
||||
|
||||
#endregion
|
||||
|
||||
var tokenDecision = await settingsDisp.ImportantSettings.PromptOnSaveAsync(this);
|
||||
if (tokenDecision == TokenStorageSaveDecision.Abort)
|
||||
return;
|
||||
|
||||
settingsDisp.SaveSettings(config);
|
||||
|
||||
if (tokenDecision == TokenStorageSaveDecision.SavePreferenceAndConvert)
|
||||
{
|
||||
var converted = await settingsDisp.ImportantSettings.ConvertAfterSaveAsync(this);
|
||||
if (!converted)
|
||||
return;
|
||||
}
|
||||
|
||||
await MessageBox.VerboseLoggingWarning_ShowIfTrue();
|
||||
await base.SaveAndCloseAsync();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using AudibleApi.Authorization;
|
||||
using AudibleUtilities;
|
||||
using Dinah.Core;
|
||||
using FileManager;
|
||||
using LibationFileManager;
|
||||
@@ -6,6 +8,8 @@ using ReactiveUI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace LibationAvalonia.ViewModels.Settings;
|
||||
|
||||
@@ -13,6 +17,13 @@ public class ImportantSettingsVM : ViewModelBase
|
||||
{
|
||||
private EnumDisplay<Configuration.Theme> themeVariant;
|
||||
private readonly Configuration config;
|
||||
private bool encryptTokens;
|
||||
private bool plaintextWarningVisible;
|
||||
private bool convertButtonEnabled;
|
||||
private bool osStoreUnavailableVisible;
|
||||
private string osStoreUnavailableMessage = "";
|
||||
private TokenStorageMethod initialTokenStorageMethod;
|
||||
private bool osSecretStoreAvailable;
|
||||
|
||||
public ImportantSettingsVM(Configuration config)
|
||||
{
|
||||
@@ -29,8 +40,33 @@ public class ImportantSettingsVM : ViewModelBase
|
||||
GridFontScaleFactor = scaleFactorToLinearRange(config.GridFontScaleFactor);
|
||||
|
||||
themeVariant = Themes.Single(v => v.Value == config.ThemeVariant);
|
||||
|
||||
initialTokenStorageMethod = config.TokenStorageMethod;
|
||||
osSecretStoreAvailable = IdentityTokenStorageWiring.IsOsSecretStoreAvailable(out var unavailableReason);
|
||||
if (!osSecretStoreAvailable)
|
||||
{
|
||||
OsStoreUnavailableVisible = true;
|
||||
OsStoreUnavailableMessage = TokenStorageSettingsUi.OsStoreUnavailableMessage(unavailableReason);
|
||||
encryptTokens = false;
|
||||
CanEditEncryptTokens = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
OsStoreUnavailableVisible = false;
|
||||
encryptTokens = TokenStorageSettingsUi.CheckboxFromMethod(config.TokenStorageMethod);
|
||||
CanEditEncryptTokens = true;
|
||||
}
|
||||
|
||||
RefreshTokenStorageUiState();
|
||||
ConvertExistingTokensCommand = ReactiveCommand.CreateFromTask(ConvertExistingTokensAsync);
|
||||
}
|
||||
|
||||
public async Task<TokenStorageSaveDecision> PromptOnSaveAsync(object? owner)
|
||||
=> await TokenStorageSettingsUi.PromptOnPreferenceSaveAsync(owner, initialTokenStorageMethod, SelectedTokenStorageMethod);
|
||||
|
||||
public async Task<bool> ConvertAfterSaveAsync(object? owner)
|
||||
=> await TokenStorageSettingsUi.RunConversionAsync(owner, SelectedTokenStorageMethod);
|
||||
|
||||
public void SaveSettings(Configuration config)
|
||||
{
|
||||
config.Books = GetBooksDirectory();
|
||||
@@ -40,6 +76,9 @@ public class ImportantSettingsVM : ViewModelBase
|
||||
config.LastWriteTime = LastWriteTime.Value;
|
||||
config.UseWebView = UseWebView;
|
||||
config.LogLevel = LoggingLevel;
|
||||
config.TokenStorageMethod = SelectedTokenStorageMethod;
|
||||
initialTokenStorageMethod = SelectedTokenStorageMethod;
|
||||
RefreshTokenStorageUiState();
|
||||
}
|
||||
|
||||
public LongPath GetBooksDirectory()
|
||||
@@ -63,6 +102,24 @@ public class ImportantSettingsVM : ViewModelBase
|
||||
Go.To.Folder(Configuration.Instance.LibationFiles.Location.ShortPathName);
|
||||
}
|
||||
|
||||
private async Task ConvertExistingTokensAsync()
|
||||
{
|
||||
var converted = await TokenStorageSettingsUi.ConfirmAndConvertAsync(null, SelectedTokenStorageMethod);
|
||||
if (converted)
|
||||
RefreshTokenStorageUiState();
|
||||
}
|
||||
|
||||
private void RefreshTokenStorageUiState()
|
||||
{
|
||||
var method = SelectedTokenStorageMethod;
|
||||
PlaintextWarningVisible = osSecretStoreAvailable && method == TokenStorageMethod.Plaintext;
|
||||
var alignment = AccountTokenStorage.GetAccountsAlignment(method);
|
||||
ConvertButtonEnabled = TokenStorageSettingsUi.IsConvertButtonEnabled(alignment);
|
||||
}
|
||||
|
||||
private TokenStorageMethod SelectedTokenStorageMethod
|
||||
=> TokenStorageSettingsUi.MethodFromCheckbox(EncryptTokens);
|
||||
|
||||
public List<Configuration.KnownDirectories> KnownDirectories { get; } = new()
|
||||
{
|
||||
Configuration.KnownDirectories.LibationFiles,
|
||||
@@ -95,6 +152,12 @@ public class ImportantSettingsVM : ViewModelBase
|
||||
.Select(v => new EnumDisplay<Configuration.Theme>(v))
|
||||
.ToArray();
|
||||
|
||||
public string EncryptTokensText { get; } = TokenStorageSettingsUi.CheckboxText;
|
||||
public string PlaintextWarningText { get; } = TokenStorageSettingsUi.PlaintextWarningText;
|
||||
public string ConvertButtonText { get; } = TokenStorageSettingsUi.ConvertButtonText;
|
||||
public string ConvertButtonToolTip { get; } = TokenStorageSettingsUi.ConvertButtonToolTip;
|
||||
public bool CanEditEncryptTokens { get; }
|
||||
|
||||
public string BooksDirectory { get; set; }
|
||||
public bool SavePodcastsToParentFolder { get; set; }
|
||||
public bool OverwriteExisting { get; set; }
|
||||
@@ -105,6 +168,42 @@ public class ImportantSettingsVM : ViewModelBase
|
||||
public bool UseWebView { get; set; }
|
||||
public Serilog.Events.LogEventLevel LoggingLevel { get; set; }
|
||||
|
||||
public bool EncryptTokens
|
||||
{
|
||||
get => encryptTokens;
|
||||
set
|
||||
{
|
||||
this.RaiseAndSetIfChanged(ref encryptTokens, value);
|
||||
RefreshTokenStorageUiState();
|
||||
}
|
||||
}
|
||||
|
||||
public bool PlaintextWarningVisible
|
||||
{
|
||||
get => plaintextWarningVisible;
|
||||
private set => this.RaiseAndSetIfChanged(ref plaintextWarningVisible, value);
|
||||
}
|
||||
|
||||
public bool ConvertButtonEnabled
|
||||
{
|
||||
get => convertButtonEnabled;
|
||||
private set => this.RaiseAndSetIfChanged(ref convertButtonEnabled, value);
|
||||
}
|
||||
|
||||
public bool OsStoreUnavailableVisible
|
||||
{
|
||||
get => osStoreUnavailableVisible;
|
||||
private set => this.RaiseAndSetIfChanged(ref osStoreUnavailableVisible, value);
|
||||
}
|
||||
|
||||
public string OsStoreUnavailableMessage
|
||||
{
|
||||
get => osStoreUnavailableMessage;
|
||||
private set => this.RaiseAndSetIfChanged(ref osStoreUnavailableMessage, value);
|
||||
}
|
||||
|
||||
public ICommand ConvertExistingTokensCommand { get; }
|
||||
|
||||
public EnumDisplay<Configuration.Theme> ThemeVariant
|
||||
{
|
||||
get => themeVariant;
|
||||
|
||||
@@ -302,7 +302,7 @@ public partial class Configuration
|
||||
AC_4
|
||||
}
|
||||
|
||||
[Description("Store authentication tokens encrypted using the OS secret store.\r\nUnchecking stores tokens in plaintext in AccountsSettings.json.\r\nChanging this setting does not convert existing tokens until you choose to convert them.")]
|
||||
[Description("Store authentication tokens encrypted. Unchecking this option stores tokens in plaintext.")]
|
||||
public TokenStorageMethod TokenStorageMethod
|
||||
{
|
||||
get => GetNonString(defaultValue: TokenStorageMethod.Encrypted);
|
||||
|
||||
210
Source/LibationUiBase/TokenStorageSettingsUi.cs
Normal file
210
Source/LibationUiBase/TokenStorageSettingsUi.cs
Normal file
@@ -0,0 +1,210 @@
|
||||
using AudibleApi.Authorization;
|
||||
using AudibleUtilities;
|
||||
using LibationUiBase.Forms;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LibationUiBase;
|
||||
|
||||
/// <summary>
|
||||
/// Shared copy and workflows for Important settings token-storage controls.
|
||||
/// </summary>
|
||||
public static class TokenStorageSettingsUi
|
||||
{
|
||||
public const string CheckboxText
|
||||
= "Store authentication tokens encrypted. Unchecking this option stores tokens in plaintext.";
|
||||
|
||||
public const string PlaintextWarningText
|
||||
= "Authentication tokens will be stored as readable plaintext in AccountsSettings.json.";
|
||||
|
||||
public const string ConvertButtonText = "Update existing stored tokens";
|
||||
|
||||
public const string ConvertButtonToolTip
|
||||
= "Convert existing tokens in AccountsSettings.json to match the currently selected storage method.";
|
||||
|
||||
public const string SavePromptCaption = "Convert existing tokens?";
|
||||
public const string ConvertConfirmCaption = "Confirm token conversion";
|
||||
public const string ConversionFailedCaption = "Token conversion failed";
|
||||
public const string ConversionSucceededCaption = "Tokens updated";
|
||||
public const string OsStoreUnavailableCaption = "Encrypted token storage unavailable";
|
||||
|
||||
public const string SavePromptEncrypted
|
||||
= "New and refreshed authentication tokens will now be stored encrypted. Would you also like to encrypt and re-save existing plaintext tokens?";
|
||||
|
||||
public const string SavePromptPlaintext
|
||||
= "New and refreshed authentication tokens will now be stored in plaintext. Would you also like to decrypt and re-save existing encrypted tokens? This will make those token values readable in the account settings file.";
|
||||
|
||||
public const string StandaloneConfirmEncrypted
|
||||
= "Encrypt and re-save existing plaintext authentication tokens so they match the current setting?";
|
||||
|
||||
public const string StandaloneConfirmPlaintext
|
||||
= "Decrypt and re-save existing encrypted authentication tokens as plaintext? This will make those token values readable in the account settings file.";
|
||||
|
||||
public const string ConversionSucceededMessage
|
||||
= "Existing authentication tokens were updated to match the selected storage method.";
|
||||
|
||||
public static TokenStorageMethod MethodFromCheckbox(bool storeEncrypted)
|
||||
=> storeEncrypted ? TokenStorageMethod.Encrypted : TokenStorageMethod.Plaintext;
|
||||
|
||||
public static bool CheckboxFromMethod(TokenStorageMethod method)
|
||||
=> method == TokenStorageMethod.Encrypted;
|
||||
|
||||
/// <summary>
|
||||
/// Convert is offered for mismatches and indeterminate state.
|
||||
/// Matching / no-token states keep the button disabled.
|
||||
/// </summary>
|
||||
public static bool IsConvertButtonEnabled(TokenStorageAlignment alignment)
|
||||
=> alignment is TokenStorageAlignment.SomeMismatch or TokenStorageAlignment.Indeterminate;
|
||||
|
||||
public static string SavePromptBody(TokenStorageMethod method)
|
||||
=> method == TokenStorageMethod.Encrypted ? SavePromptEncrypted : SavePromptPlaintext;
|
||||
|
||||
public static string StandaloneConfirmBody(TokenStorageMethod method)
|
||||
=> method == TokenStorageMethod.Encrypted ? StandaloneConfirmEncrypted : StandaloneConfirmPlaintext;
|
||||
|
||||
public static string OsStoreUnavailableMessage(string? unavailableReason)
|
||||
=> string.IsNullOrWhiteSpace(unavailableReason)
|
||||
? "Encrypted token storage requires an OS secret store, which is not available. Tokens cannot be stored encrypted."
|
||||
: $"Encrypted token storage requires an OS secret store, which is not available: {unavailableReason}";
|
||||
|
||||
public static string FormatConversionError(IdentityTokenConversionResult result)
|
||||
{
|
||||
var error = string.IsNullOrWhiteSpace(result.Error)
|
||||
? "Token conversion failed."
|
||||
: result.Error!;
|
||||
|
||||
if (result.FailedCategories.Count == 0)
|
||||
return error;
|
||||
|
||||
return $"{error}\r\n\r\nFailed categories: {string.Join(", ", result.FailedCategories)}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decide whether saving a changed preference should also convert existing tokens.
|
||||
/// Cancel / dismiss must never be treated as approval.
|
||||
/// </summary>
|
||||
public static async Task<TokenStorageSaveDecision> PromptOnPreferenceSaveAsync(
|
||||
object? owner,
|
||||
TokenStorageMethod originalMethod,
|
||||
TokenStorageMethod selectedMethod)
|
||||
{
|
||||
if (originalMethod == selectedMethod)
|
||||
return TokenStorageSaveDecision.SavePreferenceOnly;
|
||||
|
||||
if (selectedMethod == TokenStorageMethod.Encrypted
|
||||
&& !IdentityTokenStorageWiring.IsOsSecretStoreAvailable(out var unavailableReason))
|
||||
{
|
||||
await MessageBoxBase.Show(
|
||||
owner,
|
||||
OsStoreUnavailableMessage(unavailableReason),
|
||||
OsStoreUnavailableCaption,
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Error);
|
||||
return TokenStorageSaveDecision.Abort;
|
||||
}
|
||||
|
||||
var alignment = AccountTokenStorage.GetAccountsAlignment(selectedMethod);
|
||||
if (alignment is TokenStorageAlignment.AllMatch or TokenStorageAlignment.NoApplicableTokens)
|
||||
return TokenStorageSaveDecision.SavePreferenceOnly;
|
||||
|
||||
var result = await MessageBoxBase.Show(
|
||||
owner,
|
||||
SavePromptBody(selectedMethod),
|
||||
SavePromptCaption,
|
||||
MessageBoxButtons.YesNoCancel,
|
||||
MessageBoxIcon.Warning,
|
||||
defaultButton: selectedMethod == TokenStorageMethod.Plaintext
|
||||
? MessageBoxDefaultButton.Button2
|
||||
: MessageBoxDefaultButton.Button1);
|
||||
|
||||
return result switch
|
||||
{
|
||||
DialogResult.Yes => TokenStorageSaveDecision.SavePreferenceAndConvert,
|
||||
DialogResult.No => TokenStorageSaveDecision.SavePreferenceOnly,
|
||||
_ => TokenStorageSaveDecision.Abort
|
||||
};
|
||||
}
|
||||
|
||||
public static async Task<bool> ConfirmAndConvertAsync(object? owner, TokenStorageMethod method)
|
||||
{
|
||||
if (method == TokenStorageMethod.Encrypted
|
||||
&& !IdentityTokenStorageWiring.IsOsSecretStoreAvailable(out var unavailableReason))
|
||||
{
|
||||
await MessageBoxBase.Show(
|
||||
owner,
|
||||
OsStoreUnavailableMessage(unavailableReason),
|
||||
OsStoreUnavailableCaption,
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
var alignment = AccountTokenStorage.GetAccountsAlignment(method);
|
||||
if (!IsConvertButtonEnabled(alignment))
|
||||
return false;
|
||||
|
||||
var confirm = await MessageBoxBase.Show(
|
||||
owner,
|
||||
StandaloneConfirmBody(method),
|
||||
ConvertConfirmCaption,
|
||||
MessageBoxButtons.YesNo,
|
||||
MessageBoxIcon.Warning,
|
||||
defaultButton: method == TokenStorageMethod.Plaintext
|
||||
? MessageBoxDefaultButton.Button2
|
||||
: MessageBoxDefaultButton.Button1);
|
||||
|
||||
if (confirm != DialogResult.Yes)
|
||||
return false;
|
||||
|
||||
return await RunConversionAsync(owner, method);
|
||||
}
|
||||
|
||||
public static async Task<bool> RunConversionAsync(object? owner, TokenStorageMethod method)
|
||||
{
|
||||
IdentityTokenConversionResult result;
|
||||
try
|
||||
{
|
||||
result = AccountTokenStorage.ConvertAllAccounts(method);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Serilog.Log.Logger.Warning(ex, "Token conversion failed");
|
||||
await MessageBoxBase.Show(
|
||||
owner,
|
||||
"Token conversion failed. The original AccountsSettings.json file was left unchanged.",
|
||||
ConversionFailedCaption,
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
await MessageBoxBase.Show(
|
||||
owner,
|
||||
FormatConversionError(result),
|
||||
ConversionFailedCaption,
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.Changed)
|
||||
{
|
||||
await MessageBoxBase.Show(
|
||||
owner,
|
||||
ConversionSucceededMessage,
|
||||
ConversionSucceededCaption,
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Information);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public enum TokenStorageSaveDecision
|
||||
{
|
||||
Abort,
|
||||
SavePreferenceOnly,
|
||||
SavePreferenceAndConvert
|
||||
}
|
||||
@@ -58,6 +58,11 @@
|
||||
gridScaleFactorTbar = new System.Windows.Forms.TrackBar();
|
||||
gridFontScaleFactorLbl = new System.Windows.Forms.Label();
|
||||
gridFontScaleFactorTbar = new System.Windows.Forms.TrackBar();
|
||||
tokenStorageGb = new System.Windows.Forms.GroupBox();
|
||||
encryptTokensCbox = new System.Windows.Forms.CheckBox();
|
||||
plaintextTokenWarningLbl = new System.Windows.Forms.Label();
|
||||
osSecretStoreUnavailableLbl = new System.Windows.Forms.Label();
|
||||
updateExistingTokensBtn = new System.Windows.Forms.Button();
|
||||
booksGb = new System.Windows.Forms.GroupBox();
|
||||
booksSelectControl = new DirectoryOrCustomSelectControl();
|
||||
lastWriteTimeCb = new System.Windows.Forms.ComboBox();
|
||||
@@ -148,6 +153,7 @@
|
||||
groupBox1.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)gridScaleFactorTbar).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)gridFontScaleFactorTbar).BeginInit();
|
||||
tokenStorageGb.SuspendLayout();
|
||||
booksGb.SuspendLayout();
|
||||
tab2ImportLibrary.SuspendLayout();
|
||||
tab3DownloadDecrypt.SuspendLayout();
|
||||
@@ -349,7 +355,7 @@
|
||||
//
|
||||
// logsBtn
|
||||
//
|
||||
logsBtn.Location = new System.Drawing.Point(256, 424);
|
||||
logsBtn.Location = new System.Drawing.Point(256, 534);
|
||||
logsBtn.Name = "logsBtn";
|
||||
logsBtn.Size = new System.Drawing.Size(132, 23);
|
||||
logsBtn.TabIndex = 5;
|
||||
@@ -360,7 +366,7 @@
|
||||
// loggingLevelLbl
|
||||
//
|
||||
loggingLevelLbl.AutoSize = true;
|
||||
loggingLevelLbl.Location = new System.Drawing.Point(6, 427);
|
||||
loggingLevelLbl.Location = new System.Drawing.Point(6, 537);
|
||||
loggingLevelLbl.Name = "loggingLevelLbl";
|
||||
loggingLevelLbl.Size = new System.Drawing.Size(78, 15);
|
||||
loggingLevelLbl.TabIndex = 3;
|
||||
@@ -370,7 +376,7 @@
|
||||
//
|
||||
loggingLevelCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
loggingLevelCb.FormattingEnabled = true;
|
||||
loggingLevelCb.Location = new System.Drawing.Point(90, 424);
|
||||
loggingLevelCb.Location = new System.Drawing.Point(90, 534);
|
||||
loggingLevelCb.Name = "loggingLevelCb";
|
||||
loggingLevelCb.Size = new System.Drawing.Size(129, 23);
|
||||
loggingLevelCb.TabIndex = 4;
|
||||
@@ -396,6 +402,7 @@
|
||||
tab1ImportantSettings.Controls.Add(themeCb);
|
||||
tab1ImportantSettings.Controls.Add(label22);
|
||||
tab1ImportantSettings.Controls.Add(groupBox1);
|
||||
tab1ImportantSettings.Controls.Add(tokenStorageGb);
|
||||
tab1ImportantSettings.Controls.Add(booksGb);
|
||||
tab1ImportantSettings.Controls.Add(logsBtn);
|
||||
tab1ImportantSettings.Controls.Add(loggingLevelCb);
|
||||
@@ -410,7 +417,7 @@
|
||||
// themeLbl
|
||||
//
|
||||
themeLbl.AutoSize = true;
|
||||
themeLbl.Location = new System.Drawing.Point(190, 393);
|
||||
themeLbl.Location = new System.Drawing.Point(190, 503);
|
||||
themeLbl.Name = "themeLbl";
|
||||
themeLbl.Size = new System.Drawing.Size(296, 15);
|
||||
themeLbl.TabIndex = 12;
|
||||
@@ -420,7 +427,7 @@
|
||||
//
|
||||
themeCb.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
themeCb.FormattingEnabled = true;
|
||||
themeCb.Location = new System.Drawing.Point(63, 390);
|
||||
themeCb.Location = new System.Drawing.Point(63, 500);
|
||||
themeCb.Name = "themeCb";
|
||||
themeCb.Size = new System.Drawing.Size(121, 23);
|
||||
themeCb.TabIndex = 11;
|
||||
@@ -429,7 +436,7 @@
|
||||
// label22
|
||||
//
|
||||
label22.AutoSize = true;
|
||||
label22.Location = new System.Drawing.Point(4, 393);
|
||||
label22.Location = new System.Drawing.Point(4, 503);
|
||||
label22.Name = "label22";
|
||||
label22.Size = new System.Drawing.Size(44, 15);
|
||||
label22.TabIndex = 10;
|
||||
@@ -443,7 +450,7 @@
|
||||
groupBox1.Controls.Add(gridScaleFactorTbar);
|
||||
groupBox1.Controls.Add(gridFontScaleFactorLbl);
|
||||
groupBox1.Controls.Add(gridFontScaleFactorTbar);
|
||||
groupBox1.Location = new System.Drawing.Point(6, 277);
|
||||
groupBox1.Location = new System.Drawing.Point(6, 387);
|
||||
groupBox1.Name = "groupBox1";
|
||||
groupBox1.Size = new System.Drawing.Size(844, 83);
|
||||
groupBox1.TabIndex = 9;
|
||||
@@ -503,6 +510,63 @@
|
||||
gridFontScaleFactorTbar.TabIndex = 7;
|
||||
gridFontScaleFactorTbar.TickFrequency = 25;
|
||||
//
|
||||
// tokenStorageGb
|
||||
//
|
||||
tokenStorageGb.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
tokenStorageGb.Controls.Add(updateExistingTokensBtn);
|
||||
tokenStorageGb.Controls.Add(osSecretStoreUnavailableLbl);
|
||||
tokenStorageGb.Controls.Add(plaintextTokenWarningLbl);
|
||||
tokenStorageGb.Controls.Add(encryptTokensCbox);
|
||||
tokenStorageGb.Location = new System.Drawing.Point(6, 277);
|
||||
tokenStorageGb.Name = "tokenStorageGb";
|
||||
tokenStorageGb.Size = new System.Drawing.Size(844, 104);
|
||||
tokenStorageGb.TabIndex = 8;
|
||||
tokenStorageGb.TabStop = false;
|
||||
tokenStorageGb.Text = "Authentication tokens";
|
||||
//
|
||||
// encryptTokensCbox
|
||||
//
|
||||
encryptTokensCbox.AutoSize = true;
|
||||
encryptTokensCbox.Location = new System.Drawing.Point(8, 22);
|
||||
encryptTokensCbox.Name = "encryptTokensCbox";
|
||||
encryptTokensCbox.Size = new System.Drawing.Size(520, 19);
|
||||
encryptTokensCbox.TabIndex = 0;
|
||||
encryptTokensCbox.Text = "Store authentication tokens encrypted. Unchecking this option stores tokens in plaintext.";
|
||||
encryptTokensCbox.UseVisualStyleBackColor = true;
|
||||
encryptTokensCbox.CheckedChanged += encryptTokensCbox_CheckedChanged;
|
||||
//
|
||||
// plaintextTokenWarningLbl
|
||||
//
|
||||
plaintextTokenWarningLbl.AutoSize = true;
|
||||
plaintextTokenWarningLbl.ForeColor = System.Drawing.Color.DarkRed;
|
||||
plaintextTokenWarningLbl.Location = new System.Drawing.Point(27, 44);
|
||||
plaintextTokenWarningLbl.Name = "plaintextTokenWarningLbl";
|
||||
plaintextTokenWarningLbl.Size = new System.Drawing.Size(480, 15);
|
||||
plaintextTokenWarningLbl.TabIndex = 1;
|
||||
plaintextTokenWarningLbl.Text = "Authentication tokens will be stored as readable plaintext in AccountsSettings.json.";
|
||||
//
|
||||
// osSecretStoreUnavailableLbl
|
||||
//
|
||||
osSecretStoreUnavailableLbl.AutoSize = true;
|
||||
osSecretStoreUnavailableLbl.ForeColor = System.Drawing.Color.DarkRed;
|
||||
osSecretStoreUnavailableLbl.Location = new System.Drawing.Point(27, 44);
|
||||
osSecretStoreUnavailableLbl.Name = "osSecretStoreUnavailableLbl";
|
||||
osSecretStoreUnavailableLbl.Size = new System.Drawing.Size(250, 15);
|
||||
osSecretStoreUnavailableLbl.TabIndex = 2;
|
||||
osSecretStoreUnavailableLbl.Text = "[OS secret store unavailable]";
|
||||
osSecretStoreUnavailableLbl.Visible = false;
|
||||
//
|
||||
// updateExistingTokensBtn
|
||||
//
|
||||
updateExistingTokensBtn.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
|
||||
updateExistingTokensBtn.Location = new System.Drawing.Point(628, 68);
|
||||
updateExistingTokensBtn.Name = "updateExistingTokensBtn";
|
||||
updateExistingTokensBtn.Size = new System.Drawing.Size(200, 27);
|
||||
updateExistingTokensBtn.TabIndex = 3;
|
||||
updateExistingTokensBtn.Text = "Update existing stored tokens";
|
||||
updateExistingTokensBtn.UseVisualStyleBackColor = true;
|
||||
updateExistingTokensBtn.Click += updateExistingTokensBtn_Click;
|
||||
//
|
||||
// booksGb
|
||||
//
|
||||
booksGb.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
|
||||
@@ -1493,6 +1557,8 @@
|
||||
groupBox1.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)gridScaleFactorTbar).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)gridFontScaleFactorTbar).EndInit();
|
||||
tokenStorageGb.ResumeLayout(false);
|
||||
tokenStorageGb.PerformLayout();
|
||||
booksGb.ResumeLayout(false);
|
||||
booksGb.PerformLayout();
|
||||
tab2ImportLibrary.ResumeLayout(false);
|
||||
@@ -1544,6 +1610,11 @@
|
||||
private System.Windows.Forms.CheckBox splitFilesByChapterCbox;
|
||||
public System.Windows.Forms.TabControl tabControl;
|
||||
private System.Windows.Forms.TabPage tab1ImportantSettings;
|
||||
private System.Windows.Forms.GroupBox tokenStorageGb;
|
||||
private System.Windows.Forms.CheckBox encryptTokensCbox;
|
||||
private System.Windows.Forms.Label plaintextTokenWarningLbl;
|
||||
private System.Windows.Forms.Label osSecretStoreUnavailableLbl;
|
||||
private System.Windows.Forms.Button updateExistingTokensBtn;
|
||||
private System.Windows.Forms.GroupBox booksGb;
|
||||
private System.Windows.Forms.TabPage tab2ImportLibrary;
|
||||
private System.Windows.Forms.TabPage tab3DownloadDecrypt;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
using Dinah.Core;
|
||||
using AudibleApi.Authorization;
|
||||
using AudibleUtilities;
|
||||
using Dinah.Core;
|
||||
using FileManager;
|
||||
using LibationFileManager;
|
||||
using LibationUiBase;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms.Dialogs;
|
||||
@@ -20,6 +23,8 @@ public partial class SettingsDialog
|
||||
}
|
||||
private Configuration.Theme themeVariant;
|
||||
private Configuration.Theme initialThemeVariant;
|
||||
private TokenStorageMethod initialTokenStorageMethod;
|
||||
private bool osSecretStoreAvailable;
|
||||
|
||||
private void Load_Important(Configuration config)
|
||||
{
|
||||
@@ -67,9 +72,60 @@ public partial class SettingsDialog
|
||||
overwriteExistingCbox.Checked = config.OverwriteExisting;
|
||||
gridScaleFactorTbar.Value = scaleFactorToLinearRange(config.GridScaleFactor);
|
||||
gridFontScaleFactorTbar.Value = scaleFactorToLinearRange(config.GridFontScaleFactor);
|
||||
|
||||
Load_TokenStorage(config);
|
||||
}
|
||||
|
||||
private bool Save_Important(Configuration config)
|
||||
private void Load_TokenStorage(Configuration config)
|
||||
{
|
||||
initialTokenStorageMethod = config.TokenStorageMethod;
|
||||
osSecretStoreAvailable = IdentityTokenStorageWiring.IsOsSecretStoreAvailable(out var unavailableReason);
|
||||
|
||||
encryptTokensCbox.Text = TokenStorageSettingsUi.CheckboxText;
|
||||
plaintextTokenWarningLbl.Text = TokenStorageSettingsUi.PlaintextWarningText;
|
||||
updateExistingTokensBtn.Text = TokenStorageSettingsUi.ConvertButtonText;
|
||||
toolTip.SetToolTip(updateExistingTokensBtn, TokenStorageSettingsUi.ConvertButtonToolTip);
|
||||
|
||||
if (!osSecretStoreAvailable)
|
||||
{
|
||||
osSecretStoreUnavailableLbl.Text = TokenStorageSettingsUi.OsStoreUnavailableMessage(unavailableReason);
|
||||
osSecretStoreUnavailableLbl.Visible = true;
|
||||
encryptTokensCbox.Checked = false;
|
||||
encryptTokensCbox.Enabled = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
osSecretStoreUnavailableLbl.Visible = false;
|
||||
encryptTokensCbox.Enabled = true;
|
||||
encryptTokensCbox.Checked = TokenStorageSettingsUi.CheckboxFromMethod(config.TokenStorageMethod);
|
||||
}
|
||||
|
||||
RefreshTokenStorageUiState();
|
||||
}
|
||||
|
||||
private TokenStorageMethod SelectedTokenStorageMethod
|
||||
=> TokenStorageSettingsUi.MethodFromCheckbox(encryptTokensCbox.Checked);
|
||||
|
||||
private void RefreshTokenStorageUiState()
|
||||
{
|
||||
var method = SelectedTokenStorageMethod;
|
||||
plaintextTokenWarningLbl.Visible = osSecretStoreAvailable && method == TokenStorageMethod.Plaintext;
|
||||
|
||||
var alignment = AccountTokenStorage.GetAccountsAlignment(method);
|
||||
updateExistingTokensBtn.Enabled = TokenStorageSettingsUi.IsConvertButtonEnabled(alignment);
|
||||
}
|
||||
|
||||
private void encryptTokensCbox_CheckedChanged(object? sender, EventArgs e)
|
||||
=> RefreshTokenStorageUiState();
|
||||
|
||||
private async void updateExistingTokensBtn_Click(object? sender, EventArgs e)
|
||||
{
|
||||
var converted = await TokenStorageSettingsUi.ConfirmAndConvertAsync(this, SelectedTokenStorageMethod);
|
||||
if (converted)
|
||||
RefreshTokenStorageUiState();
|
||||
}
|
||||
|
||||
private async Task<bool> Save_Important(Configuration config)
|
||||
{
|
||||
var newBooks = booksSelectControl.SelectedDirectory;
|
||||
|
||||
@@ -96,6 +152,10 @@ public partial class SettingsDialog
|
||||
}
|
||||
#endregion
|
||||
|
||||
var selectedTokenMethod = SelectedTokenStorageMethod;
|
||||
var tokenDecision = await TokenStorageSettingsUi.PromptOnPreferenceSaveAsync(this, initialTokenStorageMethod, selectedTokenMethod);
|
||||
if (tokenDecision == TokenStorageSaveDecision.Abort)
|
||||
return false;
|
||||
|
||||
config.Books = newBooks;
|
||||
|
||||
@@ -116,6 +176,17 @@ public partial class SettingsDialog
|
||||
config.CreationTime = (creationTimeCb.SelectedItem as EnumDisplay<Configuration.DateTimeSource>)?.Value ?? Configuration.DateTimeSource.File;
|
||||
config.LastWriteTime = (lastWriteTimeCb.SelectedItem as EnumDisplay<Configuration.DateTimeSource>)?.Value ?? Configuration.DateTimeSource.File;
|
||||
config.ThemeVariant = (themeCb.SelectedItem as EnumDisplay<Configuration.Theme>)?.Value ?? Configuration.Theme.System;
|
||||
config.TokenStorageMethod = selectedTokenMethod;
|
||||
|
||||
if (tokenDecision == TokenStorageSaveDecision.SavePreferenceAndConvert)
|
||||
{
|
||||
var converted = await TokenStorageSettingsUi.RunConversionAsync(this, selectedTokenMethod);
|
||||
if (!converted)
|
||||
return false;
|
||||
}
|
||||
|
||||
initialTokenStorageMethod = selectedTokenMethod;
|
||||
RefreshTokenStorageUiState();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using LibationFileManager;
|
||||
using LibationFileManager.Templates;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace LibationWinForms.Dialogs;
|
||||
@@ -40,9 +41,9 @@ public partial class SettingsDialog : Form
|
||||
textBox.Text = template.EditingTemplate.TemplateText;
|
||||
}
|
||||
|
||||
private void saveBtn_Click(object sender, EventArgs e)
|
||||
private async void saveBtn_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (!Save_Important(config)) return;
|
||||
if (!await Save_Important(config)) return;
|
||||
Save_ImportLibrary(config);
|
||||
Save_DownloadDecrypt(config);
|
||||
Save_AudioSettings(config);
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
using AudibleApi.Authorization;
|
||||
using LibationUiBase;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace TokenStorageSettingsUiTests;
|
||||
|
||||
[TestClass]
|
||||
public class TokenStorageSettingsUiTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void Checkbox_maps_to_enum_values()
|
||||
{
|
||||
Assert.AreEqual(TokenStorageMethod.Encrypted, TokenStorageSettingsUi.MethodFromCheckbox(true));
|
||||
Assert.AreEqual(TokenStorageMethod.Plaintext, TokenStorageSettingsUi.MethodFromCheckbox(false));
|
||||
Assert.IsTrue(TokenStorageSettingsUi.CheckboxFromMethod(TokenStorageMethod.Encrypted));
|
||||
Assert.IsFalse(TokenStorageSettingsUi.CheckboxFromMethod(TokenStorageMethod.Plaintext));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Convert_button_enabled_only_for_mismatch_or_indeterminate()
|
||||
{
|
||||
Assert.IsTrue(TokenStorageSettingsUi.IsConvertButtonEnabled(TokenStorageAlignment.SomeMismatch));
|
||||
Assert.IsTrue(TokenStorageSettingsUi.IsConvertButtonEnabled(TokenStorageAlignment.Indeterminate));
|
||||
Assert.IsFalse(TokenStorageSettingsUi.IsConvertButtonEnabled(TokenStorageAlignment.AllMatch));
|
||||
Assert.IsFalse(TokenStorageSettingsUi.IsConvertButtonEnabled(TokenStorageAlignment.NoApplicableTokens));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Prompt_bodies_warn_clearly()
|
||||
{
|
||||
Assert.AreEqual(
|
||||
TokenStorageSettingsUi.SavePromptEncrypted,
|
||||
TokenStorageSettingsUi.SavePromptBody(TokenStorageMethod.Encrypted));
|
||||
Assert.AreEqual(
|
||||
TokenStorageSettingsUi.SavePromptPlaintext,
|
||||
TokenStorageSettingsUi.SavePromptBody(TokenStorageMethod.Plaintext));
|
||||
|
||||
StringAssert.Contains(TokenStorageSettingsUi.SavePromptPlaintext, "readable");
|
||||
StringAssert.Contains(TokenStorageSettingsUi.StandaloneConfirmPlaintext, "readable");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FormatConversionError_includes_categories_without_secret_values()
|
||||
{
|
||||
var result = IdentityTokenConversionResult.Failure(
|
||||
TokenStorageAlignment.SomeMismatch,
|
||||
"Conversion failed.",
|
||||
"RefreshToken",
|
||||
"AccessToken");
|
||||
|
||||
var message = TokenStorageSettingsUi.FormatConversionError(result);
|
||||
StringAssert.Contains(message, "RefreshToken");
|
||||
StringAssert.Contains(message, "AccessToken");
|
||||
Assert.IsFalse(message.Contains("Atna|"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user