Begin integration with Elixir code

This commit is contained in:
Mikko Ahlroth 2023-01-11 20:37:27 +02:00
parent 13e964cb22
commit b55ff1438c
20 changed files with 206 additions and 217 deletions

4
.gitignore vendored
View file

@ -36,3 +36,7 @@ npm-debug.log
/assets/node_modules/ /assets/node_modules/
.env .env
src/geo_therminator.gleam
.elixir_ls

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -1,8 +0,0 @@
defmodule GeoTherminator.PumpAPI.Auth.Token do
import GeoTherminator.TypedStruct
deftypedstruct(%{
token: String.t(),
token_valid_to: DateTime.t()
})
end

View 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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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),

View file

@ -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(),

View file

@ -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))
} }

View file

@ -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
View 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,
),
))
}

View file

@ -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 })
} }

View file

@ -1,5 +0,0 @@
import helpers/date_time.{DateTime}
pub type Token {
Token(token: String, token_valid_to: DateTime)
}

View 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,
)
}

View file

@ -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,
)
} }

View file

@ -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()
} }