defmodule PowLockout.Ecto.Schema do @moduledoc """ Handles the lockout schema for user. ## Customize PowLockout fields If you need to modify any of the fields that `PowLockout` adds to the user schema, you can override them by defining them before `pow_user_fields/0`: defmodule MyApp.Users.User do use Ecto.Schema use Pow.Ecto.Schema use Pow.Extension.Ecto.Schema, extensions: [PowLockout] schema "users" do field :unlock_token, :string field :locked_at, :utc_datetime field :failed_attempts, :integer pow_user_fields() timestamps() end end """ use Pow.Extension.Ecto.Schema.Base alias Ecto.Changeset alias Pow.UUID @doc false @impl true def attrs(_config) do [ {:unlock_token, :string}, {:locked_at, :utc_datetime}, {:failed_attempts, :integer} ] end @doc false @impl true def indexes(_config) do [{:unlock_token, true}] end @doc """ Sets the account as unlocked. This sets `:locked_at` and `:unlock_token` to nil, and sets `failed_attempts` to 0. """ @spec unlock_changeset(Ecto.Schema.t() | Changeset.t()) :: Changeset.t() def unlock_changeset(user_or_changeset) do changes = [ locked_at: nil, unlock_token: nil, failed_attempts: 0 ] user_or_changeset |> Changeset.change(changes) end @doc """ Sets the account as locked. This sets `:locked_at` to now and sets `:unlock_token` to a random UUID. """ @spec lock_changeset(Ecto.Schema.t() | Changeset.t()) :: Changeset.t() def lock_changeset(user_or_changeset) do changeset = Changeset.change(user_or_changeset) locked_at = Pow.Ecto.Schema.__timestamp_for__(changeset.data.__struct__, :locked_at) changes = [ locked_at: locked_at, unlock_token: UUID.generate() ] changeset |> Changeset.change(changes) end @doc """ Updates the failed attempt count. This increments `:failed_attempts` by 1, or sets it to 1 if it is nil. The first time it becomes greater than 10, it also locks the user. """ @spec attempt_changeset(Ecto.Schema.t() | Changeset.t()) :: Changeset.t() def attempt_changeset(%Changeset{data: %{failed_attempts: attempts}} = changeset) when is_integer(attempts) and attempts < 10 do Changeset.change(changeset, failed_attempts: attempts + 1) end def attempt_changeset( %Changeset{data: %{failed_attempts: attempts, locked_at: nil}} = changeset ) when is_integer(attempts) do lock_changeset(changeset) end def attempt_changeset( %Changeset{data: %{failed_attempts: attempts, locked_at: _locked_at}} = changeset ) when is_integer(attempts) do changeset end def attempt_changeset(%Changeset{} = changeset) do Changeset.change(changeset, failed_attempts: 1) end def attempt_changeset(user) do user |> Changeset.change() |> attempt_changeset() end @doc """ Resets the failed attempt count. This sets `:failed_attempts` to 0. """ @spec attempt_reset_changeset(Ecto.Schema.t() | Changeset.t()) :: Changeset.t() def attempt_reset_changeset(user_or_changeset) do user_or_changeset |> Changeset.change(failed_attempts: 0) end end