Fix MFA and 2FA

This commit is contained in:
Mbucari 2023-02-26 12:03:22 -07:00
parent b2f68760b2
commit ffadf90f4f
21 changed files with 305 additions and 194 deletions

View File

@ -3,7 +3,6 @@ using Dinah.Core.Net.Http;
using Dinah.Core.StepRunner;
using FileManager;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
@ -225,6 +224,7 @@ namespace AaxDecrypter
}
finally
{
nfsp.NetworkFileStream.RequestHeaders["User-Agent"] = DownloadOptions.UserAgent;
nfsp.NetworkFileStream.SpeedLimit = DownloadOptions.DownloadSpeedBps;
}

View File

@ -18,7 +18,7 @@ namespace AudibleUtilities
public class ApiExtended
{
public Api Api { get; private set; }
private const string DeviceName = "Libation";
private ApiExtended(Api api) => Api = api;
/// <summary>Get api from existing tokens else login with 'eager' choice. External browser url is provided. Response can be external browser login or continuing with native api callbacks.</summary>
@ -39,42 +39,6 @@ namespace AudibleUtilities
return new ApiExtended(api);
}
/// <summary>Get api from existing tokens else login with native api callbacks.</summary>
public static async Task<ApiExtended> CreateAsync(Account account, ILoginCallback loginCallback)
{
Serilog.Log.Logger.Information("{@DebugInfo}", new
{
LoginType = nameof(ILoginCallback),
Account = account?.MaskedLogEntry ?? "[null]",
LocaleName = account?.Locale?.Name
});
var api = await EzApiCreator.GetApiAsync(
loginCallback,
account.Locale,
AudibleApiStorage.AccountsSettingsFile,
account.GetIdentityTokensJsonPath());
return new ApiExtended(api);
}
/// <summary>Get api from existing tokens else login with external browser</summary>
public static async Task<ApiExtended> CreateAsync(Account account, ILoginExternal loginExternal)
{
Serilog.Log.Logger.Information("{@DebugInfo}", new
{
LoginType = nameof(ILoginExternal),
Account = account?.MaskedLogEntry ?? "[null]",
LocaleName = account?.Locale?.Name
});
var api = await EzApiCreator.GetApiAsync(
loginExternal,
account.Locale,
AudibleApiStorage.AccountsSettingsFile,
account.GetIdentityTokensJsonPath());
return new ApiExtended(api);
}
/// <summary>Get api from existing tokens. Assumes you have valid login tokens. Else exception</summary>
public static async Task<ApiExtended> CreateAsync(Account account)
{

View File

@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AudibleApi" Version="8.0.0.1" />
<PackageReference Include="AudibleApi" Version="8.1.0.1" />
</ItemGroup>
<ItemGroup>

View File

@ -4,13 +4,14 @@ using System.Linq;
using System.Threading.Tasks;
using AudibleApi;
using AudibleApi.Authorization;
using AudibleApi.Cryptography;
using Dinah.Core;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace AudibleUtilities
{
public partial class Mkb79Auth : IIdentityMaintainer
public partial class Mkb79Auth : IIdentityMaintainer
{
[JsonProperty("website_cookies")]
private JObject _websiteCookies { get; set; }
@ -178,7 +179,7 @@ namespace AudibleUtilities
LocaleCode = account.Locale.CountryCode,
RefreshToken = account.IdentityTokens.RefreshToken.Value,
StoreAuthenticationCookie = account.IdentityTokens.StoreAuthenticationCookie,
WebsiteCookies = new(account.IdentityTokens.Cookies.ToKeyValuePair()),
WebsiteCookies = new(account.IdentityTokens.Cookies),
};
}

View File

@ -36,6 +36,7 @@
Width="60"
Height="30"
Content="X"
HorizontalContentAlignment="Center"
IsEnabled="{Binding !IsDefault}"
Click="DeleteButton_Clicked" />

View File

@ -9,26 +9,28 @@ namespace LibationAvalonia.Dialogs.Login
{
private Account _account { get; }
public string DeviceName { get; } = "Libation";
public AvaloniaLoginCallback(Account account)
{
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
}
public async Task<string> Get2faCodeAsync()
public async Task<string> Get2faCodeAsync(string prompt)
{
var dialog = new _2faCodeDialog();
var dialog = new _2faCodeDialog(prompt);
if (await ShowDialog(dialog))
return dialog.Code;
return null;
}
public async Task<string> GetCaptchaAnswerAsync(byte[] captchaImage)
public async Task<(string password, string guess)> GetCaptchaAnswerAsync(string password, byte[] captchaImage)
{
var dialog = new CaptchaDialog(captchaImage);
var dialog = new CaptchaDialog(password, captchaImage);
if (await ShowDialog(dialog))
return dialog.Answer;
return null;
return (dialog.Password, dialog.Answer);
return (null,null);
}
public async Task<(string name, string value)> GetMfaChoiceAsync(MfaConfig mfaConfig)

View File

@ -2,22 +2,22 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="220" d:DesignHeight="180"
MinWidth="220" MinHeight="180"
MaxWidth="220" MaxHeight="180"
mc:Ignorable="d" d:DesignWidth="220" d:DesignHeight="250"
MinWidth="220" MinHeight="250"
MaxWidth="220" MaxHeight="250"
x:Class="LibationAvalonia.Dialogs.Login.CaptchaDialog"
Title="CAPTCHA"
Icon="/Assets/libation.ico">
<Grid
RowDefinitions="Auto,Auto,*"
ColumnDefinitions="Auto,*">
RowDefinitions="Auto,Auto,Auto,Auto,*"
ColumnDefinitions="Auto,*"
Margin="10">
<Panel
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2"
Margin="10"
MinWidth="200"
MinHeight="70"
Background="LightGray">
@ -30,23 +30,39 @@
<TextBlock
Grid.Row="1"
Margin="0,10,0,0"
VerticalAlignment="Center"
Text="Password:" />
<TextBox
Grid.Row="2"
Grid.Column="0"
Margin="10,0,10,0"
Grid.ColumnSpan="2"
Margin="0,10,0,0"
PasswordChar="*"
Text="{Binding Password}"
IsEnabled="{Binding Password, Converter={x:Static StringConverters.IsNullOrEmpty}}" />
<TextBlock
Grid.Row="3"
Grid.Column="0"
Margin="0,10,10,0"
VerticalAlignment="Center"
Text="CAPTCHA&#xa;answer:" />
<TextBox
Grid.Row="1"
Grid.Row="3"
Grid.Column="1"
Margin="10,0,10,0" Text="{Binding Answer}" />
Margin="0,10,0,0"
Text="{Binding Answer}" />
<Button
Grid.Row="2"
Grid.Row="4"
Grid.Column="1"
Margin="10"
Padding="0,5,0,5"
VerticalAlignment="Bottom"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Content="Submit"
Click="Submit_Click" />

View File

@ -1,5 +1,7 @@
using Avalonia.Markup.Xaml;
using Avalonia.Media.Imaging;
using LibationAvalonia.ViewModels;
using ReactiveUI;
using System.IO;
using System.Threading.Tasks;
@ -7,18 +9,35 @@ namespace LibationAvalonia.Dialogs.Login
{
public partial class CaptchaDialog : DialogWindow
{
public string Answer { get; set; }
public Bitmap CaptchaImage { get; }
public string Password => _viewModel.Password;
public string Answer => _viewModel.Answer;
private CaptchaDialogViewModel _viewModel;
public CaptchaDialog()
{
InitializeComponent();
}
public CaptchaDialog(byte[] captchaImage) :this()
public CaptchaDialog(string password, byte[] captchaImage) :this()
{
using var ms = new MemoryStream(captchaImage);
CaptchaImage = new Bitmap(ms);
DataContext = this;
//Avalonia doesn't support animated gifs.
//Deconstruct gifs into frames and manually switch them.
using var gif = SixLabors.ImageSharp.Image.Load(captchaImage);
var gifEncoder = new SixLabors.ImageSharp.Formats.Gif.GifEncoder();
var gifFrames = new Bitmap[gif.Frames.Count];
for (int i = 0; i < gif.Frames.Count; i++)
{
using var framems = new MemoryStream();
using var clonedFrame = gif.Frames.CloneFrame(i);
clonedFrame.Save(framems, gifEncoder);
framems.Position = 0;
gifFrames[i] = new Bitmap(framems);
}
DataContext = _viewModel = new(password, gifFrames);
}
private void InitializeComponent()
@ -26,15 +45,59 @@ namespace LibationAvalonia.Dialogs.Login
AvaloniaXamlLoader.Load(this);
}
protected override Task SaveAndCloseAsync()
protected override async Task SaveAndCloseAsync()
{
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { Answer });
if (string.IsNullOrWhiteSpace(_viewModel.Password))
{
await MessageBox.Show(this, "Please re-enter your password");
return;
}
return base.SaveAndCloseAsync();
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { _viewModel.Answer });
await _viewModel.StopAsync();
await base.SaveAndCloseAsync();
}
public async void Submit_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await SaveAndCloseAsync();
}
public class CaptchaDialogViewModel : ViewModelBase
{
public string Answer { get; set; }
public string Password { get; set; }
public Bitmap CaptchaImage { get => _captchaImage; private set => this.RaiseAndSetIfChanged(ref _captchaImage, value); }
private Bitmap _captchaImage;
private bool keepSwitching = true;
private readonly Task FrameSwitch;
private readonly Bitmap[] GifFrames;
private const int FRAME_INTERVAL_MS = 100;
public CaptchaDialogViewModel(string password, Bitmap[] gifFrames)
{
Password = password;
GifFrames = gifFrames;
FrameSwitch = SwitchFramesAsync();
}
public async Task StopAsync()
{
keepSwitching = false;
await FrameSwitch;
}
private async Task SwitchFramesAsync()
{
int index = 0;
while(keepSwitching)
{
CaptchaImage = GifFrames[index++];
index %= GifFrames.Length;
await Task.Delay(FRAME_INTERVAL_MS);
}
}
}
}

View File

@ -1,4 +1,3 @@
using AudibleApi;
using AudibleUtilities;
using Avalonia;
using Avalonia.Controls;
@ -49,7 +48,7 @@ namespace LibationAvalonia.Dialogs.Login
protected override async Task SaveAndCloseAsync()
{
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { ResponseUrl });
if (!Uri.TryCreate(ResponseUrl, UriKind.Absolute, out var result))
if (!Uri.TryCreate(ResponseUrl, UriKind.Absolute, out _))
{
await MessageBox.Show("Invalid response URL");
return;

View File

@ -2,9 +2,9 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="160"
MinWidth="400" MinHeight="160"
MaxWidth="400" MaxHeight="160"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="200"
MinWidth="400" MinHeight="200"
MaxWidth="400" MaxHeight="200"
x:Class="LibationAvalonia.Dialogs.Login.MfaDialog"
Title="Two-Step Verification"
Icon="/Assets/libation.ico">

View File

@ -2,30 +2,38 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="140" d:DesignHeight="100"
MinWidth="140" MinHeight="100"
MaxWidth="140" MaxHeight="100"
mc:Ignorable="d" d:DesignWidth="200" d:DesignHeight="200"
MinWidth="200" MinHeight="200"
MaxWidth="200" MaxHeight="200"
x:Class="LibationAvalonia.Dialogs.Login._2faCodeDialog"
Title="2FA Code"
Icon="/Assets/libation.ico">
<Grid RowDefinitions="Auto,Auto,*">
<Grid VerticalAlignment="Stretch" RowDefinitions="*,Auto,Auto,Auto">
<TextBlock
Margin="5"
TextAlignment="Center"
TextWrapping="Wrap"
Text="{Binding Prompt}" />
<TextBlock
Margin="5"
Grid.Row="1"
TextAlignment="Center"
Text="Enter 2FA Code" />
<TextBox
Margin="5,0,5,0"
Grid.Row="1"
Grid.Row="2"
Text="{Binding Code, Mode=TwoWay}" />
<Button
Margin="5"
Grid.Row="2"
Grid.Row="3"
VerticalAlignment="Bottom"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Content="Submit"
Click="Submit_Click" />
</Grid>

View File

@ -8,10 +8,14 @@ namespace LibationAvalonia.Dialogs.Login
public partial class _2faCodeDialog : DialogWindow
{
public string Code { get; set; }
public string Prompt { get; }
public _2faCodeDialog()
public _2faCodeDialog() => InitializeComponent();
public _2faCodeDialog(string prompt) : this()
{
InitializeComponent();
Prompt = prompt;
DataContext = this;
}

View File

@ -109,7 +109,8 @@
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.0-preview4 " />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0-preview4" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-preview4" />
<PackageReference Include="XamlNameReferenceGenerator" Version="1.5.1" />
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" />
<PackageReference Include="XamlNameReferenceGenerator" Version="1.6.1" />
</ItemGroup>
<ItemGroup>

View File

@ -9,7 +9,6 @@ using System.Linq;
namespace LibationAvalonia.Views
{
//DONE
public partial class MainWindow
{
private InterruptableTimer autoScanTimer;
@ -17,11 +16,7 @@ namespace LibationAvalonia.Views
private void Configure_ScanAuto()
{
// creating InterruptableTimer inside 'Configure_' is a break from the pattern. As long as no one else needs to access or subscribe to it, this is ok
var hours = 0;
var minutes = 5;
var seconds = 0;
var _5_minutes = new TimeSpan(hours, minutes, seconds);
autoScanTimer = new InterruptableTimer(_5_minutes);
autoScanTimer = new InterruptableTimer(TimeSpan.FromMinutes(5));
// subscribe as async/non-blocking. I'd actually rather prefer blocking but real-world testing found that caused a deadlock in the AudibleAPI
autoScanTimer.Elapsed += async (_, __) =>
@ -44,9 +39,9 @@ namespace LibationAvalonia.Views
};
_viewModel.AutoScanChecked = Configuration.Instance.AutoScan;
// if enabled: begin on load
Load += startAutoScan;
Opened += startAutoScan;
// if new 'default' account is added, run autoscan
AccountsSettingsPersister.Saving += accountsPreSave;

View File

@ -28,68 +28,95 @@
/// </summary>
private void InitializeComponent()
{
this.captchaPb = new System.Windows.Forms.PictureBox();
this.answerTb = new System.Windows.Forms.TextBox();
this.submitBtn = new System.Windows.Forms.Button();
this.answerLbl = new System.Windows.Forms.Label();
((System.ComponentModel.ISupportInitialize)(this.captchaPb)).BeginInit();
this.SuspendLayout();
captchaPb = new System.Windows.Forms.PictureBox();
answerTb = new System.Windows.Forms.TextBox();
submitBtn = new System.Windows.Forms.Button();
answerLbl = new System.Windows.Forms.Label();
label1 = new System.Windows.Forms.Label();
passwordTb = new System.Windows.Forms.TextBox();
((System.ComponentModel.ISupportInitialize)captchaPb).BeginInit();
SuspendLayout();
//
// captchaPb
//
this.captchaPb.Location = new System.Drawing.Point(12, 12);
this.captchaPb.Name = "captchaPb";
this.captchaPb.Size = new System.Drawing.Size(200, 70);
this.captchaPb.TabIndex = 0;
this.captchaPb.TabStop = false;
captchaPb.Location = new System.Drawing.Point(13, 14);
captchaPb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
captchaPb.Name = "captchaPb";
captchaPb.Size = new System.Drawing.Size(235, 81);
captchaPb.TabIndex = 0;
captchaPb.TabStop = false;
//
// answerTb
//
this.answerTb.Location = new System.Drawing.Point(118, 88);
this.answerTb.Name = "answerTb";
this.answerTb.Size = new System.Drawing.Size(94, 20);
this.answerTb.TabIndex = 1;
answerTb.Location = new System.Drawing.Point(136, 130);
answerTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
answerTb.Name = "answerTb";
answerTb.Size = new System.Drawing.Size(111, 23);
answerTb.TabIndex = 2;
//
// submitBtn
//
this.submitBtn.Location = new System.Drawing.Point(137, 114);
this.submitBtn.Name = "submitBtn";
this.submitBtn.Size = new System.Drawing.Size(75, 23);
this.submitBtn.TabIndex = 2;
this.submitBtn.Text = "Submit";
this.submitBtn.UseVisualStyleBackColor = true;
this.submitBtn.Click += new System.EventHandler(this.submitBtn_Click);
submitBtn.Location = new System.Drawing.Point(159, 171);
submitBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
submitBtn.Name = "submitBtn";
submitBtn.Size = new System.Drawing.Size(88, 27);
submitBtn.TabIndex = 2;
submitBtn.Text = "Submit";
submitBtn.UseVisualStyleBackColor = true;
submitBtn.Click += submitBtn_Click;
//
// answerLbl
//
this.answerLbl.AutoSize = true;
this.answerLbl.Location = new System.Drawing.Point(12, 91);
this.answerLbl.Name = "answerLbl";
this.answerLbl.Size = new System.Drawing.Size(100, 13);
this.answerLbl.TabIndex = 0;
this.answerLbl.Text = "CAPTCHA answer: ";
answerLbl.AutoSize = true;
answerLbl.Location = new System.Drawing.Point(13, 133);
answerLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
answerLbl.Name = "answerLbl";
answerLbl.Size = new System.Drawing.Size(106, 15);
answerLbl.TabIndex = 0;
answerLbl.Text = "CAPTCHA answer: ";
//
// label1
//
label1.AutoSize = true;
label1.Location = new System.Drawing.Point(13, 104);
label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
label1.Name = "label1";
label1.Size = new System.Drawing.Size(60, 15);
label1.TabIndex = 0;
label1.Text = "Password:";
//
// passwordTb
//
passwordTb.Location = new System.Drawing.Point(81, 101);
passwordTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
passwordTb.Name = "passwordTb";
passwordTb.PasswordChar = '*';
passwordTb.Size = new System.Drawing.Size(167, 23);
passwordTb.TabIndex = 1;
//
// CaptchaDialog
//
this.AcceptButton = this.submitBtn;
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(224, 149);
this.Controls.Add(this.answerLbl);
this.Controls.Add(this.submitBtn);
this.Controls.Add(this.answerTb);
this.Controls.Add(this.captchaPb);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "CaptchaDialog";
this.ShowIcon = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "CAPTCHA";
((System.ComponentModel.ISupportInitialize)(this.captchaPb)).EndInit();
this.ResumeLayout(false);
this.PerformLayout();
AcceptButton = submitBtn;
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
ClientSize = new System.Drawing.Size(261, 210);
Controls.Add(passwordTb);
Controls.Add(label1);
Controls.Add(answerLbl);
Controls.Add(submitBtn);
Controls.Add(answerTb);
Controls.Add(captchaPb);
FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
MaximizeBox = false;
MinimizeBox = false;
Name = "CaptchaDialog";
ShowIcon = false;
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
Text = "CAPTCHA";
((System.ComponentModel.ISupportInitialize)captchaPb).EndInit();
ResumeLayout(false);
PerformLayout();
}
#endregion
@ -98,5 +125,7 @@
private System.Windows.Forms.TextBox answerTb;
private System.Windows.Forms.Button submitBtn;
private System.Windows.Forms.Label answerLbl;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.TextBox passwordTb;
}
}

View File

@ -9,23 +9,37 @@ namespace LibationWinForms.Dialogs.Login
public partial class CaptchaDialog : Form
{
public string Answer { get; private set; }
public string Password { get; private set; }
private MemoryStream ms { get; }
private Image image { get; }
public CaptchaDialog(byte[] captchaImage)
public CaptchaDialog() => InitializeComponent();
public CaptchaDialog(string password, byte[] captchaImage) : this()
{
InitializeComponent();
this.FormClosed += (_, __) => { ms?.Dispose(); image?.Dispose(); };
ms = new MemoryStream(captchaImage);
image = Image.FromStream(ms);
this.captchaPb.Image = image;
passwordTb.Text = password;
passwordTb.Enabled = string.IsNullOrEmpty(password);
if (passwordTb.Enabled)
answerTb.Select();
}
private void submitBtn_Click(object sender, EventArgs e)
{
Answer = this.answerTb.Text;
if (string.IsNullOrWhiteSpace(passwordTb.Text))
{
MessageBox.Show(this, "Please re-enter your password");
return;
}
Answer = answerTb.Text;
Password = passwordTb.Text;
Serilog.Log.Logger.Information("Submit button clicked: {@DebugInfo}", new { Answer });

View File

@ -10,25 +10,27 @@ namespace LibationWinForms.Login
{
private Account _account { get; }
public string DeviceName { get; } = "Libation";
public WinformLoginCallback(Account account)
{
_account = Dinah.Core.ArgumentValidator.EnsureNotNull(account, nameof(account));
}
public Task<string> Get2faCodeAsync()
public Task<string> Get2faCodeAsync(string prompt)
{
using var dialog = new _2faCodeDialog();
using var dialog = new _2faCodeDialog(prompt);
if (ShowDialog(dialog))
return Task.FromResult(dialog.Code);
return Task.FromResult<string>(null);
}
public Task<string> GetCaptchaAnswerAsync(byte[] captchaImage)
public Task<(string password, string guess)> GetCaptchaAnswerAsync(string password, byte[] captchaImage)
{
using var dialog = new CaptchaDialog(captchaImage);
using var dialog = new CaptchaDialog(password, captchaImage);
if (ShowDialog(dialog))
return Task.FromResult(dialog.Answer);
return Task.FromResult<string>(null);
return Task.FromResult((dialog.Password, dialog.Answer));
return Task.FromResult<(string, string)>((null,null));
}
public Task<(string name, string value)> GetMfaChoiceAsync(MfaConfig mfaConfig)

View File

@ -28,62 +28,77 @@
/// </summary>
private void InitializeComponent()
{
this.submitBtn = new System.Windows.Forms.Button();
this.codeTb = new System.Windows.Forms.TextBox();
this.label1 = new System.Windows.Forms.Label();
this.SuspendLayout();
submitBtn = new System.Windows.Forms.Button();
codeTb = new System.Windows.Forms.TextBox();
label1 = new System.Windows.Forms.Label();
promptLbl = new System.Windows.Forms.Label();
SuspendLayout();
//
// submitBtn
//
this.submitBtn.Location = new System.Drawing.Point(15, 51);
this.submitBtn.Name = "SaveBtn";
this.submitBtn.Size = new System.Drawing.Size(79, 23);
this.submitBtn.TabIndex = 1;
this.submitBtn.Text = "Submit";
this.submitBtn.UseVisualStyleBackColor = true;
this.submitBtn.Click += new System.EventHandler(this.submitBtn_Click);
submitBtn.Location = new System.Drawing.Point(18, 108);
submitBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
submitBtn.Name = "submitBtn";
submitBtn.Size = new System.Drawing.Size(191, 27);
submitBtn.TabIndex = 1;
submitBtn.Text = "Submit";
submitBtn.UseVisualStyleBackColor = true;
submitBtn.Click += submitBtn_Click;
//
// codeTb
//
this.codeTb.Location = new System.Drawing.Point(15, 25);
this.codeTb.Name = "newTagsTb";
this.codeTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
this.codeTb.Size = new System.Drawing.Size(79, 20);
this.codeTb.TabIndex = 0;
codeTb.Location = new System.Drawing.Point(108, 79);
codeTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
codeTb.Name = "codeTb";
codeTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
codeTb.Size = new System.Drawing.Size(101, 23);
codeTb.TabIndex = 0;
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(12, 9);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(82, 13);
this.label1.TabIndex = 2;
this.label1.Text = "Enter 2FA Code";
label1.AutoSize = true;
label1.Location = new System.Drawing.Point(13, 82);
label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
label1.Name = "label1";
label1.Size = new System.Drawing.Size(87, 15);
label1.TabIndex = 2;
label1.Text = "Enter 2FA Code";
//
// promptLbl
//
promptLbl.Location = new System.Drawing.Point(13, 9);
promptLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
promptLbl.Name = "promptLbl";
promptLbl.Size = new System.Drawing.Size(196, 59);
promptLbl.TabIndex = 2;
promptLbl.Text = "[Prompt]";
//
// _2faCodeDialog
//
this.AcceptButton = this.submitBtn;
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(106, 86);
this.Controls.Add(this.label1);
this.Controls.Add(this.codeTb);
this.Controls.Add(this.submitBtn);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "_2faCodeDialog";
this.ShowIcon = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "2FA Code";
this.ResumeLayout(false);
this.PerformLayout();
AcceptButton = submitBtn;
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
ClientSize = new System.Drawing.Size(222, 147);
Controls.Add(promptLbl);
Controls.Add(label1);
Controls.Add(codeTb);
Controls.Add(submitBtn);
FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
MaximizeBox = false;
MinimizeBox = false;
Name = "_2faCodeDialog";
ShowIcon = false;
StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
Text = "2FA Code";
ResumeLayout(false);
PerformLayout();
}
#endregion
private System.Windows.Forms.Button submitBtn;
private System.Windows.Forms.TextBox codeTb;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Label promptLbl;
}
}

View File

@ -8,9 +8,10 @@ namespace LibationWinForms.Dialogs.Login
{
public string Code { get; private set; }
public _2faCodeDialog()
public _2faCodeDialog() => InitializeComponent();
public _2faCodeDialog(string prompt) : this()
{
InitializeComponent();
promptLbl.Text = prompt;
}
private void submitBtn_Click(object sender, EventArgs e)

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">

View File

@ -17,11 +17,8 @@ namespace LibationWinForms
private void Configure_ScanAuto()
{
// creating InterruptableTimer inside 'Configure_' is a break from the pattern. As long as no one else needs to access or subscribe to it, this is ok
var hours = 0;
var minutes = 5;
var seconds = 0;
var _5_minutes = new TimeSpan(hours, minutes, seconds);
autoScanTimer = new InterruptableTimer(_5_minutes);
autoScanTimer = new InterruptableTimer(TimeSpan.FromMinutes(5));
// subscribe as async/non-blocking. I'd actually rather prefer blocking but real-world testing found that caused a deadlock in the AudibleAPI
autoScanTimer.Elapsed += async (_, __) =>