mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-23 20:18:00 +01:00
add mailer, password resets, lockouts
This commit is contained in:
parent
31d637cca0
commit
76886c5329
25 changed files with 564 additions and 59 deletions
|
@ -20,8 +20,9 @@ config :philomena, :pow,
|
|||
user: Philomena.Users.User,
|
||||
repo: Philomena.Repo,
|
||||
web_module: PhilomenaWeb,
|
||||
extensions: [PowResetPassword, PowPersistentSession],
|
||||
controller_callbacks: Pow.Extension.Phoenix.ControllerCallbacks
|
||||
extensions: [PowResetPassword, PowLockout, PowPersistentSession],
|
||||
controller_callbacks: Pow.Extension.Phoenix.ControllerCallbacks,
|
||||
mailer_backend: PhilomenaWeb.PowMailer
|
||||
|
||||
config :bcrypt_elixir,
|
||||
log_rounds: 12
|
||||
|
@ -54,6 +55,7 @@ config :logger, :console,
|
|||
|
||||
# Use Jason for JSON parsing in Phoenix
|
||||
config :phoenix, :json_library, Jason
|
||||
config :bamboo, :json_library, Jason
|
||||
|
||||
# Import environment specific config. This must remain at the bottom
|
||||
# of this file so it overrides the configuration defined above.
|
||||
|
|
|
@ -62,6 +62,13 @@ config :philomena, PhilomenaWeb.Endpoint,
|
|||
# Do not include metadata nor timestamps in development logs
|
||||
config :logger, :console, format: "[$level] $message\n"
|
||||
|
||||
# Set up mailer
|
||||
config :philomena, PhilomenaWeb.Mailer,
|
||||
adapter: Bamboo.LocalAdapter
|
||||
|
||||
config :philomena, :mailer_address,
|
||||
"noreply@philomena.lc"
|
||||
|
||||
# Set a higher stacktrace during development. Avoid configuring such
|
||||
# in production as building large stacktraces may be expensive.
|
||||
config :phoenix, :stacktrace_depth, 20
|
||||
|
|
|
@ -30,6 +30,16 @@ config :philomena, Philomena.Repo,
|
|||
url: database_url,
|
||||
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
|
||||
|
||||
config :philomena, PhilomenaWeb.Mailer,
|
||||
adapter: Bamboo.SMTPAdapter,
|
||||
server: System.get_env("SMTP_RELAY"),
|
||||
hostname: System.get_env("SMTP_DOMAIN"),
|
||||
port: System.get_env("SMTP_PORT") || 587,
|
||||
username: System.get_env("SMTP_USERNAME"),
|
||||
password: System.get_env("SMTP_PASSWORD"),
|
||||
tls: :always,
|
||||
auth: :always
|
||||
|
||||
secret_key_base =
|
||||
System.get_env("SECRET_KEY_BASE") ||
|
||||
raise """
|
||||
|
|
|
@ -8,7 +8,7 @@ defmodule Philomena.Users.User do
|
|||
password_min_length: 6
|
||||
|
||||
use Pow.Extension.Ecto.Schema,
|
||||
extensions: [PowResetPassword, PowPersistentSession]
|
||||
extensions: [PowResetPassword, PowLockout, PowPersistentSession]
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
|
@ -37,10 +37,10 @@ defmodule Philomena.Users.User do
|
|||
field :current_sign_in_ip, EctoNetwork.INET
|
||||
field :last_sign_in_ip, EctoNetwork.INET
|
||||
field :otp_required_for_login, :boolean
|
||||
field :failed_attempts, :integer
|
||||
field :authentication_token, :string
|
||||
field :unlock_token, :string
|
||||
field :locked_at, :naive_datetime
|
||||
# field :failed_attempts, :integer
|
||||
# field :unlock_token, :string
|
||||
# field :locked_at, :naive_datetime
|
||||
field :encrypted_otp_secret, :string
|
||||
field :encrypted_otp_secret_iv, :string
|
||||
field :encrypted_otp_secret_salt, :string
|
||||
|
@ -117,6 +117,16 @@ defmodule Philomena.Users.User do
|
|||
|> validate_required([])
|
||||
end
|
||||
|
||||
def failure_changeset(user) do
|
||||
changeset = change(user)
|
||||
user = changeset.data
|
||||
|
||||
user
|
||||
|> change(%{
|
||||
failed_attempts: user.failed_attempts + 1,
|
||||
})
|
||||
end
|
||||
|
||||
def create_totp_secret_changeset(user) do
|
||||
secret = :crypto.strong_rand_bytes(15) |> Base.encode32()
|
||||
data = Philomena.Users.Encryptor.encrypt_model(secret)
|
||||
|
@ -233,7 +243,7 @@ defmodule Philomena.Users.User do
|
|||
do: ""
|
||||
|
||||
defp totp_valid?(user, token),
|
||||
do: :pot.valid_totp(token, totp_secret(user), window: 60)
|
||||
do: :pot.valid_totp(token, totp_secret(user), window: 1)
|
||||
|
||||
defp backup_code_valid?(user, token),
|
||||
do: Enum.any?(user.otp_backup_codes, &Password.verify_pass(token, &1))
|
||||
|
|
3
lib/philomena_web/mailer.ex
Normal file
3
lib/philomena_web/mailer.ex
Normal file
|
@ -0,0 +1,3 @@
|
|||
defmodule PhilomenaWeb.Mailer do
|
||||
use Bamboo.Mailer, otp_app: :philomena
|
||||
end
|
|
@ -1,38 +0,0 @@
|
|||
defmodule PhilomenaWeb.Plugs.EnsureUserNotLockedPlug do
|
||||
@moduledoc """
|
||||
This plug ensures that a user isn't locked.
|
||||
|
||||
## Example
|
||||
|
||||
plug PhilomenaWeb.Plugs.EnsureUserNotLockedPlug
|
||||
"""
|
||||
alias PhilomenaWeb.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
|
22
lib/philomena_web/pow_mailer.ex
Normal file
22
lib/philomena_web/pow_mailer.ex
Normal file
|
@ -0,0 +1,22 @@
|
|||
defmodule PhilomenaWeb.PowMailer do
|
||||
use Pow.Phoenix.Mailer
|
||||
alias PhilomenaWeb.Mailer
|
||||
alias Philomena.Users.User
|
||||
import Bamboo.Email
|
||||
|
||||
def cast(%{user: %User{email: email}, subject: subject, text: text, html: html, assigns: _assigns}) do
|
||||
# Build email struct to be used in `process/1`
|
||||
new_email(
|
||||
to: email,
|
||||
from: Application.get_env(:philomena, :mailer_address),
|
||||
subject: subject,
|
||||
text_body: text,
|
||||
html_body: html
|
||||
)
|
||||
end
|
||||
|
||||
def process(email) do
|
||||
email
|
||||
|> Mailer.deliver_later()
|
||||
end
|
||||
end
|
|
@ -12,7 +12,6 @@ defmodule PhilomenaWeb.Router do
|
|||
plug PhilomenaWeb.Plugs.ImageFilter
|
||||
plug PhilomenaWeb.Plugs.Pagination
|
||||
plug PhilomenaWeb.Plugs.EnsureUserEnabledPlug
|
||||
plug PhilomenaWeb.Plugs.EnsureUserNotLockedPlug
|
||||
end
|
||||
|
||||
pipeline :api do
|
||||
|
@ -28,27 +27,27 @@ defmodule PhilomenaWeb.Router do
|
|||
error_handler: Pow.Phoenix.PlugErrorHandler
|
||||
end
|
||||
|
||||
pipeline :not_authenticated do
|
||||
plug Pow.Plug.RequireNotAuthenticated,
|
||||
error_handler: Pow.Phoenix.PlugErrorHandler
|
||||
end
|
||||
|
||||
scope "/" do
|
||||
pipe_through [:browser, :ensure_totp]
|
||||
|
||||
pow_routes()
|
||||
pow_session_routes()
|
||||
pow_extension_routes()
|
||||
end
|
||||
|
||||
scope "/", Pow.Phoenix, as: "pow" do
|
||||
pipe_through [:browser, :protected, :ensure_totp]
|
||||
resources "/registration", RegistrationController, singleton: true, only: [:edit, :update]
|
||||
end
|
||||
|
||||
scope "/", PhilomenaWeb do
|
||||
pipe_through [:browser, :protected]
|
||||
|
||||
# Additional routes for TOTP
|
||||
scope "/registration", Registration, as: :registration do
|
||||
scope "/registrations", Registration, as: :registration do
|
||||
resources "/totp", TotpController, only: [:edit, :update], singleton: true
|
||||
end
|
||||
|
||||
scope "/session", Session, as: :session do
|
||||
scope "/sessions", Session, as: :session do
|
||||
resources "/totp", TotpController, only: [:new, :create], singleton: true
|
||||
end
|
||||
end
|
||||
|
|
|
@ -28,6 +28,10 @@ h1 Register
|
|||
= password_input f, :confirm_password, class: "input", placeholder: "Confirm password", required: true
|
||||
= error_tag f, :confirm_password
|
||||
|
||||
.field
|
||||
= checkbox f, :captcha, class: "js-captcha", value: 0
|
||||
= label f, :captcha, "I am not a robot!"
|
||||
|
||||
br
|
||||
|
||||
.block.block--fixed.block--warning
|
||||
|
|
|
@ -19,10 +19,6 @@ h1 Sign in
|
|||
= checkbox f, :persistent_session
|
||||
= label f, :persistent_session, "Remember me"
|
||||
|
||||
.field
|
||||
= checkbox f, :captcha, class: "js-captcha", value: 0
|
||||
= label f, :captcha, "I am not a robot!"
|
||||
|
||||
= submit "Sign in", class: "button"
|
||||
|
||||
p
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
h1 Reset password
|
||||
|
||||
= form_for @changeset, @action, [as: :user], fn f ->
|
||||
= if @changeset.action do
|
||||
.alert.alert-danger
|
||||
p Oops, something went wrong! Please check the errors below.
|
||||
|
||||
.field
|
||||
= password_input f, :password, class: "input", placeholder: "New password"
|
||||
= error_tag f, :password
|
||||
|
||||
.field
|
||||
= password_input f, :confirm_password, class: "input", placeholder: "Confirm password"
|
||||
= error_tag f, :confirm_password
|
||||
|
||||
= submit "Submit", class: "button"
|
|
@ -0,0 +1,14 @@
|
|||
h1 Forgot your password?
|
||||
p
|
||||
' Provide the email address you signed up with and we will email you
|
||||
' password reset instructions.
|
||||
|
||||
= form_for @changeset, @action, [as: :user], fn f ->
|
||||
= if @changeset.action do
|
||||
.alert.alert-danger
|
||||
p Oops, something went wrong! Please check the errors below.
|
||||
|
||||
.field
|
||||
= text_input f, :email, class: "input", placeholder: "Email"
|
||||
|
||||
= submit "Submit", class: "button"
|
|
@ -0,0 +1,3 @@
|
|||
defmodule PhilomenaWeb.PowResetPassword.ResetPasswordView do
|
||||
use PhilomenaWeb, :view
|
||||
end
|
59
lib/pow_lockout/ecto/context.ex
Normal file
59
lib/pow_lockout/ecto/context.ex
Normal file
|
@ -0,0 +1,59 @@
|
|||
defmodule PowLockout.Ecto.Context do
|
||||
@moduledoc """
|
||||
Handles lockout context for user.
|
||||
"""
|
||||
alias Pow.{Config, Ecto.Context}
|
||||
alias PowLockout.Ecto.Schema
|
||||
|
||||
@doc """
|
||||
Finds a user by the `:unlock_token` column.
|
||||
"""
|
||||
@spec get_by_unlock_token(binary(), Config.t()) :: Context.user() | nil
|
||||
def get_by_unlock_token(token, config),
|
||||
do: Context.get_by([unlock_token: token], config)
|
||||
|
||||
@doc """
|
||||
Checks if the user is current locked out.
|
||||
"""
|
||||
@spec locked_out?(Context.user(), Config.t()) :: boolean()
|
||||
def locked_out?(%{locked_at: time}, _config) when not is_nil(time),
|
||||
do: true
|
||||
def locked_out?(_user, _config),
|
||||
do: false
|
||||
|
||||
@doc """
|
||||
Unlocks the account.
|
||||
|
||||
See `PowLockout.Ecto.Schema.unlock_changeset/1`.
|
||||
"""
|
||||
@spec unlock_account(Context.user(), Config.t()) :: {:ok, Context.user()} | {:error, Context.changeset()}
|
||||
def unlock_account(user, config) do
|
||||
user
|
||||
|> Schema.unlock_changeset()
|
||||
|> Context.do_update(config)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Increases the attempts counter and possibly locks the account.
|
||||
|
||||
See `PowLockout.Ecto.Schema.attempt_changeset/1`.
|
||||
"""
|
||||
@spec fail_attempt(Context.user(), Config.t()) :: {:ok, Context.user()} | {:error, Context.changeset()}
|
||||
def fail_attempt(user, config) do
|
||||
user
|
||||
|> Schema.attempt_changeset()
|
||||
|> Context.do_update(config)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sets the attempts counter to zero.
|
||||
|
||||
See `PowLockout.Ecto.Schema.attempt_reset_changeset/1`.
|
||||
"""
|
||||
@spec succeed_attempt(Context.user(), Config.t()) :: {:ok, Context.user()} | {:error, Context.changeset()}
|
||||
def succeed_attempt(user, config) do
|
||||
user
|
||||
|> Schema.attempt_reset_changeset()
|
||||
|> Context.do_update(config)
|
||||
end
|
||||
end
|
123
lib/pow_lockout/ecto/schema.ex
Normal file
123
lib/pow_lockout/ecto/schema.ex
Normal file
|
@ -0,0 +1,123 @@
|
|||
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
|
100
lib/pow_lockout/phoenix/controllers/controller_callbacks.ex
Normal file
100
lib/pow_lockout/phoenix/controllers/controller_callbacks.ex
Normal file
|
@ -0,0 +1,100 @@
|
|||
defmodule PowLockout.Phoenix.ControllerCallbacks do
|
||||
@moduledoc """
|
||||
Controller callback logic for e-mail confirmation.
|
||||
|
||||
### User is locked out
|
||||
|
||||
Triggers on `Pow.Phoenix.SessionController.create/2`.
|
||||
|
||||
When a user is locked out, the credentials will be treated as if they were
|
||||
invalid and the user will be redirected back to `Pow.Phoenix.Routes.`.
|
||||
|
||||
### User successfully authenticates
|
||||
|
||||
Triggers on `Pow.Phoenix.SessionController.create/2`.
|
||||
|
||||
When a user successfully signs in, the failed attempts counter will be
|
||||
reset to zero.
|
||||
|
||||
### Users unsuccessfully authenticates
|
||||
|
||||
Triggers on `Pow.Phoenix.SessionController.create/2`.
|
||||
|
||||
When a user unsuccessfully signs in, the failed attempts counter will be
|
||||
incremented, and the user may be locked out.
|
||||
|
||||
See `PowLockout.Ecto.Schema` for more.
|
||||
"""
|
||||
use Pow.Extension.Phoenix.ControllerCallbacks.Base
|
||||
|
||||
alias Plug.Conn
|
||||
alias Pow.Plug
|
||||
alias Phoenix.Controller
|
||||
alias PowLockout.Phoenix.{UnlockController, Mailer}
|
||||
alias PowLockout.Plug, as: PowLockoutPlug
|
||||
|
||||
@doc false
|
||||
@impl true
|
||||
def before_respond(Pow.Phoenix.SessionController, :create, {result, conn}, _config) do
|
||||
PowLockoutPlug.user_for_attempts(conn)
|
||||
|> maybe_fail_attempt(conn, result)
|
||||
end
|
||||
|
||||
defp maybe_fail_attempt(nil, conn, result),
|
||||
do: {result, conn}
|
||||
|
||||
defp maybe_fail_attempt(%{locked_at: nil} = user, conn, :ok) do
|
||||
case PowLockoutPlug.succeed_attempt(conn, user) do
|
||||
{:error, _changeset, conn} ->
|
||||
{:halt, conn}
|
||||
|
||||
{:ok, _user, conn} ->
|
||||
{:ok, conn}
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_fail_attempt(_locked_user, conn, :ok) do
|
||||
{:error, invalid_credentials(conn)}
|
||||
end
|
||||
|
||||
defp maybe_fail_attempt(user, conn, _error) do
|
||||
PowLockoutPlug.fail_attempt(conn, user)
|
||||
|> case do
|
||||
{:error, _changeset, conn} ->
|
||||
{:halt, conn}
|
||||
|
||||
{:ok, %{locked_at: nil}, conn} ->
|
||||
{:error, invalid_credentials(conn)}
|
||||
|
||||
{:ok, user, conn} ->
|
||||
send_unlock_email(user, conn)
|
||||
|
||||
{:error, invalid_credentials(conn)}
|
||||
end
|
||||
end
|
||||
|
||||
defp invalid_credentials(conn) do
|
||||
{:ok, conn} =
|
||||
Plug.clear_authenticated_user(conn)
|
||||
|
||||
conn
|
||||
|> Conn.assign(:changeset, Plug.change_user(conn, conn.params["user"]))
|
||||
|> Controller.put_flash(:error, messages(conn).invalid_credentials(conn))
|
||||
|> Controller.render("new.html")
|
||||
end
|
||||
|
||||
@doc """
|
||||
Sends an unlock e-mail to the user.
|
||||
"""
|
||||
@spec send_unlock_email(map(), Conn.t()) :: any()
|
||||
def send_unlock_email(user, conn) do
|
||||
url = unlock_url(conn, user.unlock_token)
|
||||
email = Mailer.email_unlock(conn, user, url)
|
||||
|
||||
Pow.Phoenix.Mailer.deliver(conn, email)
|
||||
end
|
||||
|
||||
defp unlock_url(conn, token) do
|
||||
routes(conn).url_for(conn, UnlockController, :show, [token])
|
||||
end
|
||||
end
|
22
lib/pow_lockout/phoenix/controllers/unlock_controller.ex
Normal file
22
lib/pow_lockout/phoenix/controllers/unlock_controller.ex
Normal file
|
@ -0,0 +1,22 @@
|
|||
defmodule PowLockout.Phoenix.UnlockController do
|
||||
@moduledoc false
|
||||
use Pow.Extension.Phoenix.Controller.Base
|
||||
|
||||
alias Plug.Conn
|
||||
alias PowLockout.Plug
|
||||
|
||||
@spec process_show(Conn.t(), map()) :: {:ok | :error, map(), Conn.t()}
|
||||
def process_show(conn, %{"id" => token}), do: Plug.unlock_account(conn, token)
|
||||
|
||||
@spec respond_show({:ok | :error, map(), Conn.t()}) :: Conn.t()
|
||||
def respond_show({:ok, _user, conn}) do
|
||||
conn
|
||||
|> put_flash(:info, extension_messages(conn).account_has_been_unlocked(conn))
|
||||
|> redirect(to: routes(conn).session_path(conn, :new))
|
||||
end
|
||||
def respond_show({:error, _changeset, conn}) do
|
||||
conn
|
||||
|> put_flash(:error, extension_messages(conn).account_unlock_failed(conn))
|
||||
|> redirect(to: routes(conn).session_path(conn, :new))
|
||||
end
|
||||
end
|
11
lib/pow_lockout/phoenix/mailers/mailer.ex
Normal file
11
lib/pow_lockout/phoenix/mailers/mailer.ex
Normal file
|
@ -0,0 +1,11 @@
|
|||
defmodule PowLockout.Phoenix.Mailer do
|
||||
@moduledoc false
|
||||
alias Plug.Conn
|
||||
alias Pow.Phoenix.Mailer.Mail
|
||||
alias PowLockout.Phoenix.MailerView
|
||||
|
||||
@spec email_unlock(Conn.t(), map(), binary()) :: Mail.t()
|
||||
def email_unlock(conn, user, url) do
|
||||
Mail.new(conn, user, {MailerView, :email_unlock}, url: url)
|
||||
end
|
||||
end
|
23
lib/pow_lockout/phoenix/mailers/mailer_template.ex
Normal file
23
lib/pow_lockout/phoenix/mailers/mailer_template.ex
Normal file
|
@ -0,0 +1,23 @@
|
|||
defmodule PowLockout.Phoenix.MailerTemplate do
|
||||
@moduledoc false
|
||||
use Pow.Phoenix.Mailer.Template
|
||||
|
||||
template :email_unlock,
|
||||
"Unlock your account",
|
||||
"""
|
||||
Hi,
|
||||
|
||||
Your account has been automatically disabled due to too many unsuccessful
|
||||
attempts to sign in.
|
||||
|
||||
Please use the following link to unlock your account:
|
||||
|
||||
<%= @url %>
|
||||
""",
|
||||
"""
|
||||
<%= content_tag(:h3, "Hi,") %>
|
||||
<%= content_tag(:p, "Your account has been automatically disabled due to too many unsuccessful attempts to sign in.") %>
|
||||
<%= content_tag(:p, "Please use the following link to unlock your account:") %>
|
||||
<%= content_tag(:p, link(@url, to: @url)) %>
|
||||
"""
|
||||
end
|
4
lib/pow_lockout/phoenix/mailers/mailer_view.ex
Normal file
4
lib/pow_lockout/phoenix/mailers/mailer_view.ex
Normal file
|
@ -0,0 +1,4 @@
|
|||
defmodule PowLockout.Phoenix.MailerView do
|
||||
@moduledoc false
|
||||
use Pow.Phoenix.Mailer.View
|
||||
end
|
13
lib/pow_lockout/phoenix/messages.ex
Normal file
13
lib/pow_lockout/phoenix/messages.ex
Normal file
|
@ -0,0 +1,13 @@
|
|||
defmodule PowLockout.Phoenix.Messages do
|
||||
@moduledoc false
|
||||
|
||||
@doc """
|
||||
Flash message to show when account has been unlocked.
|
||||
"""
|
||||
def account_has_been_unlocked(_conn), do: "Account successfully unlocked. You may now log in."
|
||||
|
||||
@doc """
|
||||
Flash message to show when account couldn't be unlocked.
|
||||
"""
|
||||
def account_unlock_failed(_conn), do: "Account unlock failed."
|
||||
end
|
12
lib/pow_lockout/phoenix/router.ex
Normal file
12
lib/pow_lockout/phoenix/router.ex
Normal file
|
@ -0,0 +1,12 @@
|
|||
defmodule PowLockout.Phoenix.Router do
|
||||
@moduledoc false
|
||||
use Pow.Extension.Phoenix.Router.Base
|
||||
|
||||
alias Pow.Phoenix.Router
|
||||
|
||||
defmacro routes(_config) do
|
||||
quote location: :keep do
|
||||
Router.pow_resources "/unlock", UnlockController, only: [:show]
|
||||
end
|
||||
end
|
||||
end
|
85
lib/pow_lockout/plug.ex
Normal file
85
lib/pow_lockout/plug.ex
Normal file
|
@ -0,0 +1,85 @@
|
|||
defmodule PowLockout.Plug do
|
||||
@moduledoc """
|
||||
Plug helper methods.
|
||||
"""
|
||||
alias Plug.Conn
|
||||
alias Pow.Plug
|
||||
alias PowLockout.Ecto.Context
|
||||
require IEx
|
||||
|
||||
@doc """
|
||||
Check if the current user is locked out.
|
||||
"""
|
||||
@spec locked_out?(Conn.t()) :: boolean()
|
||||
def locked_out?(conn) do
|
||||
config = Plug.fetch_config(conn)
|
||||
|
||||
conn
|
||||
|> Plug.current_user()
|
||||
|> Context.locked_out?(config)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get the user belonging to the id field.
|
||||
"""
|
||||
@spec user_for_attempts(Conn.t()) :: map() | nil
|
||||
def user_for_attempts(conn) do
|
||||
config = Plug.fetch_config(conn)
|
||||
id_field = Pow.Ecto.Schema.user_id_field(config)
|
||||
id_value = to_string(conn.params["user"][to_string(id_field)])
|
||||
|
||||
Pow.Ecto.Context.get_by([{id_field, id_value}], config)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unlocks the user found by the provided unlock token.
|
||||
"""
|
||||
@spec unlock_account(Conn.t(), binary()) :: {:ok, map(), Conn.t()} | {:error, map(), Conn.t()}
|
||||
def unlock_account(conn, token) do
|
||||
config = Plug.fetch_config(conn)
|
||||
|
||||
token
|
||||
|> Context.get_by_unlock_token(config)
|
||||
|> maybe_unlock_account(conn, config)
|
||||
end
|
||||
|
||||
defp maybe_unlock_account(nil, conn, _config) do
|
||||
{:error, nil, conn}
|
||||
end
|
||||
defp maybe_unlock_account(user, conn, config) do
|
||||
user
|
||||
|> Context.unlock_account(config)
|
||||
|> case do
|
||||
{:error, changeset} -> {:error, changeset, conn}
|
||||
{:ok, user} -> {:ok, user, conn}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Increments the failed attempts counter and possibly locks the user out.
|
||||
"""
|
||||
@spec fail_attempt(Conn.t(), map()) :: {:ok, map(), Conn.t()} | {:error, map(), Conn.t()}
|
||||
def fail_attempt(conn, user) do
|
||||
config = Plug.fetch_config(conn)
|
||||
|
||||
Context.fail_attempt(user, config)
|
||||
|> case do
|
||||
{:error, changeset} -> {:error, changeset, conn}
|
||||
{:ok, user} -> {:ok, user, conn}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Resets the failed attempts counter to 0.
|
||||
"""
|
||||
@spec succeed_attempt(Conn.t(), map()) :: {:ok, map(), Conn.t()} | {:error, map(), Conn.t()}
|
||||
def succeed_attempt(conn, user) do
|
||||
config = Plug.fetch_config(conn)
|
||||
|
||||
Context.succeed_attempt(user, config)
|
||||
|> case do
|
||||
{:error, changeset} -> {:error, changeset, conn}
|
||||
{:ok, user} -> {:ok, user, conn}
|
||||
end
|
||||
end
|
||||
end
|
4
mix.exs
4
mix.exs
|
@ -55,7 +55,9 @@ defmodule Philomena.MixProject do
|
|||
{:scrivener_ecto, "~> 2.0"},
|
||||
{:pbkdf2, "~> 2.0"},
|
||||
{:qrcode, "~> 0.1.5"},
|
||||
{:redix, "~> 0.10.2"}
|
||||
{:redix, "~> 0.10.2"},
|
||||
{:bamboo, "~> 1.2"},
|
||||
{:bamboo_smtp, "~> 1.7"}
|
||||
]
|
||||
end
|
||||
|
||||
|
|
3
mix.lock
3
mix.lock
|
@ -1,4 +1,6 @@
|
|||
%{
|
||||
"bamboo": {:hex, :bamboo, "1.3.0", "9ab7c054f1c3435464efcba939396c29c5e1b28f73c34e1f169e0881297a3141", [:mix], [{:hackney, ">= 1.13.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"bamboo_smtp": {:hex, :bamboo_smtp, "1.7.0", "f0d213e18ced1f08b551a72221e9b8cfbf23d592b684e9aa1ef5250f4943ef9b", [:mix], [{:bamboo, "~> 1.2", [hex: :bamboo, repo: "hexpm", optional: false]}, {:gen_smtp, "~> 0.14.0", [hex: :gen_smtp, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"bcrypt_elixir": {:hex, :bcrypt_elixir, "2.0.3", "64e0792d5b5064391927bf3b8e436994cafd18ca2d2b76dea5c76e0adcf66b7c", [:make, :mix], [{:comeonin, "~> 5.1", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"canada": {:hex, :canada, "1.0.2", "040e4c47609b0a67d5773ac1fbe5e99f840cef173d69b739beda7c98453e0770", [:mix], [], "hexpm"},
|
||||
"canary": {:hex, :canary, "1.1.1", "4138d5e05db8497c477e4af73902eb9ae06e49dceaa13c2dd9f0b55525ded48b", [:mix], [{:canada, "~> 1.0.1", [hex: :canada, repo: "hexpm", optional: false]}, {:ecto, ">= 1.1.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
|
@ -16,6 +18,7 @@
|
|||
"elixir2fa": {:hex, :elixir2fa, "0.1.0", "4585154695ad13a01c17c46e61b0884b0ce1569ebcc667d1d09cd1cbbb4f4ba8", [:mix], [], "hexpm"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm"},
|
||||
"file_system": {:hex, :file_system, "0.2.7", "e6f7f155970975789f26e77b8b8d8ab084c59844d8ecfaf58cbda31c494d14aa", [:mix], [], "hexpm"},
|
||||
"gen_smtp": {:hex, :gen_smtp, "0.14.0", "39846a03522456077c6429b4badfd1d55e5e7d0fdfb65e935b7c5e38549d9202", [:rebar3], [], "hexpm"},
|
||||
"gettext": {:hex, :gettext, "0.17.0", "abe21542c831887a2b16f4c94556db9c421ab301aee417b7c4fbde7fbdbe01ec", [:mix], [], "hexpm"},
|
||||
"hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"httpoison": {:hex, :httpoison, "1.6.0", "0a148c836e8e5fbec82c3cea37465a603bd42e314b73a8448ad50020757a00bd", [:mix], [{:hackney, "~> 1.15 and >= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
|
|
Loading…
Reference in a new issue