From 5e329b5a410c178359d54e4bc4d6a12c3429e4d8 Mon Sep 17 00:00:00 2001 From: Mikhael Rakauskas Date: Thu, 14 Aug 2025 14:26:23 -0400 Subject: [PATCH 01/11] Enabled production deployment --- .env.production | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.production b/.env.production index 9bd25a9a..f28f09e1 100644 --- a/.env.production +++ b/.env.production @@ -12,4 +12,4 @@ AIRBRAKE_ID= AIRBRAKE_KEY= NEWRELIC_LICENSE_KEY= NEWRELIC_APP_NAME= -WEB_CONCURRENCY=2 \ No newline at end of file +WEB_CONCURRENCY=2 From c407f2f7ff220411306ece125201bb39c1d9b30a Mon Sep 17 00:00:00 2001 From: Mikhael Rakauskas Date: Fri, 15 Aug 2025 14:44:41 -0400 Subject: [PATCH 02/11] Newrelic basic setup + environments --- app/controllers/application_controller.rb | 12 ++++++++++-- app/jobs/application_job.rb | 14 ++++++++++++++ config/newrelic.yml | 2 +- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 421313aa..e8746357 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -10,8 +10,9 @@ class ApplicationController < ActionController::Base around_action :with_locale # before_action :sign_in_for_demo, if: -> { Docuseal.demo? } - before_action :maybe_authenticate_via_token - before_action :authenticate_via_token!, unless: :devise_controller? + before_action :maybe_redirect_to_setup, unless: :signed_in? + before_action :authenticate_user!, unless: :devise_controller? + # before_action :newrelic_metadata helper_method :button_title, :current_account, @@ -157,4 +158,11 @@ class ApplicationController < ActionController::Base redirect_to request.url.gsub('.co/', '.com/'), allow_other_host: true, status: :moved_permanently end + + # Add to this as required! + # def newrelic_metadata + # ::NewRelic::Agent.add_custom_attributes( + # user_id: current_user&.id + # ) + # end end diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb index 093e80b6..d10c0717 100644 --- a/app/jobs/application_job.rb +++ b/app/jobs/application_job.rb @@ -1,5 +1,19 @@ # frozen_string_literal: true class ApplicationJob < ActiveJob::Base + # include ::NewRelic::Agent::Instrumentation::ControllerInstrumentation + retry_on StandardError, wait: 6.seconds, attempts: 5 unless Docuseal.multitenant? + # unique :while_executing, on_conflict: :log + + # def perform(*args) + # receiver_str, _, message = args.shift.rpartition('.') + # time = Benchmark.measure do + # receiver_str.constantize.send(message, *args) + # end + # Rails.logger.info( + # "Finished #{receiver_str}.#{message}(#{args.map(&:to_s).join(', ')}): #{time}" + # ) + # end + # add_transaction_tracer :perform, category: :task end diff --git a/config/newrelic.yml b/config/newrelic.yml index 05b6041c..44ae7d8e 100644 --- a/config/newrelic.yml +++ b/config/newrelic.yml @@ -1114,4 +1114,4 @@ production: <<: *default_settings app_name: <%= ENV['NEWRELIC_APP_NAME'] %> Production monitor_mode: <%= ENV['NEWRELIC_MONITOR_MODE'].presence || true %> - distributed_tracing.enabled: false \ No newline at end of file + distributed_tracing.enabled: false From beb991b2a6784f0e172ee8e771b9d4f8b6ecd5dd Mon Sep 17 00:00:00 2001 From: Mikhael Rakauskas Date: Thu, 21 Aug 2025 11:28:21 -0400 Subject: [PATCH 03/11] Newrelic job tracing --- app/jobs/application_job.rb | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb index d10c0717..55b41697 100644 --- a/app/jobs/application_job.rb +++ b/app/jobs/application_job.rb @@ -1,19 +1,19 @@ # frozen_string_literal: true class ApplicationJob < ActiveJob::Base - # include ::NewRelic::Agent::Instrumentation::ControllerInstrumentation + include ::NewRelic::Agent::Instrumentation::ControllerInstrumentation retry_on StandardError, wait: 6.seconds, attempts: 5 unless Docuseal.multitenant? - # unique :while_executing, on_conflict: :log + unique :while_executing, on_conflict: :log - # def perform(*args) - # receiver_str, _, message = args.shift.rpartition('.') - # time = Benchmark.measure do - # receiver_str.constantize.send(message, *args) - # end - # Rails.logger.info( - # "Finished #{receiver_str}.#{message}(#{args.map(&:to_s).join(', ')}): #{time}" - # ) - # end - # add_transaction_tracer :perform, category: :task + def perform(*args) + receiver_str, _, message = args.shift.rpartition('.') + time = Benchmark.measure do + receiver_str.constantize.send(message, *args) + end + Rails.logger.info( + "Finished #{receiver_str}.#{message}(#{args.map(&:to_s).join(', ')}): #{time}" + ) + end + add_transaction_tracer :perform, category: :task end From 47a6ed69805397692d3d69d1d97bdd3f9e25100f Mon Sep 17 00:00:00 2001 From: Mikhael Rakauskas Date: Thu, 21 Aug 2025 13:37:34 -0400 Subject: [PATCH 04/11] Unique fix for benchmarking --- Gemfile | 1 + Gemfile.lock | 8 ++++++++ app/jobs/application_job.rb | 4 +++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index b4352fa7..893eb528 100644 --- a/Gemfile +++ b/Gemfile @@ -44,6 +44,7 @@ gem 'turbo-rails' gem 'twitter_cldr', require: false gem 'tzinfo-data' +gem 'activejob-uniqueness' gem 'airbrake' gem 'newrelic_rpm' diff --git a/Gemfile.lock b/Gemfile.lock index 52baffbd..0599aa7e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -47,6 +47,9 @@ GEM activejob (8.0.1) activesupport (= 8.0.1) globalid (>= 0.3.6) + activejob-uniqueness (0.4.0) + activejob (>= 4.2, < 8.1) + redlock (>= 2.0, < 3) activemodel (8.0.1) activesupport (= 8.0.1) activerecord (8.0.1) @@ -111,6 +114,7 @@ GEM base64 (0.2.0) bcrypt (3.1.20) benchmark (0.4.0) + benchmark-ips (2.12.0) better_html (2.1.1) actionview (>= 6.0) activesupport (>= 6.0) @@ -443,6 +447,8 @@ GEM psych (>= 4.0.0) redis-client (0.23.0) connection_pool + redlock (2.0.6) + redis-client (>= 0.14.1, < 1.0.0) regexp_parser (2.9.3) reline (0.6.0) io-console (~> 0.5) @@ -594,12 +600,14 @@ PLATFORMS x86_64-linux-musl DEPENDENCIES + activejob-uniqueness airbrake annotaterb arabic-letter-connector aws-sdk-s3 aws-sdk-secretsmanager azure-storage-blob + benchmark-ips (~> 2.12.0) better_html bootsnap brakeman diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb index 55b41697..4c38234e 100644 --- a/app/jobs/application_job.rb +++ b/app/jobs/application_job.rb @@ -1,10 +1,12 @@ # frozen_string_literal: true +require 'benchmark' + class ApplicationJob < ActiveJob::Base include ::NewRelic::Agent::Instrumentation::ControllerInstrumentation + unique :while_executing, on_conflict: :log retry_on StandardError, wait: 6.seconds, attempts: 5 unless Docuseal.multitenant? - unique :while_executing, on_conflict: :log def perform(*args) receiver_str, _, message = args.shift.rpartition('.') From fa3dad45b25d71ec3f80918108145af775445cae Mon Sep 17 00:00:00 2001 From: Mikhael Rakauskas Date: Fri, 22 Aug 2025 13:33:14 -0400 Subject: [PATCH 05/11] Update concurrency settings --- Gemfile.lock | 2 -- 1 file changed, 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 0599aa7e..566689b5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -114,7 +114,6 @@ GEM base64 (0.2.0) bcrypt (3.1.20) benchmark (0.4.0) - benchmark-ips (2.12.0) better_html (2.1.1) actionview (>= 6.0) activesupport (>= 6.0) @@ -607,7 +606,6 @@ DEPENDENCIES aws-sdk-s3 aws-sdk-secretsmanager azure-storage-blob - benchmark-ips (~> 2.12.0) better_html bootsnap brakeman From a0bb1638eb513a97d7f238775e5576d81061a723 Mon Sep 17 00:00:00 2001 From: Mikhael Rakauskas Date: Fri, 22 Aug 2025 13:33:38 -0400 Subject: [PATCH 06/11] Newrelic and airbrake updates - bare-bones new setups --- Gemfile | 1 - Gemfile.lock | 6 ------ app/controllers/application_controller.rb | 8 -------- app/jobs/application_job.rb | 16 ---------------- 4 files changed, 31 deletions(-) diff --git a/Gemfile b/Gemfile index 893eb528..b4352fa7 100644 --- a/Gemfile +++ b/Gemfile @@ -44,7 +44,6 @@ gem 'turbo-rails' gem 'twitter_cldr', require: false gem 'tzinfo-data' -gem 'activejob-uniqueness' gem 'airbrake' gem 'newrelic_rpm' diff --git a/Gemfile.lock b/Gemfile.lock index 566689b5..52baffbd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -47,9 +47,6 @@ GEM activejob (8.0.1) activesupport (= 8.0.1) globalid (>= 0.3.6) - activejob-uniqueness (0.4.0) - activejob (>= 4.2, < 8.1) - redlock (>= 2.0, < 3) activemodel (8.0.1) activesupport (= 8.0.1) activerecord (8.0.1) @@ -446,8 +443,6 @@ GEM psych (>= 4.0.0) redis-client (0.23.0) connection_pool - redlock (2.0.6) - redis-client (>= 0.14.1, < 1.0.0) regexp_parser (2.9.3) reline (0.6.0) io-console (~> 0.5) @@ -599,7 +594,6 @@ PLATFORMS x86_64-linux-musl DEPENDENCIES - activejob-uniqueness airbrake annotaterb arabic-letter-connector diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index e8746357..3f354512 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -12,7 +12,6 @@ class ApplicationController < ActionController::Base # before_action :sign_in_for_demo, if: -> { Docuseal.demo? } before_action :maybe_redirect_to_setup, unless: :signed_in? before_action :authenticate_user!, unless: :devise_controller? - # before_action :newrelic_metadata helper_method :button_title, :current_account, @@ -158,11 +157,4 @@ class ApplicationController < ActionController::Base redirect_to request.url.gsub('.co/', '.com/'), allow_other_host: true, status: :moved_permanently end - - # Add to this as required! - # def newrelic_metadata - # ::NewRelic::Agent.add_custom_attributes( - # user_id: current_user&.id - # ) - # end end diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb index 4c38234e..093e80b6 100644 --- a/app/jobs/application_job.rb +++ b/app/jobs/application_job.rb @@ -1,21 +1,5 @@ # frozen_string_literal: true -require 'benchmark' - class ApplicationJob < ActiveJob::Base - include ::NewRelic::Agent::Instrumentation::ControllerInstrumentation - unique :while_executing, on_conflict: :log - retry_on StandardError, wait: 6.seconds, attempts: 5 unless Docuseal.multitenant? - - def perform(*args) - receiver_str, _, message = args.shift.rpartition('.') - time = Benchmark.measure do - receiver_str.constantize.send(message, *args) - end - Rails.logger.info( - "Finished #{receiver_str}.#{message}(#{args.map(&:to_s).join(', ')}): #{time}" - ) - end - add_transaction_tracer :perform, category: :task end From 64909ea37e3c997e62afc16eb6c66ae8a14f8c3f Mon Sep 17 00:00:00 2001 From: Mikhael Rakauskas Date: Wed, 27 Aug 2025 17:16:01 -0400 Subject: [PATCH 07/11] Logging fix for CLI access --- config/database.yml | 22 ++++++--------------- config/environments/production.rb | 16 +++++++++------ config/environments/staging.rb | 33 +++++++------------------------ 3 files changed, 23 insertions(+), 48 deletions(-) diff --git a/config/database.yml b/config/database.yml index 0c804623..35795896 100644 --- a/config/database.yml +++ b/config/database.yml @@ -1,10 +1,14 @@ default: &default adapter: postgresql encoding: unicode + host: <%= ENV['DB_HOST'] %> + port: <%= ENV['DB_PORT'] %> + pool: <%= ENV['DB_POOL'] || 25 %> + username: <%= ENV['DB_USERNAME'] %> + password: <%= ENV['DB_PASSWORD'] %> + database: <%= ENV['DB_NAME'] %> development: - adapter: postgresql - encoding: unicode database: docuseal_development pool: 5 username: postgres @@ -12,8 +16,6 @@ development: host: localhost test: - adapter: postgresql - encoding: unicode database: docuseal_test pool: 5 username: postgres @@ -22,23 +24,11 @@ test: production: <<: *default - host: <%= ENV['DB_HOST'] %> - port: <%= ENV['DB_PORT'] %> - pool: <%= ENV['DB_POOL'] || 25 %> - username: <%= ENV['DB_USERNAME'] %> - password: <%= ENV['DB_PASSWORD'] %> - database: <%= ENV['DB_NAME'] %> sslmode: <%= ENV['DB_SSLMODE'] %> sslrootcert: <%= ENV['DB_SSLCERT'] %> staging: <<: *default - host: <%= ENV['DB_HOST'] %> - port: <%= ENV['DB_PORT'] %> - pool: <%= ENV['DB_POOL'] || 25 %> - username: <%= ENV['DB_USERNAME'] %> - password: <%= ENV['DB_PASSWORD'] %> - database: <%= ENV['DB_NAME'] %> sslmode: <%= ENV['DB_SSLMODE'] %> sslrootcert: <%= ENV['DB_SSLCERT'] %> variables: diff --git a/config/environments/production.rb b/config/environments/production.rb index 7e422baa..c745690e 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -96,13 +96,17 @@ Rails.application.configure do # Use default logging formatter so that PID and timestamp are not suppressed. config.log_formatter = Logger::Formatter.new - # Use a different logger for distributed setups. - # require "syslog/logger" - # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name") + # logger = ActiveSupport::Logger.new($stdout) + # logger.formatter = config.log_formatter + # config.logger = ActiveSupport::TaggedLogging.new(logger) - logger = ActiveSupport::Logger.new($stdout) - logger.formatter = config.log_formatter - config.logger = ActiveSupport::TaggedLogging.new(logger) + config.logger = ActiveSupport::TaggedLogging.new( + Logger.new($stdout) + ) + + config.active_job.logger = ActiveSupport::TaggedLogging.new( + Logger.new($stdout) + ) encryption_secret = ENV['ENCRYPTION_SECRET'].presence || Digest::SHA256.hexdigest(ENV['SECRET_KEY_BASE'].to_s) diff --git a/config/environments/staging.rb b/config/environments/staging.rb index 286a868a..89b8940c 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -91,32 +91,13 @@ Rails.application.configure do # Use default logging formatter so that PID and timestamp are not suppressed. config.log_formatter = Logger::Formatter.new - # Use a different logger for distributed setups. - # require "syslog/logger" - # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name") - - # logger = ActiveSupport::Logger.new($stdout) - # logger.formatter = config.log_formatter - # config.logger = ActiveSupport::TaggedLogging.new(logger) - - # NEWRELIC_MONITOR_MODE enables stdout logger sync for worker/web via NR APM - if ENV['NEWRELIC_MONITOR_MODE'].presence - config.logger = ActiveSupport::TaggedLogging.new( - Logger.new($stdout) - ) - - config.active_job.logger = ActiveSupport::TaggedLogging.new( - Logger.new($stdout) - ) - else - config.logger = ActiveSupport::TaggedLogging.new( - Syslog::Logger.new('rails-main') - ) - - config.active_job.logger = ActiveSupport::TaggedLogging.new( - Syslog::Logger.new('rails-sidekiq') - ) - end + config.logger = ActiveSupport::TaggedLogging.new( + Logger.new($stdout) + ) + + config.active_job.logger = ActiveSupport::TaggedLogging.new( + Logger.new($stdout) + ) encryption_secret = ENV['ENCRYPTION_SECRET'].presence || Digest::SHA256.hexdigest(ENV['SECRET_KEY_BASE'].to_s) From c2bd1be20a26a7b5c34ce92fc75a5eb1b43ca794 Mon Sep 17 00:00:00 2001 From: Mikhael Rakauskas Date: Fri, 29 Aug 2025 10:17:53 -0400 Subject: [PATCH 08/11] Add console access scripts --- bin/start_console_production | 277 +++++++++++++++++++++++++++++++++++ bin/start_console_staging | 277 +++++++++++++++++++++++++++++++++++ 2 files changed, 554 insertions(+) create mode 100755 bin/start_console_production create mode 100755 bin/start_console_staging diff --git a/bin/start_console_production b/bin/start_console_production new file mode 100755 index 00000000..08b5d5a9 --- /dev/null +++ b/bin/start_console_production @@ -0,0 +1,277 @@ +#!/bin/sh -e + +echo "=== CP Docuseal Production Console Startup ===" + +# Enable jemalloc for reduced memory usage and latency. +if [ -z "${LD_PRELOAD+x}" ]; then + LD_PRELOAD=$(find /usr/lib -name libjemalloc.so.2 -print -quit) + export LD_PRELOAD +fi + +check_aws_setup() { + if [ -z "$AWS_REGION" ]; then + echo "ERROR: AWS_REGION environment variable is not set" + exit 1 + fi + + if ! command -v aws &> /dev/null; then + echo "ERROR: AWS CLI is not installed. Please install it to proceed." + exit 1 + fi +} + +# Function to fetch secrets from AWS Secrets Manager +fetch_db_credentials() { + echo "Fetching database credentials from AWS Secrets Manager..." + + if [ -z "$DB_SECRETS_NAME" ]; then + echo "ERROR: DB_SECRETS_NAME environment variable is not set" + exit 1 + fi + + # Fetch the secret + echo "Retrieving secret: $DB_SECRETS_NAME" + SECRET_JSON=$(aws secretsmanager get-secret-value \ + --region "$AWS_REGION" \ + --secret-id "$DB_SECRETS_NAME" \ + --query SecretString \ + --output text) + + if [ $? -ne 0 ]; then + echo "ERROR: Failed to retrieve secrets from AWS Secrets Manager" + exit 1 + fi + + # Parse JSON and export environment variables + export DB_USERNAME=$(echo "$SECRET_JSON" | jq -r '.username') + export DB_PASSWORD=$(echo "$SECRET_JSON" | jq -r '.password') + + # Validate that we got the credentials + if [ "$DB_USERNAME" = "null" ] || [ "$DB_PASSWORD" = "null" ] || [ -z "$DB_USERNAME" ] || [ -z "$DB_PASSWORD" ]; then + echo "ERROR: Failed to parse database credentials from secrets" + echo "Expected JSON format: {\"username\": \"...\", \"password\": \"...\"}" + exit 1 + fi + + # Write credentials to .env.production file + echo "Writing database credentials to .env.production..." + + # Remove existing DB_USERNAME and DB_PASSWORD lines if they exist + if [ -f "./.env.production" ]; then + echo "Removing existing DB_USERNAME and DB_PASSWORD from .env.production" + grep -v "^DB_USERNAME=" ./.env.production > ./.env.production.tmp || true + grep -v "^DB_PASSWORD=" ./.env.production.tmp > ./.env.production || true + rm -f ./.env.production.tmp + fi + + # Append the new credentials + echo "DB_USERNAME=$DB_USERNAME" >> ./.env.production + echo "DB_PASSWORD=$DB_PASSWORD" >> ./.env.production + + echo "✓ Database credentials successfully retrieved and written to .env.production" +} + +# Function to fetch allowed hosts from AWS Secrets Manager and write to .env.production +fetch_allowed_hosts() { + echo "Fetching allowed hosts from AWS Secrets Manager..." + + if [ -z "$ALLOWED_HOSTS_NAME" ]; then + echo "ERROR: ALLOWED_HOSTS_NAME environment variable is not set" + exit 1 + fi + + # Fetch the secret value, assume kept as JSON array + ALLOWED_HOSTS_JSON=$(aws secretsmanager get-secret-value \ + --region "$AWS_REGION" \ + --secret-id "$ALLOWED_HOSTS_NAME" \ + --query SecretString \ + --output text) + + if [ $? -ne 0 ] || [ -z "$ALLOWED_HOSTS_JSON" ] || [ "$ALLOWED_HOSTS_JSON" = "null" ]; then + echo "ERROR: Failed to retrieve allowed hosts from AWS Secrets Manager" + exit 1 + fi + + # Extract the array and convert to comma-separated string + ALLOWED_HOSTS=$(echo "$ALLOWED_HOSTS_JSON" | jq -r '.allowed_hosts | join(",")') + + if [ -z "$ALLOWED_HOSTS" ] || [ "$ALLOWED_HOSTS" = "null" ]; then + echo "ERROR: Failed to parse allowed hosts from secrets. Check that the secret contains 'allowed_hosts' key." + exit 1 + fi + + # Remove existing ALLOWED_HOSTS line if it exists + if [ -f "./.env.production" ]; then + grep -v "^ALLOWED_HOSTS=" ./.env.production > ./.env.production.tmp || true + mv ./.env.production.tmp ./.env.production + fi + + # Append the new allowed hosts + echo "ALLOWED_HOSTS=$ALLOWED_HOSTS" >> ./.env.production + echo "✓ Allowed hosts successfully retrieved and written to .env.production" +} + +# Function to fetch encryption key from AWS Secrets Manager and write to config/master.key +fetch_encryption_key() { + echo "Fetching encryption key from AWS Secrets Manager..." + + ENCRYPTION_SECRET_NAME="cpdocuseal/encryption_key" + if [ -z "$AWS_REGION" ]; then + echo "ERROR: AWS_REGION environment variable is not set" + exit 1 + fi + + # Fetch the secret value (assume it's a plain string, not JSON) + ENCRYPTION_KEY=$(aws secretsmanager get-secret-value \ + --region "$AWS_REGION" \ + --secret-id "$ENCRYPTION_SECRET_NAME" \ + --query SecretString \ + --output text) + + if [ $? -ne 0 ] || [ -z "$ENCRYPTION_KEY" ] || [ "$ENCRYPTION_KEY" = "null" ]; then + echo "ERROR: Failed to retrieve encryption key from AWS Secrets Manager" + exit 1 + fi + + # Write the key to config/master.key + echo -n "$ENCRYPTION_KEY" > config/master.key + chmod 600 config/master.key + echo "✓ Encryption key written to config/master.key" +} + +fetch_env_variables() { + echo "Fetching environment variables from AWS Secrets Manager..." + + if [ -z "$CP_VARIABLES_NAME" ]; then + echo "ERROR: CP_VARIABLES_NAME environment variable is not set" + exit 1 + fi + + # Fetch the secret + echo "Retrieving secret: $CP_VARIABLES_NAME" + SECRET_JSON=$(aws secretsmanager get-secret-value \ + --region "$AWS_REGION" \ + --secret-id "$CP_VARIABLES_NAME" \ + --query SecretString \ + --output text) + + if [ $? -ne 0 ]; then + echo "ERROR: Failed to retrieve secrets from AWS Secrets Manager" + exit 1 + fi + + export DB_HOST=$(echo "$SECRET_JSON" | jq -r '.host') + export REDIS_URL=$(echo "$SECRET_JSON" | jq -r '.redis_url') + export S3_ATTACHMENTS_BUCKET=$(echo "$SECRET_JSON" | jq -r '.s3_attachments_bucket') + export AIRBRAKE_ID=$(echo "$SECRET_JSON" | jq -r '.airbrake_id') + export AIRBRAKE_KEY=$(echo "$SECRET_JSON" | jq -r '.airbrake_key') + export NEWRELIC_LICENSE_KEY=$(echo "$SECRET_JSON" | jq -r '.newrelic_license_key') + export NEWRELIC_APP_NAME=$(echo "$SECRET_JSON" | jq -r '.newrelic_app_name') + export NEWRELIC_MONITOR_MODE=$(echo "$SECRET_JSON" | jq -r '.newrelic_monitor_mode') + + + # Validate that we got the values + if [ "$DB_HOST" = "null" ] || [ "$REDIS_URL" = "null" ] || [ "$S3_ATTACHMENTS_BUCKET" = "null" ] || [ -z "$DB_HOST" ] || [ -z "$REDIS_URL" ] || [ -z "$S3_ATTACHMENTS_BUCKET" ]; then + echo "ERROR: Failed to parse variables from secrets" + echo "Expected JSON format: {\"key\": \"...\", ...}" + exit 1 + fi + + # Write variables to .env.production file + echo "Writing environment variables to .env.production..." + + # Remove existing DB_HOST, REDIS_URL, and S3_ATTACHMENTS_BUCKET lines if they exist + if [ -f "./.env.production" ]; then + echo "Removing existing variables from .env.production" + grep -v "^DB_HOST=" ./.env.production > ./.env.production.tmp || true + grep -v "^REDIS_URL=" ./.env.production.tmp > ./.env.production || true + grep -v "^S3_ATTACHMENTS_BUCKET=" ./.env.production.tmp > ./.env.production || true + grep -v "^AIRBRAKE_ID=" ./.env.production.tmp > ./.env.production || true + grep -v "^AIRBRAKE_KEY=" ./.env.production.tmp > ./.env.production || true + grep -v "^NEWRELIC_LICENSE_KEY=" ./.env.production.tmp > ./.env.production || true + grep -v "^NEWRELIC_APP_NAME=" ./.env.production.tmp > ./.env.production || true + grep -v "^NEWRELIC_MONITOR_MODE=" ./.env.production.tmp > ./.env.production || true + rm -f ./.env.production.tmp + fi + + # Append the new credentials + echo "DB_HOST=$DB_HOST" >> ./.env.production + echo "REDIS_URL=$REDIS_URL" >> ./.env.production + echo "S3_ATTACHMENTS_BUCKET=$S3_ATTACHMENTS_BUCKET" >> ./.env.production + echo "AIRBRAKE_ID=$AIRBRAKE_ID" >> ./.env.production + echo "AIRBRAKE_KEY=$AIRBRAKE_KEY" >> ./.env.production + echo "NEWRELIC_LICENSE_KEY=$NEWRELIC_LICENSE_KEY" >> ./.env.production + echo "NEWRELIC_APP_NAME=$NEWRELIC_APP_NAME" >> ./.env.production + echo "NEWRELIC_MONITOR_MODE=$NEWRELIC_MONITOR_MODE" >> ./.env.production + + echo "✓ Environment variables successfully retrieved and written to .env.production" +} + +# Function to setup database +setup_database() { + echo "Running database migrations..." + ./bin/rails db:migrate + + if [ $? -eq 0 ]; then + echo "✓ Database migrations completed successfully" + else + echo "ERROR: Database migrations failed" + exit 1 + fi +} + +set_environment() { + if [ -f "./.env.production" ]; then + echo "Setting environment variables from .env.production" + set -a + . ./.env.production + set +a + fi +} + +# Main execution +main() { + cd ../../app/ + + set_environment + + check_aws_setup + + echo "Starting CP Docuseal in production mode..." + echo "Rails Environment: ${RAILS_ENV:-production}" + + # Fetch database credentials from Secrets Manager + fetch_db_credentials + + # Fetch encryption key and write to config/master.key + fetch_encryption_key + + # Fetch allowed hosts from Secrets Manager + fetch_allowed_hosts + + # Fetch other environment variables from Secrets Manager + fetch_env_variables + + # Load updated environment variables + set_environment + + # Setup and migrate database + setup_database + + echo "=== Startup Complete - Starting Rails Console ===" + echo "Database Host: ${DB_HOST:-not set}" + echo "Database Port: ${DB_PORT:-not set}" + echo "S3 Bucket: ${S3_ATTACHMENTS_BUCKET:-not set}" + + # Check if READONLY mode is enabled + if [ "$READONLY" = "true" ]; then + echo "Starting Rails console in READONLY mode..." + exec ./bin/rails console -e 'ActiveRecord::Base.connection.execute("SET default_transaction_read_only = true")' + else + echo "Starting Rails console in normal mode..." + exec ./bin/rails console + fi +} + +# Execute main function with all arguments +main "$@" diff --git a/bin/start_console_staging b/bin/start_console_staging new file mode 100755 index 00000000..3e9513fd --- /dev/null +++ b/bin/start_console_staging @@ -0,0 +1,277 @@ +#!/bin/sh -e + +echo "=== CP Docuseal Staging Console Startup ===" + +# Enable jemalloc for reduced memory usage and latency. +if [ -z "${LD_PRELOAD+x}" ]; then + LD_PRELOAD=$(find /usr/lib -name libjemalloc.so.2 -print -quit) + export LD_PRELOAD +fi + +check_aws_setup() { + if [ -z "$AWS_REGION" ]; then + echo "ERROR: AWS_REGION environment variable is not set" + exit 1 + fi + + if ! command -v aws &> /dev/null; then + echo "ERROR: AWS CLI is not installed. Please install it to proceed." + exit 1 + fi +} + +# Function to fetch secrets from AWS Secrets Manager +fetch_db_credentials() { + echo "Fetching database credentials from AWS Secrets Manager..." + + if [ -z "$DB_SECRETS_NAME" ]; then + echo "ERROR: DB_SECRETS_NAME environment variable is not set" + exit 1 + fi + + # Fetch the secret + echo "Retrieving secret: $DB_SECRETS_NAME" + SECRET_JSON=$(aws secretsmanager get-secret-value \ + --region "$AWS_REGION" \ + --secret-id "$DB_SECRETS_NAME" \ + --query SecretString \ + --output text) + + if [ $? -ne 0 ]; then + echo "ERROR: Failed to retrieve secrets from AWS Secrets Manager" + exit 1 + fi + + # Parse JSON and export environment variables + export DB_USERNAME=$(echo "$SECRET_JSON" | jq -r '.username') + export DB_PASSWORD=$(echo "$SECRET_JSON" | jq -r '.password') + + # Validate that we got the credentials + if [ "$DB_USERNAME" = "null" ] || [ "$DB_PASSWORD" = "null" ] || [ -z "$DB_USERNAME" ] || [ -z "$DB_PASSWORD" ]; then + echo "ERROR: Failed to parse database credentials from secrets" + echo "Expected JSON format: {\"username\": \"...\", \"password\": \"...\"}" + exit 1 + fi + + # Write credentials to .env.staging file + echo "Writing database credentials to .env.staging..." + + # Remove existing DB_USERNAME and DB_PASSWORD lines if they exist + if [ -f "./.env.staging" ]; then + echo "Removing existing DB_USERNAME and DB_PASSWORD from .env.staging" + grep -v "^DB_USERNAME=" ./.env.staging > ./.env.staging.tmp || true + grep -v "^DB_PASSWORD=" ./.env.staging.tmp > ./.env.staging || true + rm -f ./.env.staging.tmp + fi + + # Append the new credentials + echo "DB_USERNAME=$DB_USERNAME" >> ./.env.staging + echo "DB_PASSWORD=$DB_PASSWORD" >> ./.env.staging + + echo "✓ Database credentials successfully retrieved and written to .env.staging" +} + +# Function to fetch allowed hosts from AWS Secrets Manager and write to .env.staging +fetch_allowed_hosts() { + echo "Fetching allowed hosts from AWS Secrets Manager..." + + if [ -z "$ALLOWED_HOSTS_NAME" ]; then + echo "ERROR: ALLOWED_HOSTS_NAME environment variable is not set" + exit 1 + fi + + # Fetch the secret value, assume kept as JSON array + ALLOWED_HOSTS_JSON=$(aws secretsmanager get-secret-value \ + --region "$AWS_REGION" \ + --secret-id "$ALLOWED_HOSTS_NAME" \ + --query SecretString \ + --output text) + + if [ $? -ne 0 ] || [ -z "$ALLOWED_HOSTS_JSON" ] || [ "$ALLOWED_HOSTS_JSON" = "null" ]; then + echo "ERROR: Failed to retrieve allowed hosts from AWS Secrets Manager" + exit 1 + fi + + # Extract the array and convert to comma-separated string + ALLOWED_HOSTS=$(echo "$ALLOWED_HOSTS_JSON" | jq -r '.allowed_hosts | join(",")') + + if [ -z "$ALLOWED_HOSTS" ] || [ "$ALLOWED_HOSTS" = "null" ]; then + echo "ERROR: Failed to parse allowed hosts from secrets. Check that the secret contains 'allowed_hosts' key." + exit 1 + fi + + # Remove existing ALLOWED_HOSTS line if it exists + if [ -f "./.env.staging" ]; then + grep -v "^ALLOWED_HOSTS=" ./.env.staging > ./.env.staging.tmp || true + mv ./.env.staging.tmp ./.env.staging + fi + + # Append the new allowed hosts + echo "ALLOWED_HOSTS=$ALLOWED_HOSTS" >> ./.env.staging + echo "✓ Allowed hosts successfully retrieved and written to .env.staging" +} + +# Function to fetch encryption key from AWS Secrets Manager and write to config/master.key +fetch_encryption_key() { + echo "Fetching encryption key from AWS Secrets Manager..." + + ENCRYPTION_SECRET_NAME="cpdocuseal/encryption_key" + if [ -z "$AWS_REGION" ]; then + echo "ERROR: AWS_REGION environment variable is not set" + exit 1 + fi + + # Fetch the secret value (assume it's a plain string, not JSON) + ENCRYPTION_KEY=$(aws secretsmanager get-secret-value \ + --region "$AWS_REGION" \ + --secret-id "$ENCRYPTION_SECRET_NAME" \ + --query SecretString \ + --output text) + + if [ $? -ne 0 ] || [ -z "$ENCRYPTION_KEY" ] || [ "$ENCRYPTION_KEY" = "null" ]; then + echo "ERROR: Failed to retrieve encryption key from AWS Secrets Manager" + exit 1 + fi + + # Write the key to config/master.key + echo -n "$ENCRYPTION_KEY" > config/master.key + chmod 600 config/master.key + echo "✓ Encryption key written to config/master.key" +} + +fetch_env_variables() { + echo "Fetching environment variables from AWS Secrets Manager..." + + if [ -z "$CP_VARIABLES_NAME" ]; then + echo "ERROR: CP_VARIABLES_NAME environment variable is not set" + exit 1 + fi + + # Fetch the secret + echo "Retrieving secret: $CP_VARIABLES_NAME" + SECRET_JSON=$(aws secretsmanager get-secret-value \ + --region "$AWS_REGION" \ + --secret-id "$CP_VARIABLES_NAME" \ + --query SecretString \ + --output text) + + if [ $? -ne 0 ]; then + echo "ERROR: Failed to retrieve secrets from AWS Secrets Manager" + exit 1 + fi + + export DB_HOST=$(echo "$SECRET_JSON" | jq -r '.host') + export REDIS_URL=$(echo "$SECRET_JSON" | jq -r '.redis_url') + export S3_ATTACHMENTS_BUCKET=$(echo "$SECRET_JSON" | jq -r '.s3_attachments_bucket') + export AIRBRAKE_ID=$(echo "$SECRET_JSON" | jq -r '.airbrake_id') + export AIRBRAKE_KEY=$(echo "$SECRET_JSON" | jq -r '.airbrake_key') + export NEWRELIC_LICENSE_KEY=$(echo "$SECRET_JSON" | jq -r '.newrelic_license_key') + export NEWRELIC_APP_NAME=$(echo "$SECRET_JSON" | jq -r '.newrelic_app_name') + export NEWRELIC_MONITOR_MODE=$(echo "$SECRET_JSON" | jq -r '.newrelic_monitor_mode') + + + # Validate that we got the values + if [ "$DB_HOST" = "null" ] || [ "$REDIS_URL" = "null" ] || [ "$S3_ATTACHMENTS_BUCKET" = "null" ] || [ -z "$DB_HOST" ] || [ -z "$REDIS_URL" ] || [ -z "$S3_ATTACHMENTS_BUCKET" ]; then + echo "ERROR: Failed to parse variables from secrets" + echo "Expected JSON format: {\"key\": \"...\", ...}" + exit 1 + fi + + # Write variables to .env.staging file + echo "Writing environment variables to .env.staging..." + + # Remove existing DB_HOST, REDIS_URL, and S3_ATTACHMENTS_BUCKET lines if they exist + if [ -f "./.env.staging" ]; then + echo "Removing existing variables from .env.staging" + grep -v "^DB_HOST=" ./.env.staging > ./.env.staging.tmp || true + grep -v "^REDIS_URL=" ./.env.staging.tmp > ./.env.staging || true + grep -v "^S3_ATTACHMENTS_BUCKET=" ./.env.staging.tmp > ./.env.staging || true + grep -v "^AIRBRAKE_ID=" ./.env.staging.tmp > ./.env.staging || true + grep -v "^AIRBRAKE_KEY=" ./.env.staging.tmp > ./.env.staging || true + grep -v "^NEWRELIC_LICENSE_KEY=" ./.env.staging.tmp > ./.env.staging || true + grep -v "^NEWRELIC_APP_NAME=" ./.env.staging.tmp > ./.env.staging || true + grep -v "^NEWRELIC_MONITOR_MODE=" ./.env.staging.tmp > ./.env.staging || true + rm -f ./.env.staging.tmp + fi + + # Append the new credentials + echo "DB_HOST=$DB_HOST" >> ./.env.staging + echo "REDIS_URL=$REDIS_URL" >> ./.env.staging + echo "S3_ATTACHMENTS_BUCKET=$S3_ATTACHMENTS_BUCKET" >> ./.env.staging + echo "AIRBRAKE_ID=$AIRBRAKE_ID" >> ./.env.staging + echo "AIRBRAKE_KEY=$AIRBRAKE_KEY" >> ./.env.staging + echo "NEWRELIC_LICENSE_KEY=$NEWRELIC_LICENSE_KEY" >> ./.env.staging + echo "NEWRELIC_APP_NAME=$NEWRELIC_APP_NAME" >> ./.env.staging + echo "NEWRELIC_MONITOR_MODE=$NEWRELIC_MONITOR_MODE" >> ./.env.staging + + echo "✓ Environment variables successfully retrieved and written to .env.staging" +} + +# Function to setup database +setup_database() { + echo "Running database migrations..." + ./bin/rails db:migrate + + if [ $? -eq 0 ]; then + echo "✓ Database migrations completed successfully" + else + echo "ERROR: Database migrations failed" + exit 1 + fi +} + +set_environment() { + if [ -f "./.env.staging" ]; then + echo "Setting environment variables from .env.staging" + set -a + . ./.env.staging + set +a + fi +} + +# Main execution +main() { + cd ../../app/ + + set_environment + + check_aws_setup + + echo "Starting CP Docuseal in staging mode..." + echo "Rails Environment: ${RAILS_ENV:-staging}" + + # Fetch database credentials from Secrets Manager + fetch_db_credentials + + # Fetch encryption key and write to config/master.key + fetch_encryption_key + + # Fetch allowed hosts from Secrets Manager + fetch_allowed_hosts + + # Fetch other environment variables from Secrets Manager + fetch_env_variables + + # Load updated environment variables + set_environment + + # Setup and migrate database + setup_database + + echo "=== Startup Complete - Starting Rails Console ===" + echo "Database Host: ${DB_HOST:-not set}" + echo "Database Port: ${DB_PORT:-not set}" + echo "S3 Bucket: ${S3_ATTACHMENTS_BUCKET:-not set}" + + # Check if READONLY mode is enabled + if [ "$READONLY" = "true" ]; then + echo "Starting Rails console in READONLY mode..." + exec ./bin/rails console -e 'ActiveRecord::Base.connection.execute("SET default_transaction_read_only = true")' + else + echo "Starting Rails console in normal mode..." + exec ./bin/rails console + fi +} + +# Execute main function with all arguments +main "$@" From e2c21ce358b730b703dc6b80e244700189db27b3 Mon Sep 17 00:00:00 2001 From: Mikhael Rakauskas Date: Fri, 29 Aug 2025 10:36:09 -0400 Subject: [PATCH 09/11] Fix bad merge --- app/controllers/application_controller.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 3f354512..1f2a1c38 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,5 +1,3 @@ -# frozen_string_literal: true - class ApplicationController < ActionController::Base BROWSER_LOCALE_REGEXP = /\A\w{2}(?:-\w{2})?/ @@ -10,8 +8,8 @@ class ApplicationController < ActionController::Base around_action :with_locale # before_action :sign_in_for_demo, if: -> { Docuseal.demo? } - before_action :maybe_redirect_to_setup, unless: :signed_in? - before_action :authenticate_user!, unless: :devise_controller? + before_action :maybe_authenticate_via_token + before_action :authenticate_via_token!, unless: :devise_controller? helper_method :button_title, :current_account, From 9da8811b23dea224d5ae28d8a5aa85e3e2fed19e Mon Sep 17 00:00:00 2001 From: Mikhael Rakauskas Date: Fri, 29 Aug 2025 11:25:46 -0400 Subject: [PATCH 10/11] Tweak readonly mode --- bin/start_console_production | 3 ++- bin/start_console_staging | 3 ++- config/initializers/readonly_mode.rb | 32 ++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 config/initializers/readonly_mode.rb diff --git a/bin/start_console_production b/bin/start_console_production index 08b5d5a9..7ba895a6 100755 --- a/bin/start_console_production +++ b/bin/start_console_production @@ -266,7 +266,8 @@ main() { # Check if READONLY mode is enabled if [ "$READONLY" = "true" ]; then echo "Starting Rails console in READONLY mode..." - exec ./bin/rails console -e 'ActiveRecord::Base.connection.execute("SET default_transaction_read_only = true")' + export RAILS_READONLY=true + exec ./bin/rails console else echo "Starting Rails console in normal mode..." exec ./bin/rails console diff --git a/bin/start_console_staging b/bin/start_console_staging index 3e9513fd..0991d160 100755 --- a/bin/start_console_staging +++ b/bin/start_console_staging @@ -266,7 +266,8 @@ main() { # Check if READONLY mode is enabled if [ "$READONLY" = "true" ]; then echo "Starting Rails console in READONLY mode..." - exec ./bin/rails console -e 'ActiveRecord::Base.connection.execute("SET default_transaction_read_only = true")' + export RAILS_READONLY=true + exec ./bin/rails console else echo "Starting Rails console in normal mode..." exec ./bin/rails console diff --git a/config/initializers/readonly_mode.rb b/config/initializers/readonly_mode.rb new file mode 100644 index 00000000..1ea1ab6e --- /dev/null +++ b/config/initializers/readonly_mode.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +# Set database to readonly mode when RAILS_READONLY environment variable is set +if ENV['RAILS_READONLY'] == 'true' + Rails.application.config.after_initialize do + if defined?(Rails::Console) + puts 'Setting database connection to read-only mode...' + + # Delay execution to ensure all connections are established + at_exit do + # Ensure we have an active connection + ActiveRecord::Base.establish_connection + + # Set readonly mode at the database level + ActiveRecord::Base.connection.execute('SET SESSION default_transaction_read_only = true') + puts '✓ Database session is now in read-only mode. Any write operations will fail.' + rescue StandardError => e + puts "⚠ Warning: Could not set read-only mode: #{e.message}" + end + + # Also set it immediately if connection is already available + begin + if ActiveRecord::Base.connection_pool.connected? + ActiveRecord::Base.connection.execute('SET SESSION default_transaction_read_only = true') + puts '✓ Database session is now in read-only mode. Any write operations will fail.' + end + rescue StandardError => e + puts "⚠ Note: Will set read-only mode when console starts: #{e.message}" + end + end + end +end From d8e5bf821cacb4a253f9b338eaaf99444480fce0 Mon Sep 17 00:00:00 2001 From: Mikhael Rakauskas Date: Fri, 29 Aug 2025 13:02:43 -0400 Subject: [PATCH 11/11] Rubocop fix --- app/controllers/application_controller.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 1f2a1c38..421313aa 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ApplicationController < ActionController::Base BROWSER_LOCALE_REGEXP = /\A\w{2}(?:-\w{2})?/