mirror of
https://github.com/chatwoot/chatwoot.git
synced 2026-06-04 21:02:35 +08:00
# Pull Request Template ## Description This PR fixes the non-functional resend confirmation feature on the V3 login page where clicking "Resend confirmation" did nothing. The issue was caused by the V3 store not having the `resendConfirmation` action that the login page was trying to dispatch. **Key improvements:** - Fixed V3 store integration by importing `resendConfirmation` directly from auth API - Added comprehensive UX improvements with loading states and 60-second cooldown timer - Implemented environment-aware debug logging for development - Added proper error handling and user feedback - Enhanced backend test coverage **Context:** Users with unconfirmed accounts were unable to resend confirmation emails from the login page, creating a poor user experience and potential support burden. Fixes #3157 ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected) - [ ] This change requires a documentation update ## How Has This Been Tested? **Backend Testing:** - All existing resend_confirmation tests passing (7/7) - Added comprehensive new test suite in `spec/requests/api/v1/resend_confirmation_spec.rb` - API endpoint returns 200 OK responses in ~0.39 seconds - Email delivery confirmed via SMTP with test user `info@airbonar.com` **Frontend Testing:** - All frontend tests passing - ESLint compliant code with automatic corrections applied - Manual testing of login page functionality: - 60-second cooldown timer with countdown display - Error handling with user-friendly messages - Development logging works (console output in dev mode only) **Test Configuration:** - Ruby/Rails backend with RSpec test suite - Vue.js frontend with Jest/testing-library - Development environment with Gmail SMTP configured - Test user: unconfirmed account `info@airbonar.com` **Reproduction Steps:** 1. Navigate to login page with unconfirmed account 2. Click "Resend confirmation link" 3. Observe loading state, API call, and success feedback 4. Verify 60-second cooldown prevents spam 5. Check email delivery. ## Checklist: - [ ] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Sojan Jose <sojan@pepalo.com> Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com> Co-authored-by: Sony Mathew <sony@chatwoot.com>
120 lines
3.3 KiB
Ruby
120 lines
3.3 KiB
Ruby
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 render_create_error_not_confirmed
|
|
render_error(
|
|
:unauthorized,
|
|
I18n.t('devise_token_auth.sessions.not_confirmed', email: @resource.email),
|
|
error_code: 'user_not_confirmed'
|
|
)
|
|
end
|
|
|
|
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')
|