Add captcha to registrations (fixes #9)

This commit is contained in:
Timothée Floure 2021-02-03 10:12:23 +01:00
parent a1dcc36dab
commit 3dfc9115a1
Signed by: tfloure
GPG Key ID: 4502C902C00A1E12
6 changed files with 52 additions and 16 deletions

View File

@ -17,6 +17,7 @@ defmodule RecycledCloud.Accounts.User do
field :email, :string, virtual: true field :email, :string, virtual: true
field :confirmed_at, :naive_datetime field :confirmed_at, :naive_datetime
field :partner_id, :integer field :partner_id, :integer
field :captcha, :integer, virtual: :true
has_many :keys, Accounts.Key has_many :keys, Accounts.Key
@ -151,8 +152,13 @@ defmodule RecycledCloud.Accounts.User do
also be very expensive to hash for certain algorithms. also be very expensive to hash for certain algorithms.
""" """
def registration_changeset(user, attrs) do def registration_changeset(user, attrs) do
expected_captcha_result = attrs |> Map.get("expected_captcha")
user user
|> insertion_changeset(attrs) |> cast(attrs, [:username, :password, :email, :dn, :captcha])
|> RecycledCloud.Captcha.validate(expected_captcha_result)
|> validate_username()
|> validate_email()
|> validate_password() |> validate_password()
end end
@ -160,6 +166,7 @@ defmodule RecycledCloud.Accounts.User do
user user
|> cast(attrs, [:username, :password, :email, :dn]) |> cast(attrs, [:username, :password, :email, :dn])
|> validate_email() |> validate_email()
|> validate_username()
end end
defp validate_email(changeset) do defp validate_email(changeset) do
@ -178,6 +185,11 @@ defmodule RecycledCloud.Accounts.User do
#|> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: "at least one digit or punctuation character") #|> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: "at least one digit or punctuation character")
end end
defp validate_username(changeset) do
changeset
|> validate_required([:username])
end
@doc """ @doc """
A user changeset for changing the email. A user changeset for changing the email.

View File

@ -3,9 +3,13 @@ defmodule RecycledCloud.Captcha do
# https://hex.pm/packages/captcha since it would break accessibility for # https://hex.pm/packages/captcha since it would break accessibility for
# blind people. Let's make it simple maths instead! # blind people. Let's make it simple maths instead!
# Captcha expression tuning:
@terms 1..6
@operands ["+", "-", "*"]
def validate(changeset, expected_result) do def validate(changeset, expected_result) do
captcha_result = Ecto.Changeset.get_field(changeset, :captcha) result = Ecto.Changeset.get_field(changeset, :captcha)
if captcha_result == expected_result do if result == expected_result do
changeset changeset
else else
Ecto.Changeset.add_error(changeset, :captcha, "does not match") Ecto.Changeset.add_error(changeset, :captcha, "does not match")
@ -13,12 +17,9 @@ defmodule RecycledCloud.Captcha do
end end
def generate do def generate do
terms = 1..6 left = @terms |> Enum.random
operands = ["+", "-", "*"] right = @terms |> Enum.random
operation = @operands |> Enum.random
left = terms |> Enum.random
right = terms |> Enum.random
operation = operands |> Enum.random
text = "#{left} #{operation} #{right}" text = "#{left} #{operation} #{right}"
value = case operation do value = case operation do

View File

@ -1,6 +1,7 @@
defmodule RecycledCloudWeb.UserRegistrationController do defmodule RecycledCloudWeb.UserRegistrationController do
use RecycledCloudWeb, :controller use RecycledCloudWeb, :controller
alias RecycledCloud.Captcha
alias RecycledCloud.Accounts alias RecycledCloud.Accounts
alias RecycledCloud.Accounts.User alias RecycledCloud.Accounts.User
alias RecycledCloudWeb.UserAuth alias RecycledCloudWeb.UserAuth
@ -11,6 +12,7 @@ defmodule RecycledCloudWeb.UserRegistrationController do
def new(conn, _params) do def new(conn, _params) do
changeset = Accounts.change_user_registration(%User{}) changeset = Accounts.change_user_registration(%User{})
{captcha_text, captcha_result} = Captcha.generate()
with_notice = unless is_registration_enabled?() do with_notice = unless is_registration_enabled?() do
conn conn
@ -20,17 +22,20 @@ defmodule RecycledCloudWeb.UserRegistrationController do
end end
with_notice with_notice
|> assign(:captcha, captcha_text)
|> put_session(:captcha, captcha_result)
|> render("new.html", changeset: changeset) |> render("new.html", changeset: changeset)
end end
def create(conn, %{"user" => user_params}) do def create(conn, %{"user" => user_params}) do
changeset = Accounts.change_user_registration(%User{})
unless is_registration_enabled?() do unless is_registration_enabled?() do
conn conn
|> redirect(to: Routes.user_registration_path(conn, :new)) |> redirect(to: Routes.user_registration_path(conn, :new))
else else
case Accounts.register_user(user_params) do attrs = user_params
|> Map.put("expected_captcha", get_session(conn, :captcha))
case Accounts.register_user(attrs) do
{:ok, user} -> {:ok, user} ->
{:ok, _} = {:ok, _} =
Accounts.deliver_user_confirmation_instructions( Accounts.deliver_user_confirmation_instructions(
@ -43,7 +48,12 @@ defmodule RecycledCloudWeb.UserRegistrationController do
|> UserAuth.log_in_user(user) |> UserAuth.log_in_user(user)
{:error, %Ecto.Changeset{} = changeset} -> {:error, %Ecto.Changeset{} = changeset} ->
render(conn, "new.html", changeset: changeset) {captcha_text, captcha_result} = Captcha.generate()
conn
|> assign(:captcha, captcha_text)
|> put_session(:captcha, captcha_result)
|> render("new.html", changeset: changeset)
end end
end end
end end

View File

@ -23,7 +23,7 @@
<br /> <br />
<p> <p>
Can you answer the following expression to confirm that you are not basic Can you answer the following expression to confirm you are not basic
robot? robot?
<br /> <br />

View File

@ -22,6 +22,18 @@
<br /> <br />
<p>
Can you answer the following expression to confirm you are not basic robot?
<br />
<b><%= @captcha %> = </b>
<%= text_input f, :captcha, placeholder: "Captcha" %>
<%= error_tag f, :captcha %>
</p>
<div> <div>
<%= submit "Register" %> <%= submit "Register" %>
</div> </div>

View File

@ -95,15 +95,16 @@ defmodule RecycledCloud.AccountsTest do
describe "change_user_registration/2" do describe "change_user_registration/2" do
test "returns a changeset" do test "returns a changeset" do
assert %Ecto.Changeset{} = changeset = Accounts.change_user_registration(%User{}) assert %Ecto.Changeset{} = changeset = Accounts.change_user_registration(%User{})
assert changeset.required == [:password, :email] assert changeset.required == [:password, :email, :username]
end end
test "allows fields to be set" do test "allows fields to be set" do
username = unique_user_name()
email = unique_user_email() email = unique_user_email()
password = valid_user_password() password = valid_user_password()
changeset = changeset =
Accounts.change_user_registration(%User{}, %{"email" => email, "password" => password}) Accounts.change_user_registration(%User{}, %{"username" => username, "email" => email, "password" => password})
assert changeset.valid? assert changeset.valid?
assert get_change(changeset, :email) == email assert get_change(changeset, :email) == email