feat: Add hCaptcha for public forms (#4017)
- added hCaptcha based verification for chatwoot signups Co-authored-by: Sojan <sojan@pepalo.com>
This commit is contained in:
parent
80d83b401c
commit
ea44a32758
@ -4,6 +4,7 @@ class Api::V1::AccountsController < Api::BaseController
|
|||||||
skip_before_action :authenticate_user!, :set_current_user, :handle_with_exception,
|
skip_before_action :authenticate_user!, :set_current_user, :handle_with_exception,
|
||||||
only: [:create], raise: false
|
only: [:create], raise: false
|
||||||
before_action :check_signup_enabled, only: [:create]
|
before_action :check_signup_enabled, only: [:create]
|
||||||
|
before_action :validate_captcha, only: [:create]
|
||||||
before_action :fetch_account, except: [:create]
|
before_action :fetch_account, except: [:create]
|
||||||
before_action :check_authorization, except: [:create]
|
before_action :check_authorization, except: [:create]
|
||||||
|
|
||||||
@ -58,6 +59,10 @@ class Api::V1::AccountsController < Api::BaseController
|
|||||||
raise ActionController::RoutingError, 'Not Found' if GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false') == 'false'
|
raise ActionController::RoutingError, 'Not Found' if GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false') == 'false'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def validate_captcha
|
||||||
|
raise ActionController::InvalidAuthenticityToken, 'Invalid Captcha' unless ChatwootCaptcha.new(params[:h_captcha_client_response]).valid?
|
||||||
|
end
|
||||||
|
|
||||||
def pundit_user
|
def pundit_user
|
||||||
{
|
{
|
||||||
user: current_user,
|
user: current_user,
|
||||||
|
|||||||
@ -26,7 +26,8 @@ class DashboardController < ActionController::Base
|
|||||||
'API_CHANNEL_THUMBNAIL',
|
'API_CHANNEL_THUMBNAIL',
|
||||||
'ANALYTICS_TOKEN',
|
'ANALYTICS_TOKEN',
|
||||||
'ANALYTICS_HOST',
|
'ANALYTICS_HOST',
|
||||||
'DIRECT_UPLOADS_ENABLED'
|
'DIRECT_UPLOADS_ENABLED',
|
||||||
|
'HCAPTCHA_SITE_KEY'
|
||||||
).merge(app_config)
|
).merge(app_config)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -30,6 +30,7 @@ export default {
|
|||||||
user_full_name: creds.fullName.trim(),
|
user_full_name: creds.fullName.trim(),
|
||||||
email: creds.email,
|
email: creds.email,
|
||||||
password: creds.password,
|
password: creds.password,
|
||||||
|
h_captcha_client_response: creds.hCaptchaClientResponse,
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
setAuthCredentials(response);
|
setAuthCredentials(response);
|
||||||
|
|||||||
@ -75,8 +75,14 @@
|
|||||||
"
|
"
|
||||||
@blur="$v.credentials.confirmPassword.$touch"
|
@blur="$v.credentials.confirmPassword.$touch"
|
||||||
/>
|
/>
|
||||||
|
<div v-if="globalConfig.hCaptchaSiteKey" class="h-captcha--box">
|
||||||
|
<vue-hcaptcha
|
||||||
|
:sitekey="globalConfig.hCaptchaSiteKey"
|
||||||
|
@verify="onRecaptchaVerified"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<woot-submit-button
|
<woot-submit-button
|
||||||
:disabled="isSignupInProgress"
|
:disabled="isSignupInProgress || !hasAValidCaptcha"
|
||||||
:button-text="$t('REGISTER.SUBMIT')"
|
:button-text="$t('REGISTER.SUBMIT')"
|
||||||
:loading="isSignupInProgress"
|
:loading="isSignupInProgress"
|
||||||
button-class="large expanded"
|
button-class="large expanded"
|
||||||
@ -107,8 +113,11 @@ import { mapGetters } from 'vuex';
|
|||||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
import { DEFAULT_REDIRECT_URL } from '../../constants';
|
import { DEFAULT_REDIRECT_URL } from '../../constants';
|
||||||
|
import VueHcaptcha from '@hcaptcha/vue-hcaptcha';
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
VueHcaptcha,
|
||||||
|
},
|
||||||
mixins: [globalConfigMixin, alertMixin],
|
mixins: [globalConfigMixin, alertMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -118,6 +127,7 @@ export default {
|
|||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
confirmPassword: '',
|
confirmPassword: '',
|
||||||
|
hCaptchaClientResponse: '',
|
||||||
},
|
},
|
||||||
isSignupInProgress: false,
|
isSignupInProgress: false,
|
||||||
error: '',
|
error: '',
|
||||||
@ -153,9 +163,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({ globalConfig: 'globalConfig/get' }),
|
||||||
globalConfig: 'globalConfig/get',
|
|
||||||
}),
|
|
||||||
termsLink() {
|
termsLink() {
|
||||||
return this.$t('REGISTER.TERMS_ACCEPT')
|
return this.$t('REGISTER.TERMS_ACCEPT')
|
||||||
.replace('https://www.chatwoot.com/terms', this.globalConfig.termsURL)
|
.replace('https://www.chatwoot.com/terms', this.globalConfig.termsURL)
|
||||||
@ -164,6 +172,12 @@ export default {
|
|||||||
this.globalConfig.privacyURL
|
this.globalConfig.privacyURL
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
hasAValidCaptcha() {
|
||||||
|
if (this.globalConfig.hCaptchaSiteKey) {
|
||||||
|
return !!this.credentials.hCaptchaClientResponse;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async submit() {
|
async submit() {
|
||||||
@ -187,6 +201,9 @@ export default {
|
|||||||
this.isSignupInProgress = false;
|
this.isSignupInProgress = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onRecaptchaVerified(token) {
|
||||||
|
this.credentials.hCaptchaClientResponse = token;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@ -234,5 +251,9 @@ export default {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
margin: var(--space-normal) 0 0 0;
|
margin: var(--space-normal) 0 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.h-captcha--box {
|
||||||
|
margin-bottom: var(--space-one);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -7,6 +7,7 @@ const {
|
|||||||
CREATE_NEW_ACCOUNT_FROM_DASHBOARD: createNewAccountFromDashboard,
|
CREATE_NEW_ACCOUNT_FROM_DASHBOARD: createNewAccountFromDashboard,
|
||||||
DIRECT_UPLOADS_ENABLED: directUploadsEnabled,
|
DIRECT_UPLOADS_ENABLED: directUploadsEnabled,
|
||||||
DISPLAY_MANIFEST: displayManifest,
|
DISPLAY_MANIFEST: displayManifest,
|
||||||
|
HCAPTCHA_SITE_KEY: hCaptchaSiteKey,
|
||||||
INSTALLATION_NAME: installationName,
|
INSTALLATION_NAME: installationName,
|
||||||
LOGO_THUMBNAIL: logoThumbnail,
|
LOGO_THUMBNAIL: logoThumbnail,
|
||||||
LOGO: logo,
|
LOGO: logo,
|
||||||
@ -24,6 +25,7 @@ const state = {
|
|||||||
createNewAccountFromDashboard,
|
createNewAccountFromDashboard,
|
||||||
directUploadsEnabled: directUploadsEnabled === 'true',
|
directUploadsEnabled: directUploadsEnabled === 'true',
|
||||||
displayManifest,
|
displayManifest,
|
||||||
|
hCaptchaSiteKey,
|
||||||
installationName,
|
installationName,
|
||||||
logo,
|
logo,
|
||||||
logoThumbnail,
|
logoThumbnail,
|
||||||
|
|||||||
@ -47,3 +47,9 @@
|
|||||||
- name: DIRECT_UPLOADS_ENABLED
|
- name: DIRECT_UPLOADS_ENABLED
|
||||||
value: false
|
value: false
|
||||||
locked: false
|
locked: false
|
||||||
|
- name: HCAPTCHA_SITE_KEY
|
||||||
|
value:
|
||||||
|
locked: false
|
||||||
|
- name: HCAPTCHA_SERVER_KEY
|
||||||
|
value:
|
||||||
|
locked: false
|
||||||
|
|||||||
5
db/migrate/20220218120357_add_h_captcha_key.rb
Normal file
5
db/migrate/20220218120357_add_h_captcha_key.rb
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
class AddHCaptchaKey < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
ConfigLoader.new.process
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2022_02_15_060751) do
|
ActiveRecord::Schema.define(version: 2022_02_18_120357) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pg_stat_statements"
|
enable_extension "pg_stat_statements"
|
||||||
|
|||||||
25
lib/chatwoot_captcha.rb
Normal file
25
lib/chatwoot_captcha.rb
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
class ChatwootCaptcha
|
||||||
|
def initialize(client_response)
|
||||||
|
@client_response = client_response
|
||||||
|
@server_key = GlobalConfigService.load('HCAPTCHA_SERVER_KEY', '')
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid?
|
||||||
|
return true if @server_key.blank?
|
||||||
|
return false if @client_response.blank?
|
||||||
|
|
||||||
|
validate_client_response?
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_client_response?
|
||||||
|
response = HTTParty.post('https://hcaptcha.com/siteverify',
|
||||||
|
body: {
|
||||||
|
response: @client_response,
|
||||||
|
secret: @server_key
|
||||||
|
})
|
||||||
|
|
||||||
|
return unless response.success?
|
||||||
|
|
||||||
|
response.parsed_response['success']
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -20,6 +20,7 @@
|
|||||||
"@braid/vue-formulate": "^2.5.2",
|
"@braid/vue-formulate": "^2.5.2",
|
||||||
"@chatwoot/prosemirror-schema": "https://github.com/chatwoot/prosemirror-schema.git#7e8acadd10d7b932c0dc0bd0a18f804434f83517",
|
"@chatwoot/prosemirror-schema": "https://github.com/chatwoot/prosemirror-schema.git#7e8acadd10d7b932c0dc0bd0a18f804434f83517",
|
||||||
"@chatwoot/utils": "^0.0.3",
|
"@chatwoot/utils": "^0.0.3",
|
||||||
|
"@hcaptcha/vue-hcaptcha": "^0.3.2",
|
||||||
"@rails/actioncable": "6.1.3",
|
"@rails/actioncable": "6.1.3",
|
||||||
"@rails/webpacker": "5.3.0",
|
"@rails/webpacker": "5.3.0",
|
||||||
"@sentry/tracing": "^6.4.1",
|
"@sentry/tracing": "^6.4.1",
|
||||||
|
|||||||
@ -30,6 +30,25 @@ RSpec.describe 'Accounts API', type: :request do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'calls ChatwootCaptcha' do
|
||||||
|
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
|
||||||
|
captcha = double
|
||||||
|
allow(account_builder).to receive(:perform).and_return([user, account])
|
||||||
|
allow(ChatwootCaptcha).to receive(:new).and_return(captcha)
|
||||||
|
allow(captcha).to receive(:valid?).and_return(true)
|
||||||
|
|
||||||
|
params = { account_name: 'test', email: email, user: nil, user_full_name: user_full_name, password: 'Password1!',
|
||||||
|
h_captcha_client_response: '123' }
|
||||||
|
|
||||||
|
post api_v1_accounts_url,
|
||||||
|
params: params,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(ChatwootCaptcha).to have_received(:new).with('123')
|
||||||
|
expect(response.headers.keys).to include('access-token', 'token-type', 'client', 'expiry', 'uid')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it 'renders error response on invalid params' do
|
it 'renders error response on invalid params' do
|
||||||
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
|
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
|
||||||
allow(account_builder).to receive(:perform).and_return(nil)
|
allow(account_builder).to receive(:perform).and_return(nil)
|
||||||
|
|||||||
25
spec/lib/chatwoot_captcha_spec.rb
Normal file
25
spec/lib/chatwoot_captcha_spec.rb
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe ChatwootCaptcha do
|
||||||
|
it 'returns true if HCAPTCHA SERVER KEY is absent' do
|
||||||
|
expect(described_class.new('random_key').valid?).to eq(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when HCAPTCHA SERVER KEY is present' do
|
||||||
|
before do
|
||||||
|
create(:installation_config, { name: 'HCAPTCHA_SERVER_KEY', value: 'hcaptch_server_key' })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false if client response is blank' do
|
||||||
|
expect(described_class.new('').valid?).to eq false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true if client response is valid' do
|
||||||
|
captcha_request = double
|
||||||
|
allow(HTTParty).to receive(:post).and_return(captcha_request)
|
||||||
|
allow(captcha_request).to receive(:success?).and_return(true)
|
||||||
|
allow(captcha_request).to receive(:parsed_response).and_return({ 'success' => true })
|
||||||
|
expect(described_class.new('valid_response').valid?).to eq true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -1283,6 +1283,11 @@
|
|||||||
postcss "7.0.32"
|
postcss "7.0.32"
|
||||||
purgecss "^2.3.0"
|
purgecss "^2.3.0"
|
||||||
|
|
||||||
|
"@hcaptcha/vue-hcaptcha@^0.3.2":
|
||||||
|
version "0.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@hcaptcha/vue-hcaptcha/-/vue-hcaptcha-0.3.2.tgz#0f77d6fc19bc47eadb6b2181eee5fc132441a942"
|
||||||
|
integrity sha512-JiJsAJh+fSe+uf9N3ek7CKzX/r79+hx+rMPch+e2/h9+Ei3VyJtb2Dgk1DhG/dyUdrooPIzkNMr6gfo75Cn22g==
|
||||||
|
|
||||||
"@istanbuljs/load-nyc-config@^1.0.0":
|
"@istanbuljs/load-nyc-config@^1.0.0":
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
|
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user