diff --git a/.env.development b/.env.development
index be97e459..a042eca0 100644
--- a/.env.development
+++ b/.env.development
@@ -3,4 +3,4 @@ DB_PASSWORD=postgres
DB_PORT=5432
DB_USERNAME=postgres
REDIS_URL=redis://host.docker.internal:6379/7
-PORT=3000
+PORT=3001
diff --git a/.env.production b/.env.production
new file mode 100644
index 00000000..9bd25a9a
--- /dev/null
+++ b/.env.production
@@ -0,0 +1,15 @@
+DB_HOST=
+DB_POOL=25
+DB_PORT=5432
+DB_SSLCERT=/config/rds-combined-ca-bundle.pem
+DB_SSLMODE=verify-full
+REDIS_URL=
+PORT=3000
+S3_ATTACHMENTS_BUCKET=
+ACTIVE_STORAGE_PUBLIC=true
+FORCE_SSL=true
+AIRBRAKE_ID=
+AIRBRAKE_KEY=
+NEWRELIC_LICENSE_KEY=
+NEWRELIC_APP_NAME=
+WEB_CONCURRENCY=2
\ No newline at end of file
diff --git a/.env.staging b/.env.staging
index 0a4eecef..cf0a1b06 100644
--- a/.env.staging
+++ b/.env.staging
@@ -8,3 +8,9 @@ PORT=3000
S3_ATTACHMENTS_BUCKET=
ACTIVE_STORAGE_PUBLIC=true
FORCE_SSL=true
+AIRBRAKE_ID=
+AIRBRAKE_KEY=
+NEWRELIC_LICENSE_KEY=
+NEWRELIC_APP_NAME=
+NEWRELIC_MONITOR_MODE=
+WEB_CONCURRENCY=2
diff --git a/Dockerfile b/Dockerfile
index b34389b4..30ae7ad6 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -79,6 +79,7 @@ COPY ./tmp ./tmp
COPY LICENSE README.md Rakefile config.ru .version ./
COPY .version ./public/version
COPY ./.env.staging ./.env.staging
+COPY ./.env.production ./.env.production
COPY ./config/rds-combined-ca-bundle.pem /config/rds-combined-ca-bundle.pem
COPY --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
diff --git a/Gemfile b/Gemfile
index 2373d2d3..b4352fa7 100644
--- a/Gemfile
+++ b/Gemfile
@@ -44,6 +44,9 @@ gem 'turbo-rails'
gem 'twitter_cldr', require: false
gem 'tzinfo-data'
+gem 'airbrake'
+gem 'newrelic_rpm'
+
group :development, :test do
gem 'better_html'
gem 'bullet'
diff --git a/Gemfile.lock b/Gemfile.lock
index d38ce518..52baffbd 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -74,6 +74,10 @@ GEM
uri (>= 0.13.1)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
+ airbrake (13.0.5)
+ airbrake-ruby (~> 6.0)
+ airbrake-ruby (6.2.2)
+ rbtree3 (~> 0.6)
annotaterb (4.14.0)
arabic-letter-connector (0.1.1)
ast (2.4.2)
@@ -336,6 +340,7 @@ GEM
timeout
net-smtp (0.5.0)
net-protocol
+ newrelic_rpm (9.17.0)
nio4r (2.7.4)
nokogiri (1.18.8)
mini_portile2 (~> 2.8.2)
@@ -433,6 +438,7 @@ GEM
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.2.1)
+ rbtree3 (0.7.1)
rdoc (6.10.0)
psych (>= 4.0.0)
redis-client (0.23.0)
@@ -588,6 +594,7 @@ PLATFORMS
x86_64-linux-musl
DEPENDENCIES
+ airbrake
annotaterb
arabic-letter-connector
aws-sdk-s3
@@ -618,6 +625,7 @@ DEPENDENCIES
jwt
letter_opener_web
lograge
+ newrelic_rpm
oj
pagy
pg
diff --git a/app/controllers/api/active_storage_blobs_proxy_controller.rb b/app/controllers/api/active_storage_blobs_proxy_controller.rb
index 6f505992..f5e2a95a 100644
--- a/app/controllers/api/active_storage_blobs_proxy_controller.rb
+++ b/app/controllers/api/active_storage_blobs_proxy_controller.rb
@@ -4,7 +4,7 @@ module Api
class ActiveStorageBlobsProxyController < ApiBaseController
include ActiveStorage::Streaming
- skip_before_action :authenticate_user!
+ skip_before_action :authenticate_via_token!
skip_authorization_check
before_action :set_cors_headers
diff --git a/app/controllers/api/active_storage_blobs_proxy_legacy_controller.rb b/app/controllers/api/active_storage_blobs_proxy_legacy_controller.rb
index 77ad2c6a..20675d41 100644
--- a/app/controllers/api/active_storage_blobs_proxy_legacy_controller.rb
+++ b/app/controllers/api/active_storage_blobs_proxy_legacy_controller.rb
@@ -4,7 +4,7 @@ module Api
class ActiveStorageBlobsProxyLegacyController < ApiBaseController
include ActiveStorage::Streaming
- skip_before_action :authenticate_user!
+ skip_before_action :authenticate_via_token!
skip_authorization_check
before_action :set_cors_headers
diff --git a/app/controllers/api/api_base_controller.rb b/app/controllers/api/api_base_controller.rb
index b681aa67..05e56eb3 100644
--- a/app/controllers/api/api_base_controller.rb
+++ b/app/controllers/api/api_base_controller.rb
@@ -12,7 +12,7 @@ module Api
wrap_parameters false
- before_action :authenticate_user!
+ before_action :authenticate_via_token!
check_authorization
rescue_from Params::BaseValidator::InvalidParameterError do |e|
@@ -81,7 +81,7 @@ module Api
result
end
- def authenticate_user!
+ def authenticate_via_token!
render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user
end
diff --git a/app/controllers/api/external_auth_controller.rb b/app/controllers/api/external_auth_controller.rb
new file mode 100644
index 00000000..497a4a8f
--- /dev/null
+++ b/app/controllers/api/external_auth_controller.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Api
+ class ExternalAuthController < Api::ApiBaseController
+ skip_before_action :authenticate_via_token!
+ skip_authorization_check
+
+ def user_token
+ account = Account.find_or_create_by_external_id(
+ params[:account][:external_id]&.to_i,
+ name: params[:account][:name],
+ locale: params[:account][:locale] || 'en-US',
+ timezone: params[:account][:timezone] || 'UTC'
+ )
+
+ user = User.find_or_create_by_external_id(
+ account,
+ params[:user][:external_id]&.to_i,
+ email: params[:user][:email],
+ first_name: params[:user][:first_name],
+ last_name: params[:user][:last_name],
+ role: 'admin'
+ )
+
+ render json: { access_token: user.access_token.token }
+ rescue StandardError => e
+ Rails.logger.error("External auth error: #{e.message}")
+ Rollbar.error(e) if defined?(Rollbar)
+ render json: { error: 'Internal server error' }, status: :internal_server_error
+ end
+ end
+end
diff --git a/app/controllers/api/submitter_email_clicks_controller.rb b/app/controllers/api/submitter_email_clicks_controller.rb
index cef26542..aaf97546 100644
--- a/app/controllers/api/submitter_email_clicks_controller.rb
+++ b/app/controllers/api/submitter_email_clicks_controller.rb
@@ -2,7 +2,7 @@
module Api
class SubmitterEmailClicksController < ApiBaseController
- skip_before_action :authenticate_user!
+ skip_before_action :authenticate_via_token!
skip_authorization_check
def create
diff --git a/app/controllers/api/submitter_form_views_controller.rb b/app/controllers/api/submitter_form_views_controller.rb
index e8b52095..d2931139 100644
--- a/app/controllers/api/submitter_form_views_controller.rb
+++ b/app/controllers/api/submitter_form_views_controller.rb
@@ -2,7 +2,7 @@
module Api
class SubmitterFormViewsController < ApiBaseController
- skip_before_action :authenticate_user!
+ skip_before_action :authenticate_via_token!
skip_authorization_check
def create
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 92bf197c..421313aa 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -6,14 +6,12 @@ class ApplicationController < ActionController::Base
include ActiveStorage::SetCurrent
include Pagy::Backend
- before_action :ensure_demo_user_signed_in
-
check_authorization unless: :devise_controller?
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,
@@ -102,34 +100,42 @@ class ApplicationController < ActionController::Base
current_user&.account
end
- def maybe_redirect_to_setup
- # Skip setup redirect for iframe embedding - create demo user instead
- return if ensure_demo_user_signed_in
+ def maybe_authenticate_via_token
+ return if signed_in?
- redirect_to setup_index_path unless User.exists?
- end
+ # Check for token in params, session, or X-Auth-Token header
+ token = params[:auth_token] || session[:auth_token] || request.headers['X-Auth-Token']
+ return if token.blank?
+
+ # Try to find user by token and sign them in
+ sha256 = Digest::SHA256.hexdigest(token)
+ user = User.joins(:access_token).active.find_by(access_token: { sha256: sha256 })
- def ensure_demo_user_signed_in
- return true if signed_in?
+ return unless user
- user = find_or_create_demo_user
sign_in(user)
- true
- end
-
- def find_or_create_demo_user
- User.find_by(email: 'demo@docuseal.local') || begin
- account = Account.create!(name: 'Demo Account', locale: 'en', timezone: 'UTC')
- User.create!(
- email: 'demo@docuseal.local',
- password: 'password123',
- password_confirmation: 'password123',
- first_name: 'Demo',
- last_name: 'User',
- account: account,
- role: 'admin'
- )
+ session[:auth_token] = token
+ end
+
+ # Enhanced authentication that tries token auth and fails with error if no user found
+ # Use this when you need to enforce authentication with better token handling
+ def authenticate_via_token!
+ return if signed_in?
+
+ token = params[:auth_token] || session[:auth_token] || request.headers['X-Auth-Token']
+
+ if token.present?
+ sha256 = Digest::SHA256.hexdigest(token)
+ user = User.joins(:access_token).active.find_by(access_token: { sha256: sha256 })
+
+ if user
+ sign_in(user)
+ session[:auth_token] = token
+ return
+ end
end
+
+ render json: { error: 'Authentication required. Please provide a valid auth_token.' }, status: :unauthorized
end
def button_title(title: I18n.t('submit'), disabled_with: I18n.t('submitting'), title_class: '', icon: nil,
diff --git a/app/controllers/concerns/iframe_authentication.rb b/app/controllers/concerns/iframe_authentication.rb
new file mode 100644
index 00000000..0683b361
--- /dev/null
+++ b/app/controllers/concerns/iframe_authentication.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module IframeAuthentication
+ extend ActiveSupport::Concern
+
+ private
+
+ # Custom authentication for iframe context
+ # AJAX requests from Vue components don't include the auth token that's in the iframe URL,
+ # so we extract it from the HTTP referer header as a fallback
+ def authenticate_from_referer
+ return if signed_in?
+
+ token = params[:auth_token] || session[:auth_token] || request.headers['X-Auth-Token']
+
+ # If no token found, extract from referer URL (iframe page has the token)
+ if token.blank? && request.referer.present?
+ referer_uri = URI.parse(request.referer)
+ referer_params = CGI.parse(referer_uri.query || '')
+ token = referer_params['auth_token']&.first
+ end
+
+ if token.present?
+ sha256 = Digest::SHA256.hexdigest(token)
+ user = User.joins(:access_token).active.find_by(access_token: { sha256: sha256 })
+
+ return unless user
+
+ sign_in(user)
+ session[:auth_token] = token
+ return
+ end
+
+ Rails.logger.error "#{self.class.name}: Authentication failed"
+ render json: { error: 'Authentication required' }, status: :unauthorized
+ end
+end
diff --git a/app/controllers/console_redirect_controller.rb b/app/controllers/console_redirect_controller.rb
index dd80e9fe..ca815f60 100644
--- a/app/controllers/console_redirect_controller.rb
+++ b/app/controllers/console_redirect_controller.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class ConsoleRedirectController < ApplicationController
- skip_before_action :authenticate_user!
+ skip_before_action :authenticate_via_token!
skip_authorization_check
def index
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 23df8322..acf25d1d 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class DashboardController < ApplicationController
- skip_before_action :authenticate_user!, only: %i[index]
+ skip_before_action :authenticate_via_token!, only: %i[index]
before_action :maybe_redirect_product_url
before_action :maybe_render_landing
diff --git a/app/controllers/enquiries_controller.rb b/app/controllers/enquiries_controller.rb
index 829b578c..98c09f92 100644
--- a/app/controllers/enquiries_controller.rb
+++ b/app/controllers/enquiries_controller.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class EnquiriesController < ApplicationController
- skip_before_action :authenticate_user!
+ skip_before_action :authenticate_via_token!
skip_authorization_check
def create
diff --git a/app/controllers/export_controller.rb b/app/controllers/export_controller.rb
index 3c5403b9..3ec5992a 100644
--- a/app/controllers/export_controller.rb
+++ b/app/controllers/export_controller.rb
@@ -4,7 +4,6 @@ require 'faraday'
class ExportController < ApplicationController
skip_authorization_check
- skip_before_action :maybe_redirect_to_setup
skip_before_action :verify_authenticity_token
# Send template to third party.
diff --git a/app/controllers/send_submission_email_controller.rb b/app/controllers/send_submission_email_controller.rb
index 45852360..d916440f 100644
--- a/app/controllers/send_submission_email_controller.rb
+++ b/app/controllers/send_submission_email_controller.rb
@@ -3,7 +3,7 @@
class SendSubmissionEmailController < ApplicationController
layout 'form'
- skip_before_action :authenticate_user!
+ skip_before_action :authenticate_via_token!
skip_before_action :verify_authenticity_token
skip_authorization_check
diff --git a/app/controllers/setup_controller.rb b/app/controllers/setup_controller.rb
index 778255bc..ba2911f8 100644
--- a/app/controllers/setup_controller.rb
+++ b/app/controllers/setup_controller.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
class SetupController < ApplicationController
- skip_before_action :maybe_redirect_to_setup
- skip_before_action :authenticate_user!
+ skip_before_action :authenticate_via_token!
skip_authorization_check
before_action :redirect_to_root_if_signed, if: :signed_in?
diff --git a/app/controllers/start_form_controller.rb b/app/controllers/start_form_controller.rb
index fb2a9786..fcc87e34 100644
--- a/app/controllers/start_form_controller.rb
+++ b/app/controllers/start_form_controller.rb
@@ -3,7 +3,7 @@
class StartFormController < ApplicationController
layout 'form'
- skip_before_action :authenticate_user!
+ skip_before_action :authenticate_via_token!
skip_authorization_check
around_action :with_browser_locale, only: %i[show completed]
diff --git a/app/controllers/submissions_debug_controller.rb b/app/controllers/submissions_debug_controller.rb
index 4a6e8b9d..c5482020 100644
--- a/app/controllers/submissions_debug_controller.rb
+++ b/app/controllers/submissions_debug_controller.rb
@@ -3,7 +3,7 @@
class SubmissionsDebugController < ApplicationController
layout 'plain'
- skip_before_action :authenticate_user!
+ skip_before_action :authenticate_via_token!
skip_authorization_check
def index
diff --git a/app/controllers/submissions_download_controller.rb b/app/controllers/submissions_download_controller.rb
index 62836650..f0f3778c 100644
--- a/app/controllers/submissions_download_controller.rb
+++ b/app/controllers/submissions_download_controller.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class SubmissionsDownloadController < ApplicationController
- skip_before_action :authenticate_user!
+ skip_before_action :authenticate_via_token!
skip_authorization_check
TTL = 40.minutes
diff --git a/app/controllers/submissions_preview_controller.rb b/app/controllers/submissions_preview_controller.rb
index d8a36f0b..c56e8db9 100644
--- a/app/controllers/submissions_preview_controller.rb
+++ b/app/controllers/submissions_preview_controller.rb
@@ -2,7 +2,7 @@
class SubmissionsPreviewController < ApplicationController
around_action :with_browser_locale
- skip_before_action :authenticate_user!
+ skip_before_action :authenticate_via_token!
skip_authorization_check
prepend_before_action :maybe_redirect_com, only: %i[show completed]
diff --git a/app/controllers/submit_form_controller.rb b/app/controllers/submit_form_controller.rb
index 268abc40..4cd0b7d3 100644
--- a/app/controllers/submit_form_controller.rb
+++ b/app/controllers/submit_form_controller.rb
@@ -4,7 +4,7 @@ class SubmitFormController < ApplicationController
layout 'form'
around_action :with_browser_locale, only: %i[show completed success]
- skip_before_action :authenticate_user!
+ skip_before_action :authenticate_via_token!
skip_authorization_check
skip_before_action :verify_authenticity_token, only: :update
diff --git a/app/controllers/submit_form_decline_controller.rb b/app/controllers/submit_form_decline_controller.rb
index 15572da1..99ea9870 100644
--- a/app/controllers/submit_form_decline_controller.rb
+++ b/app/controllers/submit_form_decline_controller.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class SubmitFormDeclineController < ApplicationController
- skip_before_action :authenticate_user!
+ skip_before_action :authenticate_via_token!
skip_authorization_check
def create
diff --git a/app/controllers/submit_form_download_controller.rb b/app/controllers/submit_form_download_controller.rb
index d357019c..67815e5e 100644
--- a/app/controllers/submit_form_download_controller.rb
+++ b/app/controllers/submit_form_download_controller.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class SubmitFormDownloadController < ApplicationController
- skip_before_action :authenticate_user!
+ skip_before_action :authenticate_via_token!
skip_authorization_check
FILES_TTL = 5.minutes
diff --git a/app/controllers/submit_form_draw_signature_controller.rb b/app/controllers/submit_form_draw_signature_controller.rb
index f8352ade..7c327b71 100644
--- a/app/controllers/submit_form_draw_signature_controller.rb
+++ b/app/controllers/submit_form_draw_signature_controller.rb
@@ -4,7 +4,7 @@ class SubmitFormDrawSignatureController < ApplicationController
layout false
around_action :with_browser_locale, only: %i[show]
- skip_before_action :authenticate_user!
+ skip_before_action :authenticate_via_token!
skip_authorization_check
def show
diff --git a/app/controllers/submit_form_invite_controller.rb b/app/controllers/submit_form_invite_controller.rb
index 1d42779c..1d60e717 100644
--- a/app/controllers/submit_form_invite_controller.rb
+++ b/app/controllers/submit_form_invite_controller.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class SubmitFormInviteController < ApplicationController
- skip_before_action :authenticate_user!
+ skip_before_action :authenticate_via_token!
skip_authorization_check
def create
diff --git a/app/controllers/submit_form_values_controller.rb b/app/controllers/submit_form_values_controller.rb
index 2c4a2ad3..7c3f651d 100644
--- a/app/controllers/submit_form_values_controller.rb
+++ b/app/controllers/submit_form_values_controller.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class SubmitFormValuesController < ApplicationController
- skip_before_action :authenticate_user!
+ skip_before_action :authenticate_via_token!
skip_authorization_check
def index
diff --git a/app/controllers/submitters_request_changes_controller.rb b/app/controllers/submitters_request_changes_controller.rb
index b18d8ed0..dff9f862 100644
--- a/app/controllers/submitters_request_changes_controller.rb
+++ b/app/controllers/submitters_request_changes_controller.rb
@@ -1,8 +1,12 @@
# frozen_string_literal: true
class SubmittersRequestChangesController < ApplicationController
- before_action :load_submitter
+ include IframeAuthentication
+
skip_before_action :verify_authenticity_token, only: :request_changes
+ skip_before_action :authenticate_via_token!, only: :request_changes
+ before_action :authenticate_from_referer, only: :request_changes
+ before_action :load_submitter
def request_changes
if request.get? || request.head?
@@ -48,9 +52,9 @@ class SubmittersRequestChangesController < ApplicationController
end
def can_request_changes?
- # Only the user who created the submission can request changes
+ # Only the template author (manager) can request changes from submitters
# Only for completed submissions that haven't been declined
- current_user == @submitter.submission.created_by_user &&
+ current_user == @submitter.submission.template.author &&
@submitter.completed_at? &&
!@submitter.declined_at? &&
!@submitter.changes_requested_at?
diff --git a/app/controllers/template_documents_controller.rb b/app/controllers/template_documents_controller.rb
index 7144f5e0..7aaa039c 100644
--- a/app/controllers/template_documents_controller.rb
+++ b/app/controllers/template_documents_controller.rb
@@ -1,8 +1,13 @@
# frozen_string_literal: true
class TemplateDocumentsController < ApplicationController
+ include IframeAuthentication
+
skip_before_action :verify_authenticity_token
- load_and_authorize_resource :template
+ skip_before_action :authenticate_via_token!
+
+ before_action :authenticate_from_referer
+ load_and_authorize_resource :template, id_param: :template_id
def create
if params[:blobs].blank? && params[:files].blank?
diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb
index 12258115..2e2e5919 100644
--- a/app/controllers/templates_controller.rb
+++ b/app/controllers/templates_controller.rb
@@ -2,12 +2,13 @@
class TemplatesController < ApplicationController
include PrefillFieldsHelper
+ include IframeAuthentication
- skip_before_action :maybe_redirect_to_setup
skip_before_action :verify_authenticity_token
+ skip_before_action :authenticate_via_token!, only: [:update]
+ before_action :authenticate_from_referer, only: [:update]
load_and_authorize_resource :template
-
before_action :load_base_template, only: %i[new create]
def show
@@ -67,6 +68,7 @@ class TemplatesController < ApplicationController
name: params.dig(:template, :name),
folder_name: params[:folder_name])
else
+ @template = Template.new(template_params) if @template.nil?
@template.author = current_user
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
end
diff --git a/app/controllers/templates_dashboard_controller.rb b/app/controllers/templates_dashboard_controller.rb
index 6cb553c1..8690bed6 100644
--- a/app/controllers/templates_dashboard_controller.rb
+++ b/app/controllers/templates_dashboard_controller.rb
@@ -1,10 +1,6 @@
# frozen_string_literal: true
class TemplatesDashboardController < ApplicationController
- before_action :ensure_demo_user_signed_in
- skip_before_action :authenticate_user!
- skip_before_action :maybe_redirect_to_setup
-
load_and_authorize_resource :template_folder, parent: false
load_and_authorize_resource :template, parent: false
diff --git a/app/models/account.rb b/app/models/account.rb
index 6265734d..4a1b7701 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -4,18 +4,20 @@
#
# Table name: accounts
#
-# id :bigint not null, primary key
-# archived_at :datetime
-# locale :string not null
-# name :string not null
-# timezone :string not null
-# uuid :string not null
-# created_at :datetime not null
-# updated_at :datetime not null
+# id :bigint not null, primary key
+# archived_at :datetime
+# locale :string not null
+# name :string not null
+# timezone :string not null
+# uuid :string not null
+# created_at :datetime not null
+# updated_at :datetime not null
+# external_account_id :integer
#
# Indexes
#
-# index_accounts_on_uuid (uuid) UNIQUE
+# index_accounts_on_external_account_id (external_account_id) UNIQUE
+# index_accounts_on_uuid (uuid) UNIQUE
#
class Account < ApplicationRecord
attribute :uuid, :string, default: -> { SecureRandom.uuid }
@@ -53,8 +55,15 @@ class Account < ApplicationRecord
attribute :timezone, :string, default: 'UTC'
attribute :locale, :string, default: 'en-US'
+ validates :external_account_id, uniqueness: true, allow_nil: true
+
scope :active, -> { where(archived_at: nil) }
+ def self.find_or_create_by_external_id(external_id, attributes = {})
+ find_by(external_account_id: external_id) ||
+ create!(attributes.merge(external_account_id: external_id))
+ end
+
def testing?
linked_account_account&.testing?
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 98e500ee..542924ce 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -29,11 +29,13 @@
# created_at :datetime not null
# updated_at :datetime not null
# account_id :integer not null
+# external_user_id :integer
#
# Indexes
#
# index_users_on_account_id (account_id)
# index_users_on_email (email) UNIQUE
+# index_users_on_external_user_id (external_user_id) UNIQUE
# index_users_on_reset_password_token (reset_password_token) UNIQUE
# index_users_on_unlock_token (unlock_token) UNIQUE
# index_users_on_uuid (uuid) UNIQUE
@@ -74,6 +76,17 @@ class User < ApplicationRecord
scope :admins, -> { where(role: ADMIN_ROLE) }
validates :email, format: { with: /\A[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\z/ }
+ validates :external_user_id, uniqueness: true, allow_nil: true
+
+ def self.find_or_create_by_external_id(account, external_id, attributes = {})
+ account.users.find_by(external_user_id: external_id) ||
+ account.users.create!(
+ attributes.merge(
+ external_user_id: external_id,
+ password: SecureRandom.hex(16)
+ )
+ )
+ end
def access_token
super || build_access_token.tap(&:save!)
diff --git a/app/views/submissions/show.html.erb b/app/views/submissions/show.html.erb
index 7e878842..122521b6 100644
--- a/app/views/submissions/show.html.erb
+++ b/app/views/submissions/show.html.erb
@@ -235,7 +235,7 @@
<%= button_to t('resubmit'), submitters_resubmit_path(submitter), method: :put, class: 'btn btn-sm btn-primary w-full', form: { target: '_blank' }, data: { turbo: false } %>
<% end %>
- <% if signed_in? && submitter && submitter.completed_at? && !submitter.declined_at? && !submitter.changes_requested_at? && current_user == @submission.created_by_user %>
+ <% if signed_in? && submitter && submitter.completed_at? && !submitter.declined_at? && !submitter.changes_requested_at? && current_user == @submission.template.author %>
<%= link_to 'Request Changes', request_changes_submitter_path(submitter.slug),
class: 'btn btn-sm btn-warning w-full',
diff --git a/bin/start_production b/bin/start_production
new file mode 100755
index 00000000..6b9be8e8
--- /dev/null
+++ b/bin/start_production
@@ -0,0 +1,271 @@
+#!/bin/sh -e
+
+echo "=== CP Docuseal Production 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 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"
+}
+
+# Function to fetch allowed hosts values
+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
+
+ # Write allowed hosts to .env.production file
+ echo "Writing allowed hosts to .env.production..."
+ echo "ALLOWED_HOSTS=$ALLOWED_HOSTS" >> ./.env.production
+ echo "✓ Allowed hosts successfully retrieved and written to .env.production"
+}
+
+# Function to fetch various environment variables and write to .env file for use by app
+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
+
+ # Validate license keys exist for logging
+ if [ "$AIRBRAKE_ID" = "null" ] || [ "$AIRBRAKE_KEY" = "null" ] || [ "$NEWRELIC_LICENSE_KEY" = "null" ] || [ "$NEWRELIC_APP_NAME" = "null" ]; then
+ echo "ERROR: One or more monitor/logging license keys are missing"
+ 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 Server ==="
+ echo "Database Host: ${DB_HOST:-not set}"
+ echo "Database Port: ${DB_PORT:-not set}"
+
+ # Start the Rails server
+ exec ./bin/rails server -b 0.0.0.0 -p "${PORT:-3000}"
+}
+
+# Execute main function
+main "$@"
diff --git a/bin/start_staging b/bin/start_staging
index e9d9df5c..4647a620 100755
--- a/bin/start_staging
+++ b/bin/start_staging
@@ -71,6 +71,46 @@ fetch_db_credentials() {
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..."
@@ -123,6 +163,12 @@ fetch_env_variables() {
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
@@ -130,6 +176,12 @@ fetch_env_variables() {
echo "Expected JSON format: {\"key\": \"...\", ...}"
exit 1
fi
+
+ # Validate license keys exist for logging
+ if [ "$AIRBRAKE_ID" = "null" ] || [ "$AIRBRAKE_KEY" = "null" ] || [ "$NEWRELIC_LICENSE_KEY" = "null" ] || [ "$NEWRELIC_APP_NAME" = "null" ]; then
+ echo "ERROR: One or more monitor/logging license keys are missing"
+ exit 1
+ fi
# Write variables to .env.staging file
echo "Writing environment variables to .env.staging..."
@@ -140,6 +192,11 @@ fetch_env_variables() {
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
@@ -147,6 +204,11 @@ fetch_env_variables() {
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"
}
@@ -190,6 +252,9 @@ main() {
# 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
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 36b0bfaa..7e422baa 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -40,10 +40,6 @@ Rails.application.configure do
config.active_storage.service =
if ENV['S3_ATTACHMENTS_BUCKET'].present?
:aws_s3
- elsif ENV['GCS_BUCKET'].present?
- :google
- elsif ENV['AZURE_CONTAINER'].present?
- :azure
else
:disk
end
@@ -57,10 +53,10 @@ Rails.application.configure do
# config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ]
# Assume all access to the app is happening through a SSL-terminating reverse proxy.
- config.assume_ssl = ENV['FORCE_SSL'].present? && ENV['FORCE_SSL'] != 'false'
+ config.assume_ssl = true
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
- config.force_ssl = ENV['FORCE_SSL'].present? && ENV['FORCE_SSL'] != 'false'
+ config.force_ssl = true
# Include generic and useful information about system operation, but avoid logging too much
# information to avoid inadvertent exposure of personally identifiable information (PII).
@@ -162,4 +158,12 @@ Rails.application.configure do
raid: resource.try(:account_id)
}
end
+
+ config.host_authorization = { exclude: ->(request) { request.path == '/up' } }
+
+ # Load allowed hosts from environment variable
+ allowed_hosts = ENV['ALLOWED_HOSTS']&.split(',')&.map(&:strip) || ['.*\\.careerplug\\.com\\Z']
+
+ config.host_authorization = { exclude: ->(request) { request.path == '/up' } }
+ allowed_hosts.each { |host_pattern| config.hosts << Regexp.new(host_pattern) }
end
diff --git a/config/environments/staging.rb b/config/environments/staging.rb
index 180b6005..286a868a 100644
--- a/config/environments/staging.rb
+++ b/config/environments/staging.rb
@@ -95,9 +95,28 @@ Rails.application.configure do
# 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)
+
+ # 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
encryption_secret = ENV['ENCRYPTION_SECRET'].presence || Digest::SHA256.hexdigest(ENV['SECRET_KEY_BASE'].to_s)
@@ -154,11 +173,9 @@ Rails.application.configure do
}
end
+ # Load allowed hosts from environment variable
+ allowed_hosts = ENV['ALLOWED_HOSTS']&.split(',')&.map(&:strip) || ['.*\\.careerplug\\.com\\Z']
+
config.host_authorization = { exclude: ->(request) { request.path == '/up' } }
- [
- /.*\.careerplug\.org\Z/,
- /.*\.careerplug\.com\Z/,
- /.*\.cpstaging\d\.click\Z/,
- /.*\.cpstaging\d+\.name\Z/
- ].each { |hrexp| config.hosts << hrexp }
+ allowed_hosts.each { |host_pattern| config.hosts << Regexp.new(host_pattern) }
end
diff --git a/config/initializers/airbrake.rb b/config/initializers/airbrake.rb
new file mode 100644
index 00000000..ff7ef2af
--- /dev/null
+++ b/config/initializers/airbrake.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+unless ENV['DOCKER_BUILD'] || ENV['CI_BUILD']
+ Airbrake.configure do |config|
+ config.project_key = ENV['AIRBRAKE_KEY'] # rubocop:disable Style/FetchEnvVar
+ config.project_id = ENV['AIRBRAKE_ID'] # rubocop:disable Style/FetchEnvVar
+ config.environment = Rails.env
+ config.ignore_environments = %w[development test]
+ config.root_directory = '/var/cpd/app'
+ end
+end
diff --git a/config/newrelic.yml b/config/newrelic.yml
new file mode 100644
index 00000000..05b6041c
--- /dev/null
+++ b/config/newrelic.yml
@@ -0,0 +1,1117 @@
+#
+# This file configures the New Relic agent. New Relic monitors Ruby, Java,
+# .NET, PHP, Python, Node, and Go applications with deep visibility and low
+# overhead. For more information, visit www.newrelic.com.
+
+common: &default_settings
+ # Required license key associated with your New Relic account.
+ license_key: <%= ENV['NEWRELIC_LICENSE_KEY'] %>
+
+ # Your application name. Renaming here affects where data displays in New
+ # Relic. For more details, see https://docs.newrelic.com/docs/apm/new-relic-apm/maintenance/renaming-applications
+ app_name: <%= ENV['NEWRELIC_APP_NAME'] %>
+
+ # To disable the agent regardless of other settings, uncomment the following:
+ # agent_enabled: false
+
+ # Logging level for log/newrelic_agent.log; options are error, warn, info, or
+ # debug.
+ log_level: warn
+
+ # All of the following configuration options are optional. Review them, and
+ # uncomment or edit them if they appear relevant to your application needs.
+
+ # An array of ActiveSupport custom event names to subscribe to and instrument.
+ # For example,
+ # - one.custom.event
+ # - another.event
+ # - a.third.event
+ # active_support_custom_events_names: []
+
+ # If false, all LLM instrumentation (OpenAI only for now) will be disabled and
+ # no metrics, events, or spans will be sent. AI Monitoring is automatically
+ # disabled if high_security mode is enabled.
+ # ai_monitoring.enabled: false
+
+ # If false, LLM instrumentation (OpenAI only for now) will not capture input and
+ # output content on specific LLM events.
+ #
+ # The excluded attributes include:
+ # - content from LlmChatCompletionMessage events
+ # - input from LlmEmbedding events
+ #
+ # This is an optional security setting to prevent recording sensitive data sent
+ # to and received from your LLMs.
+ # ai_monitoring.record_content.enabled: true
+
+ # If true, enables capture of all HTTP request headers for all destinations.
+ # allow_all_headers: false
+
+ # Your New Relic userKey. Required when using the New Relic REST API v2 to
+ # record deployments using the newrelic deployments command.
+ # api_key: ""
+
+ # If true, enables log decoration and the collection of log events and metrics.
+ application_logging.enabled: true
+
+ # A hash with key/value pairs to add as custom attributes to all log events
+ # forwarded to New Relic. If sending using an environment variable, the value
+ # must be formatted like: "key1=value1,key2=value2"
+ # NOTE: We can add custom attributes HERE.
+ # application_logging.forwarding.custom_attributes: {
+ # user_id: current_user&.id
+ # }
+
+ # If true, the agent captures log records emitted by your application.
+ application_logging.forwarding.enabled: true
+
+ # If true, the agent attaches labels to log records.
+ # application_logging.forwarding.labels.enabled: false
+
+ # A case-insensitive array or comma-delimited string containing the labels to
+ # exclude from log records.
+ # application_logging.forwarding.labels.exclude: []
+
+ # Sets the minimum level a log event must have to be forwarded to New Relic.
+ #
+ # This is based on the integer values of Ruby's Logger::Severity constants.
+ #
+ # The intention is to forward logs with the level given to the configuration, as
+ # well as any logs with a higher level of severity.
+ #
+ # For example, setting this value to "debug" will forward all log events to New
+ # Relic. Setting this value to "error" will only forward log events with the
+ # levels "error", "fatal", and "unknown".
+ #
+ # Valid values (ordered lowest to highest):
+ # - "debug"
+ # - "info"
+ # - "warn"
+ # - "error"
+ # - "fatal"
+ # - "unknown"
+ application_logging.forwarding.log_level: warn
+
+ # Defines the maximum number of log records to buffer in memory at a time.
+ # application_logging.forwarding.max_samples_stored: 10000
+
+ # If true, the agent decorates logs with metadata to link to entities, hosts,
+ # traces, and spans.
+ # application_logging.local_decorating.enabled: false
+
+ # If true, the agent captures metrics related to logging for your application.
+ # application_logging.metrics.enabled: true
+
+ # If true, enables capture of attributes for all destinations.
+ # attributes.enabled: true
+
+ # Prefix of attributes to exclude from all destinations. Allows * as wildcard at
+ # end.
+ # attributes.exclude: []
+
+ # Prefix of attributes to include in all destinations. Allows * as wildcard at
+ # end.
+ # attributes.include: []
+
+ # If true, enables an audit log which logs communications with the New Relic
+ # collector.
+ # audit_log.enabled: false
+
+ # List of allowed endpoints to include in audit log.
+ # audit_log.endpoints: [".*"]
+
+ # Specifies a path to the audit log file (including the filename).
+ # audit_log.path: log/newrelic_audit.log
+
+ # An array of CLASS#METHOD (for instance methods) and/or CLASS.METHOD (for class
+ # methods) strings representing Ruby methods that the agent can automatically
+ # add custom instrumentation to. This doesn't require any modifications of the
+ # source code that defines the methods.
+ #
+ # Use fully qualified class names (using the :: delimiter) that include any
+ # module or class namespacing.
+ #
+ # Here is some Ruby source code that defines a render_png instance method for an
+ # Image class and a notify class method for a User class, both within a
+ # MyCompany module namespace:
+ #
+ # module MyCompany
+ # class Image
+ # def render_png
+ # # code to render a PNG
+ # end
+ # end
+ #
+ # class User
+ # def self.notify
+ # # code to notify users
+ # end
+ # end
+ # end
+ #
+ #
+ # Given that source code, the newrelic.yml config file might request
+ # instrumentation for both of these methods like so:
+ #
+ # automatic_custom_instrumentation_method_list:
+ # - MyCompany::Image#render_png
+ # - MyCompany::User.notify
+ #
+ #
+ # That configuration example uses YAML array syntax to specify both methods.
+ # Alternatively, you can use a comma-delimited string:
+ #
+ # automatic_custom_instrumentation_method_list: 'MyCompany::Image#render_png,
+ # MyCompany::User.notify'
+ #
+ #
+ # Whitespace around the comma(s) in the list is optional. When configuring the
+ # agent with a list of methods via the
+ # NEW_RELIC_AUTOMATIC_CUSTOM_INSTRUMENTATION_METHOD_LIST environment variable,
+ # use this comma-delimited string format:
+ #
+ # export
+ # NEW_RELIC_AUTOMATIC_CUSTOM_INSTRUMENTATION_METHOD_LIST='MyCompany::Image#render_png,
+ # MyCompany::User.notify'
+ # automatic_custom_instrumentation_method_list: []
+
+ # Specify a list of constants that should prevent the agent from starting
+ # automatically. Separate individual constants with a comma ,. For example,
+ # "Rails::Console,UninstrumentedBackgroundJob".
+ # autostart.denylisted_constants: Rails::Command::ConsoleCommand,Rails::Command::CredentialsCommand,Rails::Command::Db::System::ChangeCommand,Rails::Command::DbConsoleCommand,Rails::Command::DestroyCommand,Rails::Command::DevCommand,Rails::Command::EncryptedCommand,Rails::Command::GenerateCommand,Rails::Command::InitializersCommand,Rails::Command::NotesCommand,Rails::Command::RoutesCommand,Rails::Command::RunnerCommand,Rails::Command::SecretsCommand,Rails::Console,Rails::DBConsole
+
+ # Defines a comma-delimited list of executables that the agent should not
+ # instrument. For example, "rake,my_ruby_script.rb".
+ # autostart.denylisted_executables: irb,rspec
+
+ # Defines a comma-delimited list of Rake tasks that the agent should not
+ # instrument. For example, "assets:precompile,db:migrate".
+ # autostart.denylisted_rake_tasks: about,assets:clean,assets:clobber,assets:environment,assets:precompile,assets:precompile:all,db:create,db:drop,db:fixtures:load,db:migrate,db:migrate:status,db:rollback,db:schema:cache:clear,db:schema:cache:dump,db:schema:dump,db:schema:load,db:seed,db:setup,db:structure:dump,db:version,doc:app,log:clear,middleware,notes,notes:custom,rails:template,rails:update,routes,secret,spec,spec:features,spec:requests,spec:controllers,spec:helpers,spec:models,spec:views,spec:routing,spec:rcov,stats,test,test:all,test:all:db,test:recent,test:single,test:uncommitted,time:zones:all,tmp:clear,tmp:create,webpacker:compile
+
+ # Backports the faster ActiveRecord connection lookup introduced in Rails 6,
+ # which improves agent performance when instrumenting ActiveRecord. Note that
+ # this setting may not be compatible with other gems that patch ActiveRecord.
+ # backport_fast_active_record_connection_lookup: false
+
+ # If true, the agent captures attributes from browser monitoring.
+ # browser_monitoring.attributes.enabled: false
+
+ # Prefix of attributes to exclude from browser monitoring. Allows * as wildcard
+ # at end.
+ # browser_monitoring.attributes.exclude: []
+
+ # Prefix of attributes to include in browser monitoring. Allows * as wildcard at
+ # end.
+ # browser_monitoring.attributes.include: []
+
+ # If true, enables auto-injection of the JavaScript header for page load timing
+ # (sometimes referred to as real user monitoring or RUM).
+ # browser_monitoring.auto_instrument: true
+
+ # If true, enables auto-injection of Content Security Policy Nonce in browser
+ # monitoring scripts. For now, auto-injection only works with Rails 5.2+.
+ # browser_monitoring.content_security_policy_nonce: true
+
+ # Manual override for the path to your local CA bundle. This CA bundle validates
+ # the SSL certificate presented by New Relic's data collection service.
+ # ca_bundle_path: nil
+
+ # Enable or disable the capture of memcache keys from transaction traces.
+ # capture_memcache_keys: false
+
+ # When true, the agent captures HTTP request parameters and attaches them to
+ # transaction traces, traced errors, and TransactionError events.
+ #
+ # When using the capture_params setting, the Ruby agent will not attempt to
+ # filter secret information. Recommendation: To filter secret information from
+ # request parameters, use the attributes.include setting instead. For more
+ # information, see the Ruby attribute examples.
+ # capture_params: false
+
+ # If true, the agent will clear Tracer::State in Agent.drop_buffered_data.
+ # clear_transaction_state_after_fork: false
+
+ # The AWS account ID for the AWS account associated with this app
+ # cloud.aws.account_id: nil
+
+ # If true, the agent will report source code level metrics for traced methods.
+ # See:
+ # https://docs.newrelic.com/docs/apm/agents/ruby-agent/features/ruby-codestream-integration/
+ # code_level_metrics.enabled: true
+
+ # Path to newrelic.yml. If undefined, the agent checks the following directories
+ # (in order):
+ # - config/newrelic.yml
+ # - newrelic.yml
+ # - $HOME/.newrelic/newrelic.yml
+ # - $HOME/newrelic.yml
+ # config_path: newrelic.yml
+
+ # If false, custom attributes will not be sent on events.
+ # custom_attributes.enabled: true
+
+ # If true, the agent captures custom events.
+ # custom_insights_events.enabled: true
+
+ # - Specify a maximum number of custom events to buffer in memory at a time.
+ # - When configuring the agent for AI monitoring, set to max value 100000.
+ # This ensures the agent captures the maximum amount of LLM events.
+ # custom_insights_events.max_samples_stored: 3000
+
+ # If false, the agent will not add database_name parameter to transaction or
+ # slow sql traces.
+ # datastore_tracer.database_name_reporting.enabled: true
+
+ # If false, the agent will not report datastore instance metrics, nor add host
+ # or port_path_or_id parameters to transaction or slow SQL traces.
+ # datastore_tracer.instance_reporting.enabled: true
+
+ # If true, disables Action Cable instrumentation.
+ # disable_action_cable_instrumentation: false
+
+ # If true, disables Action Controller instrumentation.
+ # disable_action_controller: false
+
+ # If true, disables Action Mailbox instrumentation.
+ # disable_action_mailbox: false
+
+ # If true, disables Action Mailer instrumentation.
+ # disable_action_mailer: false
+
+ # If true, disables Active Record instrumentation.
+ # disable_active_record_instrumentation: false
+
+ # If true, disables instrumentation for Active Record 4+
+ # disable_active_record_notifications: false
+
+ # If true, disables Active Storage instrumentation.
+ # disable_active_storage: false
+
+ # If true, disables Active Support instrumentation.
+ # disable_active_support: false
+
+ # If true, disables Active Job instrumentation.
+ # disable_activejob: false
+
+ # If true, the agent won't sample the CPU usage of the host process.
+ # disable_cpu_sampler: false
+
+ # If true, the agent won't measure the depth of Delayed Job queues.
+ # disable_delayed_job_sampler: false
+
+ # If true, disables the use of GC::Profiler to measure time spent in garbage
+ # collection
+ # disable_gc_profiler: false
+
+ # If true, the agent won't sample the memory usage of the host process.
+ # disable_memory_sampler: false
+
+ # If true, the agent won't wrap third-party middlewares in instrumentation
+ # (regardless of whether they are installed via Rack::Builder or Rails).
+ #
+ # When middleware instrumentation is disabled, if an application is using
+ # middleware that could alter the response code, the HTTP status code reported
+ # on the transaction may not reflect the altered value.
+ # disable_middleware_instrumentation: false
+
+ # If true, disables agent middleware for Roda. This middleware is responsible
+ # for advanced feature support such as page load timing and error collection.
+ # disable_roda_auto_middleware: false
+
+ # If true, disables the collection of sampler metrics. Sampler metrics are
+ # metrics that are not event-based (such as CPU time or memory usage).
+ # disable_samplers: false
+
+ # If true, disables Sequel instrumentation.
+ # disable_sequel_instrumentation: false
+
+ # If true, disables Sidekiq instrumentation.
+ # disable_sidekiq: false
+
+ # If true, disables agent middleware for Sinatra. This middleware is responsible
+ # for advanced feature support such as cross application tracing, page load
+ # timing, and error collection.
+ #
+ # Cross application tracing is deprecated in favor of distributed tracing.
+ # Distributed tracing is on by default for Ruby agent versions 8.0.0 and above.
+ # Middlewares are not required to support distributed tracing.
+ #
+ # To continue using cross application tracing, update the following options in
+ # your newrelic.yml configuration file:
+ #
+ # cross_application_tracer:
+ # enabled: true
+ # distributed_tracing:
+ # enabled: false
+ # disable_sinatra_auto_middleware: false
+
+ # If true, disables view instrumentation.
+ # disable_view_instrumentation: false
+
+ # If true, the agent won't sample performance measurements from the Ruby VM.
+ # disable_vm_sampler: false
+
+ # Distributed tracing lets you see the path that a request takes through your
+ # distributed system. Enabling distributed tracing changes the behavior of some
+ # New Relic features, so carefully consult the transition guide before you
+ # enable this feature.
+ # distributed_tracing.enabled: true
+
+ # This setting controls the behavior of transaction sampling when a remote
+ # parent is not sampled and the trace flag is not set in the traceparent.
+ # Available values are default, always_on, and always_off.
+ # distributed_tracing.sampler.remote_parent_not_sampled: default
+
+ # This setting controls the behavior of transaction sampling when a remote
+ # parent is sampled and the trace flag is set in the traceparent. Available
+ # values are default, always_on, and always_off.
+ # distributed_tracing.sampler.remote_parent_sampled: default
+
+ # If true, the agent captures the Elasticsearch cluster name in transaction
+ # traces.
+ # elasticsearch.capture_cluster_name: true
+
+ # If true, the agent captures Elasticsearch queries in transaction traces.
+ # elasticsearch.capture_queries: true
+
+ # If true, the agent obfuscates Elasticsearch queries in transaction traces.
+ # elasticsearch.obfuscate_queries: true
+
+ # If true, the agent captures attributes from error collection.
+ # error_collector.attributes.enabled: true
+
+ # Prefix of attributes to exclude from error collection. Allows * as wildcard at
+ # end.
+ # error_collector.attributes.exclude: []
+
+ # Prefix of attributes to include in error collection. Allows * as wildcard at
+ # end.
+ # error_collector.attributes.include: []
+
+ # If true, the agent collects TransactionError events.
+ error_collector.capture_events: true
+
+ # If true, the agent captures traced errors and error count metrics.
+ error_collector.enabled: true
+
+ # A list of error classes that the agent should treat as expected.
+ # This option can't be set via environment variable.
+ # error_collector.expected_classes: []
+
+ # A map of error classes to a list of messages. When an error of one of the
+ # classes specified here occurs, if its error message contains one of the
+ # strings corresponding to it here, that error will be treated as expected.
+ # This option can't be set via environment variable.
+ # error_collector.expected_messages: {}
+
+ # A comma separated list of status codes, possibly including ranges. Errors
+ # associated with these status codes, where applicable, will be treated as
+ # expected.
+ # error_collector.expected_status_codes: ""
+
+ # A list of error classes that the agent should ignore.
+ # This option can't be set via environment variable.
+ # error_collector.ignore_classes: ["ActionController::RoutingError", "Sinatra::NotFound"]
+
+ # A map of error classes to a list of messages. When an error of one of the
+ # classes specified here occurs, if its error message contains one of the
+ # strings corresponding to it here, that error will be ignored.
+ # This option can't be set via environment variable.
+ # error_collector.ignore_messages: {ThreadError: ["queue empty"]}
+
+ # A comma separated list of status codes, possibly including ranges. Errors
+ # associated with these status codes, where applicable, will be ignored.
+ # error_collector.ignore_status_codes: ""
+
+ # Defines the maximum number of frames in an error backtrace. Backtraces over
+ # this amount are truncated in the middle, preserving the beginning and the end
+ # of the stack trace.
+ error_collector.max_backtrace_frames: 50
+
+ # Defines the maximum number of TransactionError events reported per harvest
+ # cycle.
+ error_collector.max_event_samples_stored: 100
+
+ # Allows newrelic distributed tracing headers to be suppressed on outbound
+ # requests.
+ # exclude_newrelic_header: false
+
+ # The exit handler that sends all cached data to the collector before shutting
+ # down is forcibly installed. This is true even when it detects scenarios where
+ # it generally should not be. The known use case for this option is when Sinatra
+ # runs as an embedded service within another framework. The agent detects the
+ # Sinatra app and skips the at_exit handler as a result. Sinatra classically
+ # runs the entire application in an at_exit block and would otherwise misbehave
+ # if the agent's at_exit handler was also installed in those circumstances.
+ # Note: send_data_on_exit should also be set to true in tandem with this
+ # setting.
+ # force_install_exit_handler: false
+
+ # If true, enables high security mode. Ensure you understand the implications of
+ # high security mode before enabling this setting.
+ # high_security: false
+
+ # If true (the default), data sent to the trace observer is batched instead of
+ # sending each span individually.
+ # infinite_tracing.batching: true
+
+ # Configure the compression level for data sent to the trace observer. May be
+ # one of: :none, :low, :medium, :high. Set the level to :none to disable
+ # compression.
+ # infinite_tracing.compression_level: high
+
+ # Configures the hostname for the trace observer Host. When configured, enables
+ # tail-based sampling by sending all recorded spans to a trace observer for
+ # further sampling decisions, irrespective of any usual agent sampling decision.
+ # infinite_tracing.trace_observer.host: ""
+
+ # Configures the TCP/IP port for the trace observer Host
+ # infinite_tracing.trace_observer.port: 443
+
+ # Controls auto-instrumentation of ActiveSupport::BroadcastLogger at start up.
+ # May be one of: auto, prepend, chain, disabled. Used in Rails versions >= 7.1.
+ # instrumentation.active_support_broadcast_logger: auto
+
+ # Controls auto-instrumentation of ActiveSupport::Logger at start up. May be one
+ # of: auto, prepend, chain, disabled. Used in Rails versions below 7.1.
+ # instrumentation.active_support_logger: auto
+
+ # Controls auto-instrumentation of Async::HTTP at start up. May be one of: auto,
+ # prepend, chain, disabled.
+ # instrumentation.async_http: auto
+
+ # Controls auto-instrumentation of the aws-sdk-firehose library at start-up. May
+ # be one of auto, prepend, chain, disabled.
+ # instrumentation.aws_sdk_firehose: auto
+
+ # Controls auto-instrumentation of the aws-sdk-kinesis library at start-up. May
+ # be one of auto, prepend, chain, disabled.
+ # instrumentation.aws_sdk_kinesis: auto
+
+ # Controls auto-instrumentation of the aws_sdk_lambda library at start-up. May
+ # be one of auto, prepend, chain, disabled.
+ # instrumentation.aws_sdk_lambda: auto
+
+ # Controls auto-instrumentation of the aws-sdk-sqs library at start-up. May be
+ # one of: auto, prepend, chain, disabled.
+ # instrumentation.aws_sqs: auto
+
+ # Controls auto-instrumentation of bunny at start-up. May be one of: auto,
+ # prepend, chain, disabled.
+ # instrumentation.bunny: auto
+
+ # Controls auto-instrumentation of the concurrent-ruby library at start-up. May
+ # be one of: auto, prepend, chain, disabled.
+ # instrumentation.concurrent_ruby: auto
+
+ # Controls auto-instrumentation of Curb at start-up. May be one of: auto,
+ # prepend, chain, disabled.
+ # instrumentation.curb: auto
+
+ # Controls auto-instrumentation of Delayed Job at start-up. May be one of: auto,
+ # prepend, chain, disabled.
+ # instrumentation.delayed_job: auto
+
+ # Controls auto-instrumentation of the aws-sdk-dynamodb library at start-up. May
+ # be one of auto, prepend, chain, disabled.
+ # instrumentation.dynamodb: auto
+
+ # Controls auto-instrumentation of the elasticsearch library at start-up. May be
+ # one of: auto, prepend, chain, disabled.
+ # instrumentation.elasticsearch: auto
+
+ # Controls auto-instrumentation of ethon at start up. May be one of auto,
+ # prepend, chain, disabled
+ # instrumentation.ethon: auto
+
+ # Controls auto-instrumentation of Excon at start-up. May be one of: enabled,
+ # disabled.
+ # instrumentation.excon: enabled
+
+ # Controls auto-instrumentation of the Fiber class at start-up. May be one of:
+ # auto, prepend, chain, disabled.
+ # instrumentation.fiber: auto
+
+ # Controls auto-instrumentation of Grape at start-up. May be one of: auto,
+ # prepend, chain, disabled.
+ # instrumentation.grape: auto
+
+ # Specifies a list of hostname patterns separated by commas that will match gRPC
+ # hostnames that traffic is to be ignored by New Relic for. New Relic's gRPC
+ # client instrumentation will ignore traffic streamed to a host matching any of
+ # these patterns, and New Relic's gRPC server instrumentation will ignore
+ # traffic for a server running on a host whose hostname matches any of these
+ # patterns. By default, no traffic is ignored when gRPC instrumentation is
+ # itself enabled. For example, "private.com$,exception.*"
+ # instrumentation.grpc.host_denylist: []
+
+ # Controls auto-instrumentation of gRPC clients at start-up. May be one of:
+ # auto, prepend, chain, disabled.
+ # instrumentation.grpc_client: auto
+
+ # Controls auto-instrumentation of gRPC servers at start-up. May be one of:
+ # auto, prepend, chain, disabled.
+ # instrumentation.grpc_server: auto
+
+ # Controls auto-instrumentation of HTTPClient at start-up. May be one of: auto,
+ # prepend, chain, disabled.
+ # instrumentation.httpclient: auto
+
+ # Controls auto-instrumentation of http.rb gem at start-up. May be one of: auto,
+ # prepend, chain, disabled.
+ # instrumentation.httprb: auto
+
+ # Controls auto-instrumentation of httpx at start up. May be one of auto,
+ # prepend, chain, disabled
+ # instrumentation.httpx: auto
+
+ # Controls auto-instrumentation of Ruby standard library Logger at start-up. May
+ # be one of: auto, prepend, chain, disabled.
+ # instrumentation.logger: auto
+
+ # Controls auto-instrumentation of the LogStasher library at start-up. May be
+ # one of: auto, prepend, chain, disabled.
+ # instrumentation.logstasher: auto
+
+ # Controls auto-instrumentation of dalli gem for Memcache at start-up. May be
+ # one of: auto, prepend, chain, disabled.
+ # instrumentation.memcache: auto
+
+ # Controls auto-instrumentation of memcache-client gem for Memcache at start-up.
+ # May be one of: auto, prepend, chain, disabled.
+ # instrumentation.memcache_client: auto
+
+ # Controls auto-instrumentation of memcached gem for Memcache at start-up. May
+ # be one of: auto, prepend, chain, disabled.
+ # instrumentation.memcached: auto
+
+ # Controls auto-instrumentation of Mongo at start-up. May be one of: enabled,
+ # disabled.
+ # instrumentation.mongo: enabled
+
+ # Controls auto-instrumentation of Net::HTTP at start-up. May be one of: auto,
+ # prepend, chain, disabled.
+ instrumentation.net_http: prepend
+
+ # Controls auto-instrumentation of the opensearch-ruby library at start-up. May
+ # be one of auto, prepend, chain, disabled.
+ # instrumentation.opensearch: auto
+
+ # Controls auto-instrumentation of Puma::Rack. When enabled, the agent hooks
+ # into the to_app method in Puma::Rack::Builder to find gems to instrument
+ # during application startup. May be one of: auto, prepend, chain, disabled.
+ # instrumentation.puma_rack: auto
+
+ # Controls auto-instrumentation of Puma::Rack::URLMap at start-up. May be one
+ # of: auto, prepend, chain, disabled.
+ # instrumentation.puma_rack_urlmap: auto
+
+ # Controls auto-instrumentation of Rack. When enabled, the agent hooks into the
+ # to_app method in Rack::Builder to find gems to instrument during application
+ # startup. May be one of: auto, prepend, chain, disabled.
+ # instrumentation.rack: auto
+
+ # Controls auto-instrumentation of Rack::URLMap at start-up. May be one of:
+ # auto, prepend, chain, disabled.
+ # instrumentation.rack_urlmap: auto
+
+ # Controls auto-instrumentation of rake at start-up. May be one of: auto,
+ # prepend, chain, disabled.
+ # instrumentation.rake: auto
+
+ # Controls auto-instrumentation of the rdkafka library at start-up. May be one
+ # of auto, prepend, chain, disabled.
+ # instrumentation.rdkafka: auto
+
+ # Controls auto-instrumentation of Redis at start-up. May be one of: auto,
+ # prepend, chain, disabled.
+ # instrumentation.redis: auto
+
+ # Controls auto-instrumentation of resque at start-up. May be one of: auto,
+ # prepend, chain, disabled.
+ # instrumentation.resque: auto
+
+ # Controls auto-instrumentation of Roda at start-up. May be one of: auto,
+ # prepend, chain, disabled.
+ # instrumentation.roda: auto
+
+ # Controls auto-instrumentation of the ruby-kafka library at start-up. May be
+ # one of auto, prepend, chain, disabled.
+ # instrumentation.ruby_kafka: auto
+
+ # Controls auto-instrumentation of the ruby-openai gem at start-up. May be one
+ # of: auto, prepend, chain, disabled. Defaults to disabled in high security
+ # mode.
+ # instrumentation.ruby_openai: auto
+
+ # Controls auto-instrumentation of Sinatra at start-up. May be one of: auto,
+ # prepend, chain, disabled.
+ # instrumentation.sinatra: auto
+
+ # Controls auto-instrumentation of Stripe at startup. May be one of: enabled,
+ # disabled.
+ # instrumentation.stripe: enabled
+
+ # Controls auto-instrumentation of the Thread class at start-up to allow the
+ # agent to correctly nest spans inside of an asynchronous transaction. This does
+ # not enable the agent to automatically trace all threads created (see
+ # instrumentation.thread.tracing). May be one of: auto, prepend, chain,
+ # disabled.
+ # instrumentation.thread: auto
+
+ # Controls auto-instrumentation of the Thread class at start-up to automatically
+ # add tracing to all Threads created in the application.
+ instrumentation.thread.tracing: false
+
+ # Controls auto-instrumentation of the Tilt template rendering library at
+ # start-up. May be one of: auto, prepend, chain, disabled.
+ # instrumentation.tilt: auto
+
+ # Controls auto-instrumentation of Typhoeus at start-up. May be one of: auto,
+ # prepend, chain, disabled.
+ # instrumentation.typhoeus: auto
+
+ # Controls auto-instrumentation of ViewComponent at startup. May be one of:
+ # auto, prepend, chain, disabled.
+ # instrumentation.view_component: auto
+
+ # A dictionary of label names and values that will be applied to the data sent
+ # from this agent. May also be expressed as a semicolon-delimited ; string of
+ # colon-separated : pairs. For example, Server:One;Data Center:Primary.
+ # labels: ""
+
+ # Defines a name for the log file.
+ # log_file_name: newrelic_agent.log
+
+ # Defines a path to the agent log file, excluding the filename.
+ # log_file_path: log/
+
+ # Specifies a marshaller for transmitting data to the New Relic collector.
+ # Currently json is the only valid value for this setting.
+ # marshaller: json
+
+ # If true, the agent will collect metadata about messages and attach them as
+ # segment parameters.
+ # message_tracer.segment_parameters.enabled: true
+
+ # If true, the agent captures Mongo queries in transaction traces.
+ # mongo.capture_queries: true
+
+ # If true, the agent obfuscates Mongo queries in transaction traces.
+ # mongo.obfuscate_queries: true
+
+ # When true, the agent transmits data about your app to the New Relic collector.
+ # monitor_mode: true
+
+ # If true, the agent captures OpenSearch queries in transaction traces.
+ # opensearch.capture_queries: true
+
+ # If true, the agent obfuscates OpenSearch queries in transaction traces.
+ # opensearch.obfuscate_queries: true
+
+ # If true, uses Module#prepend rather than alias_method for ActiveRecord
+ # instrumentation.
+ # prepend_active_record_instrumentation: false
+
+ # Specify a custom host name for display in the New Relic UI.
+ # process_host.display_name: default hostname
+
+ # Defines a host for communicating with the New Relic collector via a proxy
+ # server.
+ # proxy_host: nil
+
+ # Defines a password for communicating with the New Relic collector via a proxy
+ # server.
+ # proxy_pass: nil
+
+ # Defines a port for communicating with the New Relic collector via a proxy
+ # server.
+ # proxy_port: 8080
+
+ # Defines a user for communicating with the New Relic collector via a proxy
+ # server.
+ # proxy_user: nil
+
+ # Timeout for waiting on connect to complete before a rake task
+ # rake.connect_timeout: 10
+
+ # Specify an Array of Rake tasks to automatically instrument. This configuration
+ # option converts the Array to a RegEx list. If you'd like to allow all tasks by
+ # default, use rake.tasks: [.+]. No rake tasks will be instrumented unless
+ # they're added to this list. For more information, visit the New Relic Rake
+ # Instrumentation docs.
+ # rake.tasks: []
+
+ # Define transactions you want the agent to ignore, by specifying a list of
+ # patterns matching the URI you want to ignore. For more detail, see the docs on
+ # ignoring specific transactions.
+ # rules.ignore_url_regexes: []
+
+ # Applies Language Agent Security Policy settings.
+ # security_policies_token: ""
+
+ # If true, enables the exit handler that sends data to the New Relic collector
+ # before shutting down.
+ # send_data_on_exit: true
+
+ # If true, the agent will operate in a streamlined mode suitable for use with
+ # short-lived serverless functions. NOTE: Only AWS Lambda functions are
+ # supported currently and this option isn't intended for use without New Relic's
+ # Ruby Lambda layer offering.
+ # serverless_mode.enabled: false
+
+ # An array of strings that will collectively serve as a denylist for filtering
+ # which Sidekiq job arguments get reported to New Relic. To capture any Sidekiq
+ # arguments, 'job.sidekiq.args.*' must be added to the separate
+ # :'attributes.include' configuration option. Each string in this array will be
+ # turned into a regular expression via Regexp.new to permit advanced matching.
+ # For job argument hashes, if either a key or value matches the pair will be
+ # excluded. All matching job argument array elements and job argument scalars
+ # will be excluded.
+ # sidekiq.args.exclude: []
+
+ # An array of strings that will collectively serve as an allowlist for filtering
+ # which Sidekiq job arguments get reported to New Relic. To capture any Sidekiq
+ # arguments, 'job.sidekiq.args.*' must be added to the separate
+ # :'attributes.include' configuration option. Each string in this array will be
+ # turned into a regular expression via Regexp.new to permit advanced matching.
+ # For job argument hashes, if either a key or value matches the pair will be
+ # included. All matching job argument array elements and job argument scalars
+ # will be included.
+ # sidekiq.args.include: []
+
+ # If true, the agent collects slow SQL queries.
+ # slow_sql.enabled: true
+
+ # If true, the agent collects explain plans in slow SQL queries. If this setting
+ # is omitted, the transaction_tracer.explain_enabled setting will be applied as
+ # the default setting for explain plans in slow SQL as well.
+ # slow_sql.explain_enabled: true
+
+ # Specify a threshold in seconds. The agent collects slow SQL queries and
+ # explain plans that exceed this threshold.
+ # slow_sql.explain_threshold: 0.5
+
+ # Defines an obfuscation level for slow SQL queries. Valid options are
+ # obfuscated, raw, or none.
+ # slow_sql.record_sql: obfuscated
+
+ # Generate a longer sql_id for slow SQL traces. sql_id is used for aggregation
+ # of similar queries.
+ # slow_sql.use_longer_sql_id: false
+
+ # If true, the agent captures attributes on span events.
+ # span_events.attributes.enabled: true
+
+ # Prefix of attributes to exclude from span events. Allows * as wildcard at end.
+ # span_events.attributes.exclude: []
+
+ # Prefix of attributes to include on span events. Allows * as wildcard at end.
+ # span_events.attributes.include: []
+
+ # If true, enables span event sampling.
+ # span_events.enabled: true
+
+ # - Defines the maximum number of span events reported from a single harvest.
+ # Any Integer between 1 and 10000 is valid.'
+ # - When configuring the agent for AI monitoring, set to max value 10000.This
+ # ensures the agent captures the maximum amount of distributed traces.
+ # span_events.max_samples_stored: 2000
+
+ # Sets the maximum number of span events to buffer when streaming to the trace
+ # observer.
+ # span_events.queue_size: 10000
+
+ # Specify a list of exceptions you do not want the agent to strip when
+ # strip_exception_messages is true. Separate exceptions with a comma. For
+ # example, "ImportantException,PreserveMessageException".
+ # strip_exception_messages.allowed_classes: ""
+
+ # If true, the agent strips messages from all exceptions except those in the
+ # allowed classes list. Enabled automatically in high security mode.
+ # strip_exception_messages.enabled: false
+
+ # An array of strings to specify which keys and/or values inside a Stripe
+ # event's user_data hash should
+ # not be reported to New Relic. Each string in this array will be turned into a
+ # regular expression via
+ # Regexp.new to permit advanced matching. For each hash pair, if either the key
+ # or value is matched the pair
+ # isn't reported. By default, no user_data is reported. Use this option only if
+ # the
+ # stripe.user_data.include option is also used.
+ # stripe.user_data.exclude: []
+
+ # An array of strings to specify which keys inside a Stripe event's user_data
+ # hash should be reported
+ # to New Relic. Each string in this array will be turned into a regular
+ # expression via Regexp.new to
+ # enable advanced matching. Setting the value to ["."] will report all
+ # user_data.
+ # stripe.user_data.include: []
+
+ # When set to true, forces a synchronous connection to the New Relic collector
+ # during application startup. For very short-lived processes, this helps ensure
+ # the New Relic agent has time to report.
+ # sync_startup: false
+
+ # If true, tracer state storage is thread-local, otherwise, fiber-local
+ # thread_local_tracer_state: false
+
+ # If true, enables use of the thread profiler.
+ # thread_profiler.enabled: false
+
+ # Defines the maximum number of seconds the agent should spend attempting to
+ # connect to the collector.
+ # timeout: 120
+
+ # If true, the agent captures attributes from transaction events.
+ # transaction_events.attributes.enabled: true
+
+ # Prefix of attributes to exclude from transaction events. Allows * as wildcard
+ # at end.
+ # transaction_events.attributes.exclude: []
+
+ # Prefix of attributes to include in transaction events. Allows * as wildcard at
+ # end.
+ # transaction_events.attributes.include: []
+
+ # If true, enables transaction event sampling.
+ # transaction_events.enabled: true
+
+ # Defines the maximum number of transaction events reported from a single
+ # harvest.
+ # transaction_events.max_samples_stored: 1200
+
+ # If true, the agent captures attributes on transaction segments.
+ # transaction_segments.attributes.enabled: true
+
+ # Prefix of attributes to exclude from transaction segments. Allows * as
+ # wildcard at end.
+ # transaction_segments.attributes.exclude: []
+
+ # Prefix of attributes to include on transaction segments. Allows * as wildcard
+ # at end.
+ # transaction_segments.attributes.include: []
+
+ # If true, the agent captures attributes from transaction traces.
+ # transaction_tracer.attributes.enabled: true
+
+ # Prefix of attributes to exclude from transaction traces. Allows * as wildcard
+ # at end.
+ # transaction_tracer.attributes.exclude: []
+
+ # Prefix of attributes to include in transaction traces. Allows * as wildcard at
+ # end.
+ # transaction_tracer.attributes.include: []
+
+ # If true, enables collection of transaction traces.
+ # transaction_tracer.enabled: true
+
+ # If true, enables the collection of explain plans in transaction traces. This
+ # setting will also apply to explain plans in slow SQL traces if
+ # slow_sql.explain_enabled isn't set separately.
+ # transaction_tracer.explain_enabled: true
+
+ # Threshold (in seconds) above which the agent will collect explain plans.
+ # Relevant only when explain_enabled is true.
+ # transaction_tracer.explain_threshold: 0.5
+
+ # Maximum number of transaction trace nodes to record in a single transaction
+ # trace.
+ # transaction_tracer.limit_segments: 4000
+
+ # If true, the agent records Redis command arguments in transaction traces.
+ # transaction_tracer.record_redis_arguments: false
+
+ # Obfuscation level for SQL queries reported in transaction trace nodes.
+ # By default, this is set to obfuscated, which strips out the numeric and string
+ # literals.
+ # - If you do not want the agent to capture query information, set this to none.
+ # - If you want the agent to capture all query information in its original form,
+ # set this to raw.
+ # - When you enable high security mode, this is automatically set to obfuscated.
+ # transaction_tracer.record_sql: obfuscated
+
+ # Specify a threshold in seconds. The agent includes stack traces in transaction
+ # trace nodes when the stack trace duration exceeds this threshold.
+ # transaction_tracer.stack_trace_threshold: 0.5
+
+ # Specify a threshold in seconds. Transactions with a duration longer than this
+ # threshold are eligible for transaction traces. Specify a float value or the
+ # string apdex_f.
+ # transaction_tracer.transaction_threshold: 1.0
+
+ # If true, the agent automatically detects that it is running in an AWS
+ # environment.
+ # utilization.detect_aws: true
+
+ # If true, the agent automatically detects that it is running in an Azure
+ # environment.
+ # utilization.detect_azure: true
+
+ # If true, the agent automatically detects that it is running in Docker.
+ # utilization.detect_docker: true
+
+ # If true, the agent automatically detects that it is running in an Google Cloud
+ # Platform environment.
+ # utilization.detect_gcp: true
+
+ # If true, the agent automatically detects that it is running in Kubernetes.
+ # utilization.detect_kubernetes: true
+
+ # If true, the agent automatically detects that it is running in a Pivotal Cloud
+ # Foundry environment.
+ # utilization.detect_pcf: true
+
+ # BEGIN security agent
+ #
+ # NOTE: At this time, the security agent is intended for use only within
+ # a dedicated security testing environment with data that can tolerate
+ # modification or deletion. The security agent is available as a
+ # separate Ruby gem, newrelic_security. It is recommended that this
+ # separate gem only be introduced to a security testing environment
+ # by leveraging Bundler grouping like so:
+ #
+ # # Gemfile
+ # gem 'newrelic_rpm' # New Relic APM observability agent
+ # gem 'newrelic-infinite_tracing' # New Relic Infinite Tracing
+ #
+ # group :security do
+ # gem 'newrelic_security', require: false # New Relic security agent
+ # end
+ #
+ # NOTE: All "security.*" configuration parameters are related only to the
+ # security agent, and all other configuration parameters that may
+ # have "security" in the name somewhere are related to the APM agent.
+
+ # If true, the security agent is loaded (a Ruby 'require' is performed)
+ # security.agent.enabled: false
+
+ # The port the application is listening on. This setting is mandatory for
+ # Passenger servers. The agent detects other servers by default.
+ # security.application_info.port: nil
+
+ # If true, the security agent is started (the agent runs in its event loop)
+ # security.enabled: false
+
+ # Defines API paths the security agent should ignore in IAST scans. Accepts an
+ # array of regex patterns matching the URI to ignore. The regex pattern should
+ # find a complete match for the URL without the endpoint. For example,
+ # [".*account.*"], [".*/\api\/v1\/.*?\/login"]
+ # security.exclude_from_iast_scan.api: []
+
+ # An array of HTTP request body keys the security agent should ignore in IAST
+ # scans.
+ # security.exclude_from_iast_scan.http_request_parameters.body: []
+
+ # An array of HTTP request headers the security agent should ignore in IAST
+ # scans. The array should specify a list of patterns matching the headers to
+ # ignore.
+ # security.exclude_from_iast_scan.http_request_parameters.header: []
+
+ # An array of HTTP request query parameters the security agent should ignore in
+ # IAST scans. The array should specify a list of patterns matching the HTTP
+ # request query parameters to ignore.
+ # security.exclude_from_iast_scan.http_request_parameters.query: []
+
+ # If true, disables system command injection detection in IAST scans.
+ # security.exclude_from_iast_scan.iast_detection_category.command_injection: false
+
+ # If true, disables the detection of low-severity insecure settings. For
+ # example, hash, crypto, cookie, random generators, trust boundary).
+ # security.exclude_from_iast_scan.iast_detection_category.insecure_settings: false
+
+ # If true, disables file operation-related IAST detections (File Access &
+ # Application integrity violation)
+ # security.exclude_from_iast_scan.iast_detection_category.invalid_file_access: false
+
+ # If true, disables Javascript injection detection in IAST scans.
+ # security.exclude_from_iast_scan.iast_detection_category.javascript_injection: false
+
+ # If true, disables LDAP injection detection in IAST scans.
+ # security.exclude_from_iast_scan.iast_detection_category.ldap_injection: false
+
+ # If true, disables NOSQL injection detection in IAST scans.
+ # security.exclude_from_iast_scan.iast_detection_category.nosql_injection: false
+
+ # If true, disables Reflected Cross-Site Scripting (RXSS) detection in IAST
+ # scans.
+ # security.exclude_from_iast_scan.iast_detection_category.rxss: false
+
+ # If true, disables SQL injection detection in IAST scans.
+ # security.exclude_from_iast_scan.iast_detection_category.sql_injection: false
+
+ # If true, disables Sever-Side Request Forgery (SSRF) detection in IAST scans.
+ # security.exclude_from_iast_scan.iast_detection_category.ssrf: false
+
+ # If true, disables XPATH injection detection in IAST scans.
+ # security.exclude_from_iast_scan.iast_detection_category.xpath_injection: false
+
+ # A unique test identifier when runnning IAST in a CI/CD environment to
+ # differentiate between different test runs. For example, a build number.
+ # security.iast_test_identifier: nil
+
+ # Defines the mode for the security agent to operate in. Currently only IAST is
+ # supported
+ # security.mode: IAST
+
+ # Sets the maximum number of HTTP requests allowed for the IAST scan per minute.
+ # Any Integer between 12 and 3600 is valid. The default value is 3600.
+ # security.scan_controllers.iast_scan_request_rate_limit: 3600
+
+ # If true, enables the sending of HTTP responses bodies. Disabling this also
+ # disables Reflected Cross-Site Scripting (RXSS) vulnerability detection.
+ # security.scan_controllers.report_http_response_body: true
+
+ # The number of application instances for a specific entity to perform IAST
+ # analysis on.
+ # security.scan_controllers.scan_instance_count: 0
+
+ # If true, allows IAST to continuously gather trace data in the background. The
+ # security agent uses collected data to perform an IAST scan at the scheduled
+ # time.
+ # security.scan_schedule.always_sample_traces: false
+
+ # Specifies the delay time (in minutes) before the IAST scan begins after the
+ # application starts.
+ # security.scan_schedule.delay: 0
+
+ # Indicates the duration (in minutes) for which the IAST scan will be performed.
+ # security.scan_schedule.duration: 0
+
+ # Specifies a cron expression that sets when the IAST scan should run.
+ # security.scan_schedule.schedule: ""
+
+ # Defines the endpoint URL for posting security-related data
+ # security.validator_service_url: wss://csec.nr-data.net
+
+ # END security agent
+
+# Environment-specific settings are in this section.
+# RAILS_ENV or RACK_ENV (as appropriate) is used to determine the environment.
+# If your application has other named environments, configure them here.
+development:
+ <<: *default_settings
+ development_mode: true
+ monitor_mode: false
+ app_name: Development
+ distributed_tracing.enabled: false
+
+test:
+ <<: *default_settings
+ # It doesn't make sense to report to New Relic from automated test runs.
+ monitor_mode: false
+ distributed_tracing.enabled: false
+
+staging:
+ <<: *default_settings
+ monitor_mode: <%= ENV['NEWRELIC_MONITOR_MODE'].presence || false %>
+ app_name: <%= ENV['NEWRELIC_APP_NAME'] %> Staging
+ distributed_tracing.enabled: false
+
+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
diff --git a/config/puma.rb b/config/puma.rb
index ed99f0c9..ec60e285 100644
--- a/config/puma.rb
+++ b/config/puma.rb
@@ -8,7 +8,7 @@
require_relative 'dotenv'
-max_threads_count = ENV.fetch('RAILS_MAX_THREADS', 15)
+max_threads_count = ENV.fetch('RAILS_MAX_THREADS', 5)
min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count }
threads min_threads_count, max_threads_count
@@ -39,7 +39,7 @@ if ENV['WEB_CONCURRENCY_AUTO'] == 'true'
workers Etc.nprocessors
else
- workers ENV.fetch('WEB_CONCURRENCY', 0)
+ workers ENV.fetch('WEB_CONCURRENCY', 1)
end
# Use the `preload_app!` method when specifying a `workers` number.
diff --git a/config/routes.rb b/config/routes.rb
index d76fba77..eddde784 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -56,6 +56,11 @@ Rails.application.routes.draw do
resources :form_events, only: %i[index], path: 'form/:type'
resources :submission_events, only: %i[index], path: 'submission/:type'
end
+ resources :external_auth, only: [] do
+ collection do
+ post :user_token
+ end
+ end
end
resources :export, controller: 'export' do
diff --git a/db/migrate/20250814214357_add_external_ids_to_accounts_and_users.rb b/db/migrate/20250814214357_add_external_ids_to_accounts_and_users.rb
new file mode 100644
index 00000000..e5f79598
--- /dev/null
+++ b/db/migrate/20250814214357_add_external_ids_to_accounts_and_users.rb
@@ -0,0 +1,9 @@
+class AddExternalIdsToAccountsAndUsers < ActiveRecord::Migration[8.0]
+ def change
+ add_column :accounts, :external_account_id, :integer
+ add_column :users, :external_user_id, :integer
+
+ add_index :accounts, :external_account_id, unique: true
+ add_index :users, :external_user_id, unique: true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 658ea3bd..ef23e9e3 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_08_11_211829) do
+ActiveRecord::Schema[8.0].define(version: 2025_08_14_214357) do
# These are extensions that must be enabled in order to support this database
enable_extension "btree_gin"
enable_extension "pg_catalog.plpgsql"
@@ -62,6 +62,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_08_11_211829) do
t.datetime "updated_at", null: false
t.string "uuid", null: false
t.datetime "archived_at"
+ t.integer "external_account_id"
+ t.index ["external_account_id"], name: "index_accounts_on_external_account_id", unique: true
t.index ["uuid"], name: "index_accounts_on_uuid", unique: true
end
@@ -440,8 +442,10 @@ ActiveRecord::Schema[8.0].define(version: 2025_08_11_211829) do
t.string "otp_secret"
t.integer "consumed_timestep"
t.boolean "otp_required_for_login", default: false, null: false
+ t.integer "external_user_id"
t.index ["account_id"], name: "index_users_on_account_id"
t.index ["email"], name: "index_users_on_email", unique: true
+ t.index ["external_user_id"], name: "index_users_on_external_user_id", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
t.index ["unlock_token"], name: "index_users_on_unlock_token", unique: true
t.index ["uuid"], name: "index_users_on_uuid", unique: true
diff --git a/spec/controllers/concerns/iframe_authentication_spec.rb b/spec/controllers/concerns/iframe_authentication_spec.rb
new file mode 100644
index 00000000..fc9ee1a8
--- /dev/null
+++ b/spec/controllers/concerns/iframe_authentication_spec.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+describe IframeAuthentication do
+ let(:account) { create(:account) }
+ let(:user) { create(:user, account: account) }
+ let(:token) { user.access_token.token }
+
+ let(:controller_class) do
+ Class.new(ApplicationController) do
+ include IframeAuthentication
+ end
+ end
+
+ let(:controller) { controller_class.new }
+ let(:request_double) { instance_double(ActionDispatch::Request, headers: {}, referer: nil) }
+
+ before do
+ allow(controller).to receive_messages(
+ request: request_double,
+ params: {},
+ session: {},
+ signed_in?: false,
+ sign_in: nil,
+ render: nil
+ )
+ allow(Rails.logger).to receive(:error)
+ end
+
+ describe '#authenticate_from_referer' do
+ it 'does nothing when already signed in' do
+ allow(controller).to receive(:signed_in?).and_return(true)
+ controller.send(:authenticate_from_referer)
+ expect(controller).not_to have_received(:sign_in)
+ end
+
+ it 'authenticates with valid params token' do
+ allow(controller).to receive(:params).and_return({ auth_token: token })
+ controller.send(:authenticate_from_referer)
+ expect(controller).to have_received(:sign_in).with(user)
+ end
+
+ it 'authenticates with valid session token' do
+ allow(controller).to receive(:session).and_return({ auth_token: token })
+ controller.send(:authenticate_from_referer)
+ expect(controller).to have_received(:sign_in).with(user)
+ end
+
+ it 'authenticates with valid header token' do
+ allow(request_double).to receive(:headers).and_return({ 'X-Auth-Token' => token })
+ controller.send(:authenticate_from_referer)
+ expect(controller).to have_received(:sign_in).with(user)
+ end
+
+ it 'authenticates with token from referer URL' do
+ allow(request_double).to receive(:referer).and_return("https://example.com?auth_token=#{token}")
+ controller.send(:authenticate_from_referer)
+ expect(controller).to have_received(:sign_in).with(user)
+ end
+
+ it 'does nothing with invalid token' do
+ allow(controller).to receive(:params).and_return({ auth_token: 'invalid' })
+ controller.send(:authenticate_from_referer)
+ expect(controller).not_to have_received(:sign_in)
+ expect(controller).not_to have_received(:render)
+ end
+
+ it 'renders error with no token' do
+ controller.send(:authenticate_from_referer)
+ expect(controller).to have_received(:render).with(
+ json: { error: 'Authentication required' },
+ status: :unauthorized
+ )
+ end
+ end
+end
diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb
new file mode 100644
index 00000000..aa23cda7
--- /dev/null
+++ b/spec/models/account_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe Account do
+ describe 'validations' do
+ it 'is valid with valid attributes' do
+ account = build(:account)
+ expect(account).to be_valid
+ end
+
+ it 'validates uniqueness of external_account_id when present' do
+ create(:account, external_account_id: 123)
+ duplicate = build(:account, external_account_id: 123)
+ expect(duplicate).not_to be_valid
+ end
+ end
+
+ describe '.find_or_create_by_external_id' do
+ let(:external_id) { 123 }
+ let(:attributes) { { name: 'Test Account' } }
+
+ it 'finds existing account by external_account_id' do
+ existing_account = create(:account, external_account_id: external_id)
+ result = described_class.find_or_create_by_external_id(external_id, attributes)
+ expect(result).to eq(existing_account)
+ end
+
+ it 'creates new account when none exists' do
+ result = described_class.find_or_create_by_external_id(external_id, attributes)
+ expect(result.external_account_id).to eq(external_id)
+ expect(result.name).to eq('Test Account')
+ end
+ end
+
+ describe '#testing?' do
+ let(:account) { create(:account) }
+
+ it 'delegates to linked_account_account' do
+ linked_account_account = instance_double(AccountLinkedAccount, testing?: true)
+ allow(account).to receive(:linked_account_account).and_return(linked_account_account)
+
+ expect(account.testing?).to be true
+ end
+ end
+
+ describe '#default_template_folder' do
+ it 'creates default folder when none exists' do
+ account = create(:account)
+ create(:user, account: account)
+
+ expect do
+ folder = account.default_template_folder
+ expect(folder.name).to eq(TemplateFolder::DEFAULT_NAME)
+ expect(folder).to be_persisted
+ end.to change(account.template_folders, :count).by(1)
+ end
+ end
+end
diff --git a/spec/models/submitter_spec.rb b/spec/models/submitter_spec.rb
index 7618dc07..760234db 100644
--- a/spec/models/submitter_spec.rb
+++ b/spec/models/submitter_spec.rb
@@ -1,5 +1,45 @@
# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: submitters
+#
+# id :bigint not null, primary key
+# changes_requested_at :datetime
+# completed_at :datetime
+# declined_at :datetime
+# email :string
+# ip :string
+# metadata :text not null
+# name :string
+# opened_at :datetime
+# phone :string
+# preferences :text not null
+# sent_at :datetime
+# slug :string not null
+# timezone :string
+# ua :string
+# uuid :string not null
+# values :text not null
+# created_at :datetime not null
+# updated_at :datetime not null
+# account_id :integer not null
+# external_id :string
+# submission_id :integer not null
+#
+# Indexes
+#
+# index_submitters_on_account_id_and_id (account_id,id)
+# index_submitters_on_completed_at_and_account_id (completed_at,account_id)
+# index_submitters_on_email (email)
+# index_submitters_on_external_id (external_id)
+# index_submitters_on_slug (slug) UNIQUE
+# index_submitters_on_submission_id (submission_id)
+#
+# Foreign Keys
+#
+# fk_rails_... (submission_id => submissions.id)
+#
require 'rails_helper'
RSpec.describe Submitter do
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
new file mode 100644
index 00000000..009f02fd
--- /dev/null
+++ b/spec/models/user_spec.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe User do
+ describe 'validations' do
+ it 'is valid with valid attributes' do
+ user = build(:user)
+ expect(user).to be_valid
+ end
+
+ it 'validates email format' do
+ user = build(:user, email: 'invalid-email')
+ expect(user).not_to be_valid
+ end
+
+ it 'validates uniqueness of external_user_id when present' do
+ account = create(:account)
+ create(:user, account: account, external_user_id: 123)
+ duplicate = build(:user, account: account, external_user_id: 123)
+ expect(duplicate).not_to be_valid
+ end
+ end
+
+ describe '.find_or_create_by_external_id' do
+ let(:account) { create(:account) }
+ let(:external_id) { 123 }
+ let(:attributes) { { first_name: 'Test', last_name: 'User', email: 'test@example.com' } }
+
+ it 'finds existing user by external_user_id' do
+ existing_user = create(:user, account: account, external_user_id: external_id)
+ result = described_class.find_or_create_by_external_id(account, external_id, attributes)
+ expect(result).to eq(existing_user)
+ end
+
+ it 'creates new user when none exists' do
+ result = described_class.find_or_create_by_external_id(account, external_id, attributes)
+ expect(result.external_user_id).to eq(external_id)
+ expect(result.first_name).to eq('Test')
+ expect(result.email).to eq('test@example.com')
+ expect(result.password).to be_present
+ end
+ end
+
+ describe '#active_for_authentication?' do
+ let(:account) { create(:account) }
+ let(:user) { create(:user, account: account) }
+
+ it 'returns true when user and account are active' do
+ expect(user.active_for_authentication?).to be true
+ end
+
+ it 'returns false when user is archived' do
+ user.update!(archived_at: 1.day.ago)
+ expect(user.active_for_authentication?).to be false
+ end
+
+ it 'returns false when account is archived' do
+ account.update!(archived_at: 1.day.ago)
+ expect(user.active_for_authentication?).to be false
+ end
+ end
+
+ describe '#initials' do
+ it 'returns initials from first and last name' do
+ user = build(:user, first_name: 'John', last_name: 'Doe')
+ expect(user.initials).to eq('JD')
+ end
+
+ it 'handles missing names' do
+ user = build(:user, first_name: 'John', last_name: nil)
+ expect(user.initials).to eq('J')
+ end
+ end
+
+ describe '#full_name' do
+ it 'combines first and last name' do
+ user = build(:user, first_name: 'John', last_name: 'Doe')
+ expect(user.full_name).to eq('John Doe')
+ end
+
+ it 'handles missing names' do
+ user = build(:user, first_name: 'John', last_name: nil)
+ expect(user.full_name).to eq('John')
+ end
+ end
+
+ describe '#friendly_name' do
+ it 'returns formatted name with email when full name present' do
+ user = build(:user, first_name: 'John', last_name: 'Doe', email: 'john@example.com')
+ expect(user.friendly_name).to eq('"John Doe" ')
+ end
+
+ it 'returns just email when no full name' do
+ user = build(:user, first_name: nil, last_name: nil, email: 'john@example.com')
+ expect(user.friendly_name).to eq('john@example.com')
+ end
+ end
+end
diff --git a/spec/requests/application_controller_spec.rb b/spec/requests/application_controller_spec.rb
new file mode 100644
index 00000000..68cba51b
--- /dev/null
+++ b/spec/requests/application_controller_spec.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+describe 'ApplicationController' do
+ let(:account) { create(:account) }
+ let(:user) { create(:user, account: account) }
+ let(:token) { user.access_token.token }
+
+ describe 'token authentication methods' do
+ let(:controller) { ApplicationController.new }
+
+ let(:request_double) { instance_double(ActionDispatch::Request, headers: {}) }
+
+ before do
+ allow(controller).to receive_messages(
+ request: request_double,
+ params: {},
+ session: {},
+ signed_in?: false
+ )
+ end
+
+ describe '#maybe_authenticate_via_token' do
+ it 'signs in user with valid token in header' do
+ request_double_with_token = instance_double(ActionDispatch::Request, headers: { 'X-Auth-Token' => token })
+ allow(controller).to receive(:request).and_return(request_double_with_token)
+ allow(controller).to receive(:sign_in)
+
+ controller.send(:maybe_authenticate_via_token)
+
+ expect(controller).to have_received(:sign_in).with(user)
+ end
+
+ it 'does nothing with invalid token' do
+ request_double_with_invalid = instance_double(ActionDispatch::Request, headers: { 'X-Auth-Token' => 'invalid' })
+ allow(controller).to receive(:request).and_return(request_double_with_invalid)
+ allow(controller).to receive(:sign_in)
+
+ controller.send(:maybe_authenticate_via_token)
+
+ expect(controller).not_to have_received(:sign_in)
+ end
+ end
+
+ describe '#authenticate_via_token!' do
+ it 'renders error with no token' do
+ allow(controller).to receive(:render)
+
+ controller.send(:authenticate_via_token!)
+
+ expect(controller).to have_received(:render).with(
+ json: { error: 'Authentication required. Please provide a valid auth_token.' },
+ status: :unauthorized
+ )
+ end
+
+ it 'renders error with invalid token' do
+ request_double_with_invalid = instance_double(ActionDispatch::Request, headers: { 'X-Auth-Token' => 'invalid' })
+ allow(controller).to receive(:request).and_return(request_double_with_invalid)
+ allow(controller).to receive(:render)
+
+ controller.send(:authenticate_via_token!)
+
+ expect(controller).to have_received(:render).with(
+ json: { error: 'Authentication required. Please provide a valid auth_token.' },
+ status: :unauthorized
+ )
+ end
+
+ it 'does not render error with valid token' do
+ request_double_with_token = instance_double(ActionDispatch::Request, headers: { 'X-Auth-Token' => token })
+ allow(controller).to receive(:request).and_return(request_double_with_token)
+ allow(controller).to receive_messages(sign_in: nil, render: nil)
+
+ controller.send(:authenticate_via_token!)
+
+ expect(controller).not_to have_received(:render)
+ expect(controller).to have_received(:sign_in).with(user)
+ end
+ end
+ end
+
+ describe 'API authentication' do
+ context 'with valid token' do
+ it 'authenticates user' do
+ get '/api/submissions', headers: { 'X-Auth-Token': token }
+ expect(response).to have_http_status(:ok)
+ end
+ end
+
+ context 'with invalid token' do
+ it 'returns API-specific error message' do
+ get '/api/submissions', headers: { 'X-Auth-Token': 'invalid_token' }
+ expect(response).to have_http_status(:unauthorized)
+ expect(response.parsed_body).to eq({ 'error' => 'Not authenticated' })
+ end
+ end
+ end
+end
diff --git a/spec/requests/external_auth_spec.rb b/spec/requests/external_auth_spec.rb
new file mode 100644
index 00000000..34a8e5eb
--- /dev/null
+++ b/spec/requests/external_auth_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+describe 'External Auth API' do
+ describe 'POST /api/external_auth/user_token' do
+ let(:valid_params) do
+ {
+ account: {
+ external_id: '123',
+ name: 'Test Company'
+ },
+ user: {
+ external_id: '456',
+ email: 'test@example.com',
+ first_name: 'John',
+ last_name: 'Doe'
+ }
+ }
+ end
+
+ it 'returns success with access token' do
+ post '/api/external_auth/user_token', params: valid_params, as: :json
+
+ expect(response).to have_http_status(:ok)
+ expect(response.parsed_body).to have_key('access_token')
+ end
+
+ it 'returns error when params cause exception' do
+ allow(Account).to receive(:find_or_create_by_external_id).and_raise(StandardError.new('Test error'))
+
+ post '/api/external_auth/user_token', params: valid_params, as: :json
+
+ expect(response).to have_http_status(:internal_server_error)
+ expect(response.parsed_body).to eq({ 'error' => 'Internal server error' })
+ end
+ end
+end
diff --git a/spec/signing_form_helper.rb b/spec/signing_form_helper.rb
index a0b05a2a..268c7b52 100644
--- a/spec/signing_form_helper.rb
+++ b/spec/signing_form_helper.rb
@@ -59,4 +59,18 @@ module SigningFormHelper
def template_field(template, field_name)
template.fields.find { |f| f['name'] == field_name || f['title'] == field_name } || {}
end
+
+ # Waits for a job to be queued in Sidekiq for the specified job class.
+ def wait_for_job_to_queue(job_class, timeout: 5)
+ initial_count = job_class.jobs.size
+ Timeout.timeout(timeout) do
+ loop do
+ break if job_class.jobs.size > initial_count
+
+ sleep 0.1
+ end
+ end
+ rescue Timeout::Error
+ # If timeout occurs, just continue - the test will fail with a more descriptive message
+ end
end
diff --git a/spec/system/signing_form_spec.rb b/spec/system/signing_form_spec.rb
index 95175bdb..3c86f7a3 100644
--- a/spec/system/signing_form_spec.rb
+++ b/spec/system/signing_form_spec.rb
@@ -654,6 +654,7 @@ RSpec.describe 'Signing Form' do
expect do
click_on 'Sign and Complete'
+ wait_for_job_to_queue(ProcessSubmitterCompletionJob)
end.to change(ProcessSubmitterCompletionJob.jobs, :size).by(1)
end
end