diff --git a/Gemfile b/Gemfile index ead69aa..cb91e69 100644 --- a/Gemfile +++ b/Gemfile @@ -21,6 +21,7 @@ gem 'rack-cors' gem 'react-rails' gem 'rubytree' gem 'acts_as_tenant' +gem 'httparty' group :development, :test do gem 'annotaterb' diff --git a/Gemfile.lock b/Gemfile.lock index 7e795fc..c0660de 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -126,6 +126,10 @@ GEM raabro (~> 1.4) globalid (1.2.1) activesupport (>= 6.1) + httparty (0.22.0) + csv + mini_mime (>= 1.0.0) + multi_xml (>= 0.5.2) i18n (1.14.6) concurrent-ruby (~> 1.0) importmap-rails (2.0.3) @@ -188,6 +192,8 @@ GEM money (6.19.0) i18n (>= 0.6.4, <= 2) msgpack (1.7.2) + multi_xml (0.7.1) + bigdecimal (~> 3.1) net-imap (0.5.1) date net-protocol @@ -398,6 +404,7 @@ DEPENDENCIES devise (~> 4.9) factory_bot_rails faker + httparty importmap-rails jbuilder jsonapi-rails diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index f163c3e..15a4b17 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -30,6 +30,18 @@ class ApplicationController < ActionController::Base private + def validate_captcha! + Rails.logger.info("Captcha params: #{captcha_params}") + + return if LibreCaptcha.new.valid?(id: captcha_params[:id], answer: captcha_params[:answer]) + + render json: { error: 'Incorrect CAPTCHA solution' }, status: :unprocessable_entity + end + + def captcha_params + params.expect(captcha: [:id, :answer]) + end + def default_url_options(options = {}) options.merge(path_params: { slug: ActsAsTenant.current_tenant&.slug }) end diff --git a/app/controllers/captcha_controller.rb b/app/controllers/captcha_controller.rb new file mode 100644 index 0000000..428328d --- /dev/null +++ b/app/controllers/captcha_controller.rb @@ -0,0 +1,13 @@ +# Copyright (C) 2024 Manuel Bustillo + +class CaptchaController < ApplicationController + skip_before_action :authenticate_user! + + def create + id = LibreCaptcha.new.get_id + render json: { + id:, + media_url: media_captcha_index_url(id:) + }, status: :created + end +end diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index 24d8eca..4894d4b 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -4,6 +4,8 @@ class Users::RegistrationsController < Devise::RegistrationsController clear_respond_to respond_to :json + before_action :validate_captcha!, only: :create + def create wedding = Wedding.create(wedding_params) unless wedding.persisted? diff --git a/app/services/libre_captcha.rb b/app/services/libre_captcha.rb new file mode 100644 index 0000000..8684e9f --- /dev/null +++ b/app/services/libre_captcha.rb @@ -0,0 +1,20 @@ +# Copyright (C) 2024 Manuel Bustillo + +class LibreCaptcha + def get_id + HTTParty.post("http://libre-captcha:8888/v2/captcha", + body: { + input_type: "text", + level: :hard, + media: 'image/png', + size: '350x100' + }.to_json + ).then { |raw| JSON.parse(raw)['id'] } + end + + def valid?(id:, answer:) + HTTParty.post("http://libre-captcha:8888/v2/answer", + body: { id:, answer: }.to_json + ).then { |raw| JSON.parse(raw)['result'] == 'True' } + end +end \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 10810fe..1357d9c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -23,6 +23,9 @@ Rails.application.routes.draw do resources :tables_arrangements, only: %i[index show] end + resources :captcha, only: :create do + get 'v2/media', to: 'captcha#media', on: :collection, as: :media + end mount Rswag::Ui::Engine => '/api-docs' mount Rswag::Api::Engine => '/api-docs' diff --git a/docker-compose.yml b/docker-compose.yml index e1a5977..64b54c7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,6 +36,12 @@ services: - backend volumes: - ../wedding-planner-frontend/:/app + libre-captcha: + image: librecaptcha/lc-core:latest + volumes: + - "./tmp/libre-captcha-data:/lc-core/data" + ports: + - 8888 nginx: image: nginx:latest ports: diff --git a/nginx.conf b/nginx.conf index 06d059b..58c2bb9 100644 --- a/nginx.conf +++ b/nginx.conf @@ -12,6 +12,11 @@ server { proxy_set_header Host $http_host; } + location /captcha/v2/media/ { + proxy_pass http://libre-captcha:8888/v2/media/; + proxy_set_header Host $http_host; + } + location / { proxy_pass http://frontend:3000; proxy_set_header Host $http_host; diff --git a/spec/requests/captcha_spec.rb b/spec/requests/captcha_spec.rb new file mode 100644 index 0000000..607a2d9 --- /dev/null +++ b/spec/requests/captcha_spec.rb @@ -0,0 +1,24 @@ +# Copyright (C) 2024 Manuel Bustillo + +require 'swagger_helper' + +RSpec.describe 'captcha', type: :request do + path '/captcha' do + + post('create a CAPTCHA challenge') do + tags 'CAPTCHA' + consumes 'application/json' + produces 'application/json' + + response(201, 'created') do + schema type: :object, + required: %i[id], + properties: { + id: { type: :string, format: :uuid }, + media_url: { type: :string, format: :uri }, + } + xit + end + end + end +end diff --git a/spec/requests/schemas.rb b/spec/requests/schemas.rb index 6602db8..e1f7b43 100644 --- a/spec/requests/schemas.rb +++ b/spec/requests/schemas.rb @@ -18,5 +18,16 @@ module Swagger example: :default, description: 'Wedding slug' } + + CAPTCHA = { + captcha: { + type: :object, + required: %i[id answer], + properties: { + id: { type: :string, format: :uuid }, + answer: { type: :string } + } + } + } end end \ No newline at end of file diff --git a/spec/requests/users/registrations_spec.rb b/spec/requests/users/registrations_spec.rb index ae0f86d..f29b387 100644 --- a/spec/requests/users/registrations_spec.rb +++ b/spec/requests/users/registrations_spec.rb @@ -30,7 +30,8 @@ RSpec.describe 'users/registrations', type: :request do properties: { date: { type: :string, format: :date}, } - } + }, + **Swagger::Schema::CAPTCHA } }