mirror of
https://github.com/lingble/chatwoot.git
synced 2025-10-30 18:47:51 +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