Update 2FA and Captcha controls

This commit is contained in:
Mbucari 2023-02-27 10:36:37 -07:00
parent 1f1f34b6ce
commit ded58f687d
9 changed files with 51 additions and 35 deletions

View File

@ -2,7 +2,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<Version>9.4.0.1</Version> <Version>9.3.0.1</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Octokit" Version="5.0.0" /> <PackageReference Include="Octokit" Version="5.0.0" />

View File

@ -18,7 +18,7 @@ namespace AudibleUtilities
public class ApiExtended public class ApiExtended
{ {
public Api Api { get; private set; } public Api Api { get; private set; }
private const string DeviceName = "Libation";
private ApiExtended(Api api) => Api = api; 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> /// <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>

View File

@ -35,13 +35,13 @@
Text="Password:" /> Text="Password:" />
<TextBox <TextBox
Name="passwordBox"
Grid.Row="2" Grid.Row="2"
Grid.Column="0" Grid.Column="0"
Grid.ColumnSpan="2" Grid.ColumnSpan="2"
Margin="0,10,0,0" Margin="0,10,0,0"
PasswordChar="*" PasswordChar="*"
Text="{Binding Password}" Text="{Binding Password, Mode=TwoWay}" />
IsEnabled="{Binding Password, Converter={x:Static StringConverters.IsNullOrEmpty}}" />
<TextBlock <TextBlock
Grid.Row="3" Grid.Row="3"
@ -51,10 +51,11 @@
Text="CAPTCHA&#xa;answer:" /> Text="CAPTCHA&#xa;answer:" />
<TextBox <TextBox
Name="captchaBox"
Grid.Row="3" Grid.Row="3"
Grid.Column="1" Grid.Column="1"
Margin="0,10,0,0" Margin="0,10,0,0"
Text="{Binding Answer}" /> Text="{Binding Answer, Mode=TwoWay}" />
<Button <Button
Grid.Row="4" Grid.Row="4"

View File

@ -1,3 +1,4 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using LibationAvalonia.ViewModels; using LibationAvalonia.ViewModels;
@ -12,10 +13,12 @@ namespace LibationAvalonia.Dialogs.Login
public string Password => _viewModel.Password; public string Password => _viewModel.Password;
public string Answer => _viewModel.Answer; public string Answer => _viewModel.Answer;
private CaptchaDialogViewModel _viewModel; private readonly CaptchaDialogViewModel _viewModel;
public CaptchaDialog() public CaptchaDialog()
{ {
InitializeComponent(); InitializeComponent();
passwordBox = this.FindControl<TextBox>(nameof(passwordBox));
captchaBox = this.FindControl<TextBox>(nameof(captchaBox));
} }
public CaptchaDialog(string password, byte[] captchaImage) :this() public CaptchaDialog(string password, byte[] captchaImage) :this()
@ -25,19 +28,25 @@ namespace LibationAvalonia.Dialogs.Login
using var gif = SixLabors.ImageSharp.Image.Load(captchaImage); using var gif = SixLabors.ImageSharp.Image.Load(captchaImage);
var gifEncoder = new SixLabors.ImageSharp.Formats.Gif.GifEncoder(); var gifEncoder = new SixLabors.ImageSharp.Formats.Gif.GifEncoder();
var gifFrames = new Bitmap[gif.Frames.Count]; var gifFrames = new Bitmap[gif.Frames.Count];
var frameDelayMs = new int[gif.Frames.Count];
for (int i = 0; i < gif.Frames.Count; i++) for (int i = 0; i < gif.Frames.Count; i++)
{ {
using var framems = new MemoryStream(); var frameMetadata = gif.Frames[i].Metadata.GetFormatMetadata(SixLabors.ImageSharp.Formats.Gif.GifFormat.Instance);
using var clonedFrame = gif.Frames.CloneFrame(i); using var clonedFrame = gif.Frames.CloneFrame(i);
using var framems = new MemoryStream();
clonedFrame.Save(framems, gifEncoder); clonedFrame.Save(framems, gifEncoder);
framems.Position = 0; framems.Position = 0;
gifFrames[i] = new Bitmap(framems); gifFrames[i] = new Bitmap(framems);
frameDelayMs[i] = frameMetadata.FrameDelay * 10;
} }
DataContext = _viewModel = new(password, gifFrames); DataContext = _viewModel = new(password, gifFrames, frameDelayMs);
Opened += (_, _) => (string.IsNullOrEmpty(password) ? passwordBox : captchaBox).Focus();
} }
private void InitializeComponent() private void InitializeComponent()
@ -59,6 +68,12 @@ namespace LibationAvalonia.Dialogs.Login
await base.SaveAndCloseAsync(); await base.SaveAndCloseAsync();
} }
protected override async Task CancelAndCloseAsync()
{
await _viewModel.StopAsync();
await base.CancelAndCloseAsync();
}
public async void Submit_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e) public async void Submit_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
=> await SaveAndCloseAsync(); => await SaveAndCloseAsync();
} }
@ -72,14 +87,11 @@ namespace LibationAvalonia.Dialogs.Login
private Bitmap _captchaImage; private Bitmap _captchaImage;
private bool keepSwitching = true; private bool keepSwitching = true;
private readonly Task FrameSwitch; private readonly Task FrameSwitch;
private readonly Bitmap[] GifFrames;
private const int FRAME_INTERVAL_MS = 100;
public CaptchaDialogViewModel(string password, Bitmap[] gifFrames) public CaptchaDialogViewModel(string password, Bitmap[] gifFrames, int[] frameDelayMs)
{ {
Password = password; Password = password;
GifFrames = gifFrames; FrameSwitch = SwitchFramesAsync(gifFrames, frameDelayMs);
FrameSwitch = SwitchFramesAsync();
} }
public async Task StopAsync() public async Task StopAsync()
@ -88,16 +100,19 @@ namespace LibationAvalonia.Dialogs.Login
await FrameSwitch; await FrameSwitch;
} }
private async Task SwitchFramesAsync() private async Task SwitchFramesAsync(Bitmap[] gifFrames, int[] frameDelayMs)
{ {
int index = 0; int index = 0;
while(keepSwitching) while(keepSwitching)
{ {
CaptchaImage = GifFrames[index++]; CaptchaImage = gifFrames[index];
await Task.Delay(frameDelayMs[index++]);
index %= GifFrames.Length; index %= gifFrames.Length;
await Task.Delay(FRAME_INTERVAL_MS);
} }
foreach (var frame in gifFrames)
frame.Dispose();
} }
} }
} }

View File

@ -9,10 +9,12 @@
Title="2FA Code" Title="2FA Code"
Icon="/Assets/libation.ico"> Icon="/Assets/libation.ico">
<Grid VerticalAlignment="Stretch" RowDefinitions="*,Auto,Auto,Auto"> <Grid
VerticalAlignment="Stretch"
ColumnDefinitions="*" Margin="5"
RowDefinitions="*,Auto,Auto,Auto">
<TextBlock <TextBlock
Margin="5"
TextAlignment="Center" TextAlignment="Center"
TextWrapping="Wrap" TextWrapping="Wrap"
Text="{Binding Prompt}" /> Text="{Binding Prompt}" />
@ -24,14 +26,15 @@
Text="Enter 2FA Code" /> Text="Enter 2FA Code" />
<TextBox <TextBox
Name="_2FABox"
Margin="5,0,5,0" Margin="5,0,5,0"
Grid.Row="2" Grid.Row="2"
HorizontalContentAlignment="Center"
Text="{Binding Code, Mode=TwoWay}" /> Text="{Binding Code, Mode=TwoWay}" />
<Button <Button
Margin="5" Margin="5"
Grid.Row="3" Grid.Row="3"
VerticalAlignment="Bottom"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center" HorizontalContentAlignment="Center"
Content="Submit" Content="Submit"

View File

@ -1,4 +1,3 @@
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -8,20 +7,20 @@ namespace LibationAvalonia.Dialogs.Login
public partial class _2faCodeDialog : DialogWindow public partial class _2faCodeDialog : DialogWindow
{ {
public string Code { get; set; } public string Code { get; set; }
public string Prompt { get; } public string Prompt { get; } = "For added security, please enter the One Time Password (OTP) generated by your Authenticator App";
public _2faCodeDialog() => InitializeComponent(); public _2faCodeDialog()
{
AvaloniaXamlLoader.Load(this);
_2FABox = this.FindControl<TextBox>(nameof(_2FABox));
}
public _2faCodeDialog(string prompt) : this() public _2faCodeDialog(string prompt) : this()
{ {
Prompt = prompt; Prompt = prompt;
DataContext = this; DataContext = this;
} Opened += (_, _) => _2FABox.Focus();
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
} }
protected override Task SaveAndCloseAsync() protected override Task SaveAndCloseAsync()

View File

@ -24,10 +24,8 @@ namespace LibationWinForms.Dialogs.Login
this.captchaPb.Image = image; this.captchaPb.Image = image;
passwordTb.Text = password; passwordTb.Text = password;
passwordTb.Enabled = string.IsNullOrEmpty(password);
if (passwordTb.Enabled) (string.IsNullOrEmpty(password) ? passwordTb : answerTb).Select();
answerTb.Select();
} }
private void submitBtn_Click(object sender, EventArgs e) private void submitBtn_Click(object sender, EventArgs e)

View File

@ -47,7 +47,7 @@ namespace LibationWinForms
// load init state to menu checkbox // load init state to menu checkbox
Load += updateAutoScanLibraryToolStripMenuItem; Load += updateAutoScanLibraryToolStripMenuItem;
// if enabled: begin on load // if enabled: begin on load
Load += startAutoScan; Shown += startAutoScan;
// if new 'default' account is added, run autoscan // if new 'default' account is added, run autoscan
AccountsSettingsPersister.Saving += accountsPreSave; AccountsSettingsPersister.Saving += accountsPreSave;