MYOBPosterEngine.cs 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. using InABox.Core;
  2. using InABox.Core.Postable;
  3. using InABox.DynamicGrid;
  4. using InABox.Poster.Shared;
  5. using Microsoft.Web.WebView2.Wpf;
  6. using MYOB.AccountRight.SDK;
  7. using MYOB.AccountRight.SDK.Contracts;
  8. using MYOB.AccountRight.SDK.Services;
  9. using System;
  10. using System.Collections.Generic;
  11. using System.Diagnostics.CodeAnalysis;
  12. using System.Linq;
  13. using System.Text;
  14. using System.Text.RegularExpressions;
  15. using System.Threading.Tasks;
  16. using System.Web;
  17. using System.Windows;
  18. using System.Windows.Controls;
  19. using System.Windows.Threading;
  20. namespace InABox.Poster.MYOB;
  21. public class MYOBConnectionData(ApiConfiguration configuration, CompanyFileService cfService, IOAuthKeyService authKey)
  22. {
  23. public ApiConfiguration Configuration { get; set; } = configuration;
  24. public CompanyFileService CompanyFileService { get; set; } = cfService;
  25. public CompanyFile? CompanyFile { get; set; }
  26. public CompanyFileCredentials? CompanyFileCredentials { get; set; }
  27. public CompanyFileWithResources? ActiveCompanyFile { get; set; }
  28. public IOAuthKeyService AuthKey { get; set; } = authKey;
  29. }
  30. public static partial class MYOBPosterEngine
  31. {
  32. internal static readonly string DEV_KEY = "f3b27f88-2ef9-4d8e-95c1-d0a045d0afee";
  33. internal static readonly string SECRET_KEY = "ksm0e8yo6oumcPb63A8cduaN";
  34. internal const string AUTH_URL = "https://secure.myob.com/oauth2/account/authorize";
  35. internal const string AUTH_SCOPE = "CompanyFile";
  36. internal const string REDIRECT_URL = "http://desktop";
  37. private static MYOBConnectionData? _connectionData;
  38. public static bool GetAuthorisationCode(IApiConfiguration config, out string? code)
  39. {
  40. var url = $"{AUTH_URL}?client_id={config.ClientId}&redirect_uri={HttpUtility.UrlEncode(config.RedirectUrl)}&scope={AUTH_SCOPE}&response_type=code";
  41. var window = new Window
  42. {
  43. Width = 800,
  44. Height = 600,
  45. Title = "Sign in to MYOB"
  46. };
  47. string? resultCode = null;
  48. var view = new WebView2
  49. {
  50. };
  51. view.NavigationCompleted += (o, e) =>
  52. {
  53. view.ExecuteScriptAsync("document.documentElement.innerHTML;").ContinueWith(task =>
  54. {
  55. if (task.Result is null) return;
  56. var str = Serialization.Deserialize<string>(task.Result);
  57. if (str is null) return;
  58. var match = CodeRegex().Match(str);
  59. if (!match.Success) return;
  60. resultCode = match.Groups[1].Value;
  61. window.Dispatcher.BeginInvoke(() =>
  62. {
  63. if(window.DialogResult is null)
  64. {
  65. window.DialogResult = true;
  66. window.Close();
  67. }
  68. });
  69. });
  70. };
  71. view.Source = new Uri(url);
  72. window.Content = view;
  73. if(window.ShowDialog() == true)
  74. {
  75. code = resultCode;
  76. return true;
  77. }
  78. else
  79. {
  80. code = null;
  81. return false;
  82. }
  83. }
  84. public static MYOBConnectionData? GetConnectionDataOrNull()
  85. {
  86. return _connectionData;
  87. }
  88. public static MYOBConnectionData GetConnectionData(IPosterDispatcher dispatcher)
  89. {
  90. if(_connectionData is MYOBConnectionData data)
  91. {
  92. return _connectionData;
  93. }
  94. var configuration = new ApiConfiguration(DEV_KEY, SECRET_KEY, REDIRECT_URL);
  95. var authService = new OAuthService(configuration);
  96. string? code = null;
  97. if(!dispatcher.Execute(() => GetAuthorisationCode(configuration, out code)))
  98. {
  99. throw new PostCancelledException();
  100. }
  101. if(code is null)
  102. {
  103. throw new PostFailedMessageException("No authorisation code was received.");
  104. }
  105. var keystore = new SimpleOAuthKeyService
  106. {
  107. OAuthResponse = authService.GetTokens(code)
  108. };
  109. var cfService = new CompanyFileService(configuration, null, keystore);
  110. _connectionData = new MYOBConnectionData(configuration, cfService, keystore);
  111. return _connectionData;
  112. }
  113. [GeneratedRegex("code=(.*)<")]
  114. private static partial Regex CodeRegex();
  115. }
  116. public abstract class MYOBPosterEngine<TPostable, TPoster, TSettings> :
  117. BasePosterEngine<TPostable, TPoster, TSettings>,
  118. IGlobalSettingsPosterEngine<TPoster, MYOBGlobalPosterSettings>
  119. where TPostable : Entity, IPostable, IRemotable, IPersistent, new()
  120. where TSettings : MYOBPosterSettings, new()
  121. where TPoster : class, IMYOBPoster<TPostable, TSettings>
  122. {
  123. private MYOBGlobalPosterSettings GetGlobalSettings() =>
  124. (this as IGlobalSettingsPosterEngine<TPoster, MYOBGlobalPosterSettings>).GetGlobalSettings();
  125. private void SaveGlobalSettings(MYOBGlobalPosterSettings settings) =>
  126. (this as IGlobalSettingsPosterEngine<TPoster, MYOBGlobalPosterSettings>).SaveGlobalSettings(settings);
  127. protected override TPoster CreatePoster()
  128. {
  129. var poster = base.CreatePoster();
  130. poster.Script = GetScriptDocument();
  131. return poster;
  132. }
  133. public override bool BeforePost(IDataModel<TPostable> model)
  134. {
  135. return Poster.BeforePost(model);
  136. }
  137. protected void LoadConnectionData()
  138. {
  139. var data = MYOBPosterEngine.GetConnectionData(Dispatcher);
  140. var globalSettings = GetGlobalSettings();
  141. if(data.CompanyFile is null || data.CompanyFile.Id != globalSettings.CompanyFile.ID)
  142. {
  143. CompanyFile? file;
  144. if(globalSettings.CompanyFile.ID == Guid.Empty)
  145. {
  146. file = MYOBCompanyFileSelectionDialog.SelectCompanyFile(Dispatcher);
  147. if(file is null)
  148. {
  149. throw new PostCancelledException();
  150. }
  151. else
  152. {
  153. globalSettings.CompanyFile.ID = file.Id;
  154. globalSettings.CompanyFile.Name = file.Name;
  155. SaveGlobalSettings(globalSettings);
  156. globalSettings.CommitChanges();
  157. }
  158. }
  159. if(!globalSettings.NoCredentials && globalSettings.CompanyFileUserID.IsNullOrWhiteSpace())
  160. {
  161. var credentials = new MYOBCompanyFileCredentials
  162. {
  163. UserID = globalSettings.CompanyFileUserID,
  164. Password = globalSettings.CompanyFilePassword,
  165. NoCredentials = globalSettings.NoCredentials
  166. };
  167. if (Dispatcher.Execute(() => DynamicGridUtils.EditObject(credentials, customiseGrid: grid =>
  168. {
  169. grid.OnValidate += (grid, items, errors) =>
  170. {
  171. var item = items.FirstOrDefault();
  172. if (item is null) return;
  173. if(!item.NoCredentials && item.UserID.IsNullOrWhiteSpace())
  174. {
  175. errors.Add("[UserID] cannot be blank");
  176. }
  177. };
  178. })))
  179. {
  180. globalSettings.NoCredentials = credentials.NoCredentials;
  181. globalSettings.CompanyFileUserID = credentials.UserID;
  182. globalSettings.CompanyFilePassword = credentials.Password;
  183. SaveGlobalSettings(globalSettings);
  184. globalSettings.CommitChanges();
  185. }
  186. else
  187. {
  188. throw new PostCancelledException();
  189. }
  190. }
  191. var companyFile = data.CompanyFileService.GetRange().FirstOrDefault(x => x.Id == globalSettings.CompanyFile.ID);
  192. var fileCredentials = new CompanyFileCredentials(globalSettings.CompanyFileUserID, globalSettings.CompanyFilePassword);
  193. data.CompanyFile = companyFile;
  194. data.CompanyFileCredentials = fileCredentials;
  195. // data.ActiveCompanyFile = data.CompanyFileService.Get(companyFile, fileCredentials);
  196. }
  197. Poster.ConnectionData = data;
  198. }
  199. protected override IPostResult<TPostable> DoProcess(IDataModel<TPostable> model)
  200. {
  201. LoadConnectionData();
  202. return Poster.Process(model);
  203. }
  204. public override void AfterPost(IDataModel<TPostable> model, IPostResult<TPostable> result)
  205. {
  206. }
  207. }