diff options
Diffstat (limited to 'app/assets/javascripts/captcha')
4 files changed, 109 insertions, 1 deletions
diff --git a/app/assets/javascripts/captcha/captcha_modal.vue b/app/assets/javascripts/captcha/captcha_modal.vue index e6c73bc9643..a98a52a3130 100644 --- a/app/assets/javascripts/captcha/captcha_modal.vue +++ b/app/assets/javascripts/captcha/captcha_modal.vue @@ -41,10 +41,17 @@ export default { } }, }, + mounted() { + // If this is true, we need to present the captcha modal to the user. + // When the modal is shown we will also initialize and render the form. + if (this.needsCaptchaResponse) { + this.$refs.modal.show(); + } + }, methods: { emitReceivedCaptchaResponse(captchaResponse) { - this.$emit('receivedCaptchaResponse', captchaResponse); this.$refs.modal.hide(); + this.$emit('receivedCaptchaResponse', captchaResponse); }, emitNullReceivedCaptchaResponse() { this.emitReceivedCaptchaResponse(null); @@ -103,6 +110,7 @@ export default { :action-cancel="{ text: __('Cancel') }" @shown="shown" @hide="hide" + @hidden="$emit('hidden')" > <div ref="captcha"></div> <p>{{ __('We want to be sure it is you, please confirm you are not a robot.') }}</p> diff --git a/app/assets/javascripts/captcha/captcha_modal_axios_interceptor.js b/app/assets/javascripts/captcha/captcha_modal_axios_interceptor.js new file mode 100644 index 00000000000..c9eac44eb28 --- /dev/null +++ b/app/assets/javascripts/captcha/captcha_modal_axios_interceptor.js @@ -0,0 +1,37 @@ +const supportedMethods = ['patch', 'post', 'put']; + +export function registerCaptchaModalInterceptor(axios) { + return axios.interceptors.response.use( + (response) => { + return response; + }, + (err) => { + if ( + supportedMethods.includes(err?.config?.method) && + err?.response?.data?.needs_captcha_response + ) { + const { data } = err.response; + const captchaSiteKey = data.captcha_site_key; + const spamLogId = data.spam_log_id; + // eslint-disable-next-line promise/no-promise-in-callback + return import('~/captcha/wait_for_captcha_to_be_solved') + .then(({ waitForCaptchaToBeSolved }) => waitForCaptchaToBeSolved(captchaSiteKey)) + .then((captchaResponse) => { + const errConfig = err.config; + const originalData = JSON.parse(errConfig.data); + return axios({ + method: errConfig.method, + url: errConfig.url, + data: { + ...originalData, + captcha_response: captchaResponse, + spam_log_id: spamLogId, + }, + }); + }); + } + + return Promise.reject(err); + }, + ); +} diff --git a/app/assets/javascripts/captcha/unsolved_captcha_error.js b/app/assets/javascripts/captcha/unsolved_captcha_error.js new file mode 100644 index 00000000000..1e5c2a4d852 --- /dev/null +++ b/app/assets/javascripts/captcha/unsolved_captcha_error.js @@ -0,0 +1,10 @@ +import { __ } from '~/locale'; + +class UnsolvedCaptchaError extends Error { + constructor(message) { + super(message || __('You must solve the CAPTCHA in order to submit')); + this.name = 'UnsolvedCaptchaError'; + } +} + +export default UnsolvedCaptchaError; diff --git a/app/assets/javascripts/captcha/wait_for_captcha_to_be_solved.js b/app/assets/javascripts/captcha/wait_for_captcha_to_be_solved.js new file mode 100644 index 00000000000..0fd0f571d3b --- /dev/null +++ b/app/assets/javascripts/captcha/wait_for_captcha_to_be_solved.js @@ -0,0 +1,53 @@ +import Vue from 'vue'; +import CaptchaModal from '~/captcha/captcha_modal.vue'; +import UnsolvedCaptchaError from '~/captcha/unsolved_captcha_error'; + +/** + * Opens a Captcha Modal with provided captchaSiteKey. + * + * Returns a Promise which resolves if the captcha is solved correctly, and rejects + * if the captcha process is aborted. + * + * @param captchaSiteKey + * @returns {Promise} + */ +export function waitForCaptchaToBeSolved(captchaSiteKey) { + return new Promise((resolve, reject) => { + let captchaModalElement = document.createElement('div'); + + document.body.append(captchaModalElement); + + let captchaModalVueInstance = new Vue({ + el: captchaModalElement, + render: (createElement) => { + return createElement(CaptchaModal, { + props: { + captchaSiteKey, + needsCaptchaResponse: true, + }, + on: { + hidden: () => { + // Cleaning up the modal from the DOM + captchaModalVueInstance.$destroy(); + captchaModalVueInstance.$el.remove(); + captchaModalElement.remove(); + + captchaModalElement = null; + captchaModalVueInstance = null; + }, + receivedCaptchaResponse: (captchaResponse) => { + if (captchaResponse) { + resolve(captchaResponse); + } else { + // reject the promise with a custom exception, allowing consuming apps to + // adjust their error handling, if appropriate. + const error = new UnsolvedCaptchaError(); + reject(error); + } + }, + }, + }); + }, + }); + }); +} |