{"id":13809001,"url":"https://github.com/jsonmaur/phoenix-turnstile","last_synced_at":"2025-03-26T20:31:03.751Z","repository":{"id":154449256,"uuid":"624096649","full_name":"jsonmaur/phoenix-turnstile","owner":"jsonmaur","description":"Use Cloudflare Turnstile in Phoenix","archived":false,"fork":false,"pushed_at":"2024-07-11T19:45:16.000Z","size":44,"stargazers_count":12,"open_issues_count":4,"forks_count":4,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-10-30T06:59:43.718Z","etag":null,"topics":["captcha","cloudflare","elixir","liveview","phoenix","turnstile"],"latest_commit_sha":null,"homepage":"https://hexdocs.pm/phoenix_turnstile","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jsonmaur.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-04-05T18:31:14.000Z","updated_at":"2024-10-27T18:50:59.000Z","dependencies_parsed_at":null,"dependency_job_id":"b0dd3cdb-e100-4d9b-a703-bc994effa845","html_url":"https://github.com/jsonmaur/phoenix-turnstile","commit_stats":null,"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jsonmaur%2Fphoenix-turnstile","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jsonmaur%2Fphoenix-turnstile/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jsonmaur%2Fphoenix-turnstile/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jsonmaur%2Fphoenix-turnstile/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jsonmaur","download_url":"https://codeload.github.com/jsonmaur/phoenix-turnstile/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245731296,"owners_count":20663152,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["captcha","cloudflare","elixir","liveview","phoenix","turnstile"],"created_at":"2024-08-04T01:01:57.424Z","updated_at":"2025-03-26T20:31:03.459Z","avatar_url":"https://github.com/jsonmaur.png","language":"Elixir","funding_links":[],"categories":["Framework Components"],"sub_categories":[],"readme":"# Phoenix Turnstile\n\nPhoenix components and helpers for using CAPTCHAs with [Cloudflare Turnstile](https://www.cloudflare.com/products/turnstile/). Before getting started, log into the Cloudflare dashboard and visit the Turnstile tab. Add a new site with your domain name (no need to add `localhost` if using the default test keys), and take note of your site key and secret key. You'll need these values later.\n\n- [Installation](#installation)\n- [Getting Started](#getting-started)\n- [Verification](#verification)\n- [Events](#events)\n- [Without LiveView](#without-liveview)\n- [Content Security Policies](#content-security-policies)\n- [Writing Tests](#writing-tests)\n\n## Installation\n\n```elixir\ndef deps do\n  [\n    {:phoenix_turnstile, \"~\u003e 1.0\"}\n  ]\nend\n```\n\nNow add the site key and secret key to your environment variables, and configure them in `config/runtime.exs`:\n\n```elixir\nconfig :phoenix_turnstile,\n  site_key: System.fetch_env!(\"TURNSTILE_SITE_KEY\"),\n  secret_key: System.fetch_env!(\"TURNSTILE_SECRET_KEY\")\n```\n\nYou don't need to add a site key or secret key for dev/test environments. This library will use the Turnstile test keys by default.\n\n## Getting Started\n\nTo use CAPTCHAs in a LiveView app, start out by adding the script component in your root layout:\n\n```heex\n\u003chead\u003e\n  \u003c!-- ... --\u003e\n\n  \u003cTurnstile.script /\u003e\n\u003c/head\u003e\n```\n\nNext, install the hook in `app.js` or wherever your live socket is being defined (make sure you're setting `NODE_PATH` in your [esbuild config](https://github.com/phoenixframework/esbuild#adding-to-phoenix) and including the `deps` folder):\n\n```javascript\nimport { TurnstileHook } from \"phoenix_turnstile\"\n\nconst liveSocket = new LiveSocket(\"/live\", Socket, {\n  /* ... */\n  hooks: {\n    Turnstile: TurnstileHook\n  }\n})\n```\n\nNow you can use the Turnstile widget component in any of your forms. For example:\n\n```heex\n\u003c.form for={@form} phx-submit=\"submit\"\u003e\n  \u003cTurnstile.widget theme=\"light\" /\u003e\n\n  \u003cbutton type=\"submit\"\u003eSubmit\u003c/button\u003e\n\u003c/.form\u003e\n```\n\nTo customize the widget, pass any of the render parameters [specificed here](https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/#configurations) (without the `data-` prefix).\n\n### Multiple Widgets\n\nIf you want to have multiple widgets on the same page, pass a unique ID to `Turnstile.widget/1`, `Turnstile.refresh/1`, and `Turnstile.remove/1`.\n\n## Verification\n\nThe widget by itself won't actually complete the verification. It works by generating a token which gets injected into your form as a hidden input named `cf-turnstile-response`. The token needs to be sent to the Cloudflare API for final verification before continuing with the form submission. This should be done in your submit event using `Turnstile.verify/2`:\n\n```elixir\ndef handle_event(\"submit\", values, socket) do\n  case Turnstile.verify(values) do\n    {:ok, _} -\u003e\n      # Verification passed!\n\n      {:noreply, socket}\n\n    {:error, _} -\u003e\n      socket =\n        socket\n        |\u003e put_flash(:error, \"Please try submitting again\")\n        |\u003e Turnstile.refresh()\n\n      {:noreply, socket}\n  end\nend\n```\n\nTo be extra sure the user is not a robot, you also have the option of passing their IP address to the verification API. **This step is optional.** To get the user's IP address in LiveView, add `:peer_data` to the connect info for your socket in `endpoint.ex`:\n\n```elixir\nsocket \"/live\", Phoenix.LiveView.Socket,\n  websocket: [\n    connect_info: [:peer_data, ...]\n  ]\n```\n\nand pass it as the second argument to `Turnstile.verify/2`:\n\n```elixir\ndef mount(_params, session, socket) do\n  remote_ip = get_connect_info(socket, :peer_data).address\n  {:ok, assign(socket, :remote_ip, remote_ip)}\nend\n\ndef handle_event(\"submit\", values, socket) do\n  case Turnstile.verify(values, socket.assigns.remote_ip) do\n    # ...\n  end\nend\n```\n\n## Events\n\nThe Turnstile widget supports the following events:\n\n* `:success` - When the challenge was successfully completed\n* `:error` - When there was an error (like a network error or the challenge failed)\n* `:expired` - When the challenge token expires and was not automatically reset\n* `:beforeInteractive` - Before the challenge enters interactive mode\n* `:afterInteractive` - After the challenge has left interactive mode\n* `:unsupported` - When a given client/browser is not supported by Turnstile\n* `:timeout` - When the challenge expires (after 5 minutes)\n\nThese can be useful for doing things like disabling the submit button until the challenge successfully completes, or refreshing the widget if it fails. To handle an event, add it to the `events` attribute and create a Turnstile event handler in the LiveView:\n\n```heex\n\u003cTurnstile.widget events={[:success]} /\u003e\n```\n\n```elixir\nhandle_event(\"turnstile:success\", _params, socket) do\n  # ...\n\n  {:noreply, socket}\nend\n```\n\n## Without LiveView\n\n`Turnstile.script/1` and `Turnstile.widget/1` both rely on [client hooks](https://hexdocs.pm/phoenix_live_view/js-interop.html#client-hooks-via-phx-hook), and should work in non-LiveView pages as long as `app.js` is opening a live socket (which it should by default). Simply call `Turnstile.verify/2` in the controller:\n\n```elixir\ndef create(conn, params) do\n  case Turnstile.verify(params, conn.remote_ip) do\n    {:ok, _} -\u003e\n      # Verification passed!\n\n      redirect(conn, to: ~p\"/success\")\n\n    {:error, _} -\u003e\n      conn\n      |\u003e put_flash(:error, \"Please try submitting again\")\n      |\u003e redirect(to: ~p\"/new\")\n  end\nend\n```\n\nIf your page doesn't open a live socket or your're not using HEEx, you can still run Turnstile verifications by building your own client-side widget following the [documentation](https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/) and using `Turnstile.site_key/0` to get your site key in the template:\n\n```elixir\ndef new(conn, _params) do\n  conn\n  |\u003e assign(:site_key, Turnstile.site_key())\n  |\u003e render(\"new.html\")\nend\n```\n\n```html\n\u003cform action=\"/create\" method=\"POST\"\u003e\n  \u003c!-- ... --\u003e\n\n  \u003cdiv class=\"cf-turnstile\" data-sitekey=\"\u003c%= @site_key %\u003e\"\u003e\u003c/div\u003e\n  \u003cbutton type=\"submit\"\u003eSubmit\u003c/button\u003e\n\u003c/form\u003e\n```\n\n## Content Security Policies\n\nIf your site uses a content security policy, you'll need to add `https://challenges.cloudflare.com` to your `script-src` and `frame-src` directives. You can also add attributes to the script component such as `nonce`, and they will be passed through to the script tag:\n\n```heex\n\u003chead\u003e\n  \u003c!-- ... --\u003e\n\n  \u003cTurnstile.script nonce={@script_src_nonce} /\u003e\n\u003c/head\u003e\n```\n\n## Writing Tests\n\nWhen testing forms that use Turnstile verification, you may or may not want to call the live API.\n\nAlthough we use the test keys by default, you should consider using mocks during testing. An excellent library to consider is [mox](https://github.com/dashbitco/mox). Phoenix Turnstile exposes [`Turnstile.Behavior`](Turnstile.Behaviour.html) which makes writing tests much easier.\n\nTo start using Mox with Phoenix Turnstile, add this to your `test/test_helper.ex`:\n\n```elixir\nMox.defmock(TurnstileMock, for: Turnstile.Behaviour)\n```\n\nThen in your `config/test.exs`:\n\n```elixir\nconfig :phoenix_turnstile, adapter: TurnstileMock\n```\n\nTo make sure you're using `TurnstileMock` during testing, use the adapter from the config rather than using `Turnstile` directly:\n\n```elixir\n@turnstile Application.compile_env(:phoenix_turnstile, :adapter, Turnstile)\n\ndef handle_event(\"submit\", values, socket) do\n  case @turnstile.verify(values) do\n    {:ok, _} -\u003e\n      # Verification passed!\n\n      {:noreply, socket}\n\n    {:error, _} -\u003e\n      socket =\n        socket\n        |\u003e put_flash(:error, \"Please try submitting again\")\n        |\u003e @turnstile.refresh()\n\n      {:noreply, socket}\n  end\nend\n```\n\nNow you can easily mock or stub any Turnstile function in your tests and they won't make any real API calls:\n\n```elixir\nimport Mox\n\nsetup do\n  stub(TurnstileMock, :refresh, fn socket -\u003e socket end)\n  stub(TurnstileMock, :verify, fn _values, _remoteip -\u003e {:ok, %{}} end)\nend\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjsonmaur%2Fphoenix-turnstile","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjsonmaur%2Fphoenix-turnstile","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjsonmaur%2Fphoenix-turnstile/lists"}