diff --git a/.github/actions/publish-ui-dist/action.yaml b/.github/actions/publish-ui-dist/action.yaml index 35f1d4233..3c7a18656 100644 --- a/.github/actions/publish-ui-dist/action.yaml +++ b/.github/actions/publish-ui-dist/action.yaml @@ -22,6 +22,10 @@ runs: with: dotnet-version: ${{ inputs.dotnet-version }} + - name: Restore MAUI Workloads + run: dotnet workload restore + shell: pwsh + - name: List MAUI Workloads run: dotnet workload list shell: pwsh diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 5c70a54d3..4318e42c2 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -12,6 +12,7 @@ jobs: matrix: dotnet: ['7.0'] os: [windows-latest] + framework: ['net7.0-windows10.0.19041.0'] runs-on: ${{ matrix.os }} @@ -23,6 +24,17 @@ jobs: with: dotnet-version: ${{ matrix.dotnet }} + - name: List SDKs + run: dotnet --list-sdks + + - name: Restore MAUI Workloads + run: dotnet workload restore + shell: pwsh + + - name: List MAUI Workloads + run: dotnet workload list + shell: pwsh + - name: Clean run: dotnet clean --configuration Debug && dotnet nuget locals all --clear @@ -41,7 +53,7 @@ jobs: needs: build-and-test strategy: matrix: - dotnet: [ '7.0.401' ] + dotnet: [ '7.0.410' ] framework: ['net7.0-windows10.0.19041.0'] os: [ 'win10-x64' ] @@ -80,4 +92,4 @@ jobs: tag: ${{ matrix.tag }} secret_docker_username: ${{ secrets.DOCKER_USERNAME }} secret_docker_password: ${{ secrets.DOCKER_PASSWORD }} - secret_github_package: ${{ secrets.GH_PACKAGE_SECRET}} \ No newline at end of file + secret_github_package: ${{ secrets.GH_PACKAGE_SECRET}} diff --git a/.github/workflows/publish-latest.yaml b/.github/workflows/publish-latest.yaml index 4d0a4b45b..e49ca6298 100644 --- a/.github/workflows/publish-latest.yaml +++ b/.github/workflows/publish-latest.yaml @@ -44,7 +44,7 @@ jobs: runs-on: 'windows-latest' strategy: matrix: - dotnet: [ '7.0.400' ] + dotnet: [ '7.0.410' ] framework: ['net7.0-windows10.0.19041.0'] os: [ 'win10-x64' ] diff --git a/.github/workflows/publish-release.yaml b/.github/workflows/publish-release.yaml index c7b3724a8..7d8d85d8a 100644 --- a/.github/workflows/publish-release.yaml +++ b/.github/workflows/publish-release.yaml @@ -52,7 +52,7 @@ jobs: artifact_name: ${{ steps.win-ui-create-artifact.outputs.artifact_name }} strategy: matrix: - dotnet: [ '7.0.400' ] + dotnet: [ '7.0.410' ] framework: ['net7.0-windows10.0.19041.0'] os: [ 'win10-x64' ] diff --git a/PelotonToGarmin.sln b/PelotonToGarmin.sln index fdb80f071..9812132fa 100644 --- a/PelotonToGarmin.sln +++ b/PelotonToGarmin.sln @@ -18,6 +18,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".solution", ".solution", "{ .gitignore = .gitignore configuration.example.json = configuration.example.json deviceInfo.sample.xml = deviceInfo.sample.xml + global.json = global.json README.md = README.md vNextReleaseNotes.md = vNextReleaseNotes.md EndProjectSection diff --git a/global.json b/global.json new file mode 100644 index 000000000..796f298d4 --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "7.0.410" + } +} \ No newline at end of file diff --git a/mkdocs/docs/configuration/garmin.md b/mkdocs/docs/configuration/garmin.md index f50af2111..d0d8b164c 100644 --- a/mkdocs/docs/configuration/garmin.md +++ b/mkdocs/docs/configuration/garmin.md @@ -19,7 +19,21 @@ This Garmin Settings provide settings related to uploading workouts to Garmin. "Password": "garmin", "TwoStepVerificationEnabled": false, "Upload": false, - "FormatToUpload": "fit" + "FormatToUpload": "fit", + "api": { + "ssoSignInUrl": "https://sso.garmin.com/sso/signin", + "ssoEmbedUrl": "https://sso.garmin.com/sso/embed", + "ssoMfaCodeUrl": "https://sso.garmin.com/sso/verifyMFA/loginEnterMfaCode", + "ssoUserAgent": "GCM-iOS-5.7.2.1", + "oAuth1TokenUrl": "https://connectapi.garmin.com/oauth-service/oauth/preauthorized", + "oAuth1LoginUrlParam": "https://sso.garmin.com/sso/embed&accepts-mfa-tokens=true", + "oAuth2RequestUrl": "https://connectapi.garmin.com/oauth-service/oauth/exchange/user/2.0", + "uploadActivityUrl": "https://connectapi.garmin.com/upload-service/upload", + "uploadActivityUserAgent": "GCM-iOS-5.7.2.1", + "uplaodActivityNkHeader": "NT", + "origin": "https://sso.garmin.com", + "referer": "https://sso.garmin.com/sso/signin" + } } ``` @@ -37,4 +51,5 @@ This Garmin Settings provide settings related to uploading workouts to Garmin. | Password | **yes - if Upload=true** | `null` | `Garmin Tab` | Your Garmin password used to sign in. **Note: Does not support `\` character in password** | | TwoStepVerificationEnabled | no | `false` | `Garmin Tab` | Whether or not your Garmin account is protected by Two Step Verification | | Upload | no | `false` | `Garmin Tab` | `true` indicates you wish downloaded Peloton workouts to be uploaded to Garmin Connect. | -| FormatToUpload | no | `fit` | `Garmin Tab > Advanced` | Valid values are `fit` or `tcx`. Ensure the format you specify here is also enabled in your [Format config](format.md) | \ No newline at end of file +| FormatToUpload | no | `fit` | `Garmin Tab > Advanced` | Valid values are `fit` or `tcx`. Ensure the format you specify here is also enabled in your [Format config](format.md) | +| Api | no | See sample above | `Garmin Tab > Advanced > Garmin Api Settings` | Configures how P2G communicates with the Garmin Api. **Do not modify unless told to do so** | diff --git a/src/Api.Contract/SettingsContracts.cs b/src/Api.Contract/SettingsContracts.cs index 01dc094fe..64f613d07 100644 --- a/src/Api.Contract/SettingsContracts.cs +++ b/src/Api.Contract/SettingsContracts.cs @@ -34,7 +34,8 @@ public SettingsGetResponse(Settings settings) TwoStepVerificationEnabled = settings.Garmin.TwoStepVerificationEnabled, FormatToUpload = settings.Garmin.FormatToUpload, Upload = settings.Garmin.Upload, - IsPasswordSet = !string.IsNullOrEmpty(settings.Garmin.Password) + IsPasswordSet = !string.IsNullOrEmpty(settings.Garmin.Password), + Api = settings.Garmin.Api ?? new GarminApiSettings() }; } @@ -52,6 +53,7 @@ public class SettingsGarminGetResponse public bool TwoStepVerificationEnabled { get; set; } public bool Upload { get; set; } public FileFormat FormatToUpload { get; set; } + public GarminApiSettings Api { get; set; } = new GarminApiSettings(); } public class SettingsGarminPostRequest @@ -61,6 +63,7 @@ public class SettingsGarminPostRequest public bool TwoStepVerificationEnabled { get; set; } public bool Upload { get; set; } public FileFormat FormatToUpload { get; set; } + public GarminApiSettings Api { get; set; } } public class SettingsPelotonGetResponse @@ -118,6 +121,7 @@ public static SettingsGarminPostRequest Map(this SettingsGarminGetResponse respo TwoStepVerificationEnabled = response.TwoStepVerificationEnabled, FormatToUpload = response.FormatToUpload, Upload = response.Upload, + Api = response.Api, }; } @@ -130,6 +134,7 @@ public static GarminSettings Map(this SettingsGarminPostRequest request) TwoStepVerificationEnabled = request.TwoStepVerificationEnabled, FormatToUpload = request.FormatToUpload, Upload = request.Upload, + Api = request.Api, }; } } \ No newline at end of file diff --git a/src/ClientUI/ClientUI.csproj b/src/ClientUI/ClientUI.csproj index a297e2a1d..51bafb057 100644 --- a/src/ClientUI/ClientUI.csproj +++ b/src/ClientUI/ClientUI.csproj @@ -38,6 +38,10 @@ P2G ClientUI + + @@ -60,6 +64,11 @@ + + + + + diff --git a/src/Common/Constants.cs b/src/Common/Constants.cs index cd5e7cffd..278aec717 100644 --- a/src/Common/Constants.cs +++ b/src/Common/Constants.cs @@ -9,6 +9,6 @@ public static class Constants public const string WebUIName = "p2g_webui"; public const string ClientUIName = "p2g_clientui"; - public const string AppVersion = "4.3.0"; + public const string AppVersion = "4.3.1-rc"; } } diff --git a/src/Common/Dto/Settings.cs b/src/Common/Dto/Settings.cs index 0309f6ae7..6d2beffb2 100644 --- a/src/Common/Dto/Settings.cs +++ b/src/Common/Dto/Settings.cs @@ -140,7 +140,30 @@ public class GarminSettings : ICredentials public string Password { get; set; } public bool TwoStepVerificationEnabled { get; set; } public bool Upload { get; set; } - public FileFormat FormatToUpload { get; set; } + public FileFormat FormatToUpload { get; set; } + public GarminApiSettings Api { get; set; } = new GarminApiSettings(); +} + +public class GarminApiSettings +{ + public string SsoSignInUrl { get; set; } = "https://sso.garmin.com/sso/signin"; + public string SsoEmbedUrl { get; set; } = "https://sso.garmin.com/sso/embed"; + public string SsoMfaCodeUrl { get; set; } = "https://sso.garmin.com/sso/verifyMFA/loginEnterMfaCode"; + public string SsoUserAgent { get; set; } = "GCM-iOS-5.7.2.1"; + + public string OAuth1TokenUrl { get; set; } = "https://connectapi.garmin.com/oauth-service/oauth/preauthorized"; + public string OAuth1LoginUrlParam { get; set; } = "https://sso.garmin.com/sso/embed&accepts-mfa-tokens=true"; + + public string OAuth2RequestUrl { get; set; } = "https://connectapi.garmin.com/oauth-service/oauth/exchange/user/2.0"; + + public string UploadActivityUrl { get; set; } = "https://connectapi.garmin.com/upload-service/upload"; + public string UploadActivityUserAgent { get; set; } = "GCM-iOS-5.7.2.1"; + public string UplaodActivityNkHeader { get; set; } = "NT"; + + public string Origin { get; set; } = "https://sso.garmin.com"; + public string Referer { get; set; } = "https://sso.garmin.com/sso/signin"; + + } public enum FileFormat : byte diff --git a/src/Garmin/ApiClient.cs b/src/Garmin/ApiClient.cs index 7f9879d28..c02fafff5 100644 --- a/src/Garmin/ApiClient.cs +++ b/src/Garmin/ApiClient.cs @@ -1,5 +1,6 @@ using Common.Http; using Common.Observe; +using Common.Service; using Flurl.Http; using Garmin.Auth; using Garmin.Dto; @@ -13,51 +14,56 @@ namespace Garmin { public interface IGarminApiClient { - Task InitCookieJarAsync(object queryParams, string userAgent, out CookieJar jar); - Task GetCsrfTokenAsync(object queryParams, CookieJar jar, string userAgent); - Task SendCredentialsAsync(string email, string password, object queryParams, object loginData, string userAgent, CookieJar jar); - Task SendMfaCodeAsync(string userAgent, object queryParams, object mfaData, CookieJar jar); - Task GetOAuth1TokenAsync(ConsumerCredentials credentials, string ticket, string userAgent); - Task GetOAuth2TokenAsync(OAuth1Token oAuth1Token, ConsumerCredentials credentials, string userAgent); + Task InitCookieJarAsync(object queryParams); + Task GetCsrfTokenAsync(object queryParams, CookieJar jar); + Task SendCredentialsAsync(string email, string password, object queryParams, object loginData, CookieJar jar); + Task SendMfaCodeAsync(object queryParams, object mfaData, CookieJar jar); + Task GetOAuth1TokenAsync(ConsumerCredentials credentials, string ticket); + Task GetOAuth2TokenAsync(OAuth1Token oAuth1Token, ConsumerCredentials credentials); Task GetConsumerCredentialsAsync(); - Task UploadActivity(string filePath, string format, GarminApiAuthentication auth, string userAgent); + Task UploadActivity(string filePath, string format, GarminApiAuthentication auth); } public class ApiClient : IGarminApiClient { - private const string SSO_SIGNIN_URL = "https://sso.garmin.com/sso/signin"; - private const string SSO_EMBED_URL = "https://sso.garmin.com/sso/embed"; - - private static string UPLOAD_URL = $"https://connectapi.garmin.com/upload-service/upload"; - - private const string ORIGIN = "https://sso.garmin.com"; - private const string REFERER = "https://sso.garmin.com/sso/signin"; + private ISettingsService _settingsService; private static readonly ILogger _logger = LogContext.ForClass(); + public ApiClient(ISettingsService settingsService) + { + _settingsService = settingsService; + } + public Task GetConsumerCredentialsAsync() { return "https://thegarth.s3.amazonaws.com/oauth_consumer.json" .GetJsonAsync(); } - public Task InitCookieJarAsync(object queryParams, string userAgent, out CookieJar jar) + public async Task InitCookieJarAsync(object queryParams) { - return SSO_EMBED_URL - .WithHeader("User-Agent", userAgent) - .WithHeader("origin", ORIGIN) + var setttings = await _settingsService.GetSettingsAsync(); + + await setttings.Garmin.Api.SsoEmbedUrl + .WithHeader("User-Agent", setttings.Garmin.Api.SsoUserAgent) + .WithHeader("origin", setttings.Garmin.Api.Origin) .SetQueryParams(queryParams) - .WithCookies(out jar) + .WithCookies(out var jar) .GetStringAsync(); + + return jar; } - public async Task SendCredentialsAsync(string email, string password, object queryParams, object loginData, string userAgent, CookieJar jar) + public async Task SendCredentialsAsync(string email, string password, object queryParams, object loginData, CookieJar jar) { + var setttings = await _settingsService.GetSettingsAsync(); + var result = new SendCredentialsResult(); - result.RawResponseBody = await SSO_SIGNIN_URL - .WithHeader("User-Agent", userAgent) - .WithHeader("origin", ORIGIN) - .WithHeader("referer", REFERER) + result.RawResponseBody = await setttings.Garmin.Api.SsoSignInUrl + .WithHeader("User-Agent", setttings.Garmin.Api.SsoUserAgent) + .WithHeader("origin", setttings.Garmin.Api.Origin) + .WithHeader("referer", setttings.Garmin.Api.Referer) .WithHeader("NK", "NT") .SetQueryParams(queryParams) .WithCookies(jar) @@ -69,12 +75,14 @@ public async Task SendCredentialsAsync(string email, stri return result; } - public async Task GetCsrfTokenAsync(object queryParams, CookieJar jar, string userAgent) + public async Task GetCsrfTokenAsync(object queryParams, CookieJar jar) { + var setttings = await _settingsService.GetSettingsAsync(); + var result = new GarminResult(); - result.RawResponseBody = await SSO_SIGNIN_URL - .WithHeader("User-Agent", userAgent) - .WithHeader("origin", ORIGIN) + result.RawResponseBody = await setttings.Garmin.Api.SsoSignInUrl + .WithHeader("User-Agent", setttings.Garmin.Api.SsoUserAgent) + .WithHeader("origin", setttings.Garmin.Api.Origin) .SetQueryParams(queryParams) .WithCookies(jar) .GetAsync() @@ -83,11 +91,13 @@ public async Task GetCsrfTokenAsync(object queryParams, CookieJar return result; } - public Task SendMfaCodeAsync(string userAgent, object queryParams, object mfaData, CookieJar jar) + public async Task SendMfaCodeAsync(object queryParams, object mfaData, CookieJar jar) { - return "https://sso.garmin.com/sso/verifyMFA/loginEnterMfaCode" - .WithHeader("User-Agent", userAgent) - .WithHeader("origin", ORIGIN) + var setttings = await _settingsService.GetSettingsAsync(); + + return await setttings.Garmin.Api.SsoMfaCodeUrl + .WithHeader("User-Agent", setttings.Garmin.Api.SsoUserAgent) + .WithHeader("origin", setttings.Garmin.Api.Origin) .SetQueryParams(queryParams) .WithCookies(jar) .OnRedirect(redir => redir.Request.WithCookies(jar)) @@ -95,37 +105,43 @@ public Task SendMfaCodeAsync(string userAgent, object queryParams, objec .ReceiveString(); } - public Task GetOAuth1TokenAsync(ConsumerCredentials credentials, string ticket, string userAgent) + public async Task GetOAuth1TokenAsync(ConsumerCredentials credentials, string ticket) { + var setttings = await _settingsService.GetSettingsAsync(); + OAuthRequest oauthClient = OAuthRequest.ForRequestToken(credentials.Consumer_Key, credentials.Consumer_Secret); - oauthClient.RequestUrl = $"https://connectapi.garmin.com/oauth-service/oauth/preauthorized?ticket={ticket}&login-url=https://sso.garmin.com/sso/embed&accepts-mfa-tokens=true"; + oauthClient.RequestUrl = $"{setttings.Garmin.Api.OAuth1TokenUrl}?ticket={ticket}&login-url={setttings.Garmin.Api.OAuth1LoginUrlParam}"; - return oauthClient.RequestUrl - .WithHeader("User-Agent", userAgent) + return await oauthClient.RequestUrl + .WithHeader("User-Agent", setttings.Garmin.Api.SsoUserAgent) .WithHeader("Authorization", oauthClient.GetAuthorizationHeader()) .GetStringAsync(); } - public Task GetOAuth2TokenAsync(OAuth1Token oAuth1Token, ConsumerCredentials credentials, string userAgent) + public async Task GetOAuth2TokenAsync(OAuth1Token oAuth1Token, ConsumerCredentials credentials) { - OAuthRequest oauthClient2 = OAuthRequest.ForProtectedResource("POST", credentials.Consumer_Key, credentials.Consumer_Secret, oAuth1Token.Token, oAuth1Token.TokenSecret); - oauthClient2.RequestUrl = "https://connectapi.garmin.com/oauth-service/oauth/exchange/user/2.0"; + var setttings = await _settingsService.GetSettingsAsync(); - return oauthClient2.RequestUrl - .WithHeader("User-Agent", userAgent) + OAuthRequest oauthClient2 = OAuthRequest.ForProtectedResource("POST", credentials.Consumer_Key, credentials.Consumer_Secret, oAuth1Token.Token, oAuth1Token.TokenSecret); + oauthClient2.RequestUrl = setttings.Garmin.Api.OAuth2RequestUrl; + + return await oauthClient2.RequestUrl + .WithHeader("User-Agent", setttings.Garmin.Api.SsoUserAgent) .WithHeader("Authorization", oauthClient2.GetAuthorizationHeader()) .WithHeader("Content-Type", "application/x-www-form-urlencoded") // this header is required, without it you get a 500 .PostUrlEncodedAsync(new object()) // hack: PostAsync() will drop the content-type header, by posting empty object we trick flurl into leaving the header .ReceiveJson(); } - public async Task UploadActivity(string filePath, string format, GarminApiAuthentication auth, string userAgent) + public async Task UploadActivity(string filePath, string format, GarminApiAuthentication auth) { + var settings = await _settingsService.GetSettingsAsync(); + var fileName = Path.GetFileName(filePath); - var response = await $"{UPLOAD_URL}/{format}" + var response = await $"{settings.Garmin.Api.UploadActivityUrl}/{format}" .WithOAuthBearerToken(auth.OAuth2Token.Access_Token) - .WithHeader("NK", "NT") - .WithHeader("origin", ORIGIN) - .WithHeader("User-Agent", userAgent) + .WithHeader("NK", settings.Garmin.Api.UplaodActivityNkHeader) + .WithHeader("origin", settings.Garmin.Api.Origin) + .WithHeader("User-Agent", settings.Garmin.Api.UploadActivityUserAgent) .AllowHttpStatus("2xx,409") .PostMultipartAsync((data) => { diff --git a/src/Garmin/Auth/GarminAuthenticationService.cs b/src/Garmin/Auth/GarminAuthenticationService.cs index bfffd1a08..d5c0956b1 100644 --- a/src/Garmin/Auth/GarminAuthenticationService.cs +++ b/src/Garmin/Auth/GarminAuthenticationService.cs @@ -75,11 +75,8 @@ public async Task GetGarminAuthenticationAsync() { var consumerCredentials = await _apiClient.GetConsumerCredentialsAsync(); var appConfig = await _settingsService.GetAppConfigurationAsync(); - var userAgent = Defaults.DefaultUserAgent; - if (!string.IsNullOrEmpty(appConfig.Developer.UserAgent)) - userAgent = appConfig.Developer.UserAgent; - return await ExchangeOAuth1ForOAuth2Async(oAuth1Token, consumerCredentials, userAgent); + return await ExchangeOAuth1ForOAuth2Async(oAuth1Token, consumerCredentials); } catch (Exception ex) { @@ -98,18 +95,15 @@ public async Task SignInAsync() await SignOutAsync(); CookieJar jar = null; - var userAgent = Defaults.DefaultUserAgent; var appConfig = await _settingsService.GetAppConfigurationAsync(); - if (!string.IsNullOrEmpty(appConfig.Developer.UserAgent)) - userAgent = appConfig.Developer.UserAgent; ///////////////////////////////// // Init Auth Flow //////////////////////////////// try { - await _apiClient.InitCookieJarAsync(CommonQueryParams, userAgent, out jar); + jar = await _apiClient.InitCookieJarAsync(CommonQueryParams); } catch (FlurlHttpException e) { @@ -123,17 +117,17 @@ public async Task SignInAsync() { id = "gauth-widget", embedWidget = "true", - gauthHost = "https://sso.garmin.com/sso/embed", - service = "https://sso.garmin.com/sso/embed", - source = "https://sso.garmin.com/sso/embed", - redirectAfterAccountLoginUrl = "https://sso.garmin.com/sso/embed", - redirectAfterAccountCreationUrl = "https://sso.garmin.com/sso/embed", + gauthHost = settings.Garmin.Api.SsoEmbedUrl, + service = settings.Garmin.Api.SsoEmbedUrl, + source = settings.Garmin.Api.SsoEmbedUrl, + redirectAfterAccountLoginUrl = settings.Garmin.Api.SsoEmbedUrl, + redirectAfterAccountCreationUrl = settings.Garmin.Api.SsoEmbedUrl, }; var csrfToken = string.Empty; try { - var tokenResult = await _apiClient.GetCsrfTokenAsync(csrfRequest, jar, userAgent); + var tokenResult = await _apiClient.GetCsrfTokenAsync(csrfRequest, jar); csrfToken = FindCsrfToken(tokenResult.RawResponseBody, failureStepCode: Code.FailedPriorToCredentialsUsed); } catch (FlurlHttpException e) @@ -154,7 +148,7 @@ public async Task SignInAsync() SendCredentialsResult sendCredentialsResult = null; try { - sendCredentialsResult = await _apiClient.SendCredentialsAsync(settings.Garmin.Email, settings.Garmin.Password, csrfRequest, sendCredentialsRequest, userAgent, jar); + sendCredentialsResult = await _apiClient.SendCredentialsAsync(settings.Garmin.Email, settings.Garmin.Password, csrfRequest, sendCredentialsRequest, jar); } catch (FlurlHttpException e) when (e.StatusCode is (int)HttpStatusCode.Forbidden) { @@ -172,7 +166,7 @@ public async Task SignInAsync() if (sendCredentialsResult.WasRedirected && sendCredentialsResult.RedirectedTo.Contains("https://sso.garmin.com/sso/verifyMFA/loginEnterMfaCode")) { if (!settings.Garmin.TwoStepVerificationEnabled) - throw new GarminAuthenticationError("Detected Garmin TwoFactorAuthentication but TwoFactorAuthenctication is not enabled in P2G settings. Please enable TwoFactorAuthentication in your P2G Garmin settings.") { Code = Code.UnexpectedMfa }; + throw new GarminAuthenticationError("Detected Garmin TwoFactorAuthentication but TwoFactorAuthentication is not enabled in P2G settings. Please enable TwoFactorAuthentication in your P2G Garmin settings.") { Code = Code.UnexpectedMfa }; var mfaCsrfToken = FindCsrfToken(sendCredentialsResult.RawResponseBody, failureStepCode: Code.FailedPriorToMfaUsed); @@ -182,7 +176,6 @@ public async Task SignInAsync() AuthStage = AuthStage.NeedMfaToken, MFACsrfToken = mfaCsrfToken, CookieJarString = jar.ToString(), - UserAgent = userAgent, }; await _garminDb.UpsertPartialGarminAuthenticationAsync(1, partialAuthentication); @@ -190,10 +183,10 @@ public async Task SignInAsync() } var loginResult = sendCredentialsResult?.RawResponseBody; - return await CompleteGarminAuthenticationAsync(loginResult, userAgent); + return await CompleteGarminAuthenticationAsync(loginResult); } - private async Task CompleteGarminAuthenticationAsync(string loginResult, string userAgent) + private async Task CompleteGarminAuthenticationAsync(string loginResult) { // Try to find the full post login ServiceTicket var ticketRegex = new Regex("embed\\?ticket=(?[^\"]+)\""); @@ -211,13 +204,13 @@ private async Task CompleteGarminAuthenticationAsync(st // Get OAuth1 Tokens /////////////////////////////////////////// var consumerCredentials = await _apiClient.GetConsumerCredentialsAsync(); - var oAuth1Token = await GetOAuth1Async(ticket, consumerCredentials, userAgent); + var oAuth1Token = await GetOAuth1Async(ticket, consumerCredentials); await _garminDb.UpsertGarminOAuth1TokenAsync(1, oAuth1Token); //////////////////////////////////////////// // Exchange for OAuth2 Token /////////////////////////////////////////// - var result = await ExchangeOAuth1ForOAuth2Async(oAuth1Token, consumerCredentials, userAgent); + var result = await ExchangeOAuth1ForOAuth2Async(oAuth1Token, consumerCredentials); // Clear partial data await _garminDb.UpsertPartialGarminAuthenticationAsync(1, null); @@ -225,12 +218,12 @@ private async Task CompleteGarminAuthenticationAsync(st return result; } - private async Task ExchangeOAuth1ForOAuth2Async(OAuth1Token oAuth1Token, ConsumerCredentials consumerCredentials, string userAgent) + private async Task ExchangeOAuth1ForOAuth2Async(OAuth1Token oAuth1Token, ConsumerCredentials consumerCredentials) { OAuth2Token oAuth2Token = null; try { - oAuth2Token = await _apiClient.GetOAuth2TokenAsync(oAuth1Token, consumerCredentials, userAgent); + oAuth2Token = await _apiClient.GetOAuth2TokenAsync(oAuth1Token, consumerCredentials); oAuth2Token.ExpiresAt = DateTime.Now.AddSeconds(oAuth2Token.Expires_In); await _garminDb.UpsertGarminOAuth2TokenAsync(1, oAuth2Token); @@ -260,9 +253,6 @@ public async Task CompleteMFAAuthAsync(string mfaCode) if (partialAuth.AuthStage != AuthStage.NeedMfaToken) throw new ArgumentException($"We're in the wrong GarminAuthStage, expected NeedMfaToken but found {partialAuth.AuthStage}"); - if (string.IsNullOrEmpty(partialAuth.UserAgent)) - partialAuth.UserAgent = Defaults.DefaultUserAgent; - var mfaData = new List>() { new KeyValuePair("embed", "true"), @@ -278,8 +268,8 @@ public async Task CompleteMFAAuthAsync(string mfaCode) { SendMFAResult mfaResponse = new(); var jar = CookieJar.LoadFromString(partialAuth.CookieJarString); - mfaResponse.RawResponseBody = await _apiClient.SendMfaCodeAsync(partialAuth.UserAgent, CommonQueryParams, mfaData, jar); - return await CompleteGarminAuthenticationAsync(mfaResponse.RawResponseBody, partialAuth.UserAgent); + mfaResponse.RawResponseBody = await _apiClient.SendMfaCodeAsync(CommonQueryParams, mfaData, jar); + return await CompleteGarminAuthenticationAsync(mfaResponse.RawResponseBody); } catch (FlurlHttpException e) when (e.StatusCode is (int)HttpStatusCode.Forbidden) { @@ -314,12 +304,12 @@ private string FindCsrfToken(string rawResponseBody, Code failureStepCode) } } - private async Task GetOAuth1Async(string ticket, ConsumerCredentials credentials, string userAgent) + private async Task GetOAuth1Async(string ticket, ConsumerCredentials credentials) { string oauth1Response = null; try { - oauth1Response = await _apiClient.GetOAuth1TokenAsync(credentials, ticket, userAgent); + oauth1Response = await _apiClient.GetOAuth1TokenAsync(credentials, ticket); } catch (Exception e) { throw new GarminAuthenticationError("Auth appeared successful but failed to get the OAuth1 token.", e) { Code = Code.AuthAppearedSuccessful }; diff --git a/src/Garmin/GarminUploader.cs b/src/Garmin/GarminUploader.cs index fd30b72ef..2dfe7009d 100644 --- a/src/Garmin/GarminUploader.cs +++ b/src/Garmin/GarminUploader.cs @@ -89,17 +89,12 @@ private async Task UploadAsync(string[] files, Settings settings) if (auth.AuthStage == Dto.AuthStage.None) throw new GarminUploadException("Expected user to be authenticated with Garmin at this point, but they are not. AuthStage: None.", -3); - var userAgent = Defaults.DefaultUserAgent; - var appConfig = await _settingsService.GetAppConfigurationAsync(); - if (!string.IsNullOrEmpty(appConfig.Developer.UserAgent)) - userAgent = appConfig.Developer.UserAgent; - foreach (var file in files) { try { _logger.Information("Uploading to Garmin: {@file}", file); - await _api.UploadActivity(file, settings.Format.Fit ? ".fit" : ".tcx", auth, userAgent); + await _api.UploadActivity(file, settings.Format.Fit ? ".fit" : ".tcx", auth); await RateLimit(); } catch (Exception e) { diff --git a/src/SharedUI/Shared/GarminSettingsForm.razor b/src/SharedUI/Shared/GarminSettingsForm.razor index f01176448..3c864f906 100644 --- a/src/SharedUI/Shared/GarminSettingsForm.razor +++ b/src/SharedUI/Shared/GarminSettingsForm.razor @@ -5,9 +5,12 @@
-
+
+ +
- + +
Auth @@ -39,37 +42,113 @@ -
- +
+ +
+
Advanced -
-
+ + + Most users should not need to modify these settings. Please be sure you've read the documentation before changing. + +
+ + + Upload Format Settings + + ? + + + -
-
+ Data="@formatTypes" + Nullable="false" + NullDataText="Loading info..." + @bind-Value="garminSettings.FormatToUpload" /> +
+ +
+ + + Garmin Api Settings + + ? + + + + +
+
+ +
+
+ +
+
+ +
+
+ +
+

+
+ +
+
+ +
+
+ +
+

+
+ +
+
+ +
+
+ +
+

+
+ +
+
+ +
+
+
+
-
- -
- Save +
+
+
+
+ Save
+ -@code { +@code { private static ICollection formatTypes = Enum.GetValues(typeof(FileFormat)).Cast().ToList(); private SettingsGarminGetResponse garminSettings; + private string configDocumentationBase; + private string configDocumentation; public GarminSettingsForm() { @@ -89,6 +168,10 @@ var settings = await _apiClient.SettingsGetAsync(); garminSettings = settings.Garmin; + + var systemInfo = await _apiClient.SystemInfoGetAsync(new SystemInfoGetRequest() { CheckForUpdate = settings.App.CheckForUpdates }); + configDocumentationBase = systemInfo.Documentation; + configDocumentation = systemInfo.Documentation + "/configuration/garmin"; } protected void ClearGarminPassword() @@ -119,5 +202,34 @@ _toaster.AddError($"Failed to save Garmin Settings - {e.Message} - See logs for details."); Log.Error("UI - Failed to save Garmin settings.", e); } - } + } + + protected async Task RestoreDefaultApiSettings() + { + using var tracing = Tracing.ClientTrace($"{nameof(GarminSettingsForm)}.{nameof(RestoreDefaultApiSettings)}", kind: ActivityKind.Client); + + _toaster.Clear(); + + try + { + garminSettings.Api = new GarminApiSettings(); + + garminSettings = await _apiClient.SettingsGarminPostAsync(garminSettings.Map()); + _toaster.AddInformation("Garmin Settings Saved!"); + } + catch (FlurlHttpException e) when (e.StatusCode is StatusCodes.Status400BadRequest) + { + var error = await e.GetResponseJsonAsync(); + _toaster.AddError(error.Message); + } + catch (Exception e) + { + _toaster.AddError($"Failed to save Garmin Settings - {e.Message} - See logs for details."); + Log.Error("UI - Failed to save Garmin settings.", e); + } + + } + + private string UploadFormatDocumentation => $"Garmin supports uploading either FIT files or TCX files. FIT is the more modern format and in most cases will result in the most data and features syncing to Garmin.

You must upload the FIT format in order to get extra fields calculated by Garmin such as TSS/VO2/TE.

Documentation

(click the ? to pin this window)"; + private string GarminApiDocumentation => $"These settings control how P2G communicates with the Garmin Api.

Please do not change these value unless you've been given explicit guidance to do so.

Documentation

(click the ? to pin this window)"; } diff --git a/src/SharedUI/SharedUI.csproj b/src/SharedUI/SharedUI.csproj index 8c9464f87..32d557090 100644 --- a/src/SharedUI/SharedUI.csproj +++ b/src/SharedUI/SharedUI.csproj @@ -11,7 +11,7 @@
- + diff --git a/src/WebUI/WebUI.csproj b/src/WebUI/WebUI.csproj index 77e69bb1c..5886a2fa4 100644 --- a/src/WebUI/WebUI.csproj +++ b/src/WebUI/WebUI.csproj @@ -26,6 +26,7 @@ + diff --git a/vNextReleaseNotes.md b/vNextReleaseNotes.md index dad5dc10f..970f4fde8 100644 --- a/vNextReleaseNotes.md +++ b/vNextReleaseNotes.md @@ -1,30 +1,28 @@ [![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/philosowaffle) Buy Me A Coffee donate button --- -## Features +## Fixes -- [#585] - - Garmin Authentication now saves and refreshes tokens. Users using MFA will now only need provide their MFA code once. - - For those running via Docker, automatic syncing now works for MFA users after you have entered your code the first time. +- [#683] Initial fix for Garmin Upload error. Additionally introduces new settings for configuring Garmin Api. -## Misc - -- [#587] Dependency updates + switched to the official Garmin FIT SDK nuget package +> [!CAUTION] +> **Windows App Users** +> When editing settings, you may encounter an issue where your mouse stops working within the P2G app. Keyboard navigation continues to work. If this happens, quit P2G and restart. I will be investigating how to get a proper fix for this on a future release. ## Docker Tags - Console - `console-stable` - `console-latest` - - `console-v4.3.0` + - `console-v4.3.1` - `console-v4` - Api - `api-stable` - `api-latest` - - `api-v4.3.0` + - `api-v4.3.1` - `api-v4` - WebUI - `webui-stable` - `webui-latest` - - `webui-v4.3.0` + - `webui-v4.3.1` - `webui-v4`