summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components/MkCaptcha.vue
diff options
context:
space:
mode:
authorHazelnoot <acomputerdog@gmail.com>2025-02-03 14:31:26 -0500
committerHazelnoot <acomputerdog@gmail.com>2025-02-03 14:36:09 -0500
commita4e86758c1c53f4e623b6e8f613d4a6e34e96156 (patch)
treed09bf325b7f52512a1fe2a9d35f1953d2b310309 /packages/frontend/src/components/MkCaptcha.vue
parentmerge: Use package manager version from package.json (!883) (diff)
parentfix(build): corepackのバグの回避 (#15387) (diff)
downloadsharkey-a4e86758c1c53f4e623b6e8f613d4a6e34e96156.tar.gz
sharkey-a4e86758c1c53f4e623b6e8f613d4a6e34e96156.tar.bz2
sharkey-a4e86758c1c53f4e623b6e8f613d4a6e34e96156.zip
merge upstream 2025-02-03
Diffstat (limited to 'packages/frontend/src/components/MkCaptcha.vue')
-rw-r--r--packages/frontend/src/components/MkCaptcha.vue64
1 files changed, 59 insertions, 5 deletions
diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue
index e9493edbd1..aeed90722f 100644
--- a/packages/frontend/src/components/MkCaptcha.vue
+++ b/packages/frontend/src/components/MkCaptcha.vue
@@ -30,6 +30,9 @@ import { ref, shallowRef, computed, onMounted, onBeforeUnmount, watch, onUnmount
import { defaultStore } from '@/store.js';
// APIs provided by Captcha services
+// see: https://docs.hcaptcha.com/configuration/#javascript-api
+// see: https://developers.google.com/recaptcha/docs/display?hl=ja
+// see: https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/#explicitly-render-the-turnstile-widget
export type Captcha = {
render(container: string | Node, options: {
readonly [_ in 'sitekey' | 'theme' | 'type' | 'size' | 'tabindex' | 'callback' | 'expired' | 'expired-callback' | 'error-callback' | 'endpoint']?: unknown;
@@ -56,6 +59,7 @@ declare global {
const props = defineProps<{
provider: CaptchaProvider;
sitekey: string | null; // null will show error on request
+ secretKey?: string | null;
instanceUrl?: string | null;
modelValue?: string | null;
}>();
@@ -67,7 +71,7 @@ const emit = defineEmits<{
const available = ref(false);
const captchaEl = shallowRef<HTMLDivElement | undefined>();
-
+const captchaWidgetId = ref<string | undefined>(undefined);
const testcaptchaInput = ref('');
const testcaptchaPassed = ref(false);
@@ -99,6 +103,15 @@ const scriptId = computed(() => `script-${props.provider}`);
const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha);
+watch(() => [props.instanceUrl, props.sitekey, props.secretKey], async () => {
+ // 変更があったときはリフレッシュと再レンダリングをしておかないと、変更後の値で再検証が出来ない
+ if (available.value) {
+ callback(undefined);
+ clearWidget();
+ await requestRender();
+ }
+});
+
if (loaded || props.provider === 'mcaptcha' || props.provider === 'testcaptcha') {
available.value = true;
} else if (src.value !== null) {
@@ -111,14 +124,38 @@ if (loaded || props.provider === 'mcaptcha' || props.provider === 'testcaptcha')
}
function reset() {
- if (captcha.value.reset) captcha.value.reset();
+ if (captcha.value.reset && captchaWidgetId.value !== undefined) {
+ try {
+ captcha.value.reset(captchaWidgetId.value);
+ } catch (error: unknown) {
+ // ignore
+ if (_DEV_) console.warn(error);
+ }
+ }
testcaptchaPassed.value = false;
testcaptchaInput.value = '';
}
+function remove() {
+ if (captcha.value.remove && captchaWidgetId.value) {
+ try {
+ if (_DEV_) console.log('remove', props.provider, captchaWidgetId.value);
+ captcha.value.remove(captchaWidgetId.value);
+ } catch (error: unknown) {
+ // ignore
+ if (_DEV_) console.warn(error);
+ }
+ }
+}
+
async function requestRender() {
- if (captcha.value.render && captchaEl.value instanceof Element) {
- captcha.value.render(captchaEl.value, {
+ if (captcha.value.render && captchaEl.value instanceof Element && props.sitekey) {
+ // reCAPTCHAのレンダリング重複判定を回避するため、captchaEl配下に仮のdivを用意する.
+ // (同じdivに対して複数回renderを呼び出すとreCAPTCHAはエラーを返すので)
+ const elem = document.createElement('div');
+ captchaEl.value.appendChild(elem);
+
+ captchaWidgetId.value = captcha.value.render(elem, {
sitekey: props.sitekey,
theme: defaultStore.state.darkMode ? 'dark' : 'light',
callback: callback,
@@ -146,6 +183,23 @@ async function requestRender() {
}
}
+function clearWidget() {
+ if (props.provider === 'mcaptcha') {
+ const container = document.getElementById('mcaptcha__widget-container');
+ if (container) {
+ container.innerHTML = '';
+ }
+ } else {
+ reset();
+ remove();
+
+ if (captchaEl.value) {
+ // レンダリング先のコンテナの中身を掃除し、フォームが増殖するのを抑止
+ captchaEl.value.innerHTML = '';
+ }
+ }
+}
+
function callback(response?: string) {
emit('update:modelValue', typeof response === 'string' ? response : null);
}
@@ -178,7 +232,7 @@ onUnmounted(() => {
});
onBeforeUnmount(() => {
- reset();
+ clearWidget();
});
defineExpose({