diff --git a/lib/cannery/accounts/user.ex b/lib/cannery/accounts/user.ex index a340ad64..4a058f93 100644 --- a/lib/cannery/accounts/user.ex +++ b/lib/cannery/accounts/user.ex @@ -1,6 +1,7 @@ defmodule Cannery.Accounts.User do use Ecto.Schema import Ecto.Changeset + alias Cannery.Accounts.{User} @derive {Inspect, except: [:password]} @primary_key {:id, :binary_id, autogenerate: true} @@ -10,10 +11,23 @@ defmodule Cannery.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 timestamps() end + @type t :: %{ + id: Ecto.UUID.t(), + 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() + } + @doc """ A user changeset for registration. @@ -31,13 +45,25 @@ defmodule Cannery.Accounts.User do validations on a LiveView form), this option can be set to `false`. Defaults to `true`. """ + @spec registration_changeset(User.t(), map()) :: Ecto.Changeset.t() + @spec registration_changeset(User.t(), map(), keyword()) :: Ecto.Changeset.t() 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(User.t(), atom()) :: Ecto.Changeset.t() + def role_changeset(user, role) do + user |> cast(%{"role" => role}, [:role]) + end + + @spec validate_email(Ecto.Changeset.t()) :: Ecto.Changeset.t() defp validate_email(changeset) do changeset |> validate_required([:email]) @@ -47,6 +73,7 @@ defmodule Cannery.Accounts.User do |> unique_constraint(:email) end + @spec validate_password(Ecto.Changeset.t(), keyword()) :: Ecto.Changeset.t() defp validate_password(changeset, opts) do changeset |> validate_required([:password]) @@ -57,6 +84,7 @@ defmodule Cannery.Accounts.User do |> maybe_hash_password(opts) end + @spec maybe_hash_password(Ecto.Changeset.t(), keyword()) :: Ecto.Changeset.t() defp maybe_hash_password(changeset, opts) do hash_password? = Keyword.get(opts, :hash_password, true) password = get_change(changeset, :password) @@ -75,6 +103,7 @@ defmodule Cannery.Accounts.User do It requires the email to change otherwise an error is added. """ + @spec email_changeset(User.t(), map()) :: Ecto.Changeset.t() def email_changeset(user, attrs) do user |> cast(attrs, [:email]) @@ -97,6 +126,8 @@ defmodule Cannery.Accounts.User do validations on a LiveView form), this option can be set to `false`. Defaults to `true`. """ + @spec password_changeset(User.t(), map()) :: Ecto.Changeset.t() + @spec password_changeset(User.t(), map(), keyword()) :: Ecto.Changeset.t() def password_changeset(user, attrs, opts \\ []) do user |> cast(attrs, [:password]) @@ -107,6 +138,7 @@ defmodule Cannery.Accounts.User do @doc """ Confirms the account by setting `confirmed_at`. """ + @spec confirm_changeset(User.t()) :: Ecto.Changeset.t() def confirm_changeset(user) do now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second) change(user, confirmed_at: now) @@ -118,7 +150,8 @@ defmodule Cannery.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?(%Cannery.Accounts.User{hashed_password: hashed_password}, password) + @spec valid_password?(User.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 @@ -131,6 +164,7 @@ defmodule Cannery.Accounts.User do @doc """ Validates the current password otherwise adds an error to the changeset. """ + @spec validate_current_password(Ecto.Changeset.t(), String.t()) :: Ecto.UUID.t() def validate_current_password(changeset, password) do if valid_password?(changeset.data, password) do changeset diff --git a/lib/cannery/accounts/user_token.ex b/lib/cannery/accounts/user_token.ex index 48e76c43..5ecdf239 100644 --- a/lib/cannery/accounts/user_token.ex +++ b/lib/cannery/accounts/user_token.ex @@ -1,6 +1,7 @@ defmodule Cannery.Accounts.UserToken do use Ecto.Schema import Ecto.Query + alias Cannery.{Accounts} @hash_algorithm :sha256 @rand_size 32 @@ -18,7 +19,7 @@ defmodule Cannery.Accounts.UserToken do field :token, :binary field :context, :string field :sent_to, :string - belongs_to :user, Cannery.Accounts.User + belongs_to :user, Accounts.User timestamps(updated_at: false) end @@ -30,7 +31,7 @@ defmodule Cannery.Accounts.UserToken do """ def build_session_token(user) do token = :crypto.strong_rand_bytes(@rand_size) - {token, %Cannery.Accounts.UserToken{token: token, context: "session", user_id: user.id}} + {token, %Accounts.UserToken{token: token, context: "session", user_id: user.id}} end @doc """ @@ -65,7 +66,7 @@ defmodule Cannery.Accounts.UserToken do hashed_token = :crypto.hash(@hash_algorithm, token) {Base.url_encode64(token, padding: false), - %Cannery.Accounts.UserToken{ + %Accounts.UserToken{ token: hashed_token, context: context, sent_to: sent_to, @@ -125,17 +126,17 @@ defmodule Cannery.Accounts.UserToken do Returns the given token with the given context. """ def token_and_context_query(token, context) do - from Cannery.Accounts.UserToken, where: [token: ^token, context: ^context] + from Accounts.UserToken, where: [token: ^token, context: ^context] end @doc """ Gets all tokens for the given user for the given contexts. """ def user_and_contexts_query(user, :all) do - from t in Cannery.Accounts.UserToken, where: t.user_id == ^user.id + from t in Accounts.UserToken, where: t.user_id == ^user.id end def user_and_contexts_query(user, [_ | _] = contexts) do - from t in Cannery.Accounts.UserToken, where: t.user_id == ^user.id and t.context in ^contexts + from t in Accounts.UserToken, where: t.user_id == ^user.id and t.context in ^contexts end end