From aa314e5ca14a85e5bbf21033baf595be3fe4f13b Mon Sep 17 00:00:00 2001 From: shibao Date: Fri, 25 Feb 2022 21:55:17 -0500 Subject: [PATCH] update main routes --- lib/lokal/accounts/user.ex | 69 +++++++++++++++++++++++++++++++------- lib/lokal_web/router.ex | 65 ++++++++++++++++++----------------- lib/lokal_web/telemetry.ex | 2 +- 3 files changed, 89 insertions(+), 47 deletions(-) diff --git a/lib/lokal/accounts/user.ex b/lib/lokal/accounts/user.ex index 91eb2a4..67646b0 100644 --- a/lib/lokal/accounts/user.ex +++ b/lib/lokal/accounts/user.ex @@ -1,10 +1,13 @@ defmodule Lokal.Accounts.User do @moduledoc """ - Schema for a registered user + A Lokal user """ use Ecto.Schema import Ecto.Changeset + import LokalWeb.Gettext + alias Ecto.{Changeset, UUID} + alias Lokal.{Accounts.User, Invites.Invite} @derive {Inspect, except: [:password]} @primary_key {:id, :binary_id, autogenerate: true} @@ -14,10 +17,27 @@ defmodule Lokal.Accounts.User do field :password, :string, virtual: true field :hashed_password, :string field :confirmed_at, :naive_datetime + field :role, Ecto.Enum, values: [:admin, :user], default: :user + + has_many :invites, Invite, on_delete: :delete_all timestamps() end + @type t :: %User{ + id: id(), + email: String.t(), + password: String.t(), + hashed_password: String.t(), + confirmed_at: NaiveDateTime.t(), + role: atom(), + invites: [Invite.t()], + inserted_at: NaiveDateTime.t(), + updated_at: NaiveDateTime.t() + } + @type new_user :: %User{} + @type id :: UUID.t() + @doc """ A user changeset for registration. @@ -35,22 +55,39 @@ defmodule Lokal.Accounts.User do validations on a LiveView form), this option can be set to `false`. Defaults to `true`. """ + @spec registration_changeset(t() | new_user(), attrs :: map()) :: Changeset.t(t() | new_user()) + @spec registration_changeset(t() | new_user(), attrs :: map(), opts :: keyword()) :: + Changeset.t(t() | new_user()) def registration_changeset(user, attrs, opts \\ []) do user - |> cast(attrs, [:email, :password]) + |> cast(attrs, [:email, :password, :role]) |> validate_email() |> validate_password(opts) end + @doc """ + A user changeset for role. + + """ + @spec role_changeset(t(), role :: atom()) :: Changeset.t(t()) + def role_changeset(user, role) do + user |> cast(%{"role" => role}, [:role]) + end + + @spec validate_email(Changeset.t(t() | new_user())) :: Changeset.t(t() | new_user()) defp validate_email(changeset) do changeset |> validate_required([:email]) - |> validate_format(:email, ~r/^[^\s]+@[^\s]+$/, message: "must have the @ sign and no spaces") + |> validate_format(:email, ~r/^[^\s]+@[^\s]+$/, + message: dgettext("errors", "must have the @ sign and no spaces") + ) |> validate_length(:email, max: 160) |> unsafe_validate_unique(:email, Lokal.Repo) |> unique_constraint(:email) end + @spec validate_password(Changeset.t(t() | new_user()), opts :: keyword()) :: + Changeset.t(t() | new_user()) defp validate_password(changeset, opts) do changeset |> validate_required([:password]) @@ -61,6 +98,8 @@ defmodule Lokal.Accounts.User do |> maybe_hash_password(opts) end + @spec maybe_hash_password(Changeset.t(t() | new_user()), opts :: keyword()) :: + Changeset.t(t() | new_user()) defp maybe_hash_password(changeset, opts) do hash_password? = Keyword.get(opts, :hash_password, true) password = get_change(changeset, :password) @@ -79,13 +118,14 @@ defmodule Lokal.Accounts.User do It requires the email to change otherwise an error is added. """ + @spec email_changeset(t(), attrs :: map()) :: Changeset.t(t()) def email_changeset(user, attrs) do user |> cast(attrs, [:email]) |> validate_email() |> case do %{changes: %{email: _}} = changeset -> changeset - %{} = changeset -> add_error(changeset, :email, "did not change") + %{} = changeset -> add_error(changeset, :email, dgettext("errors", "did not change")) end end @@ -101,19 +141,22 @@ defmodule Lokal.Accounts.User do validations on a LiveView form), this option can be set to `false`. Defaults to `true`. """ + @spec password_changeset(t(), attrs :: map()) :: Changeset.t(t()) + @spec password_changeset(t(), attrs :: map(), opts :: keyword()) :: Changeset.t(t()) def password_changeset(user, attrs, opts \\ []) do user |> cast(attrs, [:password]) - |> validate_confirmation(:password, message: "does not match password") + |> validate_confirmation(:password, message: dgettext("errors", "does not match password")) |> validate_password(opts) end @doc """ Confirms the account by setting `confirmed_at`. """ - def confirm_changeset(user) do + @spec confirm_changeset(t() | Changeset.t(t())) :: Changeset.t(t()) + def confirm_changeset(user_or_changeset) do now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second) - change(user, confirmed_at: now) + user_or_changeset |> change(confirmed_at: now) end @doc """ @@ -122,7 +165,8 @@ defmodule Lokal.Accounts.User do If there is no user or the user doesn't have a password, we call `Bcrypt.no_user_verify/0` to avoid timing attacks. """ - def valid_password?(%Lokal.Accounts.User{hashed_password: hashed_password}, password) + @spec valid_password?(t(), String.t()) :: boolean() + def valid_password?(%User{hashed_password: hashed_password}, password) when is_binary(hashed_password) and byte_size(password) > 0 do Bcrypt.verify_pass(password, hashed_password) end @@ -135,11 +179,10 @@ defmodule Lokal.Accounts.User do @doc """ Validates the current password otherwise adds an error to the changeset. """ + @spec validate_current_password(Changeset.t(t()), String.t()) :: Changeset.t(t()) def validate_current_password(changeset, password) do - if valid_password?(changeset.data, password) do - changeset - else - add_error(changeset, :current_password, "is not valid") - end + if valid_password?(changeset.data, password), + do: changeset, + else: changeset |> add_error(:current_password, dgettext("errors", "is not valid")) end end diff --git a/lib/lokal_web/router.ex b/lib/lokal_web/router.ex index 47ace70..76e5e5c 100644 --- a/lib/lokal_web/router.ex +++ b/lib/lokal_web/router.ex @@ -1,5 +1,6 @@ defmodule LokalWeb.Router do use LokalWeb, :router + import Phoenix.LiveDashboard.Router import LokalWeb.UserAuth pipeline :browser do @@ -12,6 +13,10 @@ defmodule LokalWeb.Router do plug :fetch_current_user end + pipeline :require_admin do + plug :require_role, role: :admin + end + pipeline :api do plug :accepts, ["json"] end @@ -22,39 +27,6 @@ defmodule LokalWeb.Router do live "/", PageLive end - # Other scopes may use custom stacks. - # scope "/api", LokalWeb do - # pipe_through :api - # end - - # Enables LiveDashboard only for development - # - # If you want to use the LiveDashboard in production, you should put - # it behind authentication and allow only admins to access it. - # If your application does not have an admins-only section yet, - # you can use Plug.BasicAuth to set up some basic authentication - # as long as you are also using SSL (which you should anyway). - if Mix.env() in [:dev, :test] do - import Phoenix.LiveDashboard.Router - - scope "/" do - pipe_through :browser - live_dashboard "/dashboard", metrics: LokalWeb.Telemetry - end - end - - # Enables the Swoosh mailbox preview in development. - # - # Note that preview only shows emails that were sent by the same - # node running the Phoenix server. - if Mix.env() == :dev do - scope "/dev" do - pipe_through :browser - - forward "/mailbox", Plug.Swoosh.MailboxPreview - end - end - ## Authentication routes scope "/", LokalWeb do @@ -75,9 +47,20 @@ defmodule LokalWeb.Router do get "/users/settings", UserSettingsController, :edit put "/users/settings", UserSettingsController, :update + delete "/users/settings/:id", UserSettingsController, :delete get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email end + scope "/", LokalWeb do + pipe_through [:browser, :require_authenticated_user, :require_admin] + + live_dashboard "/dashboard", metrics: LokalWeb.Telemetry, ecto_repos: [Lokal.Repo] + + live "/invites", InviteLive.Index, :index + live "/invites/new", InviteLive.Index, :new + live "/invites/:id/edit", InviteLive.Index, :edit + end + scope "/", LokalWeb do pipe_through [:browser] @@ -86,4 +69,20 @@ defmodule LokalWeb.Router do post "/users/confirm", UserConfirmationController, :create get "/users/confirm/:token", UserConfirmationController, :confirm end + + # Enables the Swoosh mailbox preview in development. + # + # Note that preview only shows emails that were sent by the same + # node running the Phoenix server. + if Mix.env() == :dev do + scope "/dev" do + pipe_through :browser + + forward "/mailbox", Plug.Swoosh.MailboxPreview + end + + scope "/dev" do + get "/preview/:id", LokalWeb.EmailController, :preview + end + end end diff --git a/lib/lokal_web/telemetry.ex b/lib/lokal_web/telemetry.ex index c30cb18..581b7fe 100644 --- a/lib/lokal_web/telemetry.ex +++ b/lib/lokal_web/telemetry.ex @@ -1,6 +1,6 @@ defmodule LokalWeb.Telemetry do @moduledoc """ - Telemetry genserver + Collects telemetry """ use Supervisor