Add Important settings UI for encrypted token storage

This commit is contained in:
Robert McRackan
2026-07-25 21:32:42 -04:00
parent bb18e9adc4
commit abea874db8
9 changed files with 569 additions and 16 deletions

View File

@@ -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">

View File

@@ -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();
}

View File

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

View File

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

View 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
}

View File

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

View File

@@ -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;
}

View File

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

View File

@@ -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|"));
}
}