diff --git a/packages/backend/src/core/CaptchaService.ts b/packages/backend/src/core/CaptchaService.ts index 0c2b09312c..661972f5f3 100644 --- a/packages/backend/src/core/CaptchaService.ts +++ b/packages/backend/src/core/CaptchaService.ts @@ -6,8 +6,10 @@ import { Injectable } from '@nestjs/common'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; +import { MetaService } from '@/core/MetaService.js'; +import { MiMeta } from '@/models/Meta.js'; -export const supportedCaptchaProviders = ['hcaptcha', 'mcaptcha', 'recaptcha', 'turnstile', 'testcaptcha'] as const; +export const supportedCaptchaProviders = ['none', 'hcaptcha', 'mcaptcha', 'recaptcha', 'turnstile', 'testcaptcha'] as const; export type CaptchaProvider = typeof supportedCaptchaProviders[number]; export const captchaErrorCodes = { @@ -30,14 +32,14 @@ export class CaptchaError extends Error { } } -export type ValidateSuccess = { +export type CaptchaSaveSuccess = { success: true; } -export type ValidateFailure = { +export type CaptchaSaveFailure = { success: false; error: CaptchaError; } -export type ValidateResult = ValidateSuccess | ValidateFailure; +export type CaptchaSaveResult = CaptchaSaveSuccess | CaptchaSaveFailure; type CaptchaResponse = { success: boolean; @@ -48,6 +50,7 @@ type CaptchaResponse = { export class CaptchaService { constructor( private httpRequestService: HttpRequestService, + private metaService: MetaService, ) { } @@ -166,16 +169,15 @@ export class CaptchaService { } /** - * フロントエンド側で受け取ったcaptchaからの戻り値を検証します. + * captchaの設定を更新します. その際、フロントエンド側で受け取ったcaptchaからの戻り値を検証し、passした場合のみ設定を更新します. * 実際の検証処理はサービス内で定義されている各captchaプロバイダの検証関数に委譲します. * + * @param provider 検証するcaptchaのプロバイダ * @param params - * @param params.provider 検証するcaptchaのプロバイダ - * @param params.sitekey mcaptchaの場合に指定するsitekey. それ以外のプロバイダでは無視されます + * @param params.sitekey hcaptcha, recaptcha, turnstile, mcaptchaの場合に指定するsitekey. それ以外のプロバイダでは無視されます * @param params.secret hcaptcha, recaptcha, turnstile, mcaptchaの場合に指定するsecret. それ以外のプロバイダでは無視されます * @param params.instanceUrl mcaptchaの場合に指定するインスタンスのURL. それ以外のプロバイダでは無視されます * @param params.captchaResult フロントエンド側で受け取ったcaptchaプロバイダからの戻り値. この値を使ってサーバサイドでの検証を行います - * * @see verifyHcaptcha * @see verifyMcaptcha * @see verifyRecaptcha @@ -183,56 +185,70 @@ export class CaptchaService { * @see verifyTestcaptcha */ @bindThis - public async verify(params: { - provider: CaptchaProvider; - sitekey?: string; - secret?: string; - instanceUrl?: string; - captchaResult?: string | null; - }): Promise { - if (!supportedCaptchaProviders.includes(params.provider)) { + public async save( + provider: CaptchaProvider, + params?: { + sitekey?: string | null; + secret?: string | null; + instanceUrl?: string | null; + captchaResult?: string | null; + }, + ): Promise { + if (!supportedCaptchaProviders.includes(provider)) { return { success: false, - error: new CaptchaError(captchaErrorCodes.invalidProvider, `Invalid captcha provider: ${params.provider}`), + error: new CaptchaError(captchaErrorCodes.invalidProvider, `Invalid captcha provider: ${provider}`), }; } const operation = { + none: async () => { + await this.updateMeta(provider, params); + }, hcaptcha: async () => { - if (!params.secret) { - throw new CaptchaError(captchaErrorCodes.invalidParameters, 'hcaptcha-failed: secret and response are required'); + if (!params?.secret || !params.captchaResult) { + throw new CaptchaError(captchaErrorCodes.invalidParameters, 'hcaptcha-failed: secret and captureResult are required'); } - return this.verifyHcaptcha(params.secret, params.captchaResult); + await this.verifyHcaptcha(params.secret, params.captchaResult); + await this.updateMeta(provider, params); }, mcaptcha: async () => { - if (!params.secret || !params.sitekey || !params.instanceUrl) { - throw new CaptchaError(captchaErrorCodes.invalidParameters, 'mcaptcha-failed: secret, sitekey, instanceUrl and response are required'); + if (!params?.secret || !params.sitekey || !params.instanceUrl || !params.captchaResult) { + throw new CaptchaError(captchaErrorCodes.invalidParameters, 'mcaptcha-failed: secret, sitekey, instanceUrl and captureResult are required'); } - return this.verifyMcaptcha(params.secret, params.sitekey, params.instanceUrl, params.captchaResult); + await this.verifyMcaptcha(params.secret, params.sitekey, params.instanceUrl, params.captchaResult); + await this.updateMeta(provider, params); }, recaptcha: async () => { - if (!params.secret) { - throw new CaptchaError(captchaErrorCodes.invalidParameters, 'recaptcha-failed: secret and response are required'); + if (!params?.secret || !params.captchaResult) { + throw new CaptchaError(captchaErrorCodes.invalidParameters, 'recaptcha-failed: secret and captureResult are required'); } - return this.verifyRecaptcha(params.secret, params.captchaResult); + await this.verifyRecaptcha(params.secret, params.captchaResult); + await this.updateMeta(provider, params); }, turnstile: async () => { - if (!params.secret) { - throw new CaptchaError(captchaErrorCodes.invalidParameters, 'turnstile-failed: secret and response are required'); + if (!params?.secret || !params.captchaResult) { + throw new CaptchaError(captchaErrorCodes.invalidParameters, 'turnstile-failed: secret and captureResult are required'); } - return this.verifyTurnstile(params.secret, params.captchaResult); + await this.verifyTurnstile(params.secret, params.captchaResult); + await this.updateMeta(provider, params); }, testcaptcha: async () => { - return this.verifyTestcaptcha(params.captchaResult); + if (!params?.captchaResult) { + throw new CaptchaError(captchaErrorCodes.invalidParameters, 'turnstile-failed: captureResult are required'); + } + + await this.verifyTestcaptcha(params.captchaResult); + await this.updateMeta(provider, params); }, - }[params.provider]; + }[provider]; return operation() - .then(() => ({ success: true }) as ValidateSuccess) + .then(() => ({ success: true }) as CaptchaSaveSuccess) .catch(err => { const error = err instanceof CaptchaError ? err @@ -243,5 +259,63 @@ export class CaptchaService { }; }); } + + @bindThis + private async updateMeta( + provider: CaptchaProvider, + params?: { + sitekey?: string | null; + secret?: string | null; + instanceUrl?: string | null; + }, + ) { + const metaPartial: Partial< + Pick< + MiMeta, + ('enableHcaptcha' | 'hcaptchaSiteKey' | 'hcaptchaSecretKey') | + ('enableMcaptcha' | 'mcaptchaSitekey' | 'mcaptchaSecretKey' | 'mcaptchaInstanceUrl') | + ('enableRecaptcha' | 'recaptchaSiteKey' | 'recaptchaSecretKey') | + ('enableTurnstile' | 'turnstileSiteKey' | 'turnstileSecretKey') | + ('enableTestcaptcha') + > + > = { + enableHcaptcha: provider === 'hcaptcha', + enableMcaptcha: provider === 'mcaptcha', + enableRecaptcha: provider === 'recaptcha', + enableTurnstile: provider === 'turnstile', + enableTestcaptcha: provider === 'testcaptcha', + }; + + const updateIfNotUndefined = (key: K, value: typeof metaPartial[K]) => { + if (value !== undefined) { + metaPartial[key] = value; + } + }; + switch (provider) { + case 'hcaptcha': { + updateIfNotUndefined('hcaptchaSiteKey', params?.sitekey); + updateIfNotUndefined('hcaptchaSecretKey', params?.secret); + break; + } + case 'mcaptcha': { + updateIfNotUndefined('mcaptchaSitekey', params?.sitekey); + updateIfNotUndefined('mcaptchaSecretKey', params?.secret); + updateIfNotUndefined('mcaptchaInstanceUrl', params?.instanceUrl); + break; + } + case 'recaptcha': { + updateIfNotUndefined('recaptchaSiteKey', params?.sitekey); + updateIfNotUndefined('recaptchaSecretKey', params?.secret); + break; + } + case 'turnstile': { + updateIfNotUndefined('turnstileSiteKey', params?.sitekey); + updateIfNotUndefined('turnstileSecretKey', params?.secret); + break; + } + } + + await this.metaService.update(metaPartial); + } } diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index b71b6cb4d6..ce74109539 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -28,7 +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_test from './endpoints/admin/captcha/test.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'; import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.js'; @@ -417,7 +417,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_test: Provider = { provide: 'ep:admin/captcha/test', useClass: ep___admin_captcha_test.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 }; const $admin_unsetUserBanner: Provider = { provide: 'ep:admin/unset-user-banner', useClass: ep___admin_unsetUserBanner.default }; @@ -810,7 +810,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_avatarDecorations_delete, $admin_avatarDecorations_list, $admin_avatarDecorations_update, - $admin_captcha_test, + $admin_captcha_save, $admin_deleteAllFilesOfAUser, $admin_unsetUserAvatar, $admin_unsetUserBanner, @@ -1197,7 +1197,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_avatarDecorations_delete, $admin_avatarDecorations_list, $admin_avatarDecorations_update, - $admin_captcha_test, + $admin_captcha_save, $admin_deleteAllFilesOfAUser, $admin_unsetUserAvatar, $admin_unsetUserBanner, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 5fdcc86790..68c333a7cf 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -33,7 +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_test from './endpoints/admin/captcha/test.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'; import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.js'; @@ -421,7 +421,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/test', ep___admin_captcha_test], + ['admin/captcha/test', ep___admin_captcha_save], ['admin/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser], ['admin/unset-user-avatar', ep___admin_unsetUserAvatar], ['admin/unset-user-banner', ep___admin_unsetUserBanner], diff --git a/packages/backend/src/server/api/endpoints/admin/captcha/test.ts b/packages/backend/src/server/api/endpoints/admin/captcha/save.ts similarity index 85% rename from packages/backend/src/server/api/endpoints/admin/captcha/test.ts rename to packages/backend/src/server/api/endpoints/admin/captcha/save.ts index 622a2e652d..ad41e0b361 100644 --- a/packages/backend/src/server/api/endpoints/admin/captcha/test.ts +++ b/packages/backend/src/server/api/endpoints/admin/captcha/save.ts @@ -46,17 +46,17 @@ export const paramDef = { type: 'string', enum: supportedCaptchaProviders, }, + captchaResult: { + type: 'string', nullable: true, + }, sitekey: { - type: 'string', + type: 'string', nullable: true, }, secret: { - type: 'string', + type: 'string', nullable: true, }, instanceUrl: { - type: 'string', - }, - captchaResult: { - type: 'string', + type: 'string', nullable: true, }, }, required: ['provider'], @@ -67,13 +67,11 @@ export default class extends Endpoint { // eslint- constructor( private captchaService: CaptchaService, ) { - super(meta, paramDef, async (ps, me) => { - const result = await this.captchaService.verify({ - provider: ps.provider, + super(meta, paramDef, async (ps) => { + const result = await this.captchaService.save(ps.provider, ps.captchaResult, { sitekey: ps.sitekey, secret: ps.secret, instanceUrl: ps.instanceUrl, - captchaResult: ps.captchaResult, }); if (result.success) { diff --git a/packages/backend/test/unit/CaptchaService.ts b/packages/backend/test/unit/CaptchaService.ts index 41fa53e154..2d7167f95a 100644 --- a/packages/backend/test/unit/CaptchaService.ts +++ b/packages/backend/test/unit/CaptchaService.ts @@ -10,16 +10,19 @@ import { CaptchaError, CaptchaErrorCode, captchaErrorCodes, + CaptchaSaveResult, CaptchaService, - ValidateResult, } from '@/core/CaptchaService.js'; import { GlobalModule } from '@/GlobalModule.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; +import { MetaService } from '@/core/MetaService.js'; +import { MiMeta } from '@/models/Meta.js'; describe('CaptchaService', () => { let app: TestingModule; let service: CaptchaService; let httpRequestService: jest.Mocked; + let metaService: jest.Mocked; beforeAll(async () => { app = await Test.createTestingModule({ @@ -31,6 +34,9 @@ describe('CaptchaService', () => { { provide: HttpRequestService, useFactory: () => ({ send: jest.fn() }), }, + { + provide: MetaService, useFactory: () => ({ update: jest.fn() }), + }, ], }).compile(); @@ -38,10 +44,12 @@ describe('CaptchaService', () => { service = app.get(CaptchaService); httpRequestService = app.get(HttpRequestService) as jest.Mocked; + metaService = app.get(MetaService) as jest.Mocked; }); beforeEach(() => { httpRequestService.send.mockClear(); + metaService.update.mockClear(); }); afterAll(async () => { @@ -76,7 +84,6 @@ describe('CaptchaService', () => { await test(); expect(false).toBe(true); } catch (e) { - console.log(e); expect(e instanceof CaptchaError).toBe(true); const _e = e as CaptchaError; @@ -184,81 +191,194 @@ describe('CaptchaService', () => { }); }); - describe('validateSettings', () => { + describe('save', () => { const host = 'https://localhost'; - describe('success', () => { + describe('[success] 検証に成功した時だけ保存できる+他のプロバイダの設定値を誤って更新しない', () => { beforeEach(() => { successMock({ success: true, valid: true }); }); - async function assertSuccess(promise: Promise) { + async function assertSuccess(promise: Promise, expectMeta: Partial) { await expect(promise) .resolves .toStrictEqual({ success: true }); + const partialParams = metaService.update.mock.calls[0][0]; + expect(partialParams).toStrictEqual(expectMeta); } + test('none', async () => { + await assertSuccess( + service.save('none'), + { + enableHcaptcha: false, + enableMcaptcha: false, + enableRecaptcha: false, + enableTurnstile: false, + enableTestcaptcha: false, + }, + ); + }); + test('hcaptcha', async () => { - await assertSuccess(service.verify({ provider: 'hcaptcha', secret: 'secret', captchaResult: 'response' })); + await assertSuccess( + service.save('hcaptcha', { + sitekey: 'hcaptcha-sitekey', + secret: 'hcaptcha-secret', + captchaResult: 'hcaptcha-passed', + }), + { + enableHcaptcha: true, + enableMcaptcha: false, + enableRecaptcha: false, + enableTurnstile: false, + enableTestcaptcha: false, + hcaptchaSiteKey: 'hcaptcha-sitekey', + hcaptchaSecretKey: 'hcaptcha-secret', + }, + ); }); test('mcaptcha', async () => { - await assertSuccess(service.verify({ - provider: 'mcaptcha', - secret: 'secret', - sitekey: 'sitekey', - instanceUrl: host, - captchaResult: 'response', - })); + await assertSuccess( + service.save('mcaptcha', { + sitekey: 'mcaptcha-sitekey', + secret: 'mcaptcha-secret', + instanceUrl: host, + captchaResult: 'mcaptcha-passed', + }), + { + enableHcaptcha: false, + enableMcaptcha: true, + enableRecaptcha: false, + enableTurnstile: false, + enableTestcaptcha: false, + mcaptchaSitekey: 'mcaptcha-sitekey', + mcaptchaSecretKey: 'mcaptcha-secret', + mcaptchaInstanceUrl: host, + }, + ); }); test('recaptcha', async () => { - await assertSuccess(service.verify({ provider: 'recaptcha', secret: 'secret', captchaResult: 'response' })); + await assertSuccess( + service.save('recaptcha', { + sitekey: 'recaptcha-sitekey', + secret: 'recaptcha-secret', + captchaResult: 'recaptcha-passed', + }), + { + enableHcaptcha: false, + enableMcaptcha: false, + enableRecaptcha: true, + enableTurnstile: false, + enableTestcaptcha: false, + recaptchaSiteKey: 'recaptcha-sitekey', + recaptchaSecretKey: 'recaptcha-secret', + }, + ); }); test('turnstile', async () => { - await assertSuccess(service.verify({ provider: 'turnstile', secret: 'secret', captchaResult: 'response' })); + await assertSuccess( + service.save('turnstile', { + sitekey: 'turnstile-sitekey', + secret: 'turnstile-secret', + captchaResult: 'turnstile-passed', + }), + { + enableHcaptcha: false, + enableMcaptcha: false, + enableRecaptcha: false, + enableTurnstile: true, + enableTestcaptcha: false, + turnstileSiteKey: 'turnstile-sitekey', + turnstileSecretKey: 'turnstile-secret', + }, + ); }); test('testcaptcha', async () => { - await assertSuccess(service.verify({ provider: 'testcaptcha', captchaResult: 'testcaptcha-passed' })); + await assertSuccess( + service.save('testcaptcha', { + sitekey: 'testcaptcha-sitekey', + secret: 'testcaptcha-secret', + captchaResult: 'testcaptcha-passed', + }), + { + enableHcaptcha: false, + enableMcaptcha: false, + enableRecaptcha: false, + enableTurnstile: false, + enableTestcaptcha: true, + }, + ); }); }); - describe('failure', () => { - async function assertFailure(code: CaptchaErrorCode, promise: Promise) { + describe('[failure] 検証に失敗した場合は保存できない+設定値の更新そのものが発生しない', () => { + async function assertFailure(code: CaptchaErrorCode, promise: Promise) { const res = await promise; expect(res.success).toBe(false); if (!res.success) { expect(res.error.code).toBe(code); } + expect(metaService.update).not.toBeCalled(); } - describe('noResponseProvided', () => { + describe('invalidParameters', () => { test('hcaptcha', async () => { - await assertFailure(captchaErrorCodes.noResponseProvided, service.verify({ provider: 'hcaptcha', secret: 'secret', captchaResult: null })); + await assertFailure( + captchaErrorCodes.invalidParameters, + service.save('hcaptcha', { + sitekey: 'hcaptcha-sitekey', + secret: 'hcaptcha-secret', + captchaResult: null, + }), + ); }); test('mcaptcha', async () => { - await assertFailure(captchaErrorCodes.noResponseProvided, service.verify({ - provider: 'mcaptcha', - secret: 'secret', - sitekey: 'sitekey', - instanceUrl: host, - captchaResult: null, - })); + await assertFailure( + captchaErrorCodes.invalidParameters, + service.save('mcaptcha', { + sitekey: 'mcaptcha-sitekey', + secret: 'mcaptcha-secret', + instanceUrl: host, + captchaResult: null, + }), + ); }); test('recaptcha', async () => { - await assertFailure(captchaErrorCodes.noResponseProvided, service.verify({ provider: 'recaptcha', secret: 'secret', captchaResult: null })); + await assertFailure( + captchaErrorCodes.invalidParameters, + service.save('recaptcha', { + sitekey: 'recaptcha-sitekey', + secret: 'recaptcha-secret', + captchaResult: null, + }), + ); }); test('turnstile', async () => { - await assertFailure(captchaErrorCodes.noResponseProvided, service.verify({ provider: 'turnstile', secret: 'secret', captchaResult: null })); + await assertFailure( + captchaErrorCodes.invalidParameters, + service.save('turnstile', { + sitekey: 'turnstile-sitekey', + secret: 'turnstile-secret', + captchaResult: null, + }), + ); }); test('testcaptcha', async () => { - await assertFailure(captchaErrorCodes.noResponseProvided, service.verify({ provider: 'testcaptcha', captchaResult: null })); + await assertFailure( + captchaErrorCodes.invalidParameters, + service.save('testcaptcha', { + captchaResult: null, + }), + ); }); }); @@ -268,29 +388,51 @@ describe('CaptchaService', () => { }); test('hcaptcha', async () => { - await assertFailure(captchaErrorCodes.requestFailed, service.verify({ provider: 'hcaptcha', secret: 'secret', captchaResult: 'res' })); + await assertFailure( + captchaErrorCodes.requestFailed, + service.save('hcaptcha', { + sitekey: 'hcaptcha-sitekey', + secret: 'hcaptcha-secret', + captchaResult: 'hcaptcha-passed', + }), + ); }); test('mcaptcha', async () => { - await assertFailure(captchaErrorCodes.requestFailed, service.verify({ - provider: 'mcaptcha', - secret: 'secret', - sitekey: 'sitekey', - instanceUrl: host, - captchaResult: 'res', - })); + await assertFailure( + captchaErrorCodes.requestFailed, + service.save('mcaptcha', { + sitekey: 'mcaptcha-sitekey', + secret: 'mcaptcha-secret', + instanceUrl: host, + captchaResult: 'mcaptcha-passed', + }), + ); }); test('recaptcha', async () => { - await assertFailure(captchaErrorCodes.requestFailed, service.verify({ provider: 'recaptcha', secret: 'secret', captchaResult: 'res' })); + await assertFailure( + captchaErrorCodes.requestFailed, + service.save('recaptcha', { + sitekey: 'recaptcha-sitekey', + secret: 'recaptcha-secret', + captchaResult: 'recaptcha-passed', + }), + ); }); test('turnstile', async () => { - await assertFailure(captchaErrorCodes.requestFailed, service.verify({ provider: 'turnstile', secret: 'secret', captchaResult: 'res' })); + await assertFailure( + captchaErrorCodes.requestFailed, + service.save('turnstile', { + sitekey: 'turnstile-sitekey', + secret: 'turnstile-secret', + captchaResult: 'turnstile-passed', + }), + ); }); - // testcaptchaはrequestFailedが発生しない - // test('testcaptcha', () => {}); + // testchapchaはrequestFailedがない }); describe('verificationFailed', () => { @@ -299,29 +441,57 @@ describe('CaptchaService', () => { }); test('hcaptcha', async () => { - await assertFailure(captchaErrorCodes.verificationFailed, service.verify({ provider: 'hcaptcha', secret: 'secret', captchaResult: 'res' })); + await assertFailure( + captchaErrorCodes.verificationFailed, + service.save('hcaptcha', { + sitekey: 'hcaptcha-sitekey', + secret: 'hcaptcha-secret', + captchaResult: 'hccaptcha-passed', + }), + ); }); test('mcaptcha', async () => { - await assertFailure(captchaErrorCodes.verificationFailed, service.verify({ - provider: 'mcaptcha', - secret: 'secret', - sitekey: 'sitekey', - instanceUrl: host, - captchaResult: 'res', - })); + await assertFailure( + captchaErrorCodes.verificationFailed, + service.save('mcaptcha', { + sitekey: 'mcaptcha-sitekey', + secret: 'mcaptcha-secret', + instanceUrl: host, + captchaResult: 'mcaptcha-passed', + }), + ); }); test('recaptcha', async () => { - await assertFailure(captchaErrorCodes.verificationFailed, service.verify({ provider: 'recaptcha', secret: 'secret', captchaResult: 'res' })); + await assertFailure( + captchaErrorCodes.verificationFailed, + service.save('recaptcha', { + sitekey: 'recaptcha-sitekey', + secret: 'recaptcha-secret', + captchaResult: 'recaptcha-passed', + }), + ); }); test('turnstile', async () => { - await assertFailure(captchaErrorCodes.verificationFailed, service.verify({ provider: 'turnstile', secret: 'secret', captchaResult: 'res' })); + await assertFailure( + captchaErrorCodes.verificationFailed, + service.save('turnstile', { + sitekey: 'turnstile-sitekey', + secret: 'turnstile-secret', + captchaResult: 'turnstile-passed', + }), + ); }); test('testcaptcha', async () => { - await assertFailure(captchaErrorCodes.verificationFailed, service.verify({ provider: 'testcaptcha', captchaResult: 'testcaptcha-failed' })); + await assertFailure( + captchaErrorCodes.verificationFailed, + service.save('testcaptcha', { + captchaResult: 'testcaptcha-failed', + }), + ); }); }); }); diff --git a/packages/frontend/src/pages/admin/bot-protection.vue b/packages/frontend/src/pages/admin/bot-protection.vue index 4be2f467da..3e6810240b 100644 --- a/packages/frontend/src/pages/admin/bot-protection.vue +++ b/packages/frontend/src/pages/admin/bot-protection.vue @@ -258,9 +258,9 @@ watch(captchaResult, async () => { if (captchaResult.value) { const result = await misskeyApi('admin/captcha/test', { provider: provider as Misskey.entities.AdminCaptchaTestRequest['provider'], - sitekey: sitekey ?? undefined, - secret: secret ?? undefined, - instanceUrl: botProtectionForm.state.mcaptchaInstanceUrl ?? undefined, + sitekey: sitekey, + secret: secret, + instanceUrl: botProtectionForm.state.mcaptchaInstanceUrl, captchaResult: captchaResult.value, }); diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 5809dc0a20..db25600b7f 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -6586,11 +6586,11 @@ export type operations = { content: { 'application/json': { /** @enum {string} */ - provider: 'hcaptcha' | 'mcaptcha' | 'recaptcha' | 'turnstile' | 'testcaptcha'; - sitekey?: string; - secret?: string; - instanceUrl?: string; - captchaResult?: string; + provider: 'none' | 'hcaptcha' | 'mcaptcha' | 'recaptcha' | 'turnstile' | 'testcaptcha'; + captchaResult?: string | null; + sitekey?: string | null; + secret?: string | null; + instanceUrl?: string | null; }; }; };