lib/auth/session.ex

defmodule Auth.Session do
  @moduledoc """
  Schema and helper functions for managing Sessions for People/Apps.
  The Epic is https://github.com/dwyl/auth/issues/30 however
  we have chosen to make this *much* simpler for now.
  """
  alias Auth.Repo
  import Ecto.Changeset
  import Ecto.Query, warn: false
  use Ecto.Schema
  import Plug.Conn, only: [assign: 3]

  # This is an MVP of sessions, if you need more, please open an issue!
  schema "sessions" do
    field :app_id, :id
    field :auth_provider, :string
    field :end, :naive_datetime
    field :person_id, :id
    field :user_agent_id, :id

    timestamps()
  end

  @doc """
  `changeset/2` validates the session data before inserting/updating.
  """
  def changeset(session, attrs) do
    cast(session, attrs, [:app_id, :auth_provider, :person_id, :user_agent_id, :end])
    |> validate_required([:app_id, :person_id])
  end

  @doc """
  `insert/1` inserts a session based on the data in the conn.assigns.person
  i.e. we are extending our auth flow to include the concept of a session.
  This will allow us to have multiple active sessions (i.e. devices/browsers) 
  for the same person/app. 
  So you can be logged in to an app from multiple devices.
  """
  def insert(conn, person) do
    %Auth.Session{}
    |> changeset(%{
      app_id: person.app_id,
      person_id: person.id,
      user_agent_id: Auth.UserAgent.get_user_agent_id(conn),
      auth_provider: person.auth_provider
    })
    |> Repo.insert!()
  end

  @doc """
  `start_session/1` starts the session and returns conn with conn.assigns.sid.
  invokes `insert/1` above but returns conn instead of the session record.
  """
  def start_session(conn, person) do
    session = insert(conn, person)
    assign(conn, :sid, session.id)
  end

  @doc """
  `get/1` retrieves the current session from DB 
  based on conn.assigns.person data.
  See tests: test/auth/session_test.exs for sample usage.
  """
  def get(conn) do
    Repo.one(
      from s in __MODULE__,
        # match on UA in case person has multiple devices/sessions
        #  only the sessions that haven't been "ended"
        where:
          s.app_id == ^conn.assigns.person.app_id and
            s.person_id == ^conn.assigns.person.id and
            s.user_agent_id == ^Auth.UserAgent.get_user_agent_id(conn) and
            is_nil(s.end),
        # sort by most recent in case there are older un-ended sessions:
        order_by: [desc: :inserted_at]
    )
  end

  @doc """
  `get_by_id/1` retrieves a session by id.
  """
  def get_by_id(conn) do
    Repo.one(
      from s in __MODULE__,
        where: s.id == ^conn.assigns.sid
    )
  end

  @doc """
  `update_session_end/1` update session to end it.
  """
  def update_session_end(conn) do
    get_by_id(conn)
    |> changeset(%{end: DateTime.utc_now()})
    |> Repo.update!()
  end

  @doc """
  `end_session/1` update session to end it.
  """
  def end_session(conn) do
    update_session_end(conn)
    update_in(conn.assigns, &Map.drop(&1, [:sid]))
  end
end