mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 02:57:57 +00:00 
			
		
		
		
	feat: Show a confirmation banner if the email is not verified (#8808)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
		| @@ -31,6 +31,11 @@ class Api::V1::ProfilesController < Api::BaseController | |||||||
|     head :ok |     head :ok | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  |   def resend_confirmation | ||||||
|  |     @user.send_confirmation_instructions unless @user.confirmed? | ||||||
|  |     head :ok | ||||||
|  |   end | ||||||
|  |  | ||||||
|   private |   private | ||||||
|  |  | ||||||
|   def set_user |   def set_user | ||||||
|   | |||||||
| @@ -2,12 +2,13 @@ | |||||||
|   <div |   <div | ||||||
|     v-if="!authUIFlags.isFetching && !accountUIFlags.isFetchingItem" |     v-if="!authUIFlags.isFetching && !accountUIFlags.isFetchingItem" | ||||||
|     id="app" |     id="app" | ||||||
|     class="app-wrapper h-full flex-grow-0 min-h-0 w-full" |     class="flex-grow-0 w-full h-full min-h-0 app-wrapper" | ||||||
|     :class="{ 'app-rtl--wrapper': isRTLView }" |     :class="{ 'app-rtl--wrapper': isRTLView }" | ||||||
|     :dir="isRTLView ? 'rtl' : 'ltr'" |     :dir="isRTLView ? 'rtl' : 'ltr'" | ||||||
|   > |   > | ||||||
|     <update-banner :latest-chatwoot-version="latestChatwootVersion" /> |     <update-banner :latest-chatwoot-version="latestChatwootVersion" /> | ||||||
|     <template v-if="currentAccountId"> |     <template v-if="currentAccountId"> | ||||||
|  |       <pending-email-verification-banner /> | ||||||
|       <payment-pending-banner /> |       <payment-pending-banner /> | ||||||
|       <upgrade-banner /> |       <upgrade-banner /> | ||||||
|     </template> |     </template> | ||||||
| @@ -32,6 +33,7 @@ import NetworkNotification from './components/NetworkNotification.vue'; | |||||||
| import UpdateBanner from './components/app/UpdateBanner.vue'; | import UpdateBanner from './components/app/UpdateBanner.vue'; | ||||||
| import UpgradeBanner from './components/app/UpgradeBanner.vue'; | import UpgradeBanner from './components/app/UpgradeBanner.vue'; | ||||||
| import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue'; | import PaymentPendingBanner from './components/app/PaymentPendingBanner.vue'; | ||||||
|  | import PendingEmailVerificationBanner from './components/app/PendingEmailVerificationBanner.vue'; | ||||||
| import vueActionCable from './helper/actionCable'; | import vueActionCable from './helper/actionCable'; | ||||||
| import WootSnackbarBox from './components/SnackbarContainer.vue'; | import WootSnackbarBox from './components/SnackbarContainer.vue'; | ||||||
| import rtlMixin from 'shared/mixins/rtlMixin'; | import rtlMixin from 'shared/mixins/rtlMixin'; | ||||||
| @@ -52,6 +54,7 @@ export default { | |||||||
|     PaymentPendingBanner, |     PaymentPendingBanner, | ||||||
|     WootSnackbarBox, |     WootSnackbarBox, | ||||||
|     UpgradeBanner, |     UpgradeBanner, | ||||||
|  |     PendingEmailVerificationBanner, | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   mixins: [rtlMixin], |   mixins: [rtlMixin], | ||||||
|   | |||||||
| @@ -98,4 +98,8 @@ export default { | |||||||
|       }, |       }, | ||||||
|     }); |     }); | ||||||
|   }, |   }, | ||||||
|  |   resendConfirmation() { | ||||||
|  |     const urlData = endPoints('resendConfirmation'); | ||||||
|  |     return axios.post(urlData.url); | ||||||
|  |   }, | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -47,6 +47,10 @@ const endPoints = { | |||||||
|   setActiveAccount: { |   setActiveAccount: { | ||||||
|     url: '/api/v1/profile/set_active_account', |     url: '/api/v1/profile/set_active_account', | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|  |   resendConfirmation: { | ||||||
|  |     url: '/api/v1/profile/resend_confirmation', | ||||||
|  |   }, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export default page => { | export default page => { | ||||||
|   | |||||||
| @@ -0,0 +1,43 @@ | |||||||
|  | <template> | ||||||
|  |   <banner | ||||||
|  |     v-if="shouldShowBanner" | ||||||
|  |     color-scheme="alert" | ||||||
|  |     :banner-message="bannerMessage" | ||||||
|  |     :action-button-label="actionButtonMessage" | ||||||
|  |     action-button-icon="mail" | ||||||
|  |     has-action-button | ||||||
|  |     @click="resendVerificationEmail" | ||||||
|  |   /> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | import Banner from 'dashboard/components/ui/Banner.vue'; | ||||||
|  | import { mapGetters } from 'vuex'; | ||||||
|  | import accountMixin from 'dashboard/mixins/account'; | ||||||
|  | import alertMixin from 'shared/mixins/alertMixin'; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   components: { Banner }, | ||||||
|  |   mixins: [accountMixin, alertMixin], | ||||||
|  |   computed: { | ||||||
|  |     ...mapGetters({ | ||||||
|  |       currentUser: 'getCurrentUser', | ||||||
|  |     }), | ||||||
|  |     bannerMessage() { | ||||||
|  |       return this.$t('APP_GLOBAL.EMAIL_VERIFICATION_PENDING'); | ||||||
|  |     }, | ||||||
|  |     actionButtonMessage() { | ||||||
|  |       return this.$t('APP_GLOBAL.RESEND_VERIFICATION_MAIL'); | ||||||
|  |     }, | ||||||
|  |     shouldShowBanner() { | ||||||
|  |       return !this.currentUser.confirmed; | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     resendVerificationEmail() { | ||||||
|  |       this.$store.dispatch('resendConfirmation'); | ||||||
|  |       this.showAlert(this.$t('APP_GLOBAL.EMAIL_VERIFICATION_SENT')); | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  | </script> | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| <template> | <template> | ||||||
|   <div |   <div | ||||||
|     class="banner flex items-center h-12 gap-4 text-white dark:text-white text-xs py-3 px-4 justify-center" |     class="flex items-center justify-center h-12 gap-4 px-4 py-3 text-xs text-white banner dark:text-white" | ||||||
|     :class="bannerClasses" |     :class="bannerClasses" | ||||||
|   > |   > | ||||||
|     <span class="banner-message"> |     <span class="banner-message"> | ||||||
| @@ -18,7 +18,7 @@ | |||||||
|       <woot-button |       <woot-button | ||||||
|         v-if="hasActionButton" |         v-if="hasActionButton" | ||||||
|         size="tiny" |         size="tiny" | ||||||
|         icon="arrow-right" |         :icon="actionButtonIcon" | ||||||
|         :variant="actionButtonVariant" |         :variant="actionButtonVariant" | ||||||
|         color-scheme="primary" |         color-scheme="primary" | ||||||
|         class-names="banner-action__button" |         class-names="banner-action__button" | ||||||
| @@ -67,6 +67,10 @@ export default { | |||||||
|       type: String, |       type: String, | ||||||
|       default: '', |       default: '', | ||||||
|     }, |     }, | ||||||
|  |     actionButtonIcon: { | ||||||
|  |       type: String, | ||||||
|  |       default: 'arrow-right', | ||||||
|  |     }, | ||||||
|     colorScheme: { |     colorScheme: { | ||||||
|       type: String, |       type: String, | ||||||
|       default: '', |       default: '', | ||||||
|   | |||||||
| @@ -156,6 +156,9 @@ | |||||||
|     "TRIAL_MESSAGE": "days trial remaining.", |     "TRIAL_MESSAGE": "days trial remaining.", | ||||||
|     "TRAIL_BUTTON": "Buy Now", |     "TRAIL_BUTTON": "Buy Now", | ||||||
|     "DELETED_USER": "Deleted User", |     "DELETED_USER": "Deleted User", | ||||||
|  |     "EMAIL_VERIFICATION_PENDING": "It seems that you haven't verified your email address yet. Please check your inbox for the verification email.", | ||||||
|  |     "RESEND_VERIFICATION_MAIL": "Resend verification email", | ||||||
|  |     "EMAIL_VERIFICATION_SENT": "Verification email has been sent. Please check your inbox.", | ||||||
|     "ACCOUNT_SUSPENDED": { |     "ACCOUNT_SUSPENDED": { | ||||||
|       "TITLE": "Account Suspended", |       "TITLE": "Account Suspended", | ||||||
|       "MESSAGE": "Your account is suspended. Please reach out to the support team for more information." |       "MESSAGE": "Your account is suspended. Please reach out to the support team for more information." | ||||||
|   | |||||||
| @@ -181,6 +181,14 @@ export const actions = { | |||||||
|       // Ignore error |       // Ignore error | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|  |   resendConfirmation: async () => { | ||||||
|  |     try { | ||||||
|  |       await authAPI.resendConfirmation(); | ||||||
|  |     } catch (error) { | ||||||
|  |       // Ignore error | ||||||
|  |     } | ||||||
|  |   }, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| // mutations | // mutations | ||||||
|   | |||||||
| @@ -94,6 +94,11 @@ class Rack::Attack | |||||||
|     end |     end | ||||||
|   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 ### |   ## Prevent Brute-Force Signup Attacks ### | ||||||
|   throttle('accounts/ip', limit: 5, period: 30.minutes) do |req| |   throttle('accounts/ip', limit: 5, period: 30.minutes) do |req| | ||||||
|     req.ip if req.path_without_extentions == '/api/v1/accounts' && req.post? |     req.ip if req.path_without_extentions == '/api/v1/accounts' && req.post? | ||||||
|   | |||||||
| @@ -248,6 +248,7 @@ Rails.application.routes.draw do | |||||||
|           post :availability |           post :availability | ||||||
|           post :auto_offline |           post :auto_offline | ||||||
|           put :set_active_account |           put :set_active_account | ||||||
|  |           post :resend_confirmation | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
|  |  | ||||||
|   | |||||||
| @@ -242,4 +242,44 @@ RSpec.describe 'Profile API', type: :request do | |||||||
|       end |       end | ||||||
|     end |     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 | end | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Shivam Mishra
					Shivam Mishra