update controllers and auth

This commit is contained in:
shibao 2022-02-25 21:53:04 -05:00 committed by oliviasculley
parent 41090c46d0
commit 74bcec6cfe
13 changed files with 256 additions and 63 deletions

View File

@ -0,0 +1,23 @@
defmodule LokalWeb.EmailController do
@moduledoc """
A dev controller used to develop on emails
"""
use LokalWeb, :controller
alias Lokal.Accounts.User
plug :put_layout, {LokalWeb.LayoutView, :email}
@sample_assigns %{
email: %{subject: "Example subject"},
url: "https://lokal.bubbletea.dev/sample_url",
user: %User{email: "sample@email.com"}
}
@doc """
Debug route used to preview emails
"""
def preview(conn, %{"id" => template}) do
render(conn, "#{template |> to_string()}.html", @sample_assigns)
end
end

View File

@ -1,12 +1,13 @@
defmodule LokalWeb.UserAuth do defmodule LokalWeb.UserAuth do
@moduledoc """ @moduledoc """
Module for any user authentication functions Functions for user session and authentication
""" """
import Plug.Conn import Plug.Conn
import Phoenix.Controller import Phoenix.Controller
import LokalWeb.Gettext import LokalWeb.Gettext
alias Lokal.{Accounts, Accounts.User} alias Lokal.{Accounts, Accounts.User}
alias LokalWeb.PageLive
alias LokalWeb.Router.Helpers, as: Routes alias LokalWeb.Router.Helpers, as: Routes
# Make the remember me cookie valid for 60 days. # Make the remember me cookie valid for 60 days.
@ -32,6 +33,7 @@ defmodule LokalWeb.UserAuth do
def log_in_user(conn, %User{confirmed_at: nil}, _params) do def log_in_user(conn, %User{confirmed_at: nil}, _params) do
conn conn
|> fetch_flash()
|> put_flash( |> put_flash(
:error, :error,
dgettext("errors", "You must confirm your account and log in to access this page.") dgettext("errors", "You must confirm your account and log in to access this page.")
@ -53,6 +55,11 @@ defmodule LokalWeb.UserAuth do
|> redirect(to: user_return_to || signed_in_path(conn)) |> redirect(to: user_return_to || signed_in_path(conn))
end end
@spec maybe_write_remember_me_cookie(
Plug.Conn.t(),
String.t() | any(),
%{required(String.t()) => String.t()} | any()
) :: Plug.Conn.t()
defp maybe_write_remember_me_cookie(conn, token, %{"remember_me" => "true"}) do defp maybe_write_remember_me_cookie(conn, token, %{"remember_me" => "true"}) do
put_resp_cookie(conn, @remember_me_cookie, token, @remember_me_options) put_resp_cookie(conn, @remember_me_cookie, token, @remember_me_options)
end end
@ -149,13 +156,31 @@ defmodule LokalWeb.UserAuth do
conn conn
else else
conn conn
|> put_flash(:error, "You must confirm your account and log in to access this page.") |> put_flash(
:error,
dgettext("errors", "You must confirm your account and log in to access this page.")
)
|> maybe_store_return_to() |> maybe_store_return_to()
|> redirect(to: Routes.user_session_path(conn, :new)) |> redirect(to: Routes.user_session_path(conn, :new))
|> halt() |> halt()
end end
end end
@doc """
Used for routes that require the user to be an admin.
"""
def require_role(conn, role: role_atom) do
if conn.assigns[:current_user] && conn.assigns.current_user.role == role_atom do
conn
else
conn
|> put_flash(:error, dgettext("errors", "You are not authorized to view this page."))
|> maybe_store_return_to()
|> redirect(to: Routes.live_path(conn, PageLive))
|> halt()
end
end
defp maybe_store_return_to(%{method: "GET"} = conn) do defp maybe_store_return_to(%{method: "GET"} = conn) do
put_session(conn, :user_return_to, current_path(conn)) put_session(conn, :user_return_to, current_path(conn))
end end

View File

@ -1,10 +1,11 @@
defmodule LokalWeb.UserConfirmationController do defmodule LokalWeb.UserConfirmationController do
use LokalWeb, :controller use LokalWeb, :controller
import LokalWeb.Gettext
alias Lokal.Accounts alias Lokal.Accounts
def new(conn, _params) do def new(conn, _params) do
render(conn, "new.html") render(conn, "new.html", page_title: gettext("Confirm your account"))
end end
def create(conn, %{"user" => %{"email" => email}}) do def create(conn, %{"user" => %{"email" => email}}) do
@ -19,8 +20,11 @@ defmodule LokalWeb.UserConfirmationController do
conn conn
|> put_flash( |> put_flash(
:info, :info,
"If your email is in our system and it has not been confirmed yet, " <> dgettext(
"you will receive an email with instructions shortly." "prompts",
"If your email is in our system and it has not been confirmed yet, " <>
"you will receive an email with instructions shortly."
)
) )
|> redirect(to: "/") |> redirect(to: "/")
end end
@ -29,9 +33,9 @@ defmodule LokalWeb.UserConfirmationController do
# leaked token giving the user access to the account. # leaked token giving the user access to the account.
def confirm(conn, %{"token" => token}) do def confirm(conn, %{"token" => token}) do
case Accounts.confirm_user(token) do case Accounts.confirm_user(token) do
{:ok, _} -> {:ok, %{email: email}} ->
conn conn
|> put_flash(:info, "User confirmed successfully.") |> put_flash(:info, dgettext("prompts", "%{email} confirmed successfully.", email: email))
|> redirect(to: "/") |> redirect(to: "/")
:error -> :error ->
@ -45,7 +49,10 @@ defmodule LokalWeb.UserConfirmationController do
%{} -> %{} ->
conn conn
|> put_flash(:error, "User confirmation link is invalid or it has expired.") |> put_flash(
:error,
dgettext("errors", "User confirmation link is invalid or it has expired.")
)
|> redirect(to: "/") |> redirect(to: "/")
end end
end end

View File

@ -1,30 +1,81 @@
defmodule LokalWeb.UserRegistrationController do defmodule LokalWeb.UserRegistrationController do
use LokalWeb, :controller use LokalWeb, :controller
import LokalWeb.Gettext
alias Lokal.Accounts alias Lokal.{Accounts, Invites}
alias Lokal.Accounts.User alias Lokal.Accounts.User
alias LokalWeb.UserAuth alias LokalWeb.{Endpoint, PageLive}
def new(conn, _params) do def new(conn, %{"invite" => invite_token}) do
changeset = Accounts.change_user_registration(%User{}) invite = Invites.get_invite_by_token(invite_token)
render(conn, "new.html", changeset: changeset)
if invite do
conn |> render_new(invite)
else
conn
|> put_flash(:error, dgettext("errors", "Sorry, this invite was not found or expired"))
|> redirect(to: Routes.live_path(Endpoint, PageLive))
end
end end
def create(conn, %{"user" => user_params}) do def new(conn, _params) do
if Accounts.allow_registration?() do
conn |> render_new()
else
conn
|> put_flash(:error, dgettext("errors", "Sorry, public registration is disabled"))
|> redirect(to: Routes.live_path(Endpoint, PageLive))
end
end
# renders new user registration page
defp render_new(conn, invite \\ nil) do
render(conn, "new.html",
changeset: Accounts.change_user_registration(%User{}),
invite: invite,
page_title: gettext("Register")
)
end
def create(conn, %{"user" => %{"invite_token" => invite_token}} = attrs) do
invite = Invites.get_invite_by_token(invite_token)
if invite do
conn |> create_user(attrs, invite)
else
conn
|> put_flash(:error, dgettext("errors", "Sorry, this invite was not found or expired"))
|> redirect(to: Routes.live_path(Endpoint, PageLive))
end
end
def create(conn, attrs) do
if Accounts.allow_registration?() do
conn |> create_user(attrs)
else
conn
|> put_flash(:error, dgettext("errors", "Sorry, public registration is disabled"))
|> redirect(to: Routes.live_path(Endpoint, PageLive))
end
end
defp create_user(conn, %{"user" => user_params}, invite \\ nil) do
case Accounts.register_user(user_params) do case Accounts.register_user(user_params) do
{:ok, user} -> {:ok, user} ->
{:ok, _} = unless invite |> is_nil() do
Accounts.deliver_user_confirmation_instructions( invite |> Invites.use_invite!()
user, end
&Routes.user_confirmation_url(conn, :confirm, &1)
) Accounts.deliver_user_confirmation_instructions(
user,
&Routes.user_confirmation_url(conn, :confirm, &1)
)
conn conn
|> put_flash(:info, "User created successfully.") |> put_flash(:info, dgettext("prompts", "Please check your email to verify your account"))
|> UserAuth.log_in_user(user) |> redirect(to: Routes.user_session_path(Endpoint, :new))
{:error, %Ecto.Changeset{} = changeset} -> {:error, %Ecto.Changeset{} = changeset} ->
render(conn, "new.html", changeset: changeset) conn |> render("new.html", changeset: changeset, invite: invite)
end end
end end
end end

View File

@ -6,7 +6,7 @@ defmodule LokalWeb.UserResetPasswordController do
plug :get_user_by_reset_password_token when action in [:edit, :update] plug :get_user_by_reset_password_token when action in [:edit, :update]
def new(conn, _params) do def new(conn, _params) do
render(conn, "new.html") render(conn, "new.html", page_title: gettext("Forgot your password?"))
end end
def create(conn, %{"user" => %{"email" => email}}) do def create(conn, %{"user" => %{"email" => email}}) do
@ -21,13 +21,20 @@ defmodule LokalWeb.UserResetPasswordController do
conn conn
|> put_flash( |> put_flash(
:info, :info,
"If your email is in our system, you will receive instructions to reset your password shortly." dgettext(
"prompts",
"If your email is in our system, you will receive instructions to " <>
"reset your password shortly."
)
) )
|> redirect(to: "/") |> redirect(to: "/")
end end
def edit(conn, _params) do def edit(conn, _params) do
render(conn, "edit.html", changeset: Accounts.change_user_password(conn.assigns.user)) render(conn, "edit.html",
changeset: Accounts.change_user_password(conn.assigns.user),
page_title: gettext("Reset your password")
)
end end
# Do not log in the user after reset password to avoid a # Do not log in the user after reset password to avoid a
@ -36,7 +43,7 @@ defmodule LokalWeb.UserResetPasswordController do
case Accounts.reset_user_password(conn.assigns.user, user_params) do case Accounts.reset_user_password(conn.assigns.user, user_params) do
{:ok, _} -> {:ok, _} ->
conn conn
|> put_flash(:info, "Password reset successfully.") |> put_flash(:info, dgettext("prompts", "Password reset successfully."))
|> redirect(to: Routes.user_session_path(conn, :new)) |> redirect(to: Routes.user_session_path(conn, :new))
{:error, changeset} -> {:error, changeset} ->
@ -51,7 +58,10 @@ defmodule LokalWeb.UserResetPasswordController do
conn |> assign(:user, user) |> assign(:token, token) conn |> assign(:user, user) |> assign(:token, token)
else else
conn conn
|> put_flash(:error, "Reset password link is invalid or it has expired.") |> put_flash(
:error,
dgettext("errors", "Reset password link is invalid or it has expired.")
)
|> redirect(to: "/") |> redirect(to: "/")
|> halt() |> halt()
end end

View File

@ -5,7 +5,7 @@ defmodule LokalWeb.UserSessionController do
alias LokalWeb.UserAuth alias LokalWeb.UserAuth
def new(conn, _params) do def new(conn, _params) do
render(conn, "new.html", error_message: nil) render(conn, "new.html", error_message: nil, page_title: gettext("Log in"))
end end
def create(conn, %{"user" => user_params}) do def create(conn, %{"user" => user_params}) do
@ -14,13 +14,13 @@ defmodule LokalWeb.UserSessionController do
if user = Accounts.get_user_by_email_and_password(email, password) do if user = Accounts.get_user_by_email_and_password(email, password) do
UserAuth.log_in_user(conn, user, user_params) UserAuth.log_in_user(conn, user, user_params)
else else
render(conn, "new.html", error_message: "Invalid email or password") render(conn, "new.html", error_message: dgettext("errors", "Invalid email or password"))
end end
end end
def delete(conn, _params) do def delete(conn, _params) do
conn conn
|> put_flash(:info, "Logged out successfully.") |> put_flash(:info, dgettext("prompts", "Logged out successfully."))
|> UserAuth.log_out_user() |> UserAuth.log_out_user()
end end
end end

View File

@ -1,13 +1,13 @@
defmodule LokalWeb.UserSettingsController do defmodule LokalWeb.UserSettingsController do
use LokalWeb, :controller use LokalWeb, :controller
import LokalWeb.Gettext
alias Lokal.Accounts alias Lokal.Accounts
alias LokalWeb.UserAuth alias LokalWeb.{PageLive, UserAuth}
plug :assign_email_and_password_changesets plug :assign_email_and_password_changesets
def edit(conn, _params) do def edit(conn, _params) do
render(conn, "edit.html") render(conn, "edit.html", page_title: gettext("Settings"))
end end
def update(conn, %{"action" => "update_email"} = params) do def update(conn, %{"action" => "update_email"} = params) do
@ -25,7 +25,10 @@ defmodule LokalWeb.UserSettingsController do
conn conn
|> put_flash( |> put_flash(
:info, :info,
"A link to confirm your email change has been sent to the new address." dgettext(
"prompts",
"A link to confirm your email change has been sent to the new address."
)
) )
|> redirect(to: Routes.user_settings_path(conn, :edit)) |> redirect(to: Routes.user_settings_path(conn, :edit))
@ -41,7 +44,7 @@ defmodule LokalWeb.UserSettingsController do
case Accounts.update_user_password(user, password, user_params) do case Accounts.update_user_password(user, password, user_params) do
{:ok, user} -> {:ok, user} ->
conn conn
|> put_flash(:info, "Password updated successfully.") |> put_flash(:info, dgettext("prompts", "Password updated successfully."))
|> put_session(:user_return_to, Routes.user_settings_path(conn, :edit)) |> put_session(:user_return_to, Routes.user_settings_path(conn, :edit))
|> UserAuth.log_in_user(user) |> UserAuth.log_in_user(user)
@ -54,16 +57,33 @@ defmodule LokalWeb.UserSettingsController do
case Accounts.update_user_email(conn.assigns.current_user, token) do case Accounts.update_user_email(conn.assigns.current_user, token) do
:ok -> :ok ->
conn conn
|> put_flash(:info, "Email changed successfully.") |> put_flash(:info, dgettext("prompts", "Email changed successfully."))
|> redirect(to: Routes.user_settings_path(conn, :edit)) |> redirect(to: Routes.user_settings_path(conn, :edit))
:error -> :error ->
conn conn
|> put_flash(:error, "Email change link is invalid or it has expired.") |> put_flash(
:error,
dgettext("errors", "Email change link is invalid or it has expired.")
)
|> redirect(to: Routes.user_settings_path(conn, :edit)) |> redirect(to: Routes.user_settings_path(conn, :edit))
end end
end end
def delete(%{assigns: %{current_user: current_user}} = conn, %{"id" => user_id}) do
if user_id == current_user.id do
current_user |> Accounts.delete_user!(current_user)
conn
|> put_flash(:error, dgettext("prompts", "Your account has been deleted"))
|> redirect(to: Routes.live_path(conn, PageLive))
else
conn
|> put_flash(:error, dgettext("errors", "Unable to delete user"))
|> redirect(to: Routes.user_settings_path(conn, :edit))
end
end
defp assign_email_and_password_changesets(conn, _opts) do defp assign_email_and_password_changesets(conn, _opts) do
user = conn.assigns.current_user user = conn.assigns.current_user

View File

@ -4,22 +4,31 @@ defmodule LokalWeb.ErrorHelpers do
""" """
use Phoenix.HTML use Phoenix.HTML
import Phoenix.LiveView.Helpers
alias Ecto.Changeset
alias Phoenix.{HTML.Form, LiveView.Rendered}
@doc """ @doc """
Generates tag for inlined form input errors. Generates tag for inlined form input errors.
""" """
def error_tag(form, field) do @spec error_tag(Form.t(), Form.field()) :: Rendered.t()
Enum.map(Keyword.get_values(form.errors, field), fn error -> @spec error_tag(Form.t(), Form.field(), String.t()) :: Rendered.t()
content_tag(:span, translate_error(error), def error_tag(form, field, extra_class \\ "") do
class: "invalid-feedback", assigns = %{extra_class: extra_class, form: form, field: field}
phx_feedback_for: input_name(form, field)
) ~H"""
end) <%= for error <- Keyword.get_values(@form.errors, @field) do %>
<span class={"invalid-feedback #{@extra_class}"} phx-feedback-for={input_name(@form, @field)}>
<%= translate_error(error) %>
</span>
<% end %>
"""
end end
@doc """ @doc """
Translates an error message using gettext. Translates an error message using gettext.
""" """
@spec translate_error({String.t(), keyword() | map()}) :: String.t()
def translate_error({msg, opts}) do def translate_error({msg, opts}) do
# When using gettext, we typically pass the strings we want # When using gettext, we typically pass the strings we want
# to translate as a static argument: # to translate as a static argument:
@ -44,4 +53,30 @@ defmodule LokalWeb.ErrorHelpers do
Gettext.dgettext(LokalWeb.Gettext, "errors", msg, opts) Gettext.dgettext(LokalWeb.Gettext, "errors", msg, opts)
end end
end end
@doc """
Displays all errors from a changeset, or just for a single key
"""
@spec changeset_errors(Changeset.t()) :: String.t()
@spec changeset_errors(Changeset.t(), key :: atom()) :: [String.t()] | nil
def changeset_errors(changeset) do
changeset
|> changeset_error_map()
|> Enum.map_join(". ", fn {key, errors} ->
"#{key |> humanize()}: #{errors |> Enum.join(", ")}"
end)
end
def changeset_errors(changeset, key) do
changeset |> changeset_error_map() |> Map.get(key)
end
@doc """
Displays all errors from a changeset in a key value map
"""
@spec changeset_error_map(Changeset.t()) :: %{atom() => [String.t()]}
def changeset_error_map(changeset) do
changeset
|> Changeset.traverse_errors(fn error -> error |> translate_error() end)
end
end end

View File

@ -1,16 +1,16 @@
defmodule LokalWeb.ErrorView do defmodule LokalWeb.ErrorView do
use LokalWeb, :view use LokalWeb, :view
import LokalWeb.Components.Topbar
alias LokalWeb.{Endpoint, PageLive}
# If you want to customize a particular status code def template_not_found(error_path, _assigns) do
# for a certain format, you may uncomment below. error_string =
# def render("500.html", _assigns) do case error_path do
# "Internal Server Error" "404.html" -> dgettext("errors", "Not found")
# end "401.html" -> dgettext("errors", "Unauthorized")
_ -> dgettext("errors", "Internal Server Error")
end
# By default, Phoenix returns the status message from render("error.html", %{error_string: error_string})
# the template name. For example, "404.html" becomes
# "Not Found".
def template_not_found(template, _assigns) do
Phoenix.Controller.status_message_from_template(template)
end end
end end

View File

@ -1,3 +1,4 @@
defmodule LokalWeb.UserConfirmationView do defmodule LokalWeb.UserConfirmationView do
use LokalWeb, :view use LokalWeb, :view
alias Lokal.Accounts
end end

View File

@ -1,3 +1,4 @@
defmodule LokalWeb.UserResetPasswordView do defmodule LokalWeb.UserResetPasswordView do
use LokalWeb, :view use LokalWeb, :view
alias Lokal.Accounts
end end

View File

@ -1,3 +1,4 @@
defmodule LokalWeb.UserSessionView do defmodule LokalWeb.UserSessionView do
use LokalWeb, :view use LokalWeb, :view
alias Lokal.Accounts
end end

View File

@ -1,7 +1,8 @@
defmodule LokalWeb.ViewHelpers do defmodule LokalWeb.ViewHelpers do
@moduledoc """ @moduledoc """
Contains common helpers that can be used in liveviews and regular views. These Contains common helpers that can be used in liveviews and regular views. These
are automatically imported into any Phoenix View using `use LokalWeb, :view` are automatically imported into any Phoenix View using `use LokalWeb,
:view`
""" """
import Phoenix.LiveView.Helpers import Phoenix.LiveView.Helpers
@ -10,24 +11,42 @@ defmodule LokalWeb.ViewHelpers do
Returns a <time> element that renders the naivedatetime in the user's local Returns a <time> element that renders the naivedatetime in the user's local
timezone with Alpine.js timezone with Alpine.js
""" """
@spec display_datetime(NaiveDateTime.t()) :: Phoenix.LiveView.Rendered.t() @spec display_datetime(NaiveDateTime.t() | nil) :: Phoenix.LiveView.Rendered.t()
def display_datetime(nil), do: ""
def display_datetime(datetime) do def display_datetime(datetime) do
assigns = %{ assigns = %{
datetime: datetime |> DateTime.from_naive!("Etc/UTC") |> DateTime.to_iso8601(:extended) datetime: datetime |> DateTime.from_naive!("Etc/UTC") |> DateTime.to_iso8601(:extended)
} }
~H""" ~H"""
<time <time datetime={@datetime} x-data={"{
datetime={@datetime}
x-data={"{
date: date:
Intl.DateTimeFormat([], {dateStyle: 'short', timeStyle: 'long'}) Intl.DateTimeFormat([], {dateStyle: 'short', timeStyle: 'long'})
.format(new Date(\"#{@datetime}\")) .format(new Date(\"#{@datetime}\"))
}"} }"} x-text="date">
x-text="date"
>
<%= @datetime %> <%= @datetime %>
</time> </time>
""" """
end end
@doc """
Returns a <date> element that renders the Date in the user's local
timezone with Alpine.js
"""
@spec display_date(Date.t() | nil) :: Phoenix.LiveView.Rendered.t()
def display_date(nil), do: ""
def display_date(date) do
assigns = %{date: date |> Date.to_iso8601(:extended)}
~H"""
<time datetime={@date} x-data={"{
date:
Intl.DateTimeFormat([], {timeZone: 'Etc/UTC', dateStyle: 'short'}).format(new Date(\"#{@date}\"))
}"} x-text="date">
<%= @date %>
</time>
"""
end
end end