Skip to content

Instantly share code, notes, and snippets.

@droustchev
Last active September 2, 2022 18:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save droustchev/2feecde506346ce7b5ab39026f54b855 to your computer and use it in GitHub Desktop.
Save droustchev/2feecde506346ce7b5ab39026f54b855 to your computer and use it in GitHub Desktop.
Teller Challenge

Teller Bank Challenge

Mix.install([:req, :jason, :kino])

Your Solution

username = Kino.Input.text("Username") |> Kino.render()
password = Kino.Input.text("Password")
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)
@droustchev
Copy link
Author

Saved as .exs since .livemd isn't rendered properly on Github

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment