diff --git a/.env.example b/.env.example index 43fb2b7..3bc42cc 100644 --- a/.env.example +++ b/.env.example @@ -4,3 +4,6 @@ SMTP_PORT=1025 SMTP_DOMAIN=localhost SMTP_ADDRESS=localhost DB_PASSWORD= +G_RECAPTCHA_SITE_KEY= +G_RECAPTCHA_SECRET_KEY= +G_RECAPTCHA_SCORE_THRESHOLD=0.8 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0ad627..30ca3d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,11 +65,11 @@ jobs: - 5432:5432 options: --health-cmd="pg_isready" --health-interval=10s --health-timeout=5s --health-retries=3 - # redis: - # image: redis - # ports: - # - 6379:6379 - # options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 + redis: + image: redis + ports: + - 6379:6379 + options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Install packages @@ -88,12 +88,4 @@ jobs: env: RAILS_ENV: test RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }} - run: bin/rails db:test:prepare test test:system - - - name: Keep screenshots from failed system tests - uses: actions/upload-artifact@v4 - if: failure() - with: - name: screenshots - path: ${{ github.workspace }}/tmp/screenshots - if-no-files-found: ignore + run: bin/rails db:prepare && bin/bundle exec rspec diff --git a/.rubocop.yml b/.rubocop.yml index f9d86d4..2115d33 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,8 +1,13 @@ # Omakase Ruby styling for Rails inherit_gem: { rubocop-rails-omakase: rubocop.yml } -# Overwrite or add rules to create your own house style -# -# # Use `[a, [b, c]]` not `[ a, [ b, c ] ]` -# Layout/SpaceInsideArrayLiteralBrackets: -# Enabled: false +Style/StringLiterals: + Enabled: true + EnforcedStyle: double_quotes + Include: + - "app/**/*" + - "config/**/*" + - "lib/**/*" + - "test/**/*" + - "Gemfile" + - "spec/**/*" diff --git a/README.md b/README.md index f10546d..0d4c47e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Now, let's get you set up: 1. Install [Docker Desktop](https://docs.docker.com/desktop/) on your local machine. This allows you to run the database, Redis store, and [Mailpit](https://mailpit.axllent.org/) containers locally. You can, of course, skip this step if Docker's not your thing. Oh whale 🐳! 1. Pull down this repository to your local machine. -1. At the root of the project, run `cp .env.example .env`. Then slide on over to your new `.env` file and fill out the empty environment variables. If you're using Docker, feel free to put whatever you want for `SMTP_USERNAME`, `SMTP_PASSWORD`, and `DB_PASSWORD`. +1. At the root of the project, run `cp .env.example .env`. Then slide on over to your new `.env` file and fill out the empty environment variables. If you're using Docker, feel free to put whatever you want for `SMTP_USERNAME`, `SMTP_PASSWORD`, and `DB_PASSWORD`. Make sure to use reCAPTCHA v3 for all `G_RECAPTCHA_**` values. You can generate your own reCAPTCHA credentials at [https://www.google.com/recaptcha/](https://www.google.com/recaptcha/). 1. Open the Docker Desktop application. Then, in your termnial, run `docker compose up`. This will create and start the containers for PostgreSQL, Redis, and Mailpit. 1. In your terminal, run `bin/setup` to install your dependcies and set up your database. 1. In your terminal, run `bin/rails s` to fire up the development server. diff --git a/app/controllers/pledges_controller.rb b/app/controllers/pledges_controller.rb index efa0816..62014d3 100644 --- a/app/controllers/pledges_controller.rb +++ b/app/controllers/pledges_controller.rb @@ -6,7 +6,7 @@ def new def create @pledge = Pledge.new(pledge_params) - if @pledge.save + if safe_submission? && @pledge.save flash[:success] = "Thanks for signing! 🥳" PledgeMailer.new_pledge( first_name: pledge_params[:first_name], @@ -24,4 +24,10 @@ def create def pledge_params params.require(:pledge).permit(:first_name, :last_name, :email) end + + def safe_submission? + ::Recaptcha.new( + token: params[:g_recaptcha_response] + ).call + end end diff --git a/app/javascript/controllers/recaptcha_controller.js b/app/javascript/controllers/recaptcha_controller.js new file mode 100644 index 0000000..ba02966 --- /dev/null +++ b/app/javascript/controllers/recaptcha_controller.js @@ -0,0 +1,26 @@ +import { Controller } from "@hotwired/stimulus"; + +export default class extends Controller { + validate(event) { + event.preventDefault(); + + const siteKey = document + .getElementById("recaptcha-initlializer") + .src.split("?render=") + .slice(-1)[0]; + const form = this.element; + const tokenInput = form.querySelector("input[name='g_recaptcha_response']"); + const submitButton = form.querySelector("button[type='submit']"); + + grecaptcha.ready(function () { + grecaptcha + .execute(siteKey, { + action: "submit", + }) + .then(function (token) { + tokenInput.value = token; + form.requestSubmit(submitButton); + }); + }); + } +} diff --git a/app/services/recaptcha.rb b/app/services/recaptcha.rb new file mode 100644 index 0000000..887cce5 --- /dev/null +++ b/app/services/recaptcha.rb @@ -0,0 +1,31 @@ +require "net/http" +require "uri" + +class Recaptcha + def initialize(token:) + @token = token + end + + def call + endpoint = "https://www.google.com/recaptcha/api/siteverify" + data = { + secret: ENV["G_RECAPTCHA_SECRET_KEY"], + response: @token + } + response = Net::HTTP.post_form URI(endpoint), data + payload = JSON.parse(response.body) + threshold = ENV.fetch("G_RECAPTCHA_SCORE_THRESHOLD", "0.5").to_f + + if payload["success"] && payload["score"] > threshold + Rails.logger.info "reCAPTCHA evaluated as safe: payload => #{payload}" + true + else + Rails.logger.warn "reCAPTCHA evaluated as unsafe: payload => #{payload}" + false + end + + rescue StandardError => exception + Rails.logger.error "Form response failed to validate with reCAPTCHA: exception => #{exception}, payload => #{payload}" + false + end +end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 87d7056..3094e25 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -19,6 +19,7 @@ <%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %> <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> <%= javascript_importmap_tags %> +
diff --git a/app/views/pledges/form/_index.html.erb b/app/views/pledges/form/_index.html.erb index 1233027..d8fd733 100644 --- a/app/views/pledges/form/_index.html.erb +++ b/app/views/pledges/form/_index.html.erb @@ -9,7 +9,7 @@