Skip to content

Commit

Permalink
[585] feat: add background sync support for garmin mfa users (#622)
Browse files Browse the repository at this point in the history
* [585] feat: add background sync support for garmin mfa users

* clean up some dependencies

* testing and hardening, also add mfa prompt when sync toggled from ui

* some cleanup

* rebase

* uplifting to latest flurl, still need to work through serialization issues

* fix happy path upload and conflict upload

* mfa appears to be working with saved tokens

* clean up some things

* fix unit tests

* update version and release notes

* documentation updates

* fix some startup logging

* couple tweaks

* bump some dependencies
  • Loading branch information
philosowaffle authored Aug 17, 2024
1 parent 27d066f commit 9763534
Show file tree
Hide file tree
Showing 66 changed files with 1,218 additions and 1,019 deletions.
686 changes: 314 additions & 372 deletions images/P2G_diagram.drawio

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions mkdocs/docs/configuration/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

## Web UI Configuration

Most of the most common settings can be configured via the UI itself. Additional lower level settings can be provided via config file.
The most common settings can be configured via the UI itself. Additional lower level settings can be provided via config file.

1. Settings
1. [App Settings](app.md)
Expand All @@ -22,7 +22,7 @@ Most of the most common settings can be configured via the UI itself. Additiona

## Windows UI Configuration

Most of the most common settings can be configured via the UI itself.
The most common settings can be configured via the UI itself.

1. Settings
1. [App Settings](app.md)
Expand Down
14 changes: 3 additions & 11 deletions mkdocs/docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,10 @@

Below are a list of commonly asked questions. For even more help head on over to the [discussion forum](https://github.com/philosowaffle/peloton-to-garmin/discussions).

## VO2 Max and TSS
## VO2 Max, TE, TSS and more...

Garmin _unlocks_ certain workout metrics and fields based on the Garmin device you personally own, one of those metrics is VO2 Max. This means that if your personal device supports VO2 Max calucations, then your Peloton workouts will also generate VO2 Max when using the default P2G device settings. If your personal device does not support VO2 Max calculations, then unfortunately your Peloton workouts will also not generate any VO2 Max data.

You can check the [Owners Manual](https://support.garmin.com/en-US/ql/?focus=manuals) for your personal device to see if it already supports the VO2 max field.

Garmin will only generate a VO2 max for your workouts if all of the following criteria are met:

1. Your personal Garmin device already supports VO2 Max Calculations
1. The workout is associated with an [allowed device](https://support.garmin.com/en-US/?faq=lWqSVlq3w76z5WoihLy5f8)
1. You have met all of [Garmin's VO2 requirements](https://support.garmin.com/en-US/?faq=lWqSVlq3w76z5WoihLy5f8) for your workout type
Checkout the dedicated [Category](https://github.com/philosowaffle/peloton-to-garmin/discussions/categories/te-tss-vo2-intensity-minutes) in the Discussion forum, I recommend starting with [this post](https://github.com/philosowaffle/peloton-to-garmin/discussions/654).

## Garmin Two Step Verification

Only some [install options have support](install/index.md) for Garmin Two Step Verification. In all cases, automatic-syncing is never supported when your Garmin account is protected by two step verification.
Yes, P2G supports Garmin's Multi-factor Authentication option! However,oOnly some [install options have support](install/index.md) so you'll be limited to one of these options.
11 changes: 8 additions & 3 deletions src/Api.Contract/SyncContracts.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using Common.Database;

namespace Api.Contract;
namespace Api.Contract;

public record SyncGetResponse
{
Expand All @@ -25,6 +23,13 @@ public string AutoSyncHealthString
public DateTime? NextSyncTime { get; init; }
}

public enum Status : byte
{
NotRunning = 0,
Running = 1,
UnHealthy = 2,
Dead = 3
}
public record SyncPostRequest
{
public SyncPostRequest()
Expand Down
2 changes: 1 addition & 1 deletion src/Api.Service/Api.Service.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Philosowaffle.Capability.ReleaseChecks" Version="1.1.0" />
<PackageReference Include="Philosowaffle.Capability.ReleaseChecks" Version="2.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
9 changes: 6 additions & 3 deletions src/Api.Service/ApiStartupServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
using Api.Services;
using Garmin.Auth;
using Api.Service;
using Garmin.Database;
using Sync.Database;

namespace SharedStartup;

Expand All @@ -33,9 +35,10 @@ public static void ConfigureP2GApiServices(this IServiceCollection services)
// GARMIN
services.AddSingleton<IGarminUploader, GarminUploader>();
services.AddSingleton<IGarminApiClient, Garmin.ApiClient>();
services.AddSingleton<IGarminAuthenticationService, GarminAuthenticationService>();

// IO
services.AddSingleton<IGarminAuthenticationService, GarminAuthenticationService>();
services.AddSingleton<IGarminDb, GarminDb>();

// IO
services.AddSingleton<IFileHandling, IOWrapper>();

// MIGRATIONS
Expand Down
60 changes: 39 additions & 21 deletions src/Api.Service/BackgroundSyncJob.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
using Common.Database;
using Common.Dto;
using Common.Dto;
using Common.Observe;
using Common.Service;
using Common.Stateful;
using Garmin.Auth;
using Microsoft.Extensions.Hosting;
using Prometheus;
using Sync;
using Sync.Database;
using Sync.Dto;
using static Common.Observe.Metrics;
using ILogger = Serilog.ILogger;
using PromMetrics = Prometheus.Metrics;

namespace Api.Services;
namespace Api.Service;

public class BackgroundSyncJob : BackgroundService
{
Expand All @@ -23,12 +25,13 @@ public class BackgroundSyncJob : BackgroundService
private readonly ISettingsService _settingsService;
private readonly ISyncStatusDb _syncStatusDb;
private readonly ISyncService _syncService;

private readonly IGarminAuthenticationService _garminAuthService;

private bool? _previousPollingState;
private Settings _config;


public BackgroundSyncJob(ISettingsService settingsService,ISyncStatusDb syncStatusDb, ISyncService syncService)
public BackgroundSyncJob(ISettingsService settingsService, ISyncStatusDb syncStatusDb, ISyncService syncService, IGarminAuthenticationService garminAuthService)
{
_settingsService = settingsService;
_syncStatusDb = syncStatusDb;
Expand All @@ -37,6 +40,7 @@ public BackgroundSyncJob(ISettingsService settingsService,ISyncStatusDb syncStat
_syncService = syncService;

_config = new Settings();
_garminAuthService = garminAuthService;
}

protected override Task ExecuteAsync(CancellationToken stoppingToken)
Expand All @@ -49,31 +53,31 @@ private async Task RunAsync(CancellationToken stoppingToken)
{
_config = await _settingsService.GetSettingsAsync();

if (_config.Garmin.Upload && _config.Garmin.TwoStepVerificationEnabled && _config.App.EnablePolling)
{
_logger.Error("Background Sync cannot be enabled when Garmin TwoStepVerification is enabled.");
_logger.Information("Sync Service stopped.");
return;
}

SyncServiceState.Enabled = _config.App.EnablePolling;
SyncServiceState.PollingIntervalSeconds = _config.App.PollingIntervalSeconds;

while (!stoppingToken.IsCancellationRequested)
{
int stepIntervalSeconds = 5;

if (await NotPollingAsync())
if (await PollingDisabled())
{
Thread.Sleep(stepIntervalSeconds * 1000);
continue;
}

if (await NeedToWaitForMFAToBeCompletedAsync())
{
_logger.Information("Can't start background syncing until MFA flow is completed for the first time.");
Thread.Sleep(stepIntervalSeconds * 1000);
continue;
}

await SyncAsync();

_logger.Information("Sleeping for {@Seconds} seconds...", SyncServiceState.PollingIntervalSeconds);
for (int i = 1; i < SyncServiceState.PollingIntervalSeconds; i+=stepIntervalSeconds)

for (int i = 1; i < SyncServiceState.PollingIntervalSeconds; i += stepIntervalSeconds)
{
Thread.Sleep(stepIntervalSeconds * 1000);
if (await StateChangedAsync()) break;
Expand All @@ -92,9 +96,9 @@ private async Task<bool> StateChangedAsync()
return _previousPollingState != SyncServiceState.Enabled;
}

private async Task<bool> NotPollingAsync()
private async Task<bool> PollingDisabled()
{
using var tracing = Tracing.Trace($"{nameof(BackgroundService)}.{nameof(NotPollingAsync)}");
using var tracing = Tracing.Trace($"{nameof(BackgroundService)}.{nameof(PollingDisabled)}");

if (await StateChangedAsync())
{
Expand All @@ -111,26 +115,40 @@ private async Task<bool> NotPollingAsync()
return !SyncServiceState.Enabled;
}

private async Task<bool> NeedToWaitForMFAToBeCompletedAsync()
{
_config = await _settingsService.GetSettingsAsync();
if (_config.Garmin.TwoStepVerificationEnabled)
{
var alreadyHaveToken = await _garminAuthService.GarminAuthTokenExistsAndIsValidAsync();
return !alreadyHaveToken;
}
return false;
}

private async Task SyncAsync()
{
using var tracing = Tracing.Trace($"{nameof(BackgroundService)}.{nameof(SyncAsync)}");

try
{
var result = await _syncService.SyncAsync(_config.Peloton.NumWorkoutsToDownload);
if(result.SyncSuccess)
if (result.SyncSuccess)
{
Health.Set(HealthStatus.Healthy);
} else
}
else
{
Health.Set(HealthStatus.UnHealthy);
}

} catch (Exception e)
}
catch (Exception e)
{
_logger.Error(e, "Uncaught Exception.");

} finally
}
finally
{
var now = DateTime.UtcNow;
var nextRunTime = now.AddSeconds(_config.App.PollingIntervalSeconds);
Expand Down
25 changes: 9 additions & 16 deletions src/Api.Service/SettingsUpdaterService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using Common;
using Common.Dto;
using Common.Service;
using Common.Stateful;
using Garmin.Auth;

namespace Api.Service;

Expand All @@ -17,11 +17,13 @@ public class SettingsUpdaterService : ISettingsUpdaterService
{
private readonly IFileHandling _fileHandler;
private readonly ISettingsService _settingsService;
private readonly IGarminAuthenticationService _garminAuthService;

public SettingsUpdaterService(IFileHandling fileHandler, ISettingsService settingsService)
public SettingsUpdaterService(IFileHandling fileHandler, ISettingsService settingsService, IGarminAuthenticationService garminAuthService)
{
_fileHandler = fileHandler;
_settingsService = settingsService;
_garminAuthService = garminAuthService;
}

public async Task<ServiceResult<App>> UpdateAppSettingsAsync(App updatedAppSettings)
Expand All @@ -38,13 +40,6 @@ public async Task<ServiceResult<App>> UpdateAppSettingsAsync(App updatedAppSetti
var settings = await _settingsService.GetSettingsAsync();
settings.App = updatedAppSettings;

if (settings.Garmin.TwoStepVerificationEnabled && settings.App.EnablePolling)
{
result.Successful = false;
result.Error = new ServiceError() { Message = "Automatic Syncing cannot be enabled when Garmin TwoStepVerification is enabled." };
return result;
}

await _settingsService.UpdateSettingsAsync(settings);
var updatedSettings = await _settingsService.GetSettingsAsync();

Expand Down Expand Up @@ -93,14 +88,12 @@ public async Task<ServiceResult<SettingsGarminGetResponse>> UpdateGarminSettings
}

var settings = await _settingsService.GetSettingsAsync();
settings.Garmin = updatedGarminSettings.Map();

if (settings.Garmin.Upload && settings.Garmin.TwoStepVerificationEnabled && settings.App.EnablePolling)
{
result.Successful = false;
result.Error = new ServiceError() { Message = "Garmin TwoStepVerification cannot be enabled while Automatic Syncing is enabled. Please disable Automatic Syncing first." };
return result;
}
if (settings.Garmin.Password != updatedGarminSettings.Password
|| settings.Garmin.Email != updatedGarminSettings.Email)
await _garminAuthService.SignOutAsync();

settings.Garmin = updatedGarminSettings.Map();

await _settingsService.UpdateSettingsAsync(settings);
var updatedSettings = await _settingsService.GetSettingsAsync();
Expand Down
7 changes: 3 additions & 4 deletions src/Api.Service/Validators/SyncValidators.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
using Api.Contract;
using Api.Service.Helpers;
using Common;
using Common.Dto;
using Common.Stateful;
using Garmin.Dto;
using Microsoft.AspNetCore.Mvc;

namespace Api.Service.Validators;
Expand All @@ -16,7 +15,7 @@ public static (bool, ErrorResponse?) IsValid(this SyncPostRequest request, Setti

if (settings.Garmin.Upload && settings.Garmin.TwoStepVerificationEnabled)
{
if (garminAuth is null || !garminAuth.IsValid(settings))
if (garminAuth is null || !garminAuth.IsValid())
{
result = new ErrorResponse("Must initialize Garmin two factor auth token before sync can be preformed.", ErrorCode.NeedToInitGarminMFAAuth);
return (false, result);
Expand All @@ -38,7 +37,7 @@ public static (bool, ActionResult?) IsValidHttp(this SyncPostRequest request, Se

if (settings.Garmin.Upload && settings.Garmin.TwoStepVerificationEnabled)
{
if (garminAuth is null || !garminAuth.IsValid(settings))
if (garminAuth is null || !garminAuth.IsValid())
{
result = new UnauthorizedObjectResult(new ErrorResponse("Must initialize Garmin two factor auth token before sync can be preformed.", ErrorCode.NeedToInitGarminMFAAuth));
return (false, result);
Expand Down
12 changes: 6 additions & 6 deletions src/Api/Controllers/GarminAuthenticationController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Api.Service.Helpers;
using Common.Service;
using Garmin.Auth;
using Garmin.Dto;
using Microsoft.AspNetCore.Mvc;

namespace Api.Controllers
Expand All @@ -25,10 +26,9 @@ public GarminAuthenticationController(IGarminAuthenticationService garminAuthSer
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<GarminAuthenticationGetResponse>> GetAsync()
{
var settings = await _settingsService.GetSettingsAsync();
var auth = _settingsService.GetGarminAuthentication(settings.Garmin.Email);
var auth = await _garminAuthService.GetGarminAuthenticationAsync();
var result = new GarminAuthenticationGetResponse() { IsAuthenticated = auth?.IsValid() ?? false };

var result = new GarminAuthenticationGetResponse() { IsAuthenticated = auth?.IsValid(settings) ?? false };
return Ok(result);
}

Expand All @@ -54,14 +54,14 @@ public async Task<ActionResult> SignInAsync()
{
if (!settings.Garmin.TwoStepVerificationEnabled)
{
await _garminAuthService.RefreshGarminAuthenticationAsync();
await _garminAuthService.SignInAsync();
return Created("api/garminauthentication", new GarminAuthenticationGetResponse() { IsAuthenticated = true });
}
else
{
var auth = await _garminAuthService.RefreshGarminAuthenticationAsync();
var auth = await _garminAuthService.SignInAsync();

if (auth.AuthStage == Common.Stateful.AuthStage.NeedMfaToken)
if (auth.AuthStage == AuthStage.NeedMfaToken)
return Accepted();

return Created("api/garminauthentication", new GarminAuthenticationGetResponse() { IsAuthenticated = true });
Expand Down
5 changes: 1 addition & 4 deletions src/Api/Controllers/SettingsController.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using Api.Contract;
using Api.Service;
using Api.Service.Helpers;
using Common;
using Common.Dto;
using Common.Service;
using Microsoft.AspNetCore.Mvc;
Expand All @@ -15,13 +14,11 @@ namespace Api.Controllers;
public class SettingsController : Controller
{
private readonly ISettingsService _settingsService;
private readonly IFileHandling _fileHandler;
private readonly ISettingsUpdaterService _settingsUpdaterService;

public SettingsController(ISettingsService settingsService, IFileHandling fileHandler, ISettingsUpdaterService settingsUpdaterService)
public SettingsController(ISettingsService settingsService, ISettingsUpdaterService settingsUpdaterService)
{
_settingsService = settingsService;
_fileHandler = fileHandler;
_settingsUpdaterService = settingsUpdaterService;
}

Expand Down
Loading

0 comments on commit 9763534

Please sign in to comment.