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

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