From dc9005068e2a138d65a4f99f1bef8a9b94f220b0 Mon Sep 17 00:00:00 2001 From: Jamil Bou Kheir Date: Thu, 21 May 2020 23:48:24 -0700 Subject: [PATCH 1/6] Checkpoint --- apps/fg_http/lib/fg_http/password_resets.ex | 104 ++++++++++++++++++ .../fg_http/password_resets/password_reset.ex | 64 +++++++++++ apps/fg_http/lib/fg_http/users.ex | 4 - apps/fg_http/lib/fg_http/users/user.ex | 2 - .../controllers/password_reset_controller.ex | 29 ++--- apps/fg_http/lib/fg_http_web/router.ex | 2 + .../fg_http_web/templates/layout/app.html.eex | 13 ++- .../templates/password_reset/edit.html.eex | 5 + .../templates/password_reset/form.html.eex | 19 ++++ .../templates/password_reset/index.html.eex | 28 +++++ .../templates/password_reset/new.html.eex | 5 + .../templates/password_reset/show.html.eex | 18 +++ .../lib/fg_http_web/views/layout_view.ex | 17 +++ .../fg_http_web/views/password_reset_view.ex | 3 + .../20200225005454_create_users.exs | 2 - .../20200521154921_create_password_resets.exs | 16 +++ .../test/fg_http/password_resets_test.exs | 87 +++++++++++++++ .../controllers/device_controller_test.exs | 4 +- .../password_reset_controller_test.exs | 20 ++++ apps/fg_http/test/support/conn_case.ex | 6 +- apps/fg_http/test/support/fixtures.ex | 30 +++-- 21 files changed, 433 insertions(+), 45 deletions(-) create mode 100644 apps/fg_http/lib/fg_http/password_resets.ex create mode 100644 apps/fg_http/lib/fg_http/password_resets/password_reset.ex create mode 100644 apps/fg_http/lib/fg_http_web/templates/password_reset/edit.html.eex create mode 100644 apps/fg_http/lib/fg_http_web/templates/password_reset/form.html.eex create mode 100644 apps/fg_http/lib/fg_http_web/templates/password_reset/index.html.eex create mode 100644 apps/fg_http/lib/fg_http_web/templates/password_reset/new.html.eex create mode 100644 apps/fg_http/lib/fg_http_web/templates/password_reset/show.html.eex create mode 100644 apps/fg_http/lib/fg_http_web/views/password_reset_view.ex create mode 100644 apps/fg_http/priv/repo/migrations/20200521154921_create_password_resets.exs create mode 100644 apps/fg_http/test/fg_http/password_resets_test.exs create mode 100644 apps/fg_http/test/fg_http_web/controllers/password_reset_controller_test.exs diff --git a/apps/fg_http/lib/fg_http/password_resets.ex b/apps/fg_http/lib/fg_http/password_resets.ex new file mode 100644 index 000000000..6093b786f --- /dev/null +++ b/apps/fg_http/lib/fg_http/password_resets.ex @@ -0,0 +1,104 @@ +defmodule FgHttp.PasswordResets do + @moduledoc """ + The PasswordResets context. + """ + + import Ecto.Query, warn: false + alias FgHttp.Repo + + alias FgHttp.PasswordResets.PasswordReset + + @doc """ + Returns the list of password_resets. + + ## Examples + + iex> list_password_resets() + [%PasswordReset{}, ...] + + """ + def list_password_resets do + Repo.all(PasswordReset) + end + + @doc """ + Gets a single password_reset. + + Raises `Ecto.NoResultsError` if the Password reset does not exist. + + ## Examples + + iex> get_password_reset!(123) + %PasswordReset{} + + iex> get_password_reset!(456) + ** (Ecto.NoResultsError) + + """ + def get_password_reset!(id), do: Repo.get!(PasswordReset, id) + + @doc """ + Creates a password_reset. + + ## Examples + + iex> create_password_reset(%{field: value}) + {:ok, %PasswordReset{}} + + iex> create_password_reset(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_password_reset(attrs \\ %{}) do + %PasswordReset{} + |> PasswordReset.create_changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a password_reset. + + ## Examples + + iex> update_password_reset(password_reset, %{field: new_value}) + {:ok, %PasswordReset{}} + + iex> update_password_reset(password_reset, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_password_reset(%PasswordReset{} = password_reset, attrs) do + password_reset + |> PasswordReset.update_changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a password_reset. + + ## Examples + + iex> delete_password_reset(password_reset) + {:ok, %PasswordReset{}} + + iex> delete_password_reset(password_reset) + {:error, %Ecto.Changeset{}} + + """ + def delete_password_reset(%PasswordReset{} = password_reset) do + Repo.delete(password_reset) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking password_reset changes. + + ## Examples + + iex> change_password_reset(password_reset) + %Ecto.Changeset{data: %PasswordReset{}} + + """ + def change_password_reset(%PasswordReset{} = password_reset, attrs \\ %{}) do + PasswordReset.changeset(password_reset, attrs) + end +end diff --git a/apps/fg_http/lib/fg_http/password_resets/password_reset.ex b/apps/fg_http/lib/fg_http/password_resets/password_reset.ex new file mode 100644 index 000000000..fe9ad877c --- /dev/null +++ b/apps/fg_http/lib/fg_http/password_resets/password_reset.ex @@ -0,0 +1,64 @@ +defmodule FgHttp.PasswordResets.PasswordReset do + @moduledoc """ + Schema for PasswordReset + """ + + use Ecto.Schema + import Ecto.Changeset + + alias FgHttp.{Users, Users.User} + + @token_num_bytes 8 + + schema "password_resets" do + field :reset_sent_at, :utc_datetime + field :reset_token, :string + field :user_email, :string, virtual: true + belongs_to :user, User + + timestamps() + end + + @doc false + def changeset(password_reset, attrs) do + password_reset + |> cast(attrs, [:user_id, :user_email, :reset_sent_at, :reset_token]) + end + + @doc false + def create_changeset(password_reset, attrs) do + password_reset + |> cast(attrs, [:user_id, :user_email, :reset_sent_at, :reset_token]) + |> load_user_from_email() + |> generate_reset_token() + |> validate_required([:reset_token, :user_id]) + |> unique_constraint(:reset_token) + end + + @doc false + def update_changeset(password_reset, attrs) do + password_reset + |> cast(attrs, [:user_id, :user_email, :reset_sent_at, :reset_token]) + |> validate_required([:reset_token]) + end + + defp load_user_from_email( + %Ecto.Changeset{ + valid?: true, + changes: %{user_email: user_email} + } = changeset + ) do + user = Users.get_user!(email: user_email) + put_change(changeset, :user_id, user.id) + end + + defp load_user_from_email(changeset), do: changeset + + defp generate_reset_token(%Ecto.Changeset{valid?: true} = changeset) do + random_bytes = :crypto.strong_rand_bytes(@token_num_bytes) + random_string = Base.url_encode64(random_bytes) + put_change(changeset, :reset_token, random_string) + end + + defp generate_reset_token(changeset), do: changeset +end diff --git a/apps/fg_http/lib/fg_http/users.ex b/apps/fg_http/lib/fg_http/users.ex index 323dd66b3..08f0a2b85 100644 --- a/apps/fg_http/lib/fg_http/users.ex +++ b/apps/fg_http/lib/fg_http/users.ex @@ -39,10 +39,6 @@ defmodule FgHttp.Users do Repo.get_by!(User, email: email) end - def get_user!(reset_token: reset_token) do - Repo.get_by!(User, reset_token: reset_token) - end - def get_user!(id), do: Repo.get!(User, id) @doc """ diff --git a/apps/fg_http/lib/fg_http/users/user.ex b/apps/fg_http/lib/fg_http/users/user.ex index b01f6f987..9e682f9e3 100644 --- a/apps/fg_http/lib/fg_http/users/user.ex +++ b/apps/fg_http/lib/fg_http/users/user.ex @@ -11,8 +11,6 @@ defmodule FgHttp.Users.User do schema "users" do field :email, :string field :confirmed_at, :utc_datetime - field :reset_sent_at, :utc_datetime - field :reset_token, :string field :last_signed_in_at, :utc_datetime field :password_hash, :string 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 f5531b7db..d686cdf61 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,35 +4,28 @@ defmodule FgHttpWeb.PasswordResetController do """ use FgHttpWeb, :controller - alias FgHttp.{Users, Users.User} + alias FgHttp.{PasswordResets, PasswordResets.PasswordReset} plug FgHttpWeb.Plugs.RedirectAuthenticated def new(conn, _params) do + changeset = PasswordReset.changeset(%PasswordReset{}, %{}) + conn - |> render("new.html", changeset: User.changeset(%User{})) + |> render("new.html", changeset: changeset) end - # Don't actually create anything. Instead, update the user with a reset token and send - # the password reset email. - def create(conn, %{ - "password_reset" => - %{ - reset_token: reset_token, - password: _password, - password_confirmation: _password_confirmation, - current_password: _current_password - } = user_params - }) do - user = Users.get_user!(reset_token: reset_token) - - case Users.update_user(user, user_params) do - {:ok, _user} -> + def create(conn, %{"password_reset" => %{"user_email" => _} = password_reset_params}) do + case PasswordResets.create_password_reset(password_reset_params) do + {:ok, _password_reset} -> conn - |> render("success.html") + |> clear_session() + |> put_flash(:info, "Password reset successfully. Please sign in with your new password.") + |> redirect(to: Routes.session_path(conn, :new)) {:error, changeset} -> conn + |> put_flash(:error, "Error creating password reset.") |> render("new.html", changeset: changeset) end end diff --git a/apps/fg_http/lib/fg_http_web/router.ex b/apps/fg_http/lib/fg_http_web/router.ex index a2c7a33d0..7594b7851 100644 --- a/apps/fg_http/lib/fg_http_web/router.ex +++ b/apps/fg_http/lib/fg_http_web/router.ex @@ -20,6 +20,8 @@ defmodule FgHttpWeb.Router do scope "/", FgHttpWeb do pipe_through :browser + resources "/password_resets", PasswordResetController, only: [:new, :create] + resources "/user", UserController, singleton: true, only: [:show, :edit, :update, :delete] resources "/users", UserController, only: [:new, :create] diff --git a/apps/fg_http/lib/fg_http_web/templates/layout/app.html.eex b/apps/fg_http/lib/fg_http_web/templates/layout/app.html.eex index a4f9211f2..570eb2794 100644 --- a/apps/fg_http/lib/fg_http_web/templates/layout/app.html.eex +++ b/apps/fg_http/lib/fg_http_web/templates/layout/app.html.eex @@ -14,16 +14,17 @@ - <%= if @user_signed_in? do %> -
+ <%= render_flash(@conn) %> <%= @inner_content %>
diff --git a/apps/fg_http/lib/fg_http_web/templates/password_reset/edit.html.eex b/apps/fg_http/lib/fg_http_web/templates/password_reset/edit.html.eex new file mode 100644 index 000000000..ed57b0cd4 --- /dev/null +++ b/apps/fg_http/lib/fg_http_web/templates/password_reset/edit.html.eex @@ -0,0 +1,5 @@ +

Edit Password reset

+ +<%= render "form.html", Map.put(assigns, :action, Routes.password_reset_path(@conn, :update, @password_reset)) %> + +<%= link "Back", to: Routes.password_reset_path(@conn, :index) %> 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 new file mode 100644 index 000000000..23bcc3e43 --- /dev/null +++ b/apps/fg_http/lib/fg_http_web/templates/password_reset/form.html.eex @@ -0,0 +1,19 @@ +<%= 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/index.html.eex b/apps/fg_http/lib/fg_http_web/templates/password_reset/index.html.eex new file mode 100644 index 000000000..1139f045f --- /dev/null +++ b/apps/fg_http/lib/fg_http_web/templates/password_reset/index.html.eex @@ -0,0 +1,28 @@ +

Listing Password resets

+ + + + + + + + + + + +<%= for password_reset <- @password_resets do %> + + + + + + +<% end %> + +
Reset sent atReset token
<%= password_reset.reset_sent_at %><%= password_reset.reset_token %> + <%= link "Show", to: Routes.password_reset_path(@conn, :show, password_reset) %> + <%= link "Edit", to: Routes.password_reset_path(@conn, :edit, password_reset) %> + <%= link "Delete", to: Routes.password_reset_path(@conn, :delete, password_reset), method: :delete, data: [confirm: "Are you sure?"] %> +
+ +<%= link "New Password reset", to: Routes.password_reset_path(@conn, :new) %> 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 new file mode 100644 index 000000000..5181f4870 --- /dev/null +++ b/apps/fg_http/lib/fg_http_web/templates/password_reset/new.html.eex @@ -0,0 +1,5 @@ +

New Password reset

+ +<%= render "form.html", Map.put(assigns, :action, Routes.password_reset_path(@conn, :create)) %> + +<%= link "Back", to: Routes.session_path(@conn, :new) %> diff --git a/apps/fg_http/lib/fg_http_web/templates/password_reset/show.html.eex b/apps/fg_http/lib/fg_http_web/templates/password_reset/show.html.eex new file mode 100644 index 000000000..a3470a023 --- /dev/null +++ b/apps/fg_http/lib/fg_http_web/templates/password_reset/show.html.eex @@ -0,0 +1,18 @@ +

Show Password reset

+ + + +<%= link "Edit", to: Routes.password_reset_path(@conn, :edit, @password_reset) %> +<%= link "Back", to: Routes.password_reset_path(@conn, :index) %> diff --git a/apps/fg_http/lib/fg_http_web/views/layout_view.ex b/apps/fg_http/lib/fg_http_web/views/layout_view.ex index 083ee3439..27136b0fc 100644 --- a/apps/fg_http/lib/fg_http_web/views/layout_view.ex +++ b/apps/fg_http/lib/fg_http_web/views/layout_view.ex @@ -1,3 +1,20 @@ defmodule FgHttpWeb.LayoutView do use FgHttpWeb, :view + + def render_flash(conn) do + ~E""" +
+ <%= if get_flash(conn, :error) do %> +
+ <%= get_flash(conn, :error) %> +
+ <% end %> + <%= if get_flash(conn, :error) do %> +
+ <%= get_flash(conn, :error) %> +
+ <% end %> +
+ """ + end end diff --git a/apps/fg_http/lib/fg_http_web/views/password_reset_view.ex b/apps/fg_http/lib/fg_http_web/views/password_reset_view.ex new file mode 100644 index 000000000..16ddc7cd3 --- /dev/null +++ b/apps/fg_http/lib/fg_http_web/views/password_reset_view.ex @@ -0,0 +1,3 @@ +defmodule FgHttpWeb.PasswordResetView do + use FgHttpWeb, :view +end 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 926607f58..f2254342d 100644 --- a/apps/fg_http/priv/repo/migrations/20200225005454_create_users.exs +++ b/apps/fg_http/priv/repo/migrations/20200225005454_create_users.exs @@ -7,8 +7,6 @@ defmodule FgHttp.Repo.Migrations.CreateUsers do add :confirmed_at, :utc_datetime add :password_hash, :string add :last_signed_in_at, :utc_datetime - add :reset_sent_at, :utc_datetime - add :reset_token, :utc_datetime timestamps() 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 new file mode 100644 index 000000000..54b3ff387 --- /dev/null +++ b/apps/fg_http/priv/repo/migrations/20200521154921_create_password_resets.exs @@ -0,0 +1,16 @@ +defmodule FgHttp.Repo.Migrations.CreatePasswordResets do + use Ecto.Migration + + def change do + create table(:password_resets) do + add :reset_sent_at, :utc_datetime + add :reset_token, :string, null: false + add :user_id, references(:users, on_delete: :delete_all), null: false + + timestamps() + end + + create unique_index(:password_resets, [:reset_token]) + create index(:password_resets, [:user_id]) + 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 new file mode 100644 index 000000000..9b2d984c8 --- /dev/null +++ b/apps/fg_http/test/fg_http/password_resets_test.exs @@ -0,0 +1,87 @@ +defmodule FgHttp.PasswordResetsTest do + use FgHttp.DataCase + + alias FgHttp.{Fixtures, PasswordResets} + + describe "password_resets" do + alias FgHttp.PasswordResets.PasswordReset + + @valid_attrs %{reset_sent_at: "2010-04-17T14:00:00Z"} + @update_attrs %{reset_sent_at: "2011-05-18T15:01:01Z"} + @invalid_attrs %{reset_sent_at: nil} + + def password_reset_fixture(attrs \\ %{}) do + {:ok, password_reset} = + attrs + |> Enum.into(%{user_id: Fixtures.user().id}) + |> Enum.into(@valid_attrs) + |> PasswordResets.create_password_reset() + + password_reset + end + + test "list_password_resets/0 returns all password_resets" do + password_reset = password_reset_fixture() + assert PasswordResets.list_password_resets() == [password_reset] + end + + test "get_password_reset!/1 returns the password_reset with given id" do + password_reset = password_reset_fixture() + assert PasswordResets.get_password_reset!(password_reset.id) == password_reset + end + + test "create_password_reset/1 with valid data creates a password_reset" do + user_id = Fixtures.user().id + valid_attrs = Map.merge(@valid_attrs, %{user_id: user_id}) + + assert {:ok, %PasswordReset{} = password_reset} = + PasswordResets.create_password_reset(valid_attrs) + + assert password_reset.reset_sent_at == + DateTime.from_naive!(~N[2010-04-17T14:00:00Z], "Etc/UTC") + + assert password_reset.reset_token + assert password_reset.user_id == user_id + end + + test "create_password_reset/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = PasswordResets.create_password_reset(@invalid_attrs) + end + + test "update_password_reset/2 with valid data updates the password_reset" do + password_reset = password_reset_fixture() + + assert {:ok, %PasswordReset{} = password_reset} = + PasswordResets.update_password_reset(password_reset, @update_attrs) + + assert password_reset.reset_sent_at == + DateTime.from_naive!(~N[2011-05-18T15:01:01Z], "Etc/UTC") + + assert password_reset.reset_token + end + + test "update_password_reset/2 with invalid data returns error changeset" do + invalid_attrs = Map.merge(@invalid_attrs, %{reset_token: nil}) + password_reset = password_reset_fixture() + + assert {:error, %Ecto.Changeset{}} = + PasswordResets.update_password_reset(password_reset, invalid_attrs) + + assert password_reset == PasswordResets.get_password_reset!(password_reset.id) + end + + test "delete_password_reset/1 deletes the password_reset" do + password_reset = password_reset_fixture() + assert {:ok, %PasswordReset{}} = PasswordResets.delete_password_reset(password_reset) + + assert_raise Ecto.NoResultsError, fn -> + PasswordResets.get_password_reset!(password_reset.id) + end + end + + test "change_password_reset/1 returns a password_reset changeset" do + password_reset = password_reset_fixture() + assert %Ecto.Changeset{} = PasswordResets.change_password_reset(password_reset) + end + end +end diff --git a/apps/fg_http/test/fg_http_web/controllers/device_controller_test.exs b/apps/fg_http/test/fg_http_web/controllers/device_controller_test.exs index 173cd25a2..73462e4d0 100644 --- a/apps/fg_http/test/fg_http_web/controllers/device_controller_test.exs +++ b/apps/fg_http/test/fg_http_web/controllers/device_controller_test.exs @@ -1,7 +1,7 @@ defmodule FgHttpWeb.DeviceControllerTest do use FgHttpWeb.ConnCase, async: true - import FgHttp.Fixtures + alias FgHttp.Fixtures @create_attrs %{public_key: "foobar"} @update_attrs %{name: "some updated name"} @@ -73,7 +73,7 @@ defmodule FgHttpWeb.DeviceControllerTest do end defp create_device(_) do - device = fixture(:device) + device = Fixtures.device() {:ok, device: device} end end diff --git a/apps/fg_http/test/fg_http_web/controllers/password_reset_controller_test.exs b/apps/fg_http/test/fg_http_web/controllers/password_reset_controller_test.exs new file mode 100644 index 000000000..8d6d9ab88 --- /dev/null +++ b/apps/fg_http/test/fg_http_web/controllers/password_reset_controller_test.exs @@ -0,0 +1,20 @@ +defmodule FgHttpWeb.PasswordResetControllerTest do + use FgHttpWeb.ConnCase, async: true + + @create_attrs %{user_email: "test"} + + describe "new password_reset" do + test "renders form", %{unauthed_conn: conn} do + conn = get(conn, Routes.password_reset_path(conn, :new)) + assert html_response(conn, 200) =~ "New Password reset" + end + end + + describe "create password_reset" do + test "redirects to sign in when data is valid", %{unauthed_conn: conn} do + conn = post(conn, Routes.password_reset_path(conn, :create), password_reset: @create_attrs) + + assert redirected_to(conn) == Routes.session_path(conn, :new) + end + end +end diff --git a/apps/fg_http/test/support/conn_case.ex b/apps/fg_http/test/support/conn_case.ex index d2a6af700..613187bf6 100644 --- a/apps/fg_http/test/support/conn_case.ex +++ b/apps/fg_http/test/support/conn_case.ex @@ -19,7 +19,7 @@ defmodule FgHttpWeb.ConnCase do alias Ecto.Adapters.SQL.Sandbox - import FgHttp.Fixtures + alias FgHttp.Fixtures using do quote do @@ -38,10 +38,10 @@ defmodule FgHttpWeb.ConnCase do end def authed_conn do - user = fixture(:user) + user = Fixtures.user() session = - fixture(:session, %{ + Fixtures.session(%{ user_id: user.id, user_password: "test", user_email: "test" diff --git a/apps/fg_http/test/support/fixtures.ex b/apps/fg_http/test/support/fixtures.ex index 31f5122fc..5f759de25 100644 --- a/apps/fg_http/test/support/fixtures.ex +++ b/apps/fg_http/test/support/fixtures.ex @@ -2,12 +2,15 @@ defmodule FgHttp.Fixtures do @moduledoc """ Convenience helpers for inserting records """ - alias FgHttp.{Devices, Repo, Sessions, Users, Users.User} + alias FgHttp.{Devices, PasswordResets, Repo, Sessions, Users, Users.User} - def fixture(:user) do + def user(attrs \\ %{}) do case Repo.get_by(User, email: "test") do nil -> - attrs = %{email: "test", password: "test", password_confirmation: "test"} + attrs = + attrs + |> Enum.into(%{email: "test", password: "test", password_confirmation: "test"}) + {:ok, user} = Users.create_user(attrs) user @@ -16,13 +19,24 @@ defmodule FgHttp.Fixtures do end end - def fixture(:device) do - attrs = %{public_key: "foobar", ifname: "wg0", name: "factory"} - {:ok, device} = Devices.create_device(Map.merge(%{user_id: fixture(:user).id}, attrs)) + def device(attrs \\ %{}) do + attrs = + attrs + |> Enum.into(%{user_id: user().id}) + |> Enum.into(%{public_key: "foobar", ifname: "wg0", name: "factory"}) + + {:ok, device} = Devices.create_device(attrs) device end - def fixture(:session, attrs \\ %{}) do - {:ok, _session} = Sessions.create_session(attrs) + def session(attrs \\ %{}) do + {:ok, session} = Sessions.create_session(attrs) + session + end + + def password_reset(attrs \\ %{}) do + create_attrs = Map.merge(attrs, %{user_email: user().email}) + {:ok, password_reset} = PasswordResets.create_password_reset(create_attrs) + password_reset end end From b2f7dbc7d04bf0807aa461e8511158943f03eed0 Mon Sep 17 00:00:00 2001 From: Jamil Bou Kheir Date: Thu, 21 May 2020 23:49:55 -0700 Subject: [PATCH 2/6] Remove unused templates --- .../templates/password_reset/edit.html.eex | 5 ---- .../templates/password_reset/index.html.eex | 28 ------------------- .../templates/password_reset/show.html.eex | 18 ------------ 3 files changed, 51 deletions(-) delete mode 100644 apps/fg_http/lib/fg_http_web/templates/password_reset/edit.html.eex delete mode 100644 apps/fg_http/lib/fg_http_web/templates/password_reset/index.html.eex delete mode 100644 apps/fg_http/lib/fg_http_web/templates/password_reset/show.html.eex diff --git a/apps/fg_http/lib/fg_http_web/templates/password_reset/edit.html.eex b/apps/fg_http/lib/fg_http_web/templates/password_reset/edit.html.eex deleted file mode 100644 index ed57b0cd4..000000000 --- a/apps/fg_http/lib/fg_http_web/templates/password_reset/edit.html.eex +++ /dev/null @@ -1,5 +0,0 @@ -

Edit Password reset

- -<%= render "form.html", Map.put(assigns, :action, Routes.password_reset_path(@conn, :update, @password_reset)) %> - -<%= link "Back", to: Routes.password_reset_path(@conn, :index) %> diff --git a/apps/fg_http/lib/fg_http_web/templates/password_reset/index.html.eex b/apps/fg_http/lib/fg_http_web/templates/password_reset/index.html.eex deleted file mode 100644 index 1139f045f..000000000 --- a/apps/fg_http/lib/fg_http_web/templates/password_reset/index.html.eex +++ /dev/null @@ -1,28 +0,0 @@ -

Listing Password resets

- - - - - - - - - - - -<%= for password_reset <- @password_resets do %> - - - - - - -<% end %> - -
Reset sent atReset token
<%= password_reset.reset_sent_at %><%= password_reset.reset_token %> - <%= link "Show", to: Routes.password_reset_path(@conn, :show, password_reset) %> - <%= link "Edit", to: Routes.password_reset_path(@conn, :edit, password_reset) %> - <%= link "Delete", to: Routes.password_reset_path(@conn, :delete, password_reset), method: :delete, data: [confirm: "Are you sure?"] %> -
- -<%= link "New Password reset", to: Routes.password_reset_path(@conn, :new) %> diff --git a/apps/fg_http/lib/fg_http_web/templates/password_reset/show.html.eex b/apps/fg_http/lib/fg_http_web/templates/password_reset/show.html.eex deleted file mode 100644 index a3470a023..000000000 --- a/apps/fg_http/lib/fg_http_web/templates/password_reset/show.html.eex +++ /dev/null @@ -1,18 +0,0 @@ -

Show Password reset

- - - -<%= link "Edit", to: Routes.password_reset_path(@conn, :edit, @password_reset) %> -<%= link "Back", to: Routes.password_reset_path(@conn, :index) %> From 72b3434b23f30a3b7f633ab9836aebad2b57d497 Mon Sep 17 00:00:00 2001 From: Jamil Bou Kheir Date: Thu, 28 May 2020 18:04:27 -0700 Subject: [PATCH 3/6] fix formatting --- apps/fg_http/lib/fg_http/password_resets.ex | 11 ++++- apps/fg_http/lib/fg_http/sessions.ex | 2 +- .../password_reset.ex | 7 ++- .../fg_http/{sessions => users}/session.ex | 2 +- apps/fg_http/lib/fg_http/users/user.ex | 21 +++++++- .../controllers/password_reset_controller.ex | 49 ++++++++++++++++++- .../controllers/session_controller.ex | 2 +- .../templates/password_reset/form.html.eex | 19 ------- .../templates/password_reset/new.html.eex | 22 ++++++++- .../20200225005454_create_users.exs | 1 - .../20200521154921_create_password_resets.exs | 2 + .../test/fg_http/password_resets_test.exs | 2 +- 12 files changed, 109 insertions(+), 31 deletions(-) rename apps/fg_http/lib/fg_http/{password_resets => users}/password_reset.ex (90%) rename apps/fg_http/lib/fg_http/{sessions => users}/session.ex (96%) 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"} From a7457a4368d72cc992b1307bf1bb8d9f051978db Mon Sep 17 00:00:00 2001 From: Jamil Bou Kheir Date: Sun, 31 May 2020 21:11:21 -0700 Subject: [PATCH 4/6] Refactor session and pw_reset into users table --- apps/fg_http/lib/fg_http/password_resets.ex | 93 +++++-------------- apps/fg_http/lib/fg_http/sessions.ex | 89 +----------------- .../lib/fg_http/users/password_reset.ex | 44 ++------- apps/fg_http/lib/fg_http/users/session.ex | 41 ++++---- apps/fg_http/lib/fg_http/users/user.ex | 28 +++++- .../controllers/device_controller.ex | 4 +- .../controllers/password_reset_controller.ex | 77 ++++----------- .../controllers/session_controller.ex | 30 +++--- .../controllers/user_controller.ex | 5 +- .../plugs/redirect_authenticated.ex | 4 +- .../lib/fg_http_web/plugs/session_loader.ex | 39 +++++--- .../fg_http_web/templates/device/new.html.eex | 2 +- .../fg_http_web/templates/layout/app.html.eex | 4 +- .../templates/password_reset/form.html.eex | 0 .../templates/password_reset/new.html.eex | 14 +-- .../templates/session/new.html.eex | 6 +- .../20200225005454_create_users.exs | 5 + .../20200510162435_create_sessions.exs | 15 --- .../20200521154921_create_password_resets.exs | 18 ---- .../test/fg_http/password_resets_test.exs | 78 +++------------- .../controllers/device_controller_test.exs | 12 +-- .../password_reset_controller_test.exs | 4 +- apps/fg_http/test/support/conn_case.ex | 13 +-- apps/fg_http/test/support/fixtures.ex | 14 ++- 24 files changed, 192 insertions(+), 447 deletions(-) delete mode 100644 apps/fg_http/lib/fg_http_web/templates/password_reset/form.html.eex delete mode 100644 apps/fg_http/priv/repo/migrations/20200510162435_create_sessions.exs delete mode 100644 apps/fg_http/priv/repo/migrations/20200521154921_create_password_resets.exs diff --git a/apps/fg_http/lib/fg_http/password_resets.ex b/apps/fg_http/lib/fg_http/password_resets.ex index efd7488c0..5cb706e0a 100644 --- a/apps/fg_http/lib/fg_http/password_resets.ex +++ b/apps/fg_http/lib/fg_http/password_resets.ex @@ -8,28 +8,6 @@ defmodule FgHttp.PasswordResets do alias FgHttp.Users.PasswordReset - @doc """ - Returns the list of password_resets. - - ## Examples - - iex> list_password_resets() - [%PasswordReset{}, ...] - - """ - def list_password_resets 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. @@ -44,70 +22,41 @@ defmodule FgHttp.PasswordResets do ** (Ecto.NoResultsError) """ - def get_password_reset!(id), do: Repo.get!(PasswordReset, id) + def get_password_reset!(email: email) do + Repo.get_by( + PasswordReset, + email: email + ) + end - @doc """ - Creates a password_reset. + def get_password_reset!(reset_token: reset_token) do + validity_secs = -1 * PasswordReset.token_validity_secs() + now = DateTime.truncate(DateTime.utc_now(), :second) - ## Examples + query = + from p in PasswordReset, + where: + p.reset_token == ^reset_token and is_nil(p.reset_consumed_at) and + p.reset_sent_at > datetime_add(^now, ^validity_secs, "second") - iex> create_password_reset(%{field: value}) - {:ok, %PasswordReset{}} - - iex> create_password_reset(%{field: bad_value}) - {:error, %Ecto.Changeset{}} - - """ - def create_password_reset(attrs \\ %{}) do - %PasswordReset{} - |> PasswordReset.create_changeset(attrs) - |> Repo.insert() + Repo.one(query) end @doc """ - Updates a password_reset. + Updates a User with the password reset fields ## Examples - iex> update_password_reset(password_reset, %{field: new_value}) + iex> update_password_reset(%{field: value}) {:ok, %PasswordReset{}} - iex> update_password_reset(password_reset, %{field: bad_value}) + iex> update_password_reset(%{field: bad_value}) {:error, %Ecto.Changeset{}} """ - def update_password_reset(%PasswordReset{} = password_reset, attrs) do - password_reset - |> PasswordReset.update_changeset(attrs) + def create_password_reset(%PasswordReset{} = record, attrs) do + record + |> PasswordReset.create_changeset(attrs) |> Repo.update() end - - @doc """ - Deletes a password_reset. - - ## Examples - - iex> delete_password_reset(password_reset) - {:ok, %PasswordReset{}} - - iex> delete_password_reset(password_reset) - {:error, %Ecto.Changeset{}} - - """ - def delete_password_reset(%PasswordReset{} = password_reset) do - Repo.delete(password_reset) - end - - @doc """ - Returns an `%Ecto.Changeset{}` for tracking password_reset changes. - - ## Examples - - iex> change_password_reset(password_reset) - %Ecto.Changeset{data: %PasswordReset{}} - - """ - def change_password_reset(%PasswordReset{} = password_reset, attrs \\ %{}) do - PasswordReset.changeset(password_reset, attrs) - end end diff --git a/apps/fg_http/lib/fg_http/sessions.ex b/apps/fg_http/lib/fg_http/sessions.ex index 35cd8373c..e5160fa36 100644 --- a/apps/fg_http/lib/fg_http/sessions.ex +++ b/apps/fg_http/lib/fg_http/sessions.ex @@ -6,20 +6,7 @@ defmodule FgHttp.Sessions do import Ecto.Query, warn: false alias FgHttp.Repo - alias FgHttp.{Users.Session, Users.User} - - @doc """ - Returns the list of sessions. - - ## Examples - - iex> list_sessions() - [%Session{}, ...] - - """ - def list_sessions do - Repo.all(Session) - end + alias FgHttp.Users.Session @doc """ Gets a single session. @@ -35,6 +22,7 @@ defmodule FgHttp.Sessions do ** (Ecto.NoResultsError) """ + def get_session!(email: email), do: Repo.get_by!(Session, email: email) def get_session!(id), do: Repo.get!(Session, id) @doc """ @@ -49,78 +37,9 @@ defmodule FgHttp.Sessions do {:error, %Ecto.Changeset{}} """ - def create_session(attrs \\ %{}) do - %Session{} + def create_session(%{email: email} = attrs) do + get_session!(email: email) |> Session.create_changeset(attrs) - |> Repo.insert() - end - - @doc """ - Updates a session. - - ## Examples - - iex> update_session(session, %{field: new_value}) - {:ok, %Session{}} - - iex> update_session(session, %{field: bad_value}) - {:error, %Ecto.Changeset{}} - - """ - def update_session(%Session{} = session, attrs) do - session - |> Session.changeset(attrs) |> Repo.update() end - - @doc """ - Deletes a session. - - ## Examples - - iex> delete_session(session) - {:ok, %Session{}} - - iex> delete_session(session) - {:error, %Ecto.Changeset{}} - - """ - def delete_session(%Session{} = session, really_delete: true) do - Repo.delete(session) - end - - def delete_session(%Session{} = session) do - update_session(session, %{deleted_at: DateTime.utc_now()}) - end - - def load_session(_session_id) do - query = - from s in Session, - where: is_nil(s.deleted_at), - join: u in User, - on: s.user_id == u.id, - select: {s, u}, - preload: :user - - case session = Repo.one(query) do - nil -> - {:error, "Valid session not found"} - - _ -> - {:ok, session} - end - end - - @doc """ - Returns an `%Ecto.Changeset{}` for tracking session changes. - - ## Examples - - iex> change_session(session) - %Ecto.Changeset{source: %Session{}} - - """ - def change_session(%Session{} = session) do - Session.changeset(session, %{}) - end end diff --git a/apps/fg_http/lib/fg_http/users/password_reset.ex b/apps/fg_http/lib/fg_http/users/password_reset.ex index 07a16ad7b..e2cd64d15 100644 --- a/apps/fg_http/lib/fg_http/users/password_reset.ex +++ b/apps/fg_http/lib/fg_http/users/password_reset.ex @@ -6,59 +6,35 @@ defmodule FgHttp.Users.PasswordReset do use Ecto.Schema import Ecto.Changeset - alias FgHttp.{Users, Users.User} - @token_num_bytes 8 # 1 day @token_validity_secs 86_400 - schema "password_resets" do + schema "users" do field :reset_sent_at, :utc_datetime + field :reset_consumed_at, :utc_datetime field :reset_token, :string - field :consumed_at, :string - field :user_email, :string, virtual: true - belongs_to :user, User - - timestamps() + field :email, :string end @doc false - def changeset(password_reset, attrs) do - password_reset - |> cast(attrs, [:user_id, :user_email, :reset_sent_at, :reset_token]) + def changeset do + %__MODULE__{} + |> cast(%{}, [:email, :reset_sent_at, :reset_token]) end @doc false - def create_changeset(password_reset, attrs) do + def create_changeset(%__MODULE__{} = password_reset, attrs) do password_reset - |> cast(attrs, [:user_id, :user_email, :reset_sent_at, :reset_token]) - |> load_user_from_email() + |> cast(attrs, [:email, :reset_sent_at, :reset_token]) + |> validate_required([:email]) |> generate_reset_token() - |> validate_required([:reset_token, :user_id]) + |> validate_required([:reset_token]) |> unique_constraint(:reset_token) end - @doc false - def update_changeset(password_reset, attrs) do - password_reset - |> cast(attrs, [:user_id, :user_email, :reset_sent_at, :reset_token]) - |> validate_required([:reset_token]) - end - def token_validity_secs, do: @token_validity_secs - defp load_user_from_email( - %Ecto.Changeset{ - valid?: true, - changes: %{user_email: user_email} - } = changeset - ) do - user = Users.get_user!(email: user_email) - put_change(changeset, :user_id, user.id) - end - - defp load_user_from_email(changeset), do: changeset - defp generate_reset_token(%Ecto.Changeset{valid?: true} = changeset) do random_bytes = :crypto.strong_rand_bytes(@token_num_bytes) random_string = Base.url_encode64(random_bytes) diff --git a/apps/fg_http/lib/fg_http/users/session.ex b/apps/fg_http/lib/fg_http/users/session.ex index 11ae90101..46f405c78 100644 --- a/apps/fg_http/lib/fg_http/users/session.ex +++ b/apps/fg_http/lib/fg_http/users/session.ex @@ -8,47 +8,46 @@ defmodule FgHttp.Users.Session do alias FgHttp.{Users, Users.User} - schema "sessions" do - field :deleted_at, :utc_datetime - belongs_to :user, User - - # VIRTUAL FIELDS - field :user_email, :string, virtual: true - field :user_password, :string, virtual: true - - timestamps() - end - - @doc false - def changeset(session, attrs \\ %{}) do - session - |> cast(attrs, [:deleted_at]) - |> validate_required([]) + schema "users" do + field :email, :string + field :password, :string, virtual: true + field :last_signed_in_at, :utc_datetime end def create_changeset(session, attrs \\ %{}) do session - |> cast(attrs, [:user_email, :user_password]) - |> validate_required([:user_email, :user_password]) + |> cast(attrs, [:email, :password]) + |> validate_required([:email, :password]) |> authenticate_user() + |> set_last_signed_in_at() end + defp set_last_signed_in_at(%Ecto.Changeset{valid?: true} = changeset) do + last_signed_in_at = DateTime.truncate(DateTime.utc_now(), :second) + change(changeset, last_signed_in_at: last_signed_in_at) + end + + defp set_last_signed_in_at(changeset), do: changeset + defp authenticate_user( %Ecto.Changeset{ valid?: true, - changes: %{user_email: email, user_password: password} + changes: %{email: email, password: password} } = changeset ) do user = Users.get_user!(email: email) case User.authenticate_user(user, password) do {:ok, _} -> - change(changeset, user_id: user.id) + # Remove the user's password so it doesn't accidentally end up somewhere + changeset + |> delete_change(:password) + |> change(%{id: user.id}) {:error, error_msg} -> raise("There was an issue with your password: #{error_msg}") end end - defp authenticate_user(changeset), do: changeset + defp authenticate_user(changeset), do: delete_change(changeset, :password) end diff --git a/apps/fg_http/lib/fg_http/users/user.ex b/apps/fg_http/lib/fg_http/users/user.ex index bff3a4239..66cf3d121 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, Users.Session} + alias FgHttp.Devices.Device schema "users" do field :email, :string @@ -20,7 +20,6 @@ defmodule FgHttp.Users.User do field :current_password, :string, virtual: true has_many :devices, Device, on_delete: :delete_all - has_many :sessions, Session, on_delete: :delete_all timestamps() end @@ -67,6 +66,7 @@ defmodule FgHttp.Users.User do user |> cast(attrs, [:email, :password, :password_confirmation]) |> validate_required([:password, :password_confirmation]) + |> validate_password_equality() |> put_password_hash() |> validate_required([:password_hash]) end @@ -91,14 +91,34 @@ defmodule FgHttp.Users.User do user end + defp validate_password_equality(%Ecto.Changeset{valid?: true} = changeset) do + password = changeset.changes[:password] + password_confirmation = changeset.changes[:password_confirmation] + + if password != password_confirmation do + add_error(changeset, :password, "does not match password confirmation.") + else + changeset + end + end + + defp validate_password_equality(changeset), do: changeset + defp put_password_hash( %Ecto.Changeset{ valid?: true, changes: %{password: password} } = changeset ) do - change(changeset, password_hash: Argon2.hash_pwd_salt(password)) + changeset + |> change(password_hash: Argon2.hash_pwd_salt(password)) + |> delete_change(:password) + |> delete_change(:password_confirmation) end - defp put_password_hash(changeset), do: changeset + defp put_password_hash(changeset) do + changeset + |> delete_change(:password) + |> delete_change(:password_confirmation) + end end diff --git a/apps/fg_http/lib/fg_http_web/controllers/device_controller.ex b/apps/fg_http/lib/fg_http_web/controllers/device_controller.ex index 1bc4abb9b..0d26a9198 100644 --- a/apps/fg_http/lib/fg_http_web/controllers/device_controller.ex +++ b/apps/fg_http/lib/fg_http_web/controllers/device_controller.ex @@ -9,7 +9,7 @@ defmodule FgHttpWeb.DeviceController do plug FgHttpWeb.Plugs.SessionLoader def index(conn, _params) do - devices = Devices.list_devices(conn.assigns.current_user.id) + devices = Devices.list_devices(conn.assigns.session.id) render(conn, "index.html", devices: devices) end @@ -20,7 +20,7 @@ defmodule FgHttpWeb.DeviceController do def create(conn, %{"device" => %{"public_key" => _public_key} = device_params}) do our_params = %{ - "user_id" => conn.assigns.current_user.id, + "user_id" => conn.assigns.session.id, "name" => "Default", "ifname" => "wg0" } 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 6e822fad7..29b9559ec 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,76 +4,35 @@ defmodule FgHttpWeb.PasswordResetController do """ use FgHttpWeb, :controller - alias FgHttp.{PasswordResets, Users.PasswordReset, Users.User} + alias FgHttp.{PasswordResets, Users.PasswordReset} plug FgHttpWeb.Plugs.RedirectAuthenticated def new(conn, _params) do - changeset = PasswordReset.changeset(%PasswordReset{}, %{}) - conn - |> render("new.html", changeset: changeset) + |> render("new.html", changeset: PasswordReset.changeset()) end - def edit(conn, %{"token" => token}) when is_binary(token) do - _user = load_user(conn, token) - changeset = PasswordReset.changeset(%PasswordReset{}, %{}) + def create(conn, %{"password_reset" => %{"email" => email}}) do + case PasswordResets.get_password_reset!(email: email) do + %PasswordReset{} = record -> + case PasswordResets.create_password_reset(record, %{email: email}) do + {:ok, _password_reset} -> + conn + |> clear_session() + |> put_flash(:info, "Check your email for the password reset link.") + |> redirect(to: Routes.session_path(conn, :new)) - conn - |> render("edit.html", changeset: changeset) - end + {:error, changeset} -> + conn + |> put_flash(:error, "Error creating password reset.") + |> render("new.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} -> - conn - |> clear_session() - |> put_flash(:info, "Password reset successfully. Please sign in with your new password.") - |> redirect(to: Routes.session_path(conn, :new)) - - {:error, changeset} -> - conn - |> put_flash(:error, "Error creating password reset.") - |> 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 + |> put_flash(:error, "User not found.") + |> render("new.html", changeset: PasswordReset.changeset()) 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 44a93411f..f79e45e7b 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, Users.Session} + alias FgHttp.Sessions use FgHttpWeb, :controller plug FgHttpWeb.Plugs.RedirectAuthenticated when action in [:new] @@ -11,42 +11,34 @@ defmodule FgHttpWeb.SessionController do # GET /sessions/new def new(conn, _params) do - changeset = Session.changeset(%Session{}) - - render(conn, "new.html", changeset: changeset) + render(conn, "new.html") end - # Sign In # POST /sessions def create(conn, %{"session" => session_params}) do case Sessions.create_session(session_params) do {:ok, session} -> conn - # Prevent session fixation |> clear_session() - |> put_session(:session_id, session.id) - |> assign(:current_session, session) + |> put_session(:user_id, session.id) + |> assign(:session, session) |> put_flash(:info, "Session created successfully") |> redirect(to: Routes.device_path(conn, :index)) {:error, changeset} -> conn + |> clear_session() + |> assign(:session, nil) |> put_flash(:error, "Error creating session.") - |> render("new.html", changeset: changeset, user_signed_in?: false) + |> render("new.html", changeset: changeset) end end - # Sign Out # DELETE /session def delete(conn, _params) do - session = conn.assigns.current_session - - case Sessions.delete_session(session) do - {:ok, _session} -> - conn - |> clear_session - |> put_flash(:info, "Signed out successfully.") - |> redirect(to: "/") - end + conn + |> clear_session() + |> put_flash(:info, "Signed out successfully.") + |> redirect(to: "/") end end diff --git a/apps/fg_http/lib/fg_http_web/controllers/user_controller.ex b/apps/fg_http/lib/fg_http_web/controllers/user_controller.ex index a3912bbf4..1892dea81 100644 --- a/apps/fg_http/lib/fg_http_web/controllers/user_controller.ex +++ b/apps/fg_http/lib/fg_http_web/controllers/user_controller.ex @@ -4,7 +4,7 @@ defmodule FgHttpWeb.UserController do """ use FgHttpWeb, :controller - alias FgHttp.{Users, Users.User} + alias FgHttp.{Users, Users.Session, Users.User} plug FgHttpWeb.Plugs.SessionLoader when action in [:show, :edit, :update, :delete] @@ -19,7 +19,8 @@ defmodule FgHttpWeb.UserController do case Users.create_user(user_params) do {:ok, user} -> conn - |> assign(:current_user, user) + |> put_session(:user_id, user.id) + |> assign(:session, %Session{id: user.id, email: user.email}) |> put_flash(:info, "User created successfully.") |> redirect(to: Routes.device_path(conn, :index)) diff --git a/apps/fg_http/lib/fg_http_web/plugs/redirect_authenticated.ex b/apps/fg_http/lib/fg_http_web/plugs/redirect_authenticated.ex index ff99a1aba..433838ee7 100644 --- a/apps/fg_http/lib/fg_http_web/plugs/redirect_authenticated.ex +++ b/apps/fg_http/lib/fg_http_web/plugs/redirect_authenticated.ex @@ -9,13 +9,13 @@ defmodule FgHttpWeb.Plugs.RedirectAuthenticated do def init(default), do: default def call(conn, _default) do - if get_session(conn, :session_id) do + if get_session(conn, :email) do conn |> redirect(to: "/") |> halt() else conn - |> assign(:user_signed_in?, false) + |> assign(:session, nil) end end end diff --git a/apps/fg_http/lib/fg_http_web/plugs/session_loader.ex b/apps/fg_http/lib/fg_http_web/plugs/session_loader.ex index b6966a2e5..23e2481ae 100644 --- a/apps/fg_http/lib/fg_http_web/plugs/session_loader.ex +++ b/apps/fg_http/lib/fg_http_web/plugs/session_loader.ex @@ -4,24 +4,37 @@ defmodule FgHttpWeb.Plugs.SessionLoader do """ import Plug.Conn - import Phoenix.Controller, only: [redirect: 2] - alias FgHttp.Sessions + import Phoenix.Controller, only: [put_flash: 3, redirect: 2] + alias FgHttp.{Sessions, Users.Session} alias FgHttpWeb.Router.Helpers, as: Routes def init(default), do: default - def call(conn, _default) do - case Sessions.load_session(get_session(conn, :session_id)) do - {:ok, {session, user}} -> - conn - |> assign(:current_session, session) - |> assign(:current_user, user) - |> assign(:user_signed_in?, true) + # Don't load an already loaded session + def call(%Plug.Conn{assigns: %{session: %Session{}}} = conn, _default), do: conn - {:error, _} -> - conn - |> redirect(to: Routes.session_path(conn, :new)) - |> halt + def call(conn, _default) do + case get_session(conn, :user_id) do + nil -> + unauthed(conn) + + user_id -> + case Sessions.get_session!(user_id) do + %Session{} = session -> + conn + |> assign(:session, session) + + _ -> + unauthed(conn) + end end end + + defp unauthed(conn) do + conn + |> clear_session() + |> put_flash(:error, "There was an error loading your session. Please sign in again") + |> redirect(to: Routes.session_path(conn, :new)) + |> halt() + end end diff --git a/apps/fg_http/lib/fg_http_web/templates/device/new.html.eex b/apps/fg_http/lib/fg_http_web/templates/device/new.html.eex index a2366b5bb..fe3fea83a 100644 --- a/apps/fg_http/lib/fg_http_web/templates/device/new.html.eex +++ b/apps/fg_http/lib/fg_http_web/templates/device/new.html.eex @@ -6,7 +6,7 @@ <%= aggregated_errors(@changeset) %> <% end %> -<%= live_render(@conn, FgHttpWeb.NewDeviceLive, session: %{"current_user_id" => @conn.assigns.current_user.id}) %> +<%= live_render(@conn, FgHttpWeb.NewDeviceLive, session: %{"current_user_id" => @session.id}) %>

<%= link "Back", to: Routes.device_path(@conn, :index) %> diff --git a/apps/fg_http/lib/fg_http_web/templates/layout/app.html.eex b/apps/fg_http/lib/fg_http_web/templates/layout/app.html.eex index 570eb2794..a2aa2d9cf 100644 --- a/apps/fg_http/lib/fg_http_web/templates/layout/app.html.eex +++ b/apps/fg_http/lib/fg_http_web/templates/layout/app.html.eex @@ -15,9 +15,9 @@ <%= link("Fireguard", to: FgHttpWeb.Endpoint.url(), class: "link dim white dib mr3") %>