diff --git a/apps/fg_http/assets/css/email.css b/apps/fg_http/assets/css/email.css
new file mode 100644
index 000000000..8ca64d58d
--- /dev/null
+++ b/apps/fg_http/assets/css/email.css
@@ -0,0 +1 @@
+/* Email styles */
diff --git a/apps/fg_http/lib/fg_http/devices/device.ex b/apps/fg_http/lib/fg_http/devices/device.ex
index 178e64445..1e93550ed 100644
--- a/apps/fg_http/lib/fg_http/devices/device.ex
+++ b/apps/fg_http/lib/fg_http/devices/device.ex
@@ -17,7 +17,7 @@ defmodule FgHttp.Devices.Device do
has_many :rules, Rule
belongs_to :user, User
- timestamps()
+ timestamps(type: :utc_datetime_usec)
end
@doc false
diff --git a/apps/fg_http/lib/fg_http/email.ex b/apps/fg_http/lib/fg_http/email.ex
new file mode 100644
index 000000000..c2f93be9d
--- /dev/null
+++ b/apps/fg_http/lib/fg_http/email.ex
@@ -0,0 +1,24 @@
+defmodule FgHttp.Email do
+ @moduledoc """
+ Handles Email for the app
+ """
+
+ use Bamboo.Phoenix, view: FgHttpWeb.EmailView
+ alias FgHttp.Users.PasswordReset
+
+ @from "noreply@#{Application.get_env(:fg_http, FgHttpWeb.Endpoint)[:url][:host]}"
+
+ defp base_email(to) do
+ new_email()
+ |> put_html_layout({FgHttpWeb.LayoutView, "email.html"})
+ |> from(@from)
+ |> to(to)
+ end
+
+ def password_reset(%PasswordReset{} = password_reset) do
+ base_email(password_reset.email)
+ |> subject("FireGuard password reset")
+ |> assign(:reset_token, password_reset.reset_token)
+ |> render(:password_reset)
+ end
+end
diff --git a/apps/fg_http/lib/fg_http/mailer.ex b/apps/fg_http/lib/fg_http/mailer.ex
new file mode 100644
index 000000000..428038dde
--- /dev/null
+++ b/apps/fg_http/lib/fg_http/mailer.ex
@@ -0,0 +1,7 @@
+defmodule FgHttp.Mailer do
+ @moduledoc """
+ Mailer for the application
+ """
+
+ use Bamboo.Mailer, otp_app: :fg_http
+end
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..eab8dddae
--- /dev/null
+++ b/apps/fg_http/lib/fg_http/password_resets.ex
@@ -0,0 +1,42 @@
+defmodule FgHttp.PasswordResets do
+ @moduledoc """
+ The PasswordResets context.
+ """
+
+ import Ecto.Query, warn: false
+ alias FgHttp.Repo
+
+ alias FgHttp.Users.PasswordReset
+
+ def get_password_reset!(email: email) do
+ Repo.get_by(
+ PasswordReset,
+ email: email
+ )
+ end
+
+ def get_password_reset!(reset_token: reset_token) do
+ validity_secs = -1 * PasswordReset.token_validity_secs()
+ now = DateTime.utc_now()
+
+ query =
+ from p in PasswordReset,
+ where:
+ p.reset_token == ^reset_token and
+ p.reset_sent_at > datetime_add(^now, ^validity_secs, "second")
+
+ Repo.one(query)
+ end
+
+ def create_password_reset(%PasswordReset{} = record, attrs) do
+ record
+ |> PasswordReset.create_changeset(attrs)
+ |> Repo.update()
+ end
+
+ def update_password_reset(%PasswordReset{} = record, attrs) do
+ record
+ |> PasswordReset.update_changeset(attrs)
+ |> Repo.update()
+ end
+end
diff --git a/apps/fg_http/lib/fg_http/rules/rule.ex b/apps/fg_http/lib/fg_http/rules/rule.ex
index f5b7fe1c0..6be9a535e 100644
--- a/apps/fg_http/lib/fg_http/rules/rule.ex
+++ b/apps/fg_http/lib/fg_http/rules/rule.ex
@@ -18,7 +18,7 @@ defmodule FgHttp.Rules.Rule do
belongs_to :device, Device
- timestamps()
+ timestamps(type: :utc_datetime_usec)
end
@doc false
diff --git a/apps/fg_http/lib/fg_http/sessions.ex b/apps/fg_http/lib/fg_http/sessions.ex
index de3cb808d..0f34da247 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.{Sessions.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{}
- |> 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
+ def create_session(%Session{} = session, %{} = attrs) do
session
- |> Session.changeset(attrs)
+ |> Session.create_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/sessions/session.ex b/apps/fg_http/lib/fg_http/sessions/session.ex
deleted file mode 100644
index bc191d2a8..000000000
--- a/apps/fg_http/lib/fg_http/sessions/session.ex
+++ /dev/null
@@ -1,54 +0,0 @@
-defmodule FgHttp.Sessions.Session do
- @moduledoc """
- Represents a Session
- """
-
- use Ecto.Schema
- import Ecto.Changeset
-
- 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([])
- end
-
- def create_changeset(session, attrs \\ %{}) do
- session
- |> cast(attrs, [:user_email, :user_password])
- |> validate_required([:user_email, :user_password])
- |> authenticate_user()
- end
-
- defp authenticate_user(
- %Ecto.Changeset{
- valid?: true,
- changes: %{user_email: email, user_password: password}
- } = changeset
- ) do
- user = Users.get_user!(email: email)
-
- case User.authenticate_user(user, password) do
- {:ok, _} ->
- change(changeset, user_id: user.id)
-
- {:error, error_msg} ->
- raise("There was an issue with your password: #{error_msg}")
- end
- end
-
- defp authenticate_user(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/password_helpers.ex b/apps/fg_http/lib/fg_http/users/password_helpers.ex
new file mode 100644
index 000000000..688907dab
--- /dev/null
+++ b/apps/fg_http/lib/fg_http/users/password_helpers.ex
@@ -0,0 +1,38 @@
+defmodule FgHttp.Users.PasswordHelpers do
+ @moduledoc """
+ Helpers for validating changesets with passwords
+ """
+
+ import Ecto.Changeset
+
+ def 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
+
+ def validate_password_equality(changeset), do: changeset
+
+ def put_password_hash(
+ %Ecto.Changeset{
+ valid?: true,
+ changes: %{password: password}
+ } = changeset
+ ) do
+ changeset
+ |> change(password_hash: Argon2.hash_pwd_salt(password))
+ |> delete_change(:password)
+ |> delete_change(:password_confirmation)
+ end
+
+ def put_password_hash(changeset) do
+ changeset
+ |> delete_change(:password)
+ |> delete_change(:password_confirmation)
+ 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
new file mode 100644
index 000000000..4444648a3
--- /dev/null
+++ b/apps/fg_http/lib/fg_http/users/password_reset.ex
@@ -0,0 +1,92 @@
+defmodule FgHttp.Users.PasswordReset do
+ @moduledoc """
+ Schema for PasswordReset
+ """
+
+ use Ecto.Schema
+ import Ecto.Changeset
+ import FgHttp.Users.PasswordHelpers
+
+ @token_num_bytes 8
+ # 1 day
+ @token_validity_secs 86_400
+
+ schema "users" do
+ field :reset_sent_at, :utc_datetime_usec
+ field :password_hash, :string
+ field :password, :string, virtual: true
+ field :password_confirmation, :string, virtual: true
+ field :reset_token, :string
+ field :email, :string
+ end
+
+ @doc false
+ def changeset do
+ %__MODULE__{}
+ |> cast(%{}, [:password, :password_confirmation, :reset_token])
+ end
+
+ @doc false
+ def changeset(%__MODULE__{} = password_reset, attrs \\ %{}) do
+ password_reset
+ |> cast(attrs, [:password, :password_confirmation, :reset_token])
+ end
+
+ @doc false
+ def create_changeset(%__MODULE__{} = password_reset, attrs) do
+ password_reset
+ |> cast(attrs, [:email, :reset_sent_at, :reset_token])
+ |> validate_required([:email])
+ |> generate_reset_token()
+ |> validate_required([:reset_token])
+ |> unique_constraint(:reset_token)
+ |> set_reset_sent_at()
+ |> validate_required([:reset_sent_at])
+ end
+
+ @doc false
+ def update_changeset(%__MODULE__{} = password_reset, attrs) do
+ password_reset
+ |> cast(attrs, [
+ :password_hash,
+ :password,
+ :password_confirmation,
+ :reset_token,
+ :reset_sent_at
+ ])
+ |> validate_required([:password, :password_confirmation])
+ |> validate_password_equality()
+ |> put_password_hash()
+ |> validate_required([:password_hash])
+ |> clear_token_fields()
+ end
+
+ def token_validity_secs, do: @token_validity_secs
+
+ 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
+
+ defp clear_token_fields(
+ %Ecto.Changeset{
+ valid?: true
+ } = changeset
+ ) do
+ changeset
+ |> put_change(:reset_token, nil)
+ |> put_change(:reset_sent_at, nil)
+ end
+
+ defp clear_token_fields(changeset), do: changeset
+
+ defp set_reset_sent_at(%Ecto.Changeset{valid?: true} = changeset) do
+ changeset
+ |> put_change(:reset_sent_at, DateTime.utc_now())
+ end
+
+ defp set_reset_sent_at(changeset), do: changeset
+end
diff --git a/apps/fg_http/lib/fg_http/users/session.ex b/apps/fg_http/lib/fg_http/users/session.ex
new file mode 100644
index 000000000..c44ef2021
--- /dev/null
+++ b/apps/fg_http/lib/fg_http/users/session.ex
@@ -0,0 +1,53 @@
+defmodule FgHttp.Users.Session do
+ @moduledoc """
+ Represents a Session
+ """
+
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ alias FgHttp.{Users, Users.User}
+
+ schema "users" do
+ field :email, :string
+ field :password, :string, virtual: true
+ field :last_signed_in_at, :utc_datetime_usec
+ end
+
+ def create_changeset(session, attrs \\ %{}) do
+ session
+ |> 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.utc_now()
+ 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: %{email: email, password: password}
+ } = changeset
+ ) do
+ user = Users.get_user!(email: email)
+
+ case User.authenticate_user(user, password) do
+ {:ok, _} ->
+ # 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: 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 b01f6f987..b8b3dcf25 100644
--- a/apps/fg_http/lib/fg_http/users/user.ex
+++ b/apps/fg_http/lib/fg_http/users/user.ex
@@ -5,15 +5,14 @@ defmodule FgHttp.Users.User do
use Ecto.Schema
import Ecto.Changeset
+ import FgHttp.Users.PasswordHelpers
- alias FgHttp.{Devices.Device, Sessions.Session}
+ alias FgHttp.Devices.Device
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 :confirmed_at, :utc_datetime_usec
+ field :last_signed_in_at, :utc_datetime_usec
field :password_hash, :string
# VIRTUAL FIELDS
@@ -22,9 +21,8 @@ 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()
+ timestamps(type: :utc_datetime_usec)
end
@doc false
@@ -37,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,
%{
@@ -52,6 +50,25 @@ defmodule FgHttp.Users.User do
|> cast(attrs, [:email, :password, :password_confirmation, :current_password])
|> verify_current_password(attrs[:current_password])
|> validate_required([:password, :password_confirmation, :current_password])
+ |> validate_password_equality()
+ |> put_password_hash()
+ |> 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])
+ |> validate_password_equality()
|> put_password_hash()
|> validate_required([:password_hash])
end
@@ -75,15 +92,4 @@ defmodule FgHttp.Users.User do
{:ok, user} = authenticate_user(user, current_password)
user
end
-
- defp put_password_hash(
- %Ecto.Changeset{
- valid?: true,
- changes: %{password: password}
- } = changeset
- ) do
- change(changeset, password_hash: Argon2.hash_pwd_salt(password))
- end
-
- defp put_password_hash(changeset), do: changeset
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 f5531b7db..3d2013c74 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,36 +4,74 @@ defmodule FgHttpWeb.PasswordResetController do
"""
use FgHttpWeb, :controller
- alias FgHttp.{Users, Users.User}
+ alias FgHttp.{Email, Mailer, PasswordResets, Users.PasswordReset}
plug FgHttpWeb.Plugs.RedirectAuthenticated
+ def edit(conn, %{"reset_token" => reset_token} = params) do
+ password_reset = PasswordResets.get_password_reset!(reset_token: reset_token)
+
+ conn
+ |> render(
+ "edit.html",
+ changeset: PasswordReset.changeset(password_reset, params),
+ password_reset: password_reset
+ )
+ end
+
def new(conn, _params) do
conn
- |> render("new.html", changeset: User.changeset(%User{}))
+ |> render("new.html", changeset: PasswordReset.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)
+ def update(conn, %{"password_reset" => %{"reset_token" => reset_token} = update_params}) do
+ case PasswordResets.get_password_reset!(reset_token: reset_token) do
+ %PasswordReset{} = password_reset ->
+ case PasswordResets.update_password_reset(password_reset, update_params) do
+ {:ok, _password_reset} ->
+ conn
+ |> put_flash(:info, "Password reset successfully. You may now sign in.")
+ |> redirect(to: Routes.session_path(conn, :new))
- case Users.update_user(user, user_params) do
- {:ok, _user} ->
- conn
- |> render("success.html")
+ {:error, changeset} ->
+ conn
+ |> put_flash(:error, "Error updating password.")
+ |> render("edit.html", changeset: changeset, password_reset: password_reset)
+ end
- {:error, changeset} ->
+ nil ->
conn
- |> render("new.html", changeset: changeset)
+ |> put_flash(:error, "Reset token invalid. Try resetting your password again.")
+ |> render("new.html", changeset: PasswordReset.changeset())
end
end
+
+ 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} ->
+ send_email(password_reset)
+
+ conn
+ |> put_flash(:info, "Check your email for the password reset link.")
+ |> redirect(to: Routes.session_path(conn, :new))
+
+ {:error, changeset} ->
+ conn
+ |> put_flash(:error, "Error creating password reset.")
+ |> render("new.html", changeset: changeset)
+ end
+
+ nil ->
+ conn
+ |> put_flash(:error, "Email not found.")
+ |> render("new.html", changeset: PasswordReset.changeset())
+ end
+ end
+
+ defp send_email(password_reset) do
+ Email.password_reset(password_reset)
+ |> Mailer.deliver_later()
+ 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..5b42e7e65 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]
@@ -11,42 +11,42 @@ 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_flash(:info, "Session created successfully")
- |> redirect(to: Routes.device_path(conn, :index))
+ def create(conn, %{"session" => %{"email" => email} = session_params}) do
+ case Sessions.get_session!(email: email) do
+ %Session{} = session ->
+ case Sessions.create_session(session, session_params) do
+ {:ok, session} ->
+ conn
+ |> clear_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} ->
+ {:error, changeset} ->
+ conn
+ |> clear_session()
+ |> assign(:session, nil)
+ |> put_flash(:error, "Error creating session.")
+ |> render("new.html", changeset: changeset)
+ end
+
+ nil ->
conn
- |> put_flash(:error, "Error creating session.")
- |> render("new.html", changeset: changeset, user_signed_in?: false)
+ |> put_flash(:error, "Email not found.")
+ |> render("new.html")
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..0c1a66857 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,36 @@ 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
+ |> put_flash(:error, "Please sign in to access that page.")
+ |> redirect(to: Routes.session_path(conn, :new))
+ |> halt()
+ 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..ff9841bb3 100644
--- a/apps/fg_http/lib/fg_http_web/router.ex
+++ b/apps/fg_http/lib/fg_http_web/router.ex
@@ -5,6 +5,11 @@ defmodule FgHttpWeb.Router do
use FgHttpWeb, :router
+ # View emails locally in development
+ if Mix.env() == :dev do
+ forward "/sent_emails", Bamboo.SentEmailViewerPlug
+ end
+
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
@@ -20,6 +25,9 @@ defmodule FgHttpWeb.Router do
scope "/", FgHttpWeb do
pipe_through :browser
+ resources "/password_resets", PasswordResetController, only: [:update, :new, :create]
+ get "/password_resets/:reset_token", PasswordResetController, :edit
+
resources "/user", UserController, singleton: true, only: [:show, :edit, :update, :delete]
resources "/users", UserController, only: [:new, :create]
@@ -31,7 +39,7 @@ defmodule FgHttpWeb.Router do
resources "/sessions", SessionController, only: [:new, :create, :delete]
- get "/", DeviceController, :index
+ get "/", SessionController, :new
end
# Other scopes may use custom stacks.
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/email/password_reset.html.eex b/apps/fg_http/lib/fg_http_web/templates/email/password_reset.html.eex
new file mode 100644
index 000000000..cd028a7f6
--- /dev/null
+++ b/apps/fg_http/lib/fg_http_web/templates/email/password_reset.html.eex
@@ -0,0 +1,5 @@
+<%= link(
+ "Click here to reset your password.",
+ to: Routes.password_reset_url(FgHttpWeb.Endpoint, :edit, @reset_token)
+ )
+%>
diff --git a/apps/fg_http/lib/fg_http_web/templates/email/password_reset.text.eex b/apps/fg_http/lib/fg_http_web/templates/email/password_reset.text.eex
new file mode 100644
index 000000000..48eee5c2b
--- /dev/null
+++ b/apps/fg_http/lib/fg_http_web/templates/email/password_reset.text.eex
@@ -0,0 +1,3 @@
+Copy and paste the following URL into your browser to reset your password:
+
+<%= Routes.password_reset_url(FgHttpWeb.Endpoint, :edit, @reset_token) %>
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..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
@@ -14,16 +14,17 @@
<%= link("Fireguard", to: FgHttpWeb.Endpoint.url(), class: "link dim white dib mr3") %>
- <%= if @user_signed_in? do %>
-
+
+ <%= if assigns[:session] 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") %>
-
- <% else %>
-
- <% end %>
+ <%= link("Sign out", to: Routes.session_path(@conn, :delete, @session), method: :delete, class: "link dim white dib mr3") %>
+ <% else %>
+ <%= link("Sign in", to: Routes.session_path(@conn, :new), class: "link dim white dib mr3") %>
+ <% end %>
+
+ <%= render_flash(@conn) %>
<%= @inner_content %>
diff --git a/apps/fg_http/lib/fg_http_web/templates/layout/email.html.eex b/apps/fg_http/lib/fg_http_web/templates/layout/email.html.eex
new file mode 100644
index 000000000..0fee29ac1
--- /dev/null
+++ b/apps/fg_http/lib/fg_http_web/templates/layout/email.html.eex
@@ -0,0 +1,8 @@
+
+
+ ">
+
+
+ <%= @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..47dd241a0
--- /dev/null
+++ b/apps/fg_http/lib/fg_http_web/templates/password_reset/edit.html.eex
@@ -0,0 +1,27 @@
+Edit Password
+
+<%= form_for @changeset, Routes.password_reset_path(@conn, :update, @password_reset.id), fn f -> %>
+ <%= hidden_input f, :reset_token %>
+
+ <%= if @changeset.action do %>
+
+
Oops, something went wrong! Please check the errors below.
+
+ <% end %>
+
+ <%= label f, :password %>
+ <%= text_input f, :password %>
+ <%= error_tag f, :password %>
+
+ <%= label f, :password_confirmation %>
+ <%= text_input f, :password_confirmation %>
+ <%= error_tag f, :password_confirmation %>
+
+
+ <%= submit "Submit" %>
+
+<% end %>
+
+
+ <%= link "Back", to: Routes.session_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..220f12cd1
--- /dev/null
+++ b/apps/fg_http/lib/fg_http_web/templates/password_reset/new.html.eex
@@ -0,0 +1,19 @@
+Reset Password
+
+<%= 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, :email %>
+ <%= text_input f, :email %>
+ <%= error_tag f, :email %>
+
+
+ <%= submit "Submit" %>
+
+<% end %>
+
+<%= link "Back", to: Routes.session_path(@conn, :new) %>
diff --git a/apps/fg_http/lib/fg_http_web/templates/session/new.html.eex b/apps/fg_http/lib/fg_http_web/templates/session/new.html.eex
index 4a84e3776..0dc5d9030 100644
--- a/apps/fg_http/lib/fg_http_web/templates/session/new.html.eex
+++ b/apps/fg_http/lib/fg_http_web/templates/session/new.html.eex
@@ -1,5 +1,5 @@
- <%= form_for(@changeset, Routes.session_path(@conn, :create), [class: "measure center"], fn f -> %>
+ <%= form_for(@conn, Routes.session_path(@conn, :create), [as: :session, class: "measure center"], fn f -> %>
<%= if f.errors do %>
<% end %>
@@ -8,11 +8,11 @@
Sign In
<%= label(:session_user, :email, class: "db fw6 lh-copy f6") %>
- <%= text_input(f, :user_email, class: "pa2 input-reset ba bg-transparent hover-bg-black hover-white w-100") %>
+ <%= text_input(f, :email, class: "pa2 input-reset ba bg-transparent hover-bg-black hover-white w-100") %>
<%= label(:session_user, :password, class: "db fw6 lh-copy f6") %>
- <%= password_input(f, :user_password, class: "pa2 input-reset ba bg-transparent hover-bg-black hover-white w-100") %>
+ <%= password_input(f, :password, class: "pa2 input-reset ba bg-transparent hover-bg-black hover-white w-100") %>
Remember me
@@ -20,8 +20,8 @@
<%= submit "Sign in", class: "b ph3 pv2 input-reset ba b--black bg-transparent grow pointer f6 dib" %>
-
Sign up
-
Forgot your password?
+ <%= link("Sign up", to: Routes.user_path(@conn, :new), class: "f6 link dim black fl") %>
+ <%= link("Forgot your password?", to: Routes.password_reset_path(@conn, :new), class: "f6 link dim black fr") %>
<% end) %>
diff --git a/apps/fg_http/lib/fg_http_web/views/email_view.ex b/apps/fg_http/lib/fg_http_web/views/email_view.ex
new file mode 100644
index 000000000..b49856b3b
--- /dev/null
+++ b/apps/fg_http/lib/fg_http_web/views/email_view.ex
@@ -0,0 +1,3 @@
+defmodule FgHttpWeb.EmailView do
+ use FgHttpWeb, :view
+end
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..fe4d8cbc6 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, :info) do %>
+
+ <%= get_flash(conn, :info) %>
+
+ <% 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..128623ff3 100644
--- a/apps/fg_http/priv/repo/migrations/20200225005454_create_users.exs
+++ b/apps/fg_http/priv/repo/migrations/20200225005454_create_users.exs
@@ -4,16 +4,17 @@ defmodule FgHttp.Repo.Migrations.CreateUsers do
def change do
create table(:users) do
add :email, :string
- add :confirmed_at, :utc_datetime
+ add :confirmed_at, :utc_datetime_usec
add :password_hash, :string
- add :last_signed_in_at, :utc_datetime
- add :reset_sent_at, :utc_datetime
- add :reset_token, :utc_datetime
+ add :last_signed_in_at, :utc_datetime_usec
+ add :reset_token, :string
+ add :reset_sent_at, :utc_datetime_usec
- timestamps()
+ timestamps(type: :utc_datetime_usec)
end
create unique_index(:users, [:email])
- create unique_index(:users, [:reset_token])
+ create index(:users, [:reset_token, :reset_sent_at])
+ create index(:users, [:confirmed_at])
end
end
diff --git a/apps/fg_http/priv/repo/migrations/20200228145810_create_devices.exs b/apps/fg_http/priv/repo/migrations/20200228145810_create_devices.exs
index 4dbff63c9..d1c8d6b8e 100644
--- a/apps/fg_http/priv/repo/migrations/20200228145810_create_devices.exs
+++ b/apps/fg_http/priv/repo/migrations/20200228145810_create_devices.exs
@@ -9,7 +9,7 @@ defmodule FgHttp.Repo.Migrations.CreateDevices do
add :last_ip, :inet
add :user_id, references(:users, on_delete: :delete_all), null: false
- timestamps()
+ timestamps(type: :utc_datetime_usec)
end
create index(:devices, [:user_id])
diff --git a/apps/fg_http/priv/repo/migrations/20200228154815_create_rules.exs b/apps/fg_http/priv/repo/migrations/20200228154815_create_rules.exs
index eda009fb9..6ecb22209 100644
--- a/apps/fg_http/priv/repo/migrations/20200228154815_create_rules.exs
+++ b/apps/fg_http/priv/repo/migrations/20200228154815_create_rules.exs
@@ -14,7 +14,7 @@ defmodule FgHttp.Repo.Migrations.CreateRules do
add :port, :string
add :device_id, references(:devices, on_delete: :delete_all), null: false
- timestamps()
+ timestamps(type: :utc_datetime_usec)
end
create index(:rules, [:device_id])
diff --git a/apps/fg_http/priv/repo/migrations/20200510162435_create_sessions.exs b/apps/fg_http/priv/repo/migrations/20200510162435_create_sessions.exs
deleted file mode 100644
index f1abef545..000000000
--- a/apps/fg_http/priv/repo/migrations/20200510162435_create_sessions.exs
+++ /dev/null
@@ -1,15 +0,0 @@
-defmodule FgHttp.Repo.Migrations.CreateSessions do
- use Ecto.Migration
-
- def change do
- create table(:sessions) do
- add :user_id, references(:users, on_delete: :delete_all), null: false
- add :deleted_at, :utc_datetime
-
- timestamps()
- end
-
- create index(:sessions, [:user_id])
- create index(:sessions, [:deleted_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
new file mode 100644
index 000000000..f911aa43e
--- /dev/null
+++ b/apps/fg_http/test/fg_http/password_resets_test.exs
@@ -0,0 +1,39 @@
+defmodule FgHttp.PasswordResetsTest do
+ use FgHttp.DataCase
+
+ alias FgHttp.{Fixtures, PasswordResets}
+
+ describe "password_resets" do
+ alias FgHttp.Users.PasswordReset
+
+ @valid_attrs %{email: "test"}
+ @invalid_attrs %{email: ""}
+
+ test "get_password_reset!/1 returns the password_reset with given token" do
+ token = Fixtures.password_reset(%{reset_sent_at: DateTime.utc_now()}).reset_token
+ gotten = PasswordResets.get_password_reset!(reset_token: token)
+ assert gotten.reset_token == token
+ end
+
+ test "create_password_reset/1 with valid data creates a password_reset" do
+ email = Fixtures.user().email
+
+ assert {:ok, %PasswordReset{} = password_reset} =
+ PasswordResets.create_password_reset(Fixtures.password_reset(), @valid_attrs)
+
+ # reset_sent_at should be nil after creation
+ assert !is_nil(password_reset.reset_sent_at)
+
+ assert password_reset.reset_token
+ assert password_reset.email == email
+ end
+
+ test "create_password_reset/1 with invalid data returns error changeset" do
+ assert {:error, %Ecto.Changeset{}} =
+ PasswordResets.create_password_reset(
+ Fixtures.password_reset(),
+ @invalid_attrs
+ )
+ 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..6fa1d658f 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"}
@@ -46,11 +46,11 @@ defmodule FgHttpWeb.DeviceControllerTest do
setup [:create_device]
test "redirects when data is valid", %{authed_conn: conn, device: device} do
- conn = put(conn, Routes.device_path(conn, :update, device), device: @update_attrs)
- assert redirected_to(conn) == Routes.device_path(conn, :show, device)
+ test_conn = put(conn, Routes.device_path(conn, :update, device), device: @update_attrs)
+ assert redirected_to(test_conn) == Routes.device_path(conn, :show, device)
- conn = get(conn, Routes.device_path(conn, :show, device))
- assert html_response(conn, 200) =~ "some updated name"
+ test_conn = get(conn, Routes.device_path(conn, :show, device))
+ assert html_response(test_conn, 200) =~ "some updated name"
end
test "renders errors when data is invalid", %{authed_conn: conn, device: device} do
@@ -63,8 +63,8 @@ defmodule FgHttpWeb.DeviceControllerTest do
setup [:create_device]
test "deletes chosen device", %{authed_conn: conn, device: device} do
- conn = delete(conn, Routes.device_path(conn, :delete, device))
- assert redirected_to(conn) == Routes.device_path(conn, :index)
+ test_conn = delete(conn, Routes.device_path(conn, :delete, device))
+ assert redirected_to(test_conn) == Routes.device_path(conn, :index)
assert_error_sent 404, fn ->
get(conn, Routes.device_path(conn, :show, device))
@@ -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..051054a94
--- /dev/null
+++ b/apps/fg_http/test/fg_http_web/controllers/password_reset_controller_test.exs
@@ -0,0 +1,118 @@
+defmodule FgHttpWeb.PasswordResetControllerTest do
+ use FgHttpWeb.ConnCase, async: true
+
+ alias FgHttp.Fixtures
+
+ @valid_create_attrs %{email: "test"}
+ @invalid_create_attrs %{email: "doesnt-exist"}
+
+ 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) =~ "Reset Password"
+ 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: @valid_create_attrs)
+
+ assert redirected_to(conn) == Routes.session_path(conn, :new)
+ assert get_flash(conn, :info) == "Check your email for the password reset link."
+ end
+
+ test "displays error message when data is invalid", %{unauthed_conn: conn} do
+ conn =
+ post(conn, Routes.password_reset_path(conn, :create),
+ password_reset: @invalid_create_attrs
+ )
+
+ assert html_response(conn, 200) =~ "Reset Password"
+ assert get_flash(conn, :error) == "Email not found."
+ end
+ end
+
+ describe "edit password_reset" do
+ setup [:create_password_reset]
+
+ test "renders form", %{unauthed_conn: conn, password_reset: password_reset} do
+ conn =
+ get(
+ conn,
+ Routes.password_reset_path(conn, :edit, password_reset.reset_token)
+ )
+
+ assert html_response(conn, 200) =~ "Edit Password"
+ end
+ end
+
+ describe "update password_reset" do
+ setup [:create_password_reset]
+
+ test "redirects to sign in when the data is valid", %{
+ unauthed_conn: conn,
+ password_reset: password_reset
+ } do
+ update_params = [
+ {
+ :password_reset,
+ [
+ {:reset_token, password_reset.reset_token},
+ {:password, "new_password"},
+ {:password_confirmation, "new_password"}
+ ]
+ }
+ ]
+
+ conn =
+ patch(
+ conn,
+ Routes.password_reset_path(
+ conn,
+ :update,
+ password_reset.id,
+ update_params
+ )
+ )
+
+ assert redirected_to(conn) == Routes.session_path(conn, :new)
+ assert get_flash(conn, :info) == "Password reset successfully. You may now sign in."
+ end
+
+ test "renders errors when the data is invalid", %{
+ unauthed_conn: conn,
+ password_reset: password_reset
+ } do
+ update_params = [
+ {
+ :password_reset,
+ [
+ {:reset_token, password_reset.reset_token},
+ {:password, "new_password"},
+ {:password_confirmation, "wrong_password"}
+ ]
+ }
+ ]
+
+ conn =
+ patch(
+ conn,
+ Routes.password_reset_path(
+ conn,
+ :update,
+ password_reset.id,
+ update_params
+ )
+ )
+
+ assert get_flash(conn, :error) == "Error updating password."
+ assert html_response(conn, 200) =~ "Edit Password"
+ end
+ end
+
+ defp create_password_reset(_) do
+ password_reset = Fixtures.password_reset(%{reset_sent_at: DateTime.utc_now()})
+ {:ok, password_reset: password_reset}
+ end
+end
diff --git a/apps/fg_http/test/support/conn_case.ex b/apps/fg_http/test/support/conn_case.ex
index d2a6af700..fbe5f094d 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,19 +38,10 @@ defmodule FgHttpWeb.ConnCase do
end
def authed_conn do
- user = fixture(:user)
-
- session =
- fixture(:session, %{
- user_id: user.id,
- user_password: "test",
- user_email: "test"
- })
+ session = Fixtures.session()
new_conn()
- |> Plug.Conn.assign(:current_user, user)
- |> Plug.Conn.assign(:current_session, session)
- |> Plug.Conn.assign(:user_signed_in?, true)
+ |> Plug.Conn.assign(:session, session)
end
setup tags do
diff --git a/apps/fg_http/test/support/fixtures.ex b/apps/fg_http/test/support/fixtures.ex
index 31f5122fc..9bca64f70 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,33 @@ 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
+ email = user().email
+ record = Sessions.get_session!(email: email)
+ create_params = %{email: email, password: "test"}
+ {:ok, session} = Sessions.create_session(record, create_params)
+ session
+ end
+
+ def password_reset(attrs \\ %{}) do
+ email = user().email
+
+ create_attrs = Map.merge(attrs, %{email: email})
+
+ {:ok, password_reset} =
+ PasswordResets.get_password_reset!(email: email)
+ |> PasswordResets.create_password_reset(create_attrs)
+
+ password_reset
end
end
diff --git a/config/dev.exs b/config/dev.exs
index f4e2ff5e1..190a68bcf 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -9,6 +9,8 @@ config :fg_http, FgHttp.Repo,
show_sensitive_data_on_connection_error: true,
pool_size: 10
+config :fg_http, FgHttp.Mailer, adapter: Bamboo.LocalAdapter
+
# For development, we disable any cache and enable
# debugging and code reloading.
#
diff --git a/config/test.exs b/config/test.exs
index 619dc6a19..d16af47e6 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -23,6 +23,8 @@ end
db_url = System.get_env("DATABASE_URL")
config :fg_http, FgHttp.Repo, DBConfig.config(db_url)
+config :fg_http, FgHttp.Mailer, adapter: Bamboo.TestAdapter
+
# We don't run a server during test. If one is required,
# you can enable the server option below.
config :fg_http, FgHttpWeb.Endpoint,
diff --git a/mix.lock b/mix.lock
index a58454716..a8697d073 100644
--- a/mix.lock
+++ b/mix.lock
@@ -2,41 +2,41 @@
"argon2_elixir": {:hex, :argon2_elixir, "2.3.0", "e251bdafd69308e8c1263e111600e6d68bd44f23d2cccbe43fcb1a417a76bc8e", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "28ccb63bff213aecec1f7f3dde9648418b031f822499973281d8f494b9d5a3b3"},
"bamboo": {:hex, :bamboo, "1.5.0", "1926107d58adba6620450f254dfe8a3686637a291851fba125686fa8574842af", [:mix], [{:hackney, ">= 1.13.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d5f3d04d154e80176fd685e2531e73870d8700679f14d25a567e448abce6298d"},
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
- "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"},
+ "certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"},
"comeonin": {:hex, :comeonin, "5.3.1", "7fe612b739c78c9c1a75186ef2d322ce4d25032d119823269d0aa1e2f1e20025", [:mix], [], "hexpm", "d6222483060c17f0977fad1b7401ef0c5863c985a64352755f366aee3799c245"},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"},
- "cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "04fd8c6a39edc6aaa9c26123009200fc61f92a3a94f3178c527b70b767c6e605"},
- "cowlib": {:hex, :cowlib, "2.8.0", "fd0ff1787db84ac415b8211573e9a30a3ebe71b5cbff7f720089972b2319c8a4", [:rebar3], [], "hexpm", "79f954a7021b302186a950a32869dbc185523d99d3e44ce430cd1f3289f41ed4"},
+ "cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"},
+ "cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"},
"credo": {:hex, :credo, "1.4.0", "92339d4cbadd1e88b5ee43d427b639b68a11071b6f73854e33638e30a0ea11f5", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1fd3b70dce216574ce3c18bdf510b57e7c4c85c2ec9cad4bff854abaf7e58658"},
"db_connection": {:hex, :db_connection, "2.2.2", "3bbca41b199e1598245b716248964926303b5d4609ff065125ce98bcd368939e", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "642af240d8a8affb93b4ba5a6fcd2bbcbdc327e1a524b825d383711536f8070c"},
"decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"},
"ecto": {:hex, :ecto, "3.4.4", "a2c881e80dc756d648197ae0d936216c0308370332c5e77a2325a10293eef845", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4bd3ad62abc3b21fb629f0f7a3dab23a192fca837d257dd08449fba7373561"},
"ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"},
"ecto_network": {:hex, :ecto_network, "1.3.0", "1e77fa37c20e0f6a426d3862732f3317b0fa4c18f123d325f81752a491d7304e", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:phoenix_html, ">= 0.0.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.14.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "053a5e46ef2837e8ea5ea97c82fa0f5494699209eddd764e663c85f11b2865bd"},
- "ecto_sql": {:hex, :ecto_sql, "3.4.3", "c552aa8a7ccff2b64024f835503b3155d8e73452c180298527fbdbcd6e79710b", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ec9e59d6fa3f8cfda9963ada371e9e6659167c2338a997bd7ea23b10b245842b"},
+ "ecto_sql": {:hex, :ecto_sql, "3.4.4", "d28bac2d420f708993baed522054870086fd45016a9d09bb2cd521b9c48d32ea", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "edb49af715dd72f213b66adfd0f668a43c17ed510b5d9ac7528569b23af57fe8"},
"elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"},
"ex_machina": {:hex, :ex_machina, "2.4.0", "09a34c5d371bfb5f78399029194a8ff67aff340ebe8ba19040181af35315eabb", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "a20bc9ddc721b33ea913b93666c5d0bdca5cbad7a67540784ae277228832d72c"},
"file_system": {:hex, :file_system, "0.2.8", "f632bd287927a1eed2b718f22af727c5aeaccc9a98d8c2bd7bff709e851dc986", [:mix], [], "hexpm", "97a3b6f8d63ef53bd0113070102db2ce05352ecf0d25390eb8d747c2bde98bca"},
"gettext": {:hex, :gettext, "0.18.0", "406d6b9e0e3278162c2ae1de0a60270452c553536772167e2d701f028116f870", [:mix], [], "hexpm", "c3f850be6367ebe1a08616c2158affe4a23231c70391050bf359d5f92f66a571"},
- "hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "e0100f8ef7d1124222c11ad362c857d3df7cb5f4204054f9f0f4a728666591fc"},
- "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"},
+ "hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"},
+ "idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"},
"jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
- "phoenix": {:hex, :phoenix, "1.5.1", "95156589879dc69201d5fc0ebdbfdfc7901a09a3616ea611ec297f81340275a2", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc272b38e79d2881790fccae6f67a9fbe9b790103d6878175ea03d23003152eb"},
+ "phoenix": {:hex, :phoenix, "1.5.3", "bfe0404e48ea03dfe17f141eff34e1e058a23f15f109885bbdcf62be303b49ff", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8e16febeb9640d8b33895a691a56481464b82836d338bb3a23125cd7b6157c25"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.1.0", "a044d0756d0464c5a541b4a0bf4bcaf89bffcaf92468862408290682c73ae50d", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "c5e666a341ff104d0399d8f0e4ff094559b2fde13a5985d4cb5023b2c2ac558b"},
"phoenix_html": {:hex, :phoenix_html, "2.14.2", "b8a3899a72050f3f48a36430da507dd99caf0ac2d06c77529b1646964f3d563e", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "58061c8dfd25da5df1ea0ca47c972f161beb6c875cd293917045b92ffe1bf617"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.2.2", "38d94c30df5e2ef11000697a4fbe2b38d0fbf79239d492ff1be87bbc33bc3a84", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "a3dec3d28ddb5476c96a7c8a38ea8437923408bc88da43e5c45d97037b396280"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.12.1", "42f591c781edbf9fab921319076b7ac635d43aa23e6748d2644563326236d7e4", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.4.16 or ~> 1.5.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}], "hexpm", "585321e98df1cd5943e370b9784e950a37ca073744eb534660c9048967c52ab6"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
- "plug": {:hex, :plug, "1.10.0", "6508295cbeb4c654860845fb95260737e4a8838d34d115ad76cd487584e2fc4d", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "422a9727e667be1bf5ab1de03be6fa0ad67b775b2d84ed908f3264415ef29d4a"},
- "plug_cowboy": {:hex, :plug_cowboy, "2.2.1", "fcf58aa33227a4322a050e4783ee99c63c031a2e7f9a2eb7340d55505e17f30f", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3b43de24460d87c0971887286e7a20d40462e48eb7235954681a20cee25ddeb6"},
+ "plug": {:hex, :plug, "1.10.1", "c56a6d9da7042d581159bcbaef873ba9d87f15dce85420b0d287bca19f40f9bd", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "b5cd52259817eb8a31f2454912ba1cff4990bca7811918878091cb2ab9e52cb8"},
+ "plug_cowboy": {:hex, :plug_cowboy, "2.2.2", "7a09aa5d10e79b92d332a288f21cc49406b1b994cbda0fde76160e7f4cc890ea", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e82364b29311dbad3753d588febd7e5ef05062cd6697d8c231e0e007adab3727"},
"plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"},
"postgrex": {:hex, :postgrex, "0.15.4", "5d691c25fc79070705a2ff0e35ce0822b86a0ee3c6fdb7a4fb354623955e1aed", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "306515b9d975fcb2478dc337a1d27dc3bf8af7cd71017c333fe9db3a3d211b0a"},
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
- "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"},
+ "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"},
- "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"},
+ "unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"},
}