defmodule Rivet.Auth.Signin.Local do
@moduledoc """
Local login scheme
"""
alias Rivet.Auth
alias Rivet.Ident
import Ident.User.Lib, only: [check_user_status: 1]
@spec signup(String.t(), params :: map()) :: Auth.Domain.result()
def signup(host, args, type \\ :authed)
def signup(
hostname,
%{"handle" => handle, "password" => password, "email" => eaddr},
type
)
when is_binary(hostname) do
case Ident.Email.one([address: eaddr], [:user]) do
{:ok, %Ident.Email{}} ->
{:error, %Auth.Domain{error: "A signin already exists for `#{eaddr}`"}}
_ ->
handle = Ident.Handle.Lib.gen_good_handle(handle)
Rivet.Auth.Signin.create_user(handle, eaddr, hostname, type, %{
secret: password
})
|> Rivet.Auth.Signin.post_signin()
end
end
def signup(_, _, _) do
{:error,
%Auth.Domain{log: "Auth signup failed, arguments don't match", error: "Signup Failed"}}
end
##############################################################################
@spec check(Auth.Domain.t() | String.t(), params :: map()) :: Auth.Domain.result()
def check(%Auth.Domain{} = auth, %{"handle" => handle, "password" => password}) do
{:ok, auth}
|> load_user(handle)
|> check_user_status
|> load_password_factor
|> valid_user_factor(password)
|> Rivet.Auth.Signin.post_signin()
end
def check(hostname, args = %{"handle" => _, "password" => _}) when is_binary(hostname),
do: check(%Auth.Domain{hostname: hostname}, args)
##############################################################################
@auth_fail_msg "Unable to sign in. Did you want to sign up instead?"
@spec load_user(Auth.Domain.result(), handle :: String.t()) :: Auth.Domain.result()
def load_user({:ok, %Auth.Domain{} = auth}, handle) do
if String.contains?(handle, "@") do
case Ident.Email.one([address: handle], [:user]) do
{:ok, email} ->
{:ok, %Auth.Domain{auth | user: email.user}}
_ ->
{:error,
%Auth.Domain{
log: "Cannot find email #{handle}",
error: @auth_fail_msg
}}
end
else
case Ident.Handle.one([handle: handle], [:user]) do
{:ok, handle} ->
{:ok, %Auth.Domain{auth | handle: handle, user: handle.user}}
_ ->
{:error,
%Auth.Domain{
log: "Cannot find person ~#{handle}",
error: @auth_fail_msg
}}
end
end
end
##############################################################################
@spec load_password_factor(Auth.Domain.result()) :: Auth.Domain.result()
def load_password_factor({:ok, auth = %Auth.Domain{user: user = %Ident.User{}}}) do
case Ident.Factor.Lib.preloaded_with(user, :password) do
%Ident.User{factors: []} = user ->
Logger.metadata(uid: user.id)
{:error, %Auth.Domain{auth | log: "No auth factor for user"}}
%Ident.User{factors: [factor | _]} = user ->
Logger.metadata(uid: user.id)
{:ok, %Auth.Domain{auth | factor: factor}}
_error ->
Logger.metadata(uid: user.id)
{:error, %Auth.Domain{auth | log: "Unexpected result from factor preload"}}
end
end
def load_password_factor(pass = {:error, %Auth.Domain{}}), do: pass
##############################################################################
@spec check_password(hash :: String.t() | Ident.User.t(), password :: String.t()) :: boolean()
def check_password(%Ident.User{} = user, password) do
case load_password_factor({:ok, %Auth.Domain{user: user}}) do
{:ok, %Auth.Domain{factor: %Ident.Factor{hash: hashed}}} ->
check_password(hashed, password)
_ ->
false
end
end
def check_password(hash, password) when is_binary(hash) and not is_nil(hash) do
Bcrypt.verify_pass(password, hash)
end
def check_password(_, _), do: false
@doc """
"""
@spec valid_user_factor(Auth.Domain.result(), password :: String.t()) :: Auth.Domain.result()
def valid_user_factor(
{:ok, auth = %Auth.Domain{user: %Ident.User{}, factor: %Ident.Factor{hash: hashed}}},
password
)
when not is_nil(hashed) and hashed != "N/A" do
if check_password(hashed, password) do
{:ok, %Auth.Domain{auth | status: :authed}}
else
{:error, %Auth.Domain{auth | log: "Invalid Password"}}
end
end
def valid_user_factor({:ok, auth = %Auth.Domain{}}, _) do
{:error, %Auth.Domain{auth | log: "No password factor exists for user"}}
end
def valid_user_factor(pass = {:error, %Auth.Domain{}}, _), do: pass
#
# ##############################################################################
# @spec post_signin(Auth.Domain.result()) :: Auth.Domain.result()
# def post_signin({:ok, auth = %Auth.Domain{status: :authed, user: %Ident.User{}}}) do
# Logger.info("Signin Success", uid: user.id)
# # TODO: events/triggers to table/signin count, etc
# {:ok, auth}
# end
#
# def post_signin(pass = {:error, %Auth.Domain{}}), do: pass
end