From 4bd746d6f543c386da95bc5600086b78ca35a9ee Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 7 Oct 2025 23:19:21 +0300 Subject: [PATCH 01/21] feat: add config for deploy --- .gitignore | 6 +++ config/deploy.yml | 123 +++++++++++++++------------------------------- 2 files changed, 46 insertions(+), 83 deletions(-) diff --git a/.gitignore b/.gitignore index 74231ed..6701a45 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,9 @@ yarn-debug.log* OAUTH_TESTING.md config/credentials/development.yml.enc config/credentials/test.yml.enc + +/config/credentials/production.key +config/credentials/production.yml.enc +.kamal/secrets +.gitignore +KAMAL.md diff --git a/config/deploy.yml b/config/deploy.yml index 5e3d901..eca4d59 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -1,57 +1,29 @@ -# Name of your application. Used to uniquely configure containers. service: stackoverflow_clone -# Name of the container image. -image: your-user/stackoverflow_clone +image: amsak701/stclone -# Deploy to these servers. servers: web: - - 192.168.0.1 - # job: - # hosts: - # - 192.168.0.1 - # cmd: bin/jobs + - 89.23.97.92 -# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server. -# Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer. -# -# Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption. -proxy: - ssl: true - host: app.example.com - -# Credentials for your image host. +# Credentials for your image host (Docker Hub). registry: - # Specify the registry server, if you're not using Docker Hub - # server: registry.digitalocean.com / ghcr.io / ... - username: your-user - - # Always use an access token rather than real password when possible. + username: amsak701 password: - KAMAL_REGISTRY_PASSWORD -# Inject ENV variables into containers (secrets come from .kamal/secrets). env: secret: - RAILS_MASTER_KEY clear: - # Run the Solid Queue Supervisor inside the web server's Puma process to do jobs. - # When you start using multiple servers, you should split out job processing to a dedicated machine. - SOLID_QUEUE_IN_PUMA: true - - # Set number of processes dedicated to Solid Queue (default: 1) - # JOB_CONCURRENCY: 3 - - # Set number of cores available to the application on each server (default: 1). - # WEB_CONCURRENCY: 2 - - # Match this to any external database server to configure Active Record correctly - # Use stackoverflow_clone-db for a db accessory server on same machine via local kamal docker network. - # DB_HOST: 192.168.0.2 - - # Log everything from Rails - # RAILS_LOG_LEVEL: debug + RAILS_ENV: production + RAILS_LOG_TO_STDOUT: "1" + RAILS_SERVE_STATIC_FILES: "true" + SOLID_QUEUE_IN_PUMA: "true" + WEB_CONCURRENCY: "2" + RAILS_MAX_THREADS: "5" + REDIS_URL: redis://redis:6379/0 + ELASTICSEARCH_URL: http://elasticsearch:9200 # Aliases are triggered with "bin/kamal ". You can overwrite arguments on invocation: # "bin/kamal logs -r job" will tail logs from the first server in the job section. @@ -61,56 +33,41 @@ aliases: logs: app logs -f dbc: app exec --interactive --reuse "bin/rails dbconsole" - -# Use a persistent storage volume for sqlite database files and local Active Storage files. -# Recommended to change this to a mounted volume path that is backed up off server. volumes: - "stackoverflow_clone_storage:/rails/storage" + - "stackoverflow_clone_log:/rails/log" + - "stackoverflow_clone_tmp:/rails/tmp" - -# Bridge fingerprinted assets, like JS and CSS, between versions to avoid -# hitting 404 on in-flight requests. Combines all files from new and old -# version inside the asset_path. asset_path: /rails/public/assets -# Configure the image builder. builder: arch: amd64 - # # Build image via remote server (useful for faster amd64 builds on arm64 computers) - # remote: ssh://docker@docker-builder-server - # - # # Pass arguments and secrets to the Docker build process - # args: - # RUBY_VERSION: 3.2.6 - # secrets: - # - GITHUB_TOKEN - # - RAILS_MASTER_KEY -# Use a different ssh user than root -# ssh: -# user: app -# Use accessory services (secrets come from .kamal/secrets). -# accessories: -# db: -# image: mysql:8.0 -# host: 192.168.0.2 -# # Change to 3306 to expose port to the world instead of just local network. -# port: "127.0.0.1:3306:3306" -# env: -# clear: -# MYSQL_ROOT_HOST: '%' -# secret: -# - MYSQL_ROOT_PASSWORD -# files: -# - config/mysql/production.cnf:/etc/mysql/my.cnf -# - db/production.sql:/docker-entrypoint-initdb.d/setup.sql -# directories: -# - data:/var/lib/mysql -# redis: -# image: redis:7.0 -# host: 192.168.0.2 -# port: 6379 -# directories: -# - data:/data +accessories: + redis: + image: redis:7-alpine + host: 89.23.97.92 + port: "127.0.0.1:6379:6379" + directories: + - redis-data:/data + + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:8.13.4 + host: 89.23.97.92 + port: "127.0.0.1:9200:9200" + env: + clear: + discovery.type: single-node + xpack.security.enabled: "false" + ES_JAVA_OPTS: "-Xms512m -Xmx512m" + directories: + - es-data:/usr/share/elasticsearch/data + +healthcheck: + path: /up + interval: 5 + timeout: 2 + healthy: 2 + unhealthy: 3 From 9d47823ed411100bb39baba2153627af1552f6f9 Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 7 Oct 2025 23:21:38 +0300 Subject: [PATCH 02/21] fix: config --- config/deploy.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/config/deploy.yml b/config/deploy.yml index eca4d59..0069dc4 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -64,10 +64,3 @@ accessories: ES_JAVA_OPTS: "-Xms512m -Xmx512m" directories: - es-data:/usr/share/elasticsearch/data - -healthcheck: - path: /up - interval: 5 - timeout: 2 - healthy: 2 - unhealthy: 3 From a1cc14e63c5702effbb41737d91d75649c81b006 Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Wed, 8 Oct 2025 01:44:58 +0300 Subject: [PATCH 03/21] config for DOCKER HUB --- config/deploy.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/config/deploy.yml b/config/deploy.yml index 0069dc4..d932b27 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -6,9 +6,9 @@ servers: web: - 89.23.97.92 -# Credentials for your image host (Docker Hub). registry: - username: amsak701 + username: + - KAMAL_REGISTRY_USERNAME password: - KAMAL_REGISTRY_PASSWORD @@ -25,8 +25,6 @@ env: REDIS_URL: redis://redis:6379/0 ELASTICSEARCH_URL: http://elasticsearch:9200 -# Aliases are triggered with "bin/kamal ". You can overwrite arguments on invocation: -# "bin/kamal logs -r job" will tail logs from the first server in the job section. aliases: console: app exec --interactive --reuse "bin/rails console" shell: app exec --interactive --reuse "bash" @@ -41,9 +39,13 @@ volumes: asset_path: /rails/public/assets builder: + remote: 89.23.97.92 arch: amd64 - +# Traefik reverse proxy +proxy: + ssl: false + host: 89.23.97.92 accessories: redis: From b22dd48311433e8459080d1ecfe7843d5f0992ce Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Wed, 8 Oct 2025 17:47:27 +0300 Subject: [PATCH 04/21] config: update ip for VPS in Germany --- config/deploy.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/config/deploy.yml b/config/deploy.yml index d932b27..e7fbde7 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -4,7 +4,7 @@ image: amsak701/stclone servers: web: - - 89.23.97.92 + - 90.156.228.95 registry: username: @@ -39,25 +39,25 @@ volumes: asset_path: /rails/public/assets builder: - remote: 89.23.97.92 + remote: 90.156.228.95 arch: amd64 # Traefik reverse proxy proxy: ssl: false - host: 89.23.97.92 + host: 90.156.228.95 accessories: redis: image: redis:7-alpine - host: 89.23.97.92 + host: 90.156.228.95 port: "127.0.0.1:6379:6379" directories: - redis-data:/data elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:8.13.4 - host: 89.23.97.92 + host: 90.156.228.95 port: "127.0.0.1:9200:9200" env: clear: From 60d438f8a99386cd4395f929ca6b3e2f89154dfb Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 14 Oct 2025 01:06:15 +0300 Subject: [PATCH 05/21] deploy update --- app/mailers/application_mailer.rb | 2 +- config/deploy.yml | 30 +++++++- config/environments/production.rb | 68 ++++++++++++------- config/initializers/devise.rb | 35 ++++++---- config/initializers/doorkeeper.rb | 6 +- config/initializers/elasticsearch.rb | 12 ++++ config/routes.rb | 12 ++++ .../20251013000050_add_admin_to_users.rb | 6 ++ db/schema.rb | 4 +- 9 files changed, 131 insertions(+), 44 deletions(-) create mode 100644 db/migrate/20251013000050_add_admin_to_users.rb diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 3c34c81..4a7ea0c 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,4 +1,4 @@ class ApplicationMailer < ActionMailer::Base - default from: "from@example.com" + default from: ENV.fetch("MAILER_FROM_EMAIL", "noreply@stackoverflow-clone.com") layout "mailer" end diff --git a/config/deploy.yml b/config/deploy.yml index e7fbde7..87e03fb 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -15,15 +15,21 @@ registry: env: secret: - RAILS_MASTER_KEY + - GOOGLE_CLIENT_ID + - GOOGLE_CLIENT_SECRET + - SIDEKIQ_USERNAME + - SIDEKIQ_PASSWORD + clear: RAILS_ENV: production RAILS_LOG_TO_STDOUT: "1" RAILS_SERVE_STATIC_FILES: "true" - SOLID_QUEUE_IN_PUMA: "true" WEB_CONCURRENCY: "2" RAILS_MAX_THREADS: "5" REDIS_URL: redis://redis:6379/0 ELASTICSEARCH_URL: http://elasticsearch:9200 + APP_HOST: "90.156.228.95" + MAILER_FROM_EMAIL: "noreply@stackoverflow-clone.com" aliases: console: app exec --interactive --reuse "bin/rails console" @@ -42,10 +48,15 @@ builder: remote: 90.156.228.95 arch: amd64 -# Traefik reverse proxy +# Traefik reverse proxy with SSL proxy: - ssl: false + # ssl: true host: 90.156.228.95 + # Automatic SSL certificates via Let's Encrypt + # Note: Let's Encrypt requires a domain name, not IP + # For IP-only access, you'll need to manually configure SSL certificates + # acme: + # email: your-email@example.com accessories: redis: @@ -66,3 +77,16 @@ accessories: ES_JAVA_OPTS: "-Xms512m -Xmx512m" directories: - es-data:/usr/share/elasticsearch/data + + sidekiq: + image: amsak701/stclone + host: 90.156.228.95 + cmd: bundle exec sidekiq -C config/sidekiq.yml + env: + secret: + - RAILS_MASTER_KEY + clear: + RAILS_ENV: production + REDIS_URL: redis://redis:6379/0 + ELASTICSEARCH_URL: http://elasticsearch:9200 + diff --git a/config/environments/production.rb b/config/environments/production.rb index bdcd01d..15a073f 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -28,10 +28,11 @@ config.assume_ssl = true # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - config.force_ssl = true + # Note: Disabled for IP-based deployment. Enable when using a domain with proper SSL certificate. + config.force_ssl = false # Skip http-to-https redirect for the default health check endpoint. - # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } + config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } # Log to STDOUT with the current request id as a default log tag. config.log_tags = [ :request_id ] @@ -49,25 +50,40 @@ # Replace the default in-process memory cache store with a durable alternative. config.cache_store = :solid_cache_store - # Replace the default in-process and non-durable queuing backend for Active Job. - config.active_job.queue_adapter = :solid_queue - config.solid_queue.connects_to = { database: { writing: :queue } } - - # Ignore bad email addresses and do not raise email delivery errors. - # Set this to true and configure the email server for immediate delivery to raise delivery errors. - # config.action_mailer.raise_delivery_errors = false + # Use Sidekiq for background jobs + config.active_job.queue_adapter = :sidekiq + # Mailer configuration + config.action_mailer.raise_delivery_errors = true + # Set host to be used by links generated in mailer templates. - config.action_mailer.default_url_options = { host: "example.com" } - - # Specify outgoing SMTP server. Remember to add smtp/* credentials via rails credentials:edit. - # config.action_mailer.smtp_settings = { - # user_name: Rails.application.credentials.dig(:smtp, :user_name), - # password: Rails.application.credentials.dig(:smtp, :password), - # address: "smtp.example.com", - # port: 587, - # authentication: :plain - # } + config.action_mailer.default_url_options = { + host: ENV.fetch("APP_HOST", "90.156.228.95"), + protocol: "http" # Use http for IP-based deployment without SSL + } + + # SMTP settings - configure via environment variables or rails credentials + smtp_address = ENV["SMTP_ADDRESS"] || Rails.application.credentials.dig(:smtp, :address) + + if smtp_address.present? + # SMTP is configured - use it + config.action_mailer.perform_deliveries = true + config.action_mailer.delivery_method = :smtp + config.action_mailer.smtp_settings = { + address: smtp_address, + port: (ENV["SMTP_PORT"] || Rails.application.credentials.dig(:smtp, :port) || 587).to_i, + domain: ENV["SMTP_DOMAIN"] || Rails.application.credentials.dig(:smtp, :domain), + user_name: ENV["SMTP_USERNAME"] || Rails.application.credentials.dig(:smtp, :user_name), + password: ENV["SMTP_PASSWORD"] || Rails.application.credentials.dig(:smtp, :password), + authentication: :plain, + enable_starttls_auto: true + } + else + # SMTP not configured - disable email delivery (dev mode) + config.action_mailer.perform_deliveries = false + config.action_mailer.delivery_method = :test + Rails.logger.warn "⚠️ SMTP not configured. Email delivery is disabled. Configure SMTP_ADDRESS to enable." + end # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). @@ -80,11 +96,13 @@ config.active_record.attributes_for_inspect = [ :id ] # Enable DNS rebinding protection and other `Host` header attacks. - # config.hosts = [ - # "example.com", # Allow requests from example.com - # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` - # ] - # + config.hosts = [ + "90.156.228.95", # Allow requests from server IP + # Add your domain here when you have one: + # "yourdomain.com", + # /.*\.yourdomain\.com/ + ] + # Skip DNS rebinding protection for the default health check endpoint. - # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } + config.host_authorization = { exclude: ->(request) { request.path == "/up" } } end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 27992c4..3f789db 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -9,12 +9,15 @@ # Use this hook to configure devise mailer, warden hooks and so forth. # Many of these configuration options can be set straight in your model. Devise.setup do |config| - # The secret key used by Devise. Devise uses this key to generate - # random tokens. Changing this key will render invalid all existing - # confirmation, reset password and unlock tokens in the database. - # Devise will use the `secret_key_base` as its `secret_key` - # by default. You can change it below and use your own secret key. - # config.secret_key = '050b1fdbc8ecf2fc3aef40193adecc2a5b68f5dbf50f27d802aefe8d88ae83598e8bbebac835ecb9fba4b11faa6e7ad271e9c2ca3a373cdcd234fd837f50bbe5' + # Use Rails secret_key_base for Devise (safe and secure by default) + # Only use DEVISE_SECRET_KEY if it's explicitly set and has proper length (>= 64 bytes) + devise_key = ENV["DEVISE_SECRET_KEY"] + if devise_key.present? && devise_key.length >= 64 + config.secret_key = devise_key + else + # Fallback to Rails secret_key_base (recommended for production) + config.secret_key = Rails.application.secret_key_base + end # ==> Controller configuration # Configure the parent class to the devise controllers. @@ -24,7 +27,7 @@ # Configure the e-mail address which will be shown in Devise::Mailer, # note that it will be overwritten if you use your own mailer class # with default "from" parameter. - config.mailer_sender = "please-change-me-at-config-initializers-devise@example.com" + config.mailer_sender = ENV.fetch("MAILER_FROM_EMAIL", "noreply@stackoverflow-clone.com") # Configure the class responsible to send e-mails. # config.mailer = 'Devise::Mailer' @@ -273,12 +276,18 @@ # up on your models and hooks. # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' - config.omniauth :google_oauth2, Rails.application.credentials.dig(:google, :client_id), Rails.application.credentials.dig(:google, :client_secret), { - scope: "email,profile", - prompt: "select_account", - image_aspect_ratio: "square", - image_size: 50 - } + # Google OAuth2 configuration - using environment variables for production + google_client_id = ENV["GOOGLE_CLIENT_ID"] + google_client_secret = ENV["GOOGLE_CLIENT_SECRET"] + + if google_client_id.present? && google_client_secret.present? + config.omniauth :google_oauth2, google_client_id, google_client_secret, { + scope: "email,profile", + prompt: "select_account", + image_aspect_ratio: "square", + image_size: 50 + } + end # ==> Warden configuration # If you want to use other strategies, that are not supported by Devise, or diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 8e02906..f4305e9 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -6,7 +6,11 @@ end admin_authenticator do - current_user || redirect_to(new_user_session_url) + if current_user&.admin? + current_user + else + redirect_to(root_url, alert: "Access denied. Admin privileges required.") + end end default_scopes :public diff --git a/config/initializers/elasticsearch.rb b/config/initializers/elasticsearch.rb index 35a5c5b..35f6e83 100644 --- a/config/initializers/elasticsearch.rb +++ b/config/initializers/elasticsearch.rb @@ -18,3 +18,15 @@ if Rails.env.development? Elasticsearch::Model.client.transport.logger = Logger.new(STDOUT) end + +# Check Elasticsearch connection and log status +begin + if Elasticsearch::Model.client.ping + Rails.logger.info "✓ Elasticsearch connected successfully at #{ENV.fetch('ELASTICSEARCH_URL', 'http://127.0.0.1:9200')}" + end +rescue Faraday::ConnectionFailed, Elasticsearch::Transport::Transport::Errors::ServiceUnavailable => e + Rails.logger.warn "⚠ Elasticsearch connection failed: #{e.message}" + Rails.logger.warn "Search functionality will be limited. Please ensure Elasticsearch is running." +rescue => e + Rails.logger.error "✗ Elasticsearch error: #{e.message}" +end diff --git a/config/routes.rb b/config/routes.rb index 587bf94..876b69f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,17 @@ Rails.application.routes.draw do require "sidekiq/web" + + # Protect Sidekiq Web UI with basic authentication + Sidekiq::Web.use Rack::Auth::Basic do |username, password| + ActiveSupport::SecurityUtils.secure_compare( + ::Digest::SHA256.hexdigest(username), + ::Digest::SHA256.hexdigest(ENV.fetch("SIDEKIQ_USERNAME", "admin")) + ) & ActiveSupport::SecurityUtils.secure_compare( + ::Digest::SHA256.hexdigest(password), + ::Digest::SHA256.hexdigest(ENV.fetch("SIDEKIQ_PASSWORD", "changeme")) + ) + end + use_doorkeeper devise_for :users, controllers: { omniauth_callbacks: "users/omniauth_callbacks" diff --git a/db/migrate/20251013000050_add_admin_to_users.rb b/db/migrate/20251013000050_add_admin_to_users.rb new file mode 100644 index 0000000..655d227 --- /dev/null +++ b/db/migrate/20251013000050_add_admin_to_users.rb @@ -0,0 +1,6 @@ +class AddAdminToUsers < ActiveRecord::Migration[8.0] + def change + add_column :users, :admin, :boolean, default: false, null: false + add_index :users, :admin + end +end diff --git a/db/schema.rb b/db/schema.rb index b26f4e0..8a15748 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_09_08_175000) do +ActiveRecord::Schema[8.0].define(version: 2025_10_13_000050) do create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false @@ -160,6 +160,8 @@ t.string "confirmation_token" t.datetime "confirmation_sent_at" t.string "unconfirmed_email" + t.boolean "admin", default: false, null: false + t.index ["admin"], name: "index_users_on_admin" t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["email"], name: "index_users_on_email", unique: true t.index ["provider", "uid"], name: "index_users_on_provider_and_uid", unique: true From 6bf4c1afae7eb1cb45a77f0ad88f62eb0588a2b2 Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 14 Oct 2025 01:08:06 +0300 Subject: [PATCH 06/21] Fix Kamal deployment: resolve Devise key issue and production config --- config/environments/production.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/environments/production.rb b/config/environments/production.rb index 15a073f..ebe6453 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -82,7 +82,7 @@ # SMTP not configured - disable email delivery (dev mode) config.action_mailer.perform_deliveries = false config.action_mailer.delivery_method = :test - Rails.logger.warn "⚠️ SMTP not configured. Email delivery is disabled. Configure SMTP_ADDRESS to enable." + # Logger not available during initialization, warning will be logged at runtime if needed end # Enable locale fallbacks for I18n (makes lookups for any locale fall back to From 7539108a16e20788e8bdb7cab85ce3686bc551ca Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 14 Oct 2025 01:10:30 +0300 Subject: [PATCH 07/21] init elasticsearch --- config/initializers/elasticsearch.rb | 45 +++++++++++++++------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/config/initializers/elasticsearch.rb b/config/initializers/elasticsearch.rb index 35f6e83..472a7a4 100644 --- a/config/initializers/elasticsearch.rb +++ b/config/initializers/elasticsearch.rb @@ -4,29 +4,32 @@ # export ELASTICSEARCH_URL="https://elastic:password@localhost:9200" # If you don't have a local CA certificate handy, you can disable SSL verification in development only. -require "elasticsearch/model" +# Skip Elasticsearch initialization during asset precompilation +unless ENV['SECRET_KEY_BASE_DUMMY'] == '1' || defined?(::Rails::Console) + require "elasticsearch/model" -Elasticsearch::Model.client = Elasticsearch::Client.new( - url: ENV.fetch("ELASTICSEARCH_URL", "http://127.0.0.1:9200"), - transport_options: { - request: { timeout: 5, open_timeout: 2 } - }, - ssl: Rails.env.development? ? { verify: false } : {} -) + Elasticsearch::Model.client = Elasticsearch::Client.new( + url: ENV.fetch("ELASTICSEARCH_URL", "http://127.0.0.1:9200"), + transport_options: { + request: { timeout: 5, open_timeout: 2 } + }, + ssl: Rails.env.development? ? { verify: false } : {} + ) -# Optional: log to Rails logger in development -if Rails.env.development? - Elasticsearch::Model.client.transport.logger = Logger.new(STDOUT) -end + # Optional: log to Rails logger in development + if Rails.env.development? + Elasticsearch::Model.client.transport.logger = Logger.new(STDOUT) + end -# Check Elasticsearch connection and log status -begin - if Elasticsearch::Model.client.ping - Rails.logger.info "✓ Elasticsearch connected successfully at #{ENV.fetch('ELASTICSEARCH_URL', 'http://127.0.0.1:9200')}" + # Check Elasticsearch connection and log status + begin + if Elasticsearch::Model.client.ping + Rails.logger.info "✓ Elasticsearch connected successfully at #{ENV.fetch('ELASTICSEARCH_URL', 'http://127.0.0.1:9200')}" + end + rescue Faraday::ConnectionFailed, Elastic::Transport::Transport::Error => e + Rails.logger.warn "⚠ Elasticsearch connection failed: #{e.message}" + Rails.logger.warn "Search functionality will be limited. Please ensure Elasticsearch is running." + rescue => e + Rails.logger.error "✗ Elasticsearch error: #{e.message}" end -rescue Faraday::ConnectionFailed, Elasticsearch::Transport::Transport::Errors::ServiceUnavailable => e - Rails.logger.warn "⚠ Elasticsearch connection failed: #{e.message}" - Rails.logger.warn "Search functionality will be limited. Please ensure Elasticsearch is running." -rescue => e - Rails.logger.error "✗ Elasticsearch error: #{e.message}" end From 67c531f57f061cb6cbea65577d748e93808befc2 Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 14 Oct 2025 01:25:57 +0300 Subject: [PATCH 08/21] fix prod --- config/environments/production.rb | 35 ++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/config/environments/production.rb b/config/environments/production.rb index ebe6453..86d25d3 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -63,7 +63,32 @@ } # SMTP settings - configure via environment variables or rails credentials - smtp_address = ENV["SMTP_ADDRESS"] || Rails.application.credentials.dig(:smtp, :address) + # Only try to access credentials if RAILS_MASTER_KEY is properly set + smtp_credentials = if ENV['RAILS_MASTER_KEY'].present? && ENV['RAILS_MASTER_KEY'].length == 32 + begin + { + address: Rails.application.credentials.dig(:smtp, :address), + port: Rails.application.credentials.dig(:smtp, :port), + domain: Rails.application.credentials.dig(:smtp, :domain), + user_name: Rails.application.credentials.dig(:smtp, :user_name), + password: Rails.application.credentials.dig(:smtp, :password) + } + rescue ActiveSupport::MessageEncryptor::InvalidMessage, ArgumentError => e + # Credentials decryption failed - RAILS_MASTER_KEY is invalid + puts "⚠ Could not decrypt credentials: #{e.class}" + {} + rescue => e + # Other credentials errors - will use ENV vars only + puts "⚠ Credentials error: #{e.message}" + {} + end + else + # RAILS_MASTER_KEY not set or invalid length - skip credentials + puts "⚠ RAILS_MASTER_KEY not configured, using ENV variables only" + {} + end + + smtp_address = ENV["SMTP_ADDRESS"] || smtp_credentials[:address] if smtp_address.present? # SMTP is configured - use it @@ -71,10 +96,10 @@ config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { address: smtp_address, - port: (ENV["SMTP_PORT"] || Rails.application.credentials.dig(:smtp, :port) || 587).to_i, - domain: ENV["SMTP_DOMAIN"] || Rails.application.credentials.dig(:smtp, :domain), - user_name: ENV["SMTP_USERNAME"] || Rails.application.credentials.dig(:smtp, :user_name), - password: ENV["SMTP_PASSWORD"] || Rails.application.credentials.dig(:smtp, :password), + port: (ENV["SMTP_PORT"] || smtp_credentials[:port] || 587).to_i, + domain: ENV["SMTP_DOMAIN"] || smtp_credentials[:domain], + user_name: ENV["SMTP_USERNAME"] || smtp_credentials[:user_name], + password: ENV["SMTP_PASSWORD"] || smtp_credentials[:password], authentication: :plain, enable_starttls_auto: true } From d45641b22309bde355dd5964afa5384cc514b86c Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 14 Oct 2025 01:44:18 +0300 Subject: [PATCH 09/21] fix assets compile --- Dockerfile | 13 +++++++++++-- config/deploy.yml | 4 ++-- config/initializers/devise.rb | 13 ++++++++++++- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1375687..1719b5f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,9 +28,11 @@ ENV RAILS_ENV="production" \ # Throw-away build stage to reduce size of final image FROM base AS build -# Install packages needed to build gems +# Install packages needed to build gems and Node.js for TailwindCSS RUN apt-get update -qq && \ - apt-get install --no-install-recommends -y build-essential git libyaml-dev pkg-config && \ + apt-get install --no-install-recommends -y build-essential git libyaml-dev pkg-config curl && \ + curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ + apt-get install --no-install-recommends -y nodejs && \ rm -rf /var/lib/apt/lists /var/cache/apt/archives # Install application gems @@ -39,9 +41,16 @@ RUN bundle install && \ rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ bundle exec bootsnap precompile --gemfile +# Install JavaScript dependencies +COPY package.json package-lock.json ./ +RUN npm ci --include=dev + # Copy application code COPY . . +# Build TailwindCSS +RUN npm run build:css + # Precompile bootsnap code for faster boot times RUN bundle exec bootsnap precompile app/ lib/ diff --git a/config/deploy.yml b/config/deploy.yml index 87e03fb..02cee25 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -14,7 +14,7 @@ registry: env: secret: - - RAILS_MASTER_KEY + - SECRET_KEY_BASE - GOOGLE_CLIENT_ID - GOOGLE_CLIENT_SECRET - SIDEKIQ_USERNAME @@ -84,7 +84,7 @@ accessories: cmd: bundle exec sidekiq -C config/sidekiq.yml env: secret: - - RAILS_MASTER_KEY + - SECRET_KEY_BASE clear: RAILS_ENV: production REDIS_URL: redis://redis:6379/0 diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 3f789db..427daef 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -16,7 +16,18 @@ config.secret_key = devise_key else # Fallback to Rails secret_key_base (recommended for production) - config.secret_key = Rails.application.secret_key_base + # Safely access secret_key_base which may try to decrypt credentials + begin + config.secret_key = Rails.application.secret_key_base + rescue ActiveSupport::MessageEncryptor::InvalidMessage, ArgumentError => e + # Credentials decryption failed - generate a fallback secret + # This will be regenerated on each boot, so sessions will be invalidated + puts "⚠ WARNING: Using temporary secret key. Sessions will not persist across restarts." + config.secret_key = ENV.fetch("SECRET_KEY_BASE") do + require 'securerandom' + SecureRandom.hex(64) + end + end end # ==> Controller configuration From 752616e27c20b27f1e4b96269ade01a8c6563e40 Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 14 Oct 2025 02:21:05 +0300 Subject: [PATCH 10/21] fix: 422 ssl --- config/environments/production.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/environments/production.rb b/config/environments/production.rb index 86d25d3..c3fb637 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -25,7 +25,8 @@ config.active_storage.service = :local # Assume all access to the app is happening through a SSL-terminating reverse proxy. - config.assume_ssl = true + # Disabled for IP-based deployment without SSL certificate + config.assume_ssl = false # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # Note: Disabled for IP-based deployment. Enable when using a domain with proper SSL certificate. From 22fcc70c2df0b6326626919ebe7f84f76fb8f753 Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 14 Oct 2025 02:31:36 +0300 Subject: [PATCH 11/21] fix: 500 elasticsearch index --- app/models/answer.rb | 4 +++- app/models/comment.rb | 4 +++- app/models/question.rb | 4 +++- app/models/user.rb | 4 +++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/models/answer.rb b/app/models/answer.rb index 2fc8879..1b26984 100644 --- a/app/models/answer.rb +++ b/app/models/answer.rb @@ -2,7 +2,9 @@ class Answer < ApplicationRecord include Votable include Elasticsearch::Model - include Elasticsearch::Model::Callbacks + # Callbacks disabled to prevent 500 errors if Elasticsearch is unavailable + # Use Answer.__elasticsearch__.import for manual indexing if needed + # include Elasticsearch::Model::Callbacks settings index: { number_of_shards: 1, number_of_replicas: 0 } do mappings dynamic: false do diff --git a/app/models/comment.rb b/app/models/comment.rb index 1f0a9e3..21dab89 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -5,7 +5,9 @@ class Comment < ApplicationRecord validates :body, presence: true, length: { maximum: 2000 } include Elasticsearch::Model - include Elasticsearch::Model::Callbacks + # Callbacks disabled to prevent 500 errors if Elasticsearch is unavailable + # Use Comment.__elasticsearch__.import for manual indexing if needed + # include Elasticsearch::Model::Callbacks settings index: { number_of_shards: 1, number_of_replicas: 0 } do mappings dynamic: false do diff --git a/app/models/question.rb b/app/models/question.rb index e0b5e10..26d55ab 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -2,7 +2,9 @@ class Question < ApplicationRecord include Votable include Elasticsearch::Model - include Elasticsearch::Model::Callbacks + # Callbacks disabled to prevent 500 errors if Elasticsearch is unavailable + # Use Question.__elasticsearch__.import for manual indexing if needed + # include Elasticsearch::Model::Callbacks settings index: { number_of_shards: 1, number_of_replicas: 0 } do mappings dynamic: false do diff --git a/app/models/user.rb b/app/models/user.rb index 4d869b6..0bc076a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -4,7 +4,9 @@ class User < ApplicationRecord :confirmable, :omniauthable, omniauth_providers: [ :google_oauth2 ] include Elasticsearch::Model - include Elasticsearch::Model::Callbacks + # Callbacks disabled to prevent 500 errors if Elasticsearch is unavailable + # Use User.__elasticsearch__.import for manual indexing if needed + # include Elasticsearch::Model::Callbacks settings index: { number_of_shards: 1, number_of_replicas: 0 } do mappings dynamic: false do From ece37e38554ef228d92fb6094d6df44c458ff0b5 Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 14 Oct 2025 03:05:05 +0300 Subject: [PATCH 12/21] add async --- config/deploy.yml | 12 ++++++++++++ config/environments/production.rb | 6 +++++- config/initializers/devise_async.rb | 9 +++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 config/initializers/devise_async.rb diff --git a/config/deploy.yml b/config/deploy.yml index 02cee25..893c2e9 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -19,6 +19,9 @@ env: - GOOGLE_CLIENT_SECRET - SIDEKIQ_USERNAME - SIDEKIQ_PASSWORD + - SMTP_ADDRESS + - SMTP_USERNAME + - SMTP_PASSWORD clear: RAILS_ENV: production @@ -30,6 +33,8 @@ env: ELASTICSEARCH_URL: http://elasticsearch:9200 APP_HOST: "90.156.228.95" MAILER_FROM_EMAIL: "noreply@stackoverflow-clone.com" + SMTP_PORT: "587" + SMTP_DOMAIN: "90.156.228.95" aliases: console: app exec --interactive --reuse "bin/rails console" @@ -85,8 +90,15 @@ accessories: env: secret: - SECRET_KEY_BASE + - SMTP_ADDRESS + - SMTP_USERNAME + - SMTP_PASSWORD clear: RAILS_ENV: production REDIS_URL: redis://redis:6379/0 ELASTICSEARCH_URL: http://elasticsearch:9200 + APP_HOST: "90.156.228.95" + MAILER_FROM_EMAIL: "noreply@stackoverflow-clone.com" + SMTP_PORT: "587" + SMTP_DOMAIN: "90.156.228.95" diff --git a/config/environments/production.rb b/config/environments/production.rb index c3fb637..654c7b6 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -56,6 +56,8 @@ # Mailer configuration config.action_mailer.raise_delivery_errors = true + config.action_mailer.perform_deliveries = true + config.action_mailer.deliver_later_queue_name = :mailers # Set host to be used by links generated in mailer templates. config.action_mailer.default_url_options = { @@ -102,7 +104,9 @@ user_name: ENV["SMTP_USERNAME"] || smtp_credentials[:user_name], password: ENV["SMTP_PASSWORD"] || smtp_credentials[:password], authentication: :plain, - enable_starttls_auto: true + enable_starttls_auto: true, + open_timeout: 10, + read_timeout: 10 } else # SMTP not configured - disable email delivery (dev mode) diff --git a/config/initializers/devise_async.rb b/config/initializers/devise_async.rb new file mode 100644 index 0000000..90cec2e --- /dev/null +++ b/config/initializers/devise_async.rb @@ -0,0 +1,9 @@ +if Rails.env.production? + Devise::Models::Confirmable.class_eval do + protected + + def send_devise_notification(notification, *args) + devise_mailer.send(notification, self, *args).deliver_later + end + end +end From a8392d2c7e25a1b286ef1f14ca30be84fe7cb5d0 Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 14 Oct 2025 03:11:24 +0300 Subject: [PATCH 13/21] fix network --- config/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/deploy.yml b/config/deploy.yml index 893c2e9..cc049d4 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -67,14 +67,14 @@ accessories: redis: image: redis:7-alpine host: 90.156.228.95 - port: "127.0.0.1:6379:6379" + port: "6379" directories: - redis-data:/data elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:8.13.4 host: 90.156.228.95 - port: "127.0.0.1:9200:9200" + port: "9200" env: clear: discovery.type: single-node From bcee46a338d181b12f3a1fbdc5ea60dd6385af9e Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 14 Oct 2025 03:20:17 +0300 Subject: [PATCH 14/21] fix config --- config/deploy.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/config/deploy.yml b/config/deploy.yml index cc049d4..7ff05f0 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -29,8 +29,8 @@ env: RAILS_SERVE_STATIC_FILES: "true" WEB_CONCURRENCY: "2" RAILS_MAX_THREADS: "5" - REDIS_URL: redis://redis:6379/0 - ELASTICSEARCH_URL: http://elasticsearch:9200 + REDIS_URL: redis://stackoverflow_clone-redis:6379/0 + ELASTICSEARCH_URL: http://stackoverflow_clone-elasticsearch:9200 APP_HOST: "90.156.228.95" MAILER_FROM_EMAIL: "noreply@stackoverflow-clone.com" SMTP_PORT: "587" @@ -95,10 +95,9 @@ accessories: - SMTP_PASSWORD clear: RAILS_ENV: production - REDIS_URL: redis://redis:6379/0 - ELASTICSEARCH_URL: http://elasticsearch:9200 + REDIS_URL: redis://stackoverflow_clone-redis:6379/0 + ELASTICSEARCH_URL: http://stackoverflow_clone-elasticsearch:9200 APP_HOST: "90.156.228.95" MAILER_FROM_EMAIL: "noreply@stackoverflow-clone.com" SMTP_PORT: "587" SMTP_DOMAIN: "90.156.228.95" - From 44cbcaa33bb66f64208d4ec4a6f693fe2095f74b Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 14 Oct 2025 03:22:29 +0300 Subject: [PATCH 15/21] add mailers for sidekiq --- config/sidekiq.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/sidekiq.yml b/config/sidekiq.yml index b757ac2..f5721a9 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -1,9 +1,10 @@ :concurrency: 5 :queues: - default + - mailers :schedule: daily_digest: - cron: "0 8 * * *" # каждый день в 8 часов по времени сервера + cron: "0 8 * * *" class: "DailyDigestJob" queue: default From ce56a39d26c5d63aec52b394b3c054e3140c7d97 Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 14 Oct 2025 03:53:27 +0300 Subject: [PATCH 16/21] default --- config/environments/production.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/environments/production.rb b/config/environments/production.rb index 654c7b6..8c9426d 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -57,7 +57,7 @@ # Mailer configuration config.action_mailer.raise_delivery_errors = true config.action_mailer.perform_deliveries = true - config.action_mailer.deliver_later_queue_name = :mailers + config.action_mailer.deliver_later_queue_name = :default # Set host to be used by links generated in mailer templates. config.action_mailer.default_url_options = { From 29198ad8a39385c031296c58f7d2bae00d343b69 Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 14 Oct 2025 04:08:34 +0300 Subject: [PATCH 17/21] add storage sidekiq --- config/deploy.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/deploy.yml b/config/deploy.yml index 7ff05f0..4667df7 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -87,6 +87,10 @@ accessories: image: amsak701/stclone host: 90.156.228.95 cmd: bundle exec sidekiq -C config/sidekiq.yml + volumes: + - "stackoverflow_clone_storage:/rails/storage" + - "stackoverflow_clone_log:/rails/log" + - "stackoverflow_clone_tmp:/rails/tmp" env: secret: - SECRET_KEY_BASE From b5d00b31d6695572757f6316fd913e109b4e9653 Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 14 Oct 2025 04:22:18 +0300 Subject: [PATCH 18/21] seeds for production --- db/seeds.rb | 150 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 114 insertions(+), 36 deletions(-) diff --git a/db/seeds.rb b/db/seeds.rb index 0a510a5..2570163 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -7,51 +7,120 @@ Question.destroy_all if defined?(Question) User.destroy_all -puts "Creating test users..." -test_user = User.create!( - email: "test@example.com", - password: "password123", - password_confirmation: "password123", - confirmed_at: Time.current -) -puts "Created test user: #{test_user.email} with password: password123" +puts "Creating users..." -second_user = User.create!( - email: "user2@example.com", - password: "password123", - password_confirmation: "password123", - confirmed_at: Time.current -) -puts "Created second user: #{second_user.email} with password: password123" +# Create main users +users_data = [ + { email: "john.doe@example.com", name: "John Doe" }, + { email: "alice.smith@example.com", name: "Alice Smith" }, + { email: "bob.johnson@example.com", name: "Bob Johnson" }, + { email: "emma.wilson@example.com", name: "Emma Wilson" }, + { email: "mike.brown@example.com", name: "Mike Brown" } +] + +users = [] +users_data.each do |user_data| + user = User.create!( + email: user_data[:email], + password: "password123", + password_confirmation: "password123", + confirmed_at: Time.current + ) + users << user + puts "Created user: #{user.email}" +end puts "Creating questions..." questions = [ { - title: "How to use Active Record in Rails?", - body: "I'm new to Rails and would like to know how to properly use Active Record for database operations. What are the best practices?" + title: "How to deploy Rails application with Kamal?", + body: "I want to deploy my Rails 8 application using Kamal. What are the basic steps? I have a VPS with Ubuntu. Need help with configuration and setup." + }, + { + title: "Sidekiq not processing background jobs", + body: "My Sidekiq workers are running but jobs stay in the queue and never get processed. Redis connection is fine. What could be wrong?" + }, + { + title: "Best way to send emails in Rails production?", + body: "I'm getting SMTP timeout errors when sending emails. My hosting provider blocks port 587. What are alternative solutions for sending transactional emails?" }, { - title: "Differences between has_many and has_many :through", - body: "What's the difference between has_many and has_many :through relationships in Rails? When should I use each of them?" + title: "How to use Active Record associations?", + body: "I'm learning Rails and confused about has_many, belongs_to and has_many :through. Can someone explain with simple examples when to use each?" }, { - title: "How to set up RSpec in a Rails project?", - body: "I want to add tests to my Rails project. How do I properly set up RSpec and what gems should I add?" + title: "Docker volume permissions issue", + body: "Getting 'permission denied' when trying to access SQLite database from Docker container. How do I fix volume permissions?" }, { - title: "Problem with validation in Rails", - body: "When saving my model, I'm getting a validation error, but I can't figure out what's wrong. What's the best way to debug validations in Rails?" + title: "TailwindCSS not loading styles in production", + body: "My TailwindCSS styles work fine in development but don't load in production after deployment. Asset pipeline is configured. What am I missing?" }, { - title: "How to implement authentication in Rails?", - body: "What are the different ways to implement user authentication in Rails? Should I use Devise or build my own solution?" + title: "How to implement real-time features with ActionCable?", + body: "I want to add real-time notifications to my Rails app. Should I use ActionCable or something else? Looking for a simple tutorial." + }, + { + title: "RSpec vs Minitest - which is better?", + body: "Starting a new Rails project. Should I use RSpec or stick with Minitest? What are the pros and cons of each testing framework?" + }, + { + title: "Optimizing database queries in Rails", + body: "My application is slow with N+1 queries. I've heard about includes, joins, and eager loading. What's the difference and when should I use each?" + }, + { + title: "Authentication with Devise: setup guide", + body: "New to Devise gem. How do I set it up properly? Need basic authentication with email confirmation and password reset functionality." } ] # Create questions and answers created_questions = [] -questions.each do |question_data| - random_user = [ test_user, second_user ].sample +answers_data = { + 0 => [ # Kamal deployment + "First, install Kamal: `gem install kamal`. Then run `kamal init` in your project. Configure deploy.yml with your server details and run `kamal setup` followed by `kamal deploy`.", + "Check out the official Kamal documentation. You'll need Docker on your VPS. Basic steps: 1) Setup SSH keys 2) Configure deploy.yml 3) Run kamal setup 4) Deploy with kamal deploy. Make sure ports 80/443 are open." + ], + 1 => [ # Sidekiq not processing + "Check if Sidekiq is listening to the correct queue. Your jobs might be in 'mailers' queue but Sidekiq only processes 'default'. Add the queue to config/sidekiq.yml under :queues section.", + "Also verify that Sidekiq container has access to the same Redis instance as your app. Use `Sidekiq::Queue.new('default').size` to check queue size." + ], + 2 => [ # Email sending + "Use SendGrid or Mailgun API instead of SMTP. They provide HTTP APIs that bypass port restrictions. SendGrid free tier gives 100 emails/day. Just add their gem and configure with API key.", + "Another option is Amazon SES. It's cheap and reliable. You can also try port 2525 which some providers don't block." + ], + 3 => [ # Active Record associations + "belongs_to: child model has foreign key. has_many: parent can have multiple children. Example: User has_many :posts, Post belongs_to :user. Use has_many :through for many-to-many like User has_many :groups, through: :memberships.", + "Think of it like parent-child. belongs_to = this record belongs to another. has_many = this record owns many others. Through adds a join table in between for complex relationships." + ], + 4 => [ # Docker permissions + "Add volumes with proper permissions in your docker-compose or Kamal config. Make sure the app user inside container matches the volume owner. You can also run `chown -R rails:rails /rails/storage` in your Dockerfile.", + "Check your Dockerfile USER directive. SQLite needs write access to the directory. Mount volumes with correct user:group mapping." + ], + 5 => [ # TailwindCSS production + "Run `rails assets:precompile` before deployment. Make sure your build:css npm script runs during Docker build. Check if RAILS_SERVE_STATIC_FILES=true is set in production.", + "Verify that application.css imports your tailwind css file. Also check that NODE_ENV=production during build so Tailwind purges unused styles correctly." + ], + 6 => [ # ActionCable real-time + "ActionCable is built into Rails! Create a channel: `rails g channel Notifications`. Subscribe in JavaScript with `consumer.subscriptions.create`. Broadcast from server with `ActionCable.server.broadcast`.", + "For simple notifications, ActionCable is perfect. For complex real-time features, consider AnyCable for better performance. Check Rails guides for complete ActionCable tutorial." + ], + 7 => [ # RSpec vs Minitest + "RSpec has more readable syntax and powerful matchers. Minitest is simpler and included with Rails. For beginners, Minitest is easier. For large teams, RSpec's expressiveness helps. Both are excellent choices.", + "I prefer RSpec for its describe/context/it syntax. It makes tests read like documentation. But Minitest is faster and has less magic. Choose based on team preference." + ], + 8 => [ # Query optimization + "Use `includes` for eager loading to prevent N+1: `User.includes(:posts)`. Use `joins` for filtering: `User.joins(:posts).where(posts: {status: 'published'})`. Install bullet gem to detect N+1 queries in development.", + "The key difference: includes loads associated records (2 queries), joins just filters (1 query but doesn't load). Use select to limit columns: `User.select(:id, :email)` for better performance." + ], + 9 => [ # Devise setup + "Add gem 'devise' to Gemfile, run bundle install, then `rails generate devise:install`. Follow the instructions, then `rails generate devise User`. Run migrations. Configure mailer settings in config/environments.", + "Devise is great for quick auth setup. After generation, customize views with `rails generate devise:views`. Enable confirmable module in your User model for email confirmation." + ] +} + +questions.each_with_index do |question_data, index| + random_user = users.sample question = Question.create!( title: question_data[:title], @@ -60,20 +129,29 @@ created_at: rand(1..30).days.ago ) created_questions << question - puts "Created question: #{question.title} by #{random_user.email}" - - # Create 1-3 answers for each question - rand(1..3).times do |i| - answer_user = [ test_user, second_user ].sample + puts "Created question: #{question.title}" + # Create answers for this question + question_answers = answers_data[index] || [] + question_answers.each_with_index do |answer_body, answer_index| + answer_user = users.sample + answer = question.answers.create!( - body: "Answer #{i+1} to the question about #{question.title.downcase}. This contains a detailed explanation with code examples and recommendations.", + body: answer_body, user: answer_user, - created_at: rand(1..question.created_at.to_i).seconds.ago + created_at: question.created_at + rand(1..48).hours, + best: answer_index == 0 && rand < 0.5 # 50% chance first answer is marked as best ) - puts " - Created answer #{i+1} by #{answer_user.email} for question: #{question.title}" + puts " ✓ Created answer by #{answer_user.email}" end end +puts "\n" + "="*50 puts "Seed data created successfully!" -puts "Created #{Question.count} questions and #{Answer.count} answers." +puts "="*50 +puts "Users: #{User.count}" +puts "Questions: #{Question.count}" +puts "Answers: #{Answer.count}" +puts "\nLogin credentials for all users:" +puts "Password: password123" +puts "="*50 From d30a5dc55e4dd123136fa2c42814d1d66106bad3 Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 14 Oct 2025 04:31:45 +0300 Subject: [PATCH 19/21] seeds --- db/seeds.rb | 48 ++++++++++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/db/seeds.rb b/db/seeds.rb index 2570163..272158d 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -9,27 +9,33 @@ puts "Creating users..." -# Create main users +# Create main users with unique passwords users_data = [ - { email: "john.doe@example.com", name: "John Doe" }, - { email: "alice.smith@example.com", name: "Alice Smith" }, - { email: "bob.johnson@example.com", name: "Bob Johnson" }, - { email: "emma.wilson@example.com", name: "Emma Wilson" }, - { email: "mike.brown@example.com", name: "Mike Brown" } + { email: "john.doe@example.com", name: "John Doe", password: "john2024secure" }, + { email: "alice.smith@example.com", name: "Alice Smith", password: "alice_dev123" }, + { email: "bob.johnson@example.com", name: "Bob Johnson", password: "bob!secure99" }, + { email: "emma.wilson@example.com", name: "Emma Wilson", password: "emma@rails2024" }, + { email: "mike.brown@example.com", name: "Mike Brown", password: "mikebrown_pass" } ] users = [] +puts "\n" + "="*60 +puts "Creating users with credentials:" +puts "="*60 + users_data.each do |user_data| user = User.create!( email: user_data[:email], - password: "password123", - password_confirmation: "password123", + password: user_data[:password], + password_confirmation: user_data[:password], confirmed_at: Time.current ) users << user - puts "Created user: #{user.email}" + puts "✓ Email: #{user.email.ljust(30)} | Password: #{user_data[:password]}" end +puts "="*60 + puts "Creating questions..." questions = [ { @@ -146,12 +152,18 @@ end end -puts "\n" + "="*50 -puts "Seed data created successfully!" -puts "="*50 -puts "Users: #{User.count}" -puts "Questions: #{Question.count}" -puts "Answers: #{Answer.count}" -puts "\nLogin credentials for all users:" -puts "Password: password123" -puts "="*50 +puts "\n" + "="*70 +puts "🎉 Seed data created successfully!" +puts "="*70 +puts "📊 Statistics:" +puts " Users: #{User.count}" +puts " Questions: #{Question.count}" +puts " Answers: #{Answer.count}" +puts "\n🔐 Login Credentials:" +puts "-"*70 + +users_data.each do |user_data| + puts " Email: #{user_data[:email].ljust(35)} | Password: #{user_data[:password]}" +end + +puts "="*70 From bb5cb255afa5a5946450cb6b8d8cbb3508dcc64f Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 14 Oct 2025 04:39:15 +0300 Subject: [PATCH 20/21] chore --- app/models/answer.rb | 3 --- app/models/comment.rb | 3 --- app/models/question.rb | 3 --- app/models/user.rb | 3 --- config/deploy.yml | 8 +------- config/environments/production.rb | 6 ------ db/seeds.rb | 1 - 7 files changed, 1 insertion(+), 26 deletions(-) diff --git a/app/models/answer.rb b/app/models/answer.rb index 1b26984..33c9bc4 100644 --- a/app/models/answer.rb +++ b/app/models/answer.rb @@ -2,9 +2,6 @@ class Answer < ApplicationRecord include Votable include Elasticsearch::Model - # Callbacks disabled to prevent 500 errors if Elasticsearch is unavailable - # Use Answer.__elasticsearch__.import for manual indexing if needed - # include Elasticsearch::Model::Callbacks settings index: { number_of_shards: 1, number_of_replicas: 0 } do mappings dynamic: false do diff --git a/app/models/comment.rb b/app/models/comment.rb index 21dab89..6086ee8 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -5,9 +5,6 @@ class Comment < ApplicationRecord validates :body, presence: true, length: { maximum: 2000 } include Elasticsearch::Model - # Callbacks disabled to prevent 500 errors if Elasticsearch is unavailable - # Use Comment.__elasticsearch__.import for manual indexing if needed - # include Elasticsearch::Model::Callbacks settings index: { number_of_shards: 1, number_of_replicas: 0 } do mappings dynamic: false do diff --git a/app/models/question.rb b/app/models/question.rb index 26d55ab..9feb2ba 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -2,9 +2,6 @@ class Question < ApplicationRecord include Votable include Elasticsearch::Model - # Callbacks disabled to prevent 500 errors if Elasticsearch is unavailable - # Use Question.__elasticsearch__.import for manual indexing if needed - # include Elasticsearch::Model::Callbacks settings index: { number_of_shards: 1, number_of_replicas: 0 } do mappings dynamic: false do diff --git a/app/models/user.rb b/app/models/user.rb index 0bc076a..9982534 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -4,9 +4,6 @@ class User < ApplicationRecord :confirmable, :omniauthable, omniauth_providers: [ :google_oauth2 ] include Elasticsearch::Model - # Callbacks disabled to prevent 500 errors if Elasticsearch is unavailable - # Use User.__elasticsearch__.import for manual indexing if needed - # include Elasticsearch::Model::Callbacks settings index: { number_of_shards: 1, number_of_replicas: 0 } do mappings dynamic: false do diff --git a/config/deploy.yml b/config/deploy.yml index 4667df7..1bab561 100644 --- a/config/deploy.yml +++ b/config/deploy.yml @@ -53,15 +53,9 @@ builder: remote: 90.156.228.95 arch: amd64 -# Traefik reverse proxy with SSL proxy: - # ssl: true host: 90.156.228.95 - # Automatic SSL certificates via Let's Encrypt - # Note: Let's Encrypt requires a domain name, not IP - # For IP-only access, you'll need to manually configure SSL certificates - # acme: - # email: your-email@example.com + accessories: redis: diff --git a/config/environments/production.rb b/config/environments/production.rb index 8c9426d..daef3ab 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -65,8 +65,6 @@ protocol: "http" # Use http for IP-based deployment without SSL } - # SMTP settings - configure via environment variables or rails credentials - # Only try to access credentials if RAILS_MASTER_KEY is properly set smtp_credentials = if ENV['RAILS_MASTER_KEY'].present? && ENV['RAILS_MASTER_KEY'].length == 32 begin { @@ -77,16 +75,13 @@ password: Rails.application.credentials.dig(:smtp, :password) } rescue ActiveSupport::MessageEncryptor::InvalidMessage, ArgumentError => e - # Credentials decryption failed - RAILS_MASTER_KEY is invalid puts "⚠ Could not decrypt credentials: #{e.class}" {} rescue => e - # Other credentials errors - will use ENV vars only puts "⚠ Credentials error: #{e.message}" {} end else - # RAILS_MASTER_KEY not set or invalid length - skip credentials puts "⚠ RAILS_MASTER_KEY not configured, using ENV variables only" {} end @@ -94,7 +89,6 @@ smtp_address = ENV["SMTP_ADDRESS"] || smtp_credentials[:address] if smtp_address.present? - # SMTP is configured - use it config.action_mailer.perform_deliveries = true config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { diff --git a/db/seeds.rb b/db/seeds.rb index 272158d..9613adb 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -9,7 +9,6 @@ puts "Creating users..." -# Create main users with unique passwords users_data = [ { email: "john.doe@example.com", name: "John Doe", password: "john2024secure" }, { email: "alice.smith@example.com", name: "Alice Smith", password: "alice_dev123" }, From 0fc9646b8f4aef16e4fe4c6c8c10da17e349b48e Mon Sep 17 00:00:00 2001 From: amsak1983 Date: Tue, 14 Oct 2025 04:39:39 +0300 Subject: [PATCH 21/21] lint: rubocop -a --- config/environments/production.rb | 14 +++++++------- config/initializers/devise.rb | 4 ++-- config/initializers/elasticsearch.rb | 2 +- config/routes.rb | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/config/environments/production.rb b/config/environments/production.rb index daef3ab..eda4157 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -58,14 +58,14 @@ config.action_mailer.raise_delivery_errors = true config.action_mailer.perform_deliveries = true config.action_mailer.deliver_later_queue_name = :default - + # Set host to be used by links generated in mailer templates. - config.action_mailer.default_url_options = { + config.action_mailer.default_url_options = { host: ENV.fetch("APP_HOST", "90.156.228.95"), protocol: "http" # Use http for IP-based deployment without SSL } - smtp_credentials = if ENV['RAILS_MASTER_KEY'].present? && ENV['RAILS_MASTER_KEY'].length == 32 + smtp_credentials = if ENV["RAILS_MASTER_KEY"].present? && ENV["RAILS_MASTER_KEY"].length == 32 begin { address: Rails.application.credentials.dig(:smtp, :address), @@ -85,9 +85,9 @@ puts "⚠ RAILS_MASTER_KEY not configured, using ENV variables only" {} end - + smtp_address = ENV["SMTP_ADDRESS"] || smtp_credentials[:address] - + if smtp_address.present? config.action_mailer.perform_deliveries = true config.action_mailer.delivery_method = :smtp @@ -121,12 +121,12 @@ # Enable DNS rebinding protection and other `Host` header attacks. config.hosts = [ - "90.156.228.95", # Allow requests from server IP + "90.156.228.95" # Allow requests from server IP # Add your domain here when you have one: # "yourdomain.com", # /.*\.yourdomain\.com/ ] - + # Skip DNS rebinding protection for the default health check endpoint. config.host_authorization = { exclude: ->(request) { request.path == "/up" } } end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 427daef..47ba52c 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -24,7 +24,7 @@ # This will be regenerated on each boot, so sessions will be invalidated puts "⚠ WARNING: Using temporary secret key. Sessions will not persist across restarts." config.secret_key = ENV.fetch("SECRET_KEY_BASE") do - require 'securerandom' + require "securerandom" SecureRandom.hex(64) end end @@ -290,7 +290,7 @@ # Google OAuth2 configuration - using environment variables for production google_client_id = ENV["GOOGLE_CLIENT_ID"] google_client_secret = ENV["GOOGLE_CLIENT_SECRET"] - + if google_client_id.present? && google_client_secret.present? config.omniauth :google_oauth2, google_client_id, google_client_secret, { scope: "email,profile", diff --git a/config/initializers/elasticsearch.rb b/config/initializers/elasticsearch.rb index 472a7a4..16d709e 100644 --- a/config/initializers/elasticsearch.rb +++ b/config/initializers/elasticsearch.rb @@ -5,7 +5,7 @@ # If you don't have a local CA certificate handy, you can disable SSL verification in development only. # Skip Elasticsearch initialization during asset precompilation -unless ENV['SECRET_KEY_BASE_DUMMY'] == '1' || defined?(::Rails::Console) +unless ENV["SECRET_KEY_BASE_DUMMY"] == "1" || defined?(::Rails::Console) require "elasticsearch/model" Elasticsearch::Model.client = Elasticsearch::Client.new( diff --git a/config/routes.rb b/config/routes.rb index 876b69f..aba1e61 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,6 @@ Rails.application.routes.draw do require "sidekiq/web" - + # Protect Sidekiq Web UI with basic authentication Sidekiq::Web.use Rack::Auth::Basic do |username, password| ActiveSupport::SecurityUtils.secure_compare( @@ -11,7 +11,7 @@ ::Digest::SHA256.hexdigest(ENV.fetch("SIDEKIQ_PASSWORD", "changeme")) ) end - + use_doorkeeper devise_for :users, controllers: { omniauth_callbacks: "users/omniauth_callbacks"