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' %> +
+
+ <%= rucaptcha_image_tag(alt: 'Captcha') %> +
+
+ <% 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