fix: Implement resend confirmation feature for login page (#11970)

# 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>
This commit is contained in:
Cesar Garcia
2026-05-07 15:13:04 +05:30
committed by GitHub
co-authored by Sivin Varghese Sojan Jose Sony Mathew Sony Mathew
parent 2435d7503c
commit 7a7db22a43
4 changed files with 42 additions and 4 deletions
@@ -25,6 +25,14 @@ class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController
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?
+4 -2
View File
@@ -2,6 +2,7 @@ import {
setAuthCredentials,
throwErrorMessage,
clearLocalStorageOnLogout,
parseAPIErrorResponse,
} from 'dashboard/store/utils/api';
import wootAPI from './apiClient';
import {
@@ -42,8 +43,9 @@ export const login = async ({
mfaToken: error.response.data.mfa_token,
};
}
throwErrorMessage(error);
return null;
const loginError = new Error(parseAPIErrorResponse(error));
loginError.errorCode = error.response?.data?.error_code;
throw loginError;
}
};
+10
View File
@@ -26,6 +26,7 @@ const ERROR_MESSAGES = {
};
const IMPERSONATION_URL_SEARCH_KEY = 'impersonation';
const USER_NOT_CONFIRMED_ERROR_CODE = 'user_not_confirmed';
export default {
components: {
@@ -185,6 +186,15 @@ export default {
this.showAlertMessage(this.$t('LOGIN.API.SUCCESS_MESSAGE'));
})
.catch(response => {
if (response?.errorCode === USER_NOT_CONFIRMED_ERROR_CODE) {
this.loginApi.showLoading = false;
this.$router.push({
name: 'auth_verify_email',
state: { email: credentials.email },
});
return;
}
// Reset URL Params if the authentication is invalid
if (this.email) {
window.location = '/app/login';
@@ -16,6 +16,22 @@ RSpec.describe 'Session', type: :request do
end
end
context 'when the user is unconfirmed' do
let!(:user) { create(:user, password: 'Password1!', account: account, skip_confirmation: false) }
it 'returns an unconfirmed user error code' do
params = { email: user.email, password: 'Password1!' }
post new_user_session_url,
params: params,
as: :json
expect(response).to have_http_status(:unauthorized)
expect(response.parsed_body['error_code']).to eq('user_not_confirmed')
expect(response.parsed_body['errors'].first).to include(user.email)
end
end
context 'when it is valid credentials' do
let!(:user) { create(:user, password: 'Password1!', account: account) }
let!(:user_with_new_pwd) { create(:user, password: 'Password1!.><?', account: account) }
@@ -88,9 +104,11 @@ RSpec.describe 'Session', type: :request do
describe 'GET /auth/sign_in' do
it 'redirects to the frontend login page with error' do
get new_user_session_url
with_modified_env FRONTEND_URL: '' do
get new_user_session_url
expect(response).to redirect_to(%r{/app/login\?error=access-denied$})
expect(response).to redirect_to(%r{/app/login\?error=access-denied$})
end
end
end
end