diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2ca4dce..219c161 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ Harassment, discrimination, or abusive behavior will not be tolerated. ### Reporting Bugs -1. Check existing [issues](https://github.com/EvolutionAPI/evo-auth-service-community/issues) +1. Check existing [issues](https://github.com/evolution-foundation/evo-auth-service-community/issues) to avoid duplicates 2. Open a new issue with: - Clear, descriptive title diff --git a/README.md b/README.md index ec723d6..931df83 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@
-
+
@@ -32,10 +32,10 @@
## Part of the Evo CRM Community
-Evo CRM Auth Service is part of the [Evo CRM Community](https://github.com/EvolutionAPI/evo-crm-community) ecosystem maintained by Evolution Foundation. To use the full stack, clone the umbrella repository with submodules:
+Evo CRM Auth Service is part of the [Evo CRM Community](https://github.com/evolution-foundation/evo-crm-community) ecosystem maintained by Evolution Foundation. To use the full stack, clone the umbrella repository with submodules:
```bash
-git clone --recurse-submodules git@github.com:EvolutionAPI/evo-crm-community.git
+git clone --recurse-submodules git@github.com:evolution-foundation/evo-crm-community.git
```
The Community Edition is **single-tenant** by design — one account, no multi-tenancy overhead, no super-admin, no billing or plans. The role hierarchy is simple: `account_owner` and `agent`.
@@ -80,7 +80,7 @@ The Community Edition is **single-tenant** by design — one account, no multi-t
### Installation
```bash
-git clone git@github.com:EvolutionAPI/evo-auth-service-community.git
+git clone git@github.com:evolution-foundation/evo-auth-service-community.git
cd evo-auth-service-community
# Install dependencies
diff --git a/app/controllers/api/v1/profiles_controller.rb b/app/controllers/api/v1/profiles_controller.rb
index dc25e64..044b621 100644
--- a/app/controllers/api/v1/profiles_controller.rb
+++ b/app/controllers/api/v1/profiles_controller.rb
@@ -25,6 +25,8 @@ def update
filtered_params = profile_params
if current_user.update(filtered_params)
+ TokenValidationService.invalidate_cache_for_user(current_user) if avatar_param.present?
+
message = if current_user.unconfirmed_email.present?
'Profile updated. Confirmation email sent to the new address.'
else
diff --git a/app/controllers/api/v1/users_controller.rb b/app/controllers/api/v1/users_controller.rb
index 096055c..685ea91 100644
--- a/app/controllers/api/v1/users_controller.rb
+++ b/app/controllers/api/v1/users_controller.rb
@@ -36,6 +36,13 @@ def create
message: 'User created successfully',
status: :created
)
+ rescue ActiveRecord::RecordInvalid => e
+ error_response(
+ 'VALIDATION_ERROR',
+ e.record.errors.full_messages.join(', '),
+ status: :unprocessable_entity,
+ details: e.record.errors.messages
+ )
end
def update
diff --git a/app/controllers/setup_controller.rb b/app/controllers/setup_controller.rb
index ab87a91..292e832 100644
--- a/app/controllers/setup_controller.rb
+++ b/app/controllers/setup_controller.rb
@@ -147,7 +147,7 @@ def bootstrap
client_ip: request.remote_ip
)
- render json: { status: 'ok', message: 'Installation completed successfully' }, status: :created
+ render json: { status: 'ok', message: 'Installation completed successfully', survey_token: result[:survey_token] }, status: :created
rescue SetupBootstrapService::AlreadyBootstrappedError => e
render json: { error: e.message }, status: :conflict
@@ -156,12 +156,52 @@ def bootstrap
render json: { error: e.message }, status: :unprocessable_entity
end
+ # POST /setup/survey
+ # Saves onboarding survey answers for the initial admin user.
+ # Authenticated via a one-time survey_token generated during bootstrap (TTL: 10 min).
+ # If the token has expired, the frontend falls back to the authenticated endpoint
+ # (POST /api/v1/setup_survey) after the user logs in.
+ def survey
+ token = request.headers['X-Survey-Token'].to_s.strip
+ user_id = redis_client.get("survey_token:#{token}")
+
+ if user_id.blank?
+ render json: { error: 'Invalid or expired survey token' }, status: :unauthorized
+ return
+ end
+
+ user = User.find_by(id: user_id)
+ unless user
+ render json: { error: 'User not found' }, status: :not_found
+ return
+ end
+
+ survey = user.setup_survey_response || user.build_setup_survey_response
+ survey.assign_attributes(survey_params)
+
+ if survey.save
+ redis_client.del("survey_token:#{token}")
+ render json: { status: 'ok' }, status: :ok
+ else
+ render json: { error: survey.errors.full_messages.to_sentence }, status: :unprocessable_entity
+ end
+ end
+
private
def bootstrap_params
params.permit(:first_name, :last_name, :email, :password, :password_confirmation)
end
+ def survey_params
+ params.permit(:team_size, :daily_volume, :main_channel, :main_channel_other,
+ :uses_ai, :biggest_pain, :crm_experience, :main_goal)
+ end
+
+ def redis_client
+ Redis.new(url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/1'))
+ end
+
def resolve_instance_id(ctx)
ctx.instance_id.presence || Licensing::Store.new.load_or_create_instance_id
end
diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb
index 8c4a705..6ee6780 100644
--- a/app/models/concerns/avatarable.rb
+++ b/app/models/concerns/avatarable.rb
@@ -10,14 +10,24 @@ module Avatarable
after_save :fetch_avatar_from_gravatar
end
+ # Signed URL TTL for cloud storage avatars. Short-lived to limit exposure if the
+ # URL leaks via logs, screenshots or cached API responses — frontend should
+ # re-fetch the user payload to refresh the URL when it expires.
+ AVATAR_URL_TTL = 1.hour
+
def avatar_url
- return '' unless avatar.attached?
+ return nil unless avatar.attached?
if avatar.service.class.name == 'ActiveStorage::Service::DiskService'
- Rails.application.routes.url_helpers.rails_service_blob_proxy_path(avatar.signed_id, avatar.filename)
+ base_url = Rails.application.config.app_url.to_s.chomp('/')
+ path = Rails.application.routes.url_helpers.rails_service_blob_proxy_path(avatar.signed_id, avatar.filename)
+ "#{base_url}#{path}"
else
- avatar.url
+ avatar.blob.url(expires_in: AVATAR_URL_TTL)
end
+ rescue StandardError => e
+ Rails.logger.error("[Avatarable] Avatar URL generation failed for #{self.class.name}##{id}: #{e.class} - #{e.message}")
+ nil
end
def fetch_avatar_from_gravatar
diff --git a/app/models/setup_survey_response.rb b/app/models/setup_survey_response.rb
new file mode 100644
index 0000000..1948c94
--- /dev/null
+++ b/app/models/setup_survey_response.rb
@@ -0,0 +1,3 @@
+class SetupSurveyResponse < ApplicationRecord
+ belongs_to :user
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 7e07b1d..2ef8980 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -77,6 +77,12 @@ class User < ApplicationRecord
has_many :user_roles, dependent: :destroy
has_many :roles, through: :user_roles
+ has_many :user_tours, dependent: :destroy
+ has_one :setup_survey_response, dependent: :destroy
+
+ def setup_survey_completed?
+ setup_survey_response.present?
+ end
before_validation :set_password_and_uid, on: :create
diff --git a/app/serializers/serializers.rb b/app/serializers/serializers.rb
index 70d3ec5..25dab9b 100644
--- a/app/serializers/serializers.rb
+++ b/app/serializers/serializers.rb
@@ -50,7 +50,8 @@ def full(user, options = {})
message_signature: user.message_signature,
provider: user.provider,
uid: user.uid,
- avatar_url: user.respond_to?(:avatar_url) ? user.avatar_url : nil
+ avatar_url: user.respond_to?(:avatar_url) ? user.avatar_url : nil,
+ setup_survey_completed: user.setup_survey_completed?
)
# Add hmac_identifier if EVOLUTION_INBOX_HMAC_KEY is present
diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb
index a129d2c..6899802 100644
--- a/app/serializers/user_serializer.rb
+++ b/app/serializers/user_serializer.rb
@@ -14,6 +14,7 @@ def full(user, options = {})
type: user.type,
role: user.role_data,
pubsub_token: user.pubsub_token,
+ avatar_url: user.avatar_url,
created_at: user.created_at,
updated_at: user.updated_at,
ui_settings: user.ui_settings || {},
@@ -23,7 +24,8 @@ def full(user, options = {})
availability: user.availability,
confirmed: user.confirmed?,
confirmed_at: user.confirmed_at,
- custom_attributes: user.custom_attributes || {}
+ custom_attributes: user.custom_attributes || {},
+ setup_survey_completed: user.setup_survey_completed?
}
# Optional fields
diff --git a/app/services/setup_bootstrap_service.rb b/app/services/setup_bootstrap_service.rb
index 041acb5..80ebc8f 100644
--- a/app/services/setup_bootstrap_service.rb
+++ b/app/services/setup_bootstrap_service.rb
@@ -27,7 +27,8 @@ def call
user = create_user
assign_global_role(user)
- { user: user }
+ survey_token = generate_survey_token(user)
+ { user: user, survey_token: survey_token }
end
activate_licensing(result[:user])
@@ -96,6 +97,19 @@ def create_oauth_app
)
end
+ def generate_survey_token(user)
+ token = SecureRandom.hex(32)
+ redis_client.set("survey_token:#{token}", user.id, ex: 600) # 10 minutes TTL
+ token
+ rescue StandardError => e
+ Rails.logger.warn "[SetupBootstrap] Failed to generate survey token: #{e.message}"
+ nil
+ end
+
+ def redis_client
+ Redis.new(url: ENV.fetch('REDIS_URL', 'redis://localhost:6379/1'))
+ end
+
def activate_licensing(user)
store = Licensing::Store.new
instance_id = store.load_or_create_instance_id
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 47d346b..f3023ac 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -37,7 +37,7 @@
config.action_mailer.perform_caching = false
# Set localhost to be used by links generated in mailer templates.
- config.action_mailer.default_url_options = { host: "localhost", port: 3000 }
+ config.action_mailer.default_url_options = { host: "localhost", port: 3001 }
# Print deprecation notices to the Rails logger.
config.active_support.deprecation = :log
@@ -73,8 +73,11 @@
config.hosts << "host.docker.internal"
config.hosts << /.*\.docker\.internal/
- # Allow Docker Compose service hostnames
+ # Allow internal Docker service hostnames (used for inter-service calls)
config.hosts << "evo-auth"
+ config.hosts << "evo-crm"
+ config.hosts << "evo-core"
+ config.hosts << "evo-processor"
# Allow ngrok hosts for local development
config.hosts << "evo-auth-davidson.ngrok.app"
diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb
index 820927b..0e4929a 100644
--- a/config/initializers/cors.rb
+++ b/config/initializers/cors.rb
@@ -8,10 +8,16 @@
# Parse and clean origins (remove whitespace and filter empty strings)
cors_origins_raw = ENV.fetch('CORS_ORIGINS', 'http://localhost:3000,http://localhost:5173,http://localhost:8080,http://localhost:3001')
cors_origins = cors_origins_raw.split(',').map(&:strip).reject(&:empty?)
-
+
# Log CORS origins for debugging
Rails.logger.info "CORS Origins configured: #{cors_origins.inspect}"
+ # ActiveStorage blobs - allow all origins (public resources, served as images)
+ allow do
+ origins '*'
+ resource '/rails/active_storage/*', headers: :any, methods: [:get, :options, :head]
+ end
+
allow do
origins cors_origins
diff --git a/config/initializers/environment_config.rb b/config/initializers/environment_config.rb
index cc27c2a..21e295b 100644
--- a/config/initializers/environment_config.rb
+++ b/config/initializers/environment_config.rb
@@ -4,7 +4,29 @@
Rails.application.configure do
# Basic app configuration
Rails.application.config.app_name = 'Evo Auth Service'
- Rails.application.config.app_url = 'http://localhost:3001'
+ auth_service_url = ENV.fetch('AUTH_SERVICE_URL', 'http://localhost:3001')
+ Rails.application.config.app_url = auth_service_url
+
+ # Configure routes URL options so url_for in models generates absolute URLs.
+ # URI.parse accepts schemeless strings (e.g. "auth.example.com") without raising
+ # InvalidURIError — it returns scheme: nil, host: nil — so we validate explicitly.
+ begin
+ parsed = URI.parse(auth_service_url)
+ if parsed.scheme.nil? || !parsed.scheme.in?(%w[http https])
+ Rails.logger.warn "AUTH_SERVICE_URL must include scheme http(s):// — got #{auth_service_url.inspect}; default_url_options not configured"
+ elsif parsed.host.blank?
+ Rails.logger.warn "AUTH_SERVICE_URL has no host — got #{auth_service_url.inspect}; default_url_options not configured"
+ else
+ Rails.application.routes.default_url_options = {
+ host: parsed.host,
+ port: parsed.port,
+ protocol: parsed.scheme
+ }
+ end
+ rescue URI::InvalidURIError => e
+ Rails.logger.warn "AUTH_SERVICE_URL is not a valid URI: #{e.message}"
+ end
+
Rails.application.config.enable_account_signup = true
# MFA configuration with defaults
diff --git a/config/routes.rb b/config/routes.rb
index e0cb489..415afe6 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -118,6 +118,12 @@
resource :account, only: [:show, :update], controller: 'account'
+ # User Tours - tracks which guided tours each user has completed
+ resources :user_tours, only: [:index, :create, :destroy]
+
+ # Onboarding survey (authenticated — used when survey_token expired)
+ resource :setup_survey, only: [:show, :create], controller: 'setup_survey'
+
# User management
resources :users, only: [:index, :create, :update, :destroy] do
collection do
@@ -169,10 +175,11 @@
# License management routes
scope '/setup' do
- get '/status', to: 'setup#status'
- get '/register', to: 'setup#register'
+ get '/status', to: 'setup#status'
+ get '/register', to: 'setup#register'
get '/activate', to: 'setup#activate'
post '/bootstrap', to: 'setup#bootstrap'
+ post '/survey', to: 'setup#survey'
end
# Health check