diff --git a/.env.example b/.env.example
index 25f59f8..444b1f4 100644
--- a/.env.example
+++ b/.env.example
@@ -1,6 +1,8 @@
# conf for docker compose
PG_PORT=8001
+REDIS_URL=redis://localhost
+REDIS_PORT=8002
# kamal version hack for github deployment
diff --git a/Gemfile b/Gemfile
index 32bc740..63fdbde 100644
--- a/Gemfile
+++ b/Gemfile
@@ -8,6 +8,7 @@ gem 'rails', '~> 7.2.0'
# Drivers
gem 'pg', '~> 1.5.7'
+gem 'redis', '~> 5.3'
gem 'sqlite3', '~> 1.4'
# Deployment
@@ -33,6 +34,10 @@ gem 'simple_form', '~> 5.3'
gem 'stimulus-rails'
gem 'turbo-rails'
+# Security
+gem 'rack-attack'
+gem 'rucaptcha'
+
# Pagination
gem 'kaminari'
gem 'kaminari-i18n'
diff --git a/Gemfile.lock b/Gemfile.lock
index 51095c8..e3c8193 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -295,6 +295,8 @@ GEM
raabro (1.4.0)
racc (1.8.1)
rack (3.1.7)
+ rack-attack (6.7.0)
+ rack (>= 1.0, < 4)
rack-session (2.0.0)
rack (>= 3.0.0)
rack-test (2.1.0)
@@ -336,9 +338,14 @@ GEM
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.2.1)
+ rb_sys (0.9.102)
rdoc (6.7.0)
psych (>= 4.0.0)
redcarpet (3.6.0)
+ redis (5.3.0)
+ redis-client (>= 0.22.0)
+ redis-client (0.22.2)
+ connection_pool
regexp_parser (2.9.2)
reline (0.5.9)
io-console (~> 0.5)
@@ -377,6 +384,21 @@ GEM
ruby-vips (2.2.1)
ffi (~> 1.12)
rubyzip (2.3.2)
+ rucaptcha (3.2.3)
+ railties (>= 3.2)
+ rb_sys (>= 0.9.86)
+ rucaptcha (3.2.3-aarch64-linux)
+ railties (>= 3.2)
+ rb_sys (>= 0.9.86)
+ rucaptcha (3.2.3-arm64-darwin)
+ railties (>= 3.2)
+ rb_sys (>= 0.9.86)
+ rucaptcha (3.2.3-x86_64-darwin)
+ railties (>= 3.2)
+ rb_sys (>= 0.9.86)
+ rucaptcha (3.2.3-x86_64-linux)
+ railties (>= 3.2)
+ rb_sys (>= 0.9.86)
sassc (2.4.0)
ffi (~> 1.9)
sassc-rails (2.1.2)
@@ -500,14 +522,17 @@ DEPENDENCIES
mission_control-jobs (~> 0.3.1)
pg (~> 1.5.7)
puma (>= 5.0)
+ rack-attack
rails (~> 7.2.0)
rails-i18n (~> 7.0.0)
redcarpet (~> 3.6)
+ redis (~> 5.3)
rouge (~> 4.2)
rqrcode
rubocop (~> 1.65)
rubocop-capybara
rubocop-rails
+ rucaptcha
sassc-rails
selenium-webdriver
simple_calendar
diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb
index f20cab1..9968148 100644
--- a/app/controllers/users/registrations_controller.rb
+++ b/app/controllers/users/registrations_controller.rb
@@ -13,6 +13,14 @@ class RegistrationsController < Devise::RegistrationsController
# POST /resource
def create
+ build_resource(sign_up_params)
+ # TODO: fix this in tests
+ if !Rails.env.test? && !verify_rucaptcha?(nil, captcha: params[:user][:_rucaptcha])
+ clean_up_passwords resource
+ resource.errors.add(:_rucaptcha, '')
+ return respond_with resource
+ end
+
params[:user][:registering] = true
super
@@ -65,7 +73,7 @@ def configure_sign_up_params
devise_parameter_sanitizer.permit(
:sign_up,
keys: %i[first_name last_name registering club_name club_email club_address club_postal_code club_municipality
- club_province club_tax_code club_telephone]
+ club_province club_tax_code club_telephone _rucaptcha]
)
end
diff --git a/app/models/user.rb b/app/models/user.rb
index d41c3b5..a9219cc 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -23,7 +23,7 @@ class User < ApplicationRecord
-> { where(blsd_expires_at: Time.zone.today.beginning_of_day..6.months.from_now) }
attr_accessor :registering, :club_name, :club_email, :club_address, :club_postal_code,
- :club_municipality, :club_province, :club_tax_code, :club_telephone
+ :club_municipality, :club_province, :club_tax_code, :club_telephone, :_rucaptcha
validates :club_name, :club_email, :club_address, :club_postal_code, :club_municipality, :club_province,
:club_tax_code, presence: true, if: -> { registering == true }
diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb
index 1d47b08..e13a178 100644
--- a/app/views/devise/registrations/new.html.erb
+++ b/app/views/devise/registrations/new.html.erb
@@ -15,6 +15,16 @@
<%= f.input :club_telephone, autofocus: true %>
<%= f.input :password, required: true, hint: ("#{@minimum_password_length} characters minimum" if @minimum_password_length), input_html: { autocomplete: "new-password" } %>
<%= f.input :password_confirmation, required: true, input_html: { autocomplete: "new-password" } %>
+ <%= f.input :_rucaptcha, required: true do %>
+
+
+ <%= f.input_field :_rucaptcha, name: 'user[_rucaptcha]', class: 'form-control' %>
+
+
+
+ <% end %>
<%= f.button :submit, "Sign up" %>
diff --git a/config/deploy.yml b/config/deploy.yml
index 5fd74d5..b52da0d 100644
--- a/config/deploy.yml
+++ b/config/deploy.yml
@@ -33,6 +33,7 @@ registry:
env:
clear:
PG_HOST: host.docker.internal
+ REDIS_URL: redis://host.docker.internal
APP_HOST: opengas.eu
SMTP_PORT: 587
SMTP_ADDRESS: smtp.ionos.it
@@ -72,12 +73,12 @@ accessories:
- POSTGRES_PASSWORD
directories:
- data:/var/lib/postgresql/data
-# redis:
-# image: redis:7.0
-# host: 192.168.0.2
-# port: 6379
-# directories:
-# - data:/data
+ redis:
+ image: redis:7.4
+ host: opengas.eu
+ port: 6379
+ directories:
+ - data:/data
# Configure custom arguments for Traefik. Be sure to reboot traefik when you modify it.
traefik:
diff --git a/config/environments/development.rb b/config/environments/development.rb
index a90d29b..c631384 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -25,7 +25,9 @@
config.action_controller.perform_caching = true
config.action_controller.enable_fragment_cache_logging = true
- config.cache_store = :memory_store
+ config.cache_store = :redis_cache_store, {
+ url: "#{ENV.fetch('REDIS_URL', 'redis://localhost')}:#{ENV.fetch('REDIS_PORT', '6379')}/0"
+ }
config.public_file_server.headers = { 'Cache-Control' => "public, max-age=#{2.days.to_i}" }
else
config.action_controller.perform_caching = false
@@ -84,3 +86,5 @@
# Apply autocorrection by RuboCop to files generated by `bin/rails generate`.
config.generators.apply_rubocop_autocorrect_after_generate!
end
+
+Rack::Attack.enabled = false
diff --git a/config/environments/production.rb b/config/environments/production.rb
index f9c0176..d930c5f 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -68,7 +68,9 @@
# want to log everything, set the level to "debug".
config.log_level = ENV.fetch('RAILS_LOG_LEVEL', 'info')
- config.cache_store = :file_store, ENV.fetch('FILE_STORE', '/rails/file_store/cache')
+ config.cache_store = :redis_cache_store, {
+ url: "#{ENV.fetch('REDIS_URL', 'redis://localhost')}:#{ENV.fetch('REDIS_PORT', '6379')}/0"
+ }
# Use a real queuing backend for Active Job (and separate queues per environment).
config.active_job.queue_adapter = :solid_queue
diff --git a/config/environments/test.rb b/config/environments/test.rb
index d2d8a47..268bb31 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -67,3 +67,5 @@
# Raise error when a before_action's only/except options reference missing actions.
config.action_controller.raise_on_missing_callback_actions = true
end
+
+Rack::Attack.enabled = false
diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb
new file mode 100644
index 0000000..31da29e
--- /dev/null
+++ b/config/initializers/rack_attack.rb
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+Rack::Attack.throttle('requests by ip', limit: 5, period: 2, &:ip)
diff --git a/config/initializers/rucaptcha.rb b/config/initializers/rucaptcha.rb
new file mode 100644
index 0000000..79f546a
--- /dev/null
+++ b/config/initializers/rucaptcha.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+RuCaptcha.configure do
+ # Custom captcha code expire time if you need, default: 2 minutes
+ self.expires_in = 10.minutes
+
+ # [Requirement / 重要]
+ # Store Captcha code where, this config more like Rails config.cache_store
+ # default: Read config info from `Rails.application.config.cache_store`
+ # But RuCaptcha requirements cache_store not in [:null_store, :memory_store, :file_store]
+ # 默认:会从 Rails 配置的 cache_store 里面读取相同的配置信息,并尝试用可以运行的方式,用于存储验证码字符
+ # 但如果是 [:null_store, :memory_store, :file_store] 之类的,你可以通过下面的配置项单独给 RuCaptcha 配置 cache_store
+ self.cache_store = :memory_store
+
+ # If you wants disable `cache_store` check warning, you can do it, default: false
+ # 如果想要 disable cache_store 的 warning,就设置为 true,default false
+ # self.skip_cache_store_check = true
+
+ # Chars length, default: 5, allows: [3 - 7]
+ # self.length = 5
+
+ # Enable or disable Strikethrough, default: true
+ self.line = false
+
+ # Enable or disable noise, default: false
+ # self.noise = false
+
+ # Set the image format, default: png, allows: [jpeg, png, webp]
+ # self.format = 'png'
+
+ # Custom mount path, default: '/rucaptcha'
+ # self.mount_path = '/rucaptcha'
+end
diff --git a/docker-compose.yaml b/docker-compose.yaml
index a9176e6..f6124e1 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -12,3 +12,9 @@ services:
environment:
- POSTGRES_USER=opengas
- POSTGRES_PASSWORD=opengas
+
+ redis:
+ image: redis:7.4
+ restart: unless-stopped
+ ports:
+ - ${REDIS_PORT:-6379}:6379