defmodule Bouncer.Session do @moduledoc """ A library of functions used to work with session data. """ alias Plug.Conn @doc """ Makes the adapter specified in the environment configuration available. """ def adapter, do: Application.get_env(:bouncer, :adapter) @doc """ Creates a session for a given user and saves it to the session store. Returns the session token which will be used as the API authorization token. """ def create(conn, user) do Poison.encode!(user) |> save(Phoenix.Token.sign(conn, "user", user.id)) end @doc """ Saves session data to the session store using a key. """ def save(data, key), do: adapter.save(data, key) @doc """ Retrieves session data given a token (key) and assigns it to the connection. """ def get(conn, token) do case Phoenix.Token.verify(conn, "user", token) do { :ok, id } -> conn = Conn.assign(conn, :user_token, token) token |> adapter.get |> parse_data |> verify_user_match(id) _ -> {:error, "Could not verify token"} end end @doc """ Parses JSON session data retrieved from the store into a map. ## Examples iex> Bouncer.Session.parse_data {:ok, ~s({"id": 1})} {:ok, %{id: 1}} iex> Bouncer.Session.parse_data {:error, nil} {:error, nil} iex> Bouncer.Session.parse_data {:ok, ""} {:error, :invalid} """ def parse_data({status, response}) do case status do :ok -> case Poison.Parser.parse response, keys: :atoms! do {:ok, data} -> {:ok, data} {status, response} -> {:error, response} end status -> {status, response} end end @doc """ Verifies that the ID deciphered from the received token matches the ID in the session data. ## Examples iex> Bouncer.Session.verify_user_match {:ok, %{id: 1}}, 1 {:ok, %{id: 1}} iex> Bouncer.Session.verify_user_match {:ok, %{id: 2}}, 1 {:error, "Token ID does not match session data ID"} iex> Bouncer.Session.verify_user_match {:error, nil}, 1 {:error, nil} """ def verify_user_match({status, response}, id) do case status do :ok -> if Map.has_key?(response, :id) && response.id === id do {:ok, response} else {:error, "Token ID does not match session data ID"} end status -> {status, response} end end @doc """ Destroys a session by removing session data from the store. """ def destroy(key), do: adapter.delete(key) @doc """ Convenience function to determine if the ID from the current_user in the request matches the given User ID. ## examples iex> Bouncer.Session.user_request? %{assigns: %{current_user: %{id: 1}}}, 1 true iex> Bouncer.Session.user_request? %{assigns: %{current_user: %{id: 1}}}, 2 false iex> Bouncer.Session.user_request? %{assigns: %{}}, 1 false """ def user_request?(conn, id) do Map.has_key?(conn.assigns, :current_user) && Map.has_key?(conn.assigns.current_user, :id) && conn.assigns.current_user.id === id end end