feat(frontend): Botプロテクションの設定変更時は実際に検証を通過しないと保存できないようにする (#15151)
* feat(frontend): CAPTCHAの設定変更時は実際に検証を通過しないと保存できないようにする * なしでも保存できるようにした * fix CHANGELOG.md * フォームが増殖するのを修正 * add comment * add server-side verify * fix ci * fix * fix * fix i18n * add current.ts * fix text * fix * regenerate locales * fix MkFormFooter.vue --------- Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
This commit is contained in:
@@ -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;
|
||||
@@ -53,6 +56,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;
|
||||
}>();
|
||||
@@ -64,7 +68,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);
|
||||
|
||||
@@ -94,6 +98,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) {
|
||||
@@ -106,14 +119,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,
|
||||
@@ -133,6 +170,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);
|
||||
}
|
||||
@@ -165,7 +219,7 @@ onUnmounted(() => {
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
reset();
|
||||
clearWidget();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
|
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div :class="$style.text">{{ i18n.tsx.thereAreNChanges({ n: form.modifiedCount.value }) }}</div>
|
||||
<div style="margin-left: auto;" class="_buttons">
|
||||
<MkButton danger rounded @click="form.discard"><i class="ti ti-x"></i> {{ i18n.ts.discard }}</MkButton>
|
||||
<MkButton primary rounded @click="form.save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
|
||||
<MkButton primary rounded :disabled="!canSaving" @click="form.save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -18,7 +18,7 @@ import { } from 'vue';
|
||||
import MkButton from './MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = defineProps<{
|
||||
const props = withDefaults(defineProps<{
|
||||
form: {
|
||||
modifiedCount: {
|
||||
value: number;
|
||||
@@ -26,7 +26,10 @@ const props = defineProps<{
|
||||
discard: () => void;
|
||||
save: () => void;
|
||||
};
|
||||
}>();
|
||||
canSaving?: boolean;
|
||||
}>(), {
|
||||
canSaving: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
@@ -18,7 +18,7 @@
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self' https://newassets.hcaptcha.com/ https://challenges.cloudflare.com/ http://localhost:7493/;
|
||||
worker-src 'self';
|
||||
script-src 'self' 'unsafe-eval' https://*.hcaptcha.com https://challenges.cloudflare.com https://esm.sh;
|
||||
script-src 'self' 'unsafe-eval' https://*.hcaptcha.com https://*.recaptcha.net https://*.gstatic.com https://challenges.cloudflare.com https://esm.sh;
|
||||
style-src 'self' 'unsafe-inline';
|
||||
img-src 'self' data: blob: www.google.com xn--931a.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
|
||||
media-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
|
||||
|
@@ -11,6 +11,7 @@ import * as Misskey from 'misskey-js';
|
||||
import type { ComponentProps as CP } from 'vue-component-type-helpers';
|
||||
import type { Form, GetFormResultType } from '@/scripts/form.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import type { PostFormProps } from '@/types/post-form.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
@@ -28,15 +29,15 @@ import { pleaseLogin } from '@/scripts/please-login.js';
|
||||
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
||||
import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
|
||||
import { focusParent } from '@/scripts/focus.js';
|
||||
import type { PostFormProps } from '@/types/post-form.js';
|
||||
|
||||
export const openingWindowsCount = ref(0);
|
||||
|
||||
export type ApiWithDialogCustomErrors = Record<string, { title?: string; text: string; }>;
|
||||
export const apiWithDialog = (<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
data: P,
|
||||
token?: string | null | undefined,
|
||||
customErrors?: Record<string, { title?: string; text: string; }>,
|
||||
customErrors?: ApiWithDialogCustomErrors,
|
||||
) => {
|
||||
const promise = misskeyApi(endpoint, data, token);
|
||||
promiseDialog(promise, null, async (err) => {
|
||||
|
@@ -13,13 +13,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template v-else-if="botProtectionForm.savedState.provider === 'turnstile'" #suffix>Turnstile</template>
|
||||
<template v-else-if="botProtectionForm.savedState.provider === 'testcaptcha'" #suffix>testCaptcha</template>
|
||||
<template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template>
|
||||
<template v-if="botProtectionForm.modified.value" #footer>
|
||||
<MkFormFooter :form="botProtectionForm"/>
|
||||
<template #footer>
|
||||
<MkFormFooter :canSaving="canSaving" :form="botProtectionForm"/>
|
||||
</template>
|
||||
|
||||
<div class="_gaps_m">
|
||||
<MkRadios v-model="botProtectionForm.state.provider">
|
||||
<option :value="null">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option>
|
||||
<option value="none">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option>
|
||||
<option value="hcaptcha">hCaptcha</option>
|
||||
<option value="mcaptcha">mCaptcha</option>
|
||||
<option value="recaptcha">reCAPTCHA</option>
|
||||
@@ -28,70 +28,125 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkRadios>
|
||||
|
||||
<template v-if="botProtectionForm.state.provider === 'hcaptcha'">
|
||||
<MkInput v-model="botProtectionForm.state.hcaptchaSiteKey">
|
||||
<MkInput v-model="botProtectionForm.state.hcaptchaSiteKey" debounce>
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>{{ i18n.ts.hcaptchaSiteKey }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="botProtectionForm.state.hcaptchaSecretKey">
|
||||
<MkInput v-model="botProtectionForm.state.hcaptchaSecretKey" debounce>
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>{{ i18n.ts.hcaptchaSecretKey }}</template>
|
||||
</MkInput>
|
||||
<FormSlot>
|
||||
<template #label>{{ i18n.ts.preview }}</template>
|
||||
<MkCaptcha provider="hcaptcha" :sitekey="botProtectionForm.state.hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/>
|
||||
<FormSlot v-if="botProtectionForm.state.hcaptchaSiteKey">
|
||||
<template #label>{{ i18n.ts._captcha.verify }}</template>
|
||||
<MkCaptcha
|
||||
v-model="captchaResult"
|
||||
provider="hcaptcha"
|
||||
:sitekey="botProtectionForm.state.hcaptchaSiteKey"
|
||||
:secretKey="botProtectionForm.state.hcaptchaSecretKey"
|
||||
/>
|
||||
</FormSlot>
|
||||
<MkInfo>
|
||||
<div :class="$style.captchaInfoMsg">
|
||||
<div>{{ i18n.ts._captcha.testSiteKeyMessage }}</div>
|
||||
<div>
|
||||
<span>ref: </span><a href="https://docs.hcaptcha.com/#integration-testing-test-keys" target="_blank">hCaptcha Developer Guide</a>
|
||||
</div>
|
||||
</div>
|
||||
</MkInfo>
|
||||
</template>
|
||||
|
||||
<template v-else-if="botProtectionForm.state.provider === 'mcaptcha'">
|
||||
<MkInput v-model="botProtectionForm.state.mcaptchaSiteKey">
|
||||
<MkInput v-model="botProtectionForm.state.mcaptchaSiteKey" debounce>
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>{{ i18n.ts.mcaptchaSiteKey }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="botProtectionForm.state.mcaptchaSecretKey">
|
||||
<MkInput v-model="botProtectionForm.state.mcaptchaSecretKey" debounce>
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>{{ i18n.ts.mcaptchaSecretKey }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="botProtectionForm.state.mcaptchaInstanceUrl">
|
||||
<MkInput v-model="botProtectionForm.state.mcaptchaInstanceUrl" debounce>
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts.mcaptchaInstanceUrl }}</template>
|
||||
</MkInput>
|
||||
<FormSlot v-if="botProtectionForm.state.mcaptchaSiteKey && botProtectionForm.state.mcaptchaInstanceUrl">
|
||||
<template #label>{{ i18n.ts.preview }}</template>
|
||||
<MkCaptcha provider="mcaptcha" :sitekey="botProtectionForm.state.mcaptchaSiteKey" :instanceUrl="botProtectionForm.state.mcaptchaInstanceUrl"/>
|
||||
<template #label>{{ i18n.ts._captcha.verify }}</template>
|
||||
<MkCaptcha
|
||||
v-model="captchaResult"
|
||||
provider="mcaptcha"
|
||||
:sitekey="botProtectionForm.state.mcaptchaSiteKey"
|
||||
:secretKey="botProtectionForm.state.mcaptchaSecretKey"
|
||||
:instanceUrl="botProtectionForm.state.mcaptchaInstanceUrl"
|
||||
/>
|
||||
</FormSlot>
|
||||
</template>
|
||||
|
||||
<template v-else-if="botProtectionForm.state.provider === 'recaptcha'">
|
||||
<MkInput v-model="botProtectionForm.state.recaptchaSiteKey">
|
||||
<MkInput v-model="botProtectionForm.state.recaptchaSiteKey" debounce>
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>{{ i18n.ts.recaptchaSiteKey }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="botProtectionForm.state.recaptchaSecretKey">
|
||||
<MkInput v-model="botProtectionForm.state.recaptchaSecretKey" debounce>
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>{{ i18n.ts.recaptchaSecretKey }}</template>
|
||||
</MkInput>
|
||||
<FormSlot v-if="botProtectionForm.state.recaptchaSiteKey">
|
||||
<template #label>{{ i18n.ts.preview }}</template>
|
||||
<MkCaptcha provider="recaptcha" :sitekey="botProtectionForm.state.recaptchaSiteKey"/>
|
||||
<template #label>{{ i18n.ts._captcha.verify }}</template>
|
||||
<MkCaptcha
|
||||
v-model="captchaResult"
|
||||
provider="recaptcha"
|
||||
:sitekey="botProtectionForm.state.recaptchaSiteKey"
|
||||
:secretKey="botProtectionForm.state.recaptchaSecretKey"
|
||||
/>
|
||||
</FormSlot>
|
||||
<MkInfo>
|
||||
<div :class="$style.captchaInfoMsg">
|
||||
<div>{{ i18n.ts._captcha.testSiteKeyMessage }}</div>
|
||||
<div>
|
||||
<span>ref: </span>
|
||||
<a
|
||||
href="https://developers.google.com/recaptcha/docs/faq?hl=ja#id-like-to-run-automated-tests-with-recaptcha.-what-should-i-do"
|
||||
target="_blank"
|
||||
>reCAPTCHA FAQ</a>
|
||||
</div>
|
||||
</div>
|
||||
</MkInfo>
|
||||
</template>
|
||||
|
||||
<template v-else-if="botProtectionForm.state.provider === 'turnstile'">
|
||||
<MkInput v-model="botProtectionForm.state.turnstileSiteKey">
|
||||
<MkInput v-model="botProtectionForm.state.turnstileSiteKey" debounce>
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>{{ i18n.ts.turnstileSiteKey }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="botProtectionForm.state.turnstileSecretKey">
|
||||
<MkInput v-model="botProtectionForm.state.turnstileSecretKey" debounce>
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
<template #label>{{ i18n.ts.turnstileSecretKey }}</template>
|
||||
</MkInput>
|
||||
<FormSlot>
|
||||
<template #label>{{ i18n.ts.preview }}</template>
|
||||
<MkCaptcha provider="turnstile" :sitekey="botProtectionForm.state.turnstileSiteKey || '1x00000000000000000000AA'"/>
|
||||
<FormSlot v-if="botProtectionForm.state.turnstileSiteKey">
|
||||
<template #label>{{ i18n.ts._captcha.verify }}</template>
|
||||
<MkCaptcha
|
||||
v-model="captchaResult"
|
||||
provider="turnstile"
|
||||
:sitekey="botProtectionForm.state.turnstileSiteKey"
|
||||
:secretKey="botProtectionForm.state.turnstileSecretKey"
|
||||
/>
|
||||
</FormSlot>
|
||||
<MkInfo>
|
||||
<div :class="$style.captchaInfoMsg">
|
||||
<div>
|
||||
{{ i18n.ts._captcha.testSiteKeyMessage }}
|
||||
</div>
|
||||
<div>
|
||||
<span>ref: </span><a href="https://developers.cloudflare.com/turnstile/troubleshooting/testing/" target="_blank">Cloudflare Docs</a>
|
||||
</div>
|
||||
</div>
|
||||
</MkInfo>
|
||||
</template>
|
||||
|
||||
<template v-else-if="botProtectionForm.state.provider === 'testcaptcha'">
|
||||
<MkInfo warn><span v-html="i18n.ts.testCaptchaWarning"></span></MkInfo>
|
||||
<FormSlot>
|
||||
<template #label>{{ i18n.ts.preview }}</template>
|
||||
<MkCaptcha provider="testcaptcha"/>
|
||||
<template #label>{{ i18n.ts._captcha.verify }}</template>
|
||||
<MkCaptcha v-model="captchaResult" provider="testcaptcha" :sitekey="null"/>
|
||||
</FormSlot>
|
||||
</template>
|
||||
</div>
|
||||
@@ -99,7 +154,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, ref } from 'vue';
|
||||
import { computed, defineAsyncComponent, ref, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkRadios from '@/components/MkRadios.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import FormSlot from '@/components/form/slot.vue';
|
||||
@@ -111,49 +167,107 @@ import { useForm } from '@/scripts/use-form.js';
|
||||
import MkFormFooter from '@/components/MkFormFooter.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { ApiWithDialogCustomErrors } from '@/os.js';
|
||||
|
||||
const MkCaptcha = defineAsyncComponent(() => import('@/components/MkCaptcha.vue'));
|
||||
|
||||
const meta = await misskeyApi('admin/meta');
|
||||
const errorHandler: ApiWithDialogCustomErrors = {
|
||||
// 検証リクエストそのものに失敗
|
||||
'0f4fe2f1-2c15-4d6e-b714-efbfcde231cd': {
|
||||
title: i18n.ts._captcha._error._requestFailed.title,
|
||||
text: i18n.ts._captcha._error._requestFailed.text,
|
||||
},
|
||||
// 検証リクエストの結果が不正
|
||||
'c41c067f-24f3-4150-84b2-b5a3ae8c2214': {
|
||||
title: i18n.ts._captcha._error._verificationFailed.title,
|
||||
text: i18n.ts._captcha._error._verificationFailed.text,
|
||||
},
|
||||
// 不明なエラー
|
||||
'f868d509-e257-42a9-99c1-42614b031a97': {
|
||||
title: i18n.ts._captcha._error._unknown.title,
|
||||
text: i18n.ts._captcha._error._unknown.text,
|
||||
},
|
||||
};
|
||||
|
||||
const captchaResult = ref<string | null>(null);
|
||||
|
||||
const meta = await misskeyApi('admin/captcha/current');
|
||||
const botProtectionForm = useForm({
|
||||
provider: meta.enableHcaptcha
|
||||
? 'hcaptcha'
|
||||
: meta.enableRecaptcha
|
||||
? 'recaptcha'
|
||||
: meta.enableTurnstile
|
||||
? 'turnstile'
|
||||
: meta.enableMcaptcha
|
||||
? 'mcaptcha'
|
||||
: meta.enableTestcaptcha
|
||||
? 'testcaptcha'
|
||||
: null,
|
||||
hcaptchaSiteKey: meta.hcaptchaSiteKey,
|
||||
hcaptchaSecretKey: meta.hcaptchaSecretKey,
|
||||
mcaptchaSiteKey: meta.mcaptchaSiteKey,
|
||||
mcaptchaSecretKey: meta.mcaptchaSecretKey,
|
||||
mcaptchaInstanceUrl: meta.mcaptchaInstanceUrl,
|
||||
recaptchaSiteKey: meta.recaptchaSiteKey,
|
||||
recaptchaSecretKey: meta.recaptchaSecretKey,
|
||||
turnstileSiteKey: meta.turnstileSiteKey,
|
||||
turnstileSecretKey: meta.turnstileSecretKey,
|
||||
provider: meta.provider,
|
||||
hcaptchaSiteKey: meta.hcaptcha.siteKey,
|
||||
hcaptchaSecretKey: meta.hcaptcha.secretKey,
|
||||
mcaptchaSiteKey: meta.mcaptcha.siteKey,
|
||||
mcaptchaSecretKey: meta.mcaptcha.secretKey,
|
||||
mcaptchaInstanceUrl: meta.mcaptcha.instanceUrl,
|
||||
recaptchaSiteKey: meta.recaptcha.siteKey,
|
||||
recaptchaSecretKey: meta.recaptcha.secretKey,
|
||||
turnstileSiteKey: meta.turnstile.siteKey,
|
||||
turnstileSecretKey: meta.turnstile.secretKey,
|
||||
}, async (state) => {
|
||||
await os.apiWithDialog('admin/update-meta', {
|
||||
enableHcaptcha: state.provider === 'hcaptcha',
|
||||
hcaptchaSiteKey: state.hcaptchaSiteKey,
|
||||
hcaptchaSecretKey: state.hcaptchaSecretKey,
|
||||
enableMcaptcha: state.provider === 'mcaptcha',
|
||||
mcaptchaSiteKey: state.mcaptchaSiteKey,
|
||||
mcaptchaSecretKey: state.mcaptchaSecretKey,
|
||||
mcaptchaInstanceUrl: state.mcaptchaInstanceUrl,
|
||||
enableRecaptcha: state.provider === 'recaptcha',
|
||||
recaptchaSiteKey: state.recaptchaSiteKey,
|
||||
recaptchaSecretKey: state.recaptchaSecretKey,
|
||||
enableTurnstile: state.provider === 'turnstile',
|
||||
turnstileSiteKey: state.turnstileSiteKey,
|
||||
turnstileSecretKey: state.turnstileSecretKey,
|
||||
enableTestcaptcha: state.provider === 'testcaptcha',
|
||||
});
|
||||
fetchInstance(true);
|
||||
const provider = state.provider;
|
||||
if (provider === 'none') {
|
||||
await os.apiWithDialog(
|
||||
'admin/captcha/save',
|
||||
{ provider: provider as Misskey.entities.AdminCaptchaSaveRequest['provider'] },
|
||||
undefined,
|
||||
errorHandler,
|
||||
);
|
||||
} else {
|
||||
const sitekey = provider === 'hcaptcha'
|
||||
? state.hcaptchaSiteKey
|
||||
: provider === 'mcaptcha'
|
||||
? state.mcaptchaSiteKey
|
||||
: provider === 'recaptcha'
|
||||
? state.recaptchaSiteKey
|
||||
: provider === 'turnstile'
|
||||
? state.turnstileSiteKey
|
||||
: null;
|
||||
const secret = provider === 'hcaptcha'
|
||||
? state.hcaptchaSecretKey
|
||||
: provider === 'mcaptcha'
|
||||
? state.mcaptchaSecretKey
|
||||
: provider === 'recaptcha'
|
||||
? state.recaptchaSecretKey
|
||||
: provider === 'turnstile'
|
||||
? state.turnstileSecretKey
|
||||
: null;
|
||||
|
||||
await os.apiWithDialog(
|
||||
'admin/captcha/save',
|
||||
{
|
||||
provider: provider as Misskey.entities.AdminCaptchaSaveRequest['provider'],
|
||||
sitekey: sitekey,
|
||||
secret: secret,
|
||||
instanceUrl: state.mcaptchaInstanceUrl,
|
||||
captchaResult: captchaResult.value,
|
||||
},
|
||||
undefined,
|
||||
errorHandler,
|
||||
);
|
||||
}
|
||||
|
||||
await fetchInstance(true);
|
||||
});
|
||||
|
||||
watch(botProtectionForm.state, () => {
|
||||
captchaResult.value = null;
|
||||
});
|
||||
|
||||
const canSaving = computed((): boolean => {
|
||||
return (botProtectionForm.state.provider === 'none') ||
|
||||
(botProtectionForm.state.provider === 'hcaptcha' && !!captchaResult.value) ||
|
||||
(botProtectionForm.state.provider === 'mcaptcha' && !!captchaResult.value) ||
|
||||
(botProtectionForm.state.provider === 'recaptcha' && !!captchaResult.value) ||
|
||||
(botProtectionForm.state.provider === 'turnstile' && !!captchaResult.value) ||
|
||||
(botProtectionForm.state.provider === 'testcaptcha' && !!captchaResult.value);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.captchaInfoMsg {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
|
Reference in New Issue
Block a user