Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 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 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`.
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 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
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
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