defmodule Glific.Users do
@moduledoc """
The Users context.
"""
use Pow.Ecto.Context,
repo: Glific.Repo,
user: Glific.Users.User
import Ecto.Query, warn: false
alias Glific.{
AccessControl.Role,
AccessControl.UserRole,
Repo,
Settings.Language,
Users.User
}
require Logger
@doc """
Returns the list of filtered users.
## Examples
iex> list_users()
[%User{}, ...]
"""
@spec list_users(map()) :: [User.t()]
def list_users(args) do
Repo.list_filter(args, User, &Repo.opts_with_name/2, &Repo.filter_with/2)
end
@doc """
Return the count of users, using the same filter as list_users
"""
@spec count_users(map()) :: integer
def count_users(args),
do: Repo.count_filter(args, User, &Repo.filter_with/2)
@doc """
Gets a single user.
Raises `Ecto.NoResultsError` if the User does not exist.
## Examples
iex> get_user!(123)
%User{}
iex> get_user!(456)
** (Ecto.NoResultsError)
"""
@spec get_user!(integer) :: User.t()
def get_user!(id), do: Repo.get!(User, id)
@doc """
Creates a user.
## Examples
iex> create_user(%{field: value})
{:ok, %User{}}
iex> create_user(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
@spec create_user(map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def create_user(attrs) do
attrs =
attrs
|> Glific.atomize_keys()
|> get_default_language()
%User{}
|> User.changeset(attrs)
|> Repo.insert()
end
@spec get_default_language(map()) :: map()
defp get_default_language(attrs) do
{:ok, en} = Repo.fetch_by(Language, %{label_locale: "English"})
attrs |> Map.merge(%{language_id: en.id})
end
# special type of comparison to allow for nils, we permit comparing with
# nil (and treat it as not being updated), since we don't update these values
@spec is_updated?(any, any) :: boolean
defp is_updated?(_original, nil = _new), do: false
defp is_updated?(original, new), do: original != new
@doc """
Updates a user.
## Examples
iex> update_user(user, %{field: new_value})
{:ok, %User{}}
iex> update_user(user, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
@pow_config [otp_app: :glific]
@spec update_user(User.t(), map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def update_user(%User{} = user, attrs) do
attrs =
attrs
|> validate_add_role_ids?()
|> check_access_role(attrs)
# lets invalidate the tokens and socket for this user
# we do this ONLY if either the role or is_restricted has changed
if validate_add_role_ids?(attrs) ||
is_updated?(user.is_restricted, attrs[:is_restricted]) ||
validate_delete_role_ids?(attrs) do
GlificWeb.APIAuthPlug.delete_all_user_sessions(@pow_config, user)
end
with {:ok, updated_user} <-
user
|> User.update_fields_changeset(attrs)
|> Repo.update() do
if Map.has_key?(attrs, :add_role_ids),
do: update_user_roles(attrs, updated_user),
else: {:ok, updated_user}
end
end
@spec validate_add_role_ids?(map()) :: boolean()
defp validate_add_role_ids?(%{add_role_ids: add_role_ids} = _attrs),
do: length(add_role_ids) != 0
defp validate_add_role_ids?(_attrs), do: false
@spec validate_delete_role_ids?(map()) :: boolean()
defp validate_delete_role_ids?(%{delete_role_ids: delete_role_ids} = _attrs),
do: length(delete_role_ids) != 0
defp validate_delete_role_ids?(_attrs), do: false
@spec check_access_role(boolean(), map()) :: map()
defp check_access_role(false, attrs), do: attrs
defp check_access_role(true, %{add_role_ids: add_role_ids} = attrs) do
roles =
Role
|> select([r], r.label)
|> where([r], r.id in ^add_role_ids)
|> Repo.all()
role =
cond do
Enum.any?(roles, fn role -> role == "Admin" end) -> ["admin"]
Enum.any?(roles, fn role -> role == "Manager" end) -> ["manager"]
Enum.any?(roles, fn role -> role == "Staff" end) -> ["staff"]
Enum.any?(roles, fn role -> role == "No access" end) -> ["none"]
true -> ["manager"]
end
Map.put(attrs, :roles, role)
end
@spec update_user_roles(map(), User.t()) :: {:ok, User.t()}
defp update_user_roles(attrs, user) do
%{access_controls: access_controls} =
attrs
|> Map.put(:user_id, user.id)
|> UserRole.update_user_roles()
user
|> Map.put(:access_roles, access_controls)
|> then(&{:ok, &1})
end
@doc """
Deletes a user.
## Examples
iex> delete_user(user)
{:ok, %User{}}
iex> delete_user(user)
{:error, %Ecto.Changeset{}}
"""
@spec delete_user(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def delete_user(%User{} = user) do
# lets invalidate the tokens and socket for this user
current_user = Repo.get_current_user()
Logger.info(
"Deleting user from org_id: #{user.organization_id} user_id: #{user.id} name: #{user.name} phone: #{user.phone} by #{current_user.name}"
)
GlificWeb.APIAuthPlug.delete_all_user_sessions(@pow_config, user)
Repo.delete(user)
end
@doc """
Reset user password
"""
@spec reset_user_password(User.t(), map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def reset_user_password(%User{} = user, attrs) do
user
|> User.update_fields_changeset(attrs)
|> Repo.update()
end
@impl true
@spec authenticate(map()) :: User.t() | nil
def authenticate(params) do
authenticate_user_organization(params["organization_id"], params)
end
@spec authenticate_user_organization(non_neg_integer | nil, map()) :: User.t() | nil
defp authenticate_user_organization(nil, _params), do: nil
defp authenticate_user_organization(organization_id, params) do
User
|> Repo.get_by(phone: params["phone"], organization_id: organization_id)
|> case do
# Prevent timing attack
nil ->
%User{password_hash: nil}
user ->
user |> Repo.preload(:language)
end
|> verify_password(params["password"])
end
@spec verify_password(User.t(), String.t()) :: User.t() | nil
defp verify_password(user, password),
do:
if(User.verify_password(user, password),
do: user,
else: nil
)
@doc """
Promote the first user of the system to admin automatically.
Ignore NGO or SaaS users which are automatically created
"""
@spec promote_first_user(User.t()) :: User.t()
def promote_first_user(user) do
User
|> where([u], u.id != ^user.id)
|> where([u], not ilike(u.name, "NGO %"))
|> where([u], not ilike(u.name, "SaaS %"))
|> select([u], [u.id])
|> Repo.all()
|> maybe_promote_user(user)
end
@spec maybe_promote_user(list(), User.t()) :: User.t()
defp maybe_promote_user([], user) do
# this is the first user, since the list of valid org users is empty
{:ok, user} =
update_user(user, %{
roles: [:admin],
add_role_ids: get_role_id("Admin"),
organization_id: user.organization_id
})
user
end
defp maybe_promote_user(_list, user) do
{:ok, user} =
update_user(user, %{
roles: [:none],
add_role_ids: get_role_id("No access"),
organization_id: user.organization_id
})
user
end
@spec get_role_id(String.t()) :: list()
defp get_role_id(role) do
Role
|> select([r], r.id)
|> where([r], ilike(r.label, ^role))
|> Repo.all()
end
end