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
@connection_url "https://challenge.teller.engineering"
@device_id "ENRBADNGSEXPJPRZ"
@api_key "good-luck-at-the-teller-quiz!"
@headers [
{"user-agent", "Teller Bank iOS 1.0"},
{"api-key", @api_key},
{"device-id", @device_id},
{"accept", "application/json"}
]
# @connection_url "https://99a2-50-237-200-15.ngrok.io"
def client() do
middleware = [
{Tesla.Middleware.BaseUrl, @connection_url},
# Tesla.Middleware.Logger,
Tesla.Middleware.JSON
]
Tesla.client(middleware)
end
# # the token spec changes though....
# with the f-token-spec: eyJtZXRob2QiOiJzaGEyNTYtYmFzZTY0LW5vLXBhZGRpbmciLCJzZXBhcmF0b3IiOiI6IiwidmFsdWVzIjpbImxhc3QtcmVxdWVzdC1pZCIsImFwaS1rZXkiLCJkZXZpY2UtaWQiLCJ1c2VybmFtZSJdfQ==
# we need to
# {
# "method": "sha256-base64-no-padding",
# "separator": ":",
# "values": [
# "last-request-id",
# "api-key",
# "device-id",
# "username"
# ]
# }
# assume the method doesn't change
def token_gen(last_request_id, api_key, device_id, username, spec) do
decoded_spec = spec |> Base.decode64!(padding: true) |> Jason.decode!()
separator =
decoded_spec
|> Map.get("separator")
to_encode =
decoded_spec
|> Map.get("values")
|> Enum.reduce("", fn item, acc ->
to_append =
case item do
"last-request-id" -> last_request_id
"api-key" -> api_key
"device-id" -> device_id
"username" -> username
end
acc <> separator <> to_append
end)
|> String.trim(separator)
:crypto.hash(:sha256, to_encode)
|> Base.encode64(padding: false)
end
def get_needed_info(headers) do
{_, req_id} =
headers
|> Enum.find(fn item ->
case item do
{"f-request-id", _} -> true
_ -> false
end
end)
{_, req_token} =
headers
|> Enum.find(fn item ->
case item do
{"request-token", _} -> true
_ -> false
end
end)
{_, spec} =
headers
|> Enum.find(fn item ->
case item do
{"f-token-spec", _} -> true
_ -> false
end
end)
{req_id, req_token, spec}
end
def fetch(username, password) do
client = TellerBank.Client.client()
%{headers: headers, body: user_body = %{"mfa_devices" => mfa_devices}} =
Tesla.post!(client, "/login", %{username: username, password: password}, headers: @headers)
{req_id, req_token, spec} = get_needed_info(headers)
next_token = token_gen(req_id, @api_key, @device_id, username, spec)
id = single_device = mfa_devices |> Enum.at(0) |> Map.get("id")
%{headers: headers} =
Tesla.post!(client, "/login/mfa/request", %{device_id: id},
headers:
@headers ++
[
# {"content-type", "application/json"},
{"teller-is-hiring", "I know!"},
{"f-token", next_token},
{"request-token", req_token}
]
)
{req_id, req_token, spec} = get_needed_info(headers)
next_token = token_gen(req_id, @api_key, @device_id, username, spec)
%{headers: headers, body: %{"accounts" => %{"checking" => checking}}} =
Tesla.post!(client, "/login/mfa", %{code: TellerBank.OTPCode.generate(username)},
headers:
@headers ++
[
{"teller-is-hiring", "I know!"},
{"f-token", next_token},
{"request-token", req_token}
]
)
account_id = Enum.at(checking, 0) |> Map.get("id")
{req_id, req_token, spec} = get_needed_info(headers)
next_token = token_gen(req_id, @api_key, @device_id, username, spec)
%{headers: headers, body: %{"number" => number}} =
Tesla.get!(client, "/accounts/#{account_id}/details",
headers:
@headers ++
[
{"teller-is-hiring", "I know!"},
{"f-token", next_token},
{"request-token", req_token}
]
)
{req_id, req_token, spec} = get_needed_info(headers)
next_token = token_gen(req_id, @api_key, @device_id, username, spec)
%{headers: headers, body: %{"available" => available}} =
Tesla.get!(client, "/accounts/#{account_id}/balances",
headers:
@headers ++
[
{"teller-is-hiring", "I know!"},
{"f-token", next_token},
{"request-token", req_token}
]
)
%ChallengeResult{
account_number: number,
balance_in_cents: available
}
end
end
end
username = Kino.Input.read(username)
password = Kino.Input.read(password)
TellerBank.Client.fetch(username, password)