From 41d5afbffb693676f50df706e367d7c46a37adcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?= <46447427+samunohito@users.noreply.github.com> Date: Thu, 19 Dec 2024 09:38:11 +0900 Subject: [PATCH 01/14] =?UTF-8?q?feat(frontend):=20CAPTCHA=E3=81=AE?= =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E5=A4=89=E6=9B=B4=E6=99=82=E3=81=AF=E5=AE=9F?= =?UTF-8?q?=E9=9A=9B=E3=81=AB=E6=A4=9C=E8=A8=BC=E3=82=92=E9=80=9A=E9=81=8E?= =?UTF-8?q?=E3=81=97=E3=81=AA=E3=81=84=E3=81=A8=E4=BF=9D=E5=AD=98=E3=81=A7?= =?UTF-8?q?=E3=81=8D=E3=81=AA=E3=81=84=E3=82=88=E3=81=86=E3=81=AB=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../frontend/src/components/MkCaptcha.vue | 38 +++++++++--- .../frontend/src/components/MkFormFooter.vue | 3 +- packages/frontend/src/index.html | 2 +- .../src/pages/admin/bot-protection.vue | 62 ++++++++++++++++--- 4 files changed, 86 insertions(+), 19 deletions(-) diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue index 264cf9af06..1b69580d45 100644 --- a/packages/frontend/src/components/MkCaptcha.vue +++ b/packages/frontend/src/components/MkCaptcha.vue @@ -94,6 +94,14 @@ const scriptId = computed(() => `script-${props.provider}`); const captcha = computed(() => window[variable.value] || {} as unknown as Captcha); +watch(() => [props.instanceUrl, props.sitekey], async () => { + // 変更があったときはリフレッシュと再レンダリングをしておかないと、変更後の値で再検証が出来ない + if (available.value) { + callback(undefined); + await requestRender(); + } +}); + if (loaded || props.provider === 'mcaptcha' || props.provider === 'testcaptcha') { available.value = true; } else if (src.value !== null) { @@ -106,20 +114,34 @@ if (loaded || props.provider === 'mcaptcha' || props.provider === 'testcaptcha') } function reset() { - if (captcha.value.reset) captcha.value.reset(); + if (captcha.value.reset) { + try { + captcha.value.reset(); + } catch (error: unknown) { + // ignore + if (_DEV_) console.warn(error); + } + } testcaptchaPassed.value = false; testcaptchaInput.value = ''; } async function requestRender() { if (captcha.value.render && captchaEl.value instanceof Element) { - captcha.value.render(captchaEl.value, { - sitekey: props.sitekey, - theme: defaultStore.state.darkMode ? 'dark' : 'light', - callback: callback, - 'expired-callback': () => callback(undefined), - 'error-callback': () => callback(undefined), - }); + // 設定値の変更時などのタイミングで再レンダリングを行う際はリセットしておく必要がある + reset(); + + if (props.sitekey && props.sitekey.length > 0) { + captcha.value.render(captchaEl.value, { + sitekey: props.sitekey, + theme: defaultStore.state.darkMode ? 'dark' : 'light', + callback: callback, + 'expired-callback': () => callback(undefined), + 'error-callback': () => callback(undefined), + }); + } else { + captchaEl.value.innerHTML = ''; + } } else if (props.provider === 'mcaptcha' && props.instanceUrl && props.sitekey) { const { default: Widget } = await import('@mcaptcha/vanilla-glue'); new Widget({ diff --git a/packages/frontend/src/components/MkFormFooter.vue b/packages/frontend/src/components/MkFormFooter.vue index f409f6ce50..e23629f506 100644 --- a/packages/frontend/src/components/MkFormFooter.vue +++ b/packages/frontend/src/components/MkFormFooter.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.tsx.thereAreNChanges({ n: form.modifiedCount.value }) }}
{{ i18n.ts.discard }} - {{ i18n.ts.save }} + {{ i18n.ts.save }}
@@ -26,6 +26,7 @@ const props = defineProps<{ discard: () => void; save: () => void; }; + canSaving: boolean; }>(); diff --git a/packages/frontend/src/index.html b/packages/frontend/src/index.html index 08ff0c58dd..0be589262f 100644 --- a/packages/frontend/src/index.html +++ b/packages/frontend/src/index.html @@ -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; diff --git a/packages/frontend/src/pages/admin/bot-protection.vue b/packages/frontend/src/pages/admin/bot-protection.vue index d07add4408..07a744a6d5 100644 --- a/packages/frontend/src/pages/admin/bot-protection.vue +++ b/packages/frontend/src/pages/admin/bot-protection.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@@ -36,11 +36,18 @@ SPDX-License-Identifier: AGPL-3.0-only - + - + + +
+
サイトキーに"10000000-ffff-ffff-ffff-000000000001"と入力することで動作をテスト出来ます。
本番運用時には必ず正規のサイトキーを設定してください。
+ +
+
+ + + +
@@ -99,7 +121,7 @@ SPDX-License-Identifier: AGPL-3.0-only + + From cf579261b2c33fca1b4230bf0cbac3a1aa8b1115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?= <46447427+samunohito@users.noreply.github.com> Date: Thu, 19 Dec 2024 09:44:27 +0900 Subject: [PATCH 02/14] =?UTF-8?q?=E3=81=AA=E3=81=97=E3=81=A7=E3=82=82?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86?= =?UTF-8?q?=E3=81=AB=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/frontend/src/pages/admin/bot-protection.vue | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/pages/admin/bot-protection.vue b/packages/frontend/src/pages/admin/bot-protection.vue index 07a744a6d5..24cf8a9ef0 100644 --- a/packages/frontend/src/pages/admin/bot-protection.vue +++ b/packages/frontend/src/pages/admin/bot-protection.vue @@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only - @@ -63,7 +68,10 @@ SPDX-License-Identifier: AGPL-3.0-only - + @@ -78,12 +86,22 @@ SPDX-License-Identifier: AGPL-3.0-only - +
-
サイトキーに"6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI"と入力することで動作をテスト出来ます。
本番運用時には必ず正規のサイトキーを設定してください。
-
ref: reCAPTCHA FAQ
+
+ サイトキーに"6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI"と入力することで動作をテスト出来ます。
本番運用時には必ず正規のサイトキーを設定してください。 +
+
+ ref: reCAPTCHA FAQ +
@@ -99,12 +117,20 @@ SPDX-License-Identifier: AGPL-3.0-only - +
-
サイトキーに"1x00000000000000000000AA"と入力することで動作をテスト出来ます。
本番運用時には必ず正規のサイトキーを設定してください。
-
ref: Cloudflare Docs
+
+ サイトキーに"1x00000000000000000000AA"と入力することで動作をテスト出来ます。
本番運用時には必ず正規のサイトキーを設定してください。 +
+
+ ref: Cloudflare + Docs +
@@ -113,15 +139,20 @@ SPDX-License-Identifier: AGPL-3.0-only - + + + + {{ verifyErrorText }} + From c5dab0f7f7ce5cba7bff6111b7d2b0964f2c7325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?= <46447427+samunohito@users.noreply.github.com> Date: Fri, 20 Dec 2024 20:18:52 +0900 Subject: [PATCH 11/14] add current.ts --- packages/backend/src/core/CaptchaService.ts | 88 +++++++++++- .../backend/src/server/api/EndpointsModule.ts | 4 + packages/backend/src/server/api/endpoints.ts | 2 + .../api/endpoints/admin/captcha/current.ts | 70 ++++++++++ .../api/endpoints/admin/captcha/save.ts | 3 +- packages/backend/test/unit/CaptchaService.ts | 123 ++++++++++++++++- .../src/pages/admin/bot-protection.vue | 127 +++++++++--------- packages/misskey-js/etc/misskey-js.api.md | 4 + .../misskey-js/src/autogen/apiClientJSDoc.ts | 14 +- packages/misskey-js/src/autogen/endpoint.ts | 2 + packages/misskey-js/src/autogen/entities.ts | 1 + packages/misskey-js/src/autogen/types.ts | 81 ++++++++++- 12 files changed, 442 insertions(+), 77 deletions(-) create mode 100644 packages/backend/src/server/api/endpoints/admin/captcha/current.ts diff --git a/packages/backend/src/core/CaptchaService.ts b/packages/backend/src/core/CaptchaService.ts index 661972f5f3..8c7f66236e 100644 --- a/packages/backend/src/core/CaptchaService.ts +++ b/packages/backend/src/core/CaptchaService.ts @@ -8,6 +8,8 @@ import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; import { MetaService } from '@/core/MetaService.js'; import { MiMeta } from '@/models/Meta.js'; +import Logger from '@/logger.js'; +import { LoggerService } from './LoggerService.js'; export const supportedCaptchaProviders = ['none', 'hcaptcha', 'mcaptcha', 'recaptcha', 'turnstile', 'testcaptcha'] as const; export type CaptchaProvider = typeof supportedCaptchaProviders[number]; @@ -22,12 +24,35 @@ export const captchaErrorCodes = { } as const; export type CaptchaErrorCode = typeof captchaErrorCodes[keyof typeof captchaErrorCodes]; +export type CaptchaSetting = { + provider: CaptchaProvider; + hcaptcha: { + siteKey: string | null; + secretKey: string | null; + } + mcaptcha: { + siteKey: string | null; + secretKey: string | null; + instanceUrl: string | null; + } + recaptcha: { + siteKey: string | null; + secretKey: string | null; + } + turnstile: { + siteKey: string | null; + secretKey: string | null; + } +} + export class CaptchaError extends Error { public readonly code: CaptchaErrorCode; + public readonly cause?: unknown; - constructor(code: CaptchaErrorCode, message: string) { + constructor(code: CaptchaErrorCode, message: string, cause?: unknown) { super(message); this.code = code; + this.cause = cause; this.name = 'CaptchaError'; } } @@ -48,10 +73,14 @@ type CaptchaResponse = { @Injectable() export class CaptchaService { + private readonly logger: Logger; + constructor( private httpRequestService: HttpRequestService, private metaService: MetaService, + loggerService: LoggerService, ) { + this.logger = loggerService.getLogger('captcha'); } @bindThis @@ -126,7 +155,7 @@ export class CaptchaService { headers: { 'Content-Type': 'application/json', }, - }); + }, { throwErrorWhenResponseNotOk: false }); if (result.status !== 200) { throw new CaptchaError(captchaErrorCodes.requestFailed, 'mcaptcha-failed: mcaptcha didn\'t return 200 OK'); @@ -168,6 +197,60 @@ export class CaptchaService { } } + @bindThis + public async get(): Promise { + const meta = await this.metaService.fetch(true); + + let provider: CaptchaProvider; + switch (true) { + case meta.enableHcaptcha: { + provider = 'hcaptcha'; + break; + } + case meta.enableMcaptcha: { + provider = 'mcaptcha'; + break; + } + case meta.enableRecaptcha: { + provider = 'recaptcha'; + break; + } + case meta.enableTurnstile: { + provider = 'turnstile'; + break; + } + case meta.enableTestcaptcha: { + provider = 'testcaptcha'; + break; + } + default: { + provider = 'none'; + break; + } + } + + return { + provider: provider, + hcaptcha: { + siteKey: meta.hcaptchaSiteKey, + secretKey: meta.hcaptchaSecretKey, + }, + mcaptcha: { + siteKey: meta.mcaptchaSitekey, + secretKey: meta.mcaptchaSecretKey, + instanceUrl: meta.mcaptchaInstanceUrl, + }, + recaptcha: { + siteKey: meta.recaptchaSiteKey, + secretKey: meta.recaptchaSecretKey, + }, + turnstile: { + siteKey: meta.turnstileSiteKey, + secretKey: meta.turnstileSecretKey, + }, + }; + } + /** * captchaの設定を更新します. その際、フロントエンド側で受け取ったcaptchaからの戻り値を検証し、passした場合のみ設定を更新します. * 実際の検証処理はサービス内で定義されている各captchaプロバイダの検証関数に委譲します. @@ -250,6 +333,7 @@ export class CaptchaService { return operation() .then(() => ({ success: true }) as CaptchaSaveSuccess) .catch(err => { + this.logger.info(err); const error = err instanceof CaptchaError ? err : new CaptchaError(captchaErrorCodes.unknown, `unknown error: ${err}`); diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index ce74109539..c2462d8b3d 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -28,6 +28,7 @@ import * as ep___admin_avatarDecorations_create from './endpoints/admin/avatar-d import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-decorations/delete.js'; import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js'; import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js'; +import * as ep___admin_captcha_current from './endpoints/admin/captcha/current.js'; import * as ep___admin_captcha_save from './endpoints/admin/captcha/save.js'; import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js'; import * as ep___admin_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js'; @@ -417,6 +418,7 @@ const $admin_avatarDecorations_create: Provider = { provide: 'ep:admin/avatar-de const $admin_avatarDecorations_delete: Provider = { provide: 'ep:admin/avatar-decorations/delete', useClass: ep___admin_avatarDecorations_delete.default }; const $admin_avatarDecorations_list: Provider = { provide: 'ep:admin/avatar-decorations/list', useClass: ep___admin_avatarDecorations_list.default }; const $admin_avatarDecorations_update: Provider = { provide: 'ep:admin/avatar-decorations/update', useClass: ep___admin_avatarDecorations_update.default }; +const $admin_captcha_current: Provider = { provide: 'ep:admin/captcha/current', useClass: ep___admin_captcha_current.default }; const $admin_captcha_save: Provider = { provide: 'ep:admin/captcha/save', useClass: ep___admin_captcha_save.default }; const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default }; const $admin_unsetUserAvatar: Provider = { provide: 'ep:admin/unset-user-avatar', useClass: ep___admin_unsetUserAvatar.default }; @@ -810,6 +812,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_avatarDecorations_delete, $admin_avatarDecorations_list, $admin_avatarDecorations_update, + $admin_captcha_current, $admin_captcha_save, $admin_deleteAllFilesOfAUser, $admin_unsetUserAvatar, @@ -1197,6 +1200,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_avatarDecorations_delete, $admin_avatarDecorations_list, $admin_avatarDecorations_update, + $admin_captcha_current, $admin_captcha_save, $admin_deleteAllFilesOfAUser, $admin_unsetUserAvatar, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 72db473de5..86728ef381 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -33,6 +33,7 @@ import * as ep___admin_avatarDecorations_create from './endpoints/admin/avatar-d import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-decorations/delete.js'; import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js'; import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js'; +import * as ep___admin_captcha_current from './endpoints/admin/captcha/current.js'; import * as ep___admin_captcha_save from './endpoints/admin/captcha/save.js'; import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js'; import * as ep___admin_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js'; @@ -421,6 +422,7 @@ const eps = [ ['admin/avatar-decorations/delete', ep___admin_avatarDecorations_delete], ['admin/avatar-decorations/list', ep___admin_avatarDecorations_list], ['admin/avatar-decorations/update', ep___admin_avatarDecorations_update], + ['admin/captcha/current', ep___admin_captcha_current], ['admin/captcha/save', ep___admin_captcha_save], ['admin/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser], ['admin/unset-user-avatar', ep___admin_unsetUserAvatar], diff --git a/packages/backend/src/server/api/endpoints/admin/captcha/current.ts b/packages/backend/src/server/api/endpoints/admin/captcha/current.ts new file mode 100644 index 0000000000..63ec740348 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/captcha/current.ts @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { CaptchaService, supportedCaptchaProviders } from '@/core/CaptchaService.js'; + +export const meta = { + tags: ['admin', 'captcha'], + + requireCredential: true, + requireAdmin: true, + + // 実態はmetaの取得であるため + kind: 'read:admin:meta', + + res: { + type: 'object', + properties: { + provider: { + type: 'string', + enum: supportedCaptchaProviders, + }, + hcaptcha: { + type: 'object', + properties: { + siteKey: { type: 'string', nullable: true }, + secretKey: { type: 'string', nullable: true }, + }, + }, + mcaptcha: { + type: 'object', + properties: { + siteKey: { type: 'string', nullable: true }, + secretKey: { type: 'string', nullable: true }, + instanceUrl: { type: 'string', nullable: true }, + }, + }, + recaptcha: { + type: 'object', + properties: { + siteKey: { type: 'string', nullable: true }, + secretKey: { type: 'string', nullable: true }, + }, + }, + turnstile: { + type: 'object', + properties: { + siteKey: { type: 'string', nullable: true }, + secretKey: { type: 'string', nullable: true }, + }, + }, + }, + }, +} as const; + +export const paramDef = {} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private captchaService: CaptchaService, + ) { + super(meta, paramDef, async () => { + return this.captchaService.get(); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/captcha/save.ts b/packages/backend/src/server/api/endpoints/admin/captcha/save.ts index 511cb055dc..cc6186a8d6 100644 --- a/packages/backend/src/server/api/endpoints/admin/captcha/save.ts +++ b/packages/backend/src/server/api/endpoints/admin/captcha/save.ts @@ -12,8 +12,7 @@ export const meta = { tags: ['admin', 'captcha'], requireCredential: true, - requireModerator: true, - secure: true, + requireAdmin: true, // 実態はmetaの更新であるため kind: 'write:admin:meta', diff --git a/packages/backend/test/unit/CaptchaService.ts b/packages/backend/test/unit/CaptchaService.ts index 2d7167f95a..16fcc84126 100644 --- a/packages/backend/test/unit/CaptchaService.ts +++ b/packages/backend/test/unit/CaptchaService.ts @@ -35,7 +35,10 @@ describe('CaptchaService', () => { provide: HttpRequestService, useFactory: () => ({ send: jest.fn() }), }, { - provide: MetaService, useFactory: () => ({ update: jest.fn() }), + provide: MetaService, useFactory: () => ({ + fetch: jest.fn(), + update: jest.fn(), + }), }, ], }).compile(); @@ -50,6 +53,7 @@ describe('CaptchaService', () => { beforeEach(() => { httpRequestService.send.mockClear(); metaService.update.mockClear(); + metaService.fetch.mockClear(); }); afterAll(async () => { @@ -191,6 +195,123 @@ describe('CaptchaService', () => { }); }); + describe('get', () => { + function setupMeta(meta: Partial) { + metaService.fetch.mockResolvedValue(meta as MiMeta); + } + + test('values', async () => { + setupMeta({ + enableHcaptcha: false, + enableMcaptcha: false, + enableRecaptcha: false, + enableTurnstile: false, + enableTestcaptcha: false, + hcaptchaSiteKey: 'hcaptcha-sitekey', + hcaptchaSecretKey: 'hcaptcha-secret', + mcaptchaSitekey: 'mcaptcha-sitekey', + mcaptchaSecretKey: 'mcaptcha-secret', + mcaptchaInstanceUrl: 'https://localhost', + recaptchaSiteKey: 'recaptcha-sitekey', + recaptchaSecretKey: 'recaptcha-secret', + turnstileSiteKey: 'turnstile-sitekey', + turnstileSecretKey: 'turnstile-secret', + }); + + const result = await service.get(); + expect(result.provider).toBe('none'); + expect(result.hcaptcha.siteKey).toBe('hcaptcha-sitekey'); + expect(result.hcaptcha.secretKey).toBe('hcaptcha-secret'); + expect(result.mcaptcha.siteKey).toBe('mcaptcha-sitekey'); + expect(result.mcaptcha.secretKey).toBe('mcaptcha-secret'); + expect(result.mcaptcha.instanceUrl).toBe('https://localhost'); + expect(result.recaptcha.siteKey).toBe('recaptcha-sitekey'); + expect(result.recaptcha.secretKey).toBe('recaptcha-secret'); + expect(result.turnstile.siteKey).toBe('turnstile-sitekey'); + expect(result.turnstile.secretKey).toBe('turnstile-secret'); + }); + + describe('provider', () => { + test('none', async () => { + setupMeta({ + enableHcaptcha: false, + enableMcaptcha: false, + enableRecaptcha: false, + enableTurnstile: false, + enableTestcaptcha: false, + }); + + const result = await service.get(); + expect(result.provider).toBe('none'); + }); + + test('hcaptcha', async () => { + setupMeta({ + enableHcaptcha: true, + enableMcaptcha: false, + enableRecaptcha: false, + enableTurnstile: false, + enableTestcaptcha: false, + }); + + const result = await service.get(); + expect(result.provider).toBe('hcaptcha'); + }); + + test('mcaptcha', async () => { + setupMeta({ + enableHcaptcha: false, + enableMcaptcha: true, + enableRecaptcha: false, + enableTurnstile: false, + enableTestcaptcha: false, + }); + + const result = await service.get(); + expect(result.provider).toBe('mcaptcha'); + }); + + test('recaptcha', async () => { + setupMeta({ + enableHcaptcha: false, + enableMcaptcha: false, + enableRecaptcha: true, + enableTurnstile: false, + enableTestcaptcha: false, + }); + + const result = await service.get(); + expect(result.provider).toBe('recaptcha'); + }); + + test('turnstile', async () => { + setupMeta({ + enableHcaptcha: false, + enableMcaptcha: false, + enableRecaptcha: false, + enableTurnstile: true, + enableTestcaptcha: false, + }); + + const result = await service.get(); + expect(result.provider).toBe('turnstile'); + }); + + test('testcaptcha', async () => { + setupMeta({ + enableHcaptcha: false, + enableMcaptcha: false, + enableRecaptcha: false, + enableTurnstile: false, + enableTestcaptcha: true, + }); + + const result = await service.get(); + expect(result.provider).toBe('testcaptcha'); + }); + }); + }); + describe('save', () => { const host = 'https://localhost'; diff --git a/packages/frontend/src/pages/admin/bot-protection.vue b/packages/frontend/src/pages/admin/bot-protection.vue index eaf98de15c..26253ce91e 100644 --- a/packages/frontend/src/pages/admin/bot-protection.vue +++ b/packages/frontend/src/pages/admin/bot-protection.vue @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- + @@ -158,12 +158,69 @@ import MkInfo from '@/components/MkInfo.vue'; const MkCaptcha = defineAsyncComponent(() => import('@/components/MkCaptcha.vue')); -const meta = await misskeyApi('admin/meta'); - const captchaResult = ref(null); +const meta = await misskeyApi('admin/captcha/current'); +const botProtectionForm = useForm({ + 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) => { + const provider = state.provider; + + 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; + + if (provider === 'none') { + await os.apiWithDialog( + 'admin/captcha/save', + { provider: provider as Misskey.entities.AdminCaptchaSaveRequest['provider'] }, + ); + } else { + await os.apiWithDialog( + 'admin/captcha/save', + { + provider: provider as Misskey.entities.AdminCaptchaSaveRequest['provider'], + sitekey: sitekey, + secret: secret, + instanceUrl: state.mcaptchaInstanceUrl, + captchaResult: captchaResult.value, + }, + ); + } + + await fetchInstance(true); +}); + +watch(botProtectionForm.state, () => { + captchaResult.value = null; +}); + const canSaving = computed((): boolean => { - return (botProtectionForm.state.provider === null) || + return (botProtectionForm.state.provider === 'none') || (botProtectionForm.state.provider === 'hcaptcha' && !!captchaResult.value) || (botProtectionForm.state.provider === 'mcaptcha' && !!captchaResult.value) || (botProtectionForm.state.provider === 'recaptcha' && !!captchaResult.value) || @@ -171,68 +228,6 @@ const canSaving = computed((): boolean => { (botProtectionForm.state.provider === 'testcaptcha' && !!captchaResult.value); }); -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, -}, async (state) => { - const provider = botProtectionForm.state.provider; - - const sitekey = provider === 'hcaptcha' - ? botProtectionForm.state.hcaptchaSiteKey - : provider === 'mcaptcha' - ? botProtectionForm.state.mcaptchaSiteKey - : provider === 'recaptcha' - ? botProtectionForm.state.recaptchaSiteKey - : provider === 'turnstile' - ? botProtectionForm.state.turnstileSiteKey - : null; - const secret = provider === 'hcaptcha' - ? botProtectionForm.state.hcaptchaSecretKey - : provider === 'mcaptcha' - ? botProtectionForm.state.mcaptchaSecretKey - : provider === 'recaptcha' - ? botProtectionForm.state.recaptchaSecretKey - : provider === 'turnstile' - ? botProtectionForm.state.turnstileSecretKey - : null; - - if (captchaResult.value) { - await os.apiWithDialog( - 'admin/captcha/save', - { - provider: provider as Misskey.entities.AdminCaptchaSaveRequest['provider'], - sitekey: sitekey, - secret: secret, - instanceUrl: botProtectionForm.state.mcaptchaInstanceUrl, - captchaResult: captchaResult.value, - }, - ); - - await fetchInstance(true); - } -}); - -watch(botProtectionForm.state, () => { - captchaResult.value = null; -}); From a06026941923ba4c4e978568644d02156ac041ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?= <46447427+samunohito@users.noreply.github.com> Date: Sat, 21 Dec 2024 10:45:30 +0900 Subject: [PATCH 14/14] regenerate locales --- locales/index.d.ts | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/locales/index.d.ts b/locales/index.d.ts index 63878d3d47..2fe4d203be 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -10660,6 +10660,49 @@ export interface Locale extends ILocale { "description": string; }; }; + "_captcha": { + /** + * CAPTCHAを通過してください + */ + "verify": string; + /** + * サイトキーとシークレットキーにテスト用の値を入力することでプレビューを確認できます。 + * 詳細は下記ページをご確認ください。 + */ + "testSiteKeyMessage": string; + "_error": { + "_requestFailed": { + /** + * CAPTCHAのリクエストに失敗しました + */ + "title": string; + /** + * しばらく後に実行するか、設定をもう一度ご確認ください。 + */ + "text": string; + }; + "_verificationFailed": { + /** + * CAPTCHAの検証に失敗しました + */ + "title": string; + /** + * 設定が正しいかどうかもう一度確認ください。 + */ + "text": string; + }; + "_unknown": { + /** + * CAPTCHAエラー + */ + "title": string; + /** + * 想定外のエラーが発生しました。 + */ + "text": string; + }; + }; + }; } declare const locales: { [lang: string]: Locale;