From 76045c954c46b35e0cfaffe30da6b207a6da26a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Thu, 26 Dec 2024 16:27:14 +0100 Subject: [PATCH 01/81] feat(backendApiService): can refresh tokens --- .../Contracts/ITeslaSolarChargerContext.cs | 2 +- .../TeslaSolarCharger/BackendToken.cs | 9 ++ .../Entities/TeslaSolarCharger/TeslaToken.cs | 15 -- .../TeslaSolarChargerContext.cs | 2 +- .../Solar2CarBackend/User/DtoAccessToken.cs | 8 + .../Solar2CarBackend/User/DtoCreateUser.cs | 8 + .../Dtos/Solar2CarBackend/User/DtoLogin.cs | 7 + .../User/DtoTokenRefreshModel.cs | 7 + .../Server/Scheduling/JobManager.cs | 10 +- .../Scheduling/Jobs/ApiCallCounterResetJob.cs | 24 --- .../Jobs/BackendNotificationRefreshJob.cs | 16 ++ .../Scheduling/Jobs/BackendTokenRefreshJob.cs | 16 ++ .../Jobs/FleetApiTokenRefreshJob.cs | 23 --- .../Server/ServiceCollectionExtensions.cs | 5 +- .../Server/Services/BackendApiService.cs | 149 ++++++++++-------- .../Services/Contracts/IBackendApiService.cs | 3 +- .../Contracts/IPasswordGenerationService.cs | 6 + .../Contracts/ITscConfigurationService.cs | 15 ++ .../FleetTelemetryWebSocketService.cs | 2 +- .../Services/PasswordGenerationService.cs | 26 +++ .../Server/Services/TeslaFleetApiService.cs | 82 ++++------ .../Services/TeslaFleetApiTokenHelper.cs | 2 +- .../Services/TscConfigurationService.cs | 79 +++++++--- TeslaSolarCharger/Server/appsettings.json | 6 +- .../Shared/Contracts/IConfigurationWrapper.cs | 2 +- .../Shared/Resources/Constants.cs | 13 +- .../Shared/Resources/Contracts/IConstants.cs | 5 +- .../Shared/Wrappers/ConfigurationWrapper.cs | 16 +- 28 files changed, 327 insertions(+), 231 deletions(-) create mode 100644 TeslaSolarCharger.Model/Entities/TeslaSolarCharger/BackendToken.cs delete mode 100644 TeslaSolarCharger.Model/Entities/TeslaSolarCharger/TeslaToken.cs create mode 100644 TeslaSolarCharger/Server/Dtos/Solar2CarBackend/User/DtoAccessToken.cs create mode 100644 TeslaSolarCharger/Server/Dtos/Solar2CarBackend/User/DtoCreateUser.cs create mode 100644 TeslaSolarCharger/Server/Dtos/Solar2CarBackend/User/DtoLogin.cs create mode 100644 TeslaSolarCharger/Server/Dtos/Solar2CarBackend/User/DtoTokenRefreshModel.cs delete mode 100644 TeslaSolarCharger/Server/Scheduling/Jobs/ApiCallCounterResetJob.cs create mode 100644 TeslaSolarCharger/Server/Scheduling/Jobs/BackendNotificationRefreshJob.cs create mode 100644 TeslaSolarCharger/Server/Scheduling/Jobs/BackendTokenRefreshJob.cs delete mode 100644 TeslaSolarCharger/Server/Scheduling/Jobs/FleetApiTokenRefreshJob.cs create mode 100644 TeslaSolarCharger/Server/Services/Contracts/IPasswordGenerationService.cs create mode 100644 TeslaSolarCharger/Server/Services/PasswordGenerationService.cs diff --git a/TeslaSolarCharger.Model/Contracts/ITeslaSolarChargerContext.cs b/TeslaSolarCharger.Model/Contracts/ITeslaSolarChargerContext.cs index a4cb94817..2b5847e30 100644 --- a/TeslaSolarCharger.Model/Contracts/ITeslaSolarChargerContext.cs +++ b/TeslaSolarCharger.Model/Contracts/ITeslaSolarChargerContext.cs @@ -15,7 +15,7 @@ public interface ITeslaSolarChargerContext Task SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken()); DatabaseFacade Database { get; } DbSet SpotPrices { get; set; } - DbSet TeslaTokens { get; set; } + DbSet BackendTokens { get; set; } DbSet TscConfigurations { get; set; } DbSet Cars { get; set; } DbSet RestValueConfigurations { get; set; } diff --git a/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/BackendToken.cs b/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/BackendToken.cs new file mode 100644 index 000000000..08d003500 --- /dev/null +++ b/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/BackendToken.cs @@ -0,0 +1,9 @@ +namespace TeslaSolarCharger.Model.Entities.TeslaSolarCharger; + +public class BackendToken(string accessToken, string refreshToken) +{ + public int Id { get; set; } + public string AccessToken { get; set; } = accessToken; + public string RefreshToken { get; set; } = refreshToken; + public DateTimeOffset ExpiresAtUtc { get; set; } +} diff --git a/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/TeslaToken.cs b/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/TeslaToken.cs deleted file mode 100644 index 4a61b6153..000000000 --- a/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/TeslaToken.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.Extensions.Primitives; -using TeslaSolarCharger.Model.Enums; - -namespace TeslaSolarCharger.Model.Entities.TeslaSolarCharger; - -public class TeslaToken -{ - public int Id { get; set; } - public string AccessToken { get; set; } - public string RefreshToken { get; set; } - public string IdToken { get; set; } - public int UnauthorizedCounter { get; set; } - public DateTime ExpiresAtUtc { get; set; } - public TeslaFleetApiRegion Region { get; set; } -} diff --git a/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs b/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs index b0333c1bb..31ccd1b7d 100644 --- a/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs +++ b/TeslaSolarCharger.Model/EntityFramework/TeslaSolarChargerContext.cs @@ -16,7 +16,7 @@ public class TeslaSolarChargerContext : DbContext, ITeslaSolarChargerContext public DbSet HandledCharges { get; set; } = null!; public DbSet PowerDistributions { get; set; } = null!; public DbSet SpotPrices { get; set; } = null!; - public DbSet TeslaTokens { get; set; } = null!; + public DbSet BackendTokens { get; set; } = null!; public DbSet TscConfigurations { get; set; } = null!; public DbSet Cars { get; set; } = null!; public DbSet RestValueConfigurations { get; set; } = null!; diff --git a/TeslaSolarCharger/Server/Dtos/Solar2CarBackend/User/DtoAccessToken.cs b/TeslaSolarCharger/Server/Dtos/Solar2CarBackend/User/DtoAccessToken.cs new file mode 100644 index 000000000..18cf0c874 --- /dev/null +++ b/TeslaSolarCharger/Server/Dtos/Solar2CarBackend/User/DtoAccessToken.cs @@ -0,0 +1,8 @@ +namespace TeslaSolarCharger.Server.Dtos.Solar2CarBackend.User; + +public class DtoAccessToken(string accessToken, string refreshToken) +{ + public string AccessToken { get; set; } = accessToken; + public string RefreshToken { get; set; } = refreshToken; + public long ExpiresAt { get; set; } +} diff --git a/TeslaSolarCharger/Server/Dtos/Solar2CarBackend/User/DtoCreateUser.cs b/TeslaSolarCharger/Server/Dtos/Solar2CarBackend/User/DtoCreateUser.cs new file mode 100644 index 000000000..dae46f7d5 --- /dev/null +++ b/TeslaSolarCharger/Server/Dtos/Solar2CarBackend/User/DtoCreateUser.cs @@ -0,0 +1,8 @@ +namespace TeslaSolarCharger.Server.Dtos.Solar2CarBackend.User; + +public class DtoCreateUser(string userName, string password) +{ + public string UserName { get; set; } = userName; + public string? Email { get; set; } + public string Password { get; set; } = password; +} diff --git a/TeslaSolarCharger/Server/Dtos/Solar2CarBackend/User/DtoLogin.cs b/TeslaSolarCharger/Server/Dtos/Solar2CarBackend/User/DtoLogin.cs new file mode 100644 index 000000000..249480d53 --- /dev/null +++ b/TeslaSolarCharger/Server/Dtos/Solar2CarBackend/User/DtoLogin.cs @@ -0,0 +1,7 @@ +namespace TeslaSolarCharger.Server.Dtos.Solar2CarBackend.User; + +public class DtoLogin(string userName, string password) +{ + public string UserName { get; set; } = userName; + public string Password { get; set; } = password; +} diff --git a/TeslaSolarCharger/Server/Dtos/Solar2CarBackend/User/DtoTokenRefreshModel.cs b/TeslaSolarCharger/Server/Dtos/Solar2CarBackend/User/DtoTokenRefreshModel.cs new file mode 100644 index 000000000..87960e38e --- /dev/null +++ b/TeslaSolarCharger/Server/Dtos/Solar2CarBackend/User/DtoTokenRefreshModel.cs @@ -0,0 +1,7 @@ +namespace TeslaSolarCharger.Server.Dtos.Solar2CarBackend.User; + +public class DtoTokenRefreshModel(string accessToken, string refreshToken) +{ + public string AccessToken { get; set; } = accessToken; + public string RefreshToken { get; set; } = refreshToken; +} diff --git a/TeslaSolarCharger/Server/Scheduling/JobManager.cs b/TeslaSolarCharger/Server/Scheduling/JobManager.cs index d00212425..c3e3fb1bb 100644 --- a/TeslaSolarCharger/Server/Scheduling/JobManager.cs +++ b/TeslaSolarCharger/Server/Scheduling/JobManager.cs @@ -43,10 +43,10 @@ public async Task StartJobs() var mqttReconnectionJob = JobBuilder.Create().WithIdentity(nameof(MqttReconnectionJob)).Build(); var newVersionCheckJob = JobBuilder.Create().WithIdentity(nameof(NewVersionCheckJob)).Build(); var spotPriceJob = JobBuilder.Create().WithIdentity(nameof(SpotPriceJob)).Build(); - var fleetApiTokenRefreshJob = JobBuilder.Create().WithIdentity(nameof(FleetApiTokenRefreshJob)).Build(); + var backendTokenRefreshJob = JobBuilder.Create().WithIdentity(nameof(BackendTokenRefreshJob)).Build(); var vehicleDataRefreshJob = JobBuilder.Create().WithIdentity(nameof(VehicleDataRefreshJob)).Build(); var teslaMateChargeCostUpdateJob = JobBuilder.Create().WithIdentity(nameof(TeslaMateChargeCostUpdateJob)).Build(); - var apiCallCounterResetJob = JobBuilder.Create().WithIdentity(nameof(ApiCallCounterResetJob)).Build(); + var backendNotificationRefreshJob = JobBuilder.Create().WithIdentity(nameof(BackendNotificationRefreshJob)).Build(); var errorMessagingJob = JobBuilder.Create().WithIdentity(nameof(ErrorMessagingJob)).Build(); var errorDetectionJob = JobBuilder.Create().WithIdentity(nameof(ErrorDetectionJob)).Build(); var bleApiVersionDetectionJob = JobBuilder.Create().WithIdentity(nameof(BleApiVersionDetectionJob)).Build(); @@ -96,7 +96,7 @@ public async Task StartJobs() var spotPricePlanningTrigger = TriggerBuilder.Create().WithIdentity("spotPricePlanningTrigger") .WithSchedule(SimpleScheduleBuilder.RepeatHourlyForever(1)).Build(); - var fleetApiTokenRefreshTrigger = TriggerBuilder.Create().WithIdentity("fleetApiTokenRefreshTrigger") + var backendTokenRefreshTrigger = TriggerBuilder.Create().WithIdentity("fleetApiTokenRefreshTrigger") .WithSchedule(SimpleScheduleBuilder.RepeatSecondlyForever(59)).Build(); var vehicleDataRefreshTrigger = TriggerBuilder.Create().WithIdentity("vehicleDataRefreshTrigger") @@ -154,10 +154,10 @@ public async Task StartJobs() triggersAndJobs.Add(carStateCachingJob, new HashSet { carStateCachingTrigger }); triggersAndJobs.Add(finishedChargingProcessFinalizingJob, new HashSet { finishedChargingProcessFinalizingTrigger }); triggersAndJobs.Add(mqttReconnectionJob, new HashSet { mqttReconnectionTrigger }); - triggersAndJobs.Add(fleetApiTokenRefreshJob, new HashSet { fleetApiTokenRefreshTrigger }); + triggersAndJobs.Add(backendTokenRefreshJob, new HashSet { backendTokenRefreshTrigger }); triggersAndJobs.Add(vehicleDataRefreshJob, new HashSet { vehicleDataRefreshTrigger }); triggersAndJobs.Add(teslaMateChargeCostUpdateJob, new HashSet { teslaMateChargeCostUpdateTrigger }); - triggersAndJobs.Add(apiCallCounterResetJob, new HashSet { triggerAtNight, triggerNow }); + triggersAndJobs.Add(backendNotificationRefreshJob, new HashSet { triggerAtNight, triggerNow }); } await _scheduler.ScheduleJobs(triggersAndJobs, false).ConfigureAwait(false); diff --git a/TeslaSolarCharger/Server/Scheduling/Jobs/ApiCallCounterResetJob.cs b/TeslaSolarCharger/Server/Scheduling/Jobs/ApiCallCounterResetJob.cs deleted file mode 100644 index 84c5558c4..000000000 --- a/TeslaSolarCharger/Server/Scheduling/Jobs/ApiCallCounterResetJob.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Quartz; -using TeslaSolarCharger.Server.Services.Contracts; -using TeslaSolarCharger.Shared.Contracts; -using TeslaSolarCharger.Shared.Dtos.Contracts; - -namespace TeslaSolarCharger.Server.Scheduling.Jobs; - -public class ApiCallCounterResetJob(ILogger logger, - ITeslaFleetApiService service, - IBackendApiService backendApiService, - IDateTimeProvider dateTimeProvider, - ISettings settings) : IJob -{ - public async Task Execute(IJobExecutionContext context) - { - logger.LogTrace("{method}({context})", nameof(Execute), context); - if (settings.StartupTime < dateTimeProvider.UtcNow().AddMinutes(-10)) - { - await backendApiService.PostTeslaApiCallStatistics().ConfigureAwait(false); - } - service.ResetApiRequestCounters(); - await backendApiService.GetNewBackendNotifications().ConfigureAwait(false); - } -} diff --git a/TeslaSolarCharger/Server/Scheduling/Jobs/BackendNotificationRefreshJob.cs b/TeslaSolarCharger/Server/Scheduling/Jobs/BackendNotificationRefreshJob.cs new file mode 100644 index 000000000..aa9ddcfdb --- /dev/null +++ b/TeslaSolarCharger/Server/Scheduling/Jobs/BackendNotificationRefreshJob.cs @@ -0,0 +1,16 @@ +using Quartz; +using TeslaSolarCharger.Server.Services.Contracts; + +namespace TeslaSolarCharger.Server.Scheduling.Jobs; + +public class BackendNotificationRefreshJob(ILogger logger, + ITeslaFleetApiService service, + IBackendApiService backendApiService) : IJob +{ + public async Task Execute(IJobExecutionContext context) + { + logger.LogTrace("{method}({context})", nameof(Execute), context); + service.ResetApiRequestCounters(); + await backendApiService.GetNewBackendNotifications().ConfigureAwait(false); + } +} diff --git a/TeslaSolarCharger/Server/Scheduling/Jobs/BackendTokenRefreshJob.cs b/TeslaSolarCharger/Server/Scheduling/Jobs/BackendTokenRefreshJob.cs new file mode 100644 index 000000000..b1887a764 --- /dev/null +++ b/TeslaSolarCharger/Server/Scheduling/Jobs/BackendTokenRefreshJob.cs @@ -0,0 +1,16 @@ +using Quartz; +using TeslaSolarCharger.Server.Services.Contracts; + +namespace TeslaSolarCharger.Server.Scheduling.Jobs; + +[DisallowConcurrentExecution] +public class BackendTokenRefreshJob(ILogger logger, + IBackendApiService service) + : IJob +{ + public async Task Execute(IJobExecutionContext context) + { + logger.LogTrace("{method}({context})", nameof(Execute), context); + await service.GetOrRefreshBackendToken().ConfigureAwait(false); + } +} diff --git a/TeslaSolarCharger/Server/Scheduling/Jobs/FleetApiTokenRefreshJob.cs b/TeslaSolarCharger/Server/Scheduling/Jobs/FleetApiTokenRefreshJob.cs deleted file mode 100644 index 3996a50b4..000000000 --- a/TeslaSolarCharger/Server/Scheduling/Jobs/FleetApiTokenRefreshJob.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Quartz; -using TeslaSolarCharger.Server.Services.Contracts; - -namespace TeslaSolarCharger.Server.Scheduling.Jobs; - -[DisallowConcurrentExecution] -public class FleetApiTokenRefreshJob(ILogger logger, - ITeslaFleetApiService service) - : IJob -{ - public async Task Execute(IJobExecutionContext context) - { - logger.LogTrace("{method}({context})", nameof(Execute), context); - await service.RefreshFleetApiRequestsAreAllowed().ConfigureAwait(false); - var newTokenReceived = await service.GetNewTokenFromBackend().ConfigureAwait(false); - if (newTokenReceived) - { - logger.LogInformation("A new Tesla Token was received."); - - } - await service.RefreshTokensIfAllowedAndNeeded().ConfigureAwait(false); - } -} diff --git a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs index 9ea7ce85b..0a32aff5c 100644 --- a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs +++ b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs @@ -48,10 +48,10 @@ public static IServiceCollection AddMyDependencies(this IServiceCollection servi .AddTransient() .AddTransient() .AddTransient() - .AddTransient() + .AddTransient() .AddTransient() .AddTransient() - .AddTransient() + .AddTransient() .AddTransient() .AddTransient() .AddTransient() @@ -113,6 +113,7 @@ public static IServiceCollection AddMyDependencies(this IServiceCollection servi .AddTransient() .AddTransient() .AddTransient() + .AddTransient() .AddSingleton() .AddSingleton() .AddSharedBackendDependencies(); diff --git a/TeslaSolarCharger/Server/Services/BackendApiService.cs b/TeslaSolarCharger/Server/Services/BackendApiService.cs index b36bbe554..de363e3d6 100644 --- a/TeslaSolarCharger/Server/Services/BackendApiService.cs +++ b/TeslaSolarCharger/Server/Services/BackendApiService.cs @@ -4,6 +4,7 @@ using System.Reflection; using TeslaSolarCharger.Model.Contracts; using TeslaSolarCharger.Model.Entities.TeslaSolarCharger; +using TeslaSolarCharger.Server.Dtos.Solar2CarBackend.User; using TeslaSolarCharger.Server.Dtos.TscBackend; using TeslaSolarCharger.Server.Resources.PossibleIssues.Contracts; using TeslaSolarCharger.Server.Services.Contracts; @@ -21,25 +22,22 @@ public class BackendApiService( ITeslaSolarChargerContext teslaSolarChargerContext, IConstants constants, IDateTimeProvider dateTimeProvider, - ISettings settings, IErrorHandlingService errorHandlingService, - IIssueKeys issueKeys) + IIssueKeys issueKeys, + IPasswordGenerationService passwordGenerationService) : IBackendApiService { public async Task> StartTeslaOAuth(string locale, string baseUrl) { logger.LogTrace("{method}()", nameof(StartTeslaOAuth)); - var currentTokens = await teslaSolarChargerContext.TeslaTokens.ToListAsync().ConfigureAwait(false); - teslaSolarChargerContext.TeslaTokens.RemoveRange(currentTokens); var configEntriesToRemove = await teslaSolarChargerContext.TscConfigurations .Where(c => c.Key == constants.TokenMissingScopes) .ToListAsync().ConfigureAwait(false); teslaSolarChargerContext.TscConfigurations.RemoveRange(configEntriesToRemove); await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); - var installationId = await tscConfigurationService.GetInstallationId().ConfigureAwait(false); var backendApiBaseUrl = configurationWrapper.BackendApiBaseUrl(); using var httpClient = new HttpClient(); - var requestUri = $"{backendApiBaseUrl}Tsc/StartTeslaOAuth?installationId={Uri.EscapeDataString(installationId.ToString())}&baseUrl={Uri.EscapeDataString(baseUrl)}"; + var requestUri = $"{backendApiBaseUrl}Client/AddAuthenticationStartInformation?redirectUri={Uri.EscapeDataString(baseUrl)}"; var responseString = await httpClient.GetStringAsync(requestUri).ConfigureAwait(false); var oAuthRequestInformation = JsonConvert.DeserializeObject(responseString) ?? throw new InvalidDataException("Could not get oAuth data"); var requestUrl = GenerateAuthUrl(oAuthRequestInformation, locale); @@ -73,6 +71,89 @@ public async Task> StartTeslaOAuth(string locale, string baseUr return new DtoValue(requestUrl); } + public async Task GenerateUserAccount() + { + logger.LogTrace("{method}()", nameof(GenerateUserAccount)); + var userEmail = await tscConfigurationService.GetConfigurationValueByKey(constants.EmailConfigurationKey); + var password = passwordGenerationService.GeneratePassword(configurationWrapper.BackendPasswordDefaultLength()); + var installationId = await tscConfigurationService.GetInstallationId().ConfigureAwait(false); + var dtoCreateUser = new DtoCreateUser(installationId.ToString(), password) { Email = userEmail, }; + var url = configurationWrapper.BackendApiBaseUrl() + "User/Create"; + using var httpClient = new HttpClient(); + httpClient.Timeout = TimeSpan.FromSeconds(10); + var response = await httpClient.PostAsJsonAsync(url, dtoCreateUser).ConfigureAwait(false); + if (!response.IsSuccessStatusCode) + { + var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + logger.LogError("Could not create user account. StatusCode: {statusCode}, resultBody: {resultBody}", response.StatusCode, responseString); + throw new InvalidOperationException("Could not create user account"); + } + await tscConfigurationService.SetConfigurationValueByKey(constants.BackendPasswordConfigurationKey, password); + } + + public async Task GetOrRefreshBackendToken() + { + logger.LogTrace("{method}()", nameof(GetOrRefreshBackendToken)); + var token = await teslaSolarChargerContext.BackendTokens.SingleOrDefaultAsync(); + if (token != default) + { + if (token.ExpiresAtUtc > dateTimeProvider.DateTimeOffSetUtcNow()) + { + return; + } + logger.LogInformation("Backend Token expired. Refresh token..."); + await RefreshBackendToken(token); + await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); + logger.LogInformation("Backend Token refreshed"); + return; + } + var backendPassword = await tscConfigurationService.GetConfigurationValueByKey(constants.BackendPasswordConfigurationKey).ConfigureAwait(false); + if (string.IsNullOrEmpty(backendPassword)) + { + return; + } + var installationId = await tscConfigurationService.GetInstallationId().ConfigureAwait(false); + var dtoLogin = new DtoLogin(installationId.ToString(), backendPassword); + var url = configurationWrapper.BackendApiBaseUrl() + "User/Login"; + using var httpClient = new HttpClient(); + httpClient.Timeout = TimeSpan.FromSeconds(10); + var response = await httpClient.PostAsJsonAsync(url, dtoLogin).ConfigureAwait(false); + if (!response.IsSuccessStatusCode) + { + var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + logger.LogError("Could not login to backend. StatusCode: {statusCode}, resultBody: {resultBody}", response.StatusCode, responseString); + throw new InvalidOperationException("Could not login to backend"); + } + var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var newToken = JsonConvert.DeserializeObject(responseContent) ?? throw new InvalidDataException("Could not parse token"); + token = new(newToken.AccessToken, newToken.RefreshToken) + { + ExpiresAtUtc = DateTimeOffset.FromUnixTimeSeconds(newToken.ExpiresAt), + }; + teslaSolarChargerContext.BackendTokens.Add(token); + await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); + } + + private async Task RefreshBackendToken(BackendToken token) + { + logger.LogTrace("{method}(token)", nameof(RefreshBackendToken)); + var url = configurationWrapper.BackendApiBaseUrl() + "User/RefreshToken"; + var dtoRefreshToken = new DtoTokenRefreshModel(token.AccessToken, token.RefreshToken); + using var httpClient = new HttpClient(); + httpClient.Timeout = TimeSpan.FromSeconds(10); + var response = await httpClient.PostAsJsonAsync(url, dtoRefreshToken).ConfigureAwait(false); + if (!response.IsSuccessStatusCode) + { + var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + logger.LogError("Could not refresh backend token. StatusCode: {statusCode}, resultBody: {resultBody}", response.StatusCode, responseString); + throw new InvalidOperationException("Could not refresh backend token"); + } + var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var newToken = JsonConvert.DeserializeObject(responseContent) ?? throw new InvalidDataException("Could not parse token"); + token.AccessToken = newToken.AccessToken; + token.RefreshToken = newToken.RefreshToken; + } + internal string GenerateAuthUrl(DtoTeslaOAuthRequestInformation oAuthInformation, string locale) { logger.LogTrace("{method}({@oAuthInformation})", nameof(GenerateAuthUrl), oAuthInformation); @@ -141,62 +222,6 @@ public async Task PostErrorInformation(string source, string methodName, string return Task.FromResult(fileVersionInfo.ProductVersion); } - public async Task PostTeslaApiCallStatistics() - { - logger.LogTrace("{method}()", nameof(PostTeslaApiCallStatistics)); - var shouldTransferDate = configurationWrapper.SendTeslaApiStatsToBackend(); - var currentDate = dateTimeProvider.UtcNow().Date; - if (!shouldTransferDate) - { - logger.LogWarning("You manually disabled tesla API stats transfer to the backend. This means your usage won't be considered in future optimizations."); - return; - } - - Func predicate = d => d > (currentDate.AddDays(-1)) && (d < currentDate); - var cars = settings.Cars.Where(c => c.WakeUpCalls.Count(predicate) > 0 - || c.VehicleDataCalls.Count(predicate) > 0 - || c.VehicleCalls.Count(predicate) > 0 - || c.ChargeStartCalls.Count(predicate) > 0 - || c.ChargeStopCalls.Count(predicate) > 0 - || c.SetChargingAmpsCall.Count(predicate) > 0 - || c.OtherCommandCalls.Count(predicate) > 0).ToList(); - - var getVehicleDataFromTesla = configurationWrapper.GetVehicleDataFromTesla(); - foreach (var car in cars) - { - var statistics = new DtoTeslaApiCallStatistic - { - Date = DateOnly.FromDateTime(currentDate.AddDays(-1)), - InstallationId = await tscConfigurationService.GetInstallationId().ConfigureAwait(false), - StartupTime = settings.StartupTime, - GetDataFromTesla = getVehicleDataFromTesla, - ApiRefreshInterval = (int) configurationWrapper.FleetApiRefreshInterval().TotalSeconds, - UseBle = car.UseBle, - Vin = car.Vin, - WakeUpCalls = car.WakeUpCalls.Where(predicate).ToList(), - VehicleDataCalls = car.VehicleDataCalls.Where(predicate).ToList(), - VehicleCalls = car.VehicleCalls.Where(predicate).ToList(), - ChargeStartCalls = car.ChargeStartCalls.Where(predicate).ToList(), - ChargeStopCalls = car.ChargeStopCalls.Where(predicate).ToList(), - SetChargingAmpsCall = car.SetChargingAmpsCall.Where(predicate).ToList(), - OtherCommandCalls = car.OtherCommandCalls.Where(predicate).ToList(), - }; - var url = configurationWrapper.BackendApiBaseUrl() + "Tsc/NotifyTeslaApiCallStatistics"; - try - { - using var httpClient = new HttpClient(); - httpClient.Timeout = TimeSpan.FromSeconds(10); - var response = await httpClient.PostAsJsonAsync(url, statistics).ConfigureAwait(false); - } - catch (Exception e) - { - logger.LogError(e, "Could not post tesla api call statistics"); - } - - } - - } - public async Task GetNewBackendNotifications() { logger.LogTrace("{method}()", nameof(GetNewBackendNotifications)); diff --git a/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs b/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs index 866eb8210..31ed50bc4 100644 --- a/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs +++ b/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs @@ -8,6 +8,7 @@ public interface IBackendApiService Task PostInstallationInformation(string reason); Task PostErrorInformation(string source, string methodName, string message, string issueKey, string? vin, string? stackTrace); Task GetCurrentVersion(); - Task PostTeslaApiCallStatistics(); Task GetNewBackendNotifications(); + Task GenerateUserAccount(); + Task GetOrRefreshBackendToken(); } diff --git a/TeslaSolarCharger/Server/Services/Contracts/IPasswordGenerationService.cs b/TeslaSolarCharger/Server/Services/Contracts/IPasswordGenerationService.cs new file mode 100644 index 000000000..f8b782efd --- /dev/null +++ b/TeslaSolarCharger/Server/Services/Contracts/IPasswordGenerationService.cs @@ -0,0 +1,6 @@ +namespace TeslaSolarCharger.Server.Services.Contracts; + +public interface IPasswordGenerationService +{ + string GeneratePassword(int length); +} diff --git a/TeslaSolarCharger/Server/Services/Contracts/ITscConfigurationService.cs b/TeslaSolarCharger/Server/Services/Contracts/ITscConfigurationService.cs index 55e694179..406d26008 100644 --- a/TeslaSolarCharger/Server/Services/Contracts/ITscConfigurationService.cs +++ b/TeslaSolarCharger/Server/Services/Contracts/ITscConfigurationService.cs @@ -3,4 +3,19 @@ public interface ITscConfigurationService { Task GetInstallationId(); + + /// + /// Get a configuration value by its key + /// + /// Configuration key to get the value from + /// Configuration value, null if value key does not exist in database + Task GetConfigurationValueByKey(string configurationKey); + + /// + /// Set a configuration value by its key. If the key does not exist, it will be created. If it exists, the value will be updated. + /// + /// Key to update + /// Value to update + /// + Task SetConfigurationValueByKey(string configurationKey, string configurationValue); } diff --git a/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs b/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs index 4a2723c47..efac4b405 100644 --- a/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs +++ b/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs @@ -120,7 +120,7 @@ private async Task ConnectToFleetTelemetryApi(string vin, bool useFleetTelemetry var currentDate = dateTimeProvider.UtcNow(); var scope = serviceProvider.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); - var token = await context.TeslaTokens + var token = await context.BackendTokens .Where(t => t.ExpiresAtUtc > currentDate) .OrderByDescending(t => t.ExpiresAtUtc) .FirstOrDefaultAsync().ConfigureAwait(false); diff --git a/TeslaSolarCharger/Server/Services/PasswordGenerationService.cs b/TeslaSolarCharger/Server/Services/PasswordGenerationService.cs new file mode 100644 index 000000000..1ba90cebf --- /dev/null +++ b/TeslaSolarCharger/Server/Services/PasswordGenerationService.cs @@ -0,0 +1,26 @@ +using System.Security.Cryptography; +using System.Text; +using TeslaSolarCharger.Server.Services.Contracts; + +namespace TeslaSolarCharger.Server.Services; + +public class PasswordGenerationService : IPasswordGenerationService +{ + private const string CharPool = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{};:'\",.\\/?`~"; + + + public string GeneratePassword(int length) + { + using var rng = RandomNumberGenerator.Create(); + var password = new StringBuilder(); + + for (var i = 0; i < length; i++) + { + var randomNumber = new byte[1]; + rng.GetBytes(randomNumber); + var charIndex = randomNumber[0] % CharPool.Length; + password.Append(CharPool[charIndex]); + } + return password.ToString(); + } +} diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index 4557d94d4..0cbbb7cc9 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -65,12 +65,6 @@ public class TeslaFleetApiService( BleCompatible = true, TeslaApiRequestType = TeslaApiRequestType.Command, }; - private DtoFleetApiRequest SetScheduledChargingRequest => new() - { - RequestUrl = constants.SetScheduledChargingRequestUrl, - NeedsProxy = true, - TeslaApiRequestType = TeslaApiRequestType.Command, - }; private DtoFleetApiRequest SetChargeLimitRequest => new() { RequestUrl = constants.SetChargeLimitRequestUrl, @@ -164,27 +158,6 @@ public async Task SetAmp(int carId, int amps) car.LastSetAmp = amps; } - public async Task SetScheduledCharging(int carId, DateTimeOffset? chargingStartTime) - { - logger.LogTrace("{method}({param1}, {param2})", nameof(SetScheduledCharging), carId, chargingStartTime); - var vin = GetVinByCarId(carId); - var car = settings.Cars.First(c => c.Id == carId); - if (!IsChargingScheduleChangeNeeded(chargingStartTime, dateTimeProvider.DateTimeOffSetNow(), car, out var parameters)) - { - logger.LogDebug("No change in updating scheduled charging needed."); - return; - } - - await WakeUpCarIfNeeded(carId, car.State).ConfigureAwait(false); - - var result = await SendCommandToTeslaApi(vin, SetScheduledChargingRequest, HttpMethod.Post, JsonConvert.SerializeObject(parameters)).ConfigureAwait(false); - //assume update was sucessfull as update is not working after mosquitto restart (or wrong cached State) - if (parameters["enable"] == "false") - { - car.ScheduledChargingStartTime = null; - } - } - public async Task SetChargeLimit(int carId, int limitSoC) { logger.LogTrace("{method}({param1}, {param2})", nameof(SetChargeLimit), carId, limitSoC); @@ -771,26 +744,28 @@ internal DateTimeOffset RoundToNextQuarterHour(DateTimeOffset chargingStartTime) private async Task WakeUpCarIfNeeded(int carId, CarStateEnum? carState) { - if (carState is CarStateEnum.Asleep or CarStateEnum.Offline or CarStateEnum.Suspended) + if (carState is not (CarStateEnum.Asleep or CarStateEnum.Offline or CarStateEnum.Suspended)) { - var car = settings.Cars.First(c => c.Id == carId); - switch (carState) - { - case CarStateEnum.Offline or CarStateEnum.Asleep: - logger.LogInformation("Wakeup car."); - await WakeUpCar(carId).ConfigureAwait(false); - break; - case CarStateEnum.Suspended: - logger.LogInformation("Resume logging as is suspended"); - if (car.TeslaMateCarId != default) - { - //ToDo: fix with https://github.com/pkuehnel/TeslaSolarCharger/issues/1511 - //await teslamateApiService.ResumeLogging(car.TeslaMateCarId.Value).ConfigureAwait(false); - } - break; - } + return; + } + + var car = settings.Cars.First(c => c.Id == carId); + switch (carState) + { + case CarStateEnum.Offline or CarStateEnum.Asleep: + logger.LogInformation("Wakeup car."); + await WakeUpCar(carId).ConfigureAwait(false); + break; + case CarStateEnum.Suspended: + logger.LogInformation("Resume logging as is suspended"); + if (car.TeslaMateCarId != default) + { + //ToDo: fix with https://github.com/pkuehnel/TeslaSolarCharger/issues/1511 + //await teslamateApiService.ResumeLogging(car.TeslaMateCarId.Value).ConfigureAwait(false); + } + + break; } - } private async Task?> SendCommandToTeslaApi(string vin, DtoFleetApiRequest fleetApiRequest, HttpMethod httpMethod, string contentData = "{}", int? amp = null) where T : class @@ -898,13 +873,18 @@ await errorHandlingService.HandleError(nameof(TeslaFleetApiService), nameof(Send } await errorHandlingService.HandleErrorResolved(issueKeys.CarRateLimited, car.Vin); var fleetApiProxyRequired = await IsFleetApiProxyEnabled(vin).ConfigureAwait(false); - var baseUrl = GetFleetApiBaseUrl(accessToken.Region, fleetApiRequest.NeedsProxy, fleetApiProxyRequired.Value); - var requestUri = $"{baseUrl}api/1/vehicles/{vin}/{fleetApiRequest.RequestUrl}"; + var baseUrl = configurationWrapper.BackendApiBaseUrl(); + var requestUri = $"{baseUrl}{fleetApiRequest.RequestUrl}?encryptionKey={accessToken.EncryptionKey}&vin={vin}&carRequiresProxy={fleetApiProxyRequired.Value}"; + if (fleetApiRequest.RequestUrl == SetChargingAmpsRequest.RequestUrl) + { + requestUri += $"&s={amp}"; + } + //ToDo: add set charge limit request parameter var request = new HttpRequestMessage() { Content = content, RequestUri = new Uri(requestUri), - Method = httpMethod, + Method = HttpMethod.Post, }; var response = await httpClient.SendAsync(request).ConfigureAwait(false); var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); @@ -1238,7 +1218,7 @@ public async Task AddNewTokenAsync(DtoTeslaTscDeliveryToken token) var currentTokens = await teslaSolarChargerContext.TeslaTokens.ToListAsync().ConfigureAwait(false); teslaSolarChargerContext.TeslaTokens.RemoveRange(currentTokens); await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); - teslaSolarChargerContext.TeslaTokens.Add(new TeslaToken + teslaSolarChargerContext.TeslaTokens.Add(new BackendToken { AccessToken = token.AccessToken, RefreshToken = token.RefreshToken, @@ -1258,7 +1238,7 @@ public async Task> GetFleetApiTokenState() - private async Task GetAccessToken() + private async Task GetAccessToken() { logger.LogTrace("{method}()", nameof(GetAccessToken)); var token = await teslaSolarChargerContext.TeslaTokens @@ -1272,7 +1252,7 @@ public async Task> GetFleetApiTokenState() return token; } - private async Task HandleNonSuccessTeslaApiStatusCodes(HttpStatusCode statusCode, TeslaToken token, + private async Task HandleNonSuccessTeslaApiStatusCodes(HttpStatusCode statusCode, BackendToken token, string responseString, TeslaApiRequestType teslaApiRequestType, string? vin = null) { logger.LogTrace("{method}({statusCode}, {token}, {responseString})", nameof(HandleNonSuccessTeslaApiStatusCodes), statusCode, token, responseString); diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs index 322983422..c5f36aa46 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs @@ -30,7 +30,7 @@ public async Task GetFleetApiTokenState() { return FleetApiTokenState.MissingScopes; } - var token = await teslaSolarChargerContext.TeslaTokens.FirstOrDefaultAsync().ConfigureAwait(false); + var token = await teslaSolarChargerContext.BackendTokens.FirstOrDefaultAsync().ConfigureAwait(false); if (token != null) { if (token.UnauthorizedCounter > constants.MaxTokenUnauthorizedCount) diff --git a/TeslaSolarCharger/Server/Services/TscConfigurationService.cs b/TeslaSolarCharger/Server/Services/TscConfigurationService.cs index 6e3d27cd2..f5fc88aef 100644 --- a/TeslaSolarCharger/Server/Services/TscConfigurationService.cs +++ b/TeslaSolarCharger/Server/Services/TscConfigurationService.cs @@ -1,32 +1,22 @@ -using TeslaSolarCharger.Model.Contracts; +using Microsoft.EntityFrameworkCore; +using TeslaSolarCharger.Model.Contracts; using TeslaSolarCharger.Model.Entities.TeslaSolarCharger; -using TeslaSolarCharger.Server.Contracts; -using TeslaSolarCharger.Server.Dtos.TscBackend; using TeslaSolarCharger.Server.Services.Contracts; -using TeslaSolarCharger.Shared.Contracts; using TeslaSolarCharger.Shared.Resources.Contracts; -using TeslaSolarCharger.SharedBackend.Contracts; namespace TeslaSolarCharger.Server.Services; -public class TscConfigurationService : ITscConfigurationService +public class TscConfigurationService( + ILogger logger, + ITeslaSolarChargerContext teslaSolarChargerContext, + IConstants constants) + : ITscConfigurationService { - private readonly ILogger _logger; - private readonly ITeslaSolarChargerContext _teslaSolarChargerContext; - private readonly IConstants _constants; - - public TscConfigurationService(ILogger logger, ITeslaSolarChargerContext teslaSolarChargerContext, IConstants constants) - { - _logger = logger; - _teslaSolarChargerContext = teslaSolarChargerContext; - _constants = constants; - } - public async Task GetInstallationId() { - _logger.LogTrace("{method}()", nameof(GetInstallationId)); - var configurationIdString = _teslaSolarChargerContext.TscConfigurations - .Where(c => c.Key == _constants.InstallationIdKey) + logger.LogTrace("{method}()", nameof(GetInstallationId)); + var configurationIdString = teslaSolarChargerContext.TscConfigurations + .Where(c => c.Key == constants.InstallationIdKey) .Select(c => c.Value) .FirstOrDefault(); @@ -37,11 +27,54 @@ public async Task GetInstallationId() var installationIdConfiguration = new TscConfiguration() { - Key = _constants.InstallationIdKey, + Key = constants.InstallationIdKey, Value = Guid.NewGuid().ToString(), }; - _teslaSolarChargerContext.TscConfigurations.Add(installationIdConfiguration); - await _teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); + teslaSolarChargerContext.TscConfigurations.Add(installationIdConfiguration); + await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); return Guid.Parse(installationIdConfiguration.Value); } + + /// + /// Get a configuration value by its key + /// + /// Configuration key to get the value from + /// Configuration value, null if value key does not exist in database + public async Task GetConfigurationValueByKey(string configurationKey) + { + logger.LogTrace("{method}({configurationKey})", nameof(GetConfigurationValueByKey), configurationKey); + var configurationValue = await teslaSolarChargerContext.TscConfigurations + .FirstOrDefaultAsync(c => c.Key == constants.InstallationIdKey); + + return configurationValue?.Value; + } + + /// + /// Set a configuration value by its key. If the key does not exist, it will be created. If it exists, the value will be updated. + /// + /// Key to update + /// Value to update + /// + public async Task SetConfigurationValueByKey(string configurationKey, string configurationValue) + { + logger.LogTrace("{method}({configurationKey}, {configurationValue})", nameof(SetConfigurationValueByKey), configurationKey, configurationValue); + var configuration = await teslaSolarChargerContext.TscConfigurations + .FirstOrDefaultAsync(c => c.Key == configurationKey); + + if (configuration == default) + { + configuration = new() + { + Key = configurationKey, + Value = configurationValue, + }; + teslaSolarChargerContext.TscConfigurations.Add(configuration); + } + else + { + configuration.Value = configurationValue; + } + + await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); + } } diff --git a/TeslaSolarCharger/Server/appsettings.json b/TeslaSolarCharger/Server/appsettings.json index 4da2bbefa..e7b12f8fd 100644 --- a/TeslaSolarCharger/Server/appsettings.json +++ b/TeslaSolarCharger/Server/appsettings.json @@ -58,8 +58,7 @@ "GeoFence": "Home", "IgnoreSslErrors": false, "FleetApiClientId": "f29f71d6285a-4873-8b6b-80f15854892e", - "BackendApiBaseUrl": "https://www.teslasolarcharger.de/api/", - "TeslaFleetApiBaseUrl": "https://www.teslasolarcharger.de/teslaproxy/", + "BackendApiBaseUrl": "https://api.solar4car.com/api/", "UseFleetApiProxy": false, "LogLocationData": false, "SendTeslaApiStatsToBackend": true, @@ -73,7 +72,8 @@ "CarRefreshAfterCommandSeconds": 11, "BleUsageStopAfterErrorSeconds": 300, "FleetApiRefreshIntervalSeconds": 500, - "FleetTelemetryApiUrl": "wss://api.fleet-telemetry.teslasolarcharger.de/ws?", + "FleetTelemetryApiUrl": "wss://api.fleet-telemetry.solar4car.com/ws?", + "BackendPasswordDefaultLength": 25, "GridPriceProvider": { "EnergyProvider": "FixedPrice", "Octopus": { diff --git a/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs b/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs index a29cfd126..7bba962e0 100644 --- a/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs +++ b/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs @@ -88,7 +88,6 @@ public interface IConfigurationWrapper string BackendApiBaseUrl(); bool IsDevelopmentEnvironment(); string GetAwattarBaseUrl(); - string? GetFleetApiBaseUrl(); string RestoreTempDirectory(); string ConfigFileDirectory(); string AutoBackupsZipDirectory(); @@ -109,4 +108,5 @@ public interface IConfigurationWrapper string FleetTelemetryApiUrl(); bool AllowPowerBufferChangeOnHome(); TimeSpan FleetApiRefreshInterval(); + int BackendPasswordDefaultLength(); } diff --git a/TeslaSolarCharger/Shared/Resources/Constants.cs b/TeslaSolarCharger/Shared/Resources/Constants.cs index 67bcd926a..64f827287 100644 --- a/TeslaSolarCharger/Shared/Resources/Constants.cs +++ b/TeslaSolarCharger/Shared/Resources/Constants.cs @@ -32,16 +32,15 @@ public class Constants : IConstants public TimeSpan MinTokenRestLifetime => TimeSpan.FromMinutes(2); public int MaxTokenUnauthorizedCount => 5; public int ChargingDetailsAddTriggerEveryXSeconds => 59; - public string ChargeStartRequestUrl => "command/charge_start"; - public string ChargeStopRequestUrl => "command/charge_stop"; - public string SetChargingAmpsRequestUrl => "command/set_charging_amps"; - public string SetScheduledChargingRequestUrl => "command/set_scheduled_charging"; - public string SetChargeLimitRequestUrl => "command/set_charge_limit"; - public string SetSentryModeRequestUrl => "command/set_sentry_mode"; - public string FlashHeadlightsRequestUrl => "command/flash_lights"; + public string ChargeStartRequestUrl => "ChargeStart"; + public string ChargeStopRequestUrl => "ChargeStop"; + public string SetChargingAmpsRequestUrl => "SetChargingAmps"; + public string SetChargeLimitRequestUrl => "SetChargeLimit"; public string WakeUpRequestUrl => "wake_up"; public string VehicleRequestUrl => ""; public string VehicleDataRequestUrl => $"vehicle_data?endpoints={Uri.EscapeDataString("drive_state;location_data;vehicle_state;charge_state;climate_state")}"; + public string EmailConfigurationKey => "UserEmailAddress"; + public string BackendPasswordConfigurationKey => "BackendPassword"; public string GridPoleIcon => "power-pole"; } diff --git a/TeslaSolarCharger/Shared/Resources/Contracts/IConstants.cs b/TeslaSolarCharger/Shared/Resources/Contracts/IConstants.cs index cc131a389..088d4fbcf 100644 --- a/TeslaSolarCharger/Shared/Resources/Contracts/IConstants.cs +++ b/TeslaSolarCharger/Shared/Resources/Contracts/IConstants.cs @@ -34,11 +34,10 @@ public interface IConstants string ChargeStartRequestUrl { get; } string ChargeStopRequestUrl { get; } string SetChargingAmpsRequestUrl { get; } - string SetScheduledChargingRequestUrl { get; } string SetChargeLimitRequestUrl { get; } - string SetSentryModeRequestUrl { get; } - string FlashHeadlightsRequestUrl { get; } string WakeUpRequestUrl { get; } string VehicleRequestUrl { get; } string VehicleDataRequestUrl { get; } + public string EmailConfigurationKey { get; } + string BackendPasswordConfigurationKey { get; } } diff --git a/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs b/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs index b0addce06..aafb1e0df 100644 --- a/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs +++ b/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Newtonsoft.Json; +using System.Configuration; using TeslaSolarCharger.Shared.Contracts; using TeslaSolarCharger.Shared.Dtos.BaseConfiguration; using TeslaSolarCharger.Shared.Dtos.Contracts; @@ -91,6 +92,14 @@ public string BaseConfigFileFullName() return Path.Combine(configFileDirectory, value); } + public int BackendPasswordDefaultLength() + { + var environmentVariableName = "BackendPasswordDefaultLength"; + var value = configuration.GetValue(environmentVariableName); + logger.LogTrace("Config value extracted: [{key}]: {value}", environmentVariableName, value); + return value; + } + public string ConfigFileDirectory() { var environmentVariableName = "ConfigFileLocation"; @@ -101,13 +110,6 @@ public string ConfigFileDirectory() return path; } - public string? GetFleetApiBaseUrl() - { - var environmentVariableName = "TeslaFleetApiBaseUrl"; - var value = configuration.GetValue(environmentVariableName); - return value; - } - public string GetAwattarBaseUrl() { var environmentVariableName = "AwattarBaseUrl"; From ba4e0fbd5f7f772e153691d2c9925f81a710bd61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Thu, 26 Dec 2024 19:55:51 +0100 Subject: [PATCH 02/81] fic(chore): compiler errors --- .../Server/Controllers/ConfigController.cs | 4 - .../Server/Controllers/FleetApiController.cs | 3 - .../TscBackend/DtoTeslaTscDeliveryToken.cs | 12 - TeslaSolarCharger/Server/Program.cs | 8 - .../Contracts/ITeslaFleetApiService.cs | 4 - .../FleetTelemetryWebSocketService.cs | 13 +- .../Server/Services/TeslaFleetApiService.cs | 246 +++--------------- .../Services/TeslaFleetApiTokenHelper.cs | 9 +- .../Shared/Resources/Constants.cs | 6 +- .../Shared/Resources/Contracts/IConstants.cs | 2 + 10 files changed, 58 insertions(+), 249 deletions(-) delete mode 100644 TeslaSolarCharger/Server/Dtos/TscBackend/DtoTeslaTscDeliveryToken.cs diff --git a/TeslaSolarCharger/Server/Controllers/ConfigController.cs b/TeslaSolarCharger/Server/Controllers/ConfigController.cs index 644c05ac0..a894435e4 100644 --- a/TeslaSolarCharger/Server/Controllers/ConfigController.cs +++ b/TeslaSolarCharger/Server/Controllers/ConfigController.cs @@ -41,9 +41,5 @@ public ConfigController(IConfigJsonService configJsonService, ITeslaFleetApiServ [HttpPut] public Task UpdateCarBasicConfiguration(int carId, [FromBody] CarBasicConfiguration carBasicConfiguration) => _configJsonService.UpdateCarBasicConfiguration(carId, carBasicConfiguration); - - [HttpPost] - public Task AddTeslaFleetApiToken([FromBody] DtoTeslaTscDeliveryToken token) => - _teslaFleetApiService.AddNewTokenAsync(token); } } diff --git a/TeslaSolarCharger/Server/Controllers/FleetApiController.cs b/TeslaSolarCharger/Server/Controllers/FleetApiController.cs index 903472dbd..49101de38 100644 --- a/TeslaSolarCharger/Server/Controllers/FleetApiController.cs +++ b/TeslaSolarCharger/Server/Controllers/FleetApiController.cs @@ -25,9 +25,6 @@ public class FleetApiController( [HttpGet] public Task> GetOauthUrl(string locale, string baseUrl) => backendApiService.StartTeslaOAuth(locale, baseUrl); - [HttpGet] - public Task RefreshFleetApiToken() => fleetApiService.GetNewTokenFromBackend(); - /// /// Note: This endpoint is only available in development environment /// diff --git a/TeslaSolarCharger/Server/Dtos/TscBackend/DtoTeslaTscDeliveryToken.cs b/TeslaSolarCharger/Server/Dtos/TscBackend/DtoTeslaTscDeliveryToken.cs deleted file mode 100644 index 1d9facb72..000000000 --- a/TeslaSolarCharger/Server/Dtos/TscBackend/DtoTeslaTscDeliveryToken.cs +++ /dev/null @@ -1,12 +0,0 @@ -using TeslaSolarCharger.Model.Enums; - -namespace TeslaSolarCharger.Server.Dtos.TscBackend; - -public class DtoTeslaTscDeliveryToken -{ - public string AccessToken { get; set; } - public string RefreshToken { get; set; } - public string IdToken { get; set; } - public TeslaFleetApiRegion Region { get; set; } - public int ExpiresIn { get; set; } -} diff --git a/TeslaSolarCharger/Server/Program.cs b/TeslaSolarCharger/Server/Program.cs index 683dca424..0e1718191 100644 --- a/TeslaSolarCharger/Server/Program.cs +++ b/TeslaSolarCharger/Server/Program.cs @@ -97,14 +97,6 @@ async Task DoStartupStuff(WebApplication webApplication, ILogger logger var teslaFleetApiService = webApplication.Services.GetRequiredService(); await teslaFleetApiService.RefreshFleetApiRequestsAreAllowed(); - try - { - await teslaFleetApiService.RefreshTokensIfAllowedAndNeeded(); - } - catch(Exception ex) - { - logger1.LogError(ex, "Error refreshing Tesla tokens"); - } var shouldRetry = false; var baseConfiguration = await configurationWrapper.GetBaseConfigurationAsync(); diff --git a/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs index b9a5fbca4..16befeb1c 100644 --- a/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs @@ -1,5 +1,4 @@ using LanguageExt; -using TeslaSolarCharger.Server.Dtos.TscBackend; using TeslaSolarCharger.Shared.Dtos; using TeslaSolarCharger.Shared.Dtos.Car; using TeslaSolarCharger.Shared.Enums; @@ -8,13 +7,10 @@ namespace TeslaSolarCharger.Server.Services.Contracts; public interface ITeslaFleetApiService { - Task AddNewTokenAsync(DtoTeslaTscDeliveryToken token); Task> GetFleetApiTokenState(); - Task GetNewTokenFromBackend(); Task> TestFleetApiAccess(int carId); Task> IsFleetApiProxyEnabled(string vin); Task RefreshCarData(); - Task RefreshTokensIfAllowedAndNeeded(); Task RefreshFleetApiRequestsAreAllowed(); void ResetApiRequestCounters(); diff --git a/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs b/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs index efac4b405..53a443422 100644 --- a/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs +++ b/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs @@ -120,21 +120,18 @@ private async Task ConnectToFleetTelemetryApi(string vin, bool useFleetTelemetry var currentDate = dateTimeProvider.UtcNow(); var scope = serviceProvider.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); - var token = await context.BackendTokens - .Where(t => t.ExpiresAtUtc > currentDate) - .OrderByDescending(t => t.ExpiresAtUtc) - .FirstOrDefaultAsync().ConfigureAwait(false); - if (token == default) + var url = configurationWrapper.FleetTelemetryApiUrl() + + $"vin={vin}&forceReconfiguration=false&includeLocation={useFleetTelemetryForLocationData}"; + var authToken = await context.BackendTokens.AsNoTracking().SingleOrDefaultAsync(); + if(authToken == default) { logger.LogError("Can not connect to WebSocket: No token found for car {vin}", vin); return; } - var installationId = await tscConfigurationService.GetInstallationId().ConfigureAwait(false); - var url = configurationWrapper.FleetTelemetryApiUrl() + - $"teslaToken={token.AccessToken}®ion={token.Region}&vin={vin}&forceReconfiguration=false&includeLocation={useFleetTelemetryForLocationData}&installationId={installationId}"; using var client = new ClientWebSocket(); try { + client.Options.SetRequestHeader("Authorization", $"Bearer {authToken.AccessToken}"); await client.ConnectAsync(new Uri(url), new CancellationTokenSource(_heartbeatsendTimeout).Token).ConfigureAwait(false); var cancellation = new CancellationTokenSource(); var dtoClient = new DtoFleetTelemetryWebSocketClients diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index 0cbbb7cc9..69ef0190e 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -112,7 +112,7 @@ public async Task StartCharging(int carId, int startAmp, CarStateEnum? carState) var vin = GetVinByCarId(carId); await SetAmp(carId, startAmp).ConfigureAwait(false); - var result = await SendCommandToTeslaApi(vin, ChargeStartRequest, HttpMethod.Post).ConfigureAwait(false); + var result = await SendCommandToTeslaApi(vin, ChargeStartRequest).ConfigureAwait(false); } @@ -120,7 +120,7 @@ public async Task WakeUpCar(int carId) { logger.LogTrace("{method}({carId})", nameof(WakeUpCar), carId); var car = settings.Cars.First(c => c.Id == carId); - var result = await SendCommandToTeslaApi(car.Vin, WakeUpRequest, HttpMethod.Post).ConfigureAwait(false); + var result = await SendCommandToTeslaApi(car.Vin, WakeUpRequest).ConfigureAwait(false); if (car.TeslaMateCarId != default) { //ToDo: fix with https://github.com/pkuehnel/TeslaSolarCharger/issues/1511 @@ -140,7 +140,7 @@ public async Task StopCharging(int carId) } var vin = GetVinByCarId(carId); - var result = await SendCommandToTeslaApi(vin, ChargeStopRequest, HttpMethod.Post).ConfigureAwait(false); + var result = await SendCommandToTeslaApi(vin, ChargeStopRequest).ConfigureAwait(false); } public async Task SetAmp(int carId, int amps) @@ -154,7 +154,7 @@ public async Task SetAmp(int carId, int amps) } var vin = GetVinByCarId(carId); var commandData = $"{{\"charging_amps\":{amps}}}"; - var result = await SendCommandToTeslaApi(vin, SetChargingAmpsRequest, HttpMethod.Post, commandData, amps).ConfigureAwait(false); + var result = await SendCommandToTeslaApi(vin, SetChargingAmpsRequest, amps).ConfigureAwait(false); car.LastSetAmp = amps; } @@ -168,7 +168,7 @@ public async Task SetChargeLimit(int carId, int limitSoC) { { "percent", limitSoC }, }; - await SendCommandToTeslaApi(vin, SetChargeLimitRequest, HttpMethod.Post, JsonConvert.SerializeObject(parameters)).ConfigureAwait(false); + await SendCommandToTeslaApi(vin, SetChargeLimitRequest).ConfigureAwait(false); } public async Task> TestFleetApiAccess(int carId) @@ -181,7 +181,7 @@ public async Task> TestFleetApiAccess(int carId) await WakeUpCarIfNeeded(carId, inMemoryCar.State).ConfigureAwait(false); var amps = 7; var commandData = $"{{\"charging_amps\":{amps}}}"; - var result = await SendCommandToTeslaApi(vin, SetChargingAmpsRequest, HttpMethod.Post, commandData, amps).ConfigureAwait(false); + var result = await SendCommandToTeslaApi(vin, SetChargingAmpsRequest, amps).ConfigureAwait(false); var successResult = result?.Response?.Result == true; var car = teslaSolarChargerContext.Cars.First(c => c.Id == carId); car.TeslaFleetApiState = successResult ? TeslaCarFleetApiState.Ok : TeslaCarFleetApiState.NotWorking; @@ -224,7 +224,7 @@ public async Task RefreshCarData() } try { - var vehicle = await SendCommandToTeslaApi(car.Vin, VehicleRequest, HttpMethod.Get).ConfigureAwait(false); + var vehicle = await SendCommandToTeslaApi(car.Vin, VehicleRequest).ConfigureAwait(false); var vehicleResult = vehicle?.Response; logger.LogTrace("Got vehicle {@vehicle}", vehicle); if (vehicleResult == default) @@ -278,7 +278,7 @@ public async Task RefreshCarData() try { - var vehicleData = await SendCommandToTeslaApi(car.Vin, VehicleDataRequest, HttpMethod.Get) + var vehicleData = await SendCommandToTeslaApi(car.Vin, VehicleDataRequest) .ConfigureAwait(false); logger.LogTrace("Got vehicleData {@vehicleData}", vehicleData); var vehicleDataResult = vehicleData?.Response; @@ -768,9 +768,9 @@ private async Task WakeUpCarIfNeeded(int carId, CarStateEnum? carState) } } - private async Task?> SendCommandToTeslaApi(string vin, DtoFleetApiRequest fleetApiRequest, HttpMethod httpMethod, string contentData = "{}", int? amp = null) where T : class + private async Task?> SendCommandToTeslaApi(string vin, DtoFleetApiRequest fleetApiRequest, int? intParam = null) where T : class { - logger.LogTrace("{method}({vin}, {@fleetApiRequest}, {contentData})", nameof(SendCommandToTeslaApi), vin, fleetApiRequest, contentData); + logger.LogTrace("{method}({vin}, {@fleetApiRequest}, {intParam})", nameof(SendCommandToTeslaApi), vin, fleetApiRequest, intParam); var car = settings.Cars.First(c => c.Vin == vin); if (fleetApiRequest.BleCompatible) { @@ -799,7 +799,7 @@ private async Task WakeUpCarIfNeeded(int carId, CarStateEnum? carState) } else if (fleetApiRequest.RequestUrl == SetChargingAmpsRequest.RequestUrl) { - result = await bleService.SetAmp(vin, amp!.Value); + result = await bleService.SetAmp(vin, intParam!.Value); } else if (fleetApiRequest.RequestUrl == WakeUpRequest.RequestUrl) { @@ -853,7 +853,8 @@ await errorHandlingService.HandleError(nameof(TeslaFleetApiService), nameof(Send } } - var accessToken = await GetAccessToken().ConfigureAwait(false); + + var accessToken = await teslaSolarChargerContext.BackendTokens.SingleOrDefaultAsync(); if (accessToken == default) { logger.LogError("Access token not found do not send command"); @@ -861,7 +862,6 @@ await errorHandlingService.HandleError(nameof(TeslaFleetApiService), nameof(Send } using var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.AccessToken); - var content = new StringContent(contentData, System.Text.Encoding.UTF8, "application/json"); var rateLimitedUntil = await RateLimitedUntil(vin, fleetApiRequest.TeslaApiRequestType).ConfigureAwait(false); var currentDate = dateTimeProvider.UtcNow(); if (currentDate < rateLimitedUntil) @@ -874,16 +874,21 @@ await errorHandlingService.HandleError(nameof(TeslaFleetApiService), nameof(Send await errorHandlingService.HandleErrorResolved(issueKeys.CarRateLimited, car.Vin); var fleetApiProxyRequired = await IsFleetApiProxyEnabled(vin).ConfigureAwait(false); var baseUrl = configurationWrapper.BackendApiBaseUrl(); - var requestUri = $"{baseUrl}{fleetApiRequest.RequestUrl}?encryptionKey={accessToken.EncryptionKey}&vin={vin}&carRequiresProxy={fleetApiProxyRequired.Value}"; + var decryptionKey = await tscConfigurationService.GetConfigurationValueByKey(constants.TeslaTokenEncryptionKeyKey); + if (decryptionKey == default) + { + logger.LogError("Decryption key not found do not send command"); + return null; + } + var requestUri = $"{baseUrl}{fleetApiRequest.RequestUrl}?encryptionKey={decryptionKey}&vin={vin}&carRequiresProxy={fleetApiProxyRequired.Value}"; if (fleetApiRequest.RequestUrl == SetChargingAmpsRequest.RequestUrl) { - requestUri += $"&s={amp}"; + requestUri += $"&s={intParam}"; } //ToDo: add set charge limit request parameter var request = new HttpRequestMessage() { - Content = content, - RequestUri = new Uri(requestUri), + RequestUri = new(requestUri), Method = HttpMethod.Post, }; var response = await httpClient.SendAsync(request).ConfigureAwait(false); @@ -899,9 +904,9 @@ await errorHandlingService.HandleError(nameof(TeslaFleetApiService), nameof(Send else { await errorHandlingService.HandleError(nameof(TeslaFleetApiService), nameof(SendCommandToTeslaApi), $"Error while sending command to car {car.Vin}", - $"Sending command to Tesla API resulted in non succes status code: {response.StatusCode} : Command name:{fleetApiRequest.RequestUrl}, Content data:{contentData}. Response string: {responseString}", + $"Sending command to Tesla API resulted in non succes status code: {response.StatusCode} : Command name:{fleetApiRequest.RequestUrl}, Int Param:{intParam}. Response string: {responseString}", issueKeys.FleetApiNonSuccessStatusCode + fleetApiRequest.RequestUrl, car.Vin, null).ConfigureAwait(false); - logger.LogError("Sending command to Tesla API resulted in non succes status code: {statusCode} : Command name:{commandName}, Content data:{contentData}. Response string: {responseString}", response.StatusCode, fleetApiRequest.RequestUrl, contentData, responseString); + logger.LogError("Sending command to Tesla API resulted in non succes status code: {statusCode} : Command name:{commandName}, Int Param:{intParam}. Response string: {responseString}", response.StatusCode, fleetApiRequest.RequestUrl, intParam, responseString); await HandleNonSuccessTeslaApiStatusCodes(response.StatusCode, accessToken, responseString, fleetApiRequest.TeslaApiRequestType, vin).ConfigureAwait(false); if (response.StatusCode == HttpStatusCode.ServiceUnavailable) { @@ -916,10 +921,10 @@ await errorHandlingService.HandleError(nameof(TeslaFleetApiService), nameof(Send && !((fleetApiRequest.RequestUrl == ChargeStopRequest.RequestUrl) && responseString.Contains(IsNotChargingErrorMessage))) { await errorHandlingService.HandleError(nameof(TeslaFleetApiService), nameof(SendCommandToTeslaApi), $"Car {car.Vin} could not handle command", - $"Result of command request is false {fleetApiRequest.RequestUrl}, {contentData}. Response string: {responseString}", + $"Result of command request is false {fleetApiRequest.RequestUrl}, {intParam}. Response string: {responseString}", issueKeys.FleetApiNonSuccessResult + fleetApiRequest.RequestUrl, car.Vin, null) .ConfigureAwait(false); - logger.LogError("Result of command request is false {fleetApiRequest.RequestUrl}, {contentData}. Response string: {responseString}", fleetApiRequest.RequestUrl, contentData, responseString); + logger.LogError("Result of command request is false {fleetApiRequest.RequestUrl}, {intParam}. Response string: {responseString}", fleetApiRequest.RequestUrl, intParam, responseString); await HandleUnsignedCommands(vehicleCommandResult, vin).ConfigureAwait(false); } else @@ -969,10 +974,6 @@ private void AddRequestToCar(string vin, DtoFleetApiRequest fleetApiRequest) { car.SetChargingAmpsCall.Add(currentDate); } - else if (fleetApiRequest.RequestUrl == SetScheduledChargingRequest.RequestUrl) - { - car.OtherCommandCalls.Add(currentDate); - } else if (fleetApiRequest.RequestUrl == SetChargeLimitRequest.RequestUrl) { car.OtherCommandCalls.Add(currentDate); @@ -1044,143 +1045,6 @@ await errorHandlingService.HandleError(nameof(TeslaFleetApiService), nameof(Send } } - private string GetFleetApiBaseUrl(TeslaFleetApiRegion region, bool useProxyBaseUrl, bool fleetApiProxyRequired) - { - if (useProxyBaseUrl && fleetApiProxyRequired) - { - var configUrl = configurationWrapper.GetFleetApiBaseUrl(); - return configUrl ?? throw new KeyNotFoundException("Could not get Tesla HTTP proxy address"); - } - - if (region == TeslaFleetApiRegion.China) - { - return "https://fleet-api.prd.cn.vn.cloud.tesla.cn"; - } - var regionCode = region switch - { - TeslaFleetApiRegion.Emea => "eu", - TeslaFleetApiRegion.NorthAmerica => "na", - _ => throw new NotImplementedException($"Region {region} is not implemented."), - }; - return $"https://fleet-api.prd.{regionCode}.vn.cloud.tesla.com/"; - } - - /// - /// Get a new Token from TSC Backend - /// - /// True if a new Token was received - /// Token could not be extracted from result string - public async Task GetNewTokenFromBackend() - { - logger.LogTrace("{method}()", nameof(GetNewTokenFromBackend)); - //As all tokens get deleted when requesting a new one, we can assume that there is no token in the database. - var token = await teslaSolarChargerContext.TeslaTokens.FirstOrDefaultAsync().ConfigureAwait(false); - if (token != null) - { - return false; - } - - var tokenRequestedDate = await teslaFleetApiTokenHelper.GetTokenRequestedDate().ConfigureAwait(false); - if (tokenRequestedDate == null) - { - logger.LogError("Token has not been requested. Fleet API currently not working"); - return false; - } - if (tokenRequestedDate < dateTimeProvider.UtcNow().Subtract(constants.MaxTokenRequestWaitTime)) - { - logger.LogError("Last token request is too old. Request a new token."); - await errorHandlingService.HandleErrorResolved(issueKeys.FleetApiTokenNotReceived, null).ConfigureAwait(false); - return false; - } - using var httpClient = new HttpClient(); - var installationId = await tscConfigurationService.GetInstallationId().ConfigureAwait(false); - var url = configurationWrapper.BackendApiBaseUrl() + $"Tsc/DeliverAuthToken?installationId={installationId}"; - var response = await httpClient.GetAsync(url).ConfigureAwait(false); - var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - if (!response.IsSuccessStatusCode) - { - logger.LogError("Error getting token from TSC Backend. Response status code: {statusCode}, Response string: {responseString}", - response.StatusCode, responseString); - return false; - - } - - var newToken = JsonConvert.DeserializeObject(responseString) ?? throw new InvalidDataException("Could not get token from string."); - await AddNewTokenAsync(newToken).ConfigureAwait(false); - await errorHandlingService.HandleErrorResolved(issueKeys.FleetApiTokenNotReceived, null).ConfigureAwait(false); - return true; - } - - public async Task RefreshTokensIfAllowedAndNeeded() - { - logger.LogTrace("{method}()", nameof(RefreshTokensIfAllowedAndNeeded)); - var tokens = await teslaSolarChargerContext.TeslaTokens.ToListAsync().ConfigureAwait(false); - if (tokens.Count < 1) - { - logger.LogError("No token found. Cannot refresh token."); - return; - } - - var tokensToRefresh = tokens.Where(t => t.ExpiresAtUtc < (dateTimeProvider.UtcNow() + TimeSpan.FromMinutes(2))).ToList(); - if (tokensToRefresh.Count < 1) - { - logger.LogTrace("No token needs to be refreshed."); - return; - } - - foreach (var tokenToRefresh in tokensToRefresh) - { - logger.LogWarning("Token {tokenId} needs to be refreshed as it expires on {expirationDateTime}", tokenToRefresh.Id, tokenToRefresh.ExpiresAtUtc); - - //DO NOTE REMOVE *2: As normal requests could result in reaching max unauthorized count, the max value is higher here, so even if token is unauthorized, refreshing it is still tried a couple of times. - if (tokenToRefresh.UnauthorizedCounter > (constants.MaxTokenUnauthorizedCount * 2)) - { - logger.LogError("Token {tokenId} has been unauthorized too often. Do not refresh token.", tokenToRefresh.Id); - continue; - } - using var httpClient = new HttpClient(); - var tokenUrl = "https://auth.tesla.com/oauth2/v3/token"; - var requestData = new Dictionary - { - { "grant_type", "refresh_token" }, - { "client_id", configurationWrapper.FleetApiClientId() }, - { "refresh_token", tokenToRefresh.RefreshToken }, - }; - var encodedContent = new FormUrlEncodedContent(requestData); - encodedContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); - var response = await httpClient.PostAsync(tokenUrl, encodedContent).ConfigureAwait(false); - var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - if (response.IsSuccessStatusCode) - { - await errorHandlingService.HandleErrorResolved(issueKeys.FleetApiTokenRefreshNonSuccessStatusCode, null); - } - else - { - await errorHandlingService.HandleError(nameof(TeslaFleetApiService), nameof(SendCommandToTeslaApi), "Tesla Fleet API token could not be refreshed", - $"Refreshing token did result in non success status code. Response status code: {response.StatusCode} Response string: {responseString}", - issueKeys.FleetApiTokenRefreshNonSuccessStatusCode, null, null).ConfigureAwait(false); - logger.LogError("Refreshing token did result in non success status code. Response status code: {statusCode} Response string: {responseString}", response.StatusCode, responseString); - await HandleNonSuccessTeslaApiStatusCodes(response.StatusCode, tokenToRefresh, responseString, TeslaApiRequestType.Other).ConfigureAwait(false); - } - response.EnsureSuccessStatusCode(); - if (settings.AllowUnlimitedFleetApiRequests == false) - { - logger.LogError("Due to rate limitations fleet api requests are not allowed. As this version can not handle rate limits try updating to the latest version."); - teslaSolarChargerContext.TeslaTokens.Remove(tokenToRefresh); - await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); - return; - } - var newToken = JsonConvert.DeserializeObject(responseString) ?? throw new InvalidDataException("Could not get token from string."); - tokenToRefresh.AccessToken = newToken.AccessToken; - tokenToRefresh.RefreshToken = newToken.RefreshToken; - tokenToRefresh.IdToken = newToken.IdToken; - tokenToRefresh.ExpiresAtUtc = dateTimeProvider.UtcNow().AddSeconds(newToken.ExpiresIn); - tokenToRefresh.UnauthorizedCounter = 0; - await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); - logger.LogInformation("New Token saved to database."); - } - } - public async Task RefreshFleetApiRequestsAreAllowed() { logger.LogTrace("{method}()", nameof(RefreshFleetApiRequestsAreAllowed)); @@ -1213,54 +1077,21 @@ public async Task RefreshFleetApiRequestsAreAllowed() } - public async Task AddNewTokenAsync(DtoTeslaTscDeliveryToken token) - { - var currentTokens = await teslaSolarChargerContext.TeslaTokens.ToListAsync().ConfigureAwait(false); - teslaSolarChargerContext.TeslaTokens.RemoveRange(currentTokens); - await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); - teslaSolarChargerContext.TeslaTokens.Add(new BackendToken - { - AccessToken = token.AccessToken, - RefreshToken = token.RefreshToken, - IdToken = token.IdToken, - ExpiresAtUtc = dateTimeProvider.UtcNow().AddSeconds(token.ExpiresIn), - Region = token.Region, - UnauthorizedCounter = 0, - }); - await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); - } - public async Task> GetFleetApiTokenState() { var tokenState = await teslaFleetApiTokenHelper.GetFleetApiTokenState(); return new(tokenState); } - - - private async Task GetAccessToken() - { - logger.LogTrace("{method}()", nameof(GetAccessToken)); - var token = await teslaSolarChargerContext.TeslaTokens - .OrderByDescending(t => t.ExpiresAtUtc) - .FirstOrDefaultAsync().ConfigureAwait(false); - if (token != default && token.UnauthorizedCounter > constants.MaxTokenUnauthorizedCount) - { - logger.LogError("Token unauthorized counter is too high. Request a new token."); - throw new InvalidOperationException("Token unauthorized counter is too high. Request a new token."); - } - return token; - } - private async Task HandleNonSuccessTeslaApiStatusCodes(HttpStatusCode statusCode, BackendToken token, string responseString, TeslaApiRequestType teslaApiRequestType, string? vin = null) { logger.LogTrace("{method}({statusCode}, {token}, {responseString})", nameof(HandleNonSuccessTeslaApiStatusCodes), statusCode, token, responseString); if (statusCode == HttpStatusCode.Unauthorized) { + await tscConfigurationService.SetConfigurationValueByKey(constants.BackendTokenUnauthorizedKey, "true"); logger.LogError( - "Your token or refresh token is invalid. Very likely you have changed your Tesla password. Current unauthorized counter {unauthorizedCounter}, Should have been valid until: {expiresAt}, Response: {responseString}", - ++token.UnauthorizedCounter, token.ExpiresAtUtc, responseString); + "Your token or refresh token is invalid. Response: {responseString}", responseString); } else if (statusCode == HttpStatusCode.Forbidden) { @@ -1352,16 +1183,23 @@ public async Task>> GetNewCarsInAccount() public async Task>> GetAllCarsFromAccount() { logger.LogTrace("{method}()", nameof(GetAllCarsFromAccount)); - var accessToken = await GetAccessToken().ConfigureAwait(false); - if (accessToken == default || accessToken.ExpiresAtUtc < dateTimeProvider.UtcNow()) + var accessToken = await teslaSolarChargerContext.BackendTokens.SingleOrDefaultAsync(); + if (accessToken == default) { - logger.LogError("Can not add cars to TSC as no Tesla Token was found"); - return Fin>.Fail("No Tesla token found or existing token expired."); ; + logger.LogError("Can not add cars to TSC as no Backend Token was found"); + return Fin>.Fail("No Backend token found or existing token expired."); } - var baseUrl = GetFleetApiBaseUrl(accessToken.Region, false, false); using var httpClient = new HttpClient(); - httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.AccessToken); - var requestUri = $"{baseUrl}api/1/vehicles"; + httpClient.DefaultRequestHeaders.Authorization = new("Bearer", accessToken.AccessToken); + var baseUrl = configurationWrapper.BackendApiBaseUrl(); + var decryptionKey = await tscConfigurationService.GetConfigurationValueByKey(constants.TeslaTokenEncryptionKeyKey); + if (decryptionKey == default) + { + logger.LogError("Decryption key not found do not send command"); + return Fin>.Fail("No Decryption key found."); + } + var requestUri = $"{baseUrl}GetAllCarsFromAccount?encryptionKey={decryptionKey}"; + var request = new HttpRequestMessage() { RequestUri = new Uri(requestUri), diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs index c5f36aa46..444c53ee0 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs @@ -11,10 +11,10 @@ namespace TeslaSolarCharger.Server.Services; public class TeslaFleetApiTokenHelper(ILogger logger, ISettings settings, - IConfigurationWrapper configurationWrapper, ITeslaSolarChargerContext teslaSolarChargerContext, IConstants constants, - IDateTimeProvider dateTimeProvider) : ITeslaFleetApiTokenHelper + IDateTimeProvider dateTimeProvider, + ITscConfigurationService tscConfigurationService) : ITeslaFleetApiTokenHelper { public async Task GetFleetApiTokenState() { @@ -31,9 +31,10 @@ public async Task GetFleetApiTokenState() return FleetApiTokenState.MissingScopes; } var token = await teslaSolarChargerContext.BackendTokens.FirstOrDefaultAsync().ConfigureAwait(false); - if (token != null) + if (token != default) { - if (token.UnauthorizedCounter > constants.MaxTokenUnauthorizedCount) + var isTokenUnauthorized = string.Equals(await tscConfigurationService.GetConfigurationValueByKey(constants.BackendTokenUnauthorizedKey), "true", StringComparison.InvariantCultureIgnoreCase); + if (isTokenUnauthorized) { return FleetApiTokenState.TokenUnauthorized; } diff --git a/TeslaSolarCharger/Shared/Resources/Constants.cs b/TeslaSolarCharger/Shared/Resources/Constants.cs index 64f827287..4b1a884a0 100644 --- a/TeslaSolarCharger/Shared/Resources/Constants.cs +++ b/TeslaSolarCharger/Shared/Resources/Constants.cs @@ -39,8 +39,10 @@ public class Constants : IConstants public string WakeUpRequestUrl => "wake_up"; public string VehicleRequestUrl => ""; public string VehicleDataRequestUrl => $"vehicle_data?endpoints={Uri.EscapeDataString("drive_state;location_data;vehicle_state;charge_state;climate_state")}"; - public string EmailConfigurationKey => "UserEmailAddress"; - public string BackendPasswordConfigurationKey => "BackendPassword"; + public string EmailConfigurationKey => "EmailConfiguration"; + public string BackendPasswordConfigurationKey => "BackendPasswordConfiguration"; + public string TeslaTokenEncryptionKeyKey => "TeslaTokenEncryptionKey"; + public string BackendTokenUnauthorizedKey => "BackendTokenUnauthorized"; public string GridPoleIcon => "power-pole"; } diff --git a/TeslaSolarCharger/Shared/Resources/Contracts/IConstants.cs b/TeslaSolarCharger/Shared/Resources/Contracts/IConstants.cs index 088d4fbcf..e27e272dc 100644 --- a/TeslaSolarCharger/Shared/Resources/Contracts/IConstants.cs +++ b/TeslaSolarCharger/Shared/Resources/Contracts/IConstants.cs @@ -40,4 +40,6 @@ public interface IConstants string VehicleDataRequestUrl { get; } public string EmailConfigurationKey { get; } string BackendPasswordConfigurationKey { get; } + string TeslaTokenEncryptionKeyKey { get; } + string BackendTokenUnauthorizedKey { get; } } From 0ca3bc1cfb9c0f5268054ca782b69de2b9d0e5d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Thu, 26 Dec 2024 20:07:22 +0100 Subject: [PATCH 03/81] feat(TeslaFleetApiService): get cars from account via Solar4CarBackend --- .../Dtos/Solar4CarBackend/DtoTeslaResponse.cs | 9 +++++++ .../User/DtoAccessToken.cs | 2 +- .../User/DtoCreateUser.cs | 2 +- .../User/DtoLogin.cs | 2 +- .../User/DtoTokenRefreshModel.cs | 2 +- .../Server/Services/BackendApiService.cs | 2 +- .../Server/Services/TeslaFleetApiService.cs | 27 ++++++++++++++++--- .../Services/TeslaFleetApiTokenHelper.cs | 2 +- 8 files changed, 38 insertions(+), 10 deletions(-) create mode 100644 TeslaSolarCharger/Server/Dtos/Solar4CarBackend/DtoTeslaResponse.cs rename TeslaSolarCharger/Server/Dtos/{Solar2CarBackend => Solar4CarBackend}/User/DtoAccessToken.cs (78%) rename TeslaSolarCharger/Server/Dtos/{Solar2CarBackend => Solar4CarBackend}/User/DtoCreateUser.cs (76%) rename TeslaSolarCharger/Server/Dtos/{Solar2CarBackend => Solar4CarBackend}/User/DtoLogin.cs (72%) rename TeslaSolarCharger/Server/Dtos/{Solar2CarBackend => Solar4CarBackend}/User/DtoTokenRefreshModel.cs (75%) diff --git a/TeslaSolarCharger/Server/Dtos/Solar4CarBackend/DtoTeslaResponse.cs b/TeslaSolarCharger/Server/Dtos/Solar4CarBackend/DtoTeslaResponse.cs new file mode 100644 index 000000000..03e7c8191 --- /dev/null +++ b/TeslaSolarCharger/Server/Dtos/Solar4CarBackend/DtoTeslaResponse.cs @@ -0,0 +1,9 @@ +namespace TeslaSolarCharger.Server.Dtos.Solar4CarBackend; + +public class DtoTeslaResponse +{ + public int StatusCode { get; set; } + public string? JsonResponse { get; set; } + public string? Error { get; set; } + public string? ErrorDescription { get; set; } +} diff --git a/TeslaSolarCharger/Server/Dtos/Solar2CarBackend/User/DtoAccessToken.cs b/TeslaSolarCharger/Server/Dtos/Solar4CarBackend/User/DtoAccessToken.cs similarity index 78% rename from TeslaSolarCharger/Server/Dtos/Solar2CarBackend/User/DtoAccessToken.cs rename to TeslaSolarCharger/Server/Dtos/Solar4CarBackend/User/DtoAccessToken.cs index 18cf0c874..97a8c2db7 100644 --- a/TeslaSolarCharger/Server/Dtos/Solar2CarBackend/User/DtoAccessToken.cs +++ b/TeslaSolarCharger/Server/Dtos/Solar4CarBackend/User/DtoAccessToken.cs @@ -1,4 +1,4 @@ -namespace TeslaSolarCharger.Server.Dtos.Solar2CarBackend.User; +namespace TeslaSolarCharger.Server.Dtos.Solar4CarBackend.User; public class DtoAccessToken(string accessToken, string refreshToken) { diff --git a/TeslaSolarCharger/Server/Dtos/Solar2CarBackend/User/DtoCreateUser.cs b/TeslaSolarCharger/Server/Dtos/Solar4CarBackend/User/DtoCreateUser.cs similarity index 76% rename from TeslaSolarCharger/Server/Dtos/Solar2CarBackend/User/DtoCreateUser.cs rename to TeslaSolarCharger/Server/Dtos/Solar4CarBackend/User/DtoCreateUser.cs index dae46f7d5..562a81c34 100644 --- a/TeslaSolarCharger/Server/Dtos/Solar2CarBackend/User/DtoCreateUser.cs +++ b/TeslaSolarCharger/Server/Dtos/Solar4CarBackend/User/DtoCreateUser.cs @@ -1,4 +1,4 @@ -namespace TeslaSolarCharger.Server.Dtos.Solar2CarBackend.User; +namespace TeslaSolarCharger.Server.Dtos.Solar4CarBackend.User; public class DtoCreateUser(string userName, string password) { diff --git a/TeslaSolarCharger/Server/Dtos/Solar2CarBackend/User/DtoLogin.cs b/TeslaSolarCharger/Server/Dtos/Solar4CarBackend/User/DtoLogin.cs similarity index 72% rename from TeslaSolarCharger/Server/Dtos/Solar2CarBackend/User/DtoLogin.cs rename to TeslaSolarCharger/Server/Dtos/Solar4CarBackend/User/DtoLogin.cs index 249480d53..eb8047c19 100644 --- a/TeslaSolarCharger/Server/Dtos/Solar2CarBackend/User/DtoLogin.cs +++ b/TeslaSolarCharger/Server/Dtos/Solar4CarBackend/User/DtoLogin.cs @@ -1,4 +1,4 @@ -namespace TeslaSolarCharger.Server.Dtos.Solar2CarBackend.User; +namespace TeslaSolarCharger.Server.Dtos.Solar4CarBackend.User; public class DtoLogin(string userName, string password) { diff --git a/TeslaSolarCharger/Server/Dtos/Solar2CarBackend/User/DtoTokenRefreshModel.cs b/TeslaSolarCharger/Server/Dtos/Solar4CarBackend/User/DtoTokenRefreshModel.cs similarity index 75% rename from TeslaSolarCharger/Server/Dtos/Solar2CarBackend/User/DtoTokenRefreshModel.cs rename to TeslaSolarCharger/Server/Dtos/Solar4CarBackend/User/DtoTokenRefreshModel.cs index 87960e38e..8f39b7d59 100644 --- a/TeslaSolarCharger/Server/Dtos/Solar2CarBackend/User/DtoTokenRefreshModel.cs +++ b/TeslaSolarCharger/Server/Dtos/Solar4CarBackend/User/DtoTokenRefreshModel.cs @@ -1,4 +1,4 @@ -namespace TeslaSolarCharger.Server.Dtos.Solar2CarBackend.User; +namespace TeslaSolarCharger.Server.Dtos.Solar4CarBackend.User; public class DtoTokenRefreshModel(string accessToken, string refreshToken) { diff --git a/TeslaSolarCharger/Server/Services/BackendApiService.cs b/TeslaSolarCharger/Server/Services/BackendApiService.cs index de363e3d6..6c9a65945 100644 --- a/TeslaSolarCharger/Server/Services/BackendApiService.cs +++ b/TeslaSolarCharger/Server/Services/BackendApiService.cs @@ -4,7 +4,7 @@ using System.Reflection; using TeslaSolarCharger.Model.Contracts; using TeslaSolarCharger.Model.Entities.TeslaSolarCharger; -using TeslaSolarCharger.Server.Dtos.Solar2CarBackend.User; +using TeslaSolarCharger.Server.Dtos.Solar4CarBackend.User; using TeslaSolarCharger.Server.Dtos.TscBackend; using TeslaSolarCharger.Server.Resources.PossibleIssues.Contracts; using TeslaSolarCharger.Server.Services.Contracts; diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index 69ef0190e..b4afc270a 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -5,11 +5,10 @@ using System.Net.Http.Headers; using TeslaSolarCharger.Model.Contracts; using TeslaSolarCharger.Model.Entities.TeslaSolarCharger; -using TeslaSolarCharger.Model.Enums; using TeslaSolarCharger.Server.Contracts; using TeslaSolarCharger.Server.Dtos; +using TeslaSolarCharger.Server.Dtos.Solar4CarBackend; using TeslaSolarCharger.Server.Dtos.TeslaFleetApi; -using TeslaSolarCharger.Server.Dtos.TscBackend; using TeslaSolarCharger.Server.Resources.PossibleIssues.Contracts; using TeslaSolarCharger.Server.Services.Contracts; using TeslaSolarCharger.Shared.Contracts; @@ -1217,8 +1216,28 @@ public async Task>> GetAllCarsFromAccount() return Fin>.Fail(Error.New(excpetion)); } - - var vehicles = JsonConvert.DeserializeObject>>(responseBodyString); + var teslaBackendResult = JsonConvert.DeserializeObject(responseBodyString); + if (teslaBackendResult == null) + { + logger.LogError("Could not deserialize Solar4CarBackend response body {responseBodyString}", responseBodyString); + return Fin>.Fail($"Could not deserialize response body {responseBodyString}"); + } + + if (teslaBackendResult.StatusCode is >= 200 and < 300) + { + logger.LogError("Error while getting all cars from account due to communication issue between Solar4Car Backend and Tesla: Underlaying Status code: {statusCode}; Underlaying Response Body: {responseBodyString}", teslaBackendResult.StatusCode, teslaBackendResult.JsonResponse); + var excpetion = new HttpRequestException($"Requesting {requestUri} returned following body: {responseBodyString}", null, + response.StatusCode); + return Fin>.Fail(Error.New(excpetion)); + } + + if(string.IsNullOrWhiteSpace(teslaBackendResult.JsonResponse)) + { + logger.LogError("Empty Tesla JSON response body from Solar4Car Backend"); + return Fin>.Fail("Empty Tesla JSON response body from Solar4Car Backend"); + } + + var vehicles = JsonConvert.DeserializeObject>>(teslaBackendResult.JsonResponse); if (vehicles?.Response == null) { diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs index 444c53ee0..d295d2506 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs @@ -38,7 +38,7 @@ public async Task GetFleetApiTokenState() { return FleetApiTokenState.TokenUnauthorized; } - return (token.ExpiresAtUtc < dateTimeProvider.UtcNow() ? FleetApiTokenState.Expired : FleetApiTokenState.UpToDate); + return (token.ExpiresAtUtc < dateTimeProvider.DateTimeOffSetUtcNow().AddMinutes(2)) ? FleetApiTokenState.Expired : FleetApiTokenState.UpToDate; } var tokenRequestedDate = await GetTokenRequestedDate().ConfigureAwait(false); if (tokenRequestedDate == null) From 676a1d309f274ec6ab0b59a4e016c3866ed06416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Fri, 27 Dec 2024 10:30:54 +0100 Subject: [PATCH 04/81] feat(chore): create Backend user on fleet api token generation --- .../Client/Components/EditFormComponent.razor | 2 +- .../Dialogs/CreateBackendTokenDialog.razor | 36 +++++++++++++++++ .../Client/Helper/Contracts/IDialogHelper.cs | 5 ++- .../Helper/Contracts/IHttpClientHelper.cs | 4 +- .../Client/Helper/DialogHelper.cs | 14 +++++++ .../Client/Helper/HttpClientHelper.cs | 4 +- .../Client/Pages/BaseConfiguration.razor | 15 ++++++- TeslaSolarCharger/Client/Program.cs | 3 ++ .../Contracts/IFleetApiTokenCheckService.cs | 6 +++ .../Services/FleetApiTokenCheckService.cs | 22 +++++++++++ .../Controllers/BackendApiController.cs | 15 +++++++ .../Server/Services/BackendApiService.cs | 39 ++++++++++++++++++- .../Services/Contracts/IBackendApiService.cs | 3 +- 13 files changed, 159 insertions(+), 9 deletions(-) create mode 100644 TeslaSolarCharger/Client/Dialogs/CreateBackendTokenDialog.razor create mode 100644 TeslaSolarCharger/Client/Services/Contracts/IFleetApiTokenCheckService.cs create mode 100644 TeslaSolarCharger/Client/Services/FleetApiTokenCheckService.cs create mode 100644 TeslaSolarCharger/Server/Controllers/BackendApiController.cs diff --git a/TeslaSolarCharger/Client/Components/EditFormComponent.razor b/TeslaSolarCharger/Client/Components/EditFormComponent.razor index 59db6a23f..94068f158 100644 --- a/TeslaSolarCharger/Client/Components/EditFormComponent.razor +++ b/TeslaSolarCharger/Client/Components/EditFormComponent.razor @@ -12,7 +12,7 @@ Save } - + diff --git a/TeslaSolarCharger/Client/Dialogs/CreateBackendTokenDialog.razor b/TeslaSolarCharger/Client/Dialogs/CreateBackendTokenDialog.razor new file mode 100644 index 000000000..342a527d1 --- /dev/null +++ b/TeslaSolarCharger/Client/Dialogs/CreateBackendTokenDialog.razor @@ -0,0 +1,36 @@ +@using System.ComponentModel +@using TeslaSolarCharger.Client.Helper.Contracts +@using TeslaSolarCharger.Shared.Attributes +@using TeslaSolarCharger.Shared.Dtos + +@inject IHttpClientHelper HttpClientHelper + + + + +
ToDo: Link to privacy Policy
+
+ + Cancel + Yes + +
+ +@code { + [CascadingParameter] MudDialogInstance MudDialog { get; set; } + + [HelperText("While entering an E-Mail is not required it is HIGHLY!! recommended so I can contact you in case I detect anything not working")] + [DisplayName("E-Mail")] + public string EmailAddress { get; set; } = string.Empty; + + async Task Submit() + { + var result = await HttpClientHelper.SendPostRequestWithSnackbarAsync>($"api/BackendApi/GenerateUserAccount?emailAddress={EmailAddress}", null); + if (result is { Value: true }) + { + MudDialog.Close(DialogResult.Ok(true)); + } + } + + void Cancel() => MudDialog.Cancel(); +} \ No newline at end of file diff --git a/TeslaSolarCharger/Client/Helper/Contracts/IDialogHelper.cs b/TeslaSolarCharger/Client/Helper/Contracts/IDialogHelper.cs index 62ab7f021..e618a035a 100644 --- a/TeslaSolarCharger/Client/Helper/Contracts/IDialogHelper.cs +++ b/TeslaSolarCharger/Client/Helper/Contracts/IDialogHelper.cs @@ -1,6 +1,9 @@ -namespace TeslaSolarCharger.Client.Helper.Contracts; +using MudBlazor; + +namespace TeslaSolarCharger.Client.Helper.Contracts; public interface IDialogHelper { Task ShowTextDialog(string title, string dialogText); + Task ShowCreateBackendTokenDialog(); } diff --git a/TeslaSolarCharger/Client/Helper/Contracts/IHttpClientHelper.cs b/TeslaSolarCharger/Client/Helper/Contracts/IHttpClientHelper.cs index f7a6fb0c2..4d1df2f6f 100644 --- a/TeslaSolarCharger/Client/Helper/Contracts/IHttpClientHelper.cs +++ b/TeslaSolarCharger/Client/Helper/Contracts/IHttpClientHelper.cs @@ -4,6 +4,6 @@ public interface IHttpClientHelper { Task SendGetRequestWithSnackbarAsync(string url); Task SendGetRequestWithSnackbarAsync(string url); - Task SendPostRequestWithSnackbarAsync(string url, object content); - Task SendPostRequestWithSnackbarAsync(string url, object content); + Task SendPostRequestWithSnackbarAsync(string url, object? content); + Task SendPostRequestWithSnackbarAsync(string url, object? content); } diff --git a/TeslaSolarCharger/Client/Helper/DialogHelper.cs b/TeslaSolarCharger/Client/Helper/DialogHelper.cs index 46f08995c..125193ba6 100644 --- a/TeslaSolarCharger/Client/Helper/DialogHelper.cs +++ b/TeslaSolarCharger/Client/Helper/DialogHelper.cs @@ -20,4 +20,18 @@ public async Task ShowTextDialog(string title, string dialogText) var dialog = await dialogService.ShowAsync(title, parameters, options); var result = await dialog.Result; } + + public async Task ShowCreateBackendTokenDialog() + { + var options = new DialogOptions() + { + CloseButton = true, + CloseOnEscapeKey = true, + }; + var parameters = new DialogParameters + { + }; + var dialog = await dialogService.ShowAsync("Generate Backend Token", parameters, options); + return await dialog.Result; + } } diff --git a/TeslaSolarCharger/Client/Helper/HttpClientHelper.cs b/TeslaSolarCharger/Client/Helper/HttpClientHelper.cs index ad3faa572..1e46378aa 100644 --- a/TeslaSolarCharger/Client/Helper/HttpClientHelper.cs +++ b/TeslaSolarCharger/Client/Helper/HttpClientHelper.cs @@ -18,12 +18,12 @@ public async Task SendGetRequestWithSnackbarAsync(string url) await SendRequestWithSnackbarInternalAsync(HttpMethod.Get, url, null); } - public async Task SendPostRequestWithSnackbarAsync(string url, object content) + public async Task SendPostRequestWithSnackbarAsync(string url, object? content) { return await SendRequestWithSnackbarInternalAsync(HttpMethod.Post, url, content); } - public async Task SendPostRequestWithSnackbarAsync(string url, object content) + public async Task SendPostRequestWithSnackbarAsync(string url, object? content) { await SendRequestWithSnackbarInternalAsync(HttpMethod.Post, url, content); } diff --git a/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor b/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor index 8e19fbd9c..04f943977 100644 --- a/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor +++ b/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor @@ -1,14 +1,16 @@ @page "/BaseConfiguration" @using System.Globalization @using TeslaSolarCharger.Client.Helper.Contracts +@using TeslaSolarCharger.Client.Services.Contracts @using TeslaSolarCharger.Shared.Dtos.BaseConfiguration -@using TeslaSolarCharger.Shared @using TeslaSolarCharger.Shared.Enums @using TeslaSolarCharger.Shared.Dtos @inject HttpClient HttpClient @inject NavigationManager NavigationManager @inject ISnackbar Snackbar @inject IHttpClientHelper HttpClientHelper +@inject IFleetApiTokenCheckService FleetApiTokenCheckService +@inject IDialogHelper DialogHelper Base Configuration @@ -320,6 +322,17 @@ else _tokenGenerationButtonDisabled = true; var locale = CultureInfo.CurrentCulture.ToString(); var baseUrl = NavigationManager.BaseUri; + var hasValidBackendToken = await FleetApiTokenCheckService.HasValidBackendToken(); + if (!hasValidBackendToken) + { + var result = await DialogHelper.ShowCreateBackendTokenDialog(); + //ToDo: only continue on result == DialogResult.Ok() + // if (result != DialogResult.Ok()) + // { + + // } + } + var url = await HttpClient.GetFromJsonAsync>($"api/FleetApi/GetOauthUrl?locale={Uri.EscapeDataString(locale)}&baseUrl={Uri.EscapeDataString(baseUrl)}").ConfigureAwait(false); if (url?.Value != null) { diff --git a/TeslaSolarCharger/Client/Program.cs b/TeslaSolarCharger/Client/Program.cs index 6c2f2bbcb..7aef29c5a 100644 --- a/TeslaSolarCharger/Client/Program.cs +++ b/TeslaSolarCharger/Client/Program.cs @@ -8,6 +8,8 @@ using TeslaSolarCharger.Client; using TeslaSolarCharger.Client.Helper; using TeslaSolarCharger.Client.Helper.Contracts; +using TeslaSolarCharger.Client.Services; +using TeslaSolarCharger.Client.Services.Contracts; using TeslaSolarCharger.Shared; using TeslaSolarCharger.Shared.Contracts; using TeslaSolarCharger.Shared.Helper; @@ -25,6 +27,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddSharedDependencies(); builder.Services.AddMudServices(config => diff --git a/TeslaSolarCharger/Client/Services/Contracts/IFleetApiTokenCheckService.cs b/TeslaSolarCharger/Client/Services/Contracts/IFleetApiTokenCheckService.cs new file mode 100644 index 000000000..1e8823c09 --- /dev/null +++ b/TeslaSolarCharger/Client/Services/Contracts/IFleetApiTokenCheckService.cs @@ -0,0 +1,6 @@ +namespace TeslaSolarCharger.Client.Services.Contracts; + +public interface IFleetApiTokenCheckService +{ + Task HasValidBackendToken(); +} diff --git a/TeslaSolarCharger/Client/Services/FleetApiTokenCheckService.cs b/TeslaSolarCharger/Client/Services/FleetApiTokenCheckService.cs new file mode 100644 index 000000000..0d4e4247f --- /dev/null +++ b/TeslaSolarCharger/Client/Services/FleetApiTokenCheckService.cs @@ -0,0 +1,22 @@ +using TeslaSolarCharger.Client.Helper.Contracts; +using TeslaSolarCharger.Client.Services.Contracts; +using TeslaSolarCharger.Shared.Dtos; + +namespace TeslaSolarCharger.Client.Services; + +public class FleetApiTokenCheckService(ILogger logger, IHttpClientHelper httpClientHelper) : IFleetApiTokenCheckService +{ + public async Task HasValidBackendToken() + { + try + { + var response = await httpClientHelper.SendGetRequestWithSnackbarAsync>("api/BackendApi/HasValidBackendToken"); + return response?.Value ?? false; + } + catch (Exception ex) + { + logger.LogError(ex, "Error while checking backend token"); + return false; + } + } +} diff --git a/TeslaSolarCharger/Server/Controllers/BackendApiController.cs b/TeslaSolarCharger/Server/Controllers/BackendApiController.cs new file mode 100644 index 000000000..e5770c4e8 --- /dev/null +++ b/TeslaSolarCharger/Server/Controllers/BackendApiController.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Mvc; +using TeslaSolarCharger.Server.Services.Contracts; +using TeslaSolarCharger.Shared.Dtos; +using TeslaSolarCharger.SharedBackend.Abstracts; + +namespace TeslaSolarCharger.Server.Controllers; + +public class BackendApiController (IBackendApiService backendApiService) : ApiBaseController +{ + [HttpGet] + public Task> HasValidBackendToken() => backendApiService.HasValidBackendToken(); + + [HttpPost] + public Task> GenerateUserAccount(string emailAddress) => backendApiService.GenerateUserAccount(emailAddress); +} diff --git a/TeslaSolarCharger/Server/Services/BackendApiService.cs b/TeslaSolarCharger/Server/Services/BackendApiService.cs index 6c9a65945..b72794451 100644 --- a/TeslaSolarCharger/Server/Services/BackendApiService.cs +++ b/TeslaSolarCharger/Server/Services/BackendApiService.cs @@ -1,6 +1,8 @@ using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; using System.Diagnostics; +using System.Net; +using System.Net.Http.Headers; using System.Reflection; using TeslaSolarCharger.Model.Contracts; using TeslaSolarCharger.Model.Entities.TeslaSolarCharger; @@ -71,7 +73,15 @@ public async Task> StartTeslaOAuth(string locale, string baseUr return new DtoValue(requestUrl); } - public async Task GenerateUserAccount() + public async Task> GenerateUserAccount(string emailAddress) + { + logger.LogTrace("{method}({emailAddress})", nameof(GenerateUserAccount), emailAddress); + await tscConfigurationService.SetConfigurationValueByKey(constants.EmailConfigurationKey, emailAddress); + await GenerateUserAccount(); + return new(true); + } + + private async Task GenerateUserAccount() { logger.LogTrace("{method}()", nameof(GenerateUserAccount)); var userEmail = await tscConfigurationService.GetConfigurationValueByKey(constants.EmailConfigurationKey); @@ -134,6 +144,33 @@ public async Task GetOrRefreshBackendToken() await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); } + public async Task> HasValidBackendToken() + { + logger.LogTrace("{method}", nameof(HasValidBackendToken)); + var token = await teslaSolarChargerContext.BackendTokens.SingleOrDefaultAsync(); + if (token == default) + { + return new(false); + } + var url = configurationWrapper.BackendApiBaseUrl() + "Client/IsTokenValid"; + using var httpClient = new HttpClient(); + httpClient.Timeout = TimeSpan.FromSeconds(10); + var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.Authorization = new("Bearer", token.AccessToken); + var response = await httpClient.SendAsync(request).ConfigureAwait(false); + if (response.StatusCode == HttpStatusCode.Unauthorized) + { + return new(false); + } + if (!response.IsSuccessStatusCode) + { + var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + logger.LogError("Could not check if token is valid. StatusCode: {statusCode}, resultBody: {resultBody}", response.StatusCode, responseString); + throw new InvalidOperationException("Could not check if token is valid"); + } + return new(true); + } + private async Task RefreshBackendToken(BackendToken token) { logger.LogTrace("{method}(token)", nameof(RefreshBackendToken)); diff --git a/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs b/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs index 31ed50bc4..7d3630d6c 100644 --- a/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs +++ b/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs @@ -9,6 +9,7 @@ public interface IBackendApiService Task PostErrorInformation(string source, string methodName, string message, string issueKey, string? vin, string? stackTrace); Task GetCurrentVersion(); Task GetNewBackendNotifications(); - Task GenerateUserAccount(); + Task> GenerateUserAccount(string emailAddress); Task GetOrRefreshBackendToken(); + Task> HasValidBackendToken(); } From 7d636eb7ced02d710003a29b9d5134c05b533321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Fri, 27 Dec 2024 10:35:08 +0100 Subject: [PATCH 05/81] feat(EF): replace tesla tokens with backend tokens --- ...ceTeslaTokensWithBackendTokens.Designer.cs | 916 ++++++++++++++++++ ...212_ReplaceTeslaTokensWithBackendTokens.cs | 58 ++ .../TeslaSolarChargerContextModelSnapshot.cs | 54 +- 3 files changed, 996 insertions(+), 32 deletions(-) create mode 100644 TeslaSolarCharger.Model/Migrations/20241227093212_ReplaceTeslaTokensWithBackendTokens.Designer.cs create mode 100644 TeslaSolarCharger.Model/Migrations/20241227093212_ReplaceTeslaTokensWithBackendTokens.cs diff --git a/TeslaSolarCharger.Model/Migrations/20241227093212_ReplaceTeslaTokensWithBackendTokens.Designer.cs b/TeslaSolarCharger.Model/Migrations/20241227093212_ReplaceTeslaTokensWithBackendTokens.Designer.cs new file mode 100644 index 000000000..91767f9c7 --- /dev/null +++ b/TeslaSolarCharger.Model/Migrations/20241227093212_ReplaceTeslaTokensWithBackendTokens.Designer.cs @@ -0,0 +1,916 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TeslaSolarCharger.Model.EntityFramework; + +#nullable disable + +namespace TeslaSolarCharger.Model.Migrations +{ + [DbContext(typeof(TeslaSolarChargerContext))] + [Migration("20241227093212_ReplaceTeslaTokensWithBackendTokens")] + partial class ReplaceTeslaTokensWithBackendTokens + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.BackendNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BackendIssueId") + .HasColumnType("INTEGER"); + + b.Property("DetailText") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Headline") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsConfirmed") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("ValidFromDate") + .HasColumnType("TEXT"); + + b.Property("ValidFromVersion") + .HasColumnType("TEXT"); + + b.Property("ValidToDate") + .HasColumnType("TEXT"); + + b.Property("ValidToVersion") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("BackendNotifications"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.BackendToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExpiresAtUtc") + .HasColumnType("TEXT"); + + b.Property("RefreshToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("BackendTokens"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.CachedCarState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CarId") + .HasColumnType("INTEGER"); + + b.Property("CarStateJson") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastUpdated") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("CachedCarStates"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.Car", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BleApiBaseUrl") + .HasColumnType("TEXT"); + + b.Property("ChargeMode") + .HasColumnType("INTEGER"); + + b.Property("ChargeStartCalls") + .HasColumnType("TEXT"); + + b.Property("ChargeStopCalls") + .HasColumnType("TEXT"); + + b.Property("ChargerActualCurrent") + .HasColumnType("INTEGER"); + + b.Property("ChargerPhases") + .HasColumnType("INTEGER"); + + b.Property("ChargerPilotCurrent") + .HasColumnType("INTEGER"); + + b.Property("ChargerRequestedCurrent") + .HasColumnType("INTEGER"); + + b.Property("ChargerVoltage") + .HasColumnType("INTEGER"); + + b.Property("ChargingCommandsRateLimitedUntil") + .HasColumnType("TEXT"); + + b.Property("ChargingPriority") + .HasColumnType("INTEGER"); + + b.Property("ClimateOn") + .HasColumnType("INTEGER"); + + b.Property("CommandsRateLimitedUntil") + .HasColumnType("TEXT"); + + b.Property("IgnoreLatestTimeToReachSocDate") + .HasColumnType("INTEGER"); + + b.Property("IgnoreLatestTimeToReachSocDateOnWeekend") + .HasColumnType("INTEGER"); + + b.Property("IsAvailableInTeslaAccount") + .HasColumnType("INTEGER"); + + b.Property("LatestTimeToReachSoC") + .HasColumnType("TEXT"); + + b.Property("Latitude") + .HasColumnType("REAL"); + + b.Property("Longitude") + .HasColumnType("REAL"); + + b.Property("MaximumAmpere") + .HasColumnType("INTEGER"); + + b.Property("MinimumAmpere") + .HasColumnType("INTEGER"); + + b.Property("MinimumSoc") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OtherCommandCalls") + .HasColumnType("TEXT"); + + b.Property("PluggedIn") + .HasColumnType("INTEGER"); + + b.Property("SetChargingAmpsCall") + .HasColumnType("TEXT"); + + b.Property("ShouldBeManaged") + .HasColumnType("INTEGER"); + + b.Property("SoC") + .HasColumnType("INTEGER"); + + b.Property("SocLimit") + .HasColumnType("INTEGER"); + + b.Property("State") + .HasColumnType("INTEGER"); + + b.Property("TeslaFleetApiState") + .HasColumnType("INTEGER"); + + b.Property("TeslaMateCarId") + .HasColumnType("INTEGER"); + + b.Property("UsableEnergy") + .HasColumnType("INTEGER"); + + b.Property("UseBle") + .HasColumnType("INTEGER"); + + b.Property("UseFleetTelemetry") + .HasColumnType("INTEGER"); + + b.Property("UseFleetTelemetryForLocationData") + .HasColumnType("INTEGER"); + + b.Property("VehicleCalls") + .HasColumnType("TEXT"); + + b.Property("VehicleCommandProtocolRequired") + .HasColumnType("INTEGER"); + + b.Property("VehicleDataCalls") + .HasColumnType("TEXT"); + + b.Property("VehicleDataRateLimitedUntil") + .HasColumnType("TEXT"); + + b.Property("VehicleRateLimitedUntil") + .HasColumnType("TEXT"); + + b.Property("Vin") + .HasColumnType("TEXT"); + + b.Property("WakeUpCalls") + .HasColumnType("TEXT"); + + b.Property("WakeUpRateLimitedUntil") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("TeslaMateCarId") + .IsUnique(); + + b.HasIndex("Vin") + .IsUnique(); + + b.ToTable("Cars"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.CarValueLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BooleanValue") + .HasColumnType("INTEGER"); + + b.Property("CarId") + .HasColumnType("INTEGER"); + + b.Property("DoubleValue") + .HasColumnType("REAL"); + + b.Property("IntValue") + .HasColumnType("INTEGER"); + + b.Property("InvalidValue") + .HasColumnType("INTEGER"); + + b.Property("Source") + .HasColumnType("INTEGER"); + + b.Property("StringValue") + .HasColumnType("TEXT"); + + b.Property("Timestamp") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UnknownValue") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CarId"); + + b.ToTable("CarValueLogs"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ChargePrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddSpotPriceToGridPrice") + .HasColumnType("INTEGER"); + + b.Property("EnergyProvider") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(6); + + b.Property("EnergyProviderConfiguration") + .HasColumnType("TEXT"); + + b.Property("GridPrice") + .HasColumnType("TEXT"); + + b.Property("SolarPrice") + .HasColumnType("TEXT"); + + b.Property("SpotPriceCorrectionFactor") + .HasColumnType("TEXT"); + + b.Property("ValidSince") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ChargePrices"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ChargingDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChargerVoltage") + .HasColumnType("INTEGER"); + + b.Property("ChargingProcessId") + .HasColumnType("INTEGER"); + + b.Property("GridPower") + .HasColumnType("INTEGER"); + + b.Property("HomeBatteryPower") + .HasColumnType("INTEGER"); + + b.Property("SolarPower") + .HasColumnType("INTEGER"); + + b.Property("TimeStamp") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChargingProcessId"); + + b.ToTable("ChargingDetails"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ChargingProcess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CarId") + .HasColumnType("INTEGER"); + + b.Property("Cost") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("OldHandledChargeId") + .HasColumnType("INTEGER"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("UsedGridEnergyKwh") + .HasColumnType("TEXT"); + + b.Property("UsedHomeBatteryEnergyKwh") + .HasColumnType("TEXT"); + + b.Property("UsedSolarEnergyKwh") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CarId"); + + b.ToTable("ChargingProcesses"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.HandledCharge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AverageSpotPrice") + .HasColumnType("TEXT"); + + b.Property("CalculatedPrice") + .HasColumnType("TEXT"); + + b.Property("CarId") + .HasColumnType("INTEGER"); + + b.Property("ChargingProcessId") + .HasColumnType("INTEGER"); + + b.Property("UsedGridEnergy") + .HasColumnType("TEXT"); + + b.Property("UsedSolarEnergy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("HandledCharges"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.LoggedError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DismissedAt") + .HasColumnType("TEXT"); + + b.Property("EndTimeStamp") + .HasColumnType("TEXT"); + + b.Property("FurtherOccurrences") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Headline") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IssueKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Message") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("MethodName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Source") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StackTrace") + .HasColumnType("TEXT"); + + b.Property("StartTimeStamp") + .HasColumnType("TEXT"); + + b.Property("TelegramNotificationSent") + .HasColumnType("INTEGER"); + + b.Property("TelegramResolvedMessageSent") + .HasColumnType("INTEGER"); + + b.Property("Vin") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("LoggedErrors"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ModbusConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConnectDelayMilliseconds") + .HasColumnType("INTEGER"); + + b.Property("Endianess") + .HasColumnType("INTEGER"); + + b.Property("Host") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.Property("ReadTimeoutMilliseconds") + .HasColumnType("INTEGER"); + + b.Property("UnitIdentifier") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ModbusConfigurations"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ModbusResultConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Address") + .HasColumnType("INTEGER"); + + b.Property("BitStartIndex") + .HasColumnType("INTEGER"); + + b.Property("CorrectionFactor") + .HasColumnType("TEXT"); + + b.Property("InvertedByModbusResultConfigurationId") + .HasColumnType("INTEGER"); + + b.Property("Length") + .HasColumnType("INTEGER"); + + b.Property("ModbusConfigurationId") + .HasColumnType("INTEGER"); + + b.Property("Operator") + .HasColumnType("INTEGER"); + + b.Property("RegisterType") + .HasColumnType("INTEGER"); + + b.Property("UsedFor") + .HasColumnType("INTEGER"); + + b.Property("ValueType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InvertedByModbusResultConfigurationId"); + + b.HasIndex("ModbusConfigurationId"); + + b.ToTable("ModbusResultConfigurations"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.MqttConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Host") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Password") + .HasColumnType("TEXT"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MqttConfigurations"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.MqttResultConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CorrectionFactor") + .HasColumnType("TEXT"); + + b.Property("MqttConfigurationId") + .HasColumnType("INTEGER"); + + b.Property("NodePattern") + .HasColumnType("TEXT"); + + b.Property("NodePatternType") + .HasColumnType("INTEGER"); + + b.Property("Operator") + .HasColumnType("INTEGER"); + + b.Property("Topic") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UsedFor") + .HasColumnType("INTEGER"); + + b.Property("XmlAttributeHeaderName") + .HasColumnType("TEXT"); + + b.Property("XmlAttributeHeaderValue") + .HasColumnType("TEXT"); + + b.Property("XmlAttributeValueName") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MqttConfigurationId"); + + b.ToTable("MqttResultConfigurations"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.PowerDistribution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChargingPower") + .HasColumnType("INTEGER"); + + b.Property("GridProportion") + .HasColumnType("REAL"); + + b.Property("HandledChargeId") + .HasColumnType("INTEGER"); + + b.Property("PowerFromGrid") + .HasColumnType("INTEGER"); + + b.Property("TimeStamp") + .HasColumnType("TEXT"); + + b.Property("UsedWattHours") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.HasIndex("HandledChargeId"); + + b.ToTable("PowerDistributions"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.RestValueConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("HttpMethod") + .HasColumnType("INTEGER"); + + b.Property("NodePatternType") + .HasColumnType("INTEGER"); + + b.Property("Url") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("RestValueConfigurations"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.RestValueConfigurationHeader", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RestValueConfigurationId") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RestValueConfigurationId", "Key") + .IsUnique(); + + b.ToTable("RestValueConfigurationHeaders"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.RestValueResultConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CorrectionFactor") + .HasColumnType("TEXT"); + + b.Property("NodePattern") + .HasColumnType("TEXT"); + + b.Property("Operator") + .HasColumnType("INTEGER"); + + b.Property("RestValueConfigurationId") + .HasColumnType("INTEGER"); + + b.Property("UsedFor") + .HasColumnType("INTEGER"); + + b.Property("XmlAttributeHeaderName") + .HasColumnType("TEXT"); + + b.Property("XmlAttributeHeaderValue") + .HasColumnType("TEXT"); + + b.Property("XmlAttributeValueName") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RestValueConfigurationId"); + + b.ToTable("RestValueResultConfigurations"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.SpotPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SpotPrices"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.TscConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("TscConfigurations"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.CarValueLog", b => + { + b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.Car", "Car") + .WithMany("CarValueLogs") + .HasForeignKey("CarId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Car"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ChargingDetail", b => + { + b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ChargingProcess", "ChargingProcess") + .WithMany("ChargingDetails") + .HasForeignKey("ChargingProcessId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChargingProcess"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ChargingProcess", b => + { + b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.Car", "Car") + .WithMany("ChargingProcesses") + .HasForeignKey("CarId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Car"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ModbusResultConfiguration", b => + { + b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ModbusResultConfiguration", "InvertedByModbusResultConfiguration") + .WithMany() + .HasForeignKey("InvertedByModbusResultConfigurationId"); + + b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ModbusConfiguration", "ModbusConfiguration") + .WithMany("ModbusResultConfigurations") + .HasForeignKey("ModbusConfigurationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("InvertedByModbusResultConfiguration"); + + b.Navigation("ModbusConfiguration"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.MqttResultConfiguration", b => + { + b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.MqttConfiguration", "MqttConfiguration") + .WithMany("MqttResultConfigurations") + .HasForeignKey("MqttConfigurationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MqttConfiguration"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.PowerDistribution", b => + { + b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.HandledCharge", "HandledCharge") + .WithMany("PowerDistributions") + .HasForeignKey("HandledChargeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("HandledCharge"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.RestValueConfigurationHeader", b => + { + b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.RestValueConfiguration", "RestValueConfiguration") + .WithMany("Headers") + .HasForeignKey("RestValueConfigurationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RestValueConfiguration"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.RestValueResultConfiguration", b => + { + b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.RestValueConfiguration", "RestValueConfiguration") + .WithMany("RestValueResultConfigurations") + .HasForeignKey("RestValueConfigurationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RestValueConfiguration"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.Car", b => + { + b.Navigation("CarValueLogs"); + + b.Navigation("ChargingProcesses"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ChargingProcess", b => + { + b.Navigation("ChargingDetails"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.HandledCharge", b => + { + b.Navigation("PowerDistributions"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ModbusConfiguration", b => + { + b.Navigation("ModbusResultConfigurations"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.MqttConfiguration", b => + { + b.Navigation("MqttResultConfigurations"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.RestValueConfiguration", b => + { + b.Navigation("Headers"); + + b.Navigation("RestValueResultConfigurations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TeslaSolarCharger.Model/Migrations/20241227093212_ReplaceTeslaTokensWithBackendTokens.cs b/TeslaSolarCharger.Model/Migrations/20241227093212_ReplaceTeslaTokensWithBackendTokens.cs new file mode 100644 index 000000000..70a11fe25 --- /dev/null +++ b/TeslaSolarCharger.Model/Migrations/20241227093212_ReplaceTeslaTokensWithBackendTokens.cs @@ -0,0 +1,58 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TeslaSolarCharger.Model.Migrations +{ + /// + public partial class ReplaceTeslaTokensWithBackendTokens : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TeslaTokens"); + + migrationBuilder.CreateTable( + name: "BackendTokens", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + AccessToken = table.Column(type: "TEXT", nullable: false), + RefreshToken = table.Column(type: "TEXT", nullable: false), + ExpiresAtUtc = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BackendTokens", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "BackendTokens"); + + migrationBuilder.CreateTable( + name: "TeslaTokens", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + AccessToken = table.Column(type: "TEXT", nullable: false), + ExpiresAtUtc = table.Column(type: "TEXT", nullable: false), + IdToken = table.Column(type: "TEXT", nullable: false), + RefreshToken = table.Column(type: "TEXT", nullable: false), + Region = table.Column(type: "INTEGER", nullable: false), + UnauthorizedCounter = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TeslaTokens", x => x.Id); + }); + } + } +} diff --git a/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs b/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs index e4f729503..e2977bd83 100644 --- a/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs +++ b/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs @@ -57,6 +57,28 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("BackendNotifications"); }); + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.BackendToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExpiresAtUtc") + .HasColumnType("TEXT"); + + b.Property("RefreshToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("BackendTokens"); + }); + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.CachedCarState", b => { b.Property("Id") @@ -737,38 +759,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("SpotPrices"); }); - modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.TeslaToken", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessToken") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("ExpiresAtUtc") - .HasColumnType("TEXT"); - - b.Property("IdToken") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("RefreshToken") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Region") - .HasColumnType("INTEGER"); - - b.Property("UnauthorizedCounter") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("TeslaTokens"); - }); - modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.TscConfiguration", b => { b.Property("Id") From d64ddbcba44877a284ae72d0536d1a390cf87bbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Sat, 28 Dec 2024 19:48:43 +0100 Subject: [PATCH 06/81] feat(chore): refactor login --- .../Dialogs/CreateBackendTokenDialog.razor | 18 ++--- .../Client/Helper/Contracts/IDialogHelper.cs | 2 +- .../Client/Helper/DialogHelper.cs | 5 +- .../Client/Pages/BaseConfiguration.razor | 9 ++- .../Controllers/BackendApiController.cs | 3 +- .../Dtos/Solar4CarBackend/User/DtoLogin.cs | 3 +- .../Scheduling/Jobs/BackendTokenRefreshJob.cs | 2 +- .../Server/Services/BackendApiService.cs | 65 ++++++------------- .../Services/Contracts/IBackendApiService.cs | 7 +- .../Shared/Dtos/DtoBackendLogin.cs | 7 ++ .../Shared/Resources/Contracts/IConstants.cs | 2 - 11 files changed, 50 insertions(+), 73 deletions(-) create mode 100644 TeslaSolarCharger/Shared/Dtos/DtoBackendLogin.cs diff --git a/TeslaSolarCharger/Client/Dialogs/CreateBackendTokenDialog.razor b/TeslaSolarCharger/Client/Dialogs/CreateBackendTokenDialog.razor index 342a527d1..dcc176f1f 100644 --- a/TeslaSolarCharger/Client/Dialogs/CreateBackendTokenDialog.razor +++ b/TeslaSolarCharger/Client/Dialogs/CreateBackendTokenDialog.razor @@ -1,5 +1,4 @@ -@using System.ComponentModel -@using TeslaSolarCharger.Client.Helper.Contracts +@using TeslaSolarCharger.Client.Helper.Contracts @using TeslaSolarCharger.Shared.Attributes @using TeslaSolarCharger.Shared.Dtos @@ -7,8 +6,8 @@ - -
ToDo: Link to privacy Policy
+ +
Cancel @@ -20,16 +19,13 @@ [CascadingParameter] MudDialogInstance MudDialog { get; set; } [HelperText("While entering an E-Mail is not required it is HIGHLY!! recommended so I can contact you in case I detect anything not working")] - [DisplayName("E-Mail")] - public string EmailAddress { get; set; } = string.Empty; + + private readonly DtoBackendLogin _backendLogin = new DtoBackendLogin(); async Task Submit() { - var result = await HttpClientHelper.SendPostRequestWithSnackbarAsync>($"api/BackendApi/GenerateUserAccount?emailAddress={EmailAddress}", null); - if (result is { Value: true }) - { - MudDialog.Close(DialogResult.Ok(true)); - } + await HttpClientHelper.SendPostRequestWithSnackbarAsync("api/BackendApi/LoginToBackend", _backendLogin); + MudDialog.Close(DialogResult.Ok(true)); } void Cancel() => MudDialog.Cancel(); diff --git a/TeslaSolarCharger/Client/Helper/Contracts/IDialogHelper.cs b/TeslaSolarCharger/Client/Helper/Contracts/IDialogHelper.cs index e618a035a..487c6df32 100644 --- a/TeslaSolarCharger/Client/Helper/Contracts/IDialogHelper.cs +++ b/TeslaSolarCharger/Client/Helper/Contracts/IDialogHelper.cs @@ -5,5 +5,5 @@ namespace TeslaSolarCharger.Client.Helper.Contracts; public interface IDialogHelper { Task ShowTextDialog(string title, string dialogText); - Task ShowCreateBackendTokenDialog(); + Task ShowCreateBackendTokenDialog(); } diff --git a/TeslaSolarCharger/Client/Helper/DialogHelper.cs b/TeslaSolarCharger/Client/Helper/DialogHelper.cs index 125193ba6..5fc599f65 100644 --- a/TeslaSolarCharger/Client/Helper/DialogHelper.cs +++ b/TeslaSolarCharger/Client/Helper/DialogHelper.cs @@ -21,7 +21,7 @@ public async Task ShowTextDialog(string title, string dialogText) var result = await dialog.Result; } - public async Task ShowCreateBackendTokenDialog() + public async Task ShowCreateBackendTokenDialog() { var options = new DialogOptions() { @@ -32,6 +32,7 @@ public async Task ShowTextDialog(string title, string dialogText) { }; var dialog = await dialogService.ShowAsync("Generate Backend Token", parameters, options); - return await dialog.Result; + var result = await dialog.Result; + return result is { Canceled: false }; } } diff --git a/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor b/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor index 04f943977..e9f1cbf92 100644 --- a/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor +++ b/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor @@ -325,12 +325,11 @@ else var hasValidBackendToken = await FleetApiTokenCheckService.HasValidBackendToken(); if (!hasValidBackendToken) { - var result = await DialogHelper.ShowCreateBackendTokenDialog(); - //ToDo: only continue on result == DialogResult.Ok() - // if (result != DialogResult.Ok()) - // { + var success = await DialogHelper.ShowCreateBackendTokenDialog(); + if (!success) + { - // } + } } var url = await HttpClient.GetFromJsonAsync>($"api/FleetApi/GetOauthUrl?locale={Uri.EscapeDataString(locale)}&baseUrl={Uri.EscapeDataString(baseUrl)}").ConfigureAwait(false); diff --git a/TeslaSolarCharger/Server/Controllers/BackendApiController.cs b/TeslaSolarCharger/Server/Controllers/BackendApiController.cs index e5770c4e8..5bedcac96 100644 --- a/TeslaSolarCharger/Server/Controllers/BackendApiController.cs +++ b/TeslaSolarCharger/Server/Controllers/BackendApiController.cs @@ -10,6 +10,7 @@ public class BackendApiController (IBackendApiService backendApiService) : ApiBa [HttpGet] public Task> HasValidBackendToken() => backendApiService.HasValidBackendToken(); + [HttpPost] - public Task> GenerateUserAccount(string emailAddress) => backendApiService.GenerateUserAccount(emailAddress); + public Task LoginToBackend(DtoBackendLogin login) => backendApiService.GetToken(login); } diff --git a/TeslaSolarCharger/Server/Dtos/Solar4CarBackend/User/DtoLogin.cs b/TeslaSolarCharger/Server/Dtos/Solar4CarBackend/User/DtoLogin.cs index eb8047c19..b7650c520 100644 --- a/TeslaSolarCharger/Server/Dtos/Solar4CarBackend/User/DtoLogin.cs +++ b/TeslaSolarCharger/Server/Dtos/Solar4CarBackend/User/DtoLogin.cs @@ -1,7 +1,8 @@ namespace TeslaSolarCharger.Server.Dtos.Solar4CarBackend.User; -public class DtoLogin(string userName, string password) +public class DtoLogin(string userName, string password, string installationId) { public string UserName { get; set; } = userName; public string Password { get; set; } = password; + public string InstallationId { get; set; } = installationId; } diff --git a/TeslaSolarCharger/Server/Scheduling/Jobs/BackendTokenRefreshJob.cs b/TeslaSolarCharger/Server/Scheduling/Jobs/BackendTokenRefreshJob.cs index b1887a764..dabc24230 100644 --- a/TeslaSolarCharger/Server/Scheduling/Jobs/BackendTokenRefreshJob.cs +++ b/TeslaSolarCharger/Server/Scheduling/Jobs/BackendTokenRefreshJob.cs @@ -11,6 +11,6 @@ public class BackendTokenRefreshJob(ILogger logger, public async Task Execute(IJobExecutionContext context) { logger.LogTrace("{method}({context})", nameof(Execute), context); - await service.GetOrRefreshBackendToken().ConfigureAwait(false); + await service.RefreshBackendToken().ConfigureAwait(false); } } diff --git a/TeslaSolarCharger/Server/Services/BackendApiService.cs b/TeslaSolarCharger/Server/Services/BackendApiService.cs index b72794451..e5594aa8a 100644 --- a/TeslaSolarCharger/Server/Services/BackendApiService.cs +++ b/TeslaSolarCharger/Server/Services/BackendApiService.cs @@ -2,7 +2,6 @@ using Newtonsoft.Json; using System.Diagnostics; using System.Net; -using System.Net.Http.Headers; using System.Reflection; using TeslaSolarCharger.Model.Contracts; using TeslaSolarCharger.Model.Entities.TeslaSolarCharger; @@ -12,7 +11,6 @@ using TeslaSolarCharger.Server.Services.Contracts; using TeslaSolarCharger.Shared.Contracts; using TeslaSolarCharger.Shared.Dtos; -using TeslaSolarCharger.Shared.Dtos.Contracts; using TeslaSolarCharger.Shared.Resources.Contracts; namespace TeslaSolarCharger.Server.Services; @@ -25,8 +23,7 @@ public class BackendApiService( IConstants constants, IDateTimeProvider dateTimeProvider, IErrorHandlingService errorHandlingService, - IIssueKeys issueKeys, - IPasswordGenerationService passwordGenerationService) + IIssueKeys issueKeys) : IBackendApiService { public async Task> StartTeslaOAuth(string locale, string baseUrl) @@ -73,57 +70,27 @@ public async Task> StartTeslaOAuth(string locale, string baseUr return new DtoValue(requestUrl); } - public async Task> GenerateUserAccount(string emailAddress) + public async Task GetToken(DtoBackendLogin login) { - logger.LogTrace("{method}({emailAddress})", nameof(GenerateUserAccount), emailAddress); - await tscConfigurationService.SetConfigurationValueByKey(constants.EmailConfigurationKey, emailAddress); - await GenerateUserAccount(); - return new(true); - } + logger.LogTrace("{method}()", nameof(GetToken)); - private async Task GenerateUserAccount() - { - logger.LogTrace("{method}()", nameof(GenerateUserAccount)); - var userEmail = await tscConfigurationService.GetConfigurationValueByKey(constants.EmailConfigurationKey); - var password = passwordGenerationService.GeneratePassword(configurationWrapper.BackendPasswordDefaultLength()); - var installationId = await tscConfigurationService.GetInstallationId().ConfigureAwait(false); - var dtoCreateUser = new DtoCreateUser(installationId.ToString(), password) { Email = userEmail, }; - var url = configurationWrapper.BackendApiBaseUrl() + "User/Create"; - using var httpClient = new HttpClient(); - httpClient.Timeout = TimeSpan.FromSeconds(10); - var response = await httpClient.PostAsJsonAsync(url, dtoCreateUser).ConfigureAwait(false); - if (!response.IsSuccessStatusCode) + if (string.IsNullOrEmpty(login.UserName)) { - var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - logger.LogError("Could not create user account. StatusCode: {statusCode}, resultBody: {resultBody}", response.StatusCode, responseString); - throw new InvalidOperationException("Could not create user account"); + throw new InvalidOperationException("Username is empty"); + } + if (string.IsNullOrEmpty(login.Password)) + { + throw new InvalidOperationException("Password is empty"); } - await tscConfigurationService.SetConfigurationValueByKey(constants.BackendPasswordConfigurationKey, password); - } - public async Task GetOrRefreshBackendToken() - { - logger.LogTrace("{method}()", nameof(GetOrRefreshBackendToken)); var token = await teslaSolarChargerContext.BackendTokens.SingleOrDefaultAsync(); if (token != default) { - if (token.ExpiresAtUtc > dateTimeProvider.DateTimeOffSetUtcNow()) - { - return; - } - logger.LogInformation("Backend Token expired. Refresh token..."); - await RefreshBackendToken(token); - await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); - logger.LogInformation("Backend Token refreshed"); - return; - } - var backendPassword = await tscConfigurationService.GetConfigurationValueByKey(constants.BackendPasswordConfigurationKey).ConfigureAwait(false); - if (string.IsNullOrEmpty(backendPassword)) - { - return; + teslaSolarChargerContext.BackendTokens.Remove(token); } var installationId = await tscConfigurationService.GetInstallationId().ConfigureAwait(false); - var dtoLogin = new DtoLogin(installationId.ToString(), backendPassword); + + var dtoLogin = new DtoLogin(login.UserName, login.Password, installationId.ToString()); var url = configurationWrapper.BackendApiBaseUrl() + "User/Login"; using var httpClient = new HttpClient(); httpClient.Timeout = TimeSpan.FromSeconds(10); @@ -171,10 +138,16 @@ public async Task> HasValidBackendToken() return new(true); } - private async Task RefreshBackendToken(BackendToken token) + public async Task RefreshBackendToken() { logger.LogTrace("{method}(token)", nameof(RefreshBackendToken)); var url = configurationWrapper.BackendApiBaseUrl() + "User/RefreshToken"; + var token = await teslaSolarChargerContext.BackendTokens.SingleOrDefaultAsync(); + if(token == default) + { + logger.LogError("Could not refresh backend token. No token found"); + return; + } var dtoRefreshToken = new DtoTokenRefreshModel(token.AccessToken, token.RefreshToken); using var httpClient = new HttpClient(); httpClient.Timeout = TimeSpan.FromSeconds(10); diff --git a/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs b/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs index 7d3630d6c..64d62c509 100644 --- a/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs +++ b/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs @@ -1,4 +1,5 @@ -using TeslaSolarCharger.Shared.Dtos; +using TeslaSolarCharger.Model.Entities.TeslaSolarCharger; +using TeslaSolarCharger.Shared.Dtos; namespace TeslaSolarCharger.Server.Services.Contracts; @@ -9,7 +10,7 @@ public interface IBackendApiService Task PostErrorInformation(string source, string methodName, string message, string issueKey, string? vin, string? stackTrace); Task GetCurrentVersion(); Task GetNewBackendNotifications(); - Task> GenerateUserAccount(string emailAddress); - Task GetOrRefreshBackendToken(); Task> HasValidBackendToken(); + Task GetToken(DtoBackendLogin login); + Task RefreshBackendToken(); } diff --git a/TeslaSolarCharger/Shared/Dtos/DtoBackendLogin.cs b/TeslaSolarCharger/Shared/Dtos/DtoBackendLogin.cs new file mode 100644 index 000000000..fc5748ceb --- /dev/null +++ b/TeslaSolarCharger/Shared/Dtos/DtoBackendLogin.cs @@ -0,0 +1,7 @@ +namespace TeslaSolarCharger.Shared.Dtos; + +public class DtoBackendLogin() +{ + public string? UserName { get; set; } + public string? Password { get; set; } +} diff --git a/TeslaSolarCharger/Shared/Resources/Contracts/IConstants.cs b/TeslaSolarCharger/Shared/Resources/Contracts/IConstants.cs index e27e272dc..cf8fbbefc 100644 --- a/TeslaSolarCharger/Shared/Resources/Contracts/IConstants.cs +++ b/TeslaSolarCharger/Shared/Resources/Contracts/IConstants.cs @@ -38,8 +38,6 @@ public interface IConstants string WakeUpRequestUrl { get; } string VehicleRequestUrl { get; } string VehicleDataRequestUrl { get; } - public string EmailConfigurationKey { get; } - string BackendPasswordConfigurationKey { get; } string TeslaTokenEncryptionKeyKey { get; } string BackendTokenUnauthorizedKey { get; } } From 70223f3dff40b97ba1635ebae77d3a567f1de4d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Sat, 28 Dec 2024 20:28:06 +0100 Subject: [PATCH 07/81] feat(CreateBackendTokenDialog): Hide password chars --- TeslaSolarCharger/Client/Components/GenericInput.razor | 1 - .../Client/Dialogs/CreateBackendTokenDialog.razor | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/TeslaSolarCharger/Client/Components/GenericInput.razor b/TeslaSolarCharger/Client/Components/GenericInput.razor index 918a42ac8..2ad875430 100644 --- a/TeslaSolarCharger/Client/Components/GenericInput.razor +++ b/TeslaSolarCharger/Client/Components/GenericInput.razor @@ -246,7 +246,6 @@ @bind-Value="Value" For="@(For)" Required="@IsRequired" - HelperText="Leave empty if you want to keep the existing password." InputType="InputType.Password" Label="@LabelName" Disabled="IsDisabled" diff --git a/TeslaSolarCharger/Client/Dialogs/CreateBackendTokenDialog.razor b/TeslaSolarCharger/Client/Dialogs/CreateBackendTokenDialog.razor index dcc176f1f..5069c9f85 100644 --- a/TeslaSolarCharger/Client/Dialogs/CreateBackendTokenDialog.razor +++ b/TeslaSolarCharger/Client/Dialogs/CreateBackendTokenDialog.razor @@ -7,11 +7,12 @@ - + Cancel - Yes + Login From 288d3e709c6749ed45ffeade410a50a51d0a6eb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Sat, 28 Dec 2024 21:03:17 +0100 Subject: [PATCH 08/81] fix(CreateBackendTokenDialog): remove unnecessary code --- .../Client/Dialogs/CreateBackendTokenDialog.razor | 3 --- 1 file changed, 3 deletions(-) diff --git a/TeslaSolarCharger/Client/Dialogs/CreateBackendTokenDialog.razor b/TeslaSolarCharger/Client/Dialogs/CreateBackendTokenDialog.razor index 5069c9f85..ab8f913dc 100644 --- a/TeslaSolarCharger/Client/Dialogs/CreateBackendTokenDialog.razor +++ b/TeslaSolarCharger/Client/Dialogs/CreateBackendTokenDialog.razor @@ -1,5 +1,4 @@ @using TeslaSolarCharger.Client.Helper.Contracts -@using TeslaSolarCharger.Shared.Attributes @using TeslaSolarCharger.Shared.Dtos @inject IHttpClientHelper HttpClientHelper @@ -18,8 +17,6 @@ @code { [CascadingParameter] MudDialogInstance MudDialog { get; set; } - - [HelperText("While entering an E-Mail is not required it is HIGHLY!! recommended so I can contact you in case I detect anything not working")] private readonly DtoBackendLogin _backendLogin = new DtoBackendLogin(); From 6fbd8c5dd1c4f6cf6de02a22b25ceb44d612baaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Sat, 28 Dec 2024 23:15:49 +0100 Subject: [PATCH 09/81] fix(BackendApiService): use correct header --- .../Server/Services/BackendApiService.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/TeslaSolarCharger/Server/Services/BackendApiService.cs b/TeslaSolarCharger/Server/Services/BackendApiService.cs index e5594aa8a..684c24638 100644 --- a/TeslaSolarCharger/Server/Services/BackendApiService.cs +++ b/TeslaSolarCharger/Server/Services/BackendApiService.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; +using System; using System.Diagnostics; using System.Net; using System.Reflection; @@ -23,7 +24,8 @@ public class BackendApiService( IConstants constants, IDateTimeProvider dateTimeProvider, IErrorHandlingService errorHandlingService, - IIssueKeys issueKeys) + IIssueKeys issueKeys, + IPasswordGenerationService passwordGenerationService) : IBackendApiService { public async Task> StartTeslaOAuth(string locale, string baseUrl) @@ -35,9 +37,20 @@ public async Task> StartTeslaOAuth(string locale, string baseUr teslaSolarChargerContext.TscConfigurations.RemoveRange(configEntriesToRemove); await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); var backendApiBaseUrl = configurationWrapper.BackendApiBaseUrl(); + var encryptionKey = passwordGenerationService.GeneratePassword(32); + var state = Guid.NewGuid(); + var requestUri = $"{backendApiBaseUrl}Client/AddAuthenticationStartInformation?redirectUri={Uri.EscapeDataString(baseUrl)}&encryptionKey={Uri.EscapeDataString(encryptionKey)}&state={Uri.EscapeDataString(state.ToString())}"; using var httpClient = new HttpClient(); - var requestUri = $"{backendApiBaseUrl}Client/AddAuthenticationStartInformation?redirectUri={Uri.EscapeDataString(baseUrl)}"; - var responseString = await httpClient.GetStringAsync(requestUri).ConfigureAwait(false); + httpClient.Timeout = TimeSpan.FromSeconds(10); + var request = new HttpRequestMessage(HttpMethod.Post, requestUri); + var token = await teslaSolarChargerContext.BackendTokens.SingleOrDefaultAsync().ConfigureAwait(false); + if (token == default) + { + throw new InvalidOperationException("Can not start Tesla O Auth without backend token"); + } + request.Headers.Authorization = new("Bearer", token.AccessToken); + var response = await httpClient.SendAsync(request).ConfigureAwait(false); + var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); var oAuthRequestInformation = JsonConvert.DeserializeObject(responseString) ?? throw new InvalidDataException("Could not get oAuth data"); var requestUrl = GenerateAuthUrl(oAuthRequestInformation, locale); var tokenRequested = await teslaSolarChargerContext.TscConfigurations From e609859c7f727618f4add6e43ac71a763731aac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Sun, 29 Dec 2024 10:45:37 +0100 Subject: [PATCH 10/81] fix(BackendApiService): save new token on refresh receive --- TeslaSolarCharger/Server/Services/BackendApiService.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TeslaSolarCharger/Server/Services/BackendApiService.cs b/TeslaSolarCharger/Server/Services/BackendApiService.cs index 684c24638..044a9fb29 100644 --- a/TeslaSolarCharger/Server/Services/BackendApiService.cs +++ b/TeslaSolarCharger/Server/Services/BackendApiService.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; using System; using System.Diagnostics; @@ -175,6 +175,7 @@ public async Task RefreshBackendToken() var newToken = JsonConvert.DeserializeObject(responseContent) ?? throw new InvalidDataException("Could not parse token"); token.AccessToken = newToken.AccessToken; token.RefreshToken = newToken.RefreshToken; + await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); } internal string GenerateAuthUrl(DtoTeslaOAuthRequestInformation oAuthInformation, string locale) From af0d7aa1b32056d848bdbaaccd1d7f9afed6b5f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Sun, 29 Dec 2024 10:45:57 +0100 Subject: [PATCH 11/81] feat(BackendApiService): refresh backend token only if needed --- .../Scheduling/Jobs/BackendTokenRefreshJob.cs | 2 +- .../Server/Services/BackendApiService.cs | 13 +++++++++---- .../Server/Services/Contracts/IBackendApiService.cs | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/TeslaSolarCharger/Server/Scheduling/Jobs/BackendTokenRefreshJob.cs b/TeslaSolarCharger/Server/Scheduling/Jobs/BackendTokenRefreshJob.cs index dabc24230..e6cb24863 100644 --- a/TeslaSolarCharger/Server/Scheduling/Jobs/BackendTokenRefreshJob.cs +++ b/TeslaSolarCharger/Server/Scheduling/Jobs/BackendTokenRefreshJob.cs @@ -11,6 +11,6 @@ public class BackendTokenRefreshJob(ILogger logger, public async Task Execute(IJobExecutionContext context) { logger.LogTrace("{method}({context})", nameof(Execute), context); - await service.RefreshBackendToken().ConfigureAwait(false); + await service.RefreshBackendTokenIfNeeded().ConfigureAwait(false); } } diff --git a/TeslaSolarCharger/Server/Services/BackendApiService.cs b/TeslaSolarCharger/Server/Services/BackendApiService.cs index 044a9fb29..02f41b68a 100644 --- a/TeslaSolarCharger/Server/Services/BackendApiService.cs +++ b/TeslaSolarCharger/Server/Services/BackendApiService.cs @@ -1,6 +1,5 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; -using System; using System.Diagnostics; using System.Net; using System.Reflection; @@ -151,9 +150,9 @@ public async Task> HasValidBackendToken() return new(true); } - public async Task RefreshBackendToken() + public async Task RefreshBackendTokenIfNeeded() { - logger.LogTrace("{method}(token)", nameof(RefreshBackendToken)); + logger.LogTrace("{method}(token)", nameof(RefreshBackendTokenIfNeeded)); var url = configurationWrapper.BackendApiBaseUrl() + "User/RefreshToken"; var token = await teslaSolarChargerContext.BackendTokens.SingleOrDefaultAsync(); if(token == default) @@ -161,6 +160,12 @@ public async Task RefreshBackendToken() logger.LogError("Could not refresh backend token. No token found"); return; } + var currentDate = dateTimeProvider.DateTimeOffSetUtcNow(); + if(token.ExpiresAtUtc > currentDate.AddMinutes(1)) + { + logger.LogTrace("Token is still valid"); + return; + } var dtoRefreshToken = new DtoTokenRefreshModel(token.AccessToken, token.RefreshToken); using var httpClient = new HttpClient(); httpClient.Timeout = TimeSpan.FromSeconds(10); diff --git a/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs b/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs index 64d62c509..942682f21 100644 --- a/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs +++ b/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs @@ -12,5 +12,5 @@ public interface IBackendApiService Task GetNewBackendNotifications(); Task> HasValidBackendToken(); Task GetToken(DtoBackendLogin login); - Task RefreshBackendToken(); + Task RefreshBackendTokenIfNeeded(); } From 7e4fd60ea598391c746c0dc1508779b6002779d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Sun, 29 Dec 2024 14:16:10 +0100 Subject: [PATCH 12/81] feat(ErrorHandlingService): can display correct error messages on token issues --- .../Client/Pages/BaseConfiguration.razor | 104 ++++++++---------- .../Client/Pages/CarSettings.razor | 11 +- .../PossibleIssues/Contracts/IIssueKeys.cs | 7 +- .../Resources/PossibleIssues/IssueKeys.cs | 8 +- .../PossibleIssues/PossibleIssues.cs | 63 +++++------ .../Server/Services/BackendApiService.cs | 25 +---- .../Contracts/ITeslaFleetApiTokenHelper.cs | 1 - .../Server/Services/ErrorHandlingService.cs | 35 +++--- .../Server/Services/TeslaFleetApiService.cs | 2 +- .../Services/TeslaFleetApiTokenHelper.cs | 63 +++++------ .../Shared/Enums/FleetApiTokenState.cs | 13 +-- .../Shared/Resources/Constants.cs | 2 +- .../Shared/Resources/Contracts/IConstants.cs | 3 +- 13 files changed, 128 insertions(+), 209 deletions(-) diff --git a/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor b/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor index e9f1cbf92..345c0ea1b 100644 --- a/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor +++ b/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor @@ -34,52 +34,34 @@ else
- @if (_fleetApiTokenState != FleetApiTokenState.NotNeeded) +

Tesla Fleet API:

+ @switch (_fleetApiTokenState) { -

Tesla Fleet API:

- @switch (_fleetApiTokenState) - { - case FleetApiTokenState.NotRequested: -
- You have to generate a token in order to use the Tesla Fleet API. Important: You have to allow access to all scopes. -
- break; - case FleetApiTokenState.NotReceived: -
- You already have requested a token but did not receive it yet. It can take up to five minutes to receive the token. If the token did not arrive within five minutes please try again: -
- break; - case FleetApiTokenState.TokenRequestExpired: -
- Your token request has expired. Please generate a new token: -
- break; - case FleetApiTokenState.TokenUnauthorized: -
- Your token is unauthorized, Please generate a new token, allow access to all scopes and enable mobile access in your car. -
- break; - case FleetApiTokenState.MissingScopes: -
- Your token has missing scopes. Remove Tesla Solar Charger from your third party apps as you won't get asked again for the scopes. After that generate a new token and allow access to all scopes. -
- break; - case FleetApiTokenState.Expired: -
- Your token has expired. This could happen if you changed your Tesla password or did not use TeslaSolarCharger for too long. Please generate a new token: -
- break; - case FleetApiTokenState.UpToDate: -
- Everything is fine! If you want to generate a new token e.g. to switch to another Tesla Account please click the button below: -
- break; - } -
- -
-
+ case FleetApiTokenState.NoBackendApiToken: +
+ You are not logged in in the Solar4car.com account. When requesting a new Tesla Token there will also be a request to log in to your Solar4car.com account. +
+ break; + case FleetApiTokenState.FleetApiTokenUnauthorized: +
+ Your token is unauthorized. Request a new token, allow access to all scopes and enable mobile access in your car. +
+ break; + case FleetApiTokenState.FleetApiTokenMissingScopes: +
+ Your token has missing scopes. Request a new Token and allow all scopes (only required scopes are requested). +
+ break; + case FleetApiTokenState.UpToDate: +
+ Everything is fine! If you want to generate a new token e.g. to switch to another Tesla Account please click the button below: +
+ break; } +
+ +
+

TeslaMate:

- + @@ -101,7 +83,7 @@ else UnitText="" HelpText="You can use the internal port of the TeslaMate database container"> - + - + } - - + +

Home Geofence

- + - + - +

Telegram:

How to set up Telegram @@ -210,7 +192,7 @@ else DisableToolTipText="You need to save the configuration before testing it." OnButtonClicked="_ => SendTelegramTestMessage()">
- + - + @@ -227,7 +209,7 @@ else UnitText="s" HelpText=""> - + @@ -236,7 +218,7 @@ else UnitText="min" HelpText=""> - + @@ -248,23 +230,23 @@ else -
+
- + -
+
- +
@@ -276,7 +258,7 @@ else IsLoading="_submitLoading" ButtonType="ButtonType.Submit">
- + } diff --git a/TeslaSolarCharger/Client/Pages/CarSettings.razor b/TeslaSolarCharger/Client/Pages/CarSettings.razor index 9228bcfb6..597fbb6b3 100644 --- a/TeslaSolarCharger/Client/Pages/CarSettings.razor +++ b/TeslaSolarCharger/Client/Pages/CarSettings.razor @@ -11,16 +11,7 @@

Car Settings

- @if (_fleetApiTokenState == FleetApiTokenState.NotReceived) - { - -

Waiting for token

- You already requested a token. TSC is currently waiting for receiving it. This might take up to five minutes. Reload this page to get new information if available. -
- } - else if (_fleetApiTokenState != null && _fleetApiTokenState != FleetApiTokenState.UpToDate) + @if (_fleetApiTokenState != null && _fleetApiTokenState != FleetApiTokenState.UpToDate) { "VersionNotUpToDate"; - public string FleetApiTokenNotRequested => "FleetApiTokenNotRequested"; public string FleetApiTokenUnauthorized => "FleetApiTokenUnauthorized"; public string FleetApiTokenMissingScopes => "FleetApiTokenMissingScopes"; public string FleetApiTokenRequestExpired => "FleetApiTokenRequestExpired"; - public string FleetApiTokenNotReceived => "FleetApiTokenNotReceived"; public string FleetApiTokenRefreshNonSuccessStatusCode => "FleetApiTokenRefreshNonSuccessStatusCode"; - public string FleetApiTokenExpired => "FleetApiTokenExpired"; - public string FleetApiTokenNoApiRequestsAllowed => "FleetApiRequestsNotAllowed"; public string CrashedOnStartup => "CrashedOnStartup"; public string RestartNeeded => "RestartNeeded"; public string GetVehicle => "GetVehicle"; public string GetVehicleData => "GetVehicleData"; public string CarStateUnknown => "CarStateUnknown"; - public string UnhandledCarStateRefresh => "UnhandledCarStateRefresh"; public string FleetApiNonSuccessStatusCode => "FleetApiNonSuccessStatusCode_"; public string FleetApiNonSuccessResult => "FleetApiNonSuccessResult_"; public string UnsignedCommand => "UnsignedCommand"; @@ -27,5 +22,8 @@ public class IssueKeys : IIssueKeys public string SolarValuesNotAvailable => "SolarValuesNotAvailable"; public string UsingFleetApiAsBleFallback => "UsingFleetApiAsBleFallback"; public string BleVersionCompatibility => "BleVersionCompatibility"; + public string NoBackendApiToken => "NoBackendApiToken"; + public string BackendTokenUnauthorized => "BackendTokenUnauthorized"; + public string FleetApiTokenExpired => "FleetApiTokenExpired"; } diff --git a/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs b/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs index 71c4cde91..637f85868 100644 --- a/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs +++ b/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs @@ -18,15 +18,6 @@ public class PossibleIssues(IIssueKeys issueKeys) : IPossibleIssues HideOccurrenceCount = true, } }, - { issueKeys.FleetApiTokenNotRequested, new DtoIssue - { - IssueSeverity = IssueSeverity.Error, - IsTelegramEnabled = false, - ShowErrorAfterOccurrences = 1, - HasPlaceHolderIssueKey = false, - HideOccurrenceCount = true, - } - }, { issueKeys.FleetApiTokenUnauthorized, new DtoIssue { IssueSeverity = IssueSeverity.Error, @@ -52,33 +43,6 @@ public class PossibleIssues(IIssueKeys issueKeys) : IPossibleIssues HideOccurrenceCount = true, } }, - { issueKeys.FleetApiTokenNotReceived, new DtoIssue - { - IssueSeverity = IssueSeverity.Warning, - IsTelegramEnabled = false, - ShowErrorAfterOccurrences = 1, - HasPlaceHolderIssueKey = false, - HideOccurrenceCount = true, - } - }, - { issueKeys.FleetApiTokenExpired, new DtoIssue - { - IssueSeverity = IssueSeverity.Error, - IsTelegramEnabled = true, - ShowErrorAfterOccurrences = 1, - HasPlaceHolderIssueKey = false, - HideOccurrenceCount = true, - } - }, - { issueKeys.FleetApiTokenNoApiRequestsAllowed, new DtoIssue - { - IssueSeverity = IssueSeverity.Error, - IsTelegramEnabled = true, - ShowErrorAfterOccurrences = 2, - HasPlaceHolderIssueKey = false, - HideOccurrenceCount = true, - } - }, { issueKeys.CrashedOnStartup, new DtoIssue { IssueSeverity = IssueSeverity.Error, @@ -194,6 +158,33 @@ public class PossibleIssues(IIssueKeys issueKeys) : IPossibleIssues HideOccurrenceCount = true, } }, + { issueKeys.NoBackendApiToken, new DtoIssue + { + IssueSeverity = IssueSeverity.Error, + IsTelegramEnabled = true, + ShowErrorAfterOccurrences = 2, + HasPlaceHolderIssueKey = false, + HideOccurrenceCount = true, + } + }, + { issueKeys.BackendTokenUnauthorized, new DtoIssue + { + IssueSeverity = IssueSeverity.Error, + IsTelegramEnabled = true, + ShowErrorAfterOccurrences = 2, + HasPlaceHolderIssueKey = false, + HideOccurrenceCount = true, + } + }, + { issueKeys.FleetApiTokenExpired, new DtoIssue + { + IssueSeverity = IssueSeverity.Error, + IsTelegramEnabled = true, + ShowErrorAfterOccurrences = 2, + HasPlaceHolderIssueKey = false, + HideOccurrenceCount = true, + } + }, }; public DtoIssue GetIssueByKey(string key) diff --git a/TeslaSolarCharger/Server/Services/BackendApiService.cs b/TeslaSolarCharger/Server/Services/BackendApiService.cs index 02f41b68a..2bc72f4d6 100644 --- a/TeslaSolarCharger/Server/Services/BackendApiService.cs +++ b/TeslaSolarCharger/Server/Services/BackendApiService.cs @@ -52,34 +52,11 @@ public async Task> StartTeslaOAuth(string locale, string baseUr var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); var oAuthRequestInformation = JsonConvert.DeserializeObject(responseString) ?? throw new InvalidDataException("Could not get oAuth data"); var requestUrl = GenerateAuthUrl(oAuthRequestInformation, locale); - var tokenRequested = await teslaSolarChargerContext.TscConfigurations - .Where(c => c.Key == constants.FleetApiTokenRequested) - .FirstOrDefaultAsync().ConfigureAwait(false); - if (tokenRequested == null) - { - var config = new TscConfiguration - { - Key = constants.FleetApiTokenRequested, - Value = dateTimeProvider.UtcNow().ToString("O"), - }; - teslaSolarChargerContext.TscConfigurations.Add(config); - } - else - { - tokenRequested.Value = dateTimeProvider.UtcNow().ToString("O"); - } - await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); - await errorHandlingService.HandleError(nameof(BackendApiService), nameof(StartTeslaOAuth), "Waiting for Tesla token", - "Waiting for the Tesla Token from the TSC backend. This might take up to five minutes. If after five minutes this error is still displayed, open the Base Configuration and request a new token.", - issueKeys.FleetApiTokenNotReceived, null, null); - //Do not set FleetApiTokenNotReceived to resolved here, as the token might still be in transit - await errorHandlingService.HandleErrorResolved(issueKeys.FleetApiTokenNotRequested, null); await errorHandlingService.HandleErrorResolved(issueKeys.FleetApiTokenUnauthorized, null); await errorHandlingService.HandleErrorResolved(issueKeys.FleetApiTokenMissingScopes, null); await errorHandlingService.HandleErrorResolved(issueKeys.FleetApiTokenRequestExpired, null); - await errorHandlingService.HandleErrorResolved(issueKeys.FleetApiTokenExpired, null); await errorHandlingService.HandleErrorResolved(issueKeys.FleetApiTokenRefreshNonSuccessStatusCode, null); - return new DtoValue(requestUrl); + return new(requestUrl); } public async Task GetToken(DtoBackendLogin login) diff --git a/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiTokenHelper.cs b/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiTokenHelper.cs index 6d5da7ab4..d7b2a29c3 100644 --- a/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiTokenHelper.cs +++ b/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiTokenHelper.cs @@ -5,5 +5,4 @@ namespace TeslaSolarCharger.Server.Services.Contracts; public interface ITeslaFleetApiTokenHelper { Task GetFleetApiTokenState(); - Task GetTokenRequestedDate(); } diff --git a/TeslaSolarCharger/Server/Services/ErrorHandlingService.cs b/TeslaSolarCharger/Server/Services/ErrorHandlingService.cs index d3c95f995..bb0337bdf 100644 --- a/TeslaSolarCharger/Server/Services/ErrorHandlingService.cs +++ b/TeslaSolarCharger/Server/Services/ErrorHandlingService.cs @@ -137,9 +137,8 @@ await AddOrRemoveErrors(activeErrors, issueKeys.SolarValuesNotAvailable, "Solar await AddOrRemoveErrors(activeErrors, issueKeys.VersionNotUpToDate, "New software version available", "Update TSC to the latest version.", settings.IsNewVersionAvailable).ConfigureAwait(false); - await AddOrRemoveErrors(activeErrors, issueKeys.FleetApiTokenNoApiRequestsAllowed, "No Fleet API requests allowed", - "Make sure your TSC can access the internet and TSC is on its latest version.", !settings.AllowUnlimitedFleetApiRequests).ConfigureAwait(false); + //ToDo: if last check there was no token related issue, only detect token related issues every x minutes as creates high load in backend await DetectTokenStateIssues(activeErrors); foreach (var car in settings.CarsToManage) { @@ -418,24 +417,24 @@ private async Task DetectTokenStateIssues(List activeErrors) { logger.LogTrace("{method}()", nameof(DetectTokenStateIssues)); var tokenState = await teslaFleetApiTokenHelper.GetFleetApiTokenState(); - await AddOrRemoveErrors(activeErrors, issueKeys.FleetApiTokenNotRequested, "Fleet API token not requested", - "Open the Base Configuration and request a new token. Important: You need to allow access to all selectable scopes.", - tokenState == FleetApiTokenState.NotRequested).ConfigureAwait(false); + await AddOrRemoveErrors(activeErrors, issueKeys.NoBackendApiToken, "No Backen API token", + "You are currently not connected to the backend. Open the Base Configuration and request a new token.", + tokenState == FleetApiTokenState.NoBackendApiToken).ConfigureAwait(false); + await AddOrRemoveErrors(activeErrors, issueKeys.BackendTokenUnauthorized, "Backend Token Unauthorized", + "You recently changed your Solar4Car password or did not use TSC for at least 30 days. Open the Base Configuration and request a new token.", + tokenState == FleetApiTokenState.BackendTokenUnauthorized).ConfigureAwait(false); await AddOrRemoveErrors(activeErrors, issueKeys.FleetApiTokenUnauthorized, "Fleet API token is unauthorized", - "You recently changed your password or did not enable mobile access in your car. Enable mobile access in your car and open the Base Configuration and request a new token. Important: You need to allow access to all selectable scopes.", - tokenState == FleetApiTokenState.TokenUnauthorized).ConfigureAwait(false); - await AddOrRemoveErrors(activeErrors, issueKeys.FleetApiTokenMissingScopes, "Your Tesla token has missing scopes.", + "You recently changed your Tesla password or did not enable mobile access in your car. Enable mobile access in your car and open the Base Configuration and request a new token. Important: You need to allow access to all selectable scopes.", + tokenState == FleetApiTokenState.FleetApiTokenUnauthorized).ConfigureAwait(false); + await AddOrRemoveErrors(activeErrors, issueKeys.FleetApiTokenUnauthorized, "Fleet API token is expired", + "Either you recently changed your Tesla password or did not enable mobile access in your car. Enable mobile access in your car and open the Base Configuration and request a new token. Important: You need to allow access to all selectable scopes.", + tokenState == FleetApiTokenState.FleetApiTokenExpired).ConfigureAwait(false); + await AddOrRemoveErrors(activeErrors, issueKeys.FleetApiTokenExpired, "Your Tesla token has missing scopes.", "Open the Base Configuration and request a new token. Note: You need to allow all selectable scopes as otherwise TSC won't work properly.", - tokenState == FleetApiTokenState.MissingScopes).ConfigureAwait(false); - await AddOrRemoveErrors(activeErrors, issueKeys.FleetApiTokenRequestExpired, "Tesla Token could not be received", - "Open the Base Configuration and request a new token.", - tokenState == FleetApiTokenState.TokenRequestExpired).ConfigureAwait(false); - await AddOrRemoveErrors(activeErrors, issueKeys.FleetApiTokenExpired, "Tesla Token expired", - "Open the Base Configuration and request a new token.", - tokenState == FleetApiTokenState.Expired).ConfigureAwait(false); - - //Remove all token related issue keys on token error because very likely it is because of the underlaying token issue. - if (tokenState != FleetApiTokenState.UpToDate && tokenState != FleetApiTokenState.NotNeeded) + tokenState == FleetApiTokenState.FleetApiTokenMissingScopes).ConfigureAwait(false); + + //Remove all fleet api related issue keys on token error because very likely it is because of the underlaying token issue. + if (tokenState != FleetApiTokenState.UpToDate) { foreach (var activeError in activeErrors.Where(activeError => activeError.IssueKey.StartsWith(issueKeys.GetVehicleData) || activeError.IssueKey.StartsWith(issueKeys.CarStateUnknown) diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index b4afc270a..160029aa0 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -1088,7 +1088,7 @@ private async Task HandleNonSuccessTeslaApiStatusCodes(HttpStatusCode statusCode logger.LogTrace("{method}({statusCode}, {token}, {responseString})", nameof(HandleNonSuccessTeslaApiStatusCodes), statusCode, token, responseString); if (statusCode == HttpStatusCode.Unauthorized) { - await tscConfigurationService.SetConfigurationValueByKey(constants.BackendTokenUnauthorizedKey, "true"); + await tscConfigurationService.SetConfigurationValueByKey(constants.FleetApiTokenUnauthorizedKey, "true"); logger.LogError( "Your token or refresh token is invalid. Response: {responseString}", responseString); } diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs index d295d2506..19544c3e8 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs @@ -1,70 +1,57 @@ using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json; using TeslaSolarCharger.Model.Contracts; using TeslaSolarCharger.Shared.Contracts; -using TeslaSolarCharger.Shared.Dtos.Contracts; using TeslaSolarCharger.Shared.Enums; using TeslaSolarCharger.Shared.Resources.Contracts; -using System.Globalization; using TeslaSolarCharger.Server.Services.Contracts; +using TeslaSolarCharger.Shared.Dtos; namespace TeslaSolarCharger.Server.Services; public class TeslaFleetApiTokenHelper(ILogger logger, - ISettings settings, ITeslaSolarChargerContext teslaSolarChargerContext, IConstants constants, - IDateTimeProvider dateTimeProvider, - ITscConfigurationService tscConfigurationService) : ITeslaFleetApiTokenHelper + ITscConfigurationService tscConfigurationService, + IConfigurationWrapper configurationWrapper) : ITeslaFleetApiTokenHelper { public async Task GetFleetApiTokenState() { logger.LogTrace("{method}()", nameof(GetFleetApiTokenState)); - if (!settings.AllowUnlimitedFleetApiRequests) - { - return FleetApiTokenState.NoApiRequestsAllowed; - } var hasCurrentTokenMissingScopes = await teslaSolarChargerContext.TscConfigurations .Where(c => c.Key == constants.TokenMissingScopes) .AnyAsync().ConfigureAwait(false); if (hasCurrentTokenMissingScopes) { - return FleetApiTokenState.MissingScopes; + return FleetApiTokenState.FleetApiTokenMissingScopes; } - var token = await teslaSolarChargerContext.BackendTokens.FirstOrDefaultAsync().ConfigureAwait(false); - if (token != default) + var isTokenUnauthorized = string.Equals(await tscConfigurationService.GetConfigurationValueByKey(constants.FleetApiTokenUnauthorizedKey), "true", StringComparison.InvariantCultureIgnoreCase); + if (isTokenUnauthorized) { - var isTokenUnauthorized = string.Equals(await tscConfigurationService.GetConfigurationValueByKey(constants.BackendTokenUnauthorizedKey), "true", StringComparison.InvariantCultureIgnoreCase); - if (isTokenUnauthorized) - { - return FleetApiTokenState.TokenUnauthorized; - } - return (token.ExpiresAtUtc < dateTimeProvider.DateTimeOffSetUtcNow().AddMinutes(2)) ? FleetApiTokenState.Expired : FleetApiTokenState.UpToDate; + return FleetApiTokenState.FleetApiTokenUnauthorized; } - var tokenRequestedDate = await GetTokenRequestedDate().ConfigureAwait(false); - if (tokenRequestedDate == null) + var url = configurationWrapper.BackendApiBaseUrl() + "FleetApiRequests/AnyFleetApiTokenWithExpiryInFuture"; + using var httpClient = new HttpClient(); + httpClient.Timeout = TimeSpan.FromSeconds(10); + var request = new HttpRequestMessage(HttpMethod.Get, url); + var token = await teslaSolarChargerContext.BackendTokens.SingleOrDefaultAsync().ConfigureAwait(false); + if (token == default) { - return FleetApiTokenState.NotRequested; + return FleetApiTokenState.NoBackendApiToken; } - var currentDate = dateTimeProvider.UtcNow(); - if (tokenRequestedDate < (currentDate - constants.MaxTokenRequestWaitTime)) + request.Headers.Authorization = new("Bearer", token.AccessToken); + var response = await httpClient.SendAsync(request).ConfigureAwait(false); + var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + if (!response.IsSuccessStatusCode) { - return FleetApiTokenState.TokenRequestExpired; + logger.LogError("Could not check if token is valid. StatusCode: {statusCode}, resultBody: {resultBody}", response.StatusCode, responseString); + return FleetApiTokenState.BackendTokenUnauthorized; } - return FleetApiTokenState.NotReceived; - } - - public async Task GetTokenRequestedDate() - { - logger.LogTrace("{method}()", nameof(GetTokenRequestedDate)); - var tokenRequestedDateString = await teslaSolarChargerContext.TscConfigurations - .Where(c => c.Key == constants.FleetApiTokenRequested) - .Select(c => c.Value) - .FirstOrDefaultAsync().ConfigureAwait(false); - if (tokenRequestedDateString == null) + var validFleetApiToken = JsonConvert.DeserializeObject>(responseString); + if (validFleetApiToken?.Value != true) { - return null; + return FleetApiTokenState.FleetApiTokenExpired; } - var tokenRequestedDate = DateTime.Parse(tokenRequestedDateString, null, DateTimeStyles.RoundtripKind); - return tokenRequestedDate; + return FleetApiTokenState.UpToDate; } } diff --git a/TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs b/TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs index 39d9ff8eb..dceda3895 100644 --- a/TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs +++ b/TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs @@ -2,13 +2,10 @@ public enum FleetApiTokenState { - NotNeeded, - NotRequested, - TokenRequestExpired, - TokenUnauthorized, - MissingScopes, - NotReceived, - Expired, + NoBackendApiToken, + BackendTokenUnauthorized, + FleetApiTokenUnauthorized, + FleetApiTokenMissingScopes, + FleetApiTokenExpired, UpToDate, - NoApiRequestsAllowed, } diff --git a/TeslaSolarCharger/Shared/Resources/Constants.cs b/TeslaSolarCharger/Shared/Resources/Constants.cs index 4b1a884a0..5e8e8ee2e 100644 --- a/TeslaSolarCharger/Shared/Resources/Constants.cs +++ b/TeslaSolarCharger/Shared/Resources/Constants.cs @@ -42,7 +42,7 @@ public class Constants : IConstants public string EmailConfigurationKey => "EmailConfiguration"; public string BackendPasswordConfigurationKey => "BackendPasswordConfiguration"; public string TeslaTokenEncryptionKeyKey => "TeslaTokenEncryptionKey"; - public string BackendTokenUnauthorizedKey => "BackendTokenUnauthorized"; + public string FleetApiTokenUnauthorizedKey => "BackendTokenUnauthorized"; public string GridPoleIcon => "power-pole"; } diff --git a/TeslaSolarCharger/Shared/Resources/Contracts/IConstants.cs b/TeslaSolarCharger/Shared/Resources/Contracts/IConstants.cs index cf8fbbefc..8ee1560e3 100644 --- a/TeslaSolarCharger/Shared/Resources/Contracts/IConstants.cs +++ b/TeslaSolarCharger/Shared/Resources/Contracts/IConstants.cs @@ -14,7 +14,6 @@ public interface IConstants int MinimumSocDifference { get; } string InstallationIdKey { get; } - string FleetApiTokenRequested { get; } string TokenMissingScopes { get; } string NextAllowedTeslaApiRequest { get; } string BackupZipBaseFileName { get; } @@ -39,5 +38,5 @@ public interface IConstants string VehicleRequestUrl { get; } string VehicleDataRequestUrl { get; } string TeslaTokenEncryptionKeyKey { get; } - string BackendTokenUnauthorizedKey { get; } + string FleetApiTokenUnauthorizedKey { get; } } From 6c1d7a47bf32c880bb579a41628b065647328c5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Mon, 30 Dec 2024 12:13:52 +0100 Subject: [PATCH 13/81] feat(TeslaFleetApiService): can use new backend --- .../Entities/TeslaSolarCharger/Car.cs | 5 - ...1230094540_RemoveCarRateLimits.Designer.cs | 901 ++++++++++++++++++ .../20241230094540_RemoveCarRateLimits.cs | 69 ++ .../TeslaSolarChargerContextModelSnapshot.cs | 15 - TeslaSolarCharger/Client/Pages/Index.razor | 12 - ...ponse.cs => DtoBackendApiTeslaResponse.cs} | 2 +- .../PossibleIssues/Contracts/IIssueKeys.cs | 2 +- .../Resources/PossibleIssues/IssueKeys.cs | 2 +- .../PossibleIssues/PossibleIssues.cs | 8 - .../Services/ApiServices/IndexService.cs | 5 - .../Server/Services/TeslaFleetApiService.cs | 119 +-- .../IndexRazor/CarValues/DtoCarBaseStates.cs | 21 - 12 files changed, 1002 insertions(+), 159 deletions(-) create mode 100644 TeslaSolarCharger.Model/Migrations/20241230094540_RemoveCarRateLimits.Designer.cs create mode 100644 TeslaSolarCharger.Model/Migrations/20241230094540_RemoveCarRateLimits.cs rename TeslaSolarCharger/Server/Dtos/Solar4CarBackend/{DtoTeslaResponse.cs => DtoBackendApiTeslaResponse.cs} (85%) diff --git a/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/Car.cs b/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/Car.cs index bd5ec6f18..800f79aa5 100644 --- a/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/Car.cs +++ b/TeslaSolarCharger.Model/Entities/TeslaSolarCharger/Car.cs @@ -40,11 +40,6 @@ public class Car public CarStateEnum? State { get; set; } public bool VehicleCommandProtocolRequired { get; set; } - public DateTime? VehicleRateLimitedUntil { get; set; } - public DateTime? VehicleDataRateLimitedUntil { get; set; } - public DateTime? CommandsRateLimitedUntil { get; set; } - public DateTime? WakeUpRateLimitedUntil { get; set; } - public DateTime? ChargingCommandsRateLimitedUntil { get; set; } public bool UseBle { get; set; } public string? BleApiBaseUrl { get; set; } public bool UseFleetTelemetry { get; set; } diff --git a/TeslaSolarCharger.Model/Migrations/20241230094540_RemoveCarRateLimits.Designer.cs b/TeslaSolarCharger.Model/Migrations/20241230094540_RemoveCarRateLimits.Designer.cs new file mode 100644 index 000000000..b19e82170 --- /dev/null +++ b/TeslaSolarCharger.Model/Migrations/20241230094540_RemoveCarRateLimits.Designer.cs @@ -0,0 +1,901 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TeslaSolarCharger.Model.EntityFramework; + +#nullable disable + +namespace TeslaSolarCharger.Model.Migrations +{ + [DbContext(typeof(TeslaSolarChargerContext))] + [Migration("20241230094540_RemoveCarRateLimits")] + partial class RemoveCarRateLimits + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.BackendNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BackendIssueId") + .HasColumnType("INTEGER"); + + b.Property("DetailText") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Headline") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsConfirmed") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("ValidFromDate") + .HasColumnType("TEXT"); + + b.Property("ValidFromVersion") + .HasColumnType("TEXT"); + + b.Property("ValidToDate") + .HasColumnType("TEXT"); + + b.Property("ValidToVersion") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("BackendNotifications"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.BackendToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ExpiresAtUtc") + .HasColumnType("TEXT"); + + b.Property("RefreshToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("BackendTokens"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.CachedCarState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CarId") + .HasColumnType("INTEGER"); + + b.Property("CarStateJson") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastUpdated") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("CachedCarStates"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.Car", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BleApiBaseUrl") + .HasColumnType("TEXT"); + + b.Property("ChargeMode") + .HasColumnType("INTEGER"); + + b.Property("ChargeStartCalls") + .HasColumnType("TEXT"); + + b.Property("ChargeStopCalls") + .HasColumnType("TEXT"); + + b.Property("ChargerActualCurrent") + .HasColumnType("INTEGER"); + + b.Property("ChargerPhases") + .HasColumnType("INTEGER"); + + b.Property("ChargerPilotCurrent") + .HasColumnType("INTEGER"); + + b.Property("ChargerRequestedCurrent") + .HasColumnType("INTEGER"); + + b.Property("ChargerVoltage") + .HasColumnType("INTEGER"); + + b.Property("ChargingPriority") + .HasColumnType("INTEGER"); + + b.Property("ClimateOn") + .HasColumnType("INTEGER"); + + b.Property("IgnoreLatestTimeToReachSocDate") + .HasColumnType("INTEGER"); + + b.Property("IgnoreLatestTimeToReachSocDateOnWeekend") + .HasColumnType("INTEGER"); + + b.Property("IsAvailableInTeslaAccount") + .HasColumnType("INTEGER"); + + b.Property("LatestTimeToReachSoC") + .HasColumnType("TEXT"); + + b.Property("Latitude") + .HasColumnType("REAL"); + + b.Property("Longitude") + .HasColumnType("REAL"); + + b.Property("MaximumAmpere") + .HasColumnType("INTEGER"); + + b.Property("MinimumAmpere") + .HasColumnType("INTEGER"); + + b.Property("MinimumSoc") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OtherCommandCalls") + .HasColumnType("TEXT"); + + b.Property("PluggedIn") + .HasColumnType("INTEGER"); + + b.Property("SetChargingAmpsCall") + .HasColumnType("TEXT"); + + b.Property("ShouldBeManaged") + .HasColumnType("INTEGER"); + + b.Property("SoC") + .HasColumnType("INTEGER"); + + b.Property("SocLimit") + .HasColumnType("INTEGER"); + + b.Property("State") + .HasColumnType("INTEGER"); + + b.Property("TeslaFleetApiState") + .HasColumnType("INTEGER"); + + b.Property("TeslaMateCarId") + .HasColumnType("INTEGER"); + + b.Property("UsableEnergy") + .HasColumnType("INTEGER"); + + b.Property("UseBle") + .HasColumnType("INTEGER"); + + b.Property("UseFleetTelemetry") + .HasColumnType("INTEGER"); + + b.Property("UseFleetTelemetryForLocationData") + .HasColumnType("INTEGER"); + + b.Property("VehicleCalls") + .HasColumnType("TEXT"); + + b.Property("VehicleCommandProtocolRequired") + .HasColumnType("INTEGER"); + + b.Property("VehicleDataCalls") + .HasColumnType("TEXT"); + + b.Property("Vin") + .HasColumnType("TEXT"); + + b.Property("WakeUpCalls") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("TeslaMateCarId") + .IsUnique(); + + b.HasIndex("Vin") + .IsUnique(); + + b.ToTable("Cars"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.CarValueLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BooleanValue") + .HasColumnType("INTEGER"); + + b.Property("CarId") + .HasColumnType("INTEGER"); + + b.Property("DoubleValue") + .HasColumnType("REAL"); + + b.Property("IntValue") + .HasColumnType("INTEGER"); + + b.Property("InvalidValue") + .HasColumnType("INTEGER"); + + b.Property("Source") + .HasColumnType("INTEGER"); + + b.Property("StringValue") + .HasColumnType("TEXT"); + + b.Property("Timestamp") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UnknownValue") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CarId"); + + b.ToTable("CarValueLogs"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ChargePrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddSpotPriceToGridPrice") + .HasColumnType("INTEGER"); + + b.Property("EnergyProvider") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(6); + + b.Property("EnergyProviderConfiguration") + .HasColumnType("TEXT"); + + b.Property("GridPrice") + .HasColumnType("TEXT"); + + b.Property("SolarPrice") + .HasColumnType("TEXT"); + + b.Property("SpotPriceCorrectionFactor") + .HasColumnType("TEXT"); + + b.Property("ValidSince") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ChargePrices"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ChargingDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChargerVoltage") + .HasColumnType("INTEGER"); + + b.Property("ChargingProcessId") + .HasColumnType("INTEGER"); + + b.Property("GridPower") + .HasColumnType("INTEGER"); + + b.Property("HomeBatteryPower") + .HasColumnType("INTEGER"); + + b.Property("SolarPower") + .HasColumnType("INTEGER"); + + b.Property("TimeStamp") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ChargingProcessId"); + + b.ToTable("ChargingDetails"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ChargingProcess", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CarId") + .HasColumnType("INTEGER"); + + b.Property("Cost") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("OldHandledChargeId") + .HasColumnType("INTEGER"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("UsedGridEnergyKwh") + .HasColumnType("TEXT"); + + b.Property("UsedHomeBatteryEnergyKwh") + .HasColumnType("TEXT"); + + b.Property("UsedSolarEnergyKwh") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CarId"); + + b.ToTable("ChargingProcesses"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.HandledCharge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AverageSpotPrice") + .HasColumnType("TEXT"); + + b.Property("CalculatedPrice") + .HasColumnType("TEXT"); + + b.Property("CarId") + .HasColumnType("INTEGER"); + + b.Property("ChargingProcessId") + .HasColumnType("INTEGER"); + + b.Property("UsedGridEnergy") + .HasColumnType("TEXT"); + + b.Property("UsedSolarEnergy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("HandledCharges"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.LoggedError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DismissedAt") + .HasColumnType("TEXT"); + + b.Property("EndTimeStamp") + .HasColumnType("TEXT"); + + b.Property("FurtherOccurrences") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Headline") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IssueKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Message") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("MethodName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Source") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StackTrace") + .HasColumnType("TEXT"); + + b.Property("StartTimeStamp") + .HasColumnType("TEXT"); + + b.Property("TelegramNotificationSent") + .HasColumnType("INTEGER"); + + b.Property("TelegramResolvedMessageSent") + .HasColumnType("INTEGER"); + + b.Property("Vin") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("LoggedErrors"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ModbusConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConnectDelayMilliseconds") + .HasColumnType("INTEGER"); + + b.Property("Endianess") + .HasColumnType("INTEGER"); + + b.Property("Host") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.Property("ReadTimeoutMilliseconds") + .HasColumnType("INTEGER"); + + b.Property("UnitIdentifier") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ModbusConfigurations"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ModbusResultConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Address") + .HasColumnType("INTEGER"); + + b.Property("BitStartIndex") + .HasColumnType("INTEGER"); + + b.Property("CorrectionFactor") + .HasColumnType("TEXT"); + + b.Property("InvertedByModbusResultConfigurationId") + .HasColumnType("INTEGER"); + + b.Property("Length") + .HasColumnType("INTEGER"); + + b.Property("ModbusConfigurationId") + .HasColumnType("INTEGER"); + + b.Property("Operator") + .HasColumnType("INTEGER"); + + b.Property("RegisterType") + .HasColumnType("INTEGER"); + + b.Property("UsedFor") + .HasColumnType("INTEGER"); + + b.Property("ValueType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("InvertedByModbusResultConfigurationId"); + + b.HasIndex("ModbusConfigurationId"); + + b.ToTable("ModbusResultConfigurations"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.MqttConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Host") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Password") + .HasColumnType("TEXT"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MqttConfigurations"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.MqttResultConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CorrectionFactor") + .HasColumnType("TEXT"); + + b.Property("MqttConfigurationId") + .HasColumnType("INTEGER"); + + b.Property("NodePattern") + .HasColumnType("TEXT"); + + b.Property("NodePatternType") + .HasColumnType("INTEGER"); + + b.Property("Operator") + .HasColumnType("INTEGER"); + + b.Property("Topic") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UsedFor") + .HasColumnType("INTEGER"); + + b.Property("XmlAttributeHeaderName") + .HasColumnType("TEXT"); + + b.Property("XmlAttributeHeaderValue") + .HasColumnType("TEXT"); + + b.Property("XmlAttributeValueName") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MqttConfigurationId"); + + b.ToTable("MqttResultConfigurations"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.PowerDistribution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChargingPower") + .HasColumnType("INTEGER"); + + b.Property("GridProportion") + .HasColumnType("REAL"); + + b.Property("HandledChargeId") + .HasColumnType("INTEGER"); + + b.Property("PowerFromGrid") + .HasColumnType("INTEGER"); + + b.Property("TimeStamp") + .HasColumnType("TEXT"); + + b.Property("UsedWattHours") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.HasIndex("HandledChargeId"); + + b.ToTable("PowerDistributions"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.RestValueConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("HttpMethod") + .HasColumnType("INTEGER"); + + b.Property("NodePatternType") + .HasColumnType("INTEGER"); + + b.Property("Url") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("RestValueConfigurations"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.RestValueConfigurationHeader", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RestValueConfigurationId") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RestValueConfigurationId", "Key") + .IsUnique(); + + b.ToTable("RestValueConfigurationHeaders"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.RestValueResultConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CorrectionFactor") + .HasColumnType("TEXT"); + + b.Property("NodePattern") + .HasColumnType("TEXT"); + + b.Property("Operator") + .HasColumnType("INTEGER"); + + b.Property("RestValueConfigurationId") + .HasColumnType("INTEGER"); + + b.Property("UsedFor") + .HasColumnType("INTEGER"); + + b.Property("XmlAttributeHeaderName") + .HasColumnType("TEXT"); + + b.Property("XmlAttributeHeaderValue") + .HasColumnType("TEXT"); + + b.Property("XmlAttributeValueName") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RestValueConfigurationId"); + + b.ToTable("RestValueResultConfigurations"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.SpotPrice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SpotPrices"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.TscConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("TscConfigurations"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.CarValueLog", b => + { + b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.Car", "Car") + .WithMany("CarValueLogs") + .HasForeignKey("CarId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Car"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ChargingDetail", b => + { + b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ChargingProcess", "ChargingProcess") + .WithMany("ChargingDetails") + .HasForeignKey("ChargingProcessId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChargingProcess"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ChargingProcess", b => + { + b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.Car", "Car") + .WithMany("ChargingProcesses") + .HasForeignKey("CarId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Car"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ModbusResultConfiguration", b => + { + b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ModbusResultConfiguration", "InvertedByModbusResultConfiguration") + .WithMany() + .HasForeignKey("InvertedByModbusResultConfigurationId"); + + b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ModbusConfiguration", "ModbusConfiguration") + .WithMany("ModbusResultConfigurations") + .HasForeignKey("ModbusConfigurationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("InvertedByModbusResultConfiguration"); + + b.Navigation("ModbusConfiguration"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.MqttResultConfiguration", b => + { + b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.MqttConfiguration", "MqttConfiguration") + .WithMany("MqttResultConfigurations") + .HasForeignKey("MqttConfigurationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MqttConfiguration"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.PowerDistribution", b => + { + b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.HandledCharge", "HandledCharge") + .WithMany("PowerDistributions") + .HasForeignKey("HandledChargeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("HandledCharge"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.RestValueConfigurationHeader", b => + { + b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.RestValueConfiguration", "RestValueConfiguration") + .WithMany("Headers") + .HasForeignKey("RestValueConfigurationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RestValueConfiguration"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.RestValueResultConfiguration", b => + { + b.HasOne("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.RestValueConfiguration", "RestValueConfiguration") + .WithMany("RestValueResultConfigurations") + .HasForeignKey("RestValueConfigurationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RestValueConfiguration"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.Car", b => + { + b.Navigation("CarValueLogs"); + + b.Navigation("ChargingProcesses"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ChargingProcess", b => + { + b.Navigation("ChargingDetails"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.HandledCharge", b => + { + b.Navigation("PowerDistributions"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.ModbusConfiguration", b => + { + b.Navigation("ModbusResultConfigurations"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.MqttConfiguration", b => + { + b.Navigation("MqttResultConfigurations"); + }); + + modelBuilder.Entity("TeslaSolarCharger.Model.Entities.TeslaSolarCharger.RestValueConfiguration", b => + { + b.Navigation("Headers"); + + b.Navigation("RestValueResultConfigurations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TeslaSolarCharger.Model/Migrations/20241230094540_RemoveCarRateLimits.cs b/TeslaSolarCharger.Model/Migrations/20241230094540_RemoveCarRateLimits.cs new file mode 100644 index 000000000..6e0e09b42 --- /dev/null +++ b/TeslaSolarCharger.Model/Migrations/20241230094540_RemoveCarRateLimits.cs @@ -0,0 +1,69 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TeslaSolarCharger.Model.Migrations +{ + /// + public partial class RemoveCarRateLimits : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ChargingCommandsRateLimitedUntil", + table: "Cars"); + + migrationBuilder.DropColumn( + name: "CommandsRateLimitedUntil", + table: "Cars"); + + migrationBuilder.DropColumn( + name: "VehicleDataRateLimitedUntil", + table: "Cars"); + + migrationBuilder.DropColumn( + name: "VehicleRateLimitedUntil", + table: "Cars"); + + migrationBuilder.DropColumn( + name: "WakeUpRateLimitedUntil", + table: "Cars"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ChargingCommandsRateLimitedUntil", + table: "Cars", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "CommandsRateLimitedUntil", + table: "Cars", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "VehicleDataRateLimitedUntil", + table: "Cars", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "VehicleRateLimitedUntil", + table: "Cars", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "WakeUpRateLimitedUntil", + table: "Cars", + type: "TEXT", + nullable: true); + } + } +} diff --git a/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs b/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs index e2977bd83..5c892d416 100644 --- a/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs +++ b/TeslaSolarCharger.Model/Migrations/TeslaSolarChargerContextModelSnapshot.cs @@ -136,18 +136,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ChargerVoltage") .HasColumnType("INTEGER"); - b.Property("ChargingCommandsRateLimitedUntil") - .HasColumnType("TEXT"); - b.Property("ChargingPriority") .HasColumnType("INTEGER"); b.Property("ClimateOn") .HasColumnType("INTEGER"); - b.Property("CommandsRateLimitedUntil") - .HasColumnType("TEXT"); - b.Property("IgnoreLatestTimeToReachSocDate") .HasColumnType("INTEGER"); @@ -226,21 +220,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("VehicleDataCalls") .HasColumnType("TEXT"); - b.Property("VehicleDataRateLimitedUntil") - .HasColumnType("TEXT"); - - b.Property("VehicleRateLimitedUntil") - .HasColumnType("TEXT"); - b.Property("Vin") .HasColumnType("TEXT"); b.Property("WakeUpCalls") .HasColumnType("TEXT"); - b.Property("WakeUpRateLimitedUntil") - .HasColumnType("TEXT"); - b.HasKey("Id"); b.HasIndex("TeslaMateCarId") diff --git a/TeslaSolarCharger/Client/Pages/Index.razor b/TeslaSolarCharger/Client/Pages/Index.razor index cdd31209e..f410e245f 100644 --- a/TeslaSolarCharger/Client/Pages/Index.razor +++ b/TeslaSolarCharger/Client/Pages/Index.razor @@ -71,18 +71,6 @@ else @car.HomeChargePower W
- - @if (DateTime.UtcNow < car.RateLimitedUntil) - { - - } @if (_testingFleetApiCarIds.Any(i => i == car.CarId)) {
diff --git a/TeslaSolarCharger/Server/Dtos/Solar4CarBackend/DtoTeslaResponse.cs b/TeslaSolarCharger/Server/Dtos/Solar4CarBackend/DtoBackendApiTeslaResponse.cs similarity index 85% rename from TeslaSolarCharger/Server/Dtos/Solar4CarBackend/DtoTeslaResponse.cs rename to TeslaSolarCharger/Server/Dtos/Solar4CarBackend/DtoBackendApiTeslaResponse.cs index 03e7c8191..1919b3bf8 100644 --- a/TeslaSolarCharger/Server/Dtos/Solar4CarBackend/DtoTeslaResponse.cs +++ b/TeslaSolarCharger/Server/Dtos/Solar4CarBackend/DtoBackendApiTeslaResponse.cs @@ -1,6 +1,6 @@ namespace TeslaSolarCharger.Server.Dtos.Solar4CarBackend; -public class DtoTeslaResponse +public class DtoBackendApiTeslaResponse { public int StatusCode { get; set; } public string? JsonResponse { get; set; } diff --git a/TeslaSolarCharger/Server/Resources/PossibleIssues/Contracts/IIssueKeys.cs b/TeslaSolarCharger/Server/Resources/PossibleIssues/Contracts/IIssueKeys.cs index 7caca45d7..7530fec4a 100644 --- a/TeslaSolarCharger/Server/Resources/PossibleIssues/Contracts/IIssueKeys.cs +++ b/TeslaSolarCharger/Server/Resources/PossibleIssues/Contracts/IIssueKeys.cs @@ -15,7 +15,6 @@ public interface IIssueKeys string FleetApiNonSuccessStatusCode { get; } string FleetApiNonSuccessResult { get; } string UnsignedCommand { get; } - string CarRateLimited { get; } string BleCommandNoSuccess { get; } string SolarValuesNotAvailable { get; } string UsingFleetApiAsBleFallback { get; } @@ -23,4 +22,5 @@ public interface IIssueKeys string NoBackendApiToken { get; } string BackendTokenUnauthorized { get; } string FleetApiTokenExpired { get; } + string Solar4CarSideFleetApiNonSuccessStatusCode { get; } } diff --git a/TeslaSolarCharger/Server/Resources/PossibleIssues/IssueKeys.cs b/TeslaSolarCharger/Server/Resources/PossibleIssues/IssueKeys.cs index 288b94e2c..28bdb9b31 100644 --- a/TeslaSolarCharger/Server/Resources/PossibleIssues/IssueKeys.cs +++ b/TeslaSolarCharger/Server/Resources/PossibleIssues/IssueKeys.cs @@ -15,9 +15,9 @@ public class IssueKeys : IIssueKeys public string GetVehicleData => "GetVehicleData"; public string CarStateUnknown => "CarStateUnknown"; public string FleetApiNonSuccessStatusCode => "FleetApiNonSuccessStatusCode_"; + public string Solar4CarSideFleetApiNonSuccessStatusCode => "Solar4CarSideFleetApiNonSuccessStatusCode_"; public string FleetApiNonSuccessResult => "FleetApiNonSuccessResult_"; public string UnsignedCommand => "UnsignedCommand"; - public string CarRateLimited => "CarRateLimited"; public string BleCommandNoSuccess => "BleCommandNoSuccess_"; public string SolarValuesNotAvailable => "SolarValuesNotAvailable"; public string UsingFleetApiAsBleFallback => "UsingFleetApiAsBleFallback"; diff --git a/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs b/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs index 637f85868..1ebdab2f0 100644 --- a/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs +++ b/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs @@ -117,14 +117,6 @@ public class PossibleIssues(IIssueKeys issueKeys) : IPossibleIssues HasPlaceHolderIssueKey = false, } }, - { issueKeys.CarRateLimited, new DtoIssue - { - IssueSeverity = IssueSeverity.Error, - IsTelegramEnabled = true, - ShowErrorAfterOccurrences = 1, - HasPlaceHolderIssueKey = false, - } - }, { issueKeys.BleCommandNoSuccess, new DtoIssue { IssueSeverity = IssueSeverity.Error, diff --git a/TeslaSolarCharger/Server/Services/ApiServices/IndexService.cs b/TeslaSolarCharger/Server/Services/ApiServices/IndexService.cs index 9edd92dda..25e092eb9 100644 --- a/TeslaSolarCharger/Server/Services/ApiServices/IndexService.cs +++ b/TeslaSolarCharger/Server/Services/ApiServices/IndexService.cs @@ -81,11 +81,6 @@ public async Task> GetCarBaseStatesOfEnabledCars() var dbCar = await teslaSolarChargerContext.Cars.Where(c => c.Id == enabledCar.Id).SingleAsync(); dtoCarBaseValues.FleetApiState = dbCar.TeslaFleetApiState; - dtoCarBaseValues.VehicleRateLimitedUntil = dbCar.VehicleRateLimitedUntil; - dtoCarBaseValues.VehicleDataRateLimitedUntil = dbCar.VehicleDataRateLimitedUntil; - dtoCarBaseValues.CommandsRateLimitedUntil = dbCar.CommandsRateLimitedUntil; - dtoCarBaseValues.ChargingCommandsRateLimitedUntil = dbCar.ChargingCommandsRateLimitedUntil; - dtoCarBaseValues.WakeUpRateLimitedUntil = dbCar.WakeUpRateLimitedUntil; dtoCarBaseValues.ChargeInformation = GenerateChargeInformation(enabledCar); dtoCarBaseValues.ModuleTemperatureMin = await teslaSolarChargerContext.CarValueLogs .Where(c => c.CarId == enabledCar.Id && c.Type == CarValueType.ModuleTempMin) diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index 160029aa0..727a73a0b 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -861,16 +861,6 @@ await errorHandlingService.HandleError(nameof(TeslaFleetApiService), nameof(Send } using var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.AccessToken); - var rateLimitedUntil = await RateLimitedUntil(vin, fleetApiRequest.TeslaApiRequestType).ConfigureAwait(false); - var currentDate = dateTimeProvider.UtcNow(); - if (currentDate < rateLimitedUntil) - { - logger.LogError("Car with VIN {vin} rate limited until {rateLimitedUntil}. Skipping command.", vin, rateLimitedUntil); - await errorHandlingService.HandleError(nameof(TeslaFleetApiService), nameof(SendCommandToTeslaApi), $"Car {car.Vin} is rate limited", - $"Car is rate limited until {rateLimitedUntil}", issueKeys.CarRateLimited, car.Vin, null); - return null; - } - await errorHandlingService.HandleErrorResolved(issueKeys.CarRateLimited, car.Vin); var fleetApiProxyRequired = await IsFleetApiProxyEnabled(vin).ConfigureAwait(false); var baseUrl = configurationWrapper.BackendApiBaseUrl(); var decryptionKey = await tscConfigurationService.GetConfigurationValueByKey(constants.TeslaTokenEncryptionKeyKey); @@ -896,24 +886,43 @@ await errorHandlingService.HandleError(nameof(TeslaFleetApiService), nameof(Send logger.LogTrace("Response string: {responseString}", responseString); logger.LogTrace("Response headers: {@headers}", response.Headers); if (response.IsSuccessStatusCode) + { + await errorHandlingService.HandleErrorResolved(issueKeys.Solar4CarSideFleetApiNonSuccessStatusCode + fleetApiRequest.RequestUrl, car.Vin); + } + else + { + await errorHandlingService.HandleError(nameof(TeslaFleetApiService), nameof(SendCommandToTeslaApi), $"Solar4Car related error while sending command to car {car.Vin}", + $"Sending command to Tesla API resulted in non succes status code. The issue very likely is not on Tesla's side but on Solar4Car side. Status Code: {response.StatusCode} : Command name:{fleetApiRequest.RequestUrl}, Int Param:{intParam}. Response string: {responseString}", + issueKeys.Solar4CarSideFleetApiNonSuccessStatusCode + fleetApiRequest.RequestUrl, car.Vin, null).ConfigureAwait(false); + logger.LogError("Sending command to Tesla API resulted in non succes status code. The issue very likely is not on Tesla's side but on Solar4Car side. Status Code: {statusCode} : Command name:{commandName}, Int Param:{intParam}. Response string: {responseString}", response.StatusCode, fleetApiRequest.RequestUrl, intParam, responseString); + return null; + } + var backendApiResponse = JsonConvert.DeserializeObject(responseString); + if (backendApiResponse == default) + { + logger.LogError("Could not deserialize Backend API Tesla Response: {responseString}", responseString); + return null; + } + + if (backendApiResponse.StatusCode is >= 200 and < 300) { AddRequestToCar(vin, fleetApiRequest); await errorHandlingService.HandleErrorResolved(issueKeys.FleetApiNonSuccessStatusCode + fleetApiRequest.RequestUrl, car.Vin); } else { - await errorHandlingService.HandleError(nameof(TeslaFleetApiService), nameof(SendCommandToTeslaApi), $"Error while sending command to car {car.Vin}", - $"Sending command to Tesla API resulted in non succes status code: {response.StatusCode} : Command name:{fleetApiRequest.RequestUrl}, Int Param:{intParam}. Response string: {responseString}", + await errorHandlingService.HandleError(nameof(TeslaFleetApiService), nameof(SendCommandToTeslaApi), $"Tesla related error while sending command to car {car.Vin}", + $"Sending command to Tesla API resulted in non succes status code: {backendApiResponse.StatusCode} : Command name:{fleetApiRequest.RequestUrl}, Int Param:{intParam}. Response string: {responseString}", issueKeys.FleetApiNonSuccessStatusCode + fleetApiRequest.RequestUrl, car.Vin, null).ConfigureAwait(false); logger.LogError("Sending command to Tesla API resulted in non succes status code: {statusCode} : Command name:{commandName}, Int Param:{intParam}. Response string: {responseString}", response.StatusCode, fleetApiRequest.RequestUrl, intParam, responseString); - await HandleNonSuccessTeslaApiStatusCodes(response.StatusCode, accessToken, responseString, fleetApiRequest.TeslaApiRequestType, vin).ConfigureAwait(false); - if (response.StatusCode == HttpStatusCode.ServiceUnavailable) - { - return new() { Error = responseString, ErrorDescription = "ServiceUnavailable", }; - } + return null; } - var teslaCommandResultResponse = JsonConvert.DeserializeObject>(responseString); - if (response.IsSuccessStatusCode && (teslaCommandResultResponse?.Response is DtoVehicleCommandResult vehicleCommandResult)) + + + //ToDo: should be able to handle null backend API response. e.g. with an error "incompatible version". + var teslaCommandResultResponse = JsonConvert.DeserializeObject>(backendApiResponse.JsonResponse); + + if (backendApiResponse.StatusCode is >= 200 and < 300 && (teslaCommandResultResponse?.Response is DtoVehicleCommandResult vehicleCommandResult)) { if (vehicleCommandResult.Result != true && !((fleetApiRequest.RequestUrl == ChargeStartRequest.RequestUrl) && responseString.Contains(IsChargingErrorMessage)) @@ -991,39 +1000,6 @@ private void AddRequestToCar(string vin, DtoFleetApiRequest fleetApiRequest) } } - private async Task RateLimitedUntil(string vin, TeslaApiRequestType teslaApiRequestType) - { - logger.LogTrace("{method}({vin}, {teslaApiRequestType})", nameof(RateLimitedUntil), vin, teslaApiRequestType); - var rateLimitedUntil = await GetRateLimitInfos(vin); - - return teslaApiRequestType switch - { - TeslaApiRequestType.Vehicle => rateLimitedUntil.VehicleRateLimitedUntil, - TeslaApiRequestType.VehicleData => rateLimitedUntil.VehicleDataRateLimitedUntil, - TeslaApiRequestType.Command => rateLimitedUntil.CommandsRateLimitedUntil, - TeslaApiRequestType.WakeUp => rateLimitedUntil.WakeUpRateLimitedUntil, - TeslaApiRequestType.Charging => rateLimitedUntil.ChargingCommandsRateLimitedUntil, - TeslaApiRequestType.Other => null, - _ => throw new ArgumentOutOfRangeException(nameof(teslaApiRequestType), teslaApiRequestType, null) - }; - } - - private async Task GetRateLimitInfos(string vin) - { - var rateLimitedUntil = await teslaSolarChargerContext.Cars - .Where(c => c.Vin == vin) - .Select(c => new DtoRateLimitInfos() - { - VehicleRateLimitedUntil = c.VehicleRateLimitedUntil, - VehicleDataRateLimitedUntil = c.VehicleDataRateLimitedUntil, - CommandsRateLimitedUntil = c.CommandsRateLimitedUntil, - ChargingCommandsRateLimitedUntil = c.ChargingCommandsRateLimitedUntil, - WakeUpRateLimitedUntil = c.WakeUpRateLimitedUntil, - }) - .FirstAsync(); - return rateLimitedUntil; - } - private async Task HandleUnsignedCommands(DtoVehicleCommandResult vehicleCommandResult, string vin) { if (string.Equals(vehicleCommandResult.Reason, "unsigned_cmds_hardlocked")) @@ -1117,34 +1093,6 @@ private async Task HandleNonSuccessTeslaApiStatusCodes(HttpStatusCode statusCode var car = teslaSolarChargerContext.Cars.First(c => c.Vin == vin); car.TeslaFleetApiState = TeslaCarFleetApiState.NotWorking; } - else if (statusCode == HttpStatusCode.TooManyRequests) - { - logger.LogWarning("Too many requests to Tesla API for car {vin}. {responseString}", vin, responseString); - var car = teslaSolarChargerContext.Cars.First(c => c.Vin == vin); - var nextAllowedUtcTime = GetUtcTimeFromRetryInSeconds(responseString); - switch (teslaApiRequestType) - { - case TeslaApiRequestType.Vehicle: - car.VehicleRateLimitedUntil = nextAllowedUtcTime; - break; - case TeslaApiRequestType.VehicleData: - car.VehicleDataRateLimitedUntil = nextAllowedUtcTime; - break; - case TeslaApiRequestType.Command: - car.CommandsRateLimitedUntil = nextAllowedUtcTime; - break; - case TeslaApiRequestType.WakeUp: - car.WakeUpRateLimitedUntil = nextAllowedUtcTime; - break; - case TeslaApiRequestType.Charging: - car.ChargingCommandsRateLimitedUntil = nextAllowedUtcTime; - break; - case TeslaApiRequestType.Other: - break; - default: - throw new ArgumentOutOfRangeException(nameof(teslaApiRequestType), teslaApiRequestType, null); - } - } else { logger.LogWarning( @@ -1156,15 +1104,6 @@ private async Task HandleNonSuccessTeslaApiStatusCodes(HttpStatusCode statusCode await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); } - private DateTime GetUtcTimeFromRetryInSeconds(string responseString) - { - logger.LogTrace("{method}({responseString})", nameof(GetUtcTimeFromRetryInSeconds), responseString); - - var retryInSeconds = RetryInSeconds(responseString); - var nextAllowedUtcTime = dateTimeProvider.UtcNow().AddSeconds(retryInSeconds); - return nextAllowedUtcTime; - } - public async Task>> GetNewCarsInAccount() { logger.LogTrace("{method}()", nameof(GetNewCarsInAccount)); @@ -1216,7 +1155,7 @@ public async Task>> GetAllCarsFromAccount() return Fin>.Fail(Error.New(excpetion)); } - var teslaBackendResult = JsonConvert.DeserializeObject(responseBodyString); + var teslaBackendResult = JsonConvert.DeserializeObject(responseBodyString); if (teslaBackendResult == null) { logger.LogError("Could not deserialize Solar4CarBackend response body {responseBodyString}", responseBodyString); diff --git a/TeslaSolarCharger/Shared/Dtos/IndexRazor/CarValues/DtoCarBaseStates.cs b/TeslaSolarCharger/Shared/Dtos/IndexRazor/CarValues/DtoCarBaseStates.cs index 88759cc84..a723ca7b9 100644 --- a/TeslaSolarCharger/Shared/Dtos/IndexRazor/CarValues/DtoCarBaseStates.cs +++ b/TeslaSolarCharger/Shared/Dtos/IndexRazor/CarValues/DtoCarBaseStates.cs @@ -26,25 +26,4 @@ public class DtoCarBaseStates public List ChargeInformation { get; set; } = new(); public CarStateEnum? State { get; set; } public List ChargingSlots { get; set; } = new(); - - public DateTime? VehicleRateLimitedUntil { get; set; } - public DateTime? VehicleDataRateLimitedUntil { get; set; } - public DateTime? CommandsRateLimitedUntil { get; set; } - public DateTime? ChargingCommandsRateLimitedUntil { get; set; } - public DateTime? WakeUpRateLimitedUntil { get; set; } - - public DateTime? RateLimitedUntil - { - get - { - return new List - { - VehicleRateLimitedUntil, - VehicleDataRateLimitedUntil, - CommandsRateLimitedUntil, - ChargingCommandsRateLimitedUntil, - WakeUpRateLimitedUntil, - }.Max(); - } - } } From 692bd3f25c6410e4c020cf7bd2f1b3e8f8b60f8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Mon, 30 Dec 2024 16:40:55 +0100 Subject: [PATCH 14/81] feat(TeslaFleetApiTokenHelper): can handle additional token states --- .../Client/Pages/BaseConfiguration.razor | 15 +++++++++++++++ .../DtoBackendApiTeslaResponse.cs | 6 ++++-- .../PossibleIssues/Contracts/IIssueKeys.cs | 1 + .../Server/Resources/PossibleIssues/IssueKeys.cs | 1 + .../Server/Services/ErrorHandlingService.cs | 3 +++ .../Server/Services/TeslaFleetApiService.cs | 6 +++--- .../Server/Services/TeslaFleetApiTokenHelper.cs | 14 +++++++++++--- .../Shared/Enums/FleetApiTokenState.cs | 1 + 8 files changed, 39 insertions(+), 8 deletions(-) diff --git a/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor b/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor index 345c0ea1b..36cf10229 100644 --- a/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor +++ b/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor @@ -42,6 +42,16 @@ else You are not logged in in the Solar4car.com account. When requesting a new Tesla Token there will also be a request to log in to your Solar4car.com account.
break; + case FleetApiTokenState.BackendTokenUnauthorized: +
+ You are not logged in in the Solar4car.com account. When requesting a new Tesla Token there will also be a request to log in to your Solar4car.com account. +
+ break; + case FleetApiTokenState.NoFleetApiToken: +
+ You did not request a Fleet API Token, yet. Request a new token, allow access to all scopes and enable mobile access in your car. +
+ break; case FleetApiTokenState.FleetApiTokenUnauthorized:
Your token is unauthorized. Request a new token, allow access to all scopes and enable mobile access in your car. @@ -52,6 +62,11 @@ else Your token has missing scopes. Request a new Token and allow all scopes (only required scopes are requested).
break; + case FleetApiTokenState.FleetApiTokenExpired: +
+ Your Fleet API token is expired. Request a new Token and allow all scopes (only required scopes are requested). +
+ break; case FleetApiTokenState.UpToDate:
Everything is fine! If you want to generate a new token e.g. to switch to another Tesla Account please click the button below: diff --git a/TeslaSolarCharger/Server/Dtos/Solar4CarBackend/DtoBackendApiTeslaResponse.cs b/TeslaSolarCharger/Server/Dtos/Solar4CarBackend/DtoBackendApiTeslaResponse.cs index 1919b3bf8..a8ceb92e2 100644 --- a/TeslaSolarCharger/Server/Dtos/Solar4CarBackend/DtoBackendApiTeslaResponse.cs +++ b/TeslaSolarCharger/Server/Dtos/Solar4CarBackend/DtoBackendApiTeslaResponse.cs @@ -1,8 +1,10 @@ -namespace TeslaSolarCharger.Server.Dtos.Solar4CarBackend; +using System.Net; + +namespace TeslaSolarCharger.Server.Dtos.Solar4CarBackend; public class DtoBackendApiTeslaResponse { - public int StatusCode { get; set; } + public HttpStatusCode StatusCode { get; set; } public string? JsonResponse { get; set; } public string? Error { get; set; } public string? ErrorDescription { get; set; } diff --git a/TeslaSolarCharger/Server/Resources/PossibleIssues/Contracts/IIssueKeys.cs b/TeslaSolarCharger/Server/Resources/PossibleIssues/Contracts/IIssueKeys.cs index 7530fec4a..4478b362a 100644 --- a/TeslaSolarCharger/Server/Resources/PossibleIssues/Contracts/IIssueKeys.cs +++ b/TeslaSolarCharger/Server/Resources/PossibleIssues/Contracts/IIssueKeys.cs @@ -4,6 +4,7 @@ public interface IIssueKeys { string VersionNotUpToDate { get; } string FleetApiTokenUnauthorized { get; } + string NoFleetApiToken { get; } string FleetApiTokenMissingScopes { get; } string FleetApiTokenRequestExpired { get; } string FleetApiTokenRefreshNonSuccessStatusCode { get; } diff --git a/TeslaSolarCharger/Server/Resources/PossibleIssues/IssueKeys.cs b/TeslaSolarCharger/Server/Resources/PossibleIssues/IssueKeys.cs index 28bdb9b31..3573d58ab 100644 --- a/TeslaSolarCharger/Server/Resources/PossibleIssues/IssueKeys.cs +++ b/TeslaSolarCharger/Server/Resources/PossibleIssues/IssueKeys.cs @@ -6,6 +6,7 @@ public class IssueKeys : IIssueKeys { public string VersionNotUpToDate => "VersionNotUpToDate"; public string FleetApiTokenUnauthorized => "FleetApiTokenUnauthorized"; + public string NoFleetApiToken => "NoFleetApiToken"; public string FleetApiTokenMissingScopes => "FleetApiTokenMissingScopes"; public string FleetApiTokenRequestExpired => "FleetApiTokenRequestExpired"; public string FleetApiTokenRefreshNonSuccessStatusCode => "FleetApiTokenRefreshNonSuccessStatusCode"; diff --git a/TeslaSolarCharger/Server/Services/ErrorHandlingService.cs b/TeslaSolarCharger/Server/Services/ErrorHandlingService.cs index bb0337bdf..e2f5ee143 100644 --- a/TeslaSolarCharger/Server/Services/ErrorHandlingService.cs +++ b/TeslaSolarCharger/Server/Services/ErrorHandlingService.cs @@ -426,6 +426,9 @@ await AddOrRemoveErrors(activeErrors, issueKeys.BackendTokenUnauthorized, "Backe await AddOrRemoveErrors(activeErrors, issueKeys.FleetApiTokenUnauthorized, "Fleet API token is unauthorized", "You recently changed your Tesla password or did not enable mobile access in your car. Enable mobile access in your car and open the Base Configuration and request a new token. Important: You need to allow access to all selectable scopes.", tokenState == FleetApiTokenState.FleetApiTokenUnauthorized).ConfigureAwait(false); + await AddOrRemoveErrors(activeErrors, issueKeys.NoFleetApiToken, "No Fleet API Token available.", + "Open the Base Configuration and request a new token.", + tokenState == FleetApiTokenState.NoFleetApiToken).ConfigureAwait(false); await AddOrRemoveErrors(activeErrors, issueKeys.FleetApiTokenUnauthorized, "Fleet API token is expired", "Either you recently changed your Tesla password or did not enable mobile access in your car. Enable mobile access in your car and open the Base Configuration and request a new token. Important: You need to allow access to all selectable scopes.", tokenState == FleetApiTokenState.FleetApiTokenExpired).ConfigureAwait(false); diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index 727a73a0b..3899f9d59 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -904,7 +904,7 @@ await errorHandlingService.HandleError(nameof(TeslaFleetApiService), nameof(Send return null; } - if (backendApiResponse.StatusCode is >= 200 and < 300) + if (backendApiResponse.StatusCode is >= HttpStatusCode.OK and < HttpStatusCode.MultipleChoices) { AddRequestToCar(vin, fleetApiRequest); await errorHandlingService.HandleErrorResolved(issueKeys.FleetApiNonSuccessStatusCode + fleetApiRequest.RequestUrl, car.Vin); @@ -922,7 +922,7 @@ await errorHandlingService.HandleError(nameof(TeslaFleetApiService), nameof(Send //ToDo: should be able to handle null backend API response. e.g. with an error "incompatible version". var teslaCommandResultResponse = JsonConvert.DeserializeObject>(backendApiResponse.JsonResponse); - if (backendApiResponse.StatusCode is >= 200 and < 300 && (teslaCommandResultResponse?.Response is DtoVehicleCommandResult vehicleCommandResult)) + if ((backendApiResponse.StatusCode is >= HttpStatusCode.OK and < HttpStatusCode.MultipleChoices) && (teslaCommandResultResponse?.Response is DtoVehicleCommandResult vehicleCommandResult)) { if (vehicleCommandResult.Result != true && !((fleetApiRequest.RequestUrl == ChargeStartRequest.RequestUrl) && responseString.Contains(IsChargingErrorMessage)) @@ -1162,7 +1162,7 @@ public async Task>> GetAllCarsFromAccount() return Fin>.Fail($"Could not deserialize response body {responseBodyString}"); } - if (teslaBackendResult.StatusCode is >= 200 and < 300) + if (teslaBackendResult.StatusCode is >= HttpStatusCode.OK and < HttpStatusCode.MultipleChoices) { logger.LogError("Error while getting all cars from account due to communication issue between Solar4Car Backend and Tesla: Underlaying Status code: {statusCode}; Underlaying Response Body: {responseBodyString}", teslaBackendResult.StatusCode, teslaBackendResult.JsonResponse); var excpetion = new HttpRequestException($"Requesting {requestUri} returned following body: {responseBodyString}", null, diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs index 19544c3e8..112d8e305 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs @@ -30,7 +30,7 @@ public async Task GetFleetApiTokenState() { return FleetApiTokenState.FleetApiTokenUnauthorized; } - var url = configurationWrapper.BackendApiBaseUrl() + "FleetApiRequests/AnyFleetApiTokenWithExpiryInFuture"; + var url = configurationWrapper.BackendApiBaseUrl() + "FleetApiRequests/FleetApiTokenExpiresInSeconds"; using var httpClient = new HttpClient(); httpClient.Timeout = TimeSpan.FromSeconds(10); var request = new HttpRequestMessage(HttpMethod.Get, url); @@ -47,8 +47,16 @@ public async Task GetFleetApiTokenState() logger.LogError("Could not check if token is valid. StatusCode: {statusCode}, resultBody: {resultBody}", response.StatusCode, responseString); return FleetApiTokenState.BackendTokenUnauthorized; } - var validFleetApiToken = JsonConvert.DeserializeObject>(responseString); - if (validFleetApiToken?.Value != true) + var validFleetApiToken = JsonConvert.DeserializeObject>(responseString); + if (validFleetApiToken == null) + { + return FleetApiTokenState.NoFleetApiToken; + } + if (validFleetApiToken.Value == null) + { + return FleetApiTokenState.NoFleetApiToken; + } + if (validFleetApiToken.Value <= 0) { return FleetApiTokenState.FleetApiTokenExpired; } diff --git a/TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs b/TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs index dceda3895..fe7d37792 100644 --- a/TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs +++ b/TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs @@ -4,6 +4,7 @@ public enum FleetApiTokenState { NoBackendApiToken, BackendTokenUnauthorized, + NoFleetApiToken, FleetApiTokenUnauthorized, FleetApiTokenMissingScopes, FleetApiTokenExpired, From 1275ef501ccda26ede4ed24b09d9b82718b35340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Mon, 30 Dec 2024 19:20:00 +0100 Subject: [PATCH 15/81] fix(chore): various token encryption related issues --- .../Server/Resources/PossibleIssues/PossibleIssues.cs | 11 +++++++++++ .../Server/Services/BackendApiService.cs | 1 + .../Server/Services/TeslaFleetApiService.cs | 4 ++-- .../Server/Services/TeslaFleetApiTokenHelper.cs | 1 + .../Server/Services/TscConfigurationService.cs | 2 +- 5 files changed, 16 insertions(+), 3 deletions(-) diff --git a/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs b/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs index 1ebdab2f0..4556bb417 100644 --- a/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs +++ b/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs @@ -18,12 +18,22 @@ public class PossibleIssues(IIssueKeys issueKeys) : IPossibleIssues HideOccurrenceCount = true, } }, + { issueKeys.NoFleetApiToken, new DtoIssue + { + IssueSeverity = IssueSeverity.Error, + IsTelegramEnabled = false, + ShowErrorAfterOccurrences = 1, + HasPlaceHolderIssueKey = false, + HideOccurrenceCount = true, + } + }, { issueKeys.FleetApiTokenUnauthorized, new DtoIssue { IssueSeverity = IssueSeverity.Error, IsTelegramEnabled = true, ShowErrorAfterOccurrences = 1, HasPlaceHolderIssueKey = false, + HideOccurrenceCount = true, } }, { issueKeys.FleetApiTokenMissingScopes, new DtoIssue @@ -32,6 +42,7 @@ public class PossibleIssues(IIssueKeys issueKeys) : IPossibleIssues IsTelegramEnabled = true, ShowErrorAfterOccurrences = 1, HasPlaceHolderIssueKey = false, + HideOccurrenceCount = true, } }, { issueKeys.FleetApiTokenRequestExpired, new DtoIssue diff --git a/TeslaSolarCharger/Server/Services/BackendApiService.cs b/TeslaSolarCharger/Server/Services/BackendApiService.cs index 2bc72f4d6..ca0f72948 100644 --- a/TeslaSolarCharger/Server/Services/BackendApiService.cs +++ b/TeslaSolarCharger/Server/Services/BackendApiService.cs @@ -37,6 +37,7 @@ public async Task> StartTeslaOAuth(string locale, string baseUr await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); var backendApiBaseUrl = configurationWrapper.BackendApiBaseUrl(); var encryptionKey = passwordGenerationService.GeneratePassword(32); + await tscConfigurationService.SetConfigurationValueByKey(constants.TeslaTokenEncryptionKeyKey, encryptionKey).ConfigureAwait(false); var state = Guid.NewGuid(); var requestUri = $"{backendApiBaseUrl}Client/AddAuthenticationStartInformation?redirectUri={Uri.EscapeDataString(baseUrl)}&encryptionKey={Uri.EscapeDataString(encryptionKey)}&state={Uri.EscapeDataString(state.ToString())}"; using var httpClient = new HttpClient(); diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index 3899f9d59..c1cb99439 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -869,7 +869,7 @@ await errorHandlingService.HandleError(nameof(TeslaFleetApiService), nameof(Send logger.LogError("Decryption key not found do not send command"); return null; } - var requestUri = $"{baseUrl}{fleetApiRequest.RequestUrl}?encryptionKey={decryptionKey}&vin={vin}&carRequiresProxy={fleetApiProxyRequired.Value}"; + var requestUri = $"{baseUrl}{fleetApiRequest.RequestUrl}?encryptionKey={Uri.EscapeDataString(decryptionKey)}&vin={vin}&carRequiresProxy={fleetApiProxyRequired.Value}"; if (fleetApiRequest.RequestUrl == SetChargingAmpsRequest.RequestUrl) { requestUri += $"&s={intParam}"; @@ -1136,7 +1136,7 @@ public async Task>> GetAllCarsFromAccount() logger.LogError("Decryption key not found do not send command"); return Fin>.Fail("No Decryption key found."); } - var requestUri = $"{baseUrl}GetAllCarsFromAccount?encryptionKey={decryptionKey}"; + var requestUri = $"{baseUrl}FleetApiRequests/GetAllCarsFromAccount?encryptionKey={Uri.EscapeDataString(decryptionKey)}"; var request = new HttpRequestMessage() { diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs index 112d8e305..63165ca08 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs @@ -50,6 +50,7 @@ public async Task GetFleetApiTokenState() var validFleetApiToken = JsonConvert.DeserializeObject>(responseString); if (validFleetApiToken == null) { + logger.LogError("Could not check if fleet api token is available."); return FleetApiTokenState.NoFleetApiToken; } if (validFleetApiToken.Value == null) diff --git a/TeslaSolarCharger/Server/Services/TscConfigurationService.cs b/TeslaSolarCharger/Server/Services/TscConfigurationService.cs index f5fc88aef..589aae959 100644 --- a/TeslaSolarCharger/Server/Services/TscConfigurationService.cs +++ b/TeslaSolarCharger/Server/Services/TscConfigurationService.cs @@ -44,7 +44,7 @@ public async Task GetInstallationId() { logger.LogTrace("{method}({configurationKey})", nameof(GetConfigurationValueByKey), configurationKey); var configurationValue = await teslaSolarChargerContext.TscConfigurations - .FirstOrDefaultAsync(c => c.Key == constants.InstallationIdKey); + .FirstOrDefaultAsync(c => c.Key == configurationKey); return configurationValue?.Value; } From 50339bb5f87a0aa0c9842cab6e4a8f1283622f09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Mon, 30 Dec 2024 19:34:52 +0100 Subject: [PATCH 16/81] fix(Constants): update fleet api request urls --- TeslaSolarCharger/Shared/Resources/Constants.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/TeslaSolarCharger/Shared/Resources/Constants.cs b/TeslaSolarCharger/Shared/Resources/Constants.cs index 5e8e8ee2e..aeeb740f2 100644 --- a/TeslaSolarCharger/Shared/Resources/Constants.cs +++ b/TeslaSolarCharger/Shared/Resources/Constants.cs @@ -32,13 +32,13 @@ public class Constants : IConstants public TimeSpan MinTokenRestLifetime => TimeSpan.FromMinutes(2); public int MaxTokenUnauthorizedCount => 5; public int ChargingDetailsAddTriggerEveryXSeconds => 59; - public string ChargeStartRequestUrl => "ChargeStart"; - public string ChargeStopRequestUrl => "ChargeStop"; - public string SetChargingAmpsRequestUrl => "SetChargingAmps"; - public string SetChargeLimitRequestUrl => "SetChargeLimit"; - public string WakeUpRequestUrl => "wake_up"; - public string VehicleRequestUrl => ""; - public string VehicleDataRequestUrl => $"vehicle_data?endpoints={Uri.EscapeDataString("drive_state;location_data;vehicle_state;charge_state;climate_state")}"; + public string ChargeStartRequestUrl => "FleetApiRequests/ChargeStart"; + public string ChargeStopRequestUrl => "FleetApiRequests/ChargeStop"; + public string SetChargingAmpsRequestUrl => "FleetApiRequests/SetChargingAmps"; + public string SetChargeLimitRequestUrl => "FleetApiRequests/SetChargeLimit"; + public string WakeUpRequestUrl => "FleetApiRequests/WakeUp"; + public string VehicleRequestUrl => "FleetApiRequests/GetVehicle"; + public string VehicleDataRequestUrl => $"FleetApiRequests/GetVehicleData"; public string EmailConfigurationKey => "EmailConfiguration"; public string BackendPasswordConfigurationKey => "BackendPasswordConfiguration"; public string TeslaTokenEncryptionKeyKey => "TeslaTokenEncryptionKey"; From b30d40885239097668009c5024bba4a60b8e5347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Mon, 30 Dec 2024 20:21:43 +0100 Subject: [PATCH 17/81] refactor(Constants): remove unnecessary properties --- TeslaSolarCharger/Shared/Resources/Constants.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/TeslaSolarCharger/Shared/Resources/Constants.cs b/TeslaSolarCharger/Shared/Resources/Constants.cs index aeeb740f2..fddb59b5d 100644 --- a/TeslaSolarCharger/Shared/Resources/Constants.cs +++ b/TeslaSolarCharger/Shared/Resources/Constants.cs @@ -19,8 +19,6 @@ public class Constants : IConstants public Margin InputMargin => Margin.Dense; public string InstallationIdKey => "InstallationId"; - public string FleetApiTokenRequested => "FleetApiTokenRequested"; - public string TokenRefreshUnauthorized => "TokenRefreshUnauthorized"; public string TokenMissingScopes => "TokenMissingScopes"; public string CarConfigurationsConverted => "CarConfigurationsConverted"; public string BleBaseUrlConverted => "BleBaseUrlConverted"; @@ -39,8 +37,6 @@ public class Constants : IConstants public string WakeUpRequestUrl => "FleetApiRequests/WakeUp"; public string VehicleRequestUrl => "FleetApiRequests/GetVehicle"; public string VehicleDataRequestUrl => $"FleetApiRequests/GetVehicleData"; - public string EmailConfigurationKey => "EmailConfiguration"; - public string BackendPasswordConfigurationKey => "BackendPasswordConfiguration"; public string TeslaTokenEncryptionKeyKey => "TeslaTokenEncryptionKey"; public string FleetApiTokenUnauthorizedKey => "BackendTokenUnauthorized"; From cf2fec4f8a927f03649b8eae1a19b24dee7292d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Mon, 30 Dec 2024 20:22:29 +0100 Subject: [PATCH 18/81] refactor(BackendApiController): convert value to DtoValue in controller --- .../Server/Controllers/BackendApiController.cs | 2 +- TeslaSolarCharger/Server/Services/BackendApiService.cs | 8 ++++---- .../Server/Services/Contracts/IBackendApiService.cs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/TeslaSolarCharger/Server/Controllers/BackendApiController.cs b/TeslaSolarCharger/Server/Controllers/BackendApiController.cs index 5bedcac96..46d080bda 100644 --- a/TeslaSolarCharger/Server/Controllers/BackendApiController.cs +++ b/TeslaSolarCharger/Server/Controllers/BackendApiController.cs @@ -8,7 +8,7 @@ namespace TeslaSolarCharger.Server.Controllers; public class BackendApiController (IBackendApiService backendApiService) : ApiBaseController { [HttpGet] - public Task> HasValidBackendToken() => backendApiService.HasValidBackendToken(); + public async Task> HasValidBackendToken() => new(await backendApiService.HasValidBackendToken()); [HttpPost] diff --git a/TeslaSolarCharger/Server/Services/BackendApiService.cs b/TeslaSolarCharger/Server/Services/BackendApiService.cs index ca0f72948..36ae56840 100644 --- a/TeslaSolarCharger/Server/Services/BackendApiService.cs +++ b/TeslaSolarCharger/Server/Services/BackendApiService.cs @@ -101,13 +101,13 @@ public async Task GetToken(DtoBackendLogin login) await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); } - public async Task> HasValidBackendToken() + public async Task HasValidBackendToken() { logger.LogTrace("{method}", nameof(HasValidBackendToken)); var token = await teslaSolarChargerContext.BackendTokens.SingleOrDefaultAsync(); if (token == default) { - return new(false); + return false; } var url = configurationWrapper.BackendApiBaseUrl() + "Client/IsTokenValid"; using var httpClient = new HttpClient(); @@ -117,7 +117,7 @@ public async Task> HasValidBackendToken() var response = await httpClient.SendAsync(request).ConfigureAwait(false); if (response.StatusCode == HttpStatusCode.Unauthorized) { - return new(false); + return false; } if (!response.IsSuccessStatusCode) { @@ -125,7 +125,7 @@ public async Task> HasValidBackendToken() logger.LogError("Could not check if token is valid. StatusCode: {statusCode}, resultBody: {resultBody}", response.StatusCode, responseString); throw new InvalidOperationException("Could not check if token is valid"); } - return new(true); + return true; } public async Task RefreshBackendTokenIfNeeded() diff --git a/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs b/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs index 942682f21..ed01c68bd 100644 --- a/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs +++ b/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs @@ -10,7 +10,7 @@ public interface IBackendApiService Task PostErrorInformation(string source, string methodName, string message, string issueKey, string? vin, string? stackTrace); Task GetCurrentVersion(); Task GetNewBackendNotifications(); - Task> HasValidBackendToken(); + Task HasValidBackendToken(); Task GetToken(DtoBackendLogin login); Task RefreshBackendTokenIfNeeded(); } From a428d2bebd0c924ae2336ce99d386b36d344b180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Mon, 30 Dec 2024 20:40:22 +0100 Subject: [PATCH 19/81] feat(TeslaFleetApiTokenHelper): Split token issue detection in two methods --- .../Client/Dialogs/AddCarDialog.razor | 6 +-- .../Client/Pages/BaseConfiguration.razor | 21 ++++----- .../Client/Pages/CarSettings.razor | 8 ++-- .../Server/Controllers/FleetApiController.cs | 2 +- .../Contracts/ITeslaFleetApiService.cs | 2 +- .../Contracts/ITeslaFleetApiTokenHelper.cs | 3 +- .../Server/Services/ErrorHandlingService.cs | 21 ++++----- .../Server/Services/TeslaFleetApiService.cs | 2 +- .../Services/TeslaFleetApiTokenHelper.cs | 44 ++++++++++++++----- .../Shared/Enums/FleetApiTokenState.cs | 12 ----- TeslaSolarCharger/Shared/Enums/TokenState.cs | 11 +++++ .../Shared/Resources/Constants.cs | 1 + .../Shared/Resources/Contracts/IConstants.cs | 1 + 13 files changed, 78 insertions(+), 56 deletions(-) delete mode 100644 TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs create mode 100644 TeslaSolarCharger/Shared/Enums/TokenState.cs diff --git a/TeslaSolarCharger/Client/Dialogs/AddCarDialog.razor b/TeslaSolarCharger/Client/Dialogs/AddCarDialog.razor index 40887c6f0..6429bfed2 100644 --- a/TeslaSolarCharger/Client/Dialogs/AddCarDialog.razor +++ b/TeslaSolarCharger/Client/Dialogs/AddCarDialog.razor @@ -9,7 +9,7 @@ { } -else if (_fleetApiTokenState != FleetApiTokenState.UpToDate) +else if (_fleetApiTokenState != TokenState.UpToDate) { >("api/FleetApi/FleetApiTokenState").ConfigureAwait(false); + var value = await HttpClient.GetFromJsonAsync>("api/FleetApi/FleetApiTokenState").ConfigureAwait(false); if (value != null) { _fleetApiTokenState = value.Value; diff --git a/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor b/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor index 36cf10229..25ce69ec1 100644 --- a/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor +++ b/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor @@ -37,37 +37,32 @@ else

Tesla Fleet API:

@switch (_fleetApiTokenState) { - case FleetApiTokenState.NoBackendApiToken: + case TokenState.MissingPrecondition:
You are not logged in in the Solar4car.com account. When requesting a new Tesla Token there will also be a request to log in to your Solar4car.com account.
break; - case FleetApiTokenState.BackendTokenUnauthorized: -
- You are not logged in in the Solar4car.com account. When requesting a new Tesla Token there will also be a request to log in to your Solar4car.com account. -
- break; - case FleetApiTokenState.NoFleetApiToken: + case TokenState.NotAvailable:
You did not request a Fleet API Token, yet. Request a new token, allow access to all scopes and enable mobile access in your car.
break; - case FleetApiTokenState.FleetApiTokenUnauthorized: + case TokenState.Unauthorized:
Your token is unauthorized. Request a new token, allow access to all scopes and enable mobile access in your car.
break; - case FleetApiTokenState.FleetApiTokenMissingScopes: + case TokenState.MissingScopes:
Your token has missing scopes. Request a new Token and allow all scopes (only required scopes are requested).
break; - case FleetApiTokenState.FleetApiTokenExpired: + case TokenState.Expired:
Your Fleet API token is expired. Request a new Token and allow all scopes (only required scopes are requested).
break; - case FleetApiTokenState.UpToDate: + case TokenState.UpToDate:
Everything is fine! If you want to generate a new token e.g. to switch to another Tesla Account please click the button below:
@@ -280,7 +275,7 @@ else @code { private DtoBaseConfiguration? _dtoBaseConfiguration; - private FleetApiTokenState? _fleetApiTokenState; + private TokenState? _fleetApiTokenState; private bool _tokenGenerationButtonDisabled; @@ -290,7 +285,7 @@ else protected override async Task OnInitializedAsync() { _dtoBaseConfiguration = await HttpClient.GetFromJsonAsync("/api/BaseConfiguration/GetBaseConfiguration").ConfigureAwait(false); - var value = await HttpClient.GetFromJsonAsync>("api/FleetApi/FleetApiTokenState").ConfigureAwait(false); + var value = await HttpClient.GetFromJsonAsync>("api/FleetApi/FleetApiTokenState").ConfigureAwait(false); if (value != null) { _fleetApiTokenState = value.Value; diff --git a/TeslaSolarCharger/Client/Pages/CarSettings.razor b/TeslaSolarCharger/Client/Pages/CarSettings.razor index 597fbb6b3..cfffea391 100644 --- a/TeslaSolarCharger/Client/Pages/CarSettings.razor +++ b/TeslaSolarCharger/Client/Pages/CarSettings.razor @@ -11,7 +11,7 @@

Car Settings

- @if (_fleetApiTokenState != null && _fleetApiTokenState != FleetApiTokenState.UpToDate) + @if (_fleetApiTokenState != null && _fleetApiTokenState != TokenState.UpToDate) { Base Configuration, Generate a Tesla Fleet API Token and restart TSC to see cars here. } - @if (_fleetApiTokenState == FleetApiTokenState.UpToDate) + @if (_fleetApiTokenState == TokenState.UpToDate) { ? _carBasicConfigurations; private readonly List _savingCarIds = new(); - private FleetApiTokenState? _fleetApiTokenState; + private TokenState? _fleetApiTokenState; private Dictionary _pairingResults = new(); @@ -138,7 +138,7 @@ else private async Task RefreshFleetApiTokenState() { - var value = await HttpClient.GetFromJsonAsync>("api/FleetApi/FleetApiTokenState").ConfigureAwait(false); + var value = await HttpClient.GetFromJsonAsync>("api/FleetApi/FleetApiTokenState").ConfigureAwait(false); if (value != null) { _fleetApiTokenState = value.Value; diff --git a/TeslaSolarCharger/Server/Controllers/FleetApiController.cs b/TeslaSolarCharger/Server/Controllers/FleetApiController.cs index 49101de38..f6a1ca501 100644 --- a/TeslaSolarCharger/Server/Controllers/FleetApiController.cs +++ b/TeslaSolarCharger/Server/Controllers/FleetApiController.cs @@ -20,7 +20,7 @@ public class FleetApiController( : ApiBaseController { [HttpGet] - public Task> FleetApiTokenState() => fleetApiService.GetFleetApiTokenState(); + public Task> FleetApiTokenState() => fleetApiService.GetFleetApiTokenState(); [HttpGet] public Task> GetOauthUrl(string locale, string baseUrl) => backendApiService.StartTeslaOAuth(locale, baseUrl); diff --git a/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs index 16befeb1c..ed8196851 100644 --- a/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs @@ -7,7 +7,7 @@ namespace TeslaSolarCharger.Server.Services.Contracts; public interface ITeslaFleetApiService { - Task> GetFleetApiTokenState(); + Task> GetFleetApiTokenState(); Task> TestFleetApiAccess(int carId); Task> IsFleetApiProxyEnabled(string vin); Task RefreshCarData(); diff --git a/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiTokenHelper.cs b/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiTokenHelper.cs index d7b2a29c3..1a2b8a6c2 100644 --- a/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiTokenHelper.cs +++ b/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiTokenHelper.cs @@ -4,5 +4,6 @@ namespace TeslaSolarCharger.Server.Services.Contracts; public interface ITeslaFleetApiTokenHelper { - Task GetFleetApiTokenState(); + Task GetFleetApiTokenState(); + Task GetBackendTokenState(); } diff --git a/TeslaSolarCharger/Server/Services/ErrorHandlingService.cs b/TeslaSolarCharger/Server/Services/ErrorHandlingService.cs index e2f5ee143..0954f48cb 100644 --- a/TeslaSolarCharger/Server/Services/ErrorHandlingService.cs +++ b/TeslaSolarCharger/Server/Services/ErrorHandlingService.cs @@ -416,28 +416,29 @@ private List GetIssueKeysUsingReflection() private async Task DetectTokenStateIssues(List activeErrors) { logger.LogTrace("{method}()", nameof(DetectTokenStateIssues)); - var tokenState = await teslaFleetApiTokenHelper.GetFleetApiTokenState(); + var backendTokenState = await teslaFleetApiTokenHelper.GetBackendTokenState(); + var fleetApiTokenState = await teslaFleetApiTokenHelper.GetFleetApiTokenState(); await AddOrRemoveErrors(activeErrors, issueKeys.NoBackendApiToken, "No Backen API token", "You are currently not connected to the backend. Open the Base Configuration and request a new token.", - tokenState == FleetApiTokenState.NoBackendApiToken).ConfigureAwait(false); + backendTokenState == TokenState.NotAvailable).ConfigureAwait(false); await AddOrRemoveErrors(activeErrors, issueKeys.BackendTokenUnauthorized, "Backend Token Unauthorized", "You recently changed your Solar4Car password or did not use TSC for at least 30 days. Open the Base Configuration and request a new token.", - tokenState == FleetApiTokenState.BackendTokenUnauthorized).ConfigureAwait(false); + backendTokenState == TokenState.Unauthorized).ConfigureAwait(false); await AddOrRemoveErrors(activeErrors, issueKeys.FleetApiTokenUnauthorized, "Fleet API token is unauthorized", "You recently changed your Tesla password or did not enable mobile access in your car. Enable mobile access in your car and open the Base Configuration and request a new token. Important: You need to allow access to all selectable scopes.", - tokenState == FleetApiTokenState.FleetApiTokenUnauthorized).ConfigureAwait(false); + fleetApiTokenState == TokenState.Unauthorized).ConfigureAwait(false); await AddOrRemoveErrors(activeErrors, issueKeys.NoFleetApiToken, "No Fleet API Token available.", "Open the Base Configuration and request a new token.", - tokenState == FleetApiTokenState.NoFleetApiToken).ConfigureAwait(false); - await AddOrRemoveErrors(activeErrors, issueKeys.FleetApiTokenUnauthorized, "Fleet API token is expired", + fleetApiTokenState == TokenState.NotAvailable).ConfigureAwait(false); + await AddOrRemoveErrors(activeErrors, issueKeys.FleetApiTokenExpired, "Fleet API token is expired", "Either you recently changed your Tesla password or did not enable mobile access in your car. Enable mobile access in your car and open the Base Configuration and request a new token. Important: You need to allow access to all selectable scopes.", - tokenState == FleetApiTokenState.FleetApiTokenExpired).ConfigureAwait(false); - await AddOrRemoveErrors(activeErrors, issueKeys.FleetApiTokenExpired, "Your Tesla token has missing scopes.", + fleetApiTokenState == TokenState.Expired).ConfigureAwait(false); + await AddOrRemoveErrors(activeErrors, issueKeys.FleetApiTokenMissingScopes, "Your Tesla token has missing scopes.", "Open the Base Configuration and request a new token. Note: You need to allow all selectable scopes as otherwise TSC won't work properly.", - tokenState == FleetApiTokenState.FleetApiTokenMissingScopes).ConfigureAwait(false); + fleetApiTokenState == TokenState.MissingScopes).ConfigureAwait(false); //Remove all fleet api related issue keys on token error because very likely it is because of the underlaying token issue. - if (tokenState != FleetApiTokenState.UpToDate) + if (fleetApiTokenState != TokenState.UpToDate) { foreach (var activeError in activeErrors.Where(activeError => activeError.IssueKey.StartsWith(issueKeys.GetVehicleData) || activeError.IssueKey.StartsWith(issueKeys.CarStateUnknown) diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index c1cb99439..2215501ed 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -1052,7 +1052,7 @@ public async Task RefreshFleetApiRequestsAreAllowed() } - public async Task> GetFleetApiTokenState() + public async Task> GetFleetApiTokenState() { var tokenState = await teslaFleetApiTokenHelper.GetFleetApiTokenState(); return new(tokenState); diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs index 63165ca08..2739b7921 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs @@ -13,9 +13,11 @@ public class TeslaFleetApiTokenHelper(ILogger logger, ITeslaSolarChargerContext teslaSolarChargerContext, IConstants constants, ITscConfigurationService tscConfigurationService, - IConfigurationWrapper configurationWrapper) : ITeslaFleetApiTokenHelper + IConfigurationWrapper configurationWrapper, + IBackendApiService backendApiService, + IDateTimeProvider dateTimeProvider) : ITeslaFleetApiTokenHelper { - public async Task GetFleetApiTokenState() + public async Task GetFleetApiTokenState() { logger.LogTrace("{method}()", nameof(GetFleetApiTokenState)); var hasCurrentTokenMissingScopes = await teslaSolarChargerContext.TscConfigurations @@ -23,12 +25,17 @@ public async Task GetFleetApiTokenState() .AnyAsync().ConfigureAwait(false); if (hasCurrentTokenMissingScopes) { - return FleetApiTokenState.FleetApiTokenMissingScopes; + return TokenState.MissingScopes; } var isTokenUnauthorized = string.Equals(await tscConfigurationService.GetConfigurationValueByKey(constants.FleetApiTokenUnauthorizedKey), "true", StringComparison.InvariantCultureIgnoreCase); if (isTokenUnauthorized) { - return FleetApiTokenState.FleetApiTokenUnauthorized; + return TokenState.Unauthorized; + } + var backendTokenState = await GetBackendTokenState(); + if (backendTokenState != TokenState.UpToDate) + { + return TokenState.MissingPrecondition; } var url = configurationWrapper.BackendApiBaseUrl() + "FleetApiRequests/FleetApiTokenExpiresInSeconds"; using var httpClient = new HttpClient(); @@ -37,7 +44,7 @@ public async Task GetFleetApiTokenState() var token = await teslaSolarChargerContext.BackendTokens.SingleOrDefaultAsync().ConfigureAwait(false); if (token == default) { - return FleetApiTokenState.NoBackendApiToken; + return TokenState.MissingPrecondition; } request.Headers.Authorization = new("Bearer", token.AccessToken); var response = await httpClient.SendAsync(request).ConfigureAwait(false); @@ -45,22 +52,39 @@ public async Task GetFleetApiTokenState() if (!response.IsSuccessStatusCode) { logger.LogError("Could not check if token is valid. StatusCode: {statusCode}, resultBody: {resultBody}", response.StatusCode, responseString); - return FleetApiTokenState.BackendTokenUnauthorized; + return TokenState.MissingPrecondition; } var validFleetApiToken = JsonConvert.DeserializeObject>(responseString); if (validFleetApiToken == null) { logger.LogError("Could not check if fleet api token is available."); - return FleetApiTokenState.NoFleetApiToken; + return TokenState.MissingPrecondition; } if (validFleetApiToken.Value == null) { - return FleetApiTokenState.NoFleetApiToken; + return TokenState.NotAvailable; } if (validFleetApiToken.Value <= 0) { - return FleetApiTokenState.FleetApiTokenExpired; + return TokenState.Expired; + } + return TokenState.UpToDate; + } + + public async Task GetBackendTokenState() + { + logger.LogTrace("{method}", nameof(GetBackendTokenState)); + var token = await teslaSolarChargerContext.BackendTokens.SingleOrDefaultAsync().ConfigureAwait(false); + if (token == default) + { + return TokenState.NotAvailable; + } + var currentDate = dateTimeProvider.DateTimeOffSetUtcNow(); + if (token.ExpiresAtUtc < currentDate) + { + return TokenState.Expired; } - return FleetApiTokenState.UpToDate; + var isTokenValid = await backendApiService.HasValidBackendToken(); + return isTokenValid ? TokenState.UpToDate : TokenState.Unauthorized; } } diff --git a/TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs b/TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs deleted file mode 100644 index fe7d37792..000000000 --- a/TeslaSolarCharger/Shared/Enums/FleetApiTokenState.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace TeslaSolarCharger.Shared.Enums; - -public enum FleetApiTokenState -{ - NoBackendApiToken, - BackendTokenUnauthorized, - NoFleetApiToken, - FleetApiTokenUnauthorized, - FleetApiTokenMissingScopes, - FleetApiTokenExpired, - UpToDate, -} diff --git a/TeslaSolarCharger/Shared/Enums/TokenState.cs b/TeslaSolarCharger/Shared/Enums/TokenState.cs new file mode 100644 index 000000000..6979fb21c --- /dev/null +++ b/TeslaSolarCharger/Shared/Enums/TokenState.cs @@ -0,0 +1,11 @@ +namespace TeslaSolarCharger.Shared.Enums; + +public enum TokenState +{ + MissingPrecondition, + NotAvailable, + Unauthorized, + MissingScopes, + Expired, + UpToDate, +} diff --git a/TeslaSolarCharger/Shared/Resources/Constants.cs b/TeslaSolarCharger/Shared/Resources/Constants.cs index fddb59b5d..025f60ac1 100644 --- a/TeslaSolarCharger/Shared/Resources/Constants.cs +++ b/TeslaSolarCharger/Shared/Resources/Constants.cs @@ -39,6 +39,7 @@ public class Constants : IConstants public string VehicleDataRequestUrl => $"FleetApiRequests/GetVehicleData"; public string TeslaTokenEncryptionKeyKey => "TeslaTokenEncryptionKey"; public string FleetApiTokenUnauthorizedKey => "BackendTokenUnauthorized"; + public string FleetApiTokenExpirationTime => "FleetApiTokenExpirationTime"; public string GridPoleIcon => "power-pole"; } diff --git a/TeslaSolarCharger/Shared/Resources/Contracts/IConstants.cs b/TeslaSolarCharger/Shared/Resources/Contracts/IConstants.cs index 8ee1560e3..9c61d80d3 100644 --- a/TeslaSolarCharger/Shared/Resources/Contracts/IConstants.cs +++ b/TeslaSolarCharger/Shared/Resources/Contracts/IConstants.cs @@ -39,4 +39,5 @@ public interface IConstants string VehicleDataRequestUrl { get; } string TeslaTokenEncryptionKeyKey { get; } string FleetApiTokenUnauthorizedKey { get; } + string FleetApiTokenExpirationTime { get; } } From da9546f4ffee8d11263b5c4f1b765b6a0979e230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Mon, 30 Dec 2024 21:33:57 +0100 Subject: [PATCH 20/81] refactor(TokenHelper): use memory cache for some token values --- .../Client/Pages/BaseConfiguration.razor | 4 +- .../Contracts/IFleetApiTokenCheckService.cs | 6 +- .../Services/FleetApiTokenCheckService.cs | 15 +- .../Controllers/BackendApiController.cs | 9 +- .../Server/ServiceCollectionExtensions.cs | 2 +- .../Server/Services/BackendApiService.cs | 27 -- .../Services/Contracts/IBackendApiService.cs | 1 - .../Contracts/ITeslaFleetApiTokenHelper.cs | 6 +- .../Server/Services/ErrorHandlingService.cs | 4 +- .../Server/Services/TeslaFleetApiService.cs | 3 +- .../Services/TeslaFleetApiTokenHelper.cs | 90 ------- .../Server/Services/TokenHelper.cs | 234 ++++++++++++++++++ .../Shared/Resources/Constants.cs | 4 +- .../Shared/Resources/Contracts/IConstants.cs | 4 +- 14 files changed, 264 insertions(+), 145 deletions(-) delete mode 100644 TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs create mode 100644 TeslaSolarCharger/Server/Services/TokenHelper.cs diff --git a/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor b/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor index 25ce69ec1..b728c5ce1 100644 --- a/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor +++ b/TeslaSolarCharger/Client/Pages/BaseConfiguration.razor @@ -314,8 +314,8 @@ else _tokenGenerationButtonDisabled = true; var locale = CultureInfo.CurrentCulture.ToString(); var baseUrl = NavigationManager.BaseUri; - var hasValidBackendToken = await FleetApiTokenCheckService.HasValidBackendToken(); - if (!hasValidBackendToken) + var backendTokenState = await FleetApiTokenCheckService.HasValidBackendToken(); + if (backendTokenState != TokenState.UpToDate) { var success = await DialogHelper.ShowCreateBackendTokenDialog(); if (!success) diff --git a/TeslaSolarCharger/Client/Services/Contracts/IFleetApiTokenCheckService.cs b/TeslaSolarCharger/Client/Services/Contracts/IFleetApiTokenCheckService.cs index 1e8823c09..c26a16002 100644 --- a/TeslaSolarCharger/Client/Services/Contracts/IFleetApiTokenCheckService.cs +++ b/TeslaSolarCharger/Client/Services/Contracts/IFleetApiTokenCheckService.cs @@ -1,6 +1,8 @@ -namespace TeslaSolarCharger.Client.Services.Contracts; +using TeslaSolarCharger.Shared.Enums; + +namespace TeslaSolarCharger.Client.Services.Contracts; public interface IFleetApiTokenCheckService { - Task HasValidBackendToken(); + Task HasValidBackendToken(); } diff --git a/TeslaSolarCharger/Client/Services/FleetApiTokenCheckService.cs b/TeslaSolarCharger/Client/Services/FleetApiTokenCheckService.cs index 0d4e4247f..8d6b531af 100644 --- a/TeslaSolarCharger/Client/Services/FleetApiTokenCheckService.cs +++ b/TeslaSolarCharger/Client/Services/FleetApiTokenCheckService.cs @@ -1,22 +1,15 @@ using TeslaSolarCharger.Client.Helper.Contracts; using TeslaSolarCharger.Client.Services.Contracts; using TeslaSolarCharger.Shared.Dtos; +using TeslaSolarCharger.Shared.Enums; namespace TeslaSolarCharger.Client.Services; public class FleetApiTokenCheckService(ILogger logger, IHttpClientHelper httpClientHelper) : IFleetApiTokenCheckService { - public async Task HasValidBackendToken() + public async Task HasValidBackendToken() { - try - { - var response = await httpClientHelper.SendGetRequestWithSnackbarAsync>("api/BackendApi/HasValidBackendToken"); - return response?.Value ?? false; - } - catch (Exception ex) - { - logger.LogError(ex, "Error while checking backend token"); - return false; - } + var response = await httpClientHelper.SendGetRequestWithSnackbarAsync>("api/BackendApi/HasValidBackendToken"); + return response?.Value ?? TokenState.MissingPrecondition; } } diff --git a/TeslaSolarCharger/Server/Controllers/BackendApiController.cs b/TeslaSolarCharger/Server/Controllers/BackendApiController.cs index 46d080bda..c728ec182 100644 --- a/TeslaSolarCharger/Server/Controllers/BackendApiController.cs +++ b/TeslaSolarCharger/Server/Controllers/BackendApiController.cs @@ -1,15 +1,18 @@ using Microsoft.AspNetCore.Mvc; using TeslaSolarCharger.Server.Services.Contracts; using TeslaSolarCharger.Shared.Dtos; +using TeslaSolarCharger.Shared.Enums; using TeslaSolarCharger.SharedBackend.Abstracts; namespace TeslaSolarCharger.Server.Controllers; -public class BackendApiController (IBackendApiService backendApiService) : ApiBaseController +public class BackendApiController (IBackendApiService backendApiService, ITeslaFleetApiTokenHelper tokenHelper) : ApiBaseController { [HttpGet] - public async Task> HasValidBackendToken() => new(await backendApiService.HasValidBackendToken()); - + public async Task> HasValidBackendToken() + { + return new(await tokenHelper.GetBackendTokenState(false)); + } [HttpPost] public Task LoginToBackend(DtoBackendLogin login) => backendApiService.GetToken(login); diff --git a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs index 0a32aff5c..95278f84a 100644 --- a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs +++ b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs @@ -100,7 +100,7 @@ public static IServiceCollection AddMyDependencies(this IServiceCollection servi .AddTransient() .AddTransient() .AddTransient() - .AddTransient() + .AddTransient() .AddTransient() .AddTransient() .AddTransient() diff --git a/TeslaSolarCharger/Server/Services/BackendApiService.cs b/TeslaSolarCharger/Server/Services/BackendApiService.cs index 36ae56840..65b1bb262 100644 --- a/TeslaSolarCharger/Server/Services/BackendApiService.cs +++ b/TeslaSolarCharger/Server/Services/BackendApiService.cs @@ -101,33 +101,6 @@ public async Task GetToken(DtoBackendLogin login) await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); } - public async Task HasValidBackendToken() - { - logger.LogTrace("{method}", nameof(HasValidBackendToken)); - var token = await teslaSolarChargerContext.BackendTokens.SingleOrDefaultAsync(); - if (token == default) - { - return false; - } - var url = configurationWrapper.BackendApiBaseUrl() + "Client/IsTokenValid"; - using var httpClient = new HttpClient(); - httpClient.Timeout = TimeSpan.FromSeconds(10); - var request = new HttpRequestMessage(HttpMethod.Get, url); - request.Headers.Authorization = new("Bearer", token.AccessToken); - var response = await httpClient.SendAsync(request).ConfigureAwait(false); - if (response.StatusCode == HttpStatusCode.Unauthorized) - { - return false; - } - if (!response.IsSuccessStatusCode) - { - var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - logger.LogError("Could not check if token is valid. StatusCode: {statusCode}, resultBody: {resultBody}", response.StatusCode, responseString); - throw new InvalidOperationException("Could not check if token is valid"); - } - return true; - } - public async Task RefreshBackendTokenIfNeeded() { logger.LogTrace("{method}(token)", nameof(RefreshBackendTokenIfNeeded)); diff --git a/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs b/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs index ed01c68bd..79ffcae18 100644 --- a/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs +++ b/TeslaSolarCharger/Server/Services/Contracts/IBackendApiService.cs @@ -10,7 +10,6 @@ public interface IBackendApiService Task PostErrorInformation(string source, string methodName, string message, string issueKey, string? vin, string? stackTrace); Task GetCurrentVersion(); Task GetNewBackendNotifications(); - Task HasValidBackendToken(); Task GetToken(DtoBackendLogin login); Task RefreshBackendTokenIfNeeded(); } diff --git a/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiTokenHelper.cs b/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiTokenHelper.cs index 1a2b8a6c2..6fd7664cd 100644 --- a/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiTokenHelper.cs +++ b/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiTokenHelper.cs @@ -4,6 +4,8 @@ namespace TeslaSolarCharger.Server.Services.Contracts; public interface ITeslaFleetApiTokenHelper { - Task GetFleetApiTokenState(); - Task GetBackendTokenState(); + Task GetFleetApiTokenState(bool useCache); + Task GetBackendTokenState(bool useCache); + Task GetFleetApiTokenExpirationDate(); + Task GetBackendTokenExpirationDate(); } diff --git a/TeslaSolarCharger/Server/Services/ErrorHandlingService.cs b/TeslaSolarCharger/Server/Services/ErrorHandlingService.cs index 0954f48cb..59d7dec05 100644 --- a/TeslaSolarCharger/Server/Services/ErrorHandlingService.cs +++ b/TeslaSolarCharger/Server/Services/ErrorHandlingService.cs @@ -416,8 +416,8 @@ private List GetIssueKeysUsingReflection() private async Task DetectTokenStateIssues(List activeErrors) { logger.LogTrace("{method}()", nameof(DetectTokenStateIssues)); - var backendTokenState = await teslaFleetApiTokenHelper.GetBackendTokenState(); - var fleetApiTokenState = await teslaFleetApiTokenHelper.GetFleetApiTokenState(); + var backendTokenState = await teslaFleetApiTokenHelper.GetBackendTokenState(true); + var fleetApiTokenState = await teslaFleetApiTokenHelper.GetFleetApiTokenState(true); await AddOrRemoveErrors(activeErrors, issueKeys.NoBackendApiToken, "No Backen API token", "You are currently not connected to the backend. Open the Base Configuration and request a new token.", backendTokenState == TokenState.NotAvailable).ConfigureAwait(false); diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index 2215501ed..168fed4e8 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -6,7 +6,6 @@ using TeslaSolarCharger.Model.Contracts; using TeslaSolarCharger.Model.Entities.TeslaSolarCharger; using TeslaSolarCharger.Server.Contracts; -using TeslaSolarCharger.Server.Dtos; using TeslaSolarCharger.Server.Dtos.Solar4CarBackend; using TeslaSolarCharger.Server.Dtos.TeslaFleetApi; using TeslaSolarCharger.Server.Resources.PossibleIssues.Contracts; @@ -1054,7 +1053,7 @@ public async Task RefreshFleetApiRequestsAreAllowed() public async Task> GetFleetApiTokenState() { - var tokenState = await teslaFleetApiTokenHelper.GetFleetApiTokenState(); + var tokenState = await teslaFleetApiTokenHelper.GetFleetApiTokenState(false); return new(tokenState); } diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs deleted file mode 100644 index 2739b7921..000000000 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiTokenHelper.cs +++ /dev/null @@ -1,90 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Newtonsoft.Json; -using TeslaSolarCharger.Model.Contracts; -using TeslaSolarCharger.Shared.Contracts; -using TeslaSolarCharger.Shared.Enums; -using TeslaSolarCharger.Shared.Resources.Contracts; -using TeslaSolarCharger.Server.Services.Contracts; -using TeslaSolarCharger.Shared.Dtos; - -namespace TeslaSolarCharger.Server.Services; - -public class TeslaFleetApiTokenHelper(ILogger logger, - ITeslaSolarChargerContext teslaSolarChargerContext, - IConstants constants, - ITscConfigurationService tscConfigurationService, - IConfigurationWrapper configurationWrapper, - IBackendApiService backendApiService, - IDateTimeProvider dateTimeProvider) : ITeslaFleetApiTokenHelper -{ - public async Task GetFleetApiTokenState() - { - logger.LogTrace("{method}()", nameof(GetFleetApiTokenState)); - var hasCurrentTokenMissingScopes = await teslaSolarChargerContext.TscConfigurations - .Where(c => c.Key == constants.TokenMissingScopes) - .AnyAsync().ConfigureAwait(false); - if (hasCurrentTokenMissingScopes) - { - return TokenState.MissingScopes; - } - var isTokenUnauthorized = string.Equals(await tscConfigurationService.GetConfigurationValueByKey(constants.FleetApiTokenUnauthorizedKey), "true", StringComparison.InvariantCultureIgnoreCase); - if (isTokenUnauthorized) - { - return TokenState.Unauthorized; - } - var backendTokenState = await GetBackendTokenState(); - if (backendTokenState != TokenState.UpToDate) - { - return TokenState.MissingPrecondition; - } - var url = configurationWrapper.BackendApiBaseUrl() + "FleetApiRequests/FleetApiTokenExpiresInSeconds"; - using var httpClient = new HttpClient(); - httpClient.Timeout = TimeSpan.FromSeconds(10); - var request = new HttpRequestMessage(HttpMethod.Get, url); - var token = await teslaSolarChargerContext.BackendTokens.SingleOrDefaultAsync().ConfigureAwait(false); - if (token == default) - { - return TokenState.MissingPrecondition; - } - request.Headers.Authorization = new("Bearer", token.AccessToken); - var response = await httpClient.SendAsync(request).ConfigureAwait(false); - var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - if (!response.IsSuccessStatusCode) - { - logger.LogError("Could not check if token is valid. StatusCode: {statusCode}, resultBody: {resultBody}", response.StatusCode, responseString); - return TokenState.MissingPrecondition; - } - var validFleetApiToken = JsonConvert.DeserializeObject>(responseString); - if (validFleetApiToken == null) - { - logger.LogError("Could not check if fleet api token is available."); - return TokenState.MissingPrecondition; - } - if (validFleetApiToken.Value == null) - { - return TokenState.NotAvailable; - } - if (validFleetApiToken.Value <= 0) - { - return TokenState.Expired; - } - return TokenState.UpToDate; - } - - public async Task GetBackendTokenState() - { - logger.LogTrace("{method}", nameof(GetBackendTokenState)); - var token = await teslaSolarChargerContext.BackendTokens.SingleOrDefaultAsync().ConfigureAwait(false); - if (token == default) - { - return TokenState.NotAvailable; - } - var currentDate = dateTimeProvider.DateTimeOffSetUtcNow(); - if (token.ExpiresAtUtc < currentDate) - { - return TokenState.Expired; - } - var isTokenValid = await backendApiService.HasValidBackendToken(); - return isTokenValid ? TokenState.UpToDate : TokenState.Unauthorized; - } -} diff --git a/TeslaSolarCharger/Server/Services/TokenHelper.cs b/TeslaSolarCharger/Server/Services/TokenHelper.cs new file mode 100644 index 000000000..75fcf9caa --- /dev/null +++ b/TeslaSolarCharger/Server/Services/TokenHelper.cs @@ -0,0 +1,234 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; +using Newtonsoft.Json; +using TeslaSolarCharger.Model.Contracts; +using TeslaSolarCharger.Shared.Contracts; +using TeslaSolarCharger.Shared.Enums; +using TeslaSolarCharger.Shared.Resources.Contracts; +using TeslaSolarCharger.Server.Services.Contracts; +using TeslaSolarCharger.Shared.Dtos; +using System.Net; + +namespace TeslaSolarCharger.Server.Services; + +public class TokenHelper(ILogger logger, + ITeslaSolarChargerContext teslaSolarChargerContext, + IConstants constants, + ITscConfigurationService tscConfigurationService, + IConfigurationWrapper configurationWrapper, + IDateTimeProvider dateTimeProvider, + IMemoryCache memoryCache) : ITeslaFleetApiTokenHelper +{ + public async Task GetFleetApiTokenState(bool useCache) + { + logger.LogTrace("{method}()", nameof(GetFleetApiTokenState)); + if (useCache && memoryCache.TryGetValue(constants.FleetApiTokenStateKey, out TokenState cachedFleetTokenState)) + { + logger.LogTrace("Returning FleetApiTokenState from cache: {tokenState}", cachedFleetTokenState); + return cachedFleetTokenState; + } + var state = await GetUncachedFleetApiTokenState().ConfigureAwait(false); + memoryCache.Set(constants.FleetApiTokenStateKey, state.TokenState, GetCacheEntryOptions(state.ExpiresAtUtc)); + memoryCache.Set(constants.FleetApiTokenExpirationTimeKey, state, GetCacheEntryOptions(state.ExpiresAtUtc)); + return state.TokenState; + } + + public async Task GetFleetApiTokenExpirationDate() + { + logger.LogTrace("{method}()", nameof(GetFleetApiTokenExpirationDate)); + if (memoryCache.TryGetValue(constants.FleetApiTokenExpirationTimeKey, out DateTimeOffset? expirationTime)) + { + logger.LogTrace("Returning FleetApiToken ExpirationTime from cache: {expirationTime}", expirationTime); + return expirationTime; + } + var state = await GetUncachedFleetApiTokenState().ConfigureAwait(false); + memoryCache.Set(constants.FleetApiTokenStateKey, state.TokenState, GetCacheEntryOptions(state.ExpiresAtUtc)); + memoryCache.Set(constants.FleetApiTokenExpirationTimeKey, state, GetCacheEntryOptions(state.ExpiresAtUtc)); + return state.ExpiresAtUtc; + } + + public async Task GetBackendTokenState(bool useCache) + { + logger.LogTrace("{method}", nameof(GetBackendTokenState)); + if (useCache && memoryCache.TryGetValue(constants.BackendTokenStateKey, out TokenState cachedFleetTokenState)) + { + logger.LogTrace("Returning BackendTokenState from cache: {tokenState}", cachedFleetTokenState); + return cachedFleetTokenState; + } + var state = await GetUncachedBackendTokenState().ConfigureAwait(false); + memoryCache.Set(constants.BackendTokenStateKey, state.TokenState, GetCacheEntryOptions(state.ExpiresAtUtc)); + return state.TokenState; + } + + public async Task GetBackendTokenExpirationDate() + { + logger.LogTrace("{method}()", nameof(GetBackendTokenExpirationDate)); + var expirationDate = await teslaSolarChargerContext.BackendTokens.Select(t => t.ExpiresAtUtc) + .SingleOrDefaultAsync(); + return expirationDate; + } + + private async Task GetUncachedFleetApiTokenState() + { + var hasCurrentTokenMissingScopes = await teslaSolarChargerContext.TscConfigurations + .Where(c => c.Key == constants.TokenMissingScopes) + .AnyAsync().ConfigureAwait(false); + if (hasCurrentTokenMissingScopes) + { + return new() + { + TokenState = TokenState.MissingScopes, + }; + } + var isTokenUnauthorized = string.Equals(await tscConfigurationService.GetConfigurationValueByKey(constants.FleetApiTokenUnauthorizedKey), "true", StringComparison.InvariantCultureIgnoreCase); + if (isTokenUnauthorized) + { + return new() + { + TokenState = TokenState.Unauthorized, + }; + } + var backendTokenState = await GetBackendTokenState(false); + if (backendTokenState != TokenState.UpToDate) + { + return new() + { + TokenState = TokenState.MissingPrecondition, + }; + } + var url = configurationWrapper.BackendApiBaseUrl() + "FleetApiRequests/FleetApiTokenExpiresInSeconds"; + using var httpClient = new HttpClient(); + httpClient.Timeout = TimeSpan.FromSeconds(10); + var request = new HttpRequestMessage(HttpMethod.Get, url); + var token = await teslaSolarChargerContext.BackendTokens.SingleOrDefaultAsync().ConfigureAwait(false); + if (token == default) + { + return new() + { + TokenState = TokenState.MissingPrecondition, + }; + } + request.Headers.Authorization = new("Bearer", token.AccessToken); + var response = await httpClient.SendAsync(request).ConfigureAwait(false); + var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + if (!response.IsSuccessStatusCode) + { + logger.LogError("Could not check if token is valid. StatusCode: {statusCode}, resultBody: {resultBody}", response.StatusCode, responseString); + return new() + { + TokenState = TokenState.MissingPrecondition, + }; + } + var validFleetApiToken = JsonConvert.DeserializeObject>(responseString); + if (validFleetApiToken == null) + { + logger.LogError("Could not check if fleet api token is available."); + return new() + { + TokenState = TokenState.MissingPrecondition, + }; + } + if (validFleetApiToken.Value == null) + { + return new() + { + TokenState = TokenState.NotAvailable, + }; + } + if (validFleetApiToken.Value <= 0) + { + return new() + { + TokenState = TokenState.Expired, + }; + } + return new() + { + TokenState = TokenState.UpToDate, + ExpiresAtUtc = dateTimeProvider.DateTimeOffSetUtcNow().AddSeconds(validFleetApiToken.Value.Value), + }; + } + + + private async Task GetUncachedBackendTokenState() + { + var token = await teslaSolarChargerContext.BackendTokens.SingleOrDefaultAsync().ConfigureAwait(false); + if (token == default) + { + return new() + { + TokenState = TokenState.NotAvailable, + }; + } + var currentDate = dateTimeProvider.DateTimeOffSetUtcNow(); + if (token.ExpiresAtUtc < currentDate) + { + return new() + { + TokenState = TokenState.Expired, + }; + } + var isTokenValid = await HasValidBackendToken(); + if (isTokenValid) + { + return new() + { + TokenState = TokenState.UpToDate, + ExpiresAtUtc = token.ExpiresAtUtc, + }; + } + + return new() + { + TokenState = TokenState.Unauthorized, + }; + } + + private async Task HasValidBackendToken() + { + logger.LogTrace("{method}", nameof(HasValidBackendToken)); + var token = await teslaSolarChargerContext.BackendTokens.SingleOrDefaultAsync(); + if (token == default) + { + return false; + } + var url = configurationWrapper.BackendApiBaseUrl() + "Client/IsTokenValid"; + using var httpClient = new HttpClient(); + httpClient.Timeout = TimeSpan.FromSeconds(10); + var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.Authorization = new("Bearer", token.AccessToken); + var response = await httpClient.SendAsync(request).ConfigureAwait(false); + if (response.StatusCode == HttpStatusCode.Unauthorized) + { + return false; + } + if (!response.IsSuccessStatusCode) + { + var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + logger.LogError("Could not check if token is valid. StatusCode: {statusCode}, resultBody: {resultBody}", response.StatusCode, responseString); + throw new InvalidOperationException("Could not check if token is valid"); + } + return true; + } + + private MemoryCacheEntryOptions GetCacheEntryOptions(DateTimeOffset? validUntil) + { + var validFor = TimeSpan.FromMinutes(15); + var currentDate = dateTimeProvider.DateTimeOffSetUtcNow(); + if (validUntil != default && (validUntil < (currentDate + validFor))) + { + validFor = validUntil.Value - currentDate; + } + return new() + { + AbsoluteExpirationRelativeToNow = validFor, + }; + } + + private class TokenStateIncludingExpirationTime + { + public TokenState TokenState { get; init; } + public DateTimeOffset? ExpiresAtUtc { get; init; } + } + +} diff --git a/TeslaSolarCharger/Shared/Resources/Constants.cs b/TeslaSolarCharger/Shared/Resources/Constants.cs index 025f60ac1..5692573ec 100644 --- a/TeslaSolarCharger/Shared/Resources/Constants.cs +++ b/TeslaSolarCharger/Shared/Resources/Constants.cs @@ -39,7 +39,9 @@ public class Constants : IConstants public string VehicleDataRequestUrl => $"FleetApiRequests/GetVehicleData"; public string TeslaTokenEncryptionKeyKey => "TeslaTokenEncryptionKey"; public string FleetApiTokenUnauthorizedKey => "BackendTokenUnauthorized"; - public string FleetApiTokenExpirationTime => "FleetApiTokenExpirationTime"; + public string FleetApiTokenExpirationTimeKey => "FleetApiTokenExpirationTime"; + public string FleetApiTokenStateKey => "FleetApiTokenState"; + public string BackendTokenStateKey => "BackendTokenState"; public string GridPoleIcon => "power-pole"; } diff --git a/TeslaSolarCharger/Shared/Resources/Contracts/IConstants.cs b/TeslaSolarCharger/Shared/Resources/Contracts/IConstants.cs index 9c61d80d3..d0d6949e4 100644 --- a/TeslaSolarCharger/Shared/Resources/Contracts/IConstants.cs +++ b/TeslaSolarCharger/Shared/Resources/Contracts/IConstants.cs @@ -39,5 +39,7 @@ public interface IConstants string VehicleDataRequestUrl { get; } string TeslaTokenEncryptionKeyKey { get; } string FleetApiTokenUnauthorizedKey { get; } - string FleetApiTokenExpirationTime { get; } + string FleetApiTokenExpirationTimeKey { get; } + string FleetApiTokenStateKey { get; } + string BackendTokenStateKey { get; } } From 8df29f90ffb46288c993422991ebf27b8be53423 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Mon, 30 Dec 2024 21:51:20 +0100 Subject: [PATCH 21/81] feat(BackendApiService): invalidate backend token memory cache state --- .../Controllers/BackendApiController.cs | 2 +- .../Server/ServiceCollectionExtensions.cs | 2 +- .../Server/Services/BackendApiService.cs | 20 ++++++++++++------- ...FleetApiTokenHelper.cs => ITokenHelper.cs} | 2 +- .../Server/Services/ErrorHandlingService.cs | 6 +++--- .../Server/Services/TeslaFleetApiService.cs | 4 ++-- .../Server/Services/TokenHelper.cs | 2 +- 7 files changed, 22 insertions(+), 16 deletions(-) rename TeslaSolarCharger/Server/Services/Contracts/{ITeslaFleetApiTokenHelper.cs => ITokenHelper.cs} (88%) diff --git a/TeslaSolarCharger/Server/Controllers/BackendApiController.cs b/TeslaSolarCharger/Server/Controllers/BackendApiController.cs index c728ec182..7f358a253 100644 --- a/TeslaSolarCharger/Server/Controllers/BackendApiController.cs +++ b/TeslaSolarCharger/Server/Controllers/BackendApiController.cs @@ -6,7 +6,7 @@ namespace TeslaSolarCharger.Server.Controllers; -public class BackendApiController (IBackendApiService backendApiService, ITeslaFleetApiTokenHelper tokenHelper) : ApiBaseController +public class BackendApiController (IBackendApiService backendApiService, ITokenHelper tokenHelper) : ApiBaseController { [HttpGet] public async Task> HasValidBackendToken() diff --git a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs index 95278f84a..3f87e49cb 100644 --- a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs +++ b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs @@ -100,7 +100,7 @@ public static IServiceCollection AddMyDependencies(this IServiceCollection servi .AddTransient() .AddTransient() .AddTransient() - .AddTransient() + .AddTransient() .AddTransient() .AddTransient() .AddTransient() diff --git a/TeslaSolarCharger/Server/Services/BackendApiService.cs b/TeslaSolarCharger/Server/Services/BackendApiService.cs index 65b1bb262..3c008c446 100644 --- a/TeslaSolarCharger/Server/Services/BackendApiService.cs +++ b/TeslaSolarCharger/Server/Services/BackendApiService.cs @@ -1,7 +1,7 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; using Newtonsoft.Json; using System.Diagnostics; -using System.Net; using System.Reflection; using TeslaSolarCharger.Model.Contracts; using TeslaSolarCharger.Model.Entities.TeslaSolarCharger; @@ -24,7 +24,9 @@ public class BackendApiService( IDateTimeProvider dateTimeProvider, IErrorHandlingService errorHandlingService, IIssueKeys issueKeys, - IPasswordGenerationService passwordGenerationService) + IPasswordGenerationService passwordGenerationService, + ITokenHelper tokenHelper, + IMemoryCache memoryCache) : IBackendApiService { public async Task> StartTeslaOAuth(string locale, string baseUrl) @@ -104,19 +106,21 @@ public async Task GetToken(DtoBackendLogin login) public async Task RefreshBackendTokenIfNeeded() { logger.LogTrace("{method}(token)", nameof(RefreshBackendTokenIfNeeded)); - var url = configurationWrapper.BackendApiBaseUrl() + "User/RefreshToken"; - var token = await teslaSolarChargerContext.BackendTokens.SingleOrDefaultAsync(); - if(token == default) + var tokenExpriationDate = await tokenHelper.GetBackendTokenExpirationDate(); + if(tokenExpriationDate == default) { - logger.LogError("Could not refresh backend token. No token found"); + logger.LogError("Could not refresh backend token. No token found."); return; } var currentDate = dateTimeProvider.DateTimeOffSetUtcNow(); - if(token.ExpiresAtUtc > currentDate.AddMinutes(1)) + if(tokenExpriationDate > currentDate.AddMinutes(1)) { logger.LogTrace("Token is still valid"); return; } + var url = configurationWrapper.BackendApiBaseUrl() + "User/RefreshToken"; + //As expiration date is not null a token must exist. + var token = await teslaSolarChargerContext.BackendTokens.SingleAsync(); var dtoRefreshToken = new DtoTokenRefreshModel(token.AccessToken, token.RefreshToken); using var httpClient = new HttpClient(); httpClient.Timeout = TimeSpan.FromSeconds(10); @@ -125,6 +129,7 @@ public async Task RefreshBackendTokenIfNeeded() { var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); logger.LogError("Could not refresh backend token. StatusCode: {statusCode}, resultBody: {resultBody}", response.StatusCode, responseString); + memoryCache.Remove(constants.BackendTokenStateKey); throw new InvalidOperationException("Could not refresh backend token"); } var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); @@ -132,6 +137,7 @@ public async Task RefreshBackendTokenIfNeeded() token.AccessToken = newToken.AccessToken; token.RefreshToken = newToken.RefreshToken; await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); + memoryCache.Remove(constants.BackendTokenStateKey); } internal string GenerateAuthUrl(DtoTeslaOAuthRequestInformation oAuthInformation, string locale) diff --git a/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiTokenHelper.cs b/TeslaSolarCharger/Server/Services/Contracts/ITokenHelper.cs similarity index 88% rename from TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiTokenHelper.cs rename to TeslaSolarCharger/Server/Services/Contracts/ITokenHelper.cs index 6fd7664cd..a2ca0b336 100644 --- a/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiTokenHelper.cs +++ b/TeslaSolarCharger/Server/Services/Contracts/ITokenHelper.cs @@ -2,7 +2,7 @@ namespace TeslaSolarCharger.Server.Services.Contracts; -public interface ITeslaFleetApiTokenHelper +public interface ITokenHelper { Task GetFleetApiTokenState(bool useCache); Task GetBackendTokenState(bool useCache); diff --git a/TeslaSolarCharger/Server/Services/ErrorHandlingService.cs b/TeslaSolarCharger/Server/Services/ErrorHandlingService.cs index 59d7dec05..f604b7149 100644 --- a/TeslaSolarCharger/Server/Services/ErrorHandlingService.cs +++ b/TeslaSolarCharger/Server/Services/ErrorHandlingService.cs @@ -26,7 +26,7 @@ public class ErrorHandlingService(ILogger logger, IConfigurationWrapper configurationWrapper, IMapperConfigurationFactory mapperConfigurationFactory, ISettings settings, - ITeslaFleetApiTokenHelper teslaFleetApiTokenHelper, + ITokenHelper tokenHelper, IPossibleIssues possibleIssues, IConstants constants) : IErrorHandlingService { @@ -416,8 +416,8 @@ private List GetIssueKeysUsingReflection() private async Task DetectTokenStateIssues(List activeErrors) { logger.LogTrace("{method}()", nameof(DetectTokenStateIssues)); - var backendTokenState = await teslaFleetApiTokenHelper.GetBackendTokenState(true); - var fleetApiTokenState = await teslaFleetApiTokenHelper.GetFleetApiTokenState(true); + var backendTokenState = await tokenHelper.GetBackendTokenState(true); + var fleetApiTokenState = await tokenHelper.GetFleetApiTokenState(true); await AddOrRemoveErrors(activeErrors, issueKeys.NoBackendApiToken, "No Backen API token", "You are currently not connected to the backend. Open the Base Configuration and request a new token.", backendTokenState == TokenState.NotAvailable).ConfigureAwait(false); diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index 168fed4e8..1249b6c9b 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -35,7 +35,7 @@ public class TeslaFleetApiService( ISettings settings, IBleService bleService, IIssueKeys issueKeys, - ITeslaFleetApiTokenHelper teslaFleetApiTokenHelper, + ITokenHelper tokenHelper, IFleetTelemetryWebSocketService fleetTelemetryWebSocketService) : ITeslaService, ITeslaFleetApiService { @@ -1053,7 +1053,7 @@ public async Task RefreshFleetApiRequestsAreAllowed() public async Task> GetFleetApiTokenState() { - var tokenState = await teslaFleetApiTokenHelper.GetFleetApiTokenState(false); + var tokenState = await tokenHelper.GetFleetApiTokenState(false); return new(tokenState); } diff --git a/TeslaSolarCharger/Server/Services/TokenHelper.cs b/TeslaSolarCharger/Server/Services/TokenHelper.cs index 75fcf9caa..7aba9da82 100644 --- a/TeslaSolarCharger/Server/Services/TokenHelper.cs +++ b/TeslaSolarCharger/Server/Services/TokenHelper.cs @@ -17,7 +17,7 @@ public class TokenHelper(ILogger logger, ITscConfigurationService tscConfigurationService, IConfigurationWrapper configurationWrapper, IDateTimeProvider dateTimeProvider, - IMemoryCache memoryCache) : ITeslaFleetApiTokenHelper + IMemoryCache memoryCache) : ITokenHelper { public async Task GetFleetApiTokenState(bool useCache) { From 682f06c17e369d5fd21ad2c0d5920dbaff6e9e0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Mon, 30 Dec 2024 23:24:42 +0100 Subject: [PATCH 22/81] feat(TeslaFleetApiService): can refresh fleet api token --- .../Server/Scheduling/JobManager.cs | 7 +- .../Jobs/FleetApiTokenRefreshJob.cs | 16 +++++ .../Server/ServiceCollectionExtensions.cs | 1 + .../Contracts/ITeslaFleetApiService.cs | 1 + .../Server/Services/Contracts/ITokenHelper.cs | 2 +- .../Server/Services/TeslaFleetApiService.cs | 64 ++++++++++++++++++- .../Server/Services/TokenHelper.cs | 4 +- 7 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 TeslaSolarCharger/Server/Scheduling/Jobs/FleetApiTokenRefreshJob.cs diff --git a/TeslaSolarCharger/Server/Scheduling/JobManager.cs b/TeslaSolarCharger/Server/Scheduling/JobManager.cs index c3e3fb1bb..4e2d229d8 100644 --- a/TeslaSolarCharger/Server/Scheduling/JobManager.cs +++ b/TeslaSolarCharger/Server/Scheduling/JobManager.cs @@ -44,6 +44,7 @@ public async Task StartJobs() var newVersionCheckJob = JobBuilder.Create().WithIdentity(nameof(NewVersionCheckJob)).Build(); var spotPriceJob = JobBuilder.Create().WithIdentity(nameof(SpotPriceJob)).Build(); var backendTokenRefreshJob = JobBuilder.Create().WithIdentity(nameof(BackendTokenRefreshJob)).Build(); + var fleetApiTokenRefreshJob = JobBuilder.Create().WithIdentity(nameof(FleetApiTokenRefreshJob)).Build(); var vehicleDataRefreshJob = JobBuilder.Create().WithIdentity(nameof(VehicleDataRefreshJob)).Build(); var teslaMateChargeCostUpdateJob = JobBuilder.Create().WithIdentity(nameof(TeslaMateChargeCostUpdateJob)).Build(); var backendNotificationRefreshJob = JobBuilder.Create().WithIdentity(nameof(BackendNotificationRefreshJob)).Build(); @@ -96,9 +97,12 @@ public async Task StartJobs() var spotPricePlanningTrigger = TriggerBuilder.Create().WithIdentity("spotPricePlanningTrigger") .WithSchedule(SimpleScheduleBuilder.RepeatHourlyForever(1)).Build(); - var backendTokenRefreshTrigger = TriggerBuilder.Create().WithIdentity("fleetApiTokenRefreshTrigger") + var backendTokenRefreshTrigger = TriggerBuilder.Create().WithIdentity("backendTokenRefreshTrigger") .WithSchedule(SimpleScheduleBuilder.RepeatSecondlyForever(59)).Build(); + var fleetApiTokenRefreshTrigger = TriggerBuilder.Create().WithIdentity("fleetApiTokenRefreshTrigger") + .WithSchedule(SimpleScheduleBuilder.RepeatSecondlyForever(58)).Build(); + var vehicleDataRefreshTrigger = TriggerBuilder.Create().WithIdentity("vehicleDataRefreshTrigger") .WithSchedule(SimpleScheduleBuilder.RepeatSecondlyForever(configurationWrapper.CarRefreshAfterCommandSeconds())).Build(); @@ -155,6 +159,7 @@ public async Task StartJobs() triggersAndJobs.Add(finishedChargingProcessFinalizingJob, new HashSet { finishedChargingProcessFinalizingTrigger }); triggersAndJobs.Add(mqttReconnectionJob, new HashSet { mqttReconnectionTrigger }); triggersAndJobs.Add(backendTokenRefreshJob, new HashSet { backendTokenRefreshTrigger }); + triggersAndJobs.Add(fleetApiTokenRefreshJob, new HashSet { fleetApiTokenRefreshTrigger }); triggersAndJobs.Add(vehicleDataRefreshJob, new HashSet { vehicleDataRefreshTrigger }); triggersAndJobs.Add(teslaMateChargeCostUpdateJob, new HashSet { teslaMateChargeCostUpdateTrigger }); triggersAndJobs.Add(backendNotificationRefreshJob, new HashSet { triggerAtNight, triggerNow }); diff --git a/TeslaSolarCharger/Server/Scheduling/Jobs/FleetApiTokenRefreshJob.cs b/TeslaSolarCharger/Server/Scheduling/Jobs/FleetApiTokenRefreshJob.cs new file mode 100644 index 000000000..00e1c69cf --- /dev/null +++ b/TeslaSolarCharger/Server/Scheduling/Jobs/FleetApiTokenRefreshJob.cs @@ -0,0 +1,16 @@ +using Quartz; +using TeslaSolarCharger.Server.Services.Contracts; + +namespace TeslaSolarCharger.Server.Scheduling.Jobs; + +[DisallowConcurrentExecution] +public class FleetApiTokenRefreshJob(ILogger logger, + ITeslaFleetApiService service) + : IJob +{ + public async Task Execute(IJobExecutionContext context) + { + logger.LogTrace("{method}({context})", nameof(Execute), context); + await service.RefreshFleetApiTokenIfNeeded().ConfigureAwait(false); + } +} diff --git a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs index 3f87e49cb..11c614108 100644 --- a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs +++ b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs @@ -49,6 +49,7 @@ public static IServiceCollection AddMyDependencies(this IServiceCollection servi .AddTransient() .AddTransient() .AddTransient() + .AddTransient() .AddTransient() .AddTransient() .AddTransient() diff --git a/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs index ed8196851..1641656ce 100644 --- a/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/Contracts/ITeslaFleetApiService.cs @@ -16,4 +16,5 @@ public interface ITeslaFleetApiService void ResetApiRequestCounters(); Task>> GetNewCarsInAccount(); Task>> GetAllCarsFromAccount(); + Task RefreshFleetApiTokenIfNeeded(); } diff --git a/TeslaSolarCharger/Server/Services/Contracts/ITokenHelper.cs b/TeslaSolarCharger/Server/Services/Contracts/ITokenHelper.cs index a2ca0b336..1749d9857 100644 --- a/TeslaSolarCharger/Server/Services/Contracts/ITokenHelper.cs +++ b/TeslaSolarCharger/Server/Services/Contracts/ITokenHelper.cs @@ -6,6 +6,6 @@ public interface ITokenHelper { Task GetFleetApiTokenState(bool useCache); Task GetBackendTokenState(bool useCache); - Task GetFleetApiTokenExpirationDate(); + Task GetFleetApiTokenExpirationDate(bool useCache); Task GetBackendTokenExpirationDate(); } diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs index 1249b6c9b..76573787a 100644 --- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs +++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs @@ -1,5 +1,6 @@ using LanguageExt; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; using Newtonsoft.Json; using System.Net; using System.Net.Http.Headers; @@ -36,7 +37,8 @@ public class TeslaFleetApiService( IBleService bleService, IIssueKeys issueKeys, ITokenHelper tokenHelper, - IFleetTelemetryWebSocketService fleetTelemetryWebSocketService) + IFleetTelemetryWebSocketService fleetTelemetryWebSocketService, + IMemoryCache memoryCache) : ITeslaService, ITeslaFleetApiService { private const string IsChargingErrorMessage = "is_charging"; @@ -432,6 +434,66 @@ public async Task RefreshCarData() } } + public async Task RefreshFleetApiTokenIfNeeded() + { + logger.LogTrace("{method}()", nameof(RefreshFleetApiTokenIfNeeded)); + var fleetApiTokenExpiration = await tokenHelper.GetFleetApiTokenExpirationDate(true); + if (fleetApiTokenExpiration == default) + { + var fleetApiTokenState = await tokenHelper.GetFleetApiTokenState(true); + logger.LogDebug("Do not refresh Fleet API Token as state is {state}", fleetApiTokenState); + return; + } + var currentDate = dateTimeProvider.DateTimeOffSetUtcNow(); + if(fleetApiTokenExpiration > currentDate.AddMinutes(1)) + { + logger.LogDebug("Do not refresh Fleet API Token as it is still valid until {expiration}", fleetApiTokenExpiration); + return; + } + logger.LogDebug("Refresh Fleet API Token as it is expired since {expiration}", fleetApiTokenExpiration); + var backendApiBaseUrl = configurationWrapper.BackendApiBaseUrl(); + var decryptionKey = await tscConfigurationService.GetConfigurationValueByKey(constants.TeslaTokenEncryptionKeyKey); + if (decryptionKey == default) + { + logger.LogError("Decryption key not found do not send command"); + throw new InvalidOperationException("No Decryption key found."); + } + var requestUri = $"{backendApiBaseUrl}TeslaOAuth/RefreshToken?encryptionKey={Uri.EscapeDataString(decryptionKey)}"; + using var httpClient = new HttpClient(); + httpClient.Timeout = TimeSpan.FromSeconds(10); + var request = new HttpRequestMessage(HttpMethod.Post, requestUri); + var token = await teslaSolarChargerContext.BackendTokens.SingleOrDefaultAsync().ConfigureAwait(false); + if (token == default) + { + throw new InvalidOperationException("Can not start Tesla O Auth without backend token"); + } + request.Headers.Authorization = new("Bearer", token.AccessToken); + var response = await httpClient.SendAsync(request).ConfigureAwait(false); + memoryCache.Remove(constants.FleetApiTokenStateKey); + memoryCache.Remove(constants.FleetApiTokenExpirationTimeKey); + var responseString = await response.Content.ReadAsStringAsync(); + if (!response.IsSuccessStatusCode) + { + logger.LogError("Refresh Fleet API Token was not successfull. Status code: {statusCode}; response string: {responseString}", response.StatusCode, responseString); + return; + } + var result = JsonConvert.DeserializeObject(responseString); + if (result == default) + { + logger.LogError("Could not deserialize response from TeslaOAuth/RefreshToken"); + return; + } + + if (result.StatusCode is >= HttpStatusCode.OK and < HttpStatusCode.Ambiguous) + { + logger.LogInformation("Refresh Fleet API Token was successfull"); + } + else + { + logger.LogError("Refresh Token did not succeed between Backend and Tesla."); + } + } + private async Task IsCarDataRefreshNeeded(DtoCar car) { logger.LogTrace("{method}({vin})", nameof(IsCarDataRefreshNeeded), car.Vin); diff --git a/TeslaSolarCharger/Server/Services/TokenHelper.cs b/TeslaSolarCharger/Server/Services/TokenHelper.cs index 7aba9da82..31e335b3f 100644 --- a/TeslaSolarCharger/Server/Services/TokenHelper.cs +++ b/TeslaSolarCharger/Server/Services/TokenHelper.cs @@ -33,10 +33,10 @@ public async Task GetFleetApiTokenState(bool useCache) return state.TokenState; } - public async Task GetFleetApiTokenExpirationDate() + public async Task GetFleetApiTokenExpirationDate(bool useCache) { logger.LogTrace("{method}()", nameof(GetFleetApiTokenExpirationDate)); - if (memoryCache.TryGetValue(constants.FleetApiTokenExpirationTimeKey, out DateTimeOffset? expirationTime)) + if (useCache && memoryCache.TryGetValue(constants.FleetApiTokenExpirationTimeKey, out DateTimeOffset? expirationTime)) { logger.LogTrace("Returning FleetApiToken ExpirationTime from cache: {expirationTime}", expirationTime); return expirationTime; From 42e0bb56a7f88539c331fdc5685ce9b0c5cb351a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Tue, 31 Dec 2024 13:47:17 +0100 Subject: [PATCH 23/81] feat(CarValueType): add Car name as car value type --- .../Server/Services/FleetTelemetryWebSocketService.cs | 3 +++ TeslaSolarCharger/Shared/Enums/CarValueType.cs | 1 + 2 files changed, 4 insertions(+) diff --git a/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs b/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs index 53a443422..ec479e279 100644 --- a/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs +++ b/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs @@ -291,6 +291,9 @@ private async Task ReceiveMessages(DtoFleetTelemetryWebSocketClients client, str case CarValueType.ChargerVoltage: propertyName = nameof(DtoCar.ChargerVoltage); break; + case CarValueType.VehicleName: + propertyName = nameof(DtoCar.Name); + break; } if (propertyName != default) diff --git a/TeslaSolarCharger/Shared/Enums/CarValueType.cs b/TeslaSolarCharger/Shared/Enums/CarValueType.cs index d5108f416..6ace40156 100644 --- a/TeslaSolarCharger/Shared/Enums/CarValueType.cs +++ b/TeslaSolarCharger/Shared/Enums/CarValueType.cs @@ -23,5 +23,6 @@ public enum CarValueType AsleepOrOffline, Gear, Speed, + VehicleName, Unknown = 9999, } From fbaf6ca60bee325d89743f1bda3a818383b10a3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Tue, 31 Dec 2024 16:20:08 +0100 Subject: [PATCH 24/81] fix(BackendApiService): set new ExpiresAtUtc on token refresh --- TeslaSolarCharger/Server/Services/BackendApiService.cs | 1 + TeslaSolarCharger/Server/Services/TokenHelper.cs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/TeslaSolarCharger/Server/Services/BackendApiService.cs b/TeslaSolarCharger/Server/Services/BackendApiService.cs index 3c008c446..4457e05b4 100644 --- a/TeslaSolarCharger/Server/Services/BackendApiService.cs +++ b/TeslaSolarCharger/Server/Services/BackendApiService.cs @@ -136,6 +136,7 @@ public async Task RefreshBackendTokenIfNeeded() var newToken = JsonConvert.DeserializeObject(responseContent) ?? throw new InvalidDataException("Could not parse token"); token.AccessToken = newToken.AccessToken; token.RefreshToken = newToken.RefreshToken; + token.ExpiresAtUtc = DateTimeOffset.FromUnixTimeSeconds(newToken.ExpiresAt); await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false); memoryCache.Remove(constants.BackendTokenStateKey); } diff --git a/TeslaSolarCharger/Server/Services/TokenHelper.cs b/TeslaSolarCharger/Server/Services/TokenHelper.cs index 31e335b3f..a0d6c78f7 100644 --- a/TeslaSolarCharger/Server/Services/TokenHelper.cs +++ b/TeslaSolarCharger/Server/Services/TokenHelper.cs @@ -140,6 +140,7 @@ private async Task GetUncachedFleetApiTokenSt return new() { TokenState = TokenState.Expired, + ExpiresAtUtc = dateTimeProvider.DateTimeOffSetUtcNow().AddSeconds(validFleetApiToken.Value.Value), }; } return new() @@ -215,7 +216,7 @@ private MemoryCacheEntryOptions GetCacheEntryOptions(DateTimeOffset? validUntil) { var validFor = TimeSpan.FromMinutes(15); var currentDate = dateTimeProvider.DateTimeOffSetUtcNow(); - if (validUntil != default && (validUntil < (currentDate + validFor))) + if (validUntil != default && (validUntil < (currentDate + validFor)) && (validUntil > currentDate)) { validFor = validUntil.Value - currentDate; } From 997ee64550d9f09fe9d346be7eb5631ecd34946e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Tue, 31 Dec 2024 16:55:43 +0100 Subject: [PATCH 25/81] fix(FleetTelemetryWebSocketService): update url --- .../Services/FleetTelemetryWebSocketService.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs b/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs index ec479e279..7e44b40d3 100644 --- a/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs +++ b/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs @@ -15,6 +15,7 @@ using TeslaSolarCharger.Shared.Dtos.Contracts; using TeslaSolarCharger.Shared.Dtos.Settings; using TeslaSolarCharger.Shared.Enums; +using TeslaSolarCharger.Shared.Resources.Contracts; namespace TeslaSolarCharger.Server.Services; @@ -23,8 +24,7 @@ public class FleetTelemetryWebSocketService( IConfigurationWrapper configurationWrapper, IDateTimeProvider dateTimeProvider, IServiceProvider serviceProvider, - ISettings settings, - ITscConfigurationService tscConfigurationService) : IFleetTelemetryWebSocketService + ISettings settings) : IFleetTelemetryWebSocketService { private readonly TimeSpan _heartbeatsendTimeout = TimeSpan.FromSeconds(5); @@ -120,8 +120,16 @@ private async Task ConnectToFleetTelemetryApi(string vin, bool useFleetTelemetry var currentDate = dateTimeProvider.UtcNow(); var scope = serviceProvider.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); + var tscConfigurationService = scope.ServiceProvider.GetRequiredService(); + var constants = scope.ServiceProvider.GetRequiredService(); + var decryptionKey = await tscConfigurationService.GetConfigurationValueByKey(constants.TeslaTokenEncryptionKeyKey); + if (decryptionKey == default) + { + logger.LogError("Decryption key not found do not send command"); + throw new InvalidOperationException("No Decryption key found."); + } var url = configurationWrapper.FleetTelemetryApiUrl() + - $"vin={vin}&forceReconfiguration=false&includeLocation={useFleetTelemetryForLocationData}"; + $"vin={vin}&forceReconfiguration=false&includeTrackingRelevantFields={useFleetTelemetryForLocationData}&encryptionKey={Uri.EscapeDataString(decryptionKey)}"; var authToken = await context.BackendTokens.AsNoTracking().SingleOrDefaultAsync(); if(authToken == default) { From 4e5171581c6a97db52a145534adffae3ffdc385b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Wed, 1 Jan 2025 14:04:25 +0100 Subject: [PATCH 26/81] feat(PossibleIssues): show backend api isssues after first occurrence --- .../Server/Resources/PossibleIssues/PossibleIssues.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs b/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs index 4556bb417..04a1cfa16 100644 --- a/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs +++ b/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs @@ -165,7 +165,7 @@ public class PossibleIssues(IIssueKeys issueKeys) : IPossibleIssues { IssueSeverity = IssueSeverity.Error, IsTelegramEnabled = true, - ShowErrorAfterOccurrences = 2, + ShowErrorAfterOccurrences = 1, HasPlaceHolderIssueKey = false, HideOccurrenceCount = true, } @@ -174,7 +174,7 @@ public class PossibleIssues(IIssueKeys issueKeys) : IPossibleIssues { IssueSeverity = IssueSeverity.Error, IsTelegramEnabled = true, - ShowErrorAfterOccurrences = 2, + ShowErrorAfterOccurrences = 1, HasPlaceHolderIssueKey = false, HideOccurrenceCount = true, } From 11833e55d27e93ea62cd410c81d4af41254f4cf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Wed, 1 Jan 2025 14:42:53 +0100 Subject: [PATCH 27/81] fix(PossibleIssues): add missing issue --- .../Server/Resources/PossibleIssues/PossibleIssues.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs b/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs index 04a1cfa16..4762b4f0b 100644 --- a/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs +++ b/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs @@ -188,6 +188,15 @@ public class PossibleIssues(IIssueKeys issueKeys) : IPossibleIssues HideOccurrenceCount = true, } }, + { issueKeys.Solar4CarSideFleetApiNonSuccessStatusCode, new DtoIssue + { + IssueSeverity = IssueSeverity.Error, + IsTelegramEnabled = true, + ShowErrorAfterOccurrences = 2, + HasPlaceHolderIssueKey = false, + HideOccurrenceCount = true, + } + }, }; public DtoIssue GetIssueByKey(string key) From 05bb658fb16c38d7689fa119103c667b4dd78d7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Sun, 5 Jan 2025 13:20:01 +0100 Subject: [PATCH 28/81] feat(BackendApiService): send installation info to new endpoint --- .../TscBackend/DtoInstallationInformation.cs | 8 ---- .../Server/Services/BackendApiService.cs | 39 ++++++++++++++----- 2 files changed, 30 insertions(+), 17 deletions(-) delete mode 100644 TeslaSolarCharger/Server/Dtos/TscBackend/DtoInstallationInformation.cs diff --git a/TeslaSolarCharger/Server/Dtos/TscBackend/DtoInstallationInformation.cs b/TeslaSolarCharger/Server/Dtos/TscBackend/DtoInstallationInformation.cs deleted file mode 100644 index df05e1ee1..000000000 --- a/TeslaSolarCharger/Server/Dtos/TscBackend/DtoInstallationInformation.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace TeslaSolarCharger.Server.Dtos.TscBackend; - -public class DtoInstallationInformation -{ - public string InstallationId { get; set; } - public string Version { get; set; } - public string InfoReason { get; set; } -} diff --git a/TeslaSolarCharger/Server/Services/BackendApiService.cs b/TeslaSolarCharger/Server/Services/BackendApiService.cs index 4457e05b4..0e0667b2f 100644 --- a/TeslaSolarCharger/Server/Services/BackendApiService.cs +++ b/TeslaSolarCharger/Server/Services/BackendApiService.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore; +using LanguageExt.Traits; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using Newtonsoft.Json; using System.Diagnostics; @@ -11,6 +12,7 @@ using TeslaSolarCharger.Server.Services.Contracts; using TeslaSolarCharger.Shared.Contracts; using TeslaSolarCharger.Shared.Dtos; +using TeslaSolarCharger.Shared.Enums; using TeslaSolarCharger.Shared.Resources.Contracts; namespace TeslaSolarCharger.Server.Services; @@ -150,21 +152,40 @@ internal string GenerateAuthUrl(DtoTeslaOAuthRequestInformation oAuthInformation } public async Task PostInstallationInformation(string reason) + { try { - var url = configurationWrapper.BackendApiBaseUrl() + "Tsc/NotifyInstallation"; + var tokenState = await tokenHelper.GetBackendTokenState(true); var installationId = await tscConfigurationService.GetInstallationId().ConfigureAwait(false); var currentVersion = await GetCurrentVersion().ConfigureAwait(false); - var installationInformation = new DtoInstallationInformation - { - InstallationId = installationId.ToString(), - Version = currentVersion ?? "unknown", - InfoReason = reason, - }; + var url = configurationWrapper.BackendApiBaseUrl() + $"Client/NotifyInstallation?version={Uri.EscapeDataString(currentVersion ?? string.Empty)}&infoReason{Uri.EscapeDataString(reason)}"; using var httpClient = new HttpClient(); httpClient.Timeout = TimeSpan.FromSeconds(10); - var response = await httpClient.PostAsJsonAsync(url, installationInformation).ConfigureAwait(false); + if (tokenState == TokenState.UpToDate) + { + var token = await teslaSolarChargerContext.BackendTokens.SingleAsync(); + var request = new HttpRequestMessage(HttpMethod.Post, url); + request.Headers.Authorization = new("Bearer", token.AccessToken); + var response = await httpClient.SendAsync(request).ConfigureAwait(false); + if (response.IsSuccessStatusCode) + { + logger.LogInformation("Sent installation information to Backend"); + return; + } + + logger.LogWarning("Error while sending installation information to backend. StatusCode: {statusCode}. Trying again without token", response.StatusCode); + } + url += $"&installationId={Uri.EscapeDataString(installationId.ToString())}"; + var nonTokenRequest = new HttpRequestMessage(HttpMethod.Post, url); + var nonTokenResponse = await httpClient.SendAsync(nonTokenRequest).ConfigureAwait(false); + if (nonTokenResponse.IsSuccessStatusCode) + { + logger.LogInformation("Sent installation information to Backend"); + return; + } + + logger.LogWarning("Error while sending installation information to backend. StatusCode: {statusCode}.", nonTokenResponse.StatusCode); } catch (Exception e) { From 9fda99f2d08077862879e7df37eacb2c39b3414e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Sun, 5 Jan 2025 13:39:12 +0100 Subject: [PATCH 29/81] feat(BackendApiService): use separateEndpoint for no token installation info posts --- TeslaSolarCharger/Server/Services/BackendApiService.cs | 7 ++++--- TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/TeslaSolarCharger/Server/Services/BackendApiService.cs b/TeslaSolarCharger/Server/Services/BackendApiService.cs index 0e0667b2f..9596b8927 100644 --- a/TeslaSolarCharger/Server/Services/BackendApiService.cs +++ b/TeslaSolarCharger/Server/Services/BackendApiService.cs @@ -159,11 +159,12 @@ public async Task PostInstallationInformation(string reason) var tokenState = await tokenHelper.GetBackendTokenState(true); var installationId = await tscConfigurationService.GetInstallationId().ConfigureAwait(false); var currentVersion = await GetCurrentVersion().ConfigureAwait(false); - var url = configurationWrapper.BackendApiBaseUrl() + $"Client/NotifyInstallation?version={Uri.EscapeDataString(currentVersion ?? string.Empty)}&infoReason{Uri.EscapeDataString(reason)}"; + using var httpClient = new HttpClient(); httpClient.Timeout = TimeSpan.FromSeconds(10); if (tokenState == TokenState.UpToDate) { + var url = configurationWrapper.BackendApiBaseUrl() + $"Client/NotifyInstallation?version={Uri.EscapeDataString(currentVersion ?? string.Empty)}&infoReason{Uri.EscapeDataString(reason)}"; var token = await teslaSolarChargerContext.BackendTokens.SingleAsync(); var request = new HttpRequestMessage(HttpMethod.Post, url); request.Headers.Authorization = new("Bearer", token.AccessToken); @@ -176,8 +177,8 @@ public async Task PostInstallationInformation(string reason) logger.LogWarning("Error while sending installation information to backend. StatusCode: {statusCode}. Trying again without token", response.StatusCode); } - url += $"&installationId={Uri.EscapeDataString(installationId.ToString())}"; - var nonTokenRequest = new HttpRequestMessage(HttpMethod.Post, url); + var noTokenUrl = configurationWrapper.BackendApiBaseUrl() + $"Client/NotifyInstallationAnonymous?version={Uri.EscapeDataString(currentVersion ?? string.Empty)}&infoReason{Uri.EscapeDataString(reason)}&installationId={Uri.EscapeDataString(installationId.ToString())}"; + var nonTokenRequest = new HttpRequestMessage(HttpMethod.Post, noTokenUrl); var nonTokenResponse = await httpClient.SendAsync(nonTokenRequest).ConfigureAwait(false); if (nonTokenResponse.IsSuccessStatusCode) { diff --git a/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs b/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs index aafb1e0df..33d5ffbc8 100644 --- a/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs +++ b/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Newtonsoft.Json; -using System.Configuration; using TeslaSolarCharger.Shared.Contracts; using TeslaSolarCharger.Shared.Dtos.BaseConfiguration; using TeslaSolarCharger.Shared.Dtos.Contracts; From 9a4fcb3fc95a72f135952f97b3cc64456ebeec85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Sun, 5 Jan 2025 23:07:13 +0100 Subject: [PATCH 30/81] fix(PossibleIssues): MakeSolar4CarSideFleetApiStatusCode HasPlaceholder --- .../Server/Resources/PossibleIssues/PossibleIssues.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs b/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs index 4762b4f0b..d57abfeb5 100644 --- a/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs +++ b/TeslaSolarCharger/Server/Resources/PossibleIssues/PossibleIssues.cs @@ -193,7 +193,7 @@ public class PossibleIssues(IIssueKeys issueKeys) : IPossibleIssues IssueSeverity = IssueSeverity.Error, IsTelegramEnabled = true, ShowErrorAfterOccurrences = 2, - HasPlaceHolderIssueKey = false, + HasPlaceHolderIssueKey = true, HideOccurrenceCount = true, } }, From 61bf3ea1bce9d92c3b6a3c051147fb260d084333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Mon, 6 Jan 2025 14:20:19 +0100 Subject: [PATCH 31/81] refactor(BackendApiService): add generic method for backend requests --- .../BackendInformationDisplayComponent.razor | 1 - .../Dialogs/CreateBackendTokenDialog.razor | 1 + .../Helper/Contracts/IHttpClientHelper.cs | 8 +- .../Client/Helper/HttpClientHelper.cs | 113 ++++++++++++---- .../Client/Pages/BackendConnection.razor | 41 ++++++ TeslaSolarCharger/Client/Shared/NavMenu.razor | 5 + .../Middlewares/ErrorHandlingMiddleware.cs | 92 +++++++++++++ TeslaSolarCharger/Server/Program.cs | 2 + .../Server/ServiceCollectionExtensions.cs | 2 + .../Server/Services/BackendApiService.cs | 123 +++++++++++++++--- TeslaSolarCharger/Shared/Dtos/Result.cs | 7 + 11 files changed, 350 insertions(+), 45 deletions(-) create mode 100644 TeslaSolarCharger/Client/Pages/BackendConnection.razor create mode 100644 TeslaSolarCharger/Server/Middlewares/ErrorHandlingMiddleware.cs create mode 100644 TeslaSolarCharger/Shared/Dtos/Result.cs diff --git a/TeslaSolarCharger/Client/Components/BackendInformationDisplayComponent.razor b/TeslaSolarCharger/Client/Components/BackendInformationDisplayComponent.razor index 2b821fdfd..e4ccb8b52 100644 --- a/TeslaSolarCharger/Client/Components/BackendInformationDisplayComponent.razor +++ b/TeslaSolarCharger/Client/Components/BackendInformationDisplayComponent.razor @@ -1,5 +1,4 @@ @using TeslaSolarCharger.Shared.Dtos -@using TeslaSolarCharger.Shared.Dtos @using TeslaSolarCharger.Shared.Enums @inject HttpClient HttpClient diff --git a/TeslaSolarCharger/Client/Dialogs/CreateBackendTokenDialog.razor b/TeslaSolarCharger/Client/Dialogs/CreateBackendTokenDialog.razor index ab8f913dc..a93c3fca1 100644 --- a/TeslaSolarCharger/Client/Dialogs/CreateBackendTokenDialog.razor +++ b/TeslaSolarCharger/Client/Dialogs/CreateBackendTokenDialog.razor @@ -5,6 +5,7 @@ + diff --git a/TeslaSolarCharger/Client/Helper/Contracts/IHttpClientHelper.cs b/TeslaSolarCharger/Client/Helper/Contracts/IHttpClientHelper.cs index 4d1df2f6f..dfa0e460d 100644 --- a/TeslaSolarCharger/Client/Helper/Contracts/IHttpClientHelper.cs +++ b/TeslaSolarCharger/Client/Helper/Contracts/IHttpClientHelper.cs @@ -1,4 +1,6 @@ -namespace TeslaSolarCharger.Client.Helper.Contracts; +using TeslaSolarCharger.Shared.Dtos; + +namespace TeslaSolarCharger.Client.Helper.Contracts; public interface IHttpClientHelper { @@ -6,4 +8,8 @@ public interface IHttpClientHelper Task SendGetRequestWithSnackbarAsync(string url); Task SendPostRequestWithSnackbarAsync(string url, object? content); Task SendPostRequestWithSnackbarAsync(string url, object? content); + Task> SendGetRequestAsync(string url); + Task> SendGetRequestAsync(string url); + Task> SendPostRequestAsync(string url, object? content); + Task> SendPostRequestAsync(string url, object? content); } diff --git a/TeslaSolarCharger/Client/Helper/HttpClientHelper.cs b/TeslaSolarCharger/Client/Helper/HttpClientHelper.cs index 1e46378aa..6559a08b9 100644 --- a/TeslaSolarCharger/Client/Helper/HttpClientHelper.cs +++ b/TeslaSolarCharger/Client/Helper/HttpClientHelper.cs @@ -3,6 +3,7 @@ using Newtonsoft.Json; using System.Net.Http.Json; using TeslaSolarCharger.Client.Helper.Contracts; +using TeslaSolarCharger.Shared.Dtos; namespace TeslaSolarCharger.Client.Helper; @@ -28,14 +29,73 @@ public async Task SendPostRequestWithSnackbarAsync(string url, object? content) await SendRequestWithSnackbarInternalAsync(HttpMethod.Post, url, content); } + public async Task> SendGetRequestAsync(string url) + { + return await SendRequestCoreAsync(HttpMethod.Get, url, null); + } + + public async Task> SendGetRequestAsync(string url) + { + return await SendRequestCoreAsync(HttpMethod.Get, url, null); + } + + public async Task> SendPostRequestAsync(string url, object? content) + { + return await SendRequestCoreAsync(HttpMethod.Post, url, content); + } + + public async Task> SendPostRequestAsync(string url, object? content) + { + return await SendRequestCoreAsync(HttpMethod.Post, url, content); + } + private async Task SendRequestWithSnackbarInternalAsync( HttpMethod method, string url, object? content) + { + try + { + // Call the same core method + var result = await SendRequestCoreAsync(method, url, content); + + if (result.HasError) + { + // Show error in Snackbar + snackbar.Add(result.ErrorMessage ?? "EmptyErrorMessage", Severity.Error); + return default; + } + + // Return the deserialized data + return result.Data; + } + catch (Exception ex) + { + // If you need special catch logic that includes a Snackbar, do it here. + var message = $"{url}: Unexpected error: {ex.Message}"; + snackbar.Add(message, Severity.Error, config => + { + config.Action = "Details"; + config.ActionColor = Color.Primary; + config.Onclick = snackbar1 => dialogHelper.ShowTextDialog( + "Error Details", + $"Unexpected error while calling {url}: {ex.Message}{Environment.NewLine}{ex.StackTrace}" + ); + }); + + return default; + } + } + + private async Task> SendRequestCoreAsync( + HttpMethod method, + string url, + object? content) { try { HttpResponseMessage response; + if (method == HttpMethod.Get) { response = await httpClient.GetAsync(url); @@ -50,57 +110,60 @@ public async Task SendPostRequestWithSnackbarAsync(string url, object? content) } else { - throw new ArgumentException("Unsupported HTTP method", nameof(method)); + return new Result( + default, + $"Unsupported HTTP method: {method}" + ); } if (response.IsSuccessStatusCode) { var responseContent = await response.Content.ReadAsStringAsync(); + if (typeof(T) != typeof(object)) { var deserializedObject = JsonConvert.DeserializeObject(responseContent); + if (deserializedObject == null) { - snackbar.Add($"{url}: The string could not be deserialized to the object type.", Severity.Error); + return new Result( + default, + $"{url}: Could not deserialize response to {typeof(T).Name}." + ); } - return deserializedObject; - } - if (string.IsNullOrEmpty(responseContent)) + return new Result(deserializedObject, null); + } + else { - return default; + // If T=object, we don't do any deserialization + return new Result( + default, + null + ); } - snackbar.Add($"{url}: The specified object type is not supported", Severity.Error); - return default; - } - else if (response.StatusCode == System.Net.HttpStatusCode.InternalServerError) - { - var problemDetails = await response.Content.ReadFromJsonAsync(); - var message = problemDetails != null ? $"Error: {problemDetails.Detail}" : "An error occurred"; - snackbar.Add(message, Severity.Error); } else { - var message = $"{url}: Unexpected error: {response.StatusCode}"; - snackbar.Add(message, Severity.Error); + var problemDetails = await response.Content.ReadFromJsonAsync(); + var message = problemDetails != null + ? $"Error: {problemDetails.Detail}" + : "An error occurred on the server."; + + return new Result(default, message); } } catch (HttpRequestException ex) { + // Network-level error var message = $"{url}: Network error: {ex.Message}"; - snackbar.Add(message, Severity.Error); + return new Result(default, message); } catch (Exception ex) { + // Any other unexpected error var message = $"{url}: Unexpected error: {ex.Message}"; - snackbar.Add(message, Severity.Error, config => - { - config.Action = "Details"; - config.ActionColor = Color.Primary; - config.Onclick = snackbar1 => dialogHelper.ShowTextDialog("Error Details", - $"Unexpected error while calling {url}: {ex.Message}{Environment.NewLine}{ex.StackTrace}"); - }); + return new Result(default, message); } - return default; } } diff --git a/TeslaSolarCharger/Client/Pages/BackendConnection.razor b/TeslaSolarCharger/Client/Pages/BackendConnection.razor new file mode 100644 index 000000000..590cd9dfc --- /dev/null +++ b/TeslaSolarCharger/Client/Pages/BackendConnection.razor @@ -0,0 +1,41 @@ +@page "/backendconnection" +@using TeslaSolarCharger.Client.Helper.Contracts +@using TeslaSolarCharger.Shared.Dtos + +@inject IHttpClientHelper HttpClientHelper + +

Backend Connection

+ + +
You need to create a Solar4Car.com account and create at least a base app subscription before you can proceed.
+
+ + + + +@if (LoginErrorMessage != default) +{ +
+ Login failed: @LoginErrorMessage +
+} + + + + + +@code { + private readonly DtoBackendLogin _backendLogin = new DtoBackendLogin(); + + private string? LoginErrorMessage { get; set; } + + async Task Submit() + { + var result = await HttpClientHelper.SendPostRequestAsync("api/BackendApi/LoginToBackend", _backendLogin); + LoginErrorMessage = result.HasError ? result.ErrorMessage : default; + } +} diff --git a/TeslaSolarCharger/Client/Shared/NavMenu.razor b/TeslaSolarCharger/Client/Shared/NavMenu.razor index 7c9196e15..f4ae68be6 100644 --- a/TeslaSolarCharger/Client/Shared/NavMenu.razor +++ b/TeslaSolarCharger/Client/Shared/NavMenu.razor @@ -31,6 +31,11 @@ Base Configuration +