summaryrefslogtreecommitdiff
path: root/packages/frontend/src/pages/admin/bot-protection.vue
diff options
context:
space:
mode:
Diffstat (limited to 'packages/frontend/src/pages/admin/bot-protection.vue')
-rw-r--r--packages/frontend/src/pages/admin/bot-protection.vue278
1 files changed, 206 insertions, 72 deletions
diff --git a/packages/frontend/src/pages/admin/bot-protection.vue b/packages/frontend/src/pages/admin/bot-protection.vue
index 2f6dac8097..e37df40f2f 100644
--- a/packages/frontend/src/pages/admin/bot-protection.vue
+++ b/packages/frontend/src/pages/admin/bot-protection.vue
@@ -14,13 +14,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-else-if="botProtectionForm.savedState.provider === 'fc'" #suffix>FriendlyCaptcha</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>
@@ -30,71 +30,126 @@ 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 === 'fc'">
- <MkInput v-model="botProtectionForm.state.fcSiteKey">
+ <MkInput v-model="botProtectionForm.state.fcSiteKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.hcaptchaSiteKey }}</template>
</MkInput>
- <MkInput v-model="botProtectionForm.state.fcSecretKey">
+ <MkInput v-model="botProtectionForm.state.fcSecretKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.hcaptchaSecretKey }}</template>
</MkInput>
@@ -102,12 +157,32 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.preview }}</template>
<MkCaptcha provider="fc" :sitekey="botProtectionForm.state.fcSiteKey"/>
</FormSlot>
+ <FormSlot v-if="botProtectionForm.state.fcSiteKey">
+ <template #label>{{ i18n.ts._captcha.verify }}</template>
+ <MkCaptcha
+ v-model="captchaResult"
+ provider="fc"
+ :sitekey="botProtectionForm.state.fcSiteKey"
+ :secretKey="botProtectionForm.state.fcSecretKey"
+ />
+ </FormSlot>
+ <MkInfo>
+ <div :class="$style.captchaInfoMsg">
+ <div>
+ {{ i18n.ts._captcha.testSiteKeyMessage }}
+ </div>
+ <div>
+ <span>ref: </span><a href="https://docs.friendlycaptcha.com/#/installation?id=_3-verifying-the-captcha-solution-on-the-server" target="_blank">FriendlyCaptcha 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>
@@ -115,7 +190,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';
@@ -127,56 +203,114 @@ 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.enableFC
- ? 'fc'
- : 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,
- fcSiteKey: meta.fcSiteKey,
- fcSecretKey: meta.fcSecretKey,
+ 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,
+ fcSiteKey: meta.fc.siteKey,
+ fcSecretKey: meta.fc.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,
- enableFC: state.provider === 'fc',
- fcSiteKey: state.fcSiteKey,
- fcSecretKey: state.fcSecretKey,
- 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
+ : provider === 'fc'
+ ? state.fcSiteKey
+ : null;
+ const secret = provider === 'hcaptcha'
+ ? state.hcaptchaSecretKey
+ : provider === 'mcaptcha'
+ ? state.mcaptchaSecretKey
+ : provider === 'recaptcha'
+ ? state.recaptchaSecretKey
+ : provider === 'turnstile'
+ ? state.turnstileSecretKey
+ : provider === 'fc'
+ ? state.fcSecretKey
+ : 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 === 'fc' && !!captchaResult.value) ||
+ (botProtectionForm.state.provider === 'testcaptcha' && !!captchaResult.value);
+});
+
</script>
+
+<style lang="scss" module>
+.captchaInfoMsg {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+</style>