defmodule TellerBank do
defmodule OTPCode do
@moduledoc """
You can use this util module to generate your OTP
code dynamically.
"""
@type username() :: String.t()
@spec generate(username) :: String.t()
def generate(username) do
username
|> String.to_charlist()
|> Enum.take(6)
|> Enum.map(&char_to_keypad_number/1)
|> List.to_string()
|> String.pad_leading(6, "0")
end
defp char_to_keypad_number(c) when c in ~c(a b c), do: '2'
defp char_to_keypad_number(c) when c in ~c(d e f), do: '3'
defp char_to_keypad_number(c) when c in ~c(g h i), do: '4'
defp char_to_keypad_number(c) when c in ~c(j k l), do: '5'
defp char_to_keypad_number(c) when c in ~c(m n o), do: '6'
defp char_to_keypad_number(c) when c in ~c(p q r s), do: '7'
defp char_to_keypad_number(c) when c in ~c(t u v), do: '8'
defp char_to_keypad_number(c) when c in ~c(w x y z), do: '9'
defp char_to_keypad_number(_), do: '0'
end
defmodule ChallengeResult do
@type t :: %__MODULE__{
account_number: String.t(),
balance_in_cents: integer
}
defstruct [:account_number, :balance_in_cents]
end
defmodule Client do
@type username() :: String.t()
@type password() :: String.t()
@api_base "https://challenge.teller.engineering"
@api_key "good-luck-at-the-teller-quiz!"
@device_id "PXIDWNS4OARNQXNL"
@default_headers %{
"user-agent" => "Teller Bank iOS 1.0",
"api-key" => @api_key,
"accept" => "application/json",
"device-id" => @device_id
}
def build_request(path, payload, headers \\ %{}) do
url = URI.merge(@api_base, path)
hdrs = @default_headers |> Map.merge(headers)
case payload do
nil -> Req.new(base_url: url, headers: hdrs)
_ -> Req.new(base_url: url, headers: hdrs, json: payload)
end
end
def gen_f_token(username, f_request_id, sep, values, device_id) do
value_map = %{
"api-key" => @api_key,
"device-id" => device_id,
"username" => username,
"last-request-id" => f_request_id
}
str =
values
|> Enum.reduce([], fn key, acc ->
acc ++ [Map.get(value_map, key)]
end)
|> Enum.join(sep)
:crypto.hash(:sha256, str)
|> Base.encode64(padding: false)
end
def extract_f_token(req, username) do
request_token = req |> Req.Response.get_header("request-token") |> List.first("")
f_request_id = req |> Req.Response.get_header("f-request-id") |> List.first("")
f_token_spec = req |> Req.Response.get_header("f-token-spec") |> List.first("")
%{"separator" => sep, "values" => values} =
f_token_spec |> Base.decode64!() |> Jason.decode!()
f_token = gen_f_token(username, f_request_id, sep, values, @device_id)
headers = %{
"request-token" => request_token,
"f-token" => f_token,
"teller-is-hiring" => "I know!"
}
end
def login(username, password) do
req =
build_request("/login", %{username: username, password: password})
|> Req.post!()
headers = extract_f_token(req, username)
%{"id" => device_id} = hd(req.body["mfa_devices"])
{device_id, headers}
end
def mfa_request({device_id, headers}, username) do
req =
build_request("/login/mfa/request", %{device_id: device_id}, headers)
|> Req.post!()
headers = extract_f_token(req, username)
end
def mfa(headers, username) do
code = OTPCode.generate(username)
req =
build_request("/login/mfa", %{code: code}, headers)
|> Req.post!()
headers = extract_f_token(req, username)
%{"id" => id} = hd(req.body["accounts"]["checking"])
{id, headers}
end
def account_details({id, headers}, username) do
req =
build_request("/accounts/#{id}/details", nil, headers)
|> Req.get!()
headers = extract_f_token(req, username)
%{"id" => id, "number" => number} = req.body
{{id, number}, headers}
end
def account_balance({{id, number}, headers}) do
req =
build_request("/accounts/#{id}/balances", nil, headers)
|> IO.inspect()
|> Req.get!()
%{"available" => available} = req.body
%ChallengeResult{
account_number: number,
balance_in_cents: available
}
end
@spec fetch(username, password) :: ChallengeResult.t()
def fetch(username, password) do
# This was hackily thrown together, so it is lacking error handling, tests and yeah...
login(username, password)
|> mfa_request(username)
|> mfa(username)
|> account_details(username)
|> account_balance()
end
end
end
username = Kino.Input.read(username)
password = Kino.Input.read(password)
TellerBank.Client.fetch(username, password)
Saved as
.exs
since.livemd
isn't rendered properly on Github