mirror of
https://github.com/q39JzrRa/GM-Vehicle-API.git
synced 2025-12-23 23:38:45 -05:00
Code cleanup:
- folders in api project - changing json model object property names to be .NET style - etc Implemented wrapper HttpClient methods that handle token refreshing and first pass at transient error retries / throwing non-transient errors
This commit is contained in:
@@ -1,166 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
|
||||
namespace GM.Api
|
||||
{
|
||||
|
||||
|
||||
|
||||
public class DiagnosticReader
|
||||
{
|
||||
|
||||
IEnumerable<Diagnosticresponse> _dr;
|
||||
|
||||
public DiagnosticReader(IEnumerable<Diagnosticresponse> elements)
|
||||
{
|
||||
_dr = elements;
|
||||
}
|
||||
|
||||
public float AmbientAirTempCelcius => float.Parse((from f in _dr
|
||||
where f.name == "AMBIENT AIR TEMPERATURE"
|
||||
from r in f.diagnosticElement
|
||||
where r.name == "AMBIENT AIR TEMPERATURE"
|
||||
select r.value).FirstOrDefault());
|
||||
public string ChargerPowerLevel => (from f in _dr
|
||||
where f.name == "CHARGER POWER LEVEL"
|
||||
from r in f.diagnosticElement
|
||||
where r.name == "CHARGER POWER LEVEL"
|
||||
select r.value).FirstOrDefault();
|
||||
|
||||
public float EvBatteryLevelPercent => float.Parse((from f in _dr
|
||||
where f.name == "EV BATTERY LEVEL"
|
||||
from r in f.diagnosticElement
|
||||
where r.name == "EV BATTERY LEVEL"
|
||||
select r.value).FirstOrDefault());
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
public static class DiagnosticHelper
|
||||
{
|
||||
public static float GetElectricEconomyKwh(this IEnumerable<Diagnosticresponse> elements)
|
||||
{
|
||||
var itm = float.Parse((from f in elements
|
||||
where f.name == "ENERGY EFFICIENCY"
|
||||
from r in f.diagnosticElement
|
||||
where r.name == "ELECTRIC ECONOMY"
|
||||
select r.value).FirstOrDefault());
|
||||
|
||||
return itm;
|
||||
}
|
||||
|
||||
public static float GetLifetimeEfficiencyKwh(this IEnumerable<Diagnosticresponse> elements)
|
||||
{
|
||||
var itm = float.Parse((from f in elements
|
||||
where f.name == "ENERGY EFFICIENCY"
|
||||
from r in f.diagnosticElement
|
||||
where r.name == "LIFETIME EFFICIENCY"
|
||||
select r.value).FirstOrDefault());
|
||||
|
||||
return itm;
|
||||
}
|
||||
|
||||
public static float GetLifetimeMpgE(this IEnumerable<Diagnosticresponse> elements)
|
||||
{
|
||||
var itm = float.Parse((from f in elements
|
||||
where f.name == "ENERGY EFFICIENCY"
|
||||
from r in f.diagnosticElement
|
||||
where r.name == "LIFETIME MPGE"
|
||||
select r.value).FirstOrDefault());
|
||||
|
||||
return itm;
|
||||
}
|
||||
|
||||
public static float GetOdometerKm(this IEnumerable<Diagnosticresponse> elements)
|
||||
{
|
||||
var itm = float.Parse((from f in elements
|
||||
where f.name == "ENERGY EFFICIENCY"
|
||||
from r in f.diagnosticElement
|
||||
where r.name == "ODOMETER"
|
||||
select r.value).FirstOrDefault());
|
||||
|
||||
return itm;
|
||||
}
|
||||
|
||||
public static float GetEvBatteryLevelPercent(this IEnumerable<Diagnosticresponse> elements)
|
||||
{
|
||||
var itm = float.Parse((from f in elements
|
||||
where f.name == "EV BATTERY LEVEL"
|
||||
from r in f.diagnosticElement
|
||||
where r.name == "EV BATTERY LEVEL"
|
||||
select r.value).FirstOrDefault());
|
||||
|
||||
return itm;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
public class DiagnosticRequestRoot
|
||||
{
|
||||
public static readonly string[] DefaultItems = new string[]
|
||||
{
|
||||
"ENGINE COOLANT TEMP",
|
||||
"ENGINE RPM",
|
||||
"HV BATTERY ESTIMATED CAPACITY",
|
||||
"LAST TRIP FUEL ECONOMY",
|
||||
"ENERGY EFFICIENCY",
|
||||
"HYBRID BATTERY MINIMUM TEMPERATURE",
|
||||
"EV ESTIMATED CHARGE END",
|
||||
"EV BATTERY LEVEL",
|
||||
"EV PLUG VOLTAGE",
|
||||
"ODOMETER",
|
||||
"CHARGER POWER LEVEL",
|
||||
"LIFETIME EV ODOMETER",
|
||||
"EV PLUG STATE",
|
||||
"EV CHARGE STATE",
|
||||
"TIRE PRESSURE",
|
||||
"AMBIENT AIR TEMPERATURE",
|
||||
"LAST TRIP DISTANCE",
|
||||
"INTERM VOLT BATT VOLT",
|
||||
"GET COMMUTE SCHEDULE",
|
||||
"GET CHARGE MODE",
|
||||
"EV SCHEDULED CHARGE START",
|
||||
"VEHICLE RANGE"
|
||||
};
|
||||
|
||||
|
||||
//public Diagnosticsrequest diagnosticsRequest { get; set; }
|
||||
}
|
||||
|
||||
//public class Diagnosticsrequest
|
||||
//{
|
||||
// public string[] diagnosticItem { get; set; }
|
||||
//}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public class ResponseBody
|
||||
{
|
||||
public Diagnosticresponse[] diagnosticResponse { get; set; }
|
||||
}
|
||||
|
||||
public class Diagnosticresponse
|
||||
{
|
||||
public string name { get; set; }
|
||||
public Diagnosticelement[] diagnosticElement { get; set; }
|
||||
}
|
||||
|
||||
public class Diagnosticelement
|
||||
{
|
||||
public string name { get; set; }
|
||||
public string status { get; set; }
|
||||
public string message { get; set; }
|
||||
public string value { get; set; }
|
||||
public string unit { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using JWT;
|
||||
using GM.Api.Models;
|
||||
using GM.Api.Tokens;
|
||||
using JWT;
|
||||
using JWT.Algorithms;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@@ -13,9 +15,9 @@ namespace GM.Api
|
||||
{
|
||||
public class GMClient
|
||||
{
|
||||
//TODO: maybe throw exceptions?
|
||||
//TODO: all calls need to catch a 401, attempt refresh, try one more time and fail on second 401
|
||||
//TODO: all calls need to catch transient exceptions and retry a certain number of times
|
||||
public static int RetryCount { get; set; } = 3;
|
||||
|
||||
//TODO: consistent exception throwing
|
||||
|
||||
string _clientId;
|
||||
string _deviceId;
|
||||
@@ -70,23 +72,96 @@ namespace GM.Api
|
||||
}
|
||||
|
||||
|
||||
async Task<Commandresponse> VehicleConnect(string vin)
|
||||
async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, bool noAuth = false)
|
||||
{
|
||||
if (LoginData == null) throw new InvalidOperationException("Login required");
|
||||
if (LoginData.IsExpired)
|
||||
if (!noAuth)
|
||||
{
|
||||
if (!await RefreshToken())
|
||||
if (LoginData == null)
|
||||
{
|
||||
throw new InvalidOperationException("Token refresh failed");
|
||||
throw new InvalidOperationException("Not Logged in");
|
||||
}
|
||||
if (LoginData.IsExpired)
|
||||
{
|
||||
var result = await RefreshToken();
|
||||
if (!result)
|
||||
{
|
||||
throw new InvalidOperationException("Token refresh failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
request.Headers.Authorization = null;
|
||||
}
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Post, $"{_apiUrl}/v1/account/vehicles/{vin}/commands/connect");
|
||||
req.Content = new StringContent("{}", Encoding.UTF8, "application/json");
|
||||
int attempt = 0;
|
||||
while (attempt < RetryCount)
|
||||
{
|
||||
attempt++;
|
||||
HttpResponseMessage resp = null;
|
||||
try
|
||||
{
|
||||
resp = await _client.SendAsync(request);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//todo: only catch transient errors
|
||||
//todo: log this
|
||||
continue;
|
||||
}
|
||||
|
||||
var response = await _client.SendAsync(req);
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized || resp.StatusCode == System.Net.HttpStatusCode.Forbidden)
|
||||
{
|
||||
var result = await RefreshToken();
|
||||
if (!result)
|
||||
{
|
||||
throw new InvalidOperationException("Token refresh failed");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
else if (resp.StatusCode == System.Net.HttpStatusCode.BadGateway || resp.StatusCode == System.Net.HttpStatusCode.Conflict || resp.StatusCode == System.Net.HttpStatusCode.GatewayTimeout || resp.StatusCode == System.Net.HttpStatusCode.InternalServerError || resp.StatusCode == System.Net.HttpStatusCode.RequestTimeout || resp.StatusCode == System.Net.HttpStatusCode.ResetContent || resp.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)
|
||||
{
|
||||
//possible transient errors
|
||||
//todo: log this
|
||||
await Task.Delay(500);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
var respMessage = (await resp.Content.ReadAsStringAsync())??"";
|
||||
throw new InvalidOperationException("Request error. StatusCode: " + resp.StatusCode.ToString() + ", msg: " + respMessage);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
//todo: include more info
|
||||
throw new InvalidOperationException("Request failed too many times");
|
||||
}
|
||||
|
||||
|
||||
|
||||
async Task<HttpResponseMessage> PostAsync(string requestUri, HttpContent content, bool noAuth = false)
|
||||
{
|
||||
return await SendAsync(new HttpRequestMessage(HttpMethod.Post, requestUri) { Content = content }, noAuth);
|
||||
}
|
||||
|
||||
async Task<HttpResponseMessage> GetAsync(string requestUri, bool noAuth = false)
|
||||
{
|
||||
return await SendAsync(new HttpRequestMessage(HttpMethod.Get, requestUri), noAuth);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
async Task<Commandresponse> VehicleConnect(string vin)
|
||||
{
|
||||
var response = await PostAsync($"{_apiUrl}/v1/account/vehicles/{vin}/commands/connect", new StringContent("{}", Encoding.UTF8, "application/json"));
|
||||
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
@@ -106,22 +181,19 @@ namespace GM.Api
|
||||
|
||||
async Task<bool> UpgradeToken(string pin)
|
||||
{
|
||||
var payload = new UpgradeTokenPayload()
|
||||
var payload = new LoginRequest()
|
||||
{
|
||||
client_id = _clientId,
|
||||
device_id = _deviceId,
|
||||
credential = pin,
|
||||
credential_type = "PIN",
|
||||
nonce = helpers.GenerateNonce(),
|
||||
timestamp = DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffK")
|
||||
ClientId = _clientId,
|
||||
DeviceId = _deviceId,
|
||||
Credential = pin,
|
||||
CredentialType = "PIN",
|
||||
Nonce = helpers.GenerateNonce(),
|
||||
Timestamp = DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffK")
|
||||
};
|
||||
|
||||
var token = _jwtTool.EncodeToken(payload);
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Post, $"{_apiUrl}/v1/oauth/token/upgrade");
|
||||
req.Content = new StringContent(token, Encoding.UTF8, "text/plain");
|
||||
|
||||
var response = await _client.SendAsync(req);
|
||||
var response = await PostAsync($"{_apiUrl}/v1/oauth/token/upgrade", new StringContent(token, Encoding.UTF8, "text/plain"));
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
@@ -138,26 +210,21 @@ namespace GM.Api
|
||||
|
||||
public async Task<bool> Login(string username, string password)
|
||||
{
|
||||
var payload = new LoginPayload()
|
||||
var payload = new LoginRequest()
|
||||
{
|
||||
client_id = _clientId,
|
||||
device_id = _deviceId,
|
||||
grant_type = "password",
|
||||
nonce = helpers.GenerateNonce(),
|
||||
password = password,
|
||||
scope = "onstar gmoc commerce user_trailer msso",
|
||||
timestamp = DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffK"),
|
||||
username = username
|
||||
ClientId = _clientId,
|
||||
DeviceId = _deviceId,
|
||||
GrantType = "password",
|
||||
Nonce = helpers.GenerateNonce(),
|
||||
Password = password,
|
||||
Scope = "onstar gmoc commerce user_trailer msso",
|
||||
Timestamp = DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffK"),
|
||||
Username = username
|
||||
};
|
||||
|
||||
var token = _jwtTool.EncodeToken(payload);
|
||||
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Post, $"{_apiUrl}/v1/oauth/token");
|
||||
req.Headers.Authorization = null;
|
||||
req.Content = new StringContent(token, Encoding.UTF8, "text/plain");
|
||||
|
||||
var response = await _client.SendAsync(req);
|
||||
var response = await PostAsync($"{_apiUrl}/v1/oauth/token", new StringContent(token, Encoding.UTF8, "text/plain"), true);
|
||||
|
||||
|
||||
string rawResponseToken = null;
|
||||
@@ -179,7 +246,7 @@ namespace GM.Api
|
||||
var loginTokenData = _jwtTool.DecodeTokenToObject<LoginData>(rawResponseToken);
|
||||
|
||||
LoginData = loginTokenData;
|
||||
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", LoginData.access_token);
|
||||
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", LoginData.AccessToken);
|
||||
|
||||
//todo: should this be a copy rather than a reference?
|
||||
await TokenUpdateCallback?.Invoke(LoginData);
|
||||
@@ -190,26 +257,20 @@ namespace GM.Api
|
||||
{
|
||||
if (LoginData == null) return false;
|
||||
|
||||
var payload = new RefreshTokenPayload()
|
||||
var payload = new LoginRequest()
|
||||
{
|
||||
client_id = _clientId,
|
||||
device_id = _deviceId,
|
||||
grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
||||
nonce = helpers.GenerateNonce(),
|
||||
scope = "onstar gmoc commerce user_trailer",
|
||||
timestamp = DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffK"),
|
||||
assertion = LoginData.id_token
|
||||
ClientId = _clientId,
|
||||
DeviceId = _deviceId,
|
||||
GrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
||||
Nonce = helpers.GenerateNonce(),
|
||||
Scope = "onstar gmoc commerce user_trailer",
|
||||
Timestamp = DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffK"),
|
||||
Assertion = LoginData.IdToken
|
||||
};
|
||||
|
||||
var token = _jwtTool.EncodeToken(payload);
|
||||
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Post, $"{_apiUrl}/v1/oauth/token");
|
||||
req.Headers.Authorization = null;
|
||||
req.Content = new StringContent(token, Encoding.UTF8, "text/plain");
|
||||
|
||||
var response = await _client.SendAsync(req);
|
||||
|
||||
var response = await PostAsync($"{_apiUrl}/v1/oauth/token", new StringContent(token, Encoding.UTF8, "text/plain"), true);
|
||||
|
||||
string rawResponseToken = null;
|
||||
|
||||
@@ -241,14 +302,14 @@ namespace GM.Api
|
||||
|
||||
var refreshData = _jwtTool.DecodeTokenToObject<LoginData>(rawResponseToken);
|
||||
|
||||
LoginData.access_token = refreshData.access_token;
|
||||
LoginData.IssuedUtc = refreshData.IssuedUtc;
|
||||
LoginData.expires_in = refreshData.expires_in;
|
||||
LoginData.AccessToken = refreshData.AccessToken;
|
||||
LoginData.IssuedAtUtc = refreshData.IssuedAtUtc;
|
||||
LoginData.ExpiresIn = refreshData.ExpiresIn;
|
||||
|
||||
//should we assume the upgrade status is broken?
|
||||
|
||||
|
||||
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", LoginData.access_token);
|
||||
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", LoginData.AccessToken);
|
||||
|
||||
//todo: should this be a copy rather than a reference?
|
||||
await TokenUpdateCallback?.Invoke(LoginData);
|
||||
@@ -265,15 +326,6 @@ namespace GM.Api
|
||||
|
||||
public async Task<Commandresponse> InitiateCommand(string vin, string pin, string command)
|
||||
{
|
||||
if (LoginData == null) throw new InvalidOperationException("Login required");
|
||||
if (LoginData.IsExpired)
|
||||
{
|
||||
if (!await RefreshToken())
|
||||
{
|
||||
throw new InvalidOperationException("Token refresh failed");
|
||||
}
|
||||
}
|
||||
|
||||
if (!_isConnected)
|
||||
{
|
||||
await VehicleConnect(vin);
|
||||
@@ -332,15 +384,9 @@ namespace GM.Api
|
||||
reqObj = new JObject();
|
||||
}
|
||||
|
||||
var response = await PostAsync($"{_apiUrl}/v1/account/vehicles/{vin}/commands/{command}", new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(reqObj), Encoding.UTF8, "application/json"));
|
||||
|
||||
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Post, $"{_apiUrl}/v1/account/vehicles/{vin}/commands/{command}");
|
||||
|
||||
req.Content = new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(reqObj), Encoding.UTF8, "application/json");
|
||||
|
||||
var response = await _client.SendAsync(req);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
@@ -355,14 +401,6 @@ namespace GM.Api
|
||||
|
||||
public async Task<Commandresponse> WaitForCommandCompletion(string statusUrl)
|
||||
{
|
||||
if (LoginData == null) throw new InvalidOperationException("Login required");
|
||||
if (LoginData.IsExpired)
|
||||
{
|
||||
if (!await RefreshToken())
|
||||
{
|
||||
throw new InvalidOperationException("Token refresh failed");
|
||||
}
|
||||
}
|
||||
int nullResponseCount = 0;
|
||||
|
||||
while (true)
|
||||
@@ -404,9 +442,7 @@ namespace GM.Api
|
||||
|
||||
async Task<Commandresponse> PollCommandStatus(string statusUrl)
|
||||
{
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, $"{statusUrl}?units=METRIC");
|
||||
|
||||
var response = await _client.SendAsync(req);
|
||||
var response = await GetAsync($"{statusUrl}?units=METRIC");
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
@@ -422,17 +458,8 @@ namespace GM.Api
|
||||
|
||||
public async Task<IEnumerable<Vehicle>> GetVehicles()
|
||||
{
|
||||
if (LoginData == null) throw new InvalidOperationException("Login required");
|
||||
if (LoginData.IsExpired)
|
||||
{
|
||||
if (!await RefreshToken())
|
||||
{
|
||||
throw new InvalidOperationException("Token refresh failed");
|
||||
}
|
||||
}
|
||||
|
||||
//these could be parameterized, but we better stick with what the app does
|
||||
var resp = await _client.GetAsync($"{_apiUrl}/v1/account/vehicles?offset=0&limit=10&includeCommands=true&includeEntitlements=true&includeModules=true");
|
||||
var resp = await GetAsync($"{_apiUrl}/v1/account/vehicles?offset=0&limit=10&includeCommands=true&includeEntitlements=true&includeModules=true");
|
||||
|
||||
if (resp.IsSuccessStatusCode)
|
||||
{
|
||||
@@ -450,15 +477,6 @@ namespace GM.Api
|
||||
|
||||
public async Task<Diagnosticresponse[]> GetDiagnostics(string vin, string pin)
|
||||
{
|
||||
if (LoginData == null) throw new InvalidOperationException("Login required");
|
||||
if (LoginData.IsExpired)
|
||||
{
|
||||
if (!await RefreshToken())
|
||||
{
|
||||
throw new InvalidOperationException("Token refresh failed");
|
||||
}
|
||||
}
|
||||
|
||||
var result = await InitiateCommandAndWait(vin, pin, "diagnostics");
|
||||
if (result == null) return null;
|
||||
if ("success".Equals(result.status, StringComparison.OrdinalIgnoreCase))
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace GM.Api
|
||||
namespace GM.Api.Models
|
||||
{
|
||||
public class CommandResponseRoot
|
||||
{
|
||||
@@ -3,7 +3,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace GM.Api
|
||||
namespace GM.Api.Models
|
||||
{
|
||||
|
||||
public class GmConfiguration
|
||||
166
GM.Api/Models/Diagnostic.cs
Normal file
166
GM.Api/Models/Diagnostic.cs
Normal file
@@ -0,0 +1,166 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
|
||||
namespace GM.Api.Models
|
||||
{
|
||||
|
||||
|
||||
|
||||
//public class DiagnosticReader
|
||||
//{
|
||||
|
||||
// IEnumerable<Diagnosticresponse> _dr;
|
||||
|
||||
// public DiagnosticReader(IEnumerable<Diagnosticresponse> elements)
|
||||
// {
|
||||
// _dr = elements;
|
||||
// }
|
||||
|
||||
// public float AmbientAirTempCelcius => float.Parse((from f in _dr
|
||||
// where f.name == "AMBIENT AIR TEMPERATURE"
|
||||
// from r in f.diagnosticElement
|
||||
// where r.name == "AMBIENT AIR TEMPERATURE"
|
||||
// select r.value).FirstOrDefault());
|
||||
// public string ChargerPowerLevel => (from f in _dr
|
||||
// where f.name == "CHARGER POWER LEVEL"
|
||||
// from r in f.diagnosticElement
|
||||
// where r.name == "CHARGER POWER LEVEL"
|
||||
// select r.value).FirstOrDefault();
|
||||
|
||||
// public float EvBatteryLevelPercent => float.Parse((from f in _dr
|
||||
// where f.name == "EV BATTERY LEVEL"
|
||||
// from r in f.diagnosticElement
|
||||
// where r.name == "EV BATTERY LEVEL"
|
||||
// select r.value).FirstOrDefault());
|
||||
|
||||
|
||||
//}
|
||||
|
||||
|
||||
//public static class DiagnosticHelper
|
||||
//{
|
||||
// public static float GetElectricEconomyKwh(this IEnumerable<Diagnosticresponse> elements)
|
||||
// {
|
||||
// var itm = float.Parse((from f in elements
|
||||
// where f.name == "ENERGY EFFICIENCY"
|
||||
// from r in f.diagnosticElement
|
||||
// where r.name == "ELECTRIC ECONOMY"
|
||||
// select r.value).FirstOrDefault());
|
||||
|
||||
// return itm;
|
||||
// }
|
||||
|
||||
// public static float GetLifetimeEfficiencyKwh(this IEnumerable<Diagnosticresponse> elements)
|
||||
// {
|
||||
// var itm = float.Parse((from f in elements
|
||||
// where f.name == "ENERGY EFFICIENCY"
|
||||
// from r in f.diagnosticElement
|
||||
// where r.name == "LIFETIME EFFICIENCY"
|
||||
// select r.value).FirstOrDefault());
|
||||
|
||||
// return itm;
|
||||
// }
|
||||
|
||||
// public static float GetLifetimeMpgE(this IEnumerable<Diagnosticresponse> elements)
|
||||
// {
|
||||
// var itm = float.Parse((from f in elements
|
||||
// where f.name == "ENERGY EFFICIENCY"
|
||||
// from r in f.diagnosticElement
|
||||
// where r.name == "LIFETIME MPGE"
|
||||
// select r.value).FirstOrDefault());
|
||||
|
||||
// return itm;
|
||||
// }
|
||||
|
||||
// public static float GetOdometerKm(this IEnumerable<Diagnosticresponse> elements)
|
||||
// {
|
||||
// var itm = float.Parse((from f in elements
|
||||
// where f.name == "ENERGY EFFICIENCY"
|
||||
// from r in f.diagnosticElement
|
||||
// where r.name == "ODOMETER"
|
||||
// select r.value).FirstOrDefault());
|
||||
|
||||
// return itm;
|
||||
// }
|
||||
|
||||
// public static float GetEvBatteryLevelPercent(this IEnumerable<Diagnosticresponse> elements)
|
||||
// {
|
||||
// var itm = float.Parse((from f in elements
|
||||
// where f.name == "EV BATTERY LEVEL"
|
||||
// from r in f.diagnosticElement
|
||||
// where r.name == "EV BATTERY LEVEL"
|
||||
// select r.value).FirstOrDefault());
|
||||
|
||||
// return itm;
|
||||
// }
|
||||
|
||||
//}
|
||||
|
||||
|
||||
public class DiagnosticRequestRoot
|
||||
{
|
||||
public static readonly string[] DefaultItems = new string[]
|
||||
{
|
||||
"ENGINE COOLANT TEMP",
|
||||
"ENGINE RPM",
|
||||
"HV BATTERY ESTIMATED CAPACITY",
|
||||
"LAST TRIP FUEL ECONOMY",
|
||||
"ENERGY EFFICIENCY",
|
||||
"HYBRID BATTERY MINIMUM TEMPERATURE",
|
||||
"EV ESTIMATED CHARGE END",
|
||||
"EV BATTERY LEVEL",
|
||||
"EV PLUG VOLTAGE",
|
||||
"ODOMETER",
|
||||
"CHARGER POWER LEVEL",
|
||||
"LIFETIME EV ODOMETER",
|
||||
"EV PLUG STATE",
|
||||
"EV CHARGE STATE",
|
||||
"TIRE PRESSURE",
|
||||
"AMBIENT AIR TEMPERATURE",
|
||||
"LAST TRIP DISTANCE",
|
||||
"INTERM VOLT BATT VOLT",
|
||||
"GET COMMUTE SCHEDULE",
|
||||
"GET CHARGE MODE",
|
||||
"EV SCHEDULED CHARGE START",
|
||||
"VEHICLE RANGE"
|
||||
};
|
||||
|
||||
|
||||
//public Diagnosticsrequest diagnosticsRequest { get; set; }
|
||||
}
|
||||
|
||||
//public class Diagnosticsrequest
|
||||
//{
|
||||
// public string[] diagnosticItem { get; set; }
|
||||
//}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public class ResponseBody
|
||||
{
|
||||
public Diagnosticresponse[] diagnosticResponse { get; set; }
|
||||
}
|
||||
|
||||
public class Diagnosticresponse
|
||||
{
|
||||
public string name { get; set; }
|
||||
public Diagnosticelement[] diagnosticElement { get; set; }
|
||||
}
|
||||
|
||||
public class Diagnosticelement
|
||||
{
|
||||
public string name { get; set; }
|
||||
public string status { get; set; }
|
||||
public string message { get; set; }
|
||||
public string value { get; set; }
|
||||
public string unit { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace GM.Api
|
||||
namespace GM.Api.Models
|
||||
{
|
||||
|
||||
|
||||
101
GM.Api/Token.cs
101
GM.Api/Token.cs
@@ -1,101 +0,0 @@
|
||||
using JWT;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace GM.Api
|
||||
{
|
||||
public class CustomJsonSerializer : IJsonSerializer
|
||||
{
|
||||
public string Serialize(object obj)
|
||||
{
|
||||
return JsonUtility.NormalizeJsonString(Newtonsoft.Json.JsonConvert.SerializeObject(obj));
|
||||
}
|
||||
|
||||
public T Deserialize<T>(string json)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(json);
|
||||
}
|
||||
}
|
||||
|
||||
public class JsonUtility
|
||||
{
|
||||
public static string NormalizeJsonString(string json)
|
||||
{
|
||||
// Parse json string into JObject.
|
||||
var parsedObject = JObject.Parse(json);
|
||||
|
||||
// Sort properties of JObject.
|
||||
var normalizedObject = SortPropertiesAlphabetically(parsedObject);
|
||||
|
||||
// Serialize JObject .
|
||||
return JsonConvert.SerializeObject(normalizedObject);
|
||||
}
|
||||
|
||||
private static JObject SortPropertiesAlphabetically(JObject original)
|
||||
{
|
||||
var result = new JObject();
|
||||
|
||||
foreach (var property in original.Properties().ToList().OrderBy(p => p.Name))
|
||||
{
|
||||
var value = property.Value as JObject;
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
value = SortPropertiesAlphabetically(value);
|
||||
result.Add(property.Name, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Add(property.Name, property.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public class LoginPayload
|
||||
{
|
||||
public string client_id { get; set; } //from config
|
||||
public string device_id { get; set; } //random guid
|
||||
public string grant_type { get; set; } //"password"
|
||||
public string nonce { get; set; } //return new BigInteger(130, new SecureRandom()).toString(32);
|
||||
public string password { get; set; }
|
||||
public string scope { get; set; } // onstar gmoc commerce user_trailer msso
|
||||
public string timestamp { get; set; } //ISO_8601_DATE_FORMAT.format(new date());
|
||||
public string username { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
public class UpgradeTokenPayload
|
||||
{
|
||||
public string client_id { get; set; }
|
||||
public string credential { get; set; }
|
||||
public string credential_type { get; set; }
|
||||
public string device_id { get; set; }
|
||||
public string nonce { get; set; }
|
||||
public string timestamp { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
public class RefreshTokenPayload
|
||||
{
|
||||
public string assertion { get; set; }
|
||||
public string client_id { get; set; }
|
||||
public string device_id { get; set; }
|
||||
public string grant_type { get; set; }
|
||||
public string nonce { get; set; }
|
||||
public string scope { get; set; }
|
||||
public string timestamp { get; set; }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace GM.Api
|
||||
namespace GM.Api.Tokens
|
||||
{
|
||||
/// <summary>
|
||||
/// Base32 Implementation
|
||||
@@ -1,10 +1,12 @@
|
||||
using JWT;
|
||||
using JWT.Algorithms;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace GM.Api
|
||||
namespace GM.Api.Tokens
|
||||
{
|
||||
class JwtTool
|
||||
{
|
||||
@@ -19,7 +21,7 @@ namespace GM.Api
|
||||
_key = Encoding.ASCII.GetBytes(key);
|
||||
|
||||
IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
|
||||
IJsonSerializer serializer = new CustomJsonSerializer();
|
||||
IJsonSerializer serializer = new SortedJsonSerializer();
|
||||
IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
|
||||
Encoder = new JwtEncoder(algorithm, serializer, urlEncoder);
|
||||
|
||||
@@ -44,12 +46,10 @@ namespace GM.Api
|
||||
{
|
||||
return Decoder.DecodeToObject<T>(token);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
73
GM.Api/Tokens/LoginData.cs
Normal file
73
GM.Api/Tokens/LoginData.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace GM.Api.Tokens
|
||||
{
|
||||
/// <summary>
|
||||
/// Data contained within the login response JWT or as updated when refreshed
|
||||
/// </summary>
|
||||
public class LoginData
|
||||
{
|
||||
[JsonProperty("access_token")]
|
||||
public string AccessToken { get; set; }
|
||||
|
||||
[JsonProperty("token_type")]
|
||||
public string TokenType { get; set; }
|
||||
|
||||
[JsonProperty("expires_in")]
|
||||
public int ExpiresIn { get; set; }
|
||||
|
||||
[JsonProperty("scope")]
|
||||
public string Scope { get; set; }
|
||||
|
||||
[JsonProperty("onstar_account_info")]
|
||||
public Onstar_Account_Info OnStarAccountInfo { get; set; }
|
||||
|
||||
[JsonProperty("user_info")]
|
||||
public User_Info UserInfo { get; set; }
|
||||
|
||||
[JsonProperty("id_token")]
|
||||
public string IdToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp the token was recieved
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public DateTime IssuedAtUtc { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Approximate timestamp the token expires
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public DateTime ExpiresAtUtc => (IssuedAtUtc + TimeSpan.FromSeconds(ExpiresIn - 2));
|
||||
|
||||
/// <summary>
|
||||
/// Check if the token is expired based on timestamp
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool IsExpired => (DateTime.UtcNow >= ExpiresAtUtc);
|
||||
|
||||
|
||||
}
|
||||
|
||||
public class Onstar_Account_Info
|
||||
{
|
||||
[JsonProperty("country_code")]
|
||||
public string CountryCode { get; set; }
|
||||
|
||||
[JsonProperty("account_no")]
|
||||
public string AccountNo { get; set; }
|
||||
}
|
||||
|
||||
public class User_Info
|
||||
{
|
||||
[JsonProperty("RemoteUserId")]
|
||||
public string RemoteUserId { get; set; }
|
||||
|
||||
[JsonProperty("country")]
|
||||
public string Country { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
72
GM.Api/Tokens/LoginRequest.cs
Normal file
72
GM.Api/Tokens/LoginRequest.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using JWT;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace GM.Api.Tokens
|
||||
{
|
||||
|
||||
|
||||
public class LoginRequest
|
||||
{
|
||||
[JsonProperty("client_id", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string ClientId { get; set; }
|
||||
|
||||
[JsonProperty("device_id", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string DeviceId { get; set; }
|
||||
|
||||
[JsonProperty("grant_type", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string GrantType { get; set; }
|
||||
|
||||
[JsonProperty("nonce", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Nonce { get; set; }
|
||||
|
||||
[JsonProperty("password", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Scope
|
||||
/// ex: onstar gmoc commerce user_trailer msso
|
||||
/// </summary>
|
||||
[JsonProperty("scope", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Scope { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current timestamp in UTC using "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffK" format string
|
||||
/// </summary>
|
||||
[JsonProperty("timestamp", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Timestamp { get; set; }
|
||||
|
||||
[JsonProperty("username", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Username { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// OnStar PIN used to upgrade token
|
||||
/// </summary>
|
||||
[JsonProperty("credential", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Credential { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// "PIN" for onstanr pin
|
||||
/// </summary>
|
||||
[JsonProperty("credential_type", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string CredentialType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// IdToken from login payload - used for refreshing
|
||||
/// </summary>
|
||||
[JsonProperty("assertion", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string Assertion { get; set; }
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
65
GM.Api/Tokens/SortedJsonSerializer.cs
Normal file
65
GM.Api/Tokens/SortedJsonSerializer.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using JWT;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace GM.Api.Tokens
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom JSON serialized used with JWT.net
|
||||
/// JWT.net's JWT header is not alphebetized by default...
|
||||
/// </summary>
|
||||
public class SortedJsonSerializer : IJsonSerializer
|
||||
{
|
||||
public string Serialize(object obj)
|
||||
{
|
||||
return NormalizeJsonString(Newtonsoft.Json.JsonConvert.SerializeObject(obj));
|
||||
}
|
||||
|
||||
public T Deserialize<T>(string json)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(json);
|
||||
}
|
||||
|
||||
|
||||
public static string NormalizeJsonString(string json)
|
||||
{
|
||||
// Parse json string into JObject.
|
||||
var parsedObject = JObject.Parse(json);
|
||||
|
||||
// Sort properties of JObject.
|
||||
var normalizedObject = SortPropertiesAlphabetically(parsedObject);
|
||||
|
||||
// Serialize JObject .
|
||||
return JsonConvert.SerializeObject(normalizedObject);
|
||||
}
|
||||
|
||||
private static JObject SortPropertiesAlphabetically(JObject original)
|
||||
{
|
||||
var result = new JObject();
|
||||
|
||||
foreach (var property in original.Properties().ToList().OrderBy(p => p.Name))
|
||||
{
|
||||
var value = property.Value as JObject;
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
value = SortPropertiesAlphabetically(value);
|
||||
result.Add(property.Name, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Add(property.Name, property.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace GM.Api
|
||||
{
|
||||
|
||||
public class LoginData
|
||||
{
|
||||
public string access_token { get; set; }
|
||||
public string token_type { get; set; }
|
||||
public int expires_in { get; set; }
|
||||
public string scope { get; set; }
|
||||
public Onstar_Account_Info onstar_account_info { get; set; }
|
||||
public User_Info user_info { get; set; }
|
||||
public string id_token { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public DateTime IssuedUtc { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// subtracting 2 seconds for safety
|
||||
[JsonIgnore]
|
||||
public DateTime ExpiresAtUtc => (IssuedUtc + TimeSpan.FromSeconds(expires_in - 2));
|
||||
|
||||
|
||||
public bool IsExpired => (DateTime.UtcNow >= ExpiresAtUtc);
|
||||
|
||||
|
||||
}
|
||||
|
||||
public class Onstar_Account_Info
|
||||
{
|
||||
public string country_code { get; set; }
|
||||
public string account_no { get; set; }
|
||||
}
|
||||
|
||||
public class User_Info
|
||||
{
|
||||
public string RemoteUserId { get; set; }
|
||||
public string country { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10,8 +10,6 @@ namespace GM.Api
|
||||
{
|
||||
static class helpers
|
||||
{
|
||||
|
||||
|
||||
public static string GenerateNonce()
|
||||
{
|
||||
//17.25 bytes = 130 bits
|
||||
@@ -21,30 +19,16 @@ namespace GM.Api
|
||||
var byteArray = new byte[17];
|
||||
|
||||
provider.GetBytes(byteArray);
|
||||
var nonce = Base32.ToBase32String(byteArray);
|
||||
var nonce = Tokens.Base32.ToBase32String(byteArray);
|
||||
return nonce.ToLower().Substring(0, 26);
|
||||
}
|
||||
|
||||
|
||||
public static IJwtEncoder GetJwtEncoder()
|
||||
{
|
||||
IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
|
||||
IJsonSerializer serializer = new CustomJsonSerializer();
|
||||
IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
|
||||
return new JwtEncoder(algorithm, serializer, urlEncoder);
|
||||
}
|
||||
|
||||
public static IJwtDecoder GetJwtDecoder()
|
||||
{
|
||||
IJsonSerializer serializer = new CustomJsonSerializer();
|
||||
IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
|
||||
IDateTimeProvider dateTimeProvider = new UtcDateTimeProvider();
|
||||
IJwtValidator validator = new JwtValidator(serializer, dateTimeProvider);
|
||||
return new JwtDecoder(serializer, validator, urlEncoder);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Set an HTTP header to a single value, clearing any existing values
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="headerValue"></param>
|
||||
/// <param name="value"></param>
|
||||
public static void SetValue<T>(this HttpHeaderValueCollection<T> headerValue, string value) where T: class
|
||||
{
|
||||
headerValue.Clear();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Newtonsoft.Json;
|
||||
using GM.Api.Models;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@@ -22,12 +23,12 @@ namespace GM.WindowsUI
|
||||
public partial class BrandWindow : Window
|
||||
{
|
||||
|
||||
GM.Api.GmConfiguration _config;
|
||||
GmConfiguration _config;
|
||||
|
||||
public string SelectedBrand { get; set; } = null;
|
||||
|
||||
|
||||
public BrandWindow(GM.Api.GmConfiguration configuration)
|
||||
public BrandWindow(GmConfiguration configuration)
|
||||
{
|
||||
_config = configuration;
|
||||
InitializeComponent();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using GM.Api;
|
||||
using GM.Api.Models;
|
||||
using GM.Api.Tokens;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -26,10 +28,10 @@ namespace GM.WindowsUI
|
||||
GMClient _client;
|
||||
|
||||
|
||||
GM.Api.GmConfiguration _globalConfig;
|
||||
GmConfiguration _globalConfig;
|
||||
|
||||
GM.Api.ApiConfig _apiConfig;
|
||||
GM.Api.BrandClientInfo _clientCredentials;
|
||||
ApiConfig _apiConfig;
|
||||
BrandClientInfo _clientCredentials;
|
||||
|
||||
string _brand;
|
||||
string _brandDisplay;
|
||||
@@ -128,7 +130,7 @@ namespace GM.WindowsUI
|
||||
|
||||
try
|
||||
{
|
||||
_globalConfig = JsonConvert.DeserializeObject<GM.Api.GmConfiguration>(File.ReadAllText("Config\\configuration.json", Encoding.UTF8));
|
||||
_globalConfig = JsonConvert.DeserializeObject<GmConfiguration>(File.ReadAllText("Config\\configuration.json", Encoding.UTF8));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -28,3 +28,5 @@ TODO: determine how app elevates creds when using fingerprint - does the app sav
|
||||
TODO: there is a means of refreshing a token using a pin...
|
||||
|
||||
TODO: determine how long elevation lasts, keep track and re-elevate when required
|
||||
|
||||
TODO: consider using MS JWT implementation
|
||||
|
||||
Reference in New Issue
Block a user