using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Net.Http;
using System.Net.Http.Headers;
using Newtonsoft.Json.Linq;
namespace AyaNovaQBI
{
internal class util
{
#region API stuff
public const string TEST_ROUTE = "notify/hello";
public const string API_BASE_ROUTE = "api/v8/";
private const int MAX_TRIES = 3;//max times to retry an api call before giving up
private const int API_RETRY_DELAY = 3000;//pause in ms before retrying api call
public static int HTTPCLIENT_TIMEOUT_SECONDS = 100;//changed by the setting in ops anyway, just a in-case sensible default here
public static HttpClient client = null;
//url once known to be good
internal static string ApiBaseUrl { get; set; }
//auth processes url for api and this is the best guess as to the client url to use for notification / help links etc
internal static string GuessClientUrl { get; set; }
internal static string JWT { get; set; }
// internal static long AyaNovaUserId { get; set; } //probably don't need this, if I do then some code will need to be added to decode the JWT or at the server to get my currently logged in USER ID
internal static string AyaNovaUserName { get; set; }
internal static AuthorizationRoles AyaNovaUserRoles { get; set; }
internal static UserType AyaNovaUserType { get; set; }
internal static AyaNovaLicense ALicense { get; set; } = null;
public static void InitClient()
{
if (client != null)
{
client.Dispose();
client = null;
}
client = new HttpClient();
client.Timeout = TimeSpan.FromSeconds(HTTPCLIENT_TIMEOUT_SECONDS);
}
///
/// Only a return value of "OK" is ok
///
///
///
public static async Task InitAndConfirmAddressAsync(string serverUrl)
{
ApiBaseUrl = serverUrl;
InitClient();
try
{
// TimeSpan tsDefault = client.Timeout;
// client.Timeout = new TimeSpan(0, 0, 20);
HttpResponseMessage response = await client.GetAsync(serverUrl + TEST_ROUTE);
// client.Timeout = tsDefault;
if (response.IsSuccessStatusCode)
return "OK";
else
return "Failed: " + response.StatusCode.ToString();
}
catch (Exception ex)
{
while (ex.InnerException != null)
ex = ex.InnerException;
return "Failed exception: \r\n" + ex.Message;
}
}
public async static Task AuthenticateAsync(string login, string password = null)
{
InitClient();
if (password == null)
password = login;
dynamic creds = new JObject();
creds.login = login;
creds.password = password;
var requestMessage = new HttpRequestMessage(HttpMethod.Post, ApiBaseUrl + "auth");
requestMessage.Content = new StringContent(creds.ToString(), System.Text.Encoding.UTF8, "application/json");
HttpResponseMessage response;
try
{
response = await client.SendAsync(requestMessage);
}
catch (HttpRequestException ex)
{
var Err = ex.Message;
var InnerErr = "";
if (ex.InnerException != null)
InnerErr = ex.InnerException.Message;
throw new Exception("Authentication error, route: AUTH\r\nError:" + Err + "\r\nInner error:" + InnerErr);
}
var a = new ApiResponse() { HttpResponse = response, ObjectResponse = Parse(await response.Content.ReadAsStringAsync()) };
if (response.IsSuccessStatusCode)
{
bool tfa = a.ObjectResponse["data"]["tfa"].Value();
if (tfa == true)
{
//Get temp token from response
var tempToken = a.ObjectResponse["data"]["tt"].Value();
//get 2fa code and send it in
do
{
tfa t = new tfa();
if (t.ShowDialog() == System.Windows.Forms.DialogResult.Cancel) return false;
string tfaPin = t.TFAPin;
dynamic tfaCreds = new JObject();
tfaCreds.pin = tfaPin;
tfaCreds.tempToken = tempToken;
try
{
var tfaResponse = await TryPostAsync("auth/tfa-authenticate", tfaCreds.ToString(Newtonsoft.Json.Formatting.None));//trypost is no delay
if (ProcessLoginResponse(tfaResponse)) return true;
}
catch (Exception ex)
{
if (!ex.Message.Contains("2003"))//if not an authentication error (bad pin) then throw it back up for display
throw ex;
//otherwise eat it and let them re-enter the pin again to mirror how ayanova web client works
}
} while (true);
}
else
{
return ProcessLoginResponse(a);
}
}
else
{
if (a.ObjectResponse != null && a.ObjectResponse.ContainsKey("error"))
{
var errCode = a.ObjectResponse["error"]["code"].Value();
if (errCode.Contains("2003")) return false;//simple authentication error
//some other error, possibly expired ayanova license etc, show it so it's clear why the login failed so they known it's not a creds issue
var errMessage = a.ObjectResponse["error"]["message"].Value();
throw new Exception($"Code: {errCode} - {errMessage}");
}
return false;
}
}
private static bool ProcessLoginResponse(ApiResponse a)
{
if (a.ObjectResponse == null) return false;
if (!a.HttpResponse.IsSuccessStatusCode)
{
return false;
}
if (a.ObjectResponse["data"]["l"].Value())//license lockout
{
throw new Exception("Server login from QBI is disabled due to AyaNova license issue");
}
JWT = a.ObjectResponse["data"]["token"].Value();
AyaNovaUserName = a.ObjectResponse["data"]["name"].Value();
AyaNovaUserRoles = (AuthorizationRoles)(int.Parse(a.ObjectResponse["data"]["roles"].Value()));
AyaNovaUserType = (UserType)(int.Parse(a.ObjectResponse["data"]["usertype"].Value()));
return true;
}
public async static Task GetAsync(string route)
{
Exception FirstException = null;
for (int x = 0; x < MAX_TRIES; x++)
{
try
{
return await TryGetAsync(route);
}
catch (Exception ex)
{
if (FirstException == null)
FirstException = ex;
}
await Task.Delay(API_RETRY_DELAY);
}
//no luck re-throw the exception
throw new Exception("API call failed after " + MAX_TRIES.ToString() + " attempts", FirstException);
}
private async static Task TryGetAsync(string route)
{
var requestMessage = new HttpRequestMessage(HttpMethod.Get, ApiBaseUrl + route);
if (!string.IsNullOrWhiteSpace(JWT))
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", JWT);
HttpResponseMessage response = null;
try
{
response = await client.SendAsync(requestMessage);
}
catch (HttpRequestException ex)
{
var Err = ex.Message;
var InnerErr = "";
if (ex.InnerException != null)
InnerErr = ex.InnerException.Message;
throw new Exception("GET error, route: " + route + "\r\nError:" + Err + "\r\nInner error:" + InnerErr + "\r\nStack:" + ex.StackTrace);
}
var responseAsString = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
throw new Exception("GET error, code: " + (int)response.StatusCode + ", route: " + route + "\r\n" + responseAsString + "\r\n" + response.ReasonPhrase);
}
else
return new ApiResponse() { HttpResponse = response, ObjectResponse = Parse(responseAsString) };
}
public async static Task PostAsync(string route, dynamic d)
{
Exception FirstException = null;
for (int x = 0; x < MAX_TRIES; x++)
{
try
{
return await TryPostAsync(route, d.ToString(Newtonsoft.Json.Formatting.None));
}
catch (Exception ex)
{
if (FirstException == null)
FirstException = ex;
}
await Task.Delay(API_RETRY_DELAY);
}
//no luck re-throw the exception
throw new Exception("API call failed after " + MAX_TRIES.ToString() + " attempts", FirstException);
}
public async static Task PostAsync(string route, string s = null)
{
Exception FirstException = null;
for (int x = 0; x < MAX_TRIES; x++)
{
try
{
return await TryPostAsync(route, s);
}
catch (Exception ex)
{
if (FirstException == null)
FirstException = ex;
}
await Task.Delay(API_RETRY_DELAY);
}
//no luck re-throw the exception
throw new Exception("API call failed after " + MAX_TRIES.ToString() + " attempts", FirstException);
}
internal async static Task TryPostAsync(string route, string postJson = null)
{
var requestMessage = new HttpRequestMessage(HttpMethod.Post, ApiBaseUrl + route);
if (!string.IsNullOrWhiteSpace(JWT))
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", JWT);
if (!string.IsNullOrWhiteSpace(postJson))
requestMessage.Content = new StringContent(postJson, System.Text.Encoding.UTF8, "application/json");
HttpResponseMessage response = null;
try
{
response = await client.SendAsync(requestMessage);
}
catch (HttpRequestException ex)
{
var Err = ex.Message;
var InnerErr = "";
if (ex.InnerException != null)
InnerErr = ex.InnerException.Message;
throw new Exception("POST error, route: " + route + "\r\nError:" + Err + "\r\nInner error:" + InnerErr + "\r\nStack:" + ex.StackTrace + "\r\nPOSTED OBJECT:\r\n" + postJson);
}
var responseAsString = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
if (string.IsNullOrWhiteSpace(postJson))
postJson = "n/a";
throw new Exception("POST error, code: " + (int)response.StatusCode + ", route: " + route + "\r\n" + responseAsString + "\r\n" + response.ReasonPhrase + "\r\nPOSTED OBJECT:\r\n" + postJson);
}
else
return new ApiResponse() { HttpResponse = response, ObjectResponse = Parse(responseAsString) };
}
///
///
///
///
///
public static JObject Parse(string jsonString)
{
if (string.IsNullOrWhiteSpace(jsonString))
{
return null;
}
return JObject.Parse(jsonString);
}
public static long IdFromResponse(ApiResponse a)
{
return a.ObjectResponse["data"]["id"].Value();
}
public static uint CTokenFromResponse(ApiResponse a)
{
return a.ObjectResponse["data"]["concurrency"].Value();
}
public async static Task PutAsync(string route, dynamic d)
{
Exception FirstException = null;
for (int x = 0; x < MAX_TRIES; x++)
{
try
{
return await TryPutAsync(route, d.ToString(Newtonsoft.Json.Formatting.None));
}
catch (Exception ex)
{
if (FirstException == null)
FirstException = ex;
}
await Task.Delay(API_RETRY_DELAY);
}
//no luck re-throw the exception
throw new Exception("API call failed after " + MAX_TRIES.ToString() + " attempts", FirstException);
}
public async static Task PutAsync(string route)
{
Exception FirstException = null;
for (int x = 0; x < MAX_TRIES; x++)
{
try
{
return await TryPutAsync(route);
}
catch (Exception ex)
{
if (FirstException == null)
FirstException = ex;
}
await Task.Delay(API_RETRY_DELAY);
}
//no luck re-throw the exception
throw new Exception("API call failed after " + MAX_TRIES.ToString() + " attempts", FirstException);
}
public async static Task TryPutAsync(string route, string putJson = null)
{
var requestMessage = new HttpRequestMessage(HttpMethod.Put, ApiBaseUrl + route);
if (!string.IsNullOrWhiteSpace(JWT))
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", JWT);
if (!string.IsNullOrWhiteSpace(putJson))
requestMessage.Content = new StringContent(putJson, System.Text.Encoding.UTF8, "application/json");
HttpResponseMessage response = null;
try
{
response = await client.SendAsync(requestMessage);
}
catch (HttpRequestException ex)
{
var Err = ex.Message;
var InnerErr = "";
if (ex.InnerException != null)
InnerErr = ex.InnerException.Message;
throw new Exception("PUT error, route: " + route + "\r\nError:" + Err + "\r\nInner error:" + InnerErr + "\r\nStack:" + ex.StackTrace + "\r\nPOSTED OBJECT:\r\n" + putJson);
}
var responseAsString = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
if (string.IsNullOrWhiteSpace(putJson))
putJson = "n/a";
throw new Exception("PUT error, code: " + (int)response.StatusCode + ", route: " + route + "\r\n" + responseAsString + "\r\n" + response.ReasonPhrase + "\r\nPUT OBJECT:\r\n" + putJson);
}
else
return new ApiResponse() { HttpResponse = response, ObjectResponse = Parse(responseAsString) };
}
public class ApiResponse
{
public HttpResponseMessage HttpResponse { get; set; }
public JObject ObjectResponse { get; set; }
public string CompactResponse
{
get
{
return ObjectResponse.ToString(Newtonsoft.Json.Formatting.None);
}
}
}
#endregion
#region QB STUFF
public static List GetInvoiceableItems()
{
var random = new Random();
var l = new List();
for (int i = 1; i < random.Next(25, 100); i++)
l.Add(new InvoiceableItem { Customer = $"Customer {random.Next(1, 5)}", Linked = random.Next(2) == 1, Project = $"project {i}", ServiceDate = DateTime.Now.ToString("g"), ServiceNumber = (40 + i).ToString(), Status = $"Waiting to be invoiced", StatusColor = "FF00FFAA", WorkorderId = 4 });
return l.OrderBy(x => x.Customer)
.ThenBy(x => x.ServiceNumber)
.ThenBy(x => x.ServiceDate)
.ToList();
}
public static async Task InitializeQBI(StringBuilder initErrors)
{
//COPY most of this code from qbi v7 becuase it has a lot of edge cases in it and it's complex and thorough, but break it into abstracted bits so can be replicated in other accounting add-on's more easily
//This is pre-pfc block of stuff that doesn't map well from v7 qbi plugin to here so replicate it in spirit here but not much is copyable just the concepts
//LOGIN to v8 first
auth d = new auth();
if (d.ShowDialog() == System.Windows.Forms.DialogResult.Cancel)
return false;
//Get license
var r = await GetAsync("license");
ALicense = r.ObjectResponse["data"]["license"].ToObject();
//UNEXPIRED AYANOVA LICENSE?
if (ALicense.licenseWillExpire && ALicense.licenseExpiration < DateTime.UtcNow)
{
initErrors.AppendLine($"AyaNova license has expired {ALicense.licenseExpiration.ToLocalTime().ToString("g")}");
return false;
}
//QBI LICENSED?
if (ALicense.features.FirstOrDefault(z => z.Feature == "QBI") == null)
{
initErrors.AppendLine("QBI not licensed");
return false;
}
//check if build date is within licensed date (how did I do that automated build date thing?)
// copy timestamp.cs and the createtimestamp.exe utility from v7 qbi and create build event to run it here and do same thing
//check that AyaNova version matches required minimum for this QBI (
//PFC - Util.QBValidate stuff:
//Validate QB connection can be made and open connection and start session with QB (see Util.QBValidate in v7
//once connected collect the country, version we are dealing with (Util.qbValidate)
//confirm qb is 2008 or newer and bail if not (util.qbvalidate)
//cache company name and other qb info
//PFC - PopulateQBListCache()
//PFC - PopulateAyaListCache()
//PFC - integration object check (fetch or create if not present) (Util.integrationObjectCheck())
//NOTE: probably can make a class for this and use newtonsoft to convert dynamic returned from api call to class object?
//PFC - Validate settings, create if necessary (Util.ValidateSettings()) and save
//PFC - verify integration mapped objects still exist at each end (Util.PreFlightCheck() line 199)
//DONE
return true;
}
#endregion
}
}