diff options
Diffstat (limited to 'packages/frontend/src/pages/admin/bot-protection.vue')
| -rw-r--r-- | packages/frontend/src/pages/admin/bot-protection.vue | 278 |
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> |