Merge pull request 'Require a LibreCaptcha challenge for the signup action' (#157) from libre-captcha into main
Reviewed-on: #157
This commit is contained in:
commit
822b2b0fad
1
Gemfile
1
Gemfile
@ -21,6 +21,7 @@ gem 'rack-cors'
|
|||||||
gem 'react-rails'
|
gem 'react-rails'
|
||||||
gem 'rubytree'
|
gem 'rubytree'
|
||||||
gem 'acts_as_tenant'
|
gem 'acts_as_tenant'
|
||||||
|
gem 'httparty'
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
gem 'annotaterb'
|
gem 'annotaterb'
|
||||||
|
@ -126,6 +126,10 @@ GEM
|
|||||||
raabro (~> 1.4)
|
raabro (~> 1.4)
|
||||||
globalid (1.2.1)
|
globalid (1.2.1)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
|
httparty (0.22.0)
|
||||||
|
csv
|
||||||
|
mini_mime (>= 1.0.0)
|
||||||
|
multi_xml (>= 0.5.2)
|
||||||
i18n (1.14.6)
|
i18n (1.14.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
importmap-rails (2.0.3)
|
importmap-rails (2.0.3)
|
||||||
@ -188,6 +192,8 @@ GEM
|
|||||||
money (6.19.0)
|
money (6.19.0)
|
||||||
i18n (>= 0.6.4, <= 2)
|
i18n (>= 0.6.4, <= 2)
|
||||||
msgpack (1.7.2)
|
msgpack (1.7.2)
|
||||||
|
multi_xml (0.7.1)
|
||||||
|
bigdecimal (~> 3.1)
|
||||||
net-imap (0.5.1)
|
net-imap (0.5.1)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
@ -398,6 +404,7 @@ DEPENDENCIES
|
|||||||
devise (~> 4.9)
|
devise (~> 4.9)
|
||||||
factory_bot_rails
|
factory_bot_rails
|
||||||
faker
|
faker
|
||||||
|
httparty
|
||||||
importmap-rails
|
importmap-rails
|
||||||
jbuilder
|
jbuilder
|
||||||
jsonapi-rails
|
jsonapi-rails
|
||||||
|
@ -30,6 +30,18 @@ class ApplicationController < ActionController::Base
|
|||||||
|
|
||||||
private
|
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 = {})
|
def default_url_options(options = {})
|
||||||
options.merge(path_params: { slug: ActsAsTenant.current_tenant&.slug })
|
options.merge(path_params: { slug: ActsAsTenant.current_tenant&.slug })
|
||||||
end
|
end
|
||||||
|
13
app/controllers/captcha_controller.rb
Normal file
13
app/controllers/captcha_controller.rb
Normal file
@ -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
|
@ -4,6 +4,8 @@ class Users::RegistrationsController < Devise::RegistrationsController
|
|||||||
clear_respond_to
|
clear_respond_to
|
||||||
respond_to :json
|
respond_to :json
|
||||||
|
|
||||||
|
before_action :validate_captcha!, only: :create
|
||||||
|
|
||||||
def create
|
def create
|
||||||
wedding = Wedding.create(wedding_params)
|
wedding = Wedding.create(wedding_params)
|
||||||
unless wedding.persisted?
|
unless wedding.persisted?
|
||||||
|
20
app/services/libre_captcha.rb
Normal file
20
app/services/libre_captcha.rb
Normal file
@ -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
|
@ -23,6 +23,9 @@ Rails.application.routes.draw do
|
|||||||
resources :tables_arrangements, only: %i[index show]
|
resources :tables_arrangements, only: %i[index show]
|
||||||
end
|
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::Ui::Engine => '/api-docs'
|
||||||
mount Rswag::Api::Engine => '/api-docs'
|
mount Rswag::Api::Engine => '/api-docs'
|
||||||
|
@ -36,6 +36,12 @@ services:
|
|||||||
- backend
|
- backend
|
||||||
volumes:
|
volumes:
|
||||||
- ../wedding-planner-frontend/:/app
|
- ../wedding-planner-frontend/:/app
|
||||||
|
libre-captcha:
|
||||||
|
image: librecaptcha/lc-core:latest
|
||||||
|
volumes:
|
||||||
|
- "./tmp/libre-captcha-data:/lc-core/data"
|
||||||
|
ports:
|
||||||
|
- 8888
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:latest
|
image: nginx:latest
|
||||||
ports:
|
ports:
|
||||||
|
@ -12,6 +12,11 @@ server {
|
|||||||
proxy_set_header Host $http_host;
|
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 / {
|
location / {
|
||||||
proxy_pass http://frontend:3000;
|
proxy_pass http://frontend:3000;
|
||||||
proxy_set_header Host $http_host;
|
proxy_set_header Host $http_host;
|
||||||
|
24
spec/requests/captcha_spec.rb
Normal file
24
spec/requests/captcha_spec.rb
Normal file
@ -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
|
@ -18,5 +18,16 @@ module Swagger
|
|||||||
example: :default,
|
example: :default,
|
||||||
description: 'Wedding slug'
|
description: 'Wedding slug'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CAPTCHA = {
|
||||||
|
captcha: {
|
||||||
|
type: :object,
|
||||||
|
required: %i[id answer],
|
||||||
|
properties: {
|
||||||
|
id: { type: :string, format: :uuid },
|
||||||
|
answer: { type: :string }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
@ -30,7 +30,8 @@ RSpec.describe 'users/registrations', type: :request do
|
|||||||
properties: {
|
properties: {
|
||||||
date: { type: :string, format: :date},
|
date: { type: :string, format: :date},
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
**Swagger::Schema::CAPTCHA
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user