Add captcha to support requests

This commit is contained in:
Timothée Floure 2021-02-03 09:37:00 +01:00
parent a9a65adb8c
commit a1dcc36dab
Signed by: tfloure
GPG key ID: 4502C902C00A1E12
4 changed files with 69 additions and 4 deletions

View file

@ -0,0 +1,32 @@
defmodule RecycledCloud.Captcha do
# We can't use a graphical-only captcha library such as
# https://hex.pm/packages/captcha since it would break accessibility for
# blind people. Let's make it simple maths instead!
def validate(changeset, expected_result) do
captcha_result = Ecto.Changeset.get_field(changeset, :captcha)
if captcha_result == expected_result do
changeset
else
Ecto.Changeset.add_error(changeset, :captcha, "does not match")
end
end
def generate do
terms = 1..6
operands = ["+", "-", "*"]
left = terms |> Enum.random
right = terms |> Enum.random
operation = operands |> Enum.random
text = "#{left} #{operation} #{right}"
value = case operation do
"+" -> left + right
"-" -> left - right
"*" -> left * right
end
{text, value}
end
end

View file

@ -5,19 +5,24 @@ defmodule RecycledCloud.SupportRequest do
alias RecycledCloud.SupportRequest alias RecycledCloud.SupportRequest
alias RecycledCloud.Mailer alias RecycledCloud.Mailer
alias RecycledCloud.Captcha
embedded_schema do embedded_schema do
field :name, :string field :name, :string
field :email, :string field :email, :string
field :message, :string field :message, :string
field :captcha, :integer
timestamps() timestamps()
end end
def changeset(key, attrs) do def changeset(key, attrs) do
expected_captcha_result = attrs |> Map.get("expected_captcha")
key key
|> cast(attrs, [:name, :email, :message]) |> cast(attrs, [:name, :email, :message, :captcha])
|> validate_required([:name, :email, :message]) |> validate_required([:name, :email, :message, :captcha])
|> Captcha.validate(expected_captcha_result)
end end
def send(%SupportRequest{} = request) do def send(%SupportRequest{} = request) do

View file

@ -2,18 +2,26 @@ defmodule RecycledCloudWeb.SupportController do
use RecycledCloudWeb, :controller use RecycledCloudWeb, :controller
alias RecycledCloud.SupportRequest alias RecycledCloud.SupportRequest
alias RecycledCloud.Captcha
def new(conn, _params) do def new(conn, _params) do
{captcha_text, captcha_result} = Captcha.generate()
conn conn
|> assign(:request_changeset, SupportRequest.changeset(%SupportRequest{}, %{})) |> assign(:request_changeset, SupportRequest.changeset(%SupportRequest{}, %{}))
|> assign(:captcha, captcha_text)
|> put_session(:captcha, captcha_result)
|> render("new.html") |> render("new.html")
end end
def create(conn, %{"support_request" => request} = params) do def create(conn, %{"support_request" => raw} = params) do
action = SupportRequest.changeset(%SupportRequest{}, request) attrs = raw |> Map.put("expected_captcha", get_session(conn, :captcha))
action = SupportRequest.changeset(%SupportRequest{}, attrs)
|> Ecto.Changeset.apply_action(:update) |> Ecto.Changeset.apply_action(:update)
case action do case action do
{:ok, request} -> {:ok, request} ->
# FIXME: check for error?
SupportRequest.send(request) SupportRequest.send(request)
conn conn
@ -21,8 +29,12 @@ defmodule RecycledCloudWeb.SupportController do
|> redirect(to: Routes.page_path(conn, :index)) |> redirect(to: Routes.page_path(conn, :index))
{:error, changeset} -> {:error, changeset} ->
{captcha_text, captcha_result} = Captcha.generate()
conn conn
|> assign(:request_changeset, changeset) |> assign(:request_changeset, changeset)
|> assign(:captcha, captcha_text)
|> put_session(:captcha, captcha_result)
|> render("new.html") |> render("new.html")
end end
end end

View file

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