Restructure omni services and add Chatwoot research snapshot

This commit is contained in:
Ruslan Bakiev
2026-02-21 11:11:27 +07:00
parent edea7a0034
commit b73babbbf6
7732 changed files with 978203 additions and 32 deletions

View File

@@ -0,0 +1,34 @@
class DeviseOverrides::ConfirmationsController < Devise::ConfirmationsController
include AuthHelper
skip_before_action :require_no_authentication, raise: false
skip_before_action :authenticate_user!, raise: false
def create
@confirmable = User.find_by(confirmation_token: params[:confirmation_token])
render_confirmation_success and return if @confirmable&.confirm
render_confirmation_error
end
private
def render_confirmation_success
send_auth_headers(@confirmable)
render partial: 'devise/auth', formats: [:json], locals: { resource: @confirmable }
end
def render_confirmation_error
if @confirmable.blank?
render json: { message: 'Invalid token', redirect_url: '/' }, status: :unprocessable_entity
elsif @confirmable.confirmed_at
render json: { message: 'Already confirmed', redirect_url: '/' }, status: :unprocessable_entity
else
render json: { message: 'Failure', redirect_url: '/' }, status: :unprocessable_entity
end
end
def create_reset_token_link(user)
token = user.send(:set_reset_password_token)
"/app/auth/password/edit?config=default&redirect_url=&reset_password_token=#{token}"
end
end

View File

@@ -0,0 +1,90 @@
class DeviseOverrides::OmniauthCallbacksController < DeviseTokenAuth::OmniauthCallbacksController
include EmailHelper
def omniauth_success
get_resource_from_auth_hash
@resource.present? ? sign_in_user : sign_up_user
end
private
def sign_in_user
@resource.skip_confirmation! if confirmable_enabled?
# once the resource is found and verified
# we can just send them to the login page again with the SSO params
# that will log them in
encoded_email = ERB::Util.url_encode(@resource.email)
redirect_to login_page_url(email: encoded_email, sso_auth_token: @resource.generate_sso_auth_token)
end
def sign_in_user_on_mobile
@resource.skip_confirmation! if confirmable_enabled?
# once the resource is found and verified
# we can just send them to the login page again with the SSO params
# that will log them in
encoded_email = ERB::Util.url_encode(@resource.email)
params = { email: encoded_email, sso_auth_token: @resource.generate_sso_auth_token }.to_query
mobile_deep_link_base = GlobalConfigService.load('MOBILE_DEEP_LINK_BASE', 'chatwootapp')
redirect_to "#{mobile_deep_link_base}://auth/saml?#{params}", allow_other_host: true
end
def sign_up_user
return redirect_to login_page_url(error: 'no-account-found') unless account_signup_allowed?
return redirect_to login_page_url(error: 'business-account-only') unless validate_signup_email_is_business_domain?
create_account_for_user
token = @resource.send(:set_reset_password_token)
frontend_url = ENV.fetch('FRONTEND_URL', nil)
redirect_to "#{frontend_url}/app/auth/password/edit?config=default&reset_password_token=#{token}"
end
def login_page_url(error: nil, email: nil, sso_auth_token: nil)
frontend_url = ENV.fetch('FRONTEND_URL', nil)
params = { email: email, sso_auth_token: sso_auth_token }.compact
params[:error] = error if error.present?
"#{frontend_url}/app/login?#{params.to_query}"
end
def account_signup_allowed?
# set it to true by default, this is the behaviour across the app
GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false') != 'false'
end
def resource_class(_mapping = nil)
User
end
def get_resource_from_auth_hash # rubocop:disable Naming/AccessorMethodName
email = auth_hash.dig('info', 'email')
@resource = resource_class.from_email(email)
end
def validate_signup_email_is_business_domain?
# return true if the user is a business account, false if it is a blocked domain account
Account::SignUpEmailValidationService.new(auth_hash['info']['email']).perform
rescue CustomExceptions::Account::InvalidEmail
false
end
def create_account_for_user
@resource, @account = AccountBuilder.new(
account_name: extract_domain_without_tld(auth_hash['info']['email']),
user_full_name: auth_hash['info']['name'],
email: auth_hash['info']['email'],
locale: I18n.locale,
confirmed: auth_hash['info']['email_verified']
).perform
Avatar::AvatarFromUrlJob.perform_later(@resource, auth_hash['info']['image'])
end
def default_devise_mapping
'user'
end
end
DeviseOverrides::OmniauthCallbacksController.prepend_mod_with('DeviseOverrides::OmniauthCallbacksController')

View File

@@ -0,0 +1,44 @@
class DeviseOverrides::PasswordsController < Devise::PasswordsController
include AuthHelper
skip_before_action :require_no_authentication, raise: false
skip_before_action :authenticate_user!, raise: false
def create
@user = User.from_email(params[:email])
@user&.send_reset_password_instructions
build_response(I18n.t('messages.reset_password'), 200)
end
def update
# params: reset_password_token, password, password_confirmation
original_token = params[:reset_password_token]
reset_password_token = Devise.token_generator.digest(self, :reset_password_token, original_token)
@recoverable = User.find_by(reset_password_token: reset_password_token)
if @recoverable && reset_password_and_confirmation(@recoverable)
send_auth_headers(@recoverable)
render partial: 'devise/auth', formats: [:json], locals: { resource: @recoverable }
else
render json: { message: 'Invalid token', redirect_url: '/' }, status: :unprocessable_entity
end
end
private
def reset_password_and_confirmation(recoverable)
recoverable.confirm unless recoverable.confirmed? # confirm if user resets password without confirming anytime before
recoverable.reset_password(params[:password], params[:password_confirmation])
recoverable.reset_password_token = nil
recoverable.confirmation_token = nil
recoverable.reset_password_sent_at = nil
recoverable.save!
end
def build_response(message, status)
render json: {
message: message
}, status: status
end
end
DeviseOverrides::PasswordsController.prepend_mod_with('DeviseOverrides::PasswordsController')

View File

@@ -0,0 +1,111 @@
class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController
# Prevent session parameter from being passed
# Unpermitted parameter: session
wrap_parameters format: []
before_action :process_sso_auth_token, only: [:create]
def new
redirect_to login_page_url(error: 'access-denied')
end
def create
return handle_mfa_verification if mfa_verification_request?
return handle_sso_authentication if sso_authentication_request?
user = find_user_for_authentication
return handle_mfa_required(user) if user&.mfa_enabled?
# Only proceed with standard authentication if no MFA is required
super
end
def render_create_success
render partial: 'devise/auth', formats: [:json], locals: { resource: @resource }
end
private
def find_user_for_authentication
return nil unless params[:email].present? && params[:password].present?
normalized_email = params[:email].strip.downcase
user = User.from_email(normalized_email)
return nil unless user&.valid_password?(params[:password])
return nil unless user.active_for_authentication?
user
end
def mfa_verification_request?
params[:mfa_token].present?
end
def sso_authentication_request?
params[:sso_auth_token].present? && @resource.present?
end
def handle_sso_authentication
authenticate_resource_with_sso_token
yield @resource if block_given?
render_create_success
end
def login_page_url(error: nil)
frontend_url = ENV.fetch('FRONTEND_URL', nil)
"#{frontend_url}/app/login?error=#{error}"
end
def authenticate_resource_with_sso_token
@token = @resource.create_token
@resource.save!
sign_in(:user, @resource, store: false, bypass: false)
# invalidate the token after the user is signed in
@resource.invalidate_sso_auth_token(params[:sso_auth_token])
end
def process_sso_auth_token
return if params[:email].blank?
user = User.from_email(params[:email])
@resource = user if user&.valid_sso_auth_token?(params[:sso_auth_token])
end
def handle_mfa_required(user)
render json: {
mfa_required: true,
mfa_token: Mfa::TokenService.new(user: user).generate_token
}, status: :partial_content
end
def handle_mfa_verification
user = Mfa::TokenService.new(token: params[:mfa_token]).verify_token
return render_mfa_error('errors.mfa.invalid_token', :unauthorized) unless user
authenticated = Mfa::AuthenticationService.new(
user: user,
otp_code: params[:otp_code],
backup_code: params[:backup_code]
).authenticate
return render_mfa_error('errors.mfa.invalid_code') unless authenticated
sign_in_mfa_user(user)
end
def sign_in_mfa_user(user)
@resource = user
@token = @resource.create_token
@resource.save!
sign_in(:user, @resource, store: false, bypass: false)
render_create_success
end
def render_mfa_error(message_key, status = :bad_request)
render json: { error: I18n.t(message_key) }, status: status
end
end
DeviseOverrides::SessionsController.prepend_mod_with('DeviseOverrides::SessionsController')

View File

@@ -0,0 +1,10 @@
class DeviseOverrides::TokenValidationsController < DeviseTokenAuth::TokenValidationsController
def validate_token
# @resource will have been set by set_user_by_token concern
if @resource
render 'devise/token', formats: [:json]
else
render_validate_token_error
end
end
end