mirror of https://github.com/docusealco/docuseal
commit
d4a0dd379a
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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 "$@"
|
||||
@ -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
|
||||
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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" <john@example.com>')
|
||||
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
|
||||
@ -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
|
||||
@ -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
|
||||
Loading…
Reference in new issue