|
|
@@ -0,0 +1,195 @@
|
|
|
+using InABox.Core;
|
|
|
+using InABox.Core.Postable;
|
|
|
+using InABox.Poster.Shared;
|
|
|
+using Microsoft.Web.WebView2.Wpf;
|
|
|
+using System;
|
|
|
+using System.Collections.Generic;
|
|
|
+using System.Diagnostics.CodeAnalysis;
|
|
|
+using System.Linq;
|
|
|
+using System.Text;
|
|
|
+using System.Text.RegularExpressions;
|
|
|
+using System.Threading.Tasks;
|
|
|
+using System.Windows;
|
|
|
+using Xero.NetStandard.OAuth2.Api;
|
|
|
+using Xero.NetStandard.OAuth2.Client;
|
|
|
+using Xero.NetStandard.OAuth2.Config;
|
|
|
+using Xero.NetStandard.OAuth2.Token;
|
|
|
+
|
|
|
+namespace InABox.Poster.Xero;
|
|
|
+
|
|
|
+public class XeroConnectionData(XeroConfiguration configuration, IXeroToken token, XeroClient client, Guid tenantID)
|
|
|
+{
|
|
|
+ public XeroConfiguration Configuration { get; set; } = configuration;
|
|
|
+
|
|
|
+ public IXeroToken Token { get; set; } = token;
|
|
|
+
|
|
|
+ public XeroClient Client { get; set; } = client;
|
|
|
+
|
|
|
+ public Guid TenantID { get; set; } = tenantID;
|
|
|
+}
|
|
|
+
|
|
|
+public static partial class XeroPosterEngine
|
|
|
+{
|
|
|
+ internal static readonly string CLIENT_ID = "E83E844138534229BBB0DC9291D02C94";
|
|
|
+ internal static readonly string CALLBACK_URI = "http://localhost:8888/callback";
|
|
|
+
|
|
|
+ private static XeroConnectionData? _connectionData;
|
|
|
+
|
|
|
+ public static XeroConnectionData? GetConnectionDataOrNull()
|
|
|
+ {
|
|
|
+ return _connectionData;
|
|
|
+ }
|
|
|
+
|
|
|
+ public static async Task<XeroConnectionData> GetConnectionData()
|
|
|
+ {
|
|
|
+ if(_connectionData is XeroConnectionData data)
|
|
|
+ {
|
|
|
+ return _connectionData;
|
|
|
+ }
|
|
|
+
|
|
|
+ var state = CreateState();
|
|
|
+
|
|
|
+ var xconfig = new XeroConfiguration();
|
|
|
+ xconfig.ClientId = CLIENT_ID;
|
|
|
+ xconfig.CallbackUri = new Uri(CALLBACK_URI);
|
|
|
+ xconfig.Scope = "accounting.attachments";
|
|
|
+ xconfig.State = state;
|
|
|
+
|
|
|
+ var client = new XeroClient(xconfig);
|
|
|
+
|
|
|
+ // generate a random codeVerifier
|
|
|
+ var validChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-._~";
|
|
|
+ var random = new Random();
|
|
|
+ int charsLength = random.Next(43, 128);
|
|
|
+ char[] randomChars = new char[charsLength];
|
|
|
+ for (int i = 0; i < charsLength; i++)
|
|
|
+ {
|
|
|
+ randomChars[i] = validChars[random.Next(0, validChars.Length)];
|
|
|
+ }
|
|
|
+ string codeVerifier = new string(randomChars);
|
|
|
+
|
|
|
+ var link = client.BuildLoginUriPkce(codeVerifier);
|
|
|
+ if (!GetCode(link, state, out var code))
|
|
|
+ {
|
|
|
+ throw new PostCancelledException();
|
|
|
+ }
|
|
|
+ // Check State
|
|
|
+
|
|
|
+ _connectionData = Task.Run<XeroConnectionData>(async () =>
|
|
|
+ {
|
|
|
+ var token = await client.RequestAccessTokenPkceAsync(code, codeVerifier).ConfigureAwait(false);
|
|
|
+
|
|
|
+ var tenants = await client.GetConnectionsAsync(token);
|
|
|
+
|
|
|
+ if(tenants.Count == 0)
|
|
|
+ {
|
|
|
+ Logger.Send(LogType.Error, "", "Xero Error: No tenant found.");
|
|
|
+ throw new PostFailedMessageException("Post failed when connecting to server.");
|
|
|
+ }
|
|
|
+
|
|
|
+ return new(xconfig, token, client, tenants[0].TenantId);
|
|
|
+ }).Result;
|
|
|
+
|
|
|
+ return _connectionData;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static string CreateState()
|
|
|
+ {
|
|
|
+ // generate a random codeVerifier
|
|
|
+ var validChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
|
+ var random = new Random();
|
|
|
+ int charsLength = random.Next(43, 128);
|
|
|
+ char[] randomChars = new char[charsLength];
|
|
|
+ for (int i = 0; i < charsLength; i++)
|
|
|
+ {
|
|
|
+ randomChars[i] = validChars[random.Next(0, validChars.Length)];
|
|
|
+ }
|
|
|
+ return new string(randomChars);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static bool GetCode(string link, string state, [NotNullWhen(true)] out string? code)
|
|
|
+ {
|
|
|
+ var url = link;
|
|
|
+
|
|
|
+ var window = new Window
|
|
|
+ {
|
|
|
+ Width = 800,
|
|
|
+ Height = 600,
|
|
|
+ Title = "Sign in to Xero"
|
|
|
+ };
|
|
|
+
|
|
|
+ string? resultCode = null;
|
|
|
+ string? resultState = null;
|
|
|
+
|
|
|
+ var view = new WebView2
|
|
|
+ {
|
|
|
+ };
|
|
|
+ view.NavigationStarting += (o, e) =>
|
|
|
+ {
|
|
|
+ var codeMatch = CodeRegex().Match(e.Uri);
|
|
|
+ if (!codeMatch.Success) return;
|
|
|
+
|
|
|
+ var stateMatch = StateRegex().Match(e.Uri);
|
|
|
+ if (!stateMatch.Success) return;
|
|
|
+
|
|
|
+ resultCode = codeMatch.Groups[1].Value;
|
|
|
+ resultState = stateMatch.Groups[1].Value;
|
|
|
+ window.Dispatcher.BeginInvoke(() =>
|
|
|
+ {
|
|
|
+ if (window.DialogResult is null)
|
|
|
+ {
|
|
|
+ window.DialogResult = true;
|
|
|
+ window.Close();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ };
|
|
|
+ view.Source = new Uri(url);
|
|
|
+ window.Content = view;
|
|
|
+
|
|
|
+ var result = window.ShowDialog() == true;
|
|
|
+
|
|
|
+ if(resultState != state)
|
|
|
+ {
|
|
|
+ Logger.Send(LogType.Error, "", "State did not match for connecting to Xero");
|
|
|
+ throw new PostException("Error connecting to server.");
|
|
|
+ }
|
|
|
+ code = resultCode;
|
|
|
+
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ [GeneratedRegex("state=([A-Za-z]+)")]
|
|
|
+ private static partial Regex StateRegex();
|
|
|
+
|
|
|
+ [GeneratedRegex("code=([A-Za-z0-9\\-._~]+)")]
|
|
|
+ private static partial Regex CodeRegex();
|
|
|
+}
|
|
|
+
|
|
|
+public class XeroPosterEngine<TPostable, TSettings> : BasePosterEngine<TPostable, IXeroPoster<TPostable, TSettings>, TSettings>
|
|
|
+ where TPostable : Entity, IPostable, IRemotable, IPersistent, new()
|
|
|
+ where TSettings : XeroPosterSettings, new()
|
|
|
+{
|
|
|
+
|
|
|
+ protected override IXeroPoster<TPostable, TSettings> CreatePoster()
|
|
|
+ {
|
|
|
+ var poster = base.CreatePoster();
|
|
|
+ poster.Script = GetScriptDocument();
|
|
|
+ return poster;
|
|
|
+ }
|
|
|
+
|
|
|
+ public override bool BeforePost(IDataModel<TPostable> model)
|
|
|
+ {
|
|
|
+ return Poster.BeforePost(model);
|
|
|
+ }
|
|
|
+
|
|
|
+ protected override IPostResult<TPostable> DoProcess(IDataModel<TPostable> model)
|
|
|
+ {
|
|
|
+ Poster.ConnectionData = XeroPosterEngine.GetConnectionData().Result;
|
|
|
+ return Poster.Process(model);
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ public override void AfterPost(IDataModel<TPostable> model, IPostResult<TPostable> result)
|
|
|
+ {
|
|
|
+ }
|
|
|
+}
|