Packages
Drop-in, pluggable CAPTCHA components for Phoenix forms and LiveViews.
Current section
Files
Jump to
Current section
Files
README.md
# Wiztchta
Drop-in, pluggable CAPTCHA for Phoenix forms and LiveViews. No database, no
third-party service.
```heex
<.form for={@form} phx-submit="save">
<.input field={@form[:email]} />
<.wiztchta challenge={@challenge} />
<.button>Sign up</.button>
</.form>
```
- **Proof-of-work** (default): the browser solves a small SHA-256 puzzle in
about a second, invisibly. **Math**: "What is 3 + 4?", which works without JS.
You can also add your own type with the `Wiztchta.Challenge` behaviour.
- **Stateless**: each challenge travels as an encrypted, expiring token.
- **Replay protection**: each token can only be used once. Used tokens are
remembered in ETS by default, or in your own `Wiztchta.Store`.
## Installation
```elixir
# mix.exs
{:wiztchta, "~> 0.1"}
```
```elixir
# config/config.exs
config :wiztchta, endpoint: MyAppWeb.Endpoint
```
```elixir
# lib/my_app_web.ex, inside html_helpers
import Wiztchta.Components
```
```js
// assets/js/app.js (Phoenix's esbuild config already resolves packages in deps/)
import { Hooks as WiztchtaHooks, init as initWiztchta } from "wiztchta";
const liveSocket = new LiveSocket("/live", Socket, {
hooks: { ...colocatedHooks, ...WiztchtaHooks },
// ...
});
initWiztchta(); // only needed for forms rendered by controllers
```
Proof-of-work uses `crypto.subtle`, so it needs HTTPS (or localhost).
## LiveView
```elixir
def mount(_params, _session, socket) do
{:ok, assign(socket, form: to_form(%{}), challenge: Wiztchta.new())}
end
def handle_event("save", params, socket) do
socket = assign(socket, :challenge, Wiztchta.new()) # tokens are single-use
case Wiztchta.verify(params) do
:ok -> {:noreply, ...}
{:error, reason} -> {:noreply, put_flash(socket, :error, Wiztchta.error_message(reason))}
end
end
```
Keep the challenge in an assign. Without `challenge={...}`, the component makes
a new challenge each time it renders.
## Controllers
```heex
<.form for={@form} action={~p"/contact"}>
...
<.wiztchta type={Wiztchta.Challenges.Math} />
</.form>
```
```elixir
plug Wiztchta.Plug when action in [:create]
def create(conn, params) do
case conn.assigns.wiztchta do
:ok -> ...
{:error, reason} -> ...
end
end
```
## Configuration
```elixir
config :wiztchta,
endpoint: MyAppWeb.Endpoint, # or secret_key_base: "..."
default_challenge: Wiztchta.Challenges.ProofOfWork,
pow_difficulty: 16, # each +1 doubles the work
max_age: 300, # seconds a challenge stays valid
store: Wiztchta.Store.ETS, # nil disables replay protection
param: "wiztchta" # form param name
```
You can pass any of these per call too, for example
`Wiztchta.new(type: Wiztchta.Challenges.Math, max_age: 60)` or
`<.wiztchta opts={[difficulty: 18]} />`.
The default ETS store only knows about tokens used on its own node. In a cluster, a token could be
used once on each node. Implement `Wiztchta.Store` on top of shared storage to
prevent that.
## Styling
The markup uses plain classes (`wiztchta`, `wiztchta-pow`, `wiztchta-status`,
`wiztchta-math`, `wiztchta-label`, `wiztchta-input`), and proof-of-work exposes
`data-state="pending|solving|solved|error"`. Pass `class` to the component to
add your own.
## Development
```sh
mix deps.get
mix test
mix dev # demo app at http://localhost:4000 (LiveView + controller forms)
mix precommit
```
## License
MIT. See [LICENSE](LICENSE).