defmodule GlificWeb.API.V1.RegistrationController do
@moduledoc """
The Pow User Registration Controller
"""
@dialyzer {:no_return, reset_password: 2}
@dialyzer {:no_return, reset_user_password: 2}
use GlificWeb, :controller
alias Ecto.Changeset
alias PasswordlessAuth
alias Plug.Conn
alias GlificWeb.{
APIAuthPlug,
ErrorHelpers
}
alias Glific.{
Contacts,
Contacts.Contact,
Partners,
Repo,
Tags,
Users,
Users.User
}
@doc false
@spec create(Conn.t(), map()) :: Conn.t()
def create(conn, %{"user" => user_params}) do
with {:ok, _message} <- verify_otp(user_params["phone"], user_params["otp"]),
{:ok, response_data} <- create_user(conn, user_params) do
json(conn, response_data)
else
{:error, errors} ->
conn
|> put_status(500)
|> json(%{error: %{status: 500, message: "Couldn't create user", errors: errors}})
end
end
@spec verify_otp(String.t(), String.t()) :: {:ok, String.t()} | {:error, []}
defp verify_otp(phone, otp) do
case PasswordlessAuth.verify_code(phone, otp) do
:ok ->
# Remove otp code
PasswordlessAuth.remove_code(phone)
{:ok, "verified"}
{:error, error} ->
# Error response options: :attempt_blocked | :code_expired | :does_not_exist | :incorrect_code
{:error, [Atom.to_string(error)]}
end
end
@spec create_user(Conn.t(), map()) :: {:ok, map()} | {:error, []}
defp create_user(conn, user_params) do
organization_id = conn.assigns[:organization_id]
{:ok, contact} =
Repo.fetch_by(Contact, %{phone: user_params["phone"], organization_id: organization_id})
updated_user_params =
user_params
|> Map.merge(%{
"password_confirmation" => user_params["password"],
"contact_id" => contact.id,
"organization_id" => organization_id,
"language_id" => contact.language_id
})
conn
|> Pow.Plug.create_user(updated_user_params)
|> case do
{:ok, user, conn} ->
{:ok, _} =
user
|> Users.promote_first_user()
|> add_staff_tag_to_user_contact()
response_data = %{
data: %{
access_token: conn.private[:api_access_token],
token_expiry_time: conn.private[:api_token_expiry_time],
renewal_token: conn.private[:api_renewal_token]
}
}
{:ok, response_data}
{:error, changeset, _conn} ->
errors = Changeset.traverse_errors(changeset, &ErrorHelpers.translate_error/1)
{:error, errors}
end
end
@doc false
@spec add_staff_tag_to_user_contact(User.t()) :: {:ok, String.t()}
defp add_staff_tag_to_user_contact(user) do
with {:ok, contact} <-
Repo.fetch_by(Contact, %{phone: user.phone, organization_id: user.organization_id}),
{:ok, tag} <-
Repo.fetch_by(Tags.Tag, %{label: "Staff", organization_id: user.organization_id}),
{:ok, _} <-
Tags.create_contact_tag(%{
contact_id: contact.id,
tag_id: tag.id,
organization_id: user.organization_id
}),
do: {:ok, "Staff tag added to the user contact"}
end
# we need to give user permissions here so we can retrieve and send messages
# in some cases
defp build_context(organization_id) do
organization = Partners.organization(organization_id)
Repo.put_current_user(organization.root_user)
end
@doc """
verifying google captcha only when token is passed
"""
@spec send_otp(Conn.t(), map()) :: Conn.t()
def send_otp(
conn,
%{"user" => %{"token" => token, "registration" => "true", "phone" => phone}} =
_user_params
) do
case Glific.verify_google_captcha(token) do
{:ok, "success"} ->
send_otp(conn, %{"user" => %{"phone" => phone, "registration" => "true"}})
{:error, error} ->
send_otp_error(conn, error)
end
end
def send_otp(
conn,
%{"user" => %{"phone" => phone, "registration" => registration}} = _user_params
) do
organization_id = conn.assigns[:organization_id]
build_context(organization_id)
with {:ok, _contact} <- optin_contact(organization_id, phone),
{:ok, contact} <- can_send_otp_to_phone?(organization_id, phone),
true <- send_otp_allowed?(organization_id, phone, registration),
{:ok, _otp} <- create_and_send_verification_code(contact) do
json(conn, %{data: %{phone: phone, message: "OTP sent successfully to #{phone}"}})
else
_ ->
send_otp_error(conn, "Cannot send the otp to #{phone}")
end
end
@doc false
@spec send_otp_error(Conn.t(), String.t()) :: Conn.t()
defp send_otp_error(conn, msg) do
conn
|> put_status(400)
|> json(%{error: %{status: 400, message: msg}})
end
@doc """
Function for generating verification code and sending otp verification message
"""
@spec create_and_send_verification_code(Contact.t()) :: {:ok, String.t()}
def create_and_send_verification_code(contact) do
code = PasswordlessAuth.generate_code(contact.phone)
Glific.Messages.create_and_send_otp_verification_message(contact, code)
{:ok, code}
end
# see if we can send an hsm or session message to this contact
@spec can_send_message_to?(Contact.t()) :: boolean
defp can_send_message_to?(contact) do
hsm = Contacts.can_send_message_to?(contact, true)
session = Contacts.can_send_message_to?(contact, false)
elem(hsm, 0) == :ok || elem(session, 0) == :ok
end
@spec can_send_otp_to_phone?(integer, String.t()) :: {:ok, Contact.t()} | {:error, any} | false
defp can_send_otp_to_phone?(organization_id, phone) do
with {:ok, contact} <-
Repo.fetch_by(Contact, %{phone: phone, organization_id: organization_id}),
true <- can_send_message_to?(contact),
do: {:ok, contact}
end
@spec send_otp_allowed?(integer, String.t(), String.t()) :: boolean
defp send_otp_allowed?(organization_id, phone, registration) do
{result, _} = Repo.fetch_by(User, %{phone: phone, organization_id: organization_id})
(result == :ok && registration == "false") || (result == :error && registration != "false")
end
@doc """
Controller function for reset password
It also verifies OTP to authorize the request
"""
@spec reset_password(Conn.t(), map()) :: Conn.t()
def reset_password(conn, %{"user" => user_params}) do
%{"phone" => phone, "otp" => otp} = user_params
with {:ok, _data} <- verify_otp(phone, otp),
{:ok, response_data} <- reset_user_password(conn, user_params) do
json(conn, response_data)
else
{:error, _errors} ->
conn
|> put_status(500)
|> json(%{error: %{status: 500, message: "Couldn't update user password"}})
end
end
@spec reset_user_password(Conn.t(), map()) :: {:ok, map()} | {:error, []}
defp reset_user_password(conn, %{"phone" => phone, "password" => password} = user_params) do
update_params = %{"password" => password, "password_confirmation" => password}
{:ok, user} =
Repo.fetch_by(User, %{phone: phone, organization_id: conn.assigns[:organization_id]})
user
|> Users.reset_user_password(update_params)
|> case do
{:ok, user} ->
APIAuthPlug.delete_user_sessions(user, conn)
# Create new user session
{:ok, conn} =
Pow.Plug.authenticate_user(
conn,
Map.put(user_params, "organization_id", conn.assigns[:organization_id])
)
{:ok,
%{
data: %{
access_token: conn.private[:api_access_token],
token_expiry_time: conn.private[:api_token_expiry_time],
renewal_token: conn.private[:api_renewal_token]
}
}}
{:error, _error} ->
{:error, []}
end
end
@spec optin_contact(non_neg_integer(), String.t()) :: {:ok, map()} | {:error, []}
defp optin_contact(organization_id, phone) do
%{
phone: phone,
organization_id: organization_id,
method: "registration"
}
|> Contacts.optin_contact()
end
end