namespace PureGym open System open System.Collections.Generic open System.Net.Http open System.Text.Json open System.Text.Json.Serialization 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 [] (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. [] 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) : Task = task { 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 = client.PostAsync (Uri "https://auth.puregym.com/connect/token", content) if response.IsSuccessStatusCode then let! content = response.Content.ReadAsStreamAsync () let! response = JsonSerializer.DeserializeAsync (content, options) // let! response = JsonSerializer.DeserializeAsync (content, options) return AuthToken.Parse response else let! content = response.Content.ReadAsStringAsync () return failwithf $"bad status code: %+A{response.StatusCode}\n%s{content}" }