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
@moduledoc """
Module for any user authentication functions
Functions for user session and authentication
"""
import Plug.Conn
import Phoenix.Controller
import LokalWeb.Gettext
alias Lokal.{Accounts, Accounts.User}
alias LokalWeb.PageLive
alias LokalWeb.Router.Helpers, as: Routes
# 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
conn
|> fetch_flash()
|> put_flash(
:error,
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))
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
put_resp_cookie(conn, @remember_me_cookie, token, @remember_me_options)
end
@ -149,13 +156,31 @@ defmodule LokalWeb.UserAuth do
conn
else
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()
|> redirect(to: Routes.user_session_path(conn, :new))
|> halt()
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
put_session(conn, :user_return_to, current_path(conn))
end

View File

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

View File

@ -1,30 +1,81 @@
defmodule LokalWeb.UserRegistrationController do
use LokalWeb, :controller
alias Lokal.Accounts
import LokalWeb.Gettext
alias Lokal.{Accounts, Invites}
alias Lokal.Accounts.User
alias LokalWeb.UserAuth
alias LokalWeb.{Endpoint, PageLive}
def new(conn, _params) do
changeset = Accounts.change_user_registration(%User{})
render(conn, "new.html", changeset: changeset)
def new(conn, %{"invite" => invite_token}) do
invite = Invites.get_invite_by_token(invite_token)
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
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
{:ok, user} ->
{:ok, _} =
Accounts.deliver_user_confirmation_instructions(
user,
&Routes.user_confirmation_url(conn, :confirm, &1)
)
unless invite |> is_nil() do
invite |> Invites.use_invite!()
end
Accounts.deliver_user_confirmation_instructions(
user,
&Routes.user_confirmation_url(conn, :confirm, &1)
)
conn
|> put_flash(:info, "User created successfully.")
|> UserAuth.log_in_user(user)
|> put_flash(:info, dgettext("prompts", "Please check your email to verify your account"))
|> redirect(to: Routes.user_session_path(Endpoint, :new))
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "new.html", changeset: changeset)
conn |> render("new.html", changeset: changeset, invite: invite)
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]
def new(conn, _params) do
render(conn, "new.html")
render(conn, "new.html", page_title: gettext("Forgot your password?"))
end
def create(conn, %{"user" => %{"email" => email}}) do
@ -21,13 +21,20 @@ defmodule LokalWeb.UserResetPasswordController do
conn
|> put_flash(
: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: "/")
end
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
# 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
{:ok, _} ->
conn
|> put_flash(:info, "Password reset successfully.")
|> put_flash(:info, dgettext("prompts", "Password reset successfully."))
|> redirect(to: Routes.user_session_path(conn, :new))
{:error, changeset} ->
@ -51,7 +58,10 @@ defmodule LokalWeb.UserResetPasswordController do
conn |> assign(:user, user) |> assign(:token, token)
else
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: "/")
|> halt()
end

View File

@ -5,7 +5,7 @@ defmodule LokalWeb.UserSessionController do
alias LokalWeb.UserAuth
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
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
UserAuth.log_in_user(conn, user, user_params)
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
def delete(conn, _params) do
conn
|> put_flash(:info, "Logged out successfully.")
|> put_flash(:info, dgettext("prompts", "Logged out successfully."))
|> UserAuth.log_out_user()
end
end

View File

@ -1,13 +1,13 @@
defmodule LokalWeb.UserSettingsController do
use LokalWeb, :controller
import LokalWeb.Gettext
alias Lokal.Accounts
alias LokalWeb.UserAuth
alias LokalWeb.{PageLive, UserAuth}
plug :assign_email_and_password_changesets
def edit(conn, _params) do
render(conn, "edit.html")
render(conn, "edit.html", page_title: gettext("Settings"))
end
def update(conn, %{"action" => "update_email"} = params) do
@ -25,7 +25,10 @@ defmodule LokalWeb.UserSettingsController do
conn
|> put_flash(
: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))
@ -41,7 +44,7 @@ defmodule LokalWeb.UserSettingsController do
case Accounts.update_user_password(user, password, user_params) do
{:ok, user} ->
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))
|> UserAuth.log_in_user(user)
@ -54,16 +57,33 @@ defmodule LokalWeb.UserSettingsController do
case Accounts.update_user_email(conn.assigns.current_user, token) do
:ok ->
conn
|> put_flash(:info, "Email changed successfully.")
|> put_flash(:info, dgettext("prompts", "Email changed successfully."))
|> redirect(to: Routes.user_settings_path(conn, :edit))
:error ->
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))
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
user = conn.assigns.current_user

View File

@ -4,22 +4,31 @@ defmodule LokalWeb.ErrorHelpers do
"""
use Phoenix.HTML
import Phoenix.LiveView.Helpers
alias Ecto.Changeset
alias Phoenix.{HTML.Form, LiveView.Rendered}
@doc """
Generates tag for inlined form input errors.
"""
def error_tag(form, field) do
Enum.map(Keyword.get_values(form.errors, field), fn error ->
content_tag(:span, translate_error(error),
class: "invalid-feedback",
phx_feedback_for: input_name(form, field)
)
end)
@spec error_tag(Form.t(), Form.field()) :: Rendered.t()
@spec error_tag(Form.t(), Form.field(), String.t()) :: Rendered.t()
def error_tag(form, field, extra_class \\ "") do
assigns = %{extra_class: extra_class, form: form, field: field}
~H"""
<%= 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
@doc """
Translates an error message using gettext.
"""
@spec translate_error({String.t(), keyword() | map()}) :: String.t()
def translate_error({msg, opts}) do
# When using gettext, we typically pass the strings we want
# to translate as a static argument:
@ -44,4 +53,30 @@ defmodule LokalWeb.ErrorHelpers do
Gettext.dgettext(LokalWeb.Gettext, "errors", msg, opts)
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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,8 @@
defmodule LokalWeb.ViewHelpers do
@moduledoc """
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
@ -10,24 +11,42 @@ defmodule LokalWeb.ViewHelpers do
Returns a <time> element that renders the naivedatetime in the user's local
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
assigns = %{
datetime: datetime |> DateTime.from_naive!("Etc/UTC") |> DateTime.to_iso8601(:extended)
}
~H"""
<time
datetime={@datetime}
x-data={"{
<time datetime={@datetime} x-data={"{
date:
Intl.DateTimeFormat([], {dateStyle: 'short', timeStyle: 'long'})
.format(new Date(\"#{@datetime}\"))
}"}
x-text="date"
>
}"} x-text="date">
<%= @datetime %>
</time>
"""
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