diff --git a/apps/fg_http/lib/fg_http/password_resets.ex b/apps/fg_http/lib/fg_http/password_resets.ex index 6093b786f..efd7488c0 100644 --- a/apps/fg_http/lib/fg_http/password_resets.ex +++ b/apps/fg_http/lib/fg_http/password_resets.ex @@ -6,7 +6,7 @@ defmodule FgHttp.PasswordResets do import Ecto.Query, warn: false alias FgHttp.Repo - alias FgHttp.PasswordResets.PasswordReset + alias FgHttp.Users.PasswordReset @doc """ Returns the list of password_resets. @@ -21,6 +21,15 @@ defmodule FgHttp.PasswordResets do Repo.all(PasswordReset) end + def load_user_from_valid_token!(token) when is_binary(token) do + Repo.get_by!( + PasswordReset, + reset_token: token, + consumed_at: nil, + reset_sent_at: DateTime.utc_now() - PasswordReset.token_validity_secs() + ) + end + @doc """ Gets a single password_reset. diff --git a/apps/fg_http/lib/fg_http/sessions.ex b/apps/fg_http/lib/fg_http/sessions.ex index de3cb808d..35cd8373c 100644 --- a/apps/fg_http/lib/fg_http/sessions.ex +++ b/apps/fg_http/lib/fg_http/sessions.ex @@ -6,7 +6,7 @@ defmodule FgHttp.Sessions do import Ecto.Query, warn: false alias FgHttp.Repo - alias FgHttp.{Sessions.Session, Users.User} + alias FgHttp.{Users.Session, Users.User} @doc """ Returns the list of sessions. diff --git a/apps/fg_http/lib/fg_http/password_resets/password_reset.ex b/apps/fg_http/lib/fg_http/users/password_reset.ex similarity index 90% rename from apps/fg_http/lib/fg_http/password_resets/password_reset.ex rename to apps/fg_http/lib/fg_http/users/password_reset.ex index fe9ad877c..07a16ad7b 100644 --- a/apps/fg_http/lib/fg_http/password_resets/password_reset.ex +++ b/apps/fg_http/lib/fg_http/users/password_reset.ex @@ -1,4 +1,4 @@ -defmodule FgHttp.PasswordResets.PasswordReset do +defmodule FgHttp.Users.PasswordReset do @moduledoc """ Schema for PasswordReset """ @@ -9,10 +9,13 @@ defmodule FgHttp.PasswordResets.PasswordReset do alias FgHttp.{Users, Users.User} @token_num_bytes 8 + # 1 day + @token_validity_secs 86_400 schema "password_resets" do field :reset_sent_at, :utc_datetime field :reset_token, :string + field :consumed_at, :string field :user_email, :string, virtual: true belongs_to :user, User @@ -42,6 +45,8 @@ defmodule FgHttp.PasswordResets.PasswordReset do |> validate_required([:reset_token]) end + def token_validity_secs, do: @token_validity_secs + defp load_user_from_email( %Ecto.Changeset{ valid?: true, diff --git a/apps/fg_http/lib/fg_http/sessions/session.ex b/apps/fg_http/lib/fg_http/users/session.ex similarity index 96% rename from apps/fg_http/lib/fg_http/sessions/session.ex rename to apps/fg_http/lib/fg_http/users/session.ex index bc191d2a8..11ae90101 100644 --- a/apps/fg_http/lib/fg_http/sessions/session.ex +++ b/apps/fg_http/lib/fg_http/users/session.ex @@ -1,4 +1,4 @@ -defmodule FgHttp.Sessions.Session do +defmodule FgHttp.Users.Session do @moduledoc """ Represents a Session """ diff --git a/apps/fg_http/lib/fg_http/users/user.ex b/apps/fg_http/lib/fg_http/users/user.ex index 9e682f9e3..bff3a4239 100644 --- a/apps/fg_http/lib/fg_http/users/user.ex +++ b/apps/fg_http/lib/fg_http/users/user.ex @@ -6,7 +6,7 @@ defmodule FgHttp.Users.User do use Ecto.Schema import Ecto.Changeset - alias FgHttp.{Devices.Device, Sessions.Session} + alias FgHttp.{Devices.Device, Users.Session} schema "users" do field :email, :string @@ -35,7 +35,7 @@ defmodule FgHttp.Users.User do |> validate_required([:password_hash]) end - # Only password being updated + # Password updated with user logged in def update_changeset( user, %{ @@ -54,6 +54,23 @@ defmodule FgHttp.Users.User do |> validate_required([:password_hash]) end + # Password updated from token + def update_changeset( + user, + %{ + user: %{ + password: _password, + password_confirmation: _password_confirmation + } + } = attrs + ) do + user + |> cast(attrs, [:email, :password, :password_confirmation]) + |> validate_required([:password, :password_confirmation]) + |> put_password_hash() + |> validate_required([:password_hash]) + end + # Only email being updated def update_changeset(user, %{user: %{email: _email}} = attrs) do user diff --git a/apps/fg_http/lib/fg_http_web/controllers/password_reset_controller.ex b/apps/fg_http/lib/fg_http_web/controllers/password_reset_controller.ex index d686cdf61..6e822fad7 100644 --- a/apps/fg_http/lib/fg_http_web/controllers/password_reset_controller.ex +++ b/apps/fg_http/lib/fg_http_web/controllers/password_reset_controller.ex @@ -4,7 +4,7 @@ defmodule FgHttpWeb.PasswordResetController do """ use FgHttpWeb, :controller - alias FgHttp.{PasswordResets, PasswordResets.PasswordReset} + alias FgHttp.{PasswordResets, Users.PasswordReset, Users.User} plug FgHttpWeb.Plugs.RedirectAuthenticated @@ -15,6 +15,41 @@ defmodule FgHttpWeb.PasswordResetController do |> render("new.html", changeset: changeset) end + def edit(conn, %{"token" => token}) when is_binary(token) do + _user = load_user(conn, token) + changeset = PasswordReset.changeset(%PasswordReset{}, %{}) + + conn + |> render("edit.html", changeset: changeset) + end + + def update(conn, %{ + "password_reset" => + %{ + "reset_token" => token, + "user" => %{ + "password" => _password, + "password_confirmation" => _password_confirmation + } + } = password_reset_params + }) + when is_binary(token) do + user = load_user(conn, token) + + case PasswordResets.update_password_reset(user, password_reset_params) do + {:ok, _user} -> + conn + |> clear_session() + |> put_flash(:info, "User password updated successfully. Please sign in.") + |> redirect(to: Routes.session_path(conn, :new)) + + {:error, changeset} -> + conn + |> put_flash(:error, "Error updating User password.") + |> render("edit.html", changeset: changeset) + end + end + def create(conn, %{"password_reset" => %{"user_email" => _} = password_reset_params}) do case PasswordResets.create_password_reset(password_reset_params) do {:ok, _password_reset} -> @@ -29,4 +64,16 @@ defmodule FgHttpWeb.PasswordResetController do |> render("new.html", changeset: changeset) end end + + defp load_user(conn, token) do + case PasswordResets.load_user_from_valid_token!(token) do + nil -> + conn + |> put_status(:not_found) + |> halt() + + %User{} = user -> + user + end + end end diff --git a/apps/fg_http/lib/fg_http_web/controllers/session_controller.ex b/apps/fg_http/lib/fg_http_web/controllers/session_controller.ex index 9196d8046..44a93411f 100644 --- a/apps/fg_http/lib/fg_http_web/controllers/session_controller.ex +++ b/apps/fg_http/lib/fg_http_web/controllers/session_controller.ex @@ -3,7 +3,7 @@ defmodule FgHttpWeb.SessionController do Implements the CRUD for a Session """ - alias FgHttp.{Sessions, Sessions.Session} + alias FgHttp.{Sessions, Users.Session} use FgHttpWeb, :controller plug FgHttpWeb.Plugs.RedirectAuthenticated when action in [:new] diff --git a/apps/fg_http/lib/fg_http_web/templates/password_reset/form.html.eex b/apps/fg_http/lib/fg_http_web/templates/password_reset/form.html.eex index 23bcc3e43..e69de29bb 100644 --- a/apps/fg_http/lib/fg_http_web/templates/password_reset/form.html.eex +++ b/apps/fg_http/lib/fg_http_web/templates/password_reset/form.html.eex @@ -1,19 +0,0 @@ -<%= form_for @changeset, @action, fn f -> %> - <%= if @changeset.action do %> -
Oops, something went wrong! Please check the errors below.
-Oops, something went wrong! Please check the errors below.
+