defmodule AuthShield do @moduledoc """ Elixir authentication and authorization framework """ alias AuthShield.Authentication alias AuthShield.Authentication.Schemas.Session alias AuthShield.Resources alias AuthShield.Resources.Schemas.User alias AuthShield.Validations.{Login, SignUp} @doc """ Creates a new user on the system. ## Exemples: ```elixir AuthShield.signup(%{ first_name: "Lucas", last_name: "Mesquita", email: "lucas@gmail.com", password: "My_passw@rd2"} }) ``` """ @spec signup(params :: SignUp.t()) :: {:ok, User.t()} | {:error, map()} | {:error, Ecto.Changeset.t()} def signup(params) when is_map(params) do with {:ok, input} <- SignUp.validate(params) do Resources.create_user(input) end end @doc """ Login the user by its password credential. If the user and its credential is authenticated it will return `{:ok, AuthShield.Authentication.Schemas.Session.t()}`. This session should be stored and used on authentication to keep users logged. ## Exemples: ```elixir AuthShield.login( "lucas@gmail.com", "Mypass@rd23", remote_ip: "172.31.4.1", user_agent: "Mozilla/5.0 (Windows NT x.y; rv:10.0) Gecko/20100101 Firefox/10.0" ) ``` """ @spec login(params :: Login.t(), opts :: keyword()) :: {:ok, Session.t()} | {:error, :user_not_found} | {:error, :unauthenticated} | {:error, Ecto.Changeset.t()} | {:error, map()} def login(params, opts \\ []) when is_map(params) do with {:ok, input} <- Login.validate(params), {:user, %User{} = user} <- {:user, Resources.get_user_by(email: input.email)}, {:ok, :authenticated} <- Authentication.authenticate_password(user, input.password) do user |> build_session(opts) |> Authentication.create_session() else {:user, nil} -> {:error, :user_not_found} {:error, :unauthenticated} -> {:error, :unauthenticated} {:error, error} -> {:error, error} end end defp build_session(user, opts) do %{ user_id: user.id, remote_ip: opts[:remote_ip], user_agent: opts[:user_agent], expiration: get_default_expiration(), login_at: Timex.now() } end @doc """ Refresh the authenticated user session. If the user is authenticated and has an active session it will return `{:ok, AuthShield.Authentication.Schemas.Session.t()}`. This session should be stored and used on authentication to keep users logged. ## Exemples: ```elixir AuthShield.refresh_session("ecb4c67d-6380-4984-ae04-1563e885d59e") ``` """ @spec refresh_session(session_id :: String.t() | Session.t()) :: {:ok, Session.t()} | {:error, :session_expired} | {:error, :session_not_exist} | {:error, Ecto.Changeset.t()} def refresh_session(session_id) when is_binary(session_id) do case Authentication.get_session_by(id: session_id) do %Session{} = session -> refresh_session(session) nil -> {:error, :session_not_found} end end def refresh_session(%Session{} = session) do if Timex.after?(session.expiration, Timex.now()) do Authentication.update_session(session, %{expiration: get_default_expiration()}) else {:error, :session_expired} end end @doc """ Logout the authenticated user session. If the user is authenticated and has an active session it will return `{:ok, AuthShield.Authentication.Schemas.Session.t()}`. This session can be ignored because use is not active anymore. ## Exemples: ```elixir AuthShield.logout("ecb4c67d-6380-4984-ae04-1563e885d59e") ``` """ @spec logout(session_id :: String.t()) :: {:ok, Session.t()} | {:error, :session_not_exist} | {:error, Ecto.Changeset.t()} def logout(session_id) when is_binary(session_id) do case Authentication.get_session_by(id: session_id) do %Session{} = session -> logout(session) nil -> {:error, :session_not_found} end end def logout(%Session{} = session) do if Timex.after?(session.expiration, Timex.now()) do Authentication.update_session(session, %{logout_at: Timex.now()}) else {:error, :session_expired} end end defp get_default_expiration do Timex.now() |> Timex.add(Timex.Duration.from_minutes(15)) end end