lib/csrf_plus/store/memory_db.ex

defmodule CsrfPlus.Store.MemoryDb do
  @moduledoc """
  A Store to keep the `CsrfPlus.UserAccess` data in memory. The `CsrfPlus.UserAccess` is where the token and its expiration flag is set.
  """

  alias CsrfPlus.UserAccess
  use GenServer
  @behaviour CsrfPlus.Store.Behaviour

  @doc """
  Starts the `CsrfPlus.Store.MemoryDb`.
  """
  def start_link(_opts) do
    state = %{
      db: []
    }

    GenServer.start_link(__MODULE__, state, name: __MODULE__)
  end

  @doc false
  def init(init_arg) do
    {:ok, init_arg}
  end

  @doc "Retrieve all the accesses in the memory"
  def all_accesses() do
    GenServer.call(__MODULE__, :all_accesses)
  end

  @doc "Put an access in the memory"
  def put_access(
        %UserAccess{token: token, access_id: access_id, created_at: nil} =
          user_access
      )
      when is_binary(token) and is_binary(access_id) do
    created_at = System.os_time(:millisecond)

    %{user_access | created_at: created_at}
    |> put_access()
  end

  def put_access(
        %UserAccess{
          token: token,
          access_id: access_id,
          created_at: created_at
        } = user_access
      )
      when is_binary(token) and is_binary(access_id) and is_integer(created_at) do
    GenServer.call(
      __MODULE__,
      {:put_access, user_access}
    )
  end

  def put_access(_) do
    {:error, :invalid_param}
  end

  @doc "Retrieve an access from the memory by its `access_id`"
  def get_access(access_id = access_id)
      when is_binary(access_id) do
    GenServer.call(__MODULE__, {:get_access, access_id})
  end

  def get_access(_) do
    {:error, :invalid_param}
  end

  @doc "Deletes an access from the memory by its `access_id`"
  def delete_access(access_id = access_id)
      when is_binary(access_id) do
    GenServer.call(__MODULE__, {:delete_access, access_id})
  end

  def delete_access(_) do
    {:error, :invalid_param}
  end

  @doc "Actually, check the tokens age and flag all the ones that had expired."
  def delete_dead_accesses(max_age) do
    GenServer.call(__MODULE__, {:delete_dead_accesses, max_age})
  end

  @doc false
  def handle_call(:all_accesses, _from, state) do
    db = Enum.map(state.db, fn entry -> Map.merge(%UserAccess{}, entry) end)

    {:reply, db, state}
  end

  @doc false
  def handle_call(
        {:put_access, %{token: _token, access_id: _access_id, created_at: _created_at} = access},
        _from,
        state
      ) do
    state =
      %{
        state
        | db: [access | state.db]
      }

    user_access = Map.merge(%UserAccess{}, access)

    {:reply, {:ok, user_access}, state}
  end

  @doc false
  def handle_call({:get_access, access_id}, _from, state) do
    access =
      Enum.find(state.db, nil, fn entry -> entry.access_id == access_id end)

    user_access =
      if access != nil do
        Map.merge(%UserAccess{}, access)
      end

    {:reply, user_access, state}
  end

  @doc false
  def handle_call({:delete_access, access_id}, _from, state) do
    deleted = Enum.find(state.db, nil, fn entry -> entry.access_id == access_id end)

    {state, result} =
      if deleted != nil do
        {
          %{
            state
            | db:
                Enum.reject(state.db, fn entry ->
                  entry == deleted
                end)
          },
          {:ok, deleted}
        }
      else
        {state, {:error, :not_found}}
      end

    {:reply, result, state}
  end

  @doc false
  def handle_call({:delete_dead_accesses, max_age}, _from, %{db: db} = state)
      when is_integer(max_age) do
    checking_time = System.os_time(:millisecond)

    state = %{
      state
      | db:
          Enum.map(db, fn entry ->
            if checking_time > entry.created_at + max_age do
              Map.put(entry, :expired?, true)
            else
              entry
            end
          end)
    }

    {:reply, :ok, state}
  end

  @doc false
  def handle_info(_, state) do
    {:noreply, state}
  end
end