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.

-
- <% end %> - - <%= label f, :reset_sent_at %> - <%= datetime_select f, :reset_sent_at %> - <%= error_tag f, :reset_sent_at %> - - <%= label f, :reset_token %> - <%= text_input f, :reset_token %> - <%= error_tag f, :reset_token %> - -
- <%= submit "Save" %> -
-<% end %> diff --git a/apps/fg_http/lib/fg_http_web/templates/password_reset/new.html.eex b/apps/fg_http/lib/fg_http_web/templates/password_reset/new.html.eex index 5181f4870..6f6b11c15 100644 --- a/apps/fg_http/lib/fg_http_web/templates/password_reset/new.html.eex +++ b/apps/fg_http/lib/fg_http_web/templates/password_reset/new.html.eex @@ -1,5 +1,23 @@ -

New Password reset

+

Change Password

-<%= render "form.html", Map.put(assigns, :action, Routes.password_reset_path(@conn, :create)) %> +<%= form_for @changeset, Routes.password_reset_path(@conn, :create), fn f -> %> + <%= if @changeset.action do %> +
+

Oops, something went wrong! Please check the errors below.

+
+ <% end %> + + <%= label f, :reset_sent_at %> + <%= datetime_select f, :reset_sent_at %> + <%= error_tag f, :reset_sent_at %> + + <%= label f, :reset_token %> + <%= text_input f, :reset_token %> + <%= error_tag f, :reset_token %> + +
+ <%= submit "Save" %> +
+<% end %> <%= link "Back", to: Routes.session_path(@conn, :new) %> diff --git a/apps/fg_http/priv/repo/migrations/20200225005454_create_users.exs b/apps/fg_http/priv/repo/migrations/20200225005454_create_users.exs index f2254342d..fb1328e17 100644 --- a/apps/fg_http/priv/repo/migrations/20200225005454_create_users.exs +++ b/apps/fg_http/priv/repo/migrations/20200225005454_create_users.exs @@ -12,6 +12,5 @@ defmodule FgHttp.Repo.Migrations.CreateUsers do end create unique_index(:users, [:email]) - create unique_index(:users, [:reset_token]) end end diff --git a/apps/fg_http/priv/repo/migrations/20200521154921_create_password_resets.exs b/apps/fg_http/priv/repo/migrations/20200521154921_create_password_resets.exs index 54b3ff387..f634a468a 100644 --- a/apps/fg_http/priv/repo/migrations/20200521154921_create_password_resets.exs +++ b/apps/fg_http/priv/repo/migrations/20200521154921_create_password_resets.exs @@ -4,6 +4,7 @@ defmodule FgHttp.Repo.Migrations.CreatePasswordResets do def change do create table(:password_resets) do add :reset_sent_at, :utc_datetime + add :consumed_at, :utc_datetime add :reset_token, :string, null: false add :user_id, references(:users, on_delete: :delete_all), null: false @@ -12,5 +13,6 @@ defmodule FgHttp.Repo.Migrations.CreatePasswordResets do create unique_index(:password_resets, [:reset_token]) create index(:password_resets, [:user_id]) + create index(:password_resets, [:consumed_at]) end end diff --git a/apps/fg_http/test/fg_http/password_resets_test.exs b/apps/fg_http/test/fg_http/password_resets_test.exs index 9b2d984c8..8d3de347c 100644 --- a/apps/fg_http/test/fg_http/password_resets_test.exs +++ b/apps/fg_http/test/fg_http/password_resets_test.exs @@ -4,7 +4,7 @@ defmodule FgHttp.PasswordResetsTest do alias FgHttp.{Fixtures, PasswordResets} describe "password_resets" do - alias FgHttp.PasswordResets.PasswordReset + alias FgHttp.Users.PasswordReset @valid_attrs %{reset_sent_at: "2010-04-17T14:00:00Z"} @update_attrs %{reset_sent_at: "2011-05-18T15:01:01Z"}