# `MishkaCmsCore.Accounts.CaptchaCheck`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/accounts/_captcha_check.ex#L5)

The captcha (`MishkaCmsCore.Captcha`) in the actions a visitor fills a form for:
`sign_in_with_password`, `register_with_password`, `request_password_reset_token` and
`request_magic_link`, each taking the token as its `captcha_token` argument. Checked in the
action, so the sign-in pages and the `/api/auth` routes ask it alike.

## Where in the action

A `before_action` — a `before_transaction` on a create, so the provider is not asked inside a
database transaction — between the action's two rate limits:

    prepare rate_limit(per address)
    prepare &CaptchaCheck.prepare(&1, &2, :sign_in)
    prepare rate_limit(per account)

The per-address limit comes first, so one address sending tokens that fail costs the provider no
more requests than that limit lets through. The per-account limit comes after, so attempts that
never passed the captcha cannot use up an account's tries and lock its owner out. The password is
checked after both — a bot that fails the captcha never learns whether it was right — and a
registration is checked only once its fields are valid, so a typo does not spend the token.

## An extension's form

An extension's action checks its own form the same way, named by an atom of its own, and its
template draws the check with `<.captcha for={:that_atom}>` (`MishkaCmsCore.Captcha.Widget`).
The address is read from the same `ash_authentication_request` context, so the form hands it
the visitor's `%{remote_ip: address}`:

    prepare &MishkaCmsCore.Accounts.CaptchaCheck.prepare(&1, &2, :newsletter)

## Who is not asked

  * a scope that asks none — no provider, or one missing a key;
  * the JSON:API, whose requests `MishkaCmsCore.Runtime.Plugs.AuthRateLimitPlug` marks (`api?: true`
    in `ash_authentication_request`): its clients are apps, which have no captcha to show;
  * a call made with `authorize?: false` — the installer, the seeds, the console;
  * the second step of a password sign-in, once the first passed it: the sign-in page builds the
    code step's form with `captcha_passed?: true` in its context, which only server code can set.

## The answer

A refusal is a `MishkaCmsCore.Accounts.CaptchaCheck.Refused` and fires
`"auth:captcha:refused"` with `%{form, reason, detail, site_id}`. A provider that could not be
asked lets the form through and fires `"auth:captcha:unavailable"` with `%{form, detail,
site_id}`; both with `return: false`.

# `form`

```elixir
@type form() :: :sign_in | :register | :reset_request | :magic_link | atom()
```

# `change`

```elixir
@spec change(Ash.Changeset.t(), map(), form()) :: Ash.Changeset.t()
```

A change: checks the captcha of a create action's `form` before it runs — before its transaction,
so a slow provider holds no database connection.

# `describe`

```elixir
@spec describe(:missing | :failed) :: %{title: String.t(), text: String.t()}
```

What to tell whoever was refused, in their language.

# `prepare`

```elixir
@spec prepare(Ash.Query.t() | Ash.ActionInput.t(), map(), form()) ::
  Ash.Query.t() | Ash.ActionInput.t()
```

A preparation: checks the captcha of a read or generic action's `form` before it runs.

# `refusal`

```elixir
@spec refusal(term()) :: %{reason: :missing | :failed} | nil
```

The captcha refusal anywhere inside `reason`, or `nil`: an error or error class, a form's
`raw_errors`, or an `AshAuthentication.Errors.AuthenticationFailed`.

    CaptchaCheck.refusal(AshPhoenix.Form.raw_errors(form, for_path: :all))
    #=> %{reason: :missing}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
