112 lines
4.0 KiB
Forth
112 lines
4.0 KiB
Forth
namespace PureGym
|
|
|
|
open System
|
|
open System.Collections.Generic
|
|
open System.Net.Http
|
|
open System.Text.Json
|
|
open System.Text.Json.Serialization
|
|
open System.Threading
|
|
open System.Threading.Tasks
|
|
|
|
// System.Text.Json does not support internal F# records as of .NET 8, presumably because it can't find the constructor.
|
|
// So we end up with this gruesome soup of C#.
|
|
// TODO(net8): make this internal
|
|
/// An internal type. Don't use it.
|
|
type AuthResponseRaw [<JsonConstructor>] (access_token : string, expires_in : int, token_type : string, scope : string)
|
|
=
|
|
/// Don't use this internal type.
|
|
member _.access_token = access_token
|
|
/// Don't use this internal type.
|
|
member _.expires_in = expires_in
|
|
/// Don't use this internal type.
|
|
member _.token_type = token_type
|
|
/// Don't use this internal type.
|
|
member _.scope = scope
|
|
|
|
/// A token which can be used to authenticate with the PureGym API.
|
|
type AuthToken =
|
|
{
|
|
/// A string which can be passed as the "Bearer" in an Authorization header
|
|
AccessToken : string
|
|
/// Time that this token expires, if known
|
|
ExpiryTime : DateTime option
|
|
}
|
|
|
|
static member internal Parse (response : AuthResponseRaw) : AuthToken =
|
|
if response.scope <> "pgcapi" then
|
|
failwithf $"unexpected scope: %s{response.scope}"
|
|
|
|
if response.token_type <> "Bearer" then
|
|
failwithf $"unexpected type: %s{response.token_type}"
|
|
|
|
{
|
|
AccessToken = response.access_token
|
|
ExpiryTime = Some (DateTime.Now + TimeSpan.FromSeconds response.expires_in)
|
|
}
|
|
|
|
/// Authentication credentials as known to a human user
|
|
type UsernamePin =
|
|
{
|
|
/// The user's email address
|
|
Username : string
|
|
/// Eight-digit PIN
|
|
Pin : string
|
|
}
|
|
|
|
/// Any way to authenticate with the PureGym API.
|
|
type Auth =
|
|
/// Authenticate with credentials which are known to the user
|
|
| User of UsernamePin
|
|
/// An AuthToken (that is, a bearer token)
|
|
| Token of AuthToken
|
|
|
|
/// Methods for constructing PureGym authentication tokens.
|
|
[<RequireQualifiedAccess>]
|
|
module AuthToken =
|
|
/// Construct an AuthToken given that you already have a bearer token.
|
|
let ofBearerToken (token : string) : AuthToken =
|
|
{
|
|
AccessToken = token
|
|
ExpiryTime = None
|
|
}
|
|
|
|
let private options = JsonSerializerOptions (IncludeFields = true)
|
|
|
|
/// Get an AuthToken for the given user email address with the given eight-digit PureGym PIN.
|
|
let get (creds : UsernamePin) (ct : CancellationToken) : Task<AuthToken> =
|
|
async {
|
|
let! ct = Async.CancellationToken
|
|
use client = new HttpClient ()
|
|
client.BaseAddress <- Uri "https://auth.puregym.com"
|
|
client.DefaultRequestHeaders.Add ("User-Agent", "PureGym/1523 CFNetwork/1312 Darwin/21.0.0")
|
|
|
|
let request =
|
|
[
|
|
"grant_type", "password"
|
|
"username", creds.Username
|
|
"password", creds.Pin
|
|
"scope", "pgcapi"
|
|
"client_id", "ro.client"
|
|
]
|
|
|> List.map KeyValuePair
|
|
|
|
use content = new FormUrlEncodedContent (request)
|
|
|
|
let! response =
|
|
Async.AwaitTask (
|
|
client.PostAsync (Uri "https://auth.puregym.com/connect/token", content, cancellationToken = ct)
|
|
)
|
|
|
|
if response.IsSuccessStatusCode then
|
|
let! content = Async.AwaitTask (response.Content.ReadAsStreamAsync ct)
|
|
|
|
let! response =
|
|
Async.AwaitTask (JsonSerializer.DeserializeAsync<AuthResponseRaw>(content, options, ct).AsTask ())
|
|
|
|
return AuthToken.Parse response
|
|
else
|
|
let! content = Async.AwaitTask (response.Content.ReadAsStringAsync ct)
|
|
return failwithf $"bad status code: %+A{response.StatusCode}\n%s{content}"
|
|
}
|
|
|> fun a -> Async.StartAsTask (a, cancellationToken = ct)
|