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/
|
/assets/node_modules/
|
||||||
|
|
||||||
.env
|
.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
|
defmodule GeoTherminator.PumpAPI.Auth.InstallationInfo do
|
||||||
import GeoTherminator.TypedStruct
|
require Record
|
||||||
|
|
||||||
deftypedstruct(%{
|
Record.defrecord(:record, :installation_info, [:id])
|
||||||
id: integer()
|
|
||||||
# ...
|
@type t :: record(:record, id: non_neg_integer())
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
defmodule GeoTherminator.PumpAPI.Auth.Server do
|
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
|
use GenServer
|
||||||
import GeoTherminator.TypedStruct
|
import GeoTherminator.TypedStruct
|
||||||
alias GeoTherminator.PumpAPI.Auth
|
alias GeoTherminator.PumpAPI.Auth
|
||||||
require Logger
|
|
||||||
|
|
||||||
@token_check_timer 10_000
|
@token_check_timer 10_000
|
||||||
@token_check_diff 5 * 60
|
@token_check_diff 5 * 60
|
||||||
|
@ -32,24 +36,9 @@ defmodule GeoTherminator.PumpAPI.Auth.Server do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def init(%Options{} = opts) do
|
def init(%Options{} = opts) do
|
||||||
case Auth.API.auth(opts.username, opts.password) do
|
case init_state(opts.username, opts.password) do
|
||||||
{:ok, user} ->
|
{:ok, state} -> {:ok, state}
|
||||||
installations = Auth.API.installations_info(user)
|
:error -> {:stop, :error}
|
||||||
|
|
||||||
schedule_token_check()
|
|
||||||
|
|
||||||
state = %State{
|
|
||||||
authed_user: user,
|
|
||||||
installations: installations,
|
|
||||||
installations_fetched: true,
|
|
||||||
username: opts.username,
|
|
||||||
password: opts.password
|
|
||||||
}
|
|
||||||
|
|
||||||
{:ok, state}
|
|
||||||
|
|
||||||
:error ->
|
|
||||||
{:stop, :error}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -65,7 +54,8 @@ defmodule GeoTherminator.PumpAPI.Auth.Server do
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_call({:get_installation, id}, _from, state) do
|
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
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
@ -73,17 +63,23 @@ defmodule GeoTherminator.PumpAPI.Auth.Server do
|
||||||
|
|
||||||
def handle_info(:token_check, state) do
|
def handle_info(:token_check, state) do
|
||||||
now = DateTime.utc_now()
|
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()
|
schedule_token_check()
|
||||||
|
|
||||||
if diff < @token_check_diff do
|
if diff < @token_check_diff do
|
||||||
Logger.debug("Renewing auth token since #{diff} < #{@token_check_diff}")
|
Logger.debug("Renewing auth token since #{diff} < #{@token_check_diff}")
|
||||||
|
|
||||||
with {:ok, user} <- Auth.API.auth(state.username, state.password) do
|
case init_state(state.username, state.password) do
|
||||||
{:noreply, %State{state | authed_user: user}}
|
{:ok, new_state} -> {:noreply, new_state}
|
||||||
else
|
:error -> {:noreply, state}
|
||||||
_ -> {:noreply, state}
|
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
{:noreply, state}
|
{:noreply, state}
|
||||||
|
@ -108,4 +104,24 @@ defmodule GeoTherminator.PumpAPI.Auth.Server do
|
||||||
defp schedule_token_check() do
|
defp schedule_token_check() do
|
||||||
Process.send_after(self(), :token_check, @token_check_timer)
|
Process.send_after(self(), :token_check, @token_check_timer)
|
||||||
end
|
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
|
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
|
defmodule GeoTherminator.PumpAPI.Auth.User do
|
||||||
import GeoTherminator.TypedStruct
|
require Record
|
||||||
|
|
||||||
deftypedstruct(%{
|
Record.defrecord(:record, :user, [:tokens])
|
||||||
user_name: String.t(),
|
|
||||||
email: String.t(),
|
@type t :: record(:record, tokens: GeoTherminator.PumpAPI.Auth.Tokens.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()
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
defmodule GeoTherminator.PumpAPI.Device.API do
|
defmodule GeoTherminator.PumpAPI.Device.API do
|
||||||
|
require GeoTherminator.PumpAPI.Auth.InstallationInfo
|
||||||
|
|
||||||
alias GeoTherminator.PumpAPI.HTTP
|
alias GeoTherminator.PumpAPI.HTTP
|
||||||
alias GeoTherminator.PumpAPI.Device
|
alias GeoTherminator.PumpAPI.Device
|
||||||
alias GeoTherminator.PumpAPI.Auth
|
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
|
def device_info(user, installation) do
|
||||||
url =
|
url =
|
||||||
Application.get_env(:geo_therminator, :api_device_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)
|
req = HTTP.authed_req(user, :get, url)
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,18 @@
|
||||||
defmodule GeoTherminator.PumpAPI.Device.PubSub do
|
defmodule GeoTherminator.PumpAPI.Device.PubSub do
|
||||||
|
require GeoTherminator.PumpAPI.Auth.InstallationInfo
|
||||||
|
|
||||||
alias Phoenix.PubSub
|
alias Phoenix.PubSub
|
||||||
|
alias GeoTherminator.PumpAPI.Auth.InstallationInfo
|
||||||
|
|
||||||
@installation_topic "installation:"
|
@installation_topic "installation:"
|
||||||
|
|
||||||
@spec subscribe_installation(GeoTherminator.PumpAPI.Auth.InstallationInfo.t()) :: :ok
|
@spec subscribe_installation(InstallationInfo.t()) :: :ok
|
||||||
def subscribe_installation(installation) do
|
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
|
end
|
||||||
|
|
||||||
@spec broadcast_device(GeoTherminator.PumpAPI.Device.t()) :: :ok
|
@spec broadcast_device(GeoTherminator.PumpAPI.Device.t()) :: :ok
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
defmodule GeoTherminator.PumpAPI.HTTP do
|
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(
|
@spec authed_req(
|
||||||
GeoTherminator.PumpAPI.Auth.User.t(),
|
User.t(),
|
||||||
Finch.Request.method(),
|
Finch.Request.method(),
|
||||||
Finch.Request.url(),
|
Finch.Request.url(),
|
||||||
Finch.Request.headers(),
|
Finch.Request.headers(),
|
||||||
|
@ -11,7 +17,11 @@ defmodule GeoTherminator.PumpAPI.HTTP do
|
||||||
def authed_req(user, method, url, headers \\ [], body \\ nil, opts \\ []) do
|
def authed_req(user, method, url, headers \\ [], body \\ nil, opts \\ []) do
|
||||||
headers =
|
headers =
|
||||||
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)
|
req(method, url, headers, body, opts)
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,7 +8,7 @@ defmodule GeoTherminatorWeb.MainLive.Pump do
|
||||||
def mount(%{"id" => str_id}, _session, socket) do
|
def mount(%{"id" => str_id}, _session, socket) do
|
||||||
socket =
|
socket =
|
||||||
with {id, ""} <- Integer.parse(str_id),
|
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 <- Device.PubSub.subscribe_installation(info),
|
||||||
{:ok, pid} <- Device.get_device_process(Auth.Server, info),
|
{:ok, pid} <- Device.get_device_process(Auth.Server, info),
|
||||||
%Device{} = device <- Device.Server.get_device(pid),
|
%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",
|
erlc_include_path: "build/dev/erlang/#{@app}/include",
|
||||||
archives: [mix_gleam: "~> 0.6.1"],
|
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,
|
start_permanent: Mix.env() == :prod,
|
||||||
aliases: aliases(),
|
aliases: aliases(),
|
||||||
deps: deps(),
|
deps: deps(),
|
||||||
|
|
|
@ -15,10 +15,10 @@ import gleam/string
|
||||||
import gleam/list
|
import gleam/list
|
||||||
import gleam/result
|
import gleam/result
|
||||||
import gleam/int
|
import gleam/int
|
||||||
import gleam/io
|
|
||||||
import azure/utils
|
import azure/utils
|
||||||
import helpers/crypto
|
import helpers/crypto
|
||||||
import helpers/uri as uri_helpers
|
import helpers/uri as uri_helpers
|
||||||
|
import helpers/parsing
|
||||||
|
|
||||||
const challenge_length = 43
|
const challenge_length = 43
|
||||||
|
|
||||||
|
@ -33,7 +33,12 @@ const b2c_auth_url = "https://thermialogin.b2clogin.com/thermialogin.onmicrosoft
|
||||||
const b2c_authorize_prefix = "var SETTINGS = "
|
const b2c_authorize_prefix = "var SETTINGS = "
|
||||||
|
|
||||||
pub type Tokens {
|
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 {
|
pub type B2CError {
|
||||||
|
@ -91,14 +96,14 @@ fn authorize(code_challenge: String) -> Result(AuthInfo, B2CError) {
|
||||||
body_split,
|
body_split,
|
||||||
fn(line) { string.starts_with(line, b2c_authorize_prefix) },
|
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 prefix_len = string.length(b2c_authorize_prefix)
|
||||||
let settings_json =
|
let settings_json =
|
||||||
string.slice(settings, prefix_len, string.length(settings) - prefix_len - 2)
|
string.slice(settings, prefix_len, string.length(settings) - prefix_len - 2)
|
||||||
try data =
|
try data =
|
||||||
json.decode(settings_json, using: dynamic.dynamic)
|
json.decode(settings_json, using: dynamic.dynamic)
|
||||||
|> map_error(
|
|> b2c_error(
|
||||||
"Authorize settings JSON parsing error: " <> string.inspect(settings_json),
|
"Authorize settings JSON parsing error: " <> string.inspect(settings_json),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -109,7 +114,7 @@ fn authorize(code_challenge: String) -> Result(AuthInfo, B2CError) {
|
||||||
state_code_block
|
state_code_block
|
||||||
|> string.split("=")
|
|> string.split("=")
|
||||||
|> list.at(1)
|
|> list.at(1)
|
||||||
|> map_error("State code parsing error: " <> state_code_block)
|
|> b2c_error("State code parsing error: " <> state_code_block)
|
||||||
|
|
||||||
Ok(AuthInfo(
|
Ok(AuthInfo(
|
||||||
state_code: state_code,
|
state_code: state_code,
|
||||||
|
@ -163,7 +168,7 @@ fn confirm(
|
||||||
|
|
||||||
try csrf_cookie =
|
try csrf_cookie =
|
||||||
list.key_find(auth_info.cookies, csrf_cookie_key)
|
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 cookies = [#(csrf_cookie_key, csrf_cookie), ..self_asserted.cookies]
|
||||||
|
|
||||||
let req = build_req(confirm_url(), http.Get)
|
let req = build_req(confirm_url(), http.Get)
|
||||||
|
@ -182,7 +187,7 @@ fn confirm(
|
||||||
try resp =
|
try resp =
|
||||||
req
|
req
|
||||||
|> hackney.send()
|
|> hackney.send()
|
||||||
|> map_error("Confirm HTTP request failed.")
|
|> b2c_error("Confirm HTTP request failed.")
|
||||||
|
|
||||||
try resp = case resp.status {
|
try resp = case resp.status {
|
||||||
302 -> Ok(resp)
|
302 -> Ok(resp)
|
||||||
|
@ -191,13 +196,13 @@ fn confirm(
|
||||||
|
|
||||||
try location =
|
try location =
|
||||||
response.get_header(resp, "location")
|
response.get_header(resp, "location")
|
||||||
|> map_error("Location not found for confirm response.")
|
|> b2c_error("Location not found for confirm response.")
|
||||||
|
|
||||||
try code =
|
try code =
|
||||||
location
|
location
|
||||||
|> string.split("code=")
|
|> string.split("code=")
|
||||||
|> list.at(1)
|
|> list.at(1)
|
||||||
|> map_error("Confirmation code not found.")
|
|> b2c_error("Confirmation code not found.")
|
||||||
|
|
||||||
Ok(Confirmed(code: code))
|
Ok(Confirmed(code: code))
|
||||||
}
|
}
|
||||||
|
@ -222,12 +227,20 @@ fn get_tokens(
|
||||||
try resp = run_req(req)
|
try resp = run_req(req)
|
||||||
try data =
|
try data =
|
||||||
json.decode(resp.body, using: dynamic.dynamic)
|
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 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 = 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 {
|
fn hash_challenge(challenge: String) -> String {
|
||||||
|
@ -274,7 +287,7 @@ fn run_req(
|
||||||
try resp =
|
try resp =
|
||||||
req
|
req
|
||||||
|> hackney.send()
|
|> hackney.send()
|
||||||
|> map_error("HTTP request failed.")
|
|> b2c_error("HTTP request failed.")
|
||||||
|
|
||||||
case resp.status {
|
case resp.status {
|
||||||
200 -> Ok(resp)
|
200 -> Ok(resp)
|
||||||
|
@ -307,15 +320,9 @@ fn data_get(
|
||||||
key: String,
|
key: String,
|
||||||
data_type: dynamic.Decoder(a),
|
data_type: dynamic.Decoder(a),
|
||||||
) -> Result(a, B2CError) {
|
) -> Result(a, B2CError) {
|
||||||
data
|
parsing.data_get(data, key, data_type, B2CError)
|
||||||
|> dynamic.field(key, data_type)
|
|
||||||
|> map_error(
|
|
||||||
"Field " <> key <> " of correct type not found in data: " <> string.inspect(
|
|
||||||
data,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_error(r: Result(a, b), error_msg: String) -> Result(a, B2CError) {
|
fn b2c_error(r: Result(a, b), msg: String) -> Result(a, B2CError) {
|
||||||
result.map_error(r, fn(_) { B2CError(msg: error_msg) })
|
result.replace_error(r, B2CError(msg))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import gleam/dynamic.{Dynamic}
|
|
||||||
import gleam/map.{Map}
|
|
||||||
import gleam/erlang/atom.{Atom}
|
import gleam/erlang/atom.{Atom}
|
||||||
|
|
||||||
pub type DateTime =
|
pub external type DateTime
|
||||||
Map(Atom, Dynamic)
|
|
||||||
|
|
||||||
pub external fn from_iso8601(String) -> Result(#(DateTime, Int), Atom) =
|
pub external fn from_iso8601(String) -> Result(#(DateTime, Int), Atom) =
|
||||||
"Elixir.DateTime" "from_iso8601"
|
"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/result
|
||||||
import gleam/dynamic
|
import gleam/dynamic
|
||||||
import gleam/list
|
import gleam/list
|
||||||
|
import gleam/string
|
||||||
import pump_api/auth/user.{User}
|
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/auth/installation_info.{InstallationInfo}
|
||||||
import pump_api/http
|
import pump_api/http
|
||||||
import helpers/config
|
import helpers/config
|
||||||
import helpers/date_time
|
import helpers/date_time
|
||||||
|
import helpers/parsing
|
||||||
|
import azure/b2c.{B2CError}
|
||||||
|
|
||||||
pub type ApiError {
|
pub type ApiError {
|
||||||
ApiRequestFailed
|
ApiRequestFailed
|
||||||
NotOkResponse
|
NotOkResponse
|
||||||
InvalidData
|
InvalidData(msg: String)
|
||||||
|
AuthError(inner: B2CError)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn auth(username: String, password: String) -> Result(User, ApiError) {
|
pub fn auth(username: String, password: String) -> Result(User, ApiError) {
|
||||||
let url = config.api_auth_url()
|
try tokens =
|
||||||
assert Ok(raw_req) = request.from_uri(url)
|
b2c.authenticate(username, password)
|
||||||
|
|> result.map_error(fn(err) { AuthError(inner: err) })
|
||||||
let data =
|
try access_token_expires_in =
|
||||||
json.object([
|
date_time.from_unix(tokens.access_token_expires_in)
|
||||||
#("username", json.string(username)),
|
|> result.replace_error(InvalidData(
|
||||||
#("password", json.string(password)),
|
msg: "Access token expiry could not be converted into DateTime: " <> string.inspect(
|
||||||
])
|
tokens.access_token_expires_in,
|
||||||
|
),
|
||||||
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 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) {
|
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 data = run_req(http.authed_req(user, empty_req))
|
||||||
|
|
||||||
try items =
|
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) }))
|
Ok(list.map(items, fn(id) { InstallationInfo(id: id) }))
|
||||||
}
|
}
|
||||||
|
@ -77,7 +70,7 @@ fn run_req(req: request.Request(String)) {
|
||||||
try resp =
|
try resp =
|
||||||
req
|
req
|
||||||
|> hackney.send()
|
|> hackney.send()
|
||||||
|> map_error(ApiRequestFailed)
|
|> result.replace_error(ApiRequestFailed)
|
||||||
|
|
||||||
try body = case resp.status {
|
try body = case resp.status {
|
||||||
200 -> Ok(resp.body)
|
200 -> Ok(resp.body)
|
||||||
|
@ -86,19 +79,7 @@ fn run_req(req: request.Request(String)) {
|
||||||
|
|
||||||
body
|
body
|
||||||
|> json.decode(using: dynamic.dynamic)
|
|> json.decode(using: dynamic.dynamic)
|
||||||
|> map_error(InvalidData)
|
|> result.replace_error(InvalidData(
|
||||||
}
|
msg: "Could not parse InstallationInfo JSON.",
|
||||||
|
))
|
||||||
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 })
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
pub type User {
|
||||||
User(
|
User(tokens: tokens.Tokens)
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ pub type ApiRequest =
|
||||||
|
|
||||||
pub fn authed_req(user: User, r: ApiRequest) {
|
pub fn authed_req(user: User, r: ApiRequest) {
|
||||||
r
|
r
|
||||||
|> request.set_header("authorization", "Bearer " <> user.token.token)
|
|> request.set_header("authorization", "Bearer " <> user.tokens.access_token)
|
||||||
|> req()
|
|> req()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue