defmodule Coherence.PasswordControllerBase do
@moduledoc """
Handle password recovery actions.
Controller that handles the recover password feature.
Actions:
* new - render the recover password form
* create - verify user's email address, generate a token, and send the email
* edit - render the reset password form
* update - verify password, password confirmation, and update the database
"""
defmacro __using__(opts) do
quote location: :keep do
use Timex
alias Coherence.{TrackableService, Messages, Schema, Controller}
require Coherence.Config, as: Config
require Logger
@type schema :: Ecto.Schema.t()
@type conn :: Plug.Conn.t()
@type params :: map()
@schemas unquote(opts)[:schemas] || raise("Schemas option required")
def schema(which), do: Coherence.Schemas.schema(which)
@doc """
Render the recover password form.
"""
@spec new(conn, params) :: conn
def new(conn, _params) do
user_schema = Config.user_schema()
changeset = Controller.changeset(:password, user_schema, user_schema.__struct__)
render(conn, :new, email: "", changeset: changeset)
end
@doc """
Create the recovery token and send the email
"""
@spec create(conn, params) :: conn
def create(conn, %{"password" => password_params} = params) do
user_schema = Config.user_schema()
user = @schemas.get_user_by_email(password_params["email"])
recover_password(conn, user_schema, user, params)
end
@doc """
Render the password and password confirmation form.
"""
@spec edit(conn, params) :: conn
def edit(conn, params) do
user_schema = Config.user_schema()
token = params["id"]
case @schemas.get_by_user(reset_password_token: token) do
nil ->
conn
|> put_flash(:error, Messages.backend().invalid_reset_token())
|> redirect(to: logged_out_url(conn))
user ->
if expired?(user.reset_password_sent_at, days: Config.reset_token_expire_days()) do
:password
|> Controller.changeset(user_schema, user, clear_password_params())
|> @schemas.update
conn
|> put_flash(:error, Messages.backend().password_reset_token_expired())
|> redirect(to: logged_out_url(conn))
else
changeset = Controller.changeset(:password, user_schema, user)
render(conn, "edit.html", changeset: changeset)
end
end
end
@doc """
Verify the passwords and update the database
"""
@spec update(conn, params) :: conn
def update(conn, %{"password" => password_params} = params) do
user_schema = Config.user_schema()
token = password_params["reset_password_token"]
case @schemas.get_by_user(reset_password_token: token) do
nil ->
respond_with(
conn,
:password_update_error,
%{error: Messages.backend().invalid_reset_token()}
)
user ->
if expired?(user.reset_password_sent_at, days: Config.reset_token_expire_days()) do
:password
|> Controller.changeset(user_schema, user, clear_password_params())
|> @schemas.update
respond_with(
conn,
:password_update_error,
%{error: Messages.backend().password_reset_token_expired()}
)
else
params =
clear_password_params(
Controller.permit(
password_params,
Config.password_reset_permitted_attributes() ||
Schema.permitted_attributes_default(:password_reset)
)
)
:password
|> Controller.changeset(user_schema, user, params)
|> @schemas.update
|> case do
{:ok, user} ->
conn
|> TrackableService.track_password_reset(user, user_schema.trackable_table?)
|> respond_with(
:password_update_success,
%{
params: params,
info: Messages.backend().password_updated_successfully()
}
)
{:error, changeset} ->
respond_with(
conn,
:password_update_error,
%{changeset: changeset}
)
end
end
end
end
def clear_password_params(params \\ %{}) do
params
|> Map.put("reset_password_token", nil)
|> Map.put("reset_password_sent_at", nil)
end
def recover_password(conn, user_schema, nil, params) do
if Config.allow_silent_password_recovery_for_unknown_user() do
info = Messages.backend().reset_email_sent()
conn
|> send_email_if_mailer(info, fn -> true end)
|> respond_with(:password_create_success, %{params: params, info: info})
else
changeset = Controller.changeset(:password, user_schema, user_schema.__struct__)
error = Messages.backend().could_not_find_that_email_address()
conn
|> respond_with(:password_create_error, %{changeset: changeset, error: error})
end
end
def recover_password(conn, user_schema, user, params) do
token = random_string(48)
url = router_helpers().password_url(conn, :edit, token)
dt = NaiveDateTime.utc_now()
info = Messages.backend().reset_email_sent()
Config.repo().update!(
Controller.changeset(:password, user_schema, user, %{
reset_password_token: token,
reset_password_sent_at: dt
})
)
conn
|> send_email_if_mailer(info, fn -> send_user_email(:password, user, url) end)
|> respond_with(:password_create_success, %{params: params, info: info})
end
defoverridable(
recover_password: 4,
clear_password_params: 1,
new: 2,
create: 2,
edit: 2,
update: 2
)
end
end
end