diff --git a/lib/lokal_web/controllers/email_controller.ex b/lib/lokal_web/controllers/email_controller.ex
new file mode 100644
index 00000000..497cae0a
--- /dev/null
+++ b/lib/lokal_web/controllers/email_controller.ex
@@ -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
diff --git a/lib/lokal_web/controllers/user_auth.ex b/lib/lokal_web/controllers/user_auth.ex
index 57237d59..d9e918de 100644
--- a/lib/lokal_web/controllers/user_auth.ex
+++ b/lib/lokal_web/controllers/user_auth.ex
@@ -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
diff --git a/lib/lokal_web/controllers/user_confirmation_controller.ex b/lib/lokal_web/controllers/user_confirmation_controller.ex
index 2b7883ed..c03782f9 100644
--- a/lib/lokal_web/controllers/user_confirmation_controller.ex
+++ b/lib/lokal_web/controllers/user_confirmation_controller.ex
@@ -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
diff --git a/lib/lokal_web/controllers/user_registration_controller.ex b/lib/lokal_web/controllers/user_registration_controller.ex
index dcc70413..584516ea 100644
--- a/lib/lokal_web/controllers/user_registration_controller.ex
+++ b/lib/lokal_web/controllers/user_registration_controller.ex
@@ -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
diff --git a/lib/lokal_web/controllers/user_reset_password_controller.ex b/lib/lokal_web/controllers/user_reset_password_controller.ex
index 5260212d..0028bdad 100644
--- a/lib/lokal_web/controllers/user_reset_password_controller.ex
+++ b/lib/lokal_web/controllers/user_reset_password_controller.ex
@@ -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
diff --git a/lib/lokal_web/controllers/user_session_controller.ex b/lib/lokal_web/controllers/user_session_controller.ex
index e288c733..23b6b841 100644
--- a/lib/lokal_web/controllers/user_session_controller.ex
+++ b/lib/lokal_web/controllers/user_session_controller.ex
@@ -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
diff --git a/lib/lokal_web/controllers/user_settings_controller.ex b/lib/lokal_web/controllers/user_settings_controller.ex
index 8a912ea7..9c153a25 100644
--- a/lib/lokal_web/controllers/user_settings_controller.ex
+++ b/lib/lokal_web/controllers/user_settings_controller.ex
@@ -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
diff --git a/lib/lokal_web/views/error_helpers.ex b/lib/lokal_web/views/error_helpers.ex
index 6f1771ce..aa76578b 100644
--- a/lib/lokal_web/views/error_helpers.ex
+++ b/lib/lokal_web/views/error_helpers.ex
@@ -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 %>
+
+ <%= translate_error(error) %>
+
+ <% 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
diff --git a/lib/lokal_web/views/error_view.ex b/lib/lokal_web/views/error_view.ex
index 8d3af60d..bca829ed 100644
--- a/lib/lokal_web/views/error_view.ex
+++ b/lib/lokal_web/views/error_view.ex
@@ -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
diff --git a/lib/lokal_web/views/user_confirmation_view.ex b/lib/lokal_web/views/user_confirmation_view.ex
index d3706b00..c9ed5518 100644
--- a/lib/lokal_web/views/user_confirmation_view.ex
+++ b/lib/lokal_web/views/user_confirmation_view.ex
@@ -1,3 +1,4 @@
defmodule LokalWeb.UserConfirmationView do
use LokalWeb, :view
+ alias Lokal.Accounts
end
diff --git a/lib/lokal_web/views/user_reset_password_view.ex b/lib/lokal_web/views/user_reset_password_view.ex
index 68cd5043..6232dc47 100644
--- a/lib/lokal_web/views/user_reset_password_view.ex
+++ b/lib/lokal_web/views/user_reset_password_view.ex
@@ -1,3 +1,4 @@
defmodule LokalWeb.UserResetPasswordView do
use LokalWeb, :view
+ alias Lokal.Accounts
end
diff --git a/lib/lokal_web/views/user_session_view.ex b/lib/lokal_web/views/user_session_view.ex
index f61a2ae8..bb9bf6fe 100644
--- a/lib/lokal_web/views/user_session_view.ex
+++ b/lib/lokal_web/views/user_session_view.ex
@@ -1,3 +1,4 @@
defmodule LokalWeb.UserSessionView do
use LokalWeb, :view
+ alias Lokal.Accounts
end
diff --git a/lib/lokal_web/views/view_helpers.ex b/lib/lokal_web/views/view_helpers.ex
index 28b6f44a..889ca3bc 100644
--- a/lib/lokal_web/views/view_helpers.ex
+++ b/lib/lokal_web/views/view_helpers.ex
@@ -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