Files
patrick e96ae78665
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/push/all-checks-complete Pipeline was successful
Use WoofWare.Myriad entirely to generate the REST API (#9)
Co-authored-by: Smaug123 <patrick+github@patrickstevens.co.uk>
Reviewed-on: #9
2024-01-30 00:17:45 +00:00

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)