# ExRoboCop
[![Build Status](https://github.com/corneliakelinske/ex_robo_cop/workflows/Coveralls/badge.svg)](https://github.com/corneliakelinske/ex_robo_cop)
[![Build Status](https://github.com/corneliakelinske/ex_robo_cop/workflows/Credo/badge.svg)](https://github.com/corneliakelinske/ex_robo_cop)
[![codecov](https://codecov.io/gh/corneliakelinske/ex_robo_cop/branch/main/graph/badge.svg?token=P3O42SF7VJ)](https://codecov.io/gh/corneliakelinske/ex_robo_cop)
[![hex.pm](http://img.shields.io/hexpm/v/ex_robo_cop.svg?style=flat)](https://hex.pm/packages/ex_robo_cop)
[![Build Status](https://github.com/corneliakelinske/ex_robo_cop/workflows/Dialyzer/badge.svg)](https://github.com/corneliakelinske/ex_robo_cop)
[![Build Status](https://github.com/corneliakelinske/ex_robo_cop/workflows/Test/badge.svg)](https://github.com/corneliakelinske/ex_robo_cop)
ExRoboCop is a lightweight captcha library that can be used as an alternative to reCaptcha to verify that a person is
indeed a person and not a robot.
The library uses Rust to create a captcha image and corresponding text.
A GenServer creates a unique ID for each form in which a captcha image is used and stores the ID along with the captcha text
so that a user's input into the infamous "Not a Robot" field can be verified based on the form ID.
The use of this library requires the installation of Rust.
Thank you to [Alan Vardy](https://github.com/alanvardy) for writing the Rust code.
Documentation can be found at [https://hexdocs.pm/ex_robo_cop](https://hexdocs.pm/ex_robo_cop).
This library is in use at [connie.codes](https://connie.codes/).
And here is an example of a captcha image:
![Example captcha](Captcha.jpg)
## Installation
Add the package to your `mix.exs` file:
```elixir
def deps do
[
{:ex_robo_cop, "~> 0.1.5"}
]
end
```
Add the application to your supervision tree in the `application.ex` file, this is the GenServer that keeps track of the correct captcha answer and the form ID:
``` elixir
children = [
ExRoboCop.start()
... other children
]
```
And install Rust on your computer.
## For Mac users
If you have Rust installed but ExRoboCop fails to compile, try putting the following into your `~/.cargo/config`:
```
[target.x86_64-apple-darwin]
rustflags = [
"-C", "link-arg=-undefined",
"-C", "link-arg=dynamic_lookup",
]
[target.aarch64-apple-darwin]
rustflags = [
"-C", "link-arg=-undefined",
"-C", "link-arg=dynamic_lookup",
]
```
## Usage
`create_captcha\0`
creates a captcha text and a captcha image
`create_form_id\1`
creates a unique ID for a contact form and stores it in combination with the current captcha text
`not_a_robot?\1`
checks whether the combination of the user's answer to the "Not a Robot" question and the form ID match the form ID and captcha text stored in the GenServer
`get_answer_for_form_id/1`
returns the captcha text for a form ID. This can be a useful function for LiveView tests.
## Example
Assuming that you want to use `ex_robo_cop` to add a captcha to the content form on your website,
and that you are working with a `contact_controller.ex`, a `contact_view.ex` and a `new.html.heex` file, you can follow the steps below:
First of all, you need to create the captcha text, the captcha image and the id of the new contact form in the
`ContactController.new/2` function:
```elixir
{captcha_text, captcha_image} = ExRoboCop.create_captcha()
form_id = ExRoboCop.create_form_id(captcha_text)
```
The call to `ExRoboCop.create_form_id\1` stores the `form_id` and the `captcha_text` as a key-value pair in the GenServer.
The `form_id` and the `captcha_image` will then have to be passed into the assigns of the `render\3` function.
In the `ContactController` of my personal projects, the `new/2` function will typically look like this:
```elixir
def new(conn, _params) do
with {captcha_text, captcha_image} <- ExRoboCop.create_captcha() do
form_id = ExRoboCop.create_form_ID(captcha_text)
render(conn, "new.html",
page_title: "Contact",
changeset: Contact.changeset(%{}),
form_id: form_id,
captcha_image: captcha_image
)
end
end
```
The next step is rendering the captcha image in the contact form in your `.heex` template.
Since the image data is passed into the `render/3` assigns as binary, it needs to be converted in order to be displayed.
In Phoenix 1.7, all you have to do is add the captcha image as an `img` tag to your `heex` or `live` file:
```elixir
<img
src={"data:image/png;base64," <> @captcha_image}
alt="CAPTCHA"
class="mt-2 block w-full rounded-lg"
/>
```
In Phoenix 1.6, you can add the following function to your corresponding `view.ex` file:
```elixir
def display_captcha(captcha_image) do
content_tag(:img, "", src: "data:image/png;base64," <> captcha_image)
end
```
and then call this function in your `heex` template:
```html
<%= display_captcha(@captcha_image) %>
```
In Phoenix 1.5, you can convert the binary directly in the `.eex` template:
```html
<img src="data:image/png;base64,<%= Base.encode64(@captcha_image)%>">
```
The form also needs to include an input field for users to input the letters they see in the captcha image as well
as a hidden input field through which the `form_id` can be passed back to the controller when the form is submitted:
```html
<div class="field">
<%= label f, :not_a_robot, class: "label"%>
<div class="control">
<%= text_input f, :not_a_robot, class: "input", type: "text", placeholder: "Please enter the letters shown below" %>
</div>
</div>
<%= text_input f, :form_id, type: "text", hidden: true, value: @form_id %>
```
When the form is submitted, the `form_id` and the user's answer are sent back to the controller as part of the form content.
I suggest pattern matching on them in the head of the controller's `create/2` function for example like this:
```elixir
def create(conn, %{"content" => %{"not_a_robot" => captcha_answer, "form_id" => form_id} = message_params}) do
```
Now, you can pass the user's answer and the form_id as a tuple into `ExRoboCop.not_a_robot?\1` in order to verify that
the answer matches the captcha text stored for the respective `form_id` in the GenServer.
```elixir
:ok = not_a_robot?({captcha_answer, form_id})
```
## Use and tests in LiveView
In LiveView, the `form_id` (or even the `captcha_text`) can be stored in the `Socket.assigns`. In order to test the success case, the `form_id` (or the `captcha_text`, if this is stored in the `Socket.assigns` instead) needs to be retrieved as part of the testing process:
```elixir
{:ok, lv, _html} = live(conn, ~p"/contact")
socket_state = :sys.get_state(lv.pid)
form_id = socket_state.socket.assigns.form_id
```
## Production
Since `ex_robo_cop` requires Rust, you will need to add the command to install Rust to your `Dockerfile`:
```
# install build dependencies
RUN apt-get update -y && apt-get install -y build-essential git rustc\
&& apt-get clean && rm -f /var/lib/apt/lists/*_*
```
If this does not work for your deploy, try this instead:
```
# install build dependencies
RUN apt-get update -y && apt-get install -y build-essential curl git\
&& apt-get clean && rm -f /var/lib/apt/lists/*_*
# Get Rust
RUN curl https://sh.rustup.rs -sSf | bash -s -- -y
ENV PATH="/root/.cargo/bin:${PATH}"
```
## Known issues
Especially when upgrading to a new version of this library, it may be that compilation fails.
Running `mix deps.clean ex_robo_cop` usually helps.