Checkpoint

This commit is contained in:
Jamil Bou Kheir
2020-05-21 23:48:24 -07:00
parent 074702eba1
commit dc9005068e
21 changed files with 433 additions and 45 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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 """

View File

@@ -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

View File

@@ -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

View File

@@ -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]

View File

@@ -14,16 +14,17 @@
<nav class="f6 fw6 ttu tracked fl">
<%= link("Fireguard", to: FgHttpWeb.Endpoint.url(), class: "link dim white dib mr3") %>
</nav>
<%= if @user_signed_in? do %>
<nav class="f6 fw6 ttu tracked fr">
<nav class="f6 fw6 ttu tracked fr">
<%= if @user_signed_in? do %>
<%= link("Devices", to: Routes.device_path(@conn, :index), class: "link dim white dib mr3") %>
<%= link("Sign out", to: Routes.session_path(@conn, :delete, @current_session), method: :delete, class: "link dim white dib mr3") %>
</nav>
<% else %>
<% end %>
<% else %>
<%= link("Sign in", to: Routes.session_path(@conn, :new), class: "link dim white dib mr3") %>
<% end %>
</nav>
</header>
<main class="mw7 mw9-ns center pa3 pt5 ph5-ns">
<%= render_flash(@conn) %>
<%= @inner_content %>
</main>
<script type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>

View File

@@ -0,0 +1,5 @@
<h1>Edit Password reset</h1>
<%= render "form.html", Map.put(assigns, :action, Routes.password_reset_path(@conn, :update, @password_reset)) %>
<span><%= link "Back", to: Routes.password_reset_path(@conn, :index) %></span>

View File

@@ -0,0 +1,19 @@
<%= form_for @changeset, @action, fn f -> %>
<%= if @changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% 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 %>
<div>
<%= submit "Save" %>
</div>
<% end %>

View File

@@ -0,0 +1,28 @@
<h1>Listing Password resets</h1>
<table>
<thead>
<tr>
<th>Reset sent at</th>
<th>Reset token</th>
<th></th>
</tr>
</thead>
<tbody>
<%= for password_reset <- @password_resets do %>
<tr>
<td><%= password_reset.reset_sent_at %></td>
<td><%= password_reset.reset_token %></td>
<td>
<span><%= link "Show", to: Routes.password_reset_path(@conn, :show, password_reset) %></span>
<span><%= link "Edit", to: Routes.password_reset_path(@conn, :edit, password_reset) %></span>
<span><%= link "Delete", to: Routes.password_reset_path(@conn, :delete, password_reset), method: :delete, data: [confirm: "Are you sure?"] %></span>
</td>
</tr>
<% end %>
</tbody>
</table>
<span><%= link "New Password reset", to: Routes.password_reset_path(@conn, :new) %></span>

View File

@@ -0,0 +1,5 @@
<h1>New Password reset</h1>
<%= render "form.html", Map.put(assigns, :action, Routes.password_reset_path(@conn, :create)) %>
<span><%= link "Back", to: Routes.session_path(@conn, :new) %></span>

View File

@@ -0,0 +1,18 @@
<h1>Show Password reset</h1>
<ul>
<li>
<strong>Reset sent at:</strong>
<%= @password_reset.reset_sent_at %>
</li>
<li>
<strong>Reset token:</strong>
<%= @password_reset.reset_token %>
</li>
</ul>
<span><%= link "Edit", to: Routes.password_reset_path(@conn, :edit, @password_reset) %></span>
<span><%= link "Back", to: Routes.password_reset_path(@conn, :index) %></span>

View File

@@ -1,3 +1,20 @@
defmodule FgHttpWeb.LayoutView do
use FgHttpWeb, :view
def render_flash(conn) do
~E"""
<section id="flash">
<%= if get_flash(conn, :error) do %>
<div id="flash-error">
<%= get_flash(conn, :error) %>
</div>
<% end %>
<%= if get_flash(conn, :error) do %>
<div id="flash-error">
<%= get_flash(conn, :error) %>
</div>
<% end %>
</section>
"""
end
end

View File

@@ -0,0 +1,3 @@
defmodule FgHttpWeb.PasswordResetView do
use FgHttpWeb, :view
end

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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