README.md

# 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.