From 5c20d7c2e0c664fa857dc06829ee52973891b1f1 Mon Sep 17 00:00:00 2001
From: erri120 <erri120@protonmail.com>
Date: Tue, 5 May 2020 16:17:55 +0200
Subject: [PATCH] Added MFA support for INeedsLoginCredentials

---
 Wabbajack.Lib/Downloaders/INeedsLogin.cs      | 17 ++++++++---
 Wabbajack.Lib/Downloaders/MEGADownloader.cs   | 30 +++++++++++--------
 Wabbajack.Lib/Wabbajack.Lib.csproj            |  2 +-
 .../Settings/CredentialsLoginVM.cs            | 16 ++++++++--
 .../Views/Settings/CredentialsLoginView.xaml  | 14 +++++----
 .../Settings/CredentialsLoginView.xaml.cs     |  6 ++++
 6 files changed, 59 insertions(+), 26 deletions(-)

diff --git a/Wabbajack.Lib/Downloaders/INeedsLogin.cs b/Wabbajack.Lib/Downloaders/INeedsLogin.cs
index 8ce03afe..b9aae38e 100644
--- a/Wabbajack.Lib/Downloaders/INeedsLogin.cs
+++ b/Wabbajack.Lib/Downloaders/INeedsLogin.cs
@@ -16,20 +16,29 @@ namespace Wabbajack.Lib.Downloaders
         Uri? IconUri { get; }
     }
 
+    public enum LoginReturnCode
+    {
+        InternalError = -1,
+        Success = 0,
+        BadInput = 1,
+        BadCredentials = 2,
+        NeedsMFA = 3,
+    }
+
     public struct LoginReturnMessage
     {
         public string Message;
-        public bool Failure;
+        public LoginReturnCode ReturnCode;
 
-        public LoginReturnMessage(string message, bool failure)
+        public LoginReturnMessage(string message, LoginReturnCode returnCode)
         {
             Message = message;
-            Failure = failure;
+            ReturnCode = returnCode;
         }
     }
 
     public interface INeedsLoginCredentials : INeedsLogin
     {
-        LoginReturnMessage LoginWithCredentials(string username, SecureString password);
+        LoginReturnMessage LoginWithCredentials(string username, SecureString password, string? mfa);
     }
 }
diff --git a/Wabbajack.Lib/Downloaders/MEGADownloader.cs b/Wabbajack.Lib/Downloaders/MEGADownloader.cs
index cac854fe..83f172b9 100644
--- a/Wabbajack.Lib/Downloaders/MEGADownloader.cs
+++ b/Wabbajack.Lib/Downloaders/MEGADownloader.cs
@@ -38,34 +38,34 @@ namespace Wabbajack.Lib.Downloaders
                 {
                     Email = infos.Email,
                     Hash = infos.Hash,
-                    PasswordAesKey = infos.PasswordAesKey
+                    PasswordAesKey = infos.PasswordAesKey,
+                    MFAKey = infos.MFAKey
                 };
             }
 
             public MegaApiClient.AuthInfos ToAuthInfos()
             {
-                //TODO: MFAKey in the update
-                return new MegaApiClient.AuthInfos(Email, Hash, PasswordAesKey);
+                return new MegaApiClient.AuthInfos(Email, Hash, PasswordAesKey, MFAKey);
             }
         }
 
-        public LoginReturnMessage LoginWithCredentials(string username, SecureString password)
+        public LoginReturnMessage LoginWithCredentials(string username, SecureString password, string? mfa)
         {
             MegaApiClient.AuthInfos authInfos;
 
             try
             {
-                authInfos = MegaApiClient.GenerateAuthInfos(username, password.ToNormalString());
+                authInfos = MegaApiClient.GenerateAuthInfos(username, password.ToNormalString(), mfa);
             }
             catch (ApiException e)
             {
                 return e.ApiResultCode switch
                 {
                     ApiResultCode.BadArguments => new LoginReturnMessage($"Email or password was wrong! {e.Message}",
-                        true),
+                        LoginReturnCode.BadCredentials),
                     ApiResultCode.InternalError => new LoginReturnMessage(
-                        $"Internal error occured! Please report this to the Wabbajack Team! {e.Message}", true),
-                    _ => new LoginReturnMessage($"Error generating authentication information! {e.Message}", true)
+                        $"Internal error occured! Please report this to the Wabbajack Team! {e.Message}", LoginReturnCode.InternalError),
+                    _ => new LoginReturnMessage($"Error generating authentication information! {e.Message}", LoginReturnCode.InternalError)
                 };
             }
             
@@ -78,10 +78,14 @@ namespace Wabbajack.Lib.Downloaders
                 return e.ApiResultCode switch
                 {
                     ApiResultCode.TwoFactorAuthenticationError => new LoginReturnMessage(
-                        $"Two-Factor Authentication needs to be disabled before login! {e.Message}", true),
+                        $"Two-Factor Authentication is enabled. Input your TFA key above and try again! {e.Message}", LoginReturnCode.NeedsMFA),
                     ApiResultCode.InternalError => new LoginReturnMessage(
-                        $"Internal error occured! Please report this to the Wabbajack Team! {e.Message}", true),
-                    _ => new LoginReturnMessage($"Error during login: {e.Message}", true)
+                        $"Internal error occured! Please report this to the Wabbajack Team! {e.Message}", LoginReturnCode.InternalError),
+                    ApiResultCode.RequestIncomplete => new LoginReturnMessage(
+                        $"Bad credentials! {e.Message}", LoginReturnCode.BadCredentials),
+                    ApiResultCode.ResourceNotExists => new LoginReturnMessage(
+                        $"Bad credentials! {e.Message}", LoginReturnCode.BadCredentials),
+                    _ => new LoginReturnMessage($"Error during login: {e.Message}", LoginReturnCode.InternalError)
                 };
             }
 
@@ -92,7 +96,7 @@ namespace Wabbajack.Lib.Downloaders
             }
 
             return new LoginReturnMessage("Logged in successfully, you can now close this window.",
-                !MegaApiClient.IsLoggedIn || !Utils.HaveEncryptedJson(DataName));
+                !MegaApiClient.IsLoggedIn || !Utils.HaveEncryptedJson(DataName) ? LoginReturnCode.Success : LoginReturnCode.InternalError);
         }
 
         public MegaDownloader()
@@ -133,7 +137,7 @@ namespace Wabbajack.Lib.Downloaders
 
             private static MegaApiClient MegaApiClient => DownloadDispatcher.GetInstance<MegaDownloader>().MegaApiClient;
 
-            private static AsyncLock _loginLock = new AsyncLock();
+            private static readonly AsyncLock _loginLock = new AsyncLock();
             private static async Task MegaLogin()
             {
                 using var _ = await _loginLock.WaitAsync();
diff --git a/Wabbajack.Lib/Wabbajack.Lib.csproj b/Wabbajack.Lib/Wabbajack.Lib.csproj
index f13f5686..da89051e 100644
--- a/Wabbajack.Lib/Wabbajack.Lib.csproj
+++ b/Wabbajack.Lib/Wabbajack.Lib.csproj
@@ -26,7 +26,7 @@
         <Version>1.11.23</Version>
       </PackageReference>
       <PackageReference Include="MegaApiClient">
-        <Version>1.8.0</Version>
+        <Version>1.8.1</Version>
       </PackageReference>
       <PackageReference Include="Microsoft.CSharp">
         <Version>4.7.0</Version>
diff --git a/Wabbajack/View Models/Settings/CredentialsLoginVM.cs b/Wabbajack/View Models/Settings/CredentialsLoginVM.cs
index d0d7a195..38715a39 100644
--- a/Wabbajack/View Models/Settings/CredentialsLoginVM.cs	
+++ b/Wabbajack/View Models/Settings/CredentialsLoginVM.cs	
@@ -15,12 +15,18 @@ namespace Wabbajack
         [Reactive]
         public string Username { get; set; }
 
+        [Reactive]
+        public string MFAKey { get; set; }
+
         [Reactive]
         public LoginReturnMessage ReturnMessage { get; set; }
 
         private readonly ObservableAsPropertyHelper<bool> _loginEnabled;
         public bool LoginEnabled => _loginEnabled.Value;
 
+        private readonly ObservableAsPropertyHelper<bool> _mfaVisible;
+        public bool MFAVisible => _mfaVisible.Value;
+
         private readonly INeedsLoginCredentials _downloader;
 
         public CredentialsLoginVM(INeedsLoginCredentials downloader)
@@ -31,6 +37,10 @@ namespace Wabbajack
                 .Select(IsValidAddress)
                 .ToGuiProperty(this,
                     nameof(LoginEnabled));
+
+            _mfaVisible = this.WhenAny(x => x.ReturnMessage)
+                .Select(x => x.ReturnCode == LoginReturnCode.NeedsMFA)
+                .ToGuiProperty(this, nameof(MFAVisible));
         }
 
         public void Login(SecureString password)
@@ -39,17 +49,17 @@ namespace Wabbajack
             {
                 if (password == null || password.Length == 0)
                 {
-                    ReturnMessage = new LoginReturnMessage("You need to input a password!", true);
+                    ReturnMessage = new LoginReturnMessage("You need to input a password!", LoginReturnCode.BadInput);
                     return;
                 }
 
-                ReturnMessage = _downloader.LoginWithCredentials(Username, password);
+                ReturnMessage = _downloader.LoginWithCredentials(Username, password, string.IsNullOrWhiteSpace(MFAKey) ? null : MFAKey);
                 password.Clear();
             }
             catch (Exception e)
             {
                 Utils.Error(e, "Exception while trying to login");
-                ReturnMessage = new LoginReturnMessage($"Unhandled exception: {e.Message}", true);
+                ReturnMessage = new LoginReturnMessage($"Unhandled exception: {e.Message}", LoginReturnCode.InternalError);
             }
         }
 
diff --git a/Wabbajack/Views/Settings/CredentialsLoginView.xaml b/Wabbajack/Views/Settings/CredentialsLoginView.xaml
index 528e56df..cde4c645 100644
--- a/Wabbajack/Views/Settings/CredentialsLoginView.xaml
+++ b/Wabbajack/Views/Settings/CredentialsLoginView.xaml
@@ -11,10 +11,11 @@
     d:DesignHeight="450" d:DesignWidth="800">
     <Grid Margin="16">
         <Grid.RowDefinitions>
-            <RowDefinition Height="60"/>
+            <RowDefinition Height="40"/>
             <RowDefinition Height="60"/>
             <RowDefinition Height="40"/>
-            <RowDefinition Height="auto"/>
+            <RowDefinition Height="40"/>
+            <RowDefinition Height="*"/>
         </Grid.RowDefinitions>
         <Grid.ColumnDefinitions>
             <ColumnDefinition Width="Auto"/>
@@ -27,12 +28,15 @@
         <TextBox FontSize="20" x:Name="Username" Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2"/>
 
         <TextBlock FontSize="20" Grid.Row="1" Grid.Column="0" Margin="0 16 8 0">Password:</TextBlock>
-        <PasswordBox Margin="0 16 0 -8" FontSize="20" x:Name="Password" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2"/>
+        <PasswordBox Margin="0 16 0 8" FontSize="20" x:Name="Password" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2"/>
 
-        <Button Margin="8 8 0 0" x:Name="LoginButton" Grid.Row="2" Grid.Column="3" Click="LoginButton_OnClick">
+        <TextBlock FontSize="20" Grid.Row="2" Grid.Column="0" Margin="0 16 8 0" x:Name="MFAText">MFA Key:</TextBlock>
+        <TextBox FontSize="20" x:Name="MFA" Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2"/>
+
+        <Button Margin="8 8 0 0" x:Name="LoginButton" Grid.Row="3" Grid.Column="3" Click="LoginButton_OnClick">
             <TextBlock FontSize="14">Login</TextBlock>
         </Button>
 
-        <TextBlock x:Name="Message" FontSize="20" Margin="0 16 0 0" Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3"/>
+        <TextBlock x:Name="Message" FontSize="20" Margin="0 16 0 0" Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="3" TextWrapping="Wrap"/>
     </Grid>
 </rxui:ReactiveUserControl>
diff --git a/Wabbajack/Views/Settings/CredentialsLoginView.xaml.cs b/Wabbajack/Views/Settings/CredentialsLoginView.xaml.cs
index efc1951c..f369c2bf 100644
--- a/Wabbajack/Views/Settings/CredentialsLoginView.xaml.cs
+++ b/Wabbajack/Views/Settings/CredentialsLoginView.xaml.cs
@@ -24,6 +24,12 @@ namespace Wabbajack
                     .DisposeWith(disposable);
                 this.Bind(ViewModel, x => x.LoginEnabled, x => x.LoginButton.IsEnabled)
                     .DisposeWith(disposable);
+                this.Bind(ViewModel, x => x.MFAKey, x => x.MFA.Text)
+                    .DisposeWith(disposable);
+                this.OneWayBind(ViewModel, x => x.MFAVisible, x => x.MFA.Visibility)
+                    .DisposeWith(disposable);
+                this.OneWayBind(ViewModel, x => x.MFAVisible, x => x.MFAText.Visibility)
+                    .DisposeWith(disposable);
                 this.OneWayBind(ViewModel, x => x.ReturnMessage.Message, x => x.Message.Text)
                     .DisposeWith(disposable);
             });