Current section
Files
Jump to
Current section
Files
web/controllers/user_controller.ex
defmodule Shield.UserController do
use Shield.Web, :controller
use Shield.HookImporter
alias Shield.Policy.Login, as: LoginPolicy
alias Shield.Notifier.Channel.Email, as: EmailChannel
alias Authable.Utils.Crypt, as: CryptUtil
@repo Application.get_env(:authable, :repo)
@user Application.get_env(:authable, :resource_owner)
@token_store Application.get_env(:authable, :token_store)
@renderer Application.get_env(:authable, :renderer)
@views Application.get_env(:shield, :views)
@front_end Application.get_env(:shield, :front_end)
plug :scrub_params, "user" when action in [:register, :login]
plug :before_user_register when action in [:register]
plug :before_user_login when action in [:login]
plug Authable.Plug.Authenticate, [scopes: ~w(read)] when action in [:me]
plug Authable.Plug.Authenticate, [scopes: ~w(read write)] when action in [:logout]
plug Authable.Plug.Authenticate, [scopes: ~w(session read write)] when action in [:change_password]
plug Authable.Plug.UnauthorizedOnly when action in [:register, :login, :confirm, :recover_password, :reset_password]
plug Shield.Arm.Confirmable, [enabled: Application.get_env(:shield, :confirmable)] when action in [:me, :change_password]
# GET /users/me
def me(conn, _) do
conn
|> put_status(:ok)
|> render(@views[:user], "show.json", user: conn.assigns[:current_user])
end
# GET /users/confirm
def confirm(conn, %{"confirmation_token" => token_value}) do
case Shield.Arm.Confirmable.confirm(token_value) do
{:error, %{confirmation_token: error}} ->
@renderer.render(conn, :forbidden, %{errors:
%{confirmation_token: error}})
{:error, changeset} ->
conn
|> put_status(:unprocessable_entity)
|> render(@views[:changeset], "error.json", changeset: changeset)
{:ok, _} ->
@renderer.render(conn, :ok,
%{messages: "Email confirmed successfully!"})
end
end
# POST /users/recover-password
def recover_password(conn, %{"user" => %{"email" => email}}) do
recover_user_password(conn, @repo.get_by(@user, email: email))
end
# POST /users/reset-password
def reset_password(conn, %{"user" => %{"password" => new_password, "reset_token" => reset_token}}) do
query = from t in @token_store,
where: t.value == ^reset_token and
t.name == "reset_token" and
t.expires_at > ^:os.system_time(:seconds),
preload: [:user],
limit: 1
reset_password(conn, List.first(@repo.all(query)), new_password)
end
# POST /users/change-password
def change_password(conn, %{"user" => %{"password" => new_password, "old_password" => old_password}}) do
is_password_matched = CryptUtil.match_password(old_password,
Map.get(conn.assigns[:current_user], :password, ""))
change_password(conn, is_password_matched,
conn.assigns[:current_user], new_password)
end
# POST /users/register
def register(conn, %{"user" => user_params}) do
changeset = @user.registration_changeset(%@user{}, user_params)
case @repo.insert(changeset) do
{:ok, user} ->
confirmable = Application.get_env(:shield, :confirmable)
if confirmable, do: Shield.Arm.Confirmable.registration_hook(user)
conn
|> @hooks.after_user_register_success(user)
|> put_status(:created)
|> render(@views[:user], "show.json", user: user)
{:error, changeset} ->
conn
|> @hooks.after_user_register_failure(changeset)
|> put_status(:unprocessable_entity)
|> render(@views[:changeset], "error.json", changeset: changeset)
end
end
# POST /users/login
def login(conn, %{"user" => %{"email" => email, "password" => password} = params}) when is_binary(password) and is_binary(email) do
case LoginPolicy.check(params) do
{:error, {http_status_code, errors}} ->
conn
|> @hooks.after_user_login_failure(errors, http_status_code)
|> @renderer.render(http_status_code, %{errors: errors})
{:ok, %{"user" => user}} ->
insert_session_token(conn, user)
end
end
def login(conn, _) do
@renderer.render(conn, :unprocessable_entity, %{errors:
%{details: "Invalid email or password format!"}})
end
# DELETE /users/logout
def logout(conn, _) do
token_value = conn |> fetch_session |> get_session(:session_token)
token = @repo.get_by!(@token_store, name: "session_token",
value: token_value)
@repo.delete!(token)
conn
|> fetch_session
|> configure_session(drop: true)
|> send_resp(:no_content, "")
end
defp recover_user_password(conn, nil) do
conn
|> put_status(:not_found)
|> render(@views[:error], "404.json")
end
defp recover_user_password(conn, user) do
changeset = @token_store.changeset(%@token_store{}, %{
user_id: user.id,
name: "reset_token",
expires_at: :os.system_time(:seconds) + 3600
})
case @repo.insert(changeset) do
{:ok, token} ->
recover_password_url = String.replace((Map.get(@front_end, :base) <>
Map.get(@front_end, :reset_password_path)), "{{reset_token}}",
token.value)
EmailChannel.deliver(
[user.email],
:recover_password,
%{identity: user.email,
recover_password_url: recover_password_url}
)
@renderer.render(conn, :created, %{messages: "Email sent!"})
{:error, _} ->
conn
|> put_status(:unprocessable_entity)
|> render(@views[:error], "422.json")
end
end
defp reset_password(conn, nil, _) do
@renderer.render(conn, :forbidden, %{errors:
%{reset_token: "Invalid token."}})
end
defp reset_password(conn, token, password) do
@repo.delete!(token)
change_password(conn, true, token.user, password)
end
defp change_password(conn, false, _, _) do
@renderer.render(conn, :forbidden, %{errors:
%{old_password: "Wrong old password."}})
end
defp change_password(conn, true, user, password) do
changeset = @user.password_changeset(user, %{password: password})
case @repo.update(changeset) do
{:ok, _} ->
@renderer.render(conn, :ok, %{messages: "Password updated!"})
{:error, changeset} ->
conn
|> put_status(:unprocessable_entity)
|> render(@views[:changeset], "error.json", changeset: changeset)
end
end
defp insert_session_token(conn, user) do
changeset = @token_store.session_token_changeset(%@token_store{},
%{user_id: user.id, details: %{"scope" => "session"}})
case @repo.insert(changeset) do
{:ok, token} ->
conn
|> @hooks.after_user_login_token_success(token)
|> assign(:current_user, user)
|> fetch_session
|> put_session(:session_token, token.value)
|> configure_session(renew: true)
|> put_status(:created)
|> render(@views[:user], "show.json", user: user)
{:error, changeset} ->
conn
|> @hooks.after_user_login_token_failure(changeset)
|> put_status(:unprocessable_entity)
|> render(@views[:changeset], "error.json", changeset: changeset)
end
end
end