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/15] =?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 264cf9af069b..1b69580d450b 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 f409f6ce50a5..e23629f506d8 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 08ff0c58dd3a..0be589262f1c 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 d07add440842..07a744a6d5d9 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/15] =?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 07a744a6d5d9..24cf8a9ef067 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/15] 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 | 85 ++++++------ 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, 421 insertions(+), 56 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 661972f5f3b1..8c7f66236e27 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 ce7410953926..c2462d8b3da0 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 72db473de59c..86728ef381fd 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 000000000000..63ec740348d9 --- /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 511cb055dcc2..cc6186a8d64c 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 2d7167f95a0b..16fcc84126e6 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 eaf98de15c97..26253ce91e05 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,81 +158,76 @@ import MkInfo from '@/components/MkInfo.vue'; const MkCaptcha = defineAsyncComponent(() => import('@/components/MkCaptcha.vue')); -const meta = await misskeyApi('admin/meta'); - const captchaResult = ref(null); -const canSaving = computed((): boolean => { - return (botProtectionForm.state.provider === null) || - (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); -}); - +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) => { - const provider = botProtectionForm.state.provider; + const provider = state.provider; const sitekey = provider === 'hcaptcha' - ? botProtectionForm.state.hcaptchaSiteKey + ? state.hcaptchaSiteKey : provider === 'mcaptcha' - ? botProtectionForm.state.mcaptchaSiteKey + ? state.mcaptchaSiteKey : provider === 'recaptcha' - ? botProtectionForm.state.recaptchaSiteKey + ? state.recaptchaSiteKey : provider === 'turnstile' - ? botProtectionForm.state.turnstileSiteKey + ? state.turnstileSiteKey : null; const secret = provider === 'hcaptcha' - ? botProtectionForm.state.hcaptchaSecretKey + ? state.hcaptchaSecretKey : provider === 'mcaptcha' - ? botProtectionForm.state.mcaptchaSecretKey + ? state.mcaptchaSecretKey : provider === 'recaptcha' - ? botProtectionForm.state.recaptchaSecretKey + ? state.recaptchaSecretKey : provider === 'turnstile' - ? botProtectionForm.state.turnstileSecretKey + ? state.turnstileSecretKey : null; - if (captchaResult.value) { + 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: botProtectionForm.state.mcaptchaInstanceUrl, + instanceUrl: state.mcaptchaInstanceUrl, captchaResult: captchaResult.value, }, ); - - await fetchInstance(true); } + + 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); +}); + 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/15] 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 63878d3d4771..2fe4d203bea0 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; From 1be6169e06373e2baf65bbcf0f0ff7c6de91496c 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, 26 Dec 2024 21:27:51 +0900 Subject: [PATCH 15/15] fix MkFormFooter.vue --- packages/frontend/src/components/MkFormFooter.vue | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/frontend/src/components/MkFormFooter.vue b/packages/frontend/src/components/MkFormFooter.vue index e23629f506d8..96214a9542a0 100644 --- a/packages/frontend/src/components/MkFormFooter.vue +++ b/packages/frontend/src/components/MkFormFooter.vue @@ -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,8 +26,10 @@ const props = defineProps<{ discard: () => void; save: () => void; }; - canSaving: boolean; -}>(); + canSaving?: boolean; +}>(), { + canSaving: true, +});