Begin integration with Elixir code
This commit is contained in:
parent
13e964cb22
commit
b55ff1438c
20 changed files with 206 additions and 217 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -36,3 +36,7 @@ npm-debug.log
|
|||
/assets/node_modules/
|
||||
|
||||
.env
|
||||
|
||||
src/geo_therminator.gleam
|
||||
|
||||
.elixir_ls
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
defmodule GeoTherminator.PumpAPI.Auth.API do
|
||||
alias GeoTherminator.PumpAPI.HTTP
|
||||
alias GeoTherminator.PumpAPI.Auth
|
||||
|
||||
require Logger
|
||||
|
||||
@spec auth(String.t(), String.t()) :: {:ok, Auth.User.t()} | :error
|
||||
def auth(username, password) do
|
||||
url = Application.get_env(:geo_therminator, :api_auth_url)
|
||||
|
||||
req =
|
||||
HTTP.req(:post, url, [], %{
|
||||
username: username,
|
||||
password: password
|
||||
})
|
||||
|
||||
with {:ok, response} <- Finch.request(req, HTTP),
|
||||
200 <- response.status,
|
||||
{:ok, json} <- Jason.decode(response.body),
|
||||
{:ok, valid_to, _} = DateTime.from_iso8601(json["tokenValidToUtc"]) do
|
||||
token = %Auth.Token{
|
||||
token: json["token"],
|
||||
token_valid_to: valid_to
|
||||
}
|
||||
|
||||
{:ok,
|
||||
%Auth.User{
|
||||
user_name: json["userName"],
|
||||
email: json["email"],
|
||||
first_name: json["firstName"],
|
||||
last_name: json["lastName"],
|
||||
culture: json["culture"],
|
||||
eula_accepted: json["eulaAccepted"],
|
||||
is_authenticated: json["isAuthenticated"],
|
||||
time_zone: json["timeZone"],
|
||||
token: token
|
||||
}}
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
@spec installations_info(Auth.User.t()) :: [Auth.InstallationInfo.t()]
|
||||
def installations_info(user) do
|
||||
url = Application.get_env(:geo_therminator, :api_installations_url)
|
||||
|
||||
req = HTTP.authed_req(user, :get, url)
|
||||
{:ok, response} = Finch.request(req, HTTP)
|
||||
|
||||
json = Jason.decode!(response.body)
|
||||
|
||||
Enum.map(json["items"], fn item ->
|
||||
%Auth.InstallationInfo{
|
||||
id: item["id"]
|
||||
}
|
||||
end)
|
||||
end
|
||||
end
|
|
@ -1,8 +1,7 @@
|
|||
defmodule GeoTherminator.PumpAPI.Auth.InstallationInfo do
|
||||
import GeoTherminator.TypedStruct
|
||||
require Record
|
||||
|
||||
deftypedstruct(%{
|
||||
id: integer()
|
||||
# ...
|
||||
})
|
||||
Record.defrecord(:record, :installation_info, [:id])
|
||||
|
||||
@type t :: record(:record, id: non_neg_integer())
|
||||
end
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
defmodule GeoTherminator.PumpAPI.Auth.Server do
|
||||
require Logger
|
||||
require GeoTherminator.PumpAPI.Auth.User
|
||||
require GeoTherminator.PumpAPI.Auth.InstallationInfo
|
||||
require GeoTherminator.PumpAPI.Auth.Tokens
|
||||
|
||||
use GenServer
|
||||
import GeoTherminator.TypedStruct
|
||||
alias GeoTherminator.PumpAPI.Auth
|
||||
require Logger
|
||||
|
||||
@token_check_timer 10_000
|
||||
@token_check_diff 5 * 60
|
||||
|
@ -32,24 +36,9 @@ defmodule GeoTherminator.PumpAPI.Auth.Server do
|
|||
|
||||
@impl true
|
||||
def init(%Options{} = opts) do
|
||||
case Auth.API.auth(opts.username, opts.password) do
|
||||
{:ok, user} ->
|
||||
installations = Auth.API.installations_info(user)
|
||||
|
||||
schedule_token_check()
|
||||
|
||||
state = %State{
|
||||
authed_user: user,
|
||||
installations: installations,
|
||||
installations_fetched: true,
|
||||
username: opts.username,
|
||||
password: opts.password
|
||||
}
|
||||
|
||||
{:ok, state}
|
||||
|
||||
:error ->
|
||||
{:stop, :error}
|
||||
case init_state(opts.username, opts.password) do
|
||||
{:ok, state} -> {:ok, state}
|
||||
:error -> {:stop, :error}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -65,7 +54,8 @@ defmodule GeoTherminator.PumpAPI.Auth.Server do
|
|||
end
|
||||
|
||||
def handle_call({:get_installation, id}, _from, state) do
|
||||
{:reply, Enum.find(state.installations, &(&1.id == id)), state}
|
||||
{:reply, Enum.find(state.installations, &(Auth.InstallationInfo.record(&1, :id) == id)),
|
||||
state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
@ -73,17 +63,23 @@ defmodule GeoTherminator.PumpAPI.Auth.Server do
|
|||
|
||||
def handle_info(:token_check, state) do
|
||||
now = DateTime.utc_now()
|
||||
diff = DateTime.diff(state.authed_user.token.token_valid_to, now)
|
||||
|
||||
diff =
|
||||
DateTime.diff(
|
||||
state.authed_user
|
||||
|> Auth.User.record(:tokens)
|
||||
|> Auth.Tokens.record(:access_token_expiry),
|
||||
now
|
||||
)
|
||||
|
||||
schedule_token_check()
|
||||
|
||||
if diff < @token_check_diff do
|
||||
Logger.debug("Renewing auth token since #{diff} < #{@token_check_diff}")
|
||||
|
||||
with {:ok, user} <- Auth.API.auth(state.username, state.password) do
|
||||
{:noreply, %State{state | authed_user: user}}
|
||||
else
|
||||
_ -> {:noreply, state}
|
||||
case init_state(state.username, state.password) do
|
||||
{:ok, new_state} -> {:noreply, new_state}
|
||||
:error -> {:noreply, state}
|
||||
end
|
||||
else
|
||||
{:noreply, state}
|
||||
|
@ -108,4 +104,24 @@ defmodule GeoTherminator.PumpAPI.Auth.Server do
|
|||
defp schedule_token_check() do
|
||||
Process.send_after(self(), :token_check, @token_check_timer)
|
||||
end
|
||||
|
||||
@spec init_state(String.t(), String.t()) :: {:ok, State.t()} | {:stop, :error}
|
||||
defp init_state(username, password) do
|
||||
with {:ok, user} <- :pump_api@auth@api.auth(username, password),
|
||||
{:ok, installations} <- :pump_api@auth@api.installation_info(user) do
|
||||
schedule_token_check()
|
||||
|
||||
state = %State{
|
||||
authed_user: user,
|
||||
installations: installations,
|
||||
installations_fetched: true,
|
||||
username: username,
|
||||
password: password
|
||||
}
|
||||
|
||||
{:ok, state}
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
defmodule GeoTherminator.PumpAPI.Auth.Token do
|
||||
import GeoTherminator.TypedStruct
|
||||
|
||||
deftypedstruct(%{
|
||||
token: String.t(),
|
||||
token_valid_to: DateTime.t()
|
||||
})
|
||||
end
|
18
lib/geo_therminator/pump_api/auth/tokens.ex
Normal file
18
lib/geo_therminator/pump_api/auth/tokens.ex
Normal file
|
@ -0,0 +1,18 @@
|
|||
defmodule GeoTherminator.PumpAPI.Auth.Tokens do
|
||||
require Record
|
||||
|
||||
Record.defrecord(:record, :tokens, [
|
||||
:access_token,
|
||||
:access_token_expiry,
|
||||
:refresh_token,
|
||||
:refresh_token_expiry
|
||||
])
|
||||
|
||||
@type t ::
|
||||
record(:record,
|
||||
access_token: String.t(),
|
||||
access_token_expiry: DateTime.t(),
|
||||
refresh_token: String.t(),
|
||||
refresh_token_expiry: DateTime.t()
|
||||
)
|
||||
end
|
|
@ -1,15 +1,7 @@
|
|||
defmodule GeoTherminator.PumpAPI.Auth.User do
|
||||
import GeoTherminator.TypedStruct
|
||||
require Record
|
||||
|
||||
deftypedstruct(%{
|
||||
user_name: String.t(),
|
||||
email: String.t(),
|
||||
first_name: String.t(),
|
||||
last_name: String.t(),
|
||||
culture: String.t(),
|
||||
eula_accepted: boolean(),
|
||||
is_authenticated: boolean(),
|
||||
time_zone: String.t(),
|
||||
token: GeoTherminator.PumpAPI.Auth.Token.t()
|
||||
})
|
||||
Record.defrecord(:record, :user, [:tokens])
|
||||
|
||||
@type t :: record(:record, tokens: GeoTherminator.PumpAPI.Auth.Tokens.t())
|
||||
end
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
defmodule GeoTherminator.PumpAPI.Device.API do
|
||||
require GeoTherminator.PumpAPI.Auth.InstallationInfo
|
||||
|
||||
alias GeoTherminator.PumpAPI.HTTP
|
||||
alias GeoTherminator.PumpAPI.Device
|
||||
alias GeoTherminator.PumpAPI.Auth
|
||||
|
||||
@spec device_info(Auth.User.t(), Device.InstallationInfo.t()) :: Device.t()
|
||||
@spec device_info(Auth.User.t(), Auth.InstallationInfo.t()) :: Device.t()
|
||||
def device_info(user, installation) do
|
||||
url =
|
||||
Application.get_env(:geo_therminator, :api_device_url)
|
||||
|> String.replace("{id}", to_string(installation.id))
|
||||
|> String.replace("{id}", to_string(Auth.InstallationInfo.record(installation, :id)))
|
||||
|
||||
req = HTTP.authed_req(user, :get, url)
|
||||
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
defmodule GeoTherminator.PumpAPI.Device.PubSub do
|
||||
require GeoTherminator.PumpAPI.Auth.InstallationInfo
|
||||
|
||||
alias Phoenix.PubSub
|
||||
alias GeoTherminator.PumpAPI.Auth.InstallationInfo
|
||||
|
||||
@installation_topic "installation:"
|
||||
|
||||
@spec subscribe_installation(GeoTherminator.PumpAPI.Auth.InstallationInfo.t()) :: :ok
|
||||
@spec subscribe_installation(InstallationInfo.t()) :: :ok
|
||||
def subscribe_installation(installation) do
|
||||
:ok = PubSub.subscribe(__MODULE__, @installation_topic <> to_string(installation.id))
|
||||
:ok =
|
||||
PubSub.subscribe(
|
||||
__MODULE__,
|
||||
@installation_topic <> to_string(InstallationInfo.record(installation, :id))
|
||||
)
|
||||
end
|
||||
|
||||
@spec broadcast_device(GeoTherminator.PumpAPI.Device.t()) :: :ok
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
defmodule GeoTherminator.PumpAPI.HTTP do
|
||||
require GeoTherminator.PumpAPI.Auth.User
|
||||
require GeoTherminator.PumpAPI.Auth.Tokens
|
||||
|
||||
alias GeoTherminator.PumpAPI.Auth.User
|
||||
alias GeoTherminator.PumpAPI.Auth.Tokens
|
||||
|
||||
@spec authed_req(
|
||||
GeoTherminator.PumpAPI.Auth.User.t(),
|
||||
User.t(),
|
||||
Finch.Request.method(),
|
||||
Finch.Request.url(),
|
||||
Finch.Request.headers(),
|
||||
|
@ -11,7 +17,11 @@ defmodule GeoTherminator.PumpAPI.HTTP do
|
|||
def authed_req(user, method, url, headers \\ [], body \\ nil, opts \\ []) do
|
||||
headers =
|
||||
headers
|
||||
|> List.keystore("authorization", 0, {"authorization", "Bearer #{user.token.token}"})
|
||||
|> List.keystore(
|
||||
"authorization",
|
||||
0,
|
||||
{"authorization", "Bearer #{User.record(user, :tokens) |> Tokens.record(:access_token)}"}
|
||||
)
|
||||
|
||||
req(method, url, headers, body, opts)
|
||||
end
|
||||
|
|
|
@ -8,7 +8,7 @@ defmodule GeoTherminatorWeb.MainLive.Pump do
|
|||
def mount(%{"id" => str_id}, _session, socket) do
|
||||
socket =
|
||||
with {id, ""} <- Integer.parse(str_id),
|
||||
%Auth.InstallationInfo{} = info <- Auth.Server.get_installation(Auth.Server, id),
|
||||
info when info != nil <- Auth.Server.get_installation(Auth.Server, id),
|
||||
:ok <- Device.PubSub.subscribe_installation(info),
|
||||
{:ok, pid} <- Device.get_device_process(Auth.Server, info),
|
||||
%Device{} = device <- Device.Server.get_device(pid),
|
||||
|
|
7
mix.exs
7
mix.exs
|
@ -14,7 +14,12 @@ defmodule GeoTherminator.MixProject do
|
|||
],
|
||||
erlc_include_path: "build/dev/erlang/#{@app}/include",
|
||||
archives: [mix_gleam: "~> 0.6.1"],
|
||||
compilers: [:gleam, :gettext] ++ Mix.compilers(),
|
||||
compilers:
|
||||
if Mix.env() != :test do
|
||||
[:gleam]
|
||||
else
|
||||
[]
|
||||
end ++ Mix.compilers(),
|
||||
start_permanent: Mix.env() == :prod,
|
||||
aliases: aliases(),
|
||||
deps: deps(),
|
||||
|
|
|
@ -15,10 +15,10 @@ import gleam/string
|
|||
import gleam/list
|
||||
import gleam/result
|
||||
import gleam/int
|
||||
import gleam/io
|
||||
import azure/utils
|
||||
import helpers/crypto
|
||||
import helpers/uri as uri_helpers
|
||||
import helpers/parsing
|
||||
|
||||
const challenge_length = 43
|
||||
|
||||
|
@ -33,7 +33,12 @@ const b2c_auth_url = "https://thermialogin.b2clogin.com/thermialogin.onmicrosoft
|
|||
const b2c_authorize_prefix = "var SETTINGS = "
|
||||
|
||||
pub type Tokens {
|
||||
Tokens(access_token: String, refresh_token: String)
|
||||
Tokens(
|
||||
access_token: String,
|
||||
access_token_expires_in: Int,
|
||||
refresh_token: String,
|
||||
refresh_token_expires_in: Int,
|
||||
)
|
||||
}
|
||||
|
||||
pub type B2CError {
|
||||
|
@ -91,14 +96,14 @@ fn authorize(code_challenge: String) -> Result(AuthInfo, B2CError) {
|
|||
body_split,
|
||||
fn(line) { string.starts_with(line, b2c_authorize_prefix) },
|
||||
)
|
||||
|> map_error("Authorize settings string not found.")
|
||||
|> b2c_error("Authorize settings string not found.")
|
||||
|
||||
let prefix_len = string.length(b2c_authorize_prefix)
|
||||
let settings_json =
|
||||
string.slice(settings, prefix_len, string.length(settings) - prefix_len - 2)
|
||||
try data =
|
||||
json.decode(settings_json, using: dynamic.dynamic)
|
||||
|> map_error(
|
||||
|> b2c_error(
|
||||
"Authorize settings JSON parsing error: " <> string.inspect(settings_json),
|
||||
)
|
||||
|
||||
|
@ -109,7 +114,7 @@ fn authorize(code_challenge: String) -> Result(AuthInfo, B2CError) {
|
|||
state_code_block
|
||||
|> string.split("=")
|
||||
|> list.at(1)
|
||||
|> map_error("State code parsing error: " <> state_code_block)
|
||||
|> b2c_error("State code parsing error: " <> state_code_block)
|
||||
|
||||
Ok(AuthInfo(
|
||||
state_code: state_code,
|
||||
|
@ -163,7 +168,7 @@ fn confirm(
|
|||
|
||||
try csrf_cookie =
|
||||
list.key_find(auth_info.cookies, csrf_cookie_key)
|
||||
|> map_error("CSRF cookie not found in auth info.")
|
||||
|> b2c_error("CSRF cookie not found in auth info.")
|
||||
let cookies = [#(csrf_cookie_key, csrf_cookie), ..self_asserted.cookies]
|
||||
|
||||
let req = build_req(confirm_url(), http.Get)
|
||||
|
@ -182,7 +187,7 @@ fn confirm(
|
|||
try resp =
|
||||
req
|
||||
|> hackney.send()
|
||||
|> map_error("Confirm HTTP request failed.")
|
||||
|> b2c_error("Confirm HTTP request failed.")
|
||||
|
||||
try resp = case resp.status {
|
||||
302 -> Ok(resp)
|
||||
|
@ -191,13 +196,13 @@ fn confirm(
|
|||
|
||||
try location =
|
||||
response.get_header(resp, "location")
|
||||
|> map_error("Location not found for confirm response.")
|
||||
|> b2c_error("Location not found for confirm response.")
|
||||
|
||||
try code =
|
||||
location
|
||||
|> string.split("code=")
|
||||
|> list.at(1)
|
||||
|> map_error("Confirmation code not found.")
|
||||
|> b2c_error("Confirmation code not found.")
|
||||
|
||||
Ok(Confirmed(code: code))
|
||||
}
|
||||
|
@ -222,12 +227,20 @@ fn get_tokens(
|
|||
try resp = run_req(req)
|
||||
try data =
|
||||
json.decode(resp.body, using: dynamic.dynamic)
|
||||
|> map_error("Get tokens JSON parsing error: " <> string.inspect(resp.body))
|
||||
|> b2c_error("Get tokens JSON parsing error: " <> string.inspect(resp.body))
|
||||
|
||||
try token = data_get(data, "access_token", dynamic.string)
|
||||
try expires_in = data_get(data, "expires_in", dynamic.int)
|
||||
try refresh_token = data_get(data, "refresh_token", dynamic.string)
|
||||
try refresh_token_expires_in =
|
||||
data_get(data, "refresh_token_expires_in", dynamic.int)
|
||||
|
||||
Ok(Tokens(access_token: token, refresh_token: refresh_token))
|
||||
Ok(Tokens(
|
||||
access_token: token,
|
||||
access_token_expires_in: expires_in,
|
||||
refresh_token: refresh_token,
|
||||
refresh_token_expires_in: refresh_token_expires_in,
|
||||
))
|
||||
}
|
||||
|
||||
fn hash_challenge(challenge: String) -> String {
|
||||
|
@ -274,7 +287,7 @@ fn run_req(
|
|||
try resp =
|
||||
req
|
||||
|> hackney.send()
|
||||
|> map_error("HTTP request failed.")
|
||||
|> b2c_error("HTTP request failed.")
|
||||
|
||||
case resp.status {
|
||||
200 -> Ok(resp)
|
||||
|
@ -307,15 +320,9 @@ fn data_get(
|
|||
key: String,
|
||||
data_type: dynamic.Decoder(a),
|
||||
) -> Result(a, B2CError) {
|
||||
data
|
||||
|> dynamic.field(key, data_type)
|
||||
|> map_error(
|
||||
"Field " <> key <> " of correct type not found in data: " <> string.inspect(
|
||||
data,
|
||||
),
|
||||
)
|
||||
parsing.data_get(data, key, data_type, B2CError)
|
||||
}
|
||||
|
||||
fn map_error(r: Result(a, b), error_msg: String) -> Result(a, B2CError) {
|
||||
result.map_error(r, fn(_) { B2CError(msg: error_msg) })
|
||||
fn b2c_error(r: Result(a, b), msg: String) -> Result(a, B2CError) {
|
||||
result.replace_error(r, B2CError(msg))
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import gleam/dynamic.{Dynamic}
|
||||
import gleam/map.{Map}
|
||||
import gleam/erlang/atom.{Atom}
|
||||
|
||||
pub type DateTime =
|
||||
Map(Atom, Dynamic)
|
||||
pub external type DateTime
|
||||
|
||||
pub external fn from_iso8601(String) -> Result(#(DateTime, Int), Atom) =
|
||||
"Elixir.DateTime" "from_iso8601"
|
||||
|
||||
pub external fn from_unix(Int) -> Result(DateTime, #(Atom, Atom)) =
|
||||
"Elixir.DateTime" "from_unix"
|
||||
|
|
19
src/helpers/parsing.gleam
Normal file
19
src/helpers/parsing.gleam
Normal file
|
@ -0,0 +1,19 @@
|
|||
import gleam/dynamic
|
||||
import gleam/result
|
||||
import gleam/string
|
||||
|
||||
/// Get field from a dynamic data presumed to be a map, or fail with error
|
||||
pub fn data_get(
|
||||
data: dynamic.Dynamic,
|
||||
key: String,
|
||||
data_type: dynamic.Decoder(a),
|
||||
error_constructor: fn(String) -> b,
|
||||
) -> Result(a, b) {
|
||||
data
|
||||
|> dynamic.field(key, data_type)
|
||||
|> result.replace_error(error_constructor(
|
||||
"Field " <> key <> " of correct type not found in data: " <> string.inspect(
|
||||
data,
|
||||
),
|
||||
))
|
||||
}
|
|
@ -4,60 +4,48 @@ import gleam/hackney
|
|||
import gleam/result
|
||||
import gleam/dynamic
|
||||
import gleam/list
|
||||
import gleam/string
|
||||
import pump_api/auth/user.{User}
|
||||
import pump_api/auth/token.{Token}
|
||||
import pump_api/auth/tokens.{Tokens}
|
||||
import pump_api/auth/installation_info.{InstallationInfo}
|
||||
import pump_api/http
|
||||
import helpers/config
|
||||
import helpers/date_time
|
||||
import helpers/parsing
|
||||
import azure/b2c.{B2CError}
|
||||
|
||||
pub type ApiError {
|
||||
ApiRequestFailed
|
||||
NotOkResponse
|
||||
InvalidData
|
||||
InvalidData(msg: String)
|
||||
AuthError(inner: B2CError)
|
||||
}
|
||||
|
||||
pub fn auth(username: String, password: String) -> Result(User, ApiError) {
|
||||
let url = config.api_auth_url()
|
||||
assert Ok(raw_req) = request.from_uri(url)
|
||||
|
||||
let data =
|
||||
json.object([
|
||||
#("username", json.string(username)),
|
||||
#("password", json.string(password)),
|
||||
])
|
||||
|
||||
let json_req = request.set_body(raw_req, http.Json(data))
|
||||
try data = run_req(http.req(json_req))
|
||||
|
||||
try token_str = data_get(data, "token", dynamic.string)
|
||||
try valid_to = data_get(data, "tokenValidToUtc", dynamic.string)
|
||||
try valid_to_dt =
|
||||
date_time.from_iso8601(valid_to)
|
||||
|> map_error(InvalidData)
|
||||
|
||||
let token = Token(token: token_str, token_valid_to: valid_to_dt.0)
|
||||
|
||||
try user_name = data_get(data, "userName", dynamic.string)
|
||||
try email = data_get(data, "email", dynamic.string)
|
||||
try first_name = data_get(data, "firstName", dynamic.string)
|
||||
try last_name = data_get(data, "lastName", dynamic.string)
|
||||
try culture = data_get(data, "culture", dynamic.string)
|
||||
try eula_accepted = data_get(data, "eulaAccepted", dynamic.bool)
|
||||
try is_authenticated = data_get(data, "isAuthenticated", dynamic.bool)
|
||||
try time_zone = data_get(data, "timeZone", dynamic.string)
|
||||
|
||||
Ok(User(
|
||||
user_name: user_name,
|
||||
email: email,
|
||||
first_name: first_name,
|
||||
last_name: last_name,
|
||||
culture: culture,
|
||||
eula_accepted: eula_accepted,
|
||||
is_authenticated: is_authenticated,
|
||||
time_zone: time_zone,
|
||||
token: token,
|
||||
try tokens =
|
||||
b2c.authenticate(username, password)
|
||||
|> result.map_error(fn(err) { AuthError(inner: err) })
|
||||
try access_token_expires_in =
|
||||
date_time.from_unix(tokens.access_token_expires_in)
|
||||
|> result.replace_error(InvalidData(
|
||||
msg: "Access token expiry could not be converted into DateTime: " <> string.inspect(
|
||||
tokens.access_token_expires_in,
|
||||
),
|
||||
))
|
||||
try refresh_token_expires_in =
|
||||
date_time.from_unix(tokens.refresh_token_expires_in)
|
||||
|> result.replace_error(InvalidData(
|
||||
msg: "Refresh token expiry could not be converted into DateTime: " <> string.inspect(
|
||||
tokens.refresh_token_expires_in,
|
||||
),
|
||||
))
|
||||
|
||||
Ok(User(tokens: Tokens(
|
||||
access_token: tokens.access_token,
|
||||
access_token_expiry: access_token_expires_in,
|
||||
refresh_token: tokens.refresh_token,
|
||||
refresh_token_expiry: refresh_token_expires_in,
|
||||
)))
|
||||
}
|
||||
|
||||
pub fn installation_info(user: User) -> Result(List(InstallationInfo), ApiError) {
|
||||
|
@ -68,7 +56,12 @@ pub fn installation_info(user: User) -> Result(List(InstallationInfo), ApiError)
|
|||
try data = run_req(http.authed_req(user, empty_req))
|
||||
|
||||
try items =
|
||||
data_get(data, "items", dynamic.list(of: dynamic.field("id", dynamic.int)))
|
||||
parsing.data_get(
|
||||
data,
|
||||
"items",
|
||||
dynamic.list(of: dynamic.field("id", dynamic.int)),
|
||||
InvalidData,
|
||||
)
|
||||
|
||||
Ok(list.map(items, fn(id) { InstallationInfo(id: id) }))
|
||||
}
|
||||
|
@ -77,7 +70,7 @@ fn run_req(req: request.Request(String)) {
|
|||
try resp =
|
||||
req
|
||||
|> hackney.send()
|
||||
|> map_error(ApiRequestFailed)
|
||||
|> result.replace_error(ApiRequestFailed)
|
||||
|
||||
try body = case resp.status {
|
||||
200 -> Ok(resp.body)
|
||||
|
@ -86,19 +79,7 @@ fn run_req(req: request.Request(String)) {
|
|||
|
||||
body
|
||||
|> json.decode(using: dynamic.dynamic)
|
||||
|> map_error(InvalidData)
|
||||
}
|
||||
|
||||
fn data_get(
|
||||
data: dynamic.Dynamic,
|
||||
key: String,
|
||||
data_type: dynamic.Decoder(a),
|
||||
) -> Result(a, ApiError) {
|
||||
data
|
||||
|> dynamic.field(key, data_type)
|
||||
|> map_error(InvalidData)
|
||||
}
|
||||
|
||||
fn map_error(r: Result(a, b), new_error: ApiError) -> Result(a, ApiError) {
|
||||
result.map_error(r, fn(_) { new_error })
|
||||
|> result.replace_error(InvalidData(
|
||||
msg: "Could not parse InstallationInfo JSON.",
|
||||
))
|
||||
}
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import helpers/date_time.{DateTime}
|
||||
|
||||
pub type Token {
|
||||
Token(token: String, token_valid_to: DateTime)
|
||||
}
|
10
src/pump_api/auth/tokens.gleam
Normal file
10
src/pump_api/auth/tokens.gleam
Normal file
|
@ -0,0 +1,10 @@
|
|||
import helpers/date_time.{DateTime}
|
||||
|
||||
pub type Tokens {
|
||||
Tokens(
|
||||
access_token: String,
|
||||
access_token_expiry: DateTime,
|
||||
refresh_token: String,
|
||||
refresh_token_expiry: DateTime,
|
||||
)
|
||||
}
|
|
@ -1,15 +1,5 @@
|
|||
import pump_api/auth/token
|
||||
import pump_api/auth/tokens
|
||||
|
||||
pub type User {
|
||||
User(
|
||||
user_name: String,
|
||||
email: String,
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
culture: String,
|
||||
eula_accepted: Bool,
|
||||
is_authenticated: Bool,
|
||||
time_zone: String,
|
||||
token: token.Token,
|
||||
)
|
||||
User(tokens: tokens.Tokens)
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ pub type ApiRequest =
|
|||
|
||||
pub fn authed_req(user: User, r: ApiRequest) {
|
||||
r
|
||||
|> request.set_header("authorization", "Bearer " <> user.token.token)
|
||||
|> request.set_header("authorization", "Bearer " <> user.tokens.access_token)
|
||||
|> req()
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue