mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
Refactor session and pw_reset into users table
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}) %>
|
||||
|
||||
<p>
|
||||
<%= link "Back", to: Routes.device_path(@conn, :index) %>
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<%= link("Fireguard", to: FgHttpWeb.Endpoint.url(), class: "link dim white dib mr3") %>
|
||||
</nav>
|
||||
<nav class="f6 fw6 ttu tracked fr">
|
||||
<%= 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") %>
|
||||
<%= 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 %>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<h1>Change Password</h1>
|
||||
<h1>Reset Password</h1>
|
||||
|
||||
<%= form_for @changeset, Routes.password_reset_path(@conn, :create), fn f -> %>
|
||||
<%= if @changeset.action do %>
|
||||
@@ -7,16 +7,12 @@
|
||||
</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 %>
|
||||
<%= label f, :email %>
|
||||
<%= text_input f, :email %>
|
||||
<%= error_tag f, :email %>
|
||||
|
||||
<div>
|
||||
<%= submit "Save" %>
|
||||
<%= submit "Submit" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<main class="pa4 black-80">
|
||||
<%= 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 %>
|
||||
<!-- Errors -->
|
||||
<% end %>
|
||||
@@ -8,11 +8,11 @@
|
||||
<legend class="f4 fw6 ph0 mh0">Sign In</legend>
|
||||
<div class="mt3">
|
||||
<%= 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") %>
|
||||
</div>
|
||||
<div class="mv3">
|
||||
<%= 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") %>
|
||||
</div>
|
||||
<label class="pa0 ma0 lh-copy f6 pointer"><input type="checkbox"> Remember me</label>
|
||||
</fieldset>
|
||||
|
||||
@@ -7,10 +7,15 @@ defmodule FgHttp.Repo.Migrations.CreateUsers do
|
||||
add :confirmed_at, :utc_datetime
|
||||
add :password_hash, :string
|
||||
add :last_signed_in_at, :utc_datetime
|
||||
add :reset_token, :string
|
||||
add :reset_sent_at, :utc_datetime
|
||||
add :reset_consumed_at, :utc_datetime
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create unique_index(:users, [:email])
|
||||
create index(:users, [:reset_token, :reset_sent_at, :reset_consumed_at])
|
||||
create index(:users, [:confirmed_at])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
@@ -1,18 +0,0 @@
|
||||
defmodule FgHttp.Repo.Migrations.CreatePasswordResets do
|
||||
use Ecto.Migration
|
||||
|
||||
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
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create unique_index(:password_resets, [:reset_token])
|
||||
create index(:password_resets, [:user_id])
|
||||
create index(:password_resets, [:consumed_at])
|
||||
end
|
||||
end
|
||||
@@ -6,82 +6,34 @@ defmodule FgHttp.PasswordResetsTest do
|
||||
describe "password_resets" do
|
||||
alias FgHttp.Users.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}
|
||||
@valid_attrs %{email: "test"}
|
||||
@invalid_attrs %{email: ""}
|
||||
|
||||
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
|
||||
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
|
||||
user_id = Fixtures.user().id
|
||||
valid_attrs = Map.merge(@valid_attrs, %{user_id: user_id})
|
||||
email = Fixtures.user().email
|
||||
|
||||
assert {:ok, %PasswordReset{} = password_reset} =
|
||||
PasswordResets.create_password_reset(valid_attrs)
|
||||
PasswordResets.create_password_reset(Fixtures.password_reset(), @valid_attrs)
|
||||
|
||||
assert password_reset.reset_sent_at ==
|
||||
DateTime.from_naive!(~N[2010-04-17T14:00:00Z], "Etc/UTC")
|
||||
# reset_sent_at should be nil after creation
|
||||
assert is_nil(password_reset.reset_sent_at)
|
||||
|
||||
assert password_reset.reset_token
|
||||
assert password_reset.user_id == user_id
|
||||
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(@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)
|
||||
PasswordResets.create_password_reset(
|
||||
Fixtures.password_reset(),
|
||||
@invalid_attrs
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
defmodule FgHttpWeb.PasswordResetControllerTest do
|
||||
use FgHttpWeb.ConnCase, async: true
|
||||
|
||||
@create_attrs %{user_email: "test"}
|
||||
@create_attrs %{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"
|
||||
assert html_response(conn, 200) =~ "Reset Password"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -38,19 +38,10 @@ defmodule FgHttpWeb.ConnCase do
|
||||
end
|
||||
|
||||
def authed_conn do
|
||||
user = Fixtures.user()
|
||||
|
||||
session =
|
||||
Fixtures.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
|
||||
|
||||
@@ -29,14 +29,20 @@ defmodule FgHttp.Fixtures do
|
||||
device
|
||||
end
|
||||
|
||||
def session(attrs \\ %{}) do
|
||||
{:ok, session} = Sessions.create_session(attrs)
|
||||
def session(_attrs \\ %{}) do
|
||||
{:ok, session} = Sessions.create_session(%{email: user().email, password: "test"})
|
||||
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)
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user