diff --git a/app/controllers/api/v1/profiles_controller.rb b/app/controllers/api/v1/profiles_controller.rb
index cbf801e82..a77652ff8 100644
--- a/app/controllers/api/v1/profiles_controller.rb
+++ b/app/controllers/api/v1/profiles_controller.rb
@@ -31,6 +31,11 @@ class Api::V1::ProfilesController < Api::BaseController
head :ok
end
+ def resend_confirmation
+ @user.send_confirmation_instructions unless @user.confirmed?
+ head :ok
+ end
+
private
def set_user
diff --git a/app/javascript/dashboard/App.vue b/app/javascript/dashboard/App.vue
index 6fd470fcf..49fe325f6 100644
--- a/app/javascript/dashboard/App.vue
+++ b/app/javascript/dashboard/App.vue
@@ -2,12 +2,13 @@
+
@@ -32,6 +33,7 @@ import NetworkNotification from './components/NetworkNotification.vue';
import UpdateBanner from './components/app/UpdateBanner.vue';
import UpgradeBanner from './components/app/UpgradeBanner.vue';
import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue';
+import PendingEmailVerificationBanner from './components/app/PendingEmailVerificationBanner.vue';
import vueActionCable from './helper/actionCable';
import WootSnackbarBox from './components/SnackbarContainer.vue';
import rtlMixin from 'shared/mixins/rtlMixin';
@@ -52,6 +54,7 @@ export default {
PaymentPendingBanner,
WootSnackbarBox,
UpgradeBanner,
+ PendingEmailVerificationBanner,
},
mixins: [rtlMixin],
diff --git a/app/javascript/dashboard/api/auth.js b/app/javascript/dashboard/api/auth.js
index 229e4f309..dde817866 100644
--- a/app/javascript/dashboard/api/auth.js
+++ b/app/javascript/dashboard/api/auth.js
@@ -98,4 +98,8 @@ export default {
},
});
},
+ resendConfirmation() {
+ const urlData = endPoints('resendConfirmation');
+ return axios.post(urlData.url);
+ },
};
diff --git a/app/javascript/dashboard/api/endPoints.js b/app/javascript/dashboard/api/endPoints.js
index 678386d50..31337b7fc 100644
--- a/app/javascript/dashboard/api/endPoints.js
+++ b/app/javascript/dashboard/api/endPoints.js
@@ -47,6 +47,10 @@ const endPoints = {
setActiveAccount: {
url: '/api/v1/profile/set_active_account',
},
+
+ resendConfirmation: {
+ url: '/api/v1/profile/resend_confirmation',
+ },
};
export default page => {
diff --git a/app/javascript/dashboard/components/app/PendingEmailVerificationBanner.vue b/app/javascript/dashboard/components/app/PendingEmailVerificationBanner.vue
new file mode 100644
index 000000000..c9dc51754
--- /dev/null
+++ b/app/javascript/dashboard/components/app/PendingEmailVerificationBanner.vue
@@ -0,0 +1,43 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components/ui/Banner.vue b/app/javascript/dashboard/components/ui/Banner.vue
index 5d680fe48..3473111ae 100644
--- a/app/javascript/dashboard/components/ui/Banner.vue
+++ b/app/javascript/dashboard/components/ui/Banner.vue
@@ -1,6 +1,6 @@
@@ -18,7 +18,7 @@
{
+ try {
+ await authAPI.resendConfirmation();
+ } catch (error) {
+ // Ignore error
+ }
+ },
};
// mutations
diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb
index 3c6b7611f..99974f4f7 100644
--- a/config/initializers/rack_attack.rb
+++ b/config/initializers/rack_attack.rb
@@ -94,6 +94,11 @@ class Rack::Attack
end
end
+ ## Resend confirmation throttling
+ throttle('resend_confirmation/ip', limit: 5, period: 30.minutes) do |req|
+ req.ip if req.path_without_extentions == '/api/v1/profile/resend_confirmation' && req.post?
+ end
+
## Prevent Brute-Force Signup Attacks ###
throttle('accounts/ip', limit: 5, period: 30.minutes) do |req|
req.ip if req.path_without_extentions == '/api/v1/accounts' && req.post?
diff --git a/config/routes.rb b/config/routes.rb
index 0c4b0459b..e0c636e0f 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -248,6 +248,7 @@ Rails.application.routes.draw do
post :availability
post :auto_offline
put :set_active_account
+ post :resend_confirmation
end
end
diff --git a/spec/controllers/api/v1/profiles_controller_spec.rb b/spec/controllers/api/v1/profiles_controller_spec.rb
index 4feb359c6..80f6833f1 100644
--- a/spec/controllers/api/v1/profiles_controller_spec.rb
+++ b/spec/controllers/api/v1/profiles_controller_spec.rb
@@ -242,4 +242,44 @@ RSpec.describe 'Profile API', type: :request do
end
end
end
+
+ describe 'POST /api/v1/profile/resend_confirmation' do
+ context 'when it is an unauthenticated user' do
+ it 'returns unauthorized' do
+ post '/api/v1/profile/resend_confirmation'
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+
+ context 'when it is an authenticated user' do
+ let(:agent) do
+ create(:user, password: 'Test123!', email: 'test-unconfirmed@email.com', account: account, role: :agent,
+ unconfirmed_email: 'test-unconfirmed@email.com')
+ end
+
+ it 'does not send the confirmation email if the user is already confirmed' do
+ expect do
+ post '/api/v1/profile/resend_confirmation',
+ headers: agent.create_new_auth_token,
+ as: :json
+ end.not_to have_enqueued_mail(Devise::Mailer, :confirmation_instructions)
+
+ expect(response).to have_http_status(:success)
+ end
+
+ it 'resends the confirmation email if the user is unconfirmed' do
+ agent.confirmed_at = nil
+ agent.save!
+
+ expect do
+ post '/api/v1/profile/resend_confirmation',
+ headers: agent.create_new_auth_token,
+ as: :json
+ end.to have_enqueued_mail(Devise::Mailer, :confirmation_instructions)
+
+ expect(response).to have_http_status(:success)
+ end
+ end
+ end
end