Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
</p>

<p align="center">
<a href="https://github.com/EvolutionAPI/evo-auth-service-community/releases/latest"><img src="https://img.shields.io/github/v/release/EvolutionAPI/evo-auth-service-community?include_prereleases&label=version&color=00ffa7" alt="Latest version" /></a>
<a href="https://github.com/evolution-foundation/evo-auth-service-community/releases/latest"><img src="https://img.shields.io/github/v/release/evolution-foundation/evo-auth-service-community?include_prereleases&label=version&color=00ffa7" alt="Latest version" /></a>
<a href="https://opensource.org/licenses/Apache-2.0"><img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="License: Apache 2.0" /></a>
<a href="https://docs.evolutionfoundation.com.br"><img src="https://img.shields.io/badge/Docs-evolutionfoundation.com.br-00ffa7" alt="Documentation" /></a>
<a href="https://evolutionfoundation.com.br/community"><img src="https://img.shields.io/badge/Community-Join%20us-white" alt="Community" /></a>
Expand All @@ -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 [email protected]:EvolutionAPI/evo-crm-community.git
git clone --recurse-submodules [email protected]: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`.
Expand Down Expand Up @@ -80,7 +80,7 @@ The Community Edition is **single-tenant** by design — one account, no multi-t
### Installation

```bash
git clone [email protected]:EvolutionAPI/evo-auth-service-community.git
git clone [email protected]:evolution-foundation/evo-auth-service-community.git
cd evo-auth-service-community

# Install dependencies
Expand Down
2 changes: 2 additions & 0 deletions app/controllers/api/v1/profiles_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions app/controllers/api/v1/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 41 additions & 1 deletion app/controllers/setup_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
16 changes: 13 additions & 3 deletions app/models/concerns/avatarable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions app/models/setup_survey_response.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class SetupSurveyResponse < ApplicationRecord
belongs_to :user
end
6 changes: 6 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion app/serializers/serializers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion app/serializers/user_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 || {},
Expand All @@ -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
Expand Down
16 changes: 15 additions & 1 deletion app/services/setup_bootstrap_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions config/environments/development.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
8 changes: 7 additions & 1 deletion config/initializers/cors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
24 changes: 23 additions & 1 deletion config/initializers/environment_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down