Skip to content

Commit

Permalink
Persist API tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
adriankumpf committed Jul 31, 2019
1 parent dddb1e8 commit b6fd6d0
Show file tree
Hide file tree
Showing 10 changed files with 235 additions and 69 deletions.
4 changes: 2 additions & 2 deletions config/releases.exs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ config :teslamate, TeslaMateWeb.Endpoint,
live_view: [signing_salt: System.get_env("SIGNING_SALT", Util.random_encoded_bytes())]

config :teslamate, :tesla_auth,
username: System.fetch_env!("TESLA_USERNAME"),
password: System.fetch_env!("TESLA_PASSWORD")
username: System.get_env("TESLA_USERNAME"),
password: System.get_env("TESLA_PASSWORD")

if System.get_env("DISABLE_MQTT") != "true" do
config :teslamate, :mqtt,
Expand Down
130 changes: 69 additions & 61 deletions lib/teslamate/api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ defmodule TeslaMate.Api do

require Logger

alias TeslaApi.{Auth, Error, Vehicle}
alias TeslaMate.Auth.{Tokens, Credentials}
alias TeslaMate.Auth

defstruct auth: nil
alias __MODULE__, as: State
Expand All @@ -30,27 +31,77 @@ defmodule TeslaMate.Api do
GenServer.call(name, {:get_vehicle_with_state, id}, 35_000)
end

## Commands
## Internals

def wake_up(name \\ @name, id) do
GenServer.call(name, {:wake_up, id}, 35_000)
def sign_in(name \\ @name, credentials) do
GenServer.call(name, {:sign_in, credentials})
end

def signed_in?(name \\ @name) do
GenServer.call(name, :signed_in?)
end

# Callbacks

@impl true
def init(_opts) do
opts = Application.fetch_env!(:teslamate, :tesla_auth)
username = Keyword.fetch!(opts, :username)
password = Keyword.fetch!(opts, :password)
case {Auth.get_tokens(), Auth.get_credentials()} do
{nil, nil} ->
{:ok, %State{auth: nil}}

{%Tokens{access: access, refresh: refresh}, _credentials} ->
api_auth = %TeslaApi.Auth{token: access, refresh_token: refresh}

case TeslaApi.Auth.refresh(api_auth) do
{:ok, %TeslaApi.Auth{} = auth} ->
:ok = Auth.save(auth)

{:ok, %State{auth: auth}, {:continue, :schedule_refresh}}

{:error, %TeslaApi.Error{} = error} ->
{:stop, error}
end

# TODO remove with v2.0
{nil, %Credentials{email: email, password: password}} ->
case TeslaApi.Auth.login(email, password) do
{:ok, %TeslaApi.Auth{} = auth} ->
:ok = Auth.save(auth)

Logger.warn(
"Signing in with TESLA_USERNAME and TESLA_PASSWORD variables is deprecated. " <>
"An API token has already been stored in the database. " <>
"Both variables can be safely removed from the environment. "
)

{:ok, %State{auth: auth}, {:continue, :schedule_refresh}}

case Auth.login(username, password) do
{:ok, %Auth{} = auth} -> {:ok, %State{auth: auth}, {:continue, :schedule_refresh}}
{:error, %Error{} = error} -> {:stop, error}
{:error, %TeslaApi.Error{} = error} ->
{:stop, error}
end
end
end

@impl true
def handle_call({:sign_in, %Credentials{email: email, password: password}}, _from, state) do
case TeslaApi.Auth.login(email, password) do
{:ok, %TeslaApi.Auth{} = auth} ->
:ok = Auth.save(auth)
{:reply, :ok, %State{auth: auth}, {:continue, :schedule_refresh}}

{:error, %TeslaApi.Error{error: reason}} ->
{:reply, {:error, reason}, state}
end
end

def handle_call(:signed_in?, _from, %State{auth: auth} = state) do
{:reply, not is_nil(auth), state}
end

def handle_call(_command, _from, %State{auth: nil} = state) do
{:reply, {:error, :not_signed_in}, state}
end

def handle_call(:list_vehicles, _from, state) do
{:reply, do_list_vehicles(state.auth), state}
end
Expand All @@ -61,40 +112,21 @@ defmodule TeslaMate.Api do

def handle_call({:get_vehicle_with_state, id}, _from, state) do
response =
case Vehicle.get_with_state(state.auth, id) do
{:error, %Error{error: reason}} -> {:error, reason}
{:ok, %Vehicle{} = vehicle} -> {:ok, vehicle}
end

{:reply, response, state}
end

def handle_call({:wake_up, id}, _from, state) do
response =
case Vehicle.Command.wake_up(state.auth, id) do
{:ok, %Vehicle{state: "online"}} ->
:ok

{:ok, %Vehicle{state: "asleep"}} ->
wait_until_awake(state.auth, id)

{:ok, %Vehicle{state: "offline"}} ->
{:error, :vehicle_unavailable}

{:error, %Error{error: reason}} ->
{:error, reason}
case TeslaApi.Vehicle.get_with_state(state.auth, id) do
{:error, %TeslaApi.Error{error: reason}} -> {:error, reason}
{:ok, %TeslaApi.Vehicle{} = vehicle} -> {:ok, vehicle}
end

{:reply, response, state}
end

@impl true
def handle_info(:refresh_auth, %State{auth: auth} = state) do
case Auth.refresh(auth) do
{:ok, %Auth{} = auth} ->
case TeslaApi.Auth.refresh(auth) do
{:ok, %TeslaApi.Auth{} = auth} ->
{:noreply, %State{state | auth: auth}, {:continue, :schedule_refresh}}

{:error, %Error{error: error, message: reason, env: _}} ->
{:error, %TeslaApi.Error{error: error, message: reason, env: _}} ->
{:stop, {error, reason}}
end
end
Expand Down Expand Up @@ -126,39 +158,15 @@ defmodule TeslaMate.Api do
end

defp do_list_vehicles(auth) do
with {:error, %Error{error: reason}} <- Vehicle.list(auth) do
with {:error, %TeslaApi.Error{error: reason}} <- TeslaApi.Vehicle.list(auth) do
{:error, reason}
end
end

defp find_vehicle(vehicles, id) do
case Enum.find(vehicles, &match?(%Vehicle{id: ^id}, &1)) do
case Enum.find(vehicles, &match?(%TeslaApi.Vehicle{id: ^id}, &1)) do
nil -> {:error, :vehicle_not_found}
vehicle -> {:ok, vehicle}
end
end

defp wait_until_awake(auth, id, retries \\ 5)

defp wait_until_awake(auth, id, retries) when retries > 0 do
case do_get_vehicle(auth, id) do
{:ok, %Vehicle{state: "online"}} ->
:ok

{:ok, %Vehicle{state: "asleep"}} ->
Logger.info("Waiting for vehicle to become awake ...")
:timer.sleep(:timer.seconds(5))
wait_until_awake(auth, id, retries - 1)

{:ok, %Vehicle{state: "offline"}} ->
{:error, :vehicle_unavailable}

{:error, %Error{error: reason}} ->
{:error, reason}
end
end

defp wait_until_awake(_auth, _id, _retries) do
{:error, :vehicle_still_asleep}
end
end
64 changes: 64 additions & 0 deletions lib/teslamate/auth.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
defmodule TeslaMate.Auth do
@moduledoc """
The Auth context.
"""

import Ecto.Query, warn: false
alias TeslaMate.Repo

### Credentials

alias TeslaMate.Auth.Credentials

def get_credentials do
opts = Application.fetch_env!(:teslamate, :tesla_auth)

with username when not is_nil(username) <- Keyword.get(opts, :username),
password when not is_nil(password) <- Keyword.get(opts, :password) do
%Credentials{email: username, password: password}
else
_ -> nil
end
end

def change_credentials(attrs \\ %{}) do
%Credentials{} |> Credentials.changeset(attrs)
end

### Tokens

alias TeslaMate.Auth.Tokens

def get_tokens do
case Repo.all(Tokens) do
[tokens] -> tokens
[] -> nil
end
end

def save(%{token: access, refresh_token: refresh}) do
attrs = %{access: access, refresh: refresh}

maybe_created_or_updated =
case get_tokens() do
nil -> create_tokens(attrs)
tokens -> update_tokens(tokens, attrs)
end

with {:ok, _tokens} <- maybe_created_or_updated do
:ok
end
end

defp create_tokens(attrs) do
%Tokens{}
|> Tokens.changeset(attrs)
|> Repo.insert()
end

defp update_tokens(%Tokens{} = tokens, attrs) do
tokens
|> Tokens.changeset(attrs)
|> Repo.update()
end
end
16 changes: 16 additions & 0 deletions lib/teslamate/auth/credentials.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
defmodule TeslaMate.Auth.Credentials do
use Ecto.Schema
import Ecto.Changeset

schema "" do
field :email, :string
field :password, :string
end

@doc false
def changeset(credentials, attrs) do
credentials
|> cast(attrs, [:email, :password])
|> validate_required([:email, :password])
end
end
18 changes: 18 additions & 0 deletions lib/teslamate/auth/tokens.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
defmodule TeslaMate.Auth.Tokens do
use Ecto.Schema
import Ecto.Changeset

schema "tokens" do
field :refresh, :string
field :access, :string

timestamps()
end

@doc false
def changeset(tokens, attrs) do
tokens
|> cast(attrs, [:access, :refresh])
|> validate_required([:access, :refresh])
end
end
18 changes: 15 additions & 3 deletions lib/teslamate/vehicles.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,24 @@ defmodule TeslaMate.Vehicles do
|> Keyword.get_lazy(:vehicles, &list_vehicles!/0)
|> Enum.map(&{Vehicle, car: create_new!(&1)})

Supervisor.init(children, strategy: :one_for_one, max_restarts: 5, max_seconds: 60)
Supervisor.init(children,
strategy: :one_for_one,
max_restarts: 5,
max_seconds: 60
)
end

defp list_vehicles! do
{:ok, vehicles} = TeslaMate.Api.list_vehicles()
vehicles
case TeslaMate.Api.list_vehicles() do
{:error, :not_signed_in} -> fallback_vehicles()
{:ok, []} -> fallback_vehicles()
{:ok, vehicles} -> vehicles
end
end

defp fallback_vehicles do
Log.list_cars()
|> Enum.map(&%TeslaApi.Vehicle{id: &1.eid})
end

defp create_new!(%TeslaApi.Vehicle{} = vehicle) do
Expand Down
4 changes: 4 additions & 0 deletions lib/teslamate/vehicles/vehicle/summary.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ defmodule TeslaMate.Vehicles.Vehicle.Summary do
:sentry_mode
]

def into(:start, nil) do
%__MODULE__{state: :unavailable}
end

def into(state, vehicle) do
%__MODULE__{format_vehicle(vehicle) | state: format_state(state)}
end
Expand Down
3 changes: 0 additions & 3 deletions lib/teslamate/vehicles/vehicle/vehicle.ex
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,6 @@ defmodule TeslaMate.Vehicles.Vehicle do
{:ok, :start, data, {:next_event, :internal, :fetch}}
end

# TODO
# - check the vehicle state during sleep attempt - does it still work?

## Calls

### Summary
Expand Down
12 changes: 12 additions & 0 deletions priv/repo/migrations/20190730101523_create_tokens.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
defmodule TeslaMate.Repo.Migrations.CreateTokens do
use Ecto.Migration

def change do
create table(:tokens) do
add(:access, :string, null: false)
add(:refresh, :string, null: false)

timestamps()
end
end
end
35 changes: 35 additions & 0 deletions test/teslamate/auth_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
defmodule TeslaMate.AuthTest do
use TeslaMate.DataCase

alias TeslaMate.Auth

describe "tokens" do
@valid_attrs %{refresh_token: "some refresh token", token: "some access token"}
@update_attrs %{
refresh_token: "some updated refresh token",
token: "some updated access token"
}
@invalid_attrs %{refresh_token: nil, token: nil}

test "save/1 with valid data creates or updats the tokens" do
assert Auth.get_tokens() == nil

assert :ok = Auth.save(@valid_attrs)
assert tokens = Auth.get_tokens()
assert tokens.refresh == "some refresh token"
assert tokens.access == "some access token"

assert :ok = Auth.save(@update_attrs)
assert tokens = Auth.get_tokens()
assert tokens.refresh == "some updated refresh token"
assert tokens.access == "some updated access token"
end

test "save/1 with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{} = changeset} = Auth.save(@invalid_attrs)

assert %{refresh: ["can't be blank"], access: ["can't be blank"]} ==
errors_on(changeset)
end
end
end

0 comments on commit b6fd6d0

Please sign in to comment.