Steps
All checks were successful
Build, Push and Run Container / build (push) Successful in 26s

This commit is contained in:
2025-08-13 19:36:32 +02:00
parent 25f869851f
commit 1b8fda32d9
2 changed files with 110 additions and 40 deletions

View File

@@ -9,5 +9,5 @@ public record Token
public string TokenType { get; init; } = String.Empty; public string TokenType { get; init; } = String.Empty;
//For JSON deserialization //For JSON deserialization
public int ExpiresIn { get => (int)DateTimeOffset.Now.Subtract(Expires).TotalSeconds; init => Expires = DateTimeOffset.Now.AddSeconds(value); } public int ExpiresIn { get => (int)Expires.Subtract(DateTimeOffset.Now).TotalSeconds; init => Expires = DateTimeOffset.Now.AddSeconds(value); }
} }

View File

@@ -1,5 +1,6 @@
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using ProofOfConcept.Models; using ProofOfConcept.Models;
@@ -18,20 +19,12 @@ public class TeslaAuthenticatorService
const string authEndpointURL = "https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3/token"; const string authEndpointURL = "https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3/token";
const string euBaseURL = "https://fleet-api.prd.eu.vn.cloud.tesla.com"; const string euBaseURL = "https://fleet-api.prd.eu.vn.cloud.tesla.com";
private JsonSerializerOptions serializerOptions;
public TeslaAuthenticatorService(ILogger<TeslaAuthenticatorService> logger, IOptions<TeslaAuthenticatorServiceConfiguration> options, IHttpClientFactory httpClientFactory, IMemoryCache memoryCache) public TeslaAuthenticatorService(ILogger<TeslaAuthenticatorService> logger, IOptions<TeslaAuthenticatorServiceConfiguration> options, IHttpClientFactory httpClientFactory, IMemoryCache memoryCache)
{ {
this.logger = logger; this.logger = logger;
this.httpClientFactory = httpClientFactory; this.httpClientFactory = httpClientFactory;
this.memoryCache = memoryCache; this.memoryCache = memoryCache;
this.configuration = options.Value; this.configuration = options.Value;
serializerOptions = new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
};
} }
/// Asynchronously retrieves an authentication token from the Tesla partner API. /// Asynchronously retrieves an authentication token from the Tesla partner API.
@@ -39,7 +32,7 @@ public class TeslaAuthenticatorService
/// OAuth2 access token that grants access to Tesla partner services. /// OAuth2 access token that grants access to Tesla partner services.
/// <return>Returns a string containing the authentication token received from the API.</return> /// <return>Returns a string containing the authentication token received from the API.</return>
/// <exception cref="HttpRequestException">Thrown when the HTTP request to the API fails.</exception> /// <exception cref="HttpRequestException">Thrown when the HTTP request to the API fails.</exception>
public async Task<Token> AcquirePartnerAuthenticationTokenAsync() public async Task<Result<Token>> AcquirePartnerAuthenticationTokenAsync()
{ {
this.logger.LogTrace("Acquiring authentication token from Tesla partner API..."); this.logger.LogTrace("Acquiring authentication token from Tesla partner API...");
@@ -60,6 +53,9 @@ public class TeslaAuthenticatorService
FormUrlEncodedContent content = new FormUrlEncodedContent(formData); FormUrlEncodedContent content = new FormUrlEncodedContent(formData);
string json = "";
try
{
// Send POST request // Send POST request
HttpResponseMessage response = await client.PostAsync(authEndpointURL, content); HttpResponseMessage response = await client.PostAsync(authEndpointURL, content);
this.logger.LogTrace("Response status code: {0}", response.StatusCode); this.logger.LogTrace("Response status code: {0}", response.StatusCode);
@@ -68,22 +64,54 @@ public class TeslaAuthenticatorService
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
// Read the response body as a string // Read the response body as a string
string json = await response.Content.ReadAsStringAsync(); json = await response.Content.ReadAsStringAsync();
// Deserialize
try
{
Token? result = JsonSerializer.Deserialize<Token>(json);
if (result is null)
throw new FormatException("Response could not be deserialized (null).");
// Return the token response
return result;
} }
catch (HttpRequestException httpRequestException)
{
this.logger.LogError(httpRequestException, "HTTP Request error while acquiring partner authentication token from Tesla API");
return new Result<Token>(httpRequestException);
}
catch (Exception e) catch (Exception e)
{ {
throw new AggregateException($"Could not deserialize token response: {json}", e); this.logger.LogError(e, "Unexpected communication error while acquiring partner authentication token from Tesla API");
return new Result<Token>(e);
}
try
{
// Check if the response is empty
if (String.IsNullOrWhiteSpace(json))
throw new JsonException("Response could not be deserialized (empty).");
// Deserializer options: {"access_token":"999 random chars including some special characters","expires_in":28800,"token_type":"Bearer"}
JsonSerializerOptions serializerOptions = new JsonSerializerOptions()
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
};
// Deserialize the JSON response
Token? result = JsonSerializer.Deserialize<Token>(json, serializerOptions);
if (result is null)
throw new JsonException("Response could not be deserialized (null).");
this.logger.LogInformation("Authentication token acquired from Tesla partner API");
return new Result<Token>(result);
}
catch (JsonException jsonException)
{
this.logger.LogError(jsonException, "JSON deserialization error while acquiring partner authentication token from Tesla API");
return new Result<Token>(jsonException);
}
catch (Exception e)
{
this.logger.LogError(e, "Unexpected serialization error while acquiring partner authentication token from Tesla API");
return new Result<Token>(e);
} }
} }
@@ -94,7 +122,7 @@ public class TeslaAuthenticatorService
/// <return>Returns a Token object containing the authentication token and its expiration details.</return> /// <return>Returns a Token object containing the authentication token and its expiration details.</return>
/// <exception cref="HttpRequestException">Thrown when the HTTP request to acquire a new partner token fails.</exception> /// <exception cref="HttpRequestException">Thrown when the HTTP request to acquire a new partner token fails.</exception>
/// <exception cref="Exception">Thrown for any unexpected errors during the token retrieval or caching process.</exception> /// <exception cref="Exception">Thrown for any unexpected errors during the token retrieval or caching process.</exception>
public async Task<Token> GetPartnerAuthenticationTokenAsync() public async Task<Result<Token>> GetPartnerAuthenticationTokenAsync()
{ {
this.logger.LogTrace("Getting partner authentication token..."); this.logger.LogTrace("Getting partner authentication token...");
@@ -102,17 +130,25 @@ public class TeslaAuthenticatorService
if (this.memoryCache.TryGetValue(Keys.TeslaPartnerToken, out Token? token) && token is not null) if (this.memoryCache.TryGetValue(Keys.TeslaPartnerToken, out Token? token) && token is not null)
{ {
this.logger.LogInformation("Partner authentication token provided from cache"); this.logger.LogInformation("Partner authentication token provided from cache");
return token; return Result.Success(token);
} }
//Acquire token //Acquire token
token = await AcquirePartnerAuthenticationTokenAsync(); Result<Token> aquirationResult = await AcquirePartnerAuthenticationTokenAsync();
//Save to cache //Save to cache
if (aquirationResult.IsSuccessful)
{
token = aquirationResult.Value;
this.memoryCache.Set(Keys.TeslaPartnerToken, token, token.Expires.Subtract(this.configuration.MemoryCacheDelta)); this.memoryCache.Set(Keys.TeslaPartnerToken, token, token.Expires.Subtract(this.configuration.MemoryCacheDelta));
this.logger.LogInformation("Partner authentication token provided provided by acquiring from API"); this.logger.LogInformation("Partner authentication token provided by acquiring from API");
return token; }
else
this.logger.LogError("Failed to get partner authentication token");
return aquirationResult;
} }
public async Task<Result> RegisterApplicationAsync(CancellationToken cancellationToken = default(CancellationToken)) public async Task<Result> RegisterApplicationAsync(CancellationToken cancellationToken = default(CancellationToken))
@@ -121,7 +157,13 @@ public class TeslaAuthenticatorService
//Create client and get partner token //Create client and get partner token
HttpClient httpClient = httpClientFactory.CreateClient(); HttpClient httpClient = httpClientFactory.CreateClient();
Token partnerToken = await GetPartnerAuthenticationTokenAsync(); Result<Token> partnerTokenResult = await GetPartnerAuthenticationTokenAsync();
if (!partnerTokenResult.IsSuccessful)
return Result.Fail(partnerTokenResult.Exception ?? new Exception("Partner authentication token not found"));
//Set partner token
Token partnerToken = partnerTokenResult.Value;
//Set headers //Set headers
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
@@ -135,7 +177,29 @@ public class TeslaAuthenticatorService
bool success = response.IsSuccessStatusCode; bool success = response.IsSuccessStatusCode;
logger.LogInformation("Application registration result: {Success}", success ? "Success" : "Failed"); logger.LogInformation("Application registration result: {Success}", success ? "Success" : "Failed");
return new Result(success);
//Load response from server
string json = await response.Content.ReadAsStringAsync(cancellationToken);
this.logger.LogTrace("Registration response from server: {Response}", json);
//{"isSuccessful":true,"isFailed":false,"exceptions":[],"exception":null}
//Parse JSON
JsonNode? root = JsonNode.Parse(json);
bool isSuccessful = root?["isSuccessful"]?.GetValue<bool>() ?? false;
bool isFailed = root?["isFailed"]?.GetValue<bool>() ?? false;
if (success && isSuccessful && !isFailed)
{
logger.LogInformation("Application registered successfully");
return Result.Success();
}
else
{
logger.LogError("Application registration failed");
return Result.Fail(new AggregateException(new Exception(root?["exception"]?.ToJsonString()), new Exception(root?["exceptions"]?.ToJsonString())));
}
} }
public async Task<Result<bool>> CheckApplicationRegistrationAsync(CancellationToken cancellationToken = default(CancellationToken)) public async Task<Result<bool>> CheckApplicationRegistrationAsync(CancellationToken cancellationToken = default(CancellationToken))
@@ -144,14 +208,20 @@ public class TeslaAuthenticatorService
//Create client and get partner token //Create client and get partner token
HttpClient httpClient = httpClientFactory.CreateClient(); HttpClient httpClient = httpClientFactory.CreateClient();
Token partnerToken = await GetPartnerAuthenticationTokenAsync(); Result<Token> partnerTokenResult = await GetPartnerAuthenticationTokenAsync();
if (!partnerTokenResult.IsSuccessful)
return Result.Fail(partnerTokenResult.Exception ?? new Exception("Partner authentication token not found"));
//Set partner token
Token partnerToken = partnerTokenResult.Value;
//Set headers //Set headers
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", partnerToken.AccessToken); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", partnerToken.AccessToken);
//Send request //Send request
HttpResponseMessage response = await httpClient.GetAsync($"{euBaseURL}/api/1/partner_accounts", cancellationToken); HttpResponseMessage response = await httpClient.GetAsync($"{euBaseURL}/api/1/partner_accounts/public_key?domain={this.configuration.Domain}", cancellationToken);
string responseBody = await response.Content.ReadAsStringAsync(cancellationToken); string responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
logger.LogInformation("Application registration result: {Result}", responseBody); logger.LogInformation("Application registration result: {Result}", responseBody);