How to lock users
Locking users is trivial, and you won't need an extension for this. It can be done in several ways, but we'll work with the most straight forward setup.
Update your schema
Add a locked_at
column to your user schema, and a lock_changeset/1
method to lock the account:
defmodule MyApp.Users.User do
use Ecto.Schema
use Pow.Ecto.Schema
alias Ecto.Changeset
schema "users" do
field :locked_at, :utc_datetime
pow_user_fields()
timestamps()
end
@spec lock_changeset(Ecto.Schema.t() | Ecto.Changeset.t()) :: Ecto.Changeset.t()
def lock_changeset(user_or_changeset) do
changeset = Changeset.change(user_or_changeset)
locked_at = DateTime.from_unix!(System.system_time(:second), :second)
case changeset do
%{data: %{locked_at: nil}} -> Changeset.change(changeset, locked_at: locked_at)
changeset -> changeset
end
end
end
Add a lock action to your user context module:
defmodule MyApp.Users do
alias MyApp.Users.User
@spec lock(map()) :: {:ok, map()} | {:error, map()}
def lock(user) do
user
|> User.lock_changeset()
|> Repo.update()
end
end
Set up controller
Create or modify you user management controller so you (likely the admin) can lock the account:
defmodule MyAppWeb.Admin.UserController do
use MyAppWeb, :controller
plug :load_user when action in [:delete]
@spec delete(Plug.Conn.t(), map()) :: Plug.Conn.t()
def delete(%{assigns: %{user: user}} = conn, _params) do
case MyApp.Users.lock(user) do
{:ok, _user} -> # User has been locked
{:error, _changeset} -> # Something went wrong
end
end
defp load_user(%{params: %{"id" => user_id}} = conn, _opts) do
config = Pow.Plug.fetch_config(conn)
case Pow.Ecto.Context.get_by([id: user_id], config) do
nil -> # Invalid user id
user -> Plug.Conn.assign(conn, :user, user)
end
end
end
Remember to add this route to your router.ex
file.
Prevent sign in for locked users
This is all you need to ensure locked users can't sign in:
defmodule MyAppWeb.EnsureUserNotLockedPlug do
@moduledoc """
This plug ensures that a user isn't locked.
## Example
plug MyAppWeb.EnsureUserNotLockedPlug
"""
alias MyAppWeb.Router.Helpers, as: Routes
alias Phoenix.Controller
alias Plug.Conn
alias Pow.Plug
@doc false
@spec init(any()) :: any()
def init(opts), do: opts
@doc false
@spec call(Conn.t(), any()) :: Conn.t()
def call(conn, _opts) do
conn
|> Plug.current_user()
|> locked?()
|> maybe_halt(conn)
end
defp locked?(%{locked_at: locked_at}) when not is_nil(locked_at), do: true
defp locked?(_user), do: false
defp maybe_halt(true, conn) do
{:ok, conn} = Plug.clear_authenticated_user(conn)
conn
|> Controller.put_flash(:error, "Sorry, your account is locked.")
|> Controller.redirect(to: Routes.pow_session_path(conn, :new))
end
defp maybe_halt(_any, conn), do: conn
end
Add plug MyAppWeb.EnsureUserNotLockedPlug
to your endpoint or pipeline, and presto!
Optional: PowResetPassword
The above will prevent any locked users access, but it doesn't prevent them from using features that doesn't require authentication such as resetting their password. Be advised that this is a entirely optional step as this only affects UX.
While there are many different ways of handling this, the most explicit one is to simply override the logic entirely with a custom controller:
defmodule MyAppWeb.ResetPasswordController do
use MyAppWeb, :controller
alias PowResetPassword.{Phoenix.ResetPasswordController, Plug, ResetTokenCache}
def create(conn, params) do
conn
|> ResetPasswordController.process_create(params)
|> maybe_halt()
|> ResetPasswordController.respond_create()
end
defp maybe_halt({:ok, %{token: token, user: %{locked_at: locked_at}}, conn}) when not is_nil(locked_at) do
user = Plug.change_user(conn)
expire_token(conn, token)
{:error, %{user | action: :update}, conn}
end
defp maybe_halt(response), do: response
defp expire_token(conn, token) do
backend =
conn
|> Pow.Plug.fetch_config()
|> Pow.Config.get(:cache_store_backend, Pow.Store.Backend.EtsCache)
ResetTokenCache.delete([backend: backend], token)
end
end
To make the code simpler for us we're leveraging the methods from PowResetPassword.Phoenix.ResetPasswordController
here.
Now all we got to do is to catch the route before the pow_extension_routes/0
call:
defmodule MyAppWeb.Router do
use MyAppWeb, :router
use Pow.Phoenix.Router
use Pow.Extension.Phoenix.Router, otp_app: :my_app
# ...
scope "/", MyAppWeb do
pipe_through :browser
post "/reset-password", ResetPasswordController, :create
end
scope "/" do
pow_routes()
pow_extension_routes()
end
# ...
end