mirror of
https://github.com/misskey-dev/misskey.git
synced 2024-12-28 16:38:23 +01:00
fix
This commit is contained in:
parent
3dade7a577
commit
ce7f2054c8
7 changed files with 351 additions and 109 deletions
|
@ -6,8 +6,10 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
import { bindThis } from '@/decorators.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 type CaptchaProvider = typeof supportedCaptchaProviders[number];
|
||||||
|
|
||||||
export const captchaErrorCodes = {
|
export const captchaErrorCodes = {
|
||||||
|
@ -30,14 +32,14 @@ export class CaptchaError extends Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ValidateSuccess = {
|
export type CaptchaSaveSuccess = {
|
||||||
success: true;
|
success: true;
|
||||||
}
|
}
|
||||||
export type ValidateFailure = {
|
export type CaptchaSaveFailure = {
|
||||||
success: false;
|
success: false;
|
||||||
error: CaptchaError;
|
error: CaptchaError;
|
||||||
}
|
}
|
||||||
export type ValidateResult = ValidateSuccess | ValidateFailure;
|
export type CaptchaSaveResult = CaptchaSaveSuccess | CaptchaSaveFailure;
|
||||||
|
|
||||||
type CaptchaResponse = {
|
type CaptchaResponse = {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
@ -48,6 +50,7 @@ type CaptchaResponse = {
|
||||||
export class CaptchaService {
|
export class CaptchaService {
|
||||||
constructor(
|
constructor(
|
||||||
private httpRequestService: HttpRequestService,
|
private httpRequestService: HttpRequestService,
|
||||||
|
private metaService: MetaService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,16 +169,15 @@ export class CaptchaService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* フロントエンド側で受け取ったcaptchaからの戻り値を検証します.
|
* captchaの設定を更新します. その際、フロントエンド側で受け取ったcaptchaからの戻り値を検証し、passした場合のみ設定を更新します.
|
||||||
* 実際の検証処理はサービス内で定義されている各captchaプロバイダの検証関数に委譲します.
|
* 実際の検証処理はサービス内で定義されている各captchaプロバイダの検証関数に委譲します.
|
||||||
*
|
*
|
||||||
|
* @param provider 検証するcaptchaのプロバイダ
|
||||||
* @param params
|
* @param params
|
||||||
* @param params.provider 検証するcaptchaのプロバイダ
|
* @param params.sitekey hcaptcha, recaptcha, turnstile, mcaptchaの場合に指定するsitekey. それ以外のプロバイダでは無視されます
|
||||||
* @param params.sitekey mcaptchaの場合に指定するsitekey. それ以外のプロバイダでは無視されます
|
|
||||||
* @param params.secret hcaptcha, recaptcha, turnstile, mcaptchaの場合に指定するsecret. それ以外のプロバイダでは無視されます
|
* @param params.secret hcaptcha, recaptcha, turnstile, mcaptchaの場合に指定するsecret. それ以外のプロバイダでは無視されます
|
||||||
* @param params.instanceUrl mcaptchaの場合に指定するインスタンスのURL. それ以外のプロバイダでは無視されます
|
* @param params.instanceUrl mcaptchaの場合に指定するインスタンスのURL. それ以外のプロバイダでは無視されます
|
||||||
* @param params.captchaResult フロントエンド側で受け取ったcaptchaプロバイダからの戻り値. この値を使ってサーバサイドでの検証を行います
|
* @param params.captchaResult フロントエンド側で受け取ったcaptchaプロバイダからの戻り値. この値を使ってサーバサイドでの検証を行います
|
||||||
*
|
|
||||||
* @see verifyHcaptcha
|
* @see verifyHcaptcha
|
||||||
* @see verifyMcaptcha
|
* @see verifyMcaptcha
|
||||||
* @see verifyRecaptcha
|
* @see verifyRecaptcha
|
||||||
|
@ -183,56 +185,70 @@ export class CaptchaService {
|
||||||
* @see verifyTestcaptcha
|
* @see verifyTestcaptcha
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async verify(params: {
|
public async save(
|
||||||
provider: CaptchaProvider;
|
provider: CaptchaProvider,
|
||||||
sitekey?: string;
|
params?: {
|
||||||
secret?: string;
|
sitekey?: string | null;
|
||||||
instanceUrl?: string;
|
secret?: string | null;
|
||||||
captchaResult?: string | null;
|
instanceUrl?: string | null;
|
||||||
}): Promise<ValidateResult> {
|
captchaResult?: string | null;
|
||||||
if (!supportedCaptchaProviders.includes(params.provider)) {
|
},
|
||||||
|
): Promise<CaptchaSaveResult> {
|
||||||
|
if (!supportedCaptchaProviders.includes(provider)) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: new CaptchaError(captchaErrorCodes.invalidProvider, `Invalid captcha provider: ${params.provider}`),
|
error: new CaptchaError(captchaErrorCodes.invalidProvider, `Invalid captcha provider: ${provider}`),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const operation = {
|
const operation = {
|
||||||
|
none: async () => {
|
||||||
|
await this.updateMeta(provider, params);
|
||||||
|
},
|
||||||
hcaptcha: async () => {
|
hcaptcha: async () => {
|
||||||
if (!params.secret) {
|
if (!params?.secret || !params.captchaResult) {
|
||||||
throw new CaptchaError(captchaErrorCodes.invalidParameters, 'hcaptcha-failed: secret and response are required');
|
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 () => {
|
mcaptcha: async () => {
|
||||||
if (!params.secret || !params.sitekey || !params.instanceUrl) {
|
if (!params?.secret || !params.sitekey || !params.instanceUrl || !params.captchaResult) {
|
||||||
throw new CaptchaError(captchaErrorCodes.invalidParameters, 'mcaptcha-failed: secret, sitekey, instanceUrl and response are required');
|
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 () => {
|
recaptcha: async () => {
|
||||||
if (!params.secret) {
|
if (!params?.secret || !params.captchaResult) {
|
||||||
throw new CaptchaError(captchaErrorCodes.invalidParameters, 'recaptcha-failed: secret and response are required');
|
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 () => {
|
turnstile: async () => {
|
||||||
if (!params.secret) {
|
if (!params?.secret || !params.captchaResult) {
|
||||||
throw new CaptchaError(captchaErrorCodes.invalidParameters, 'turnstile-failed: secret and response are required');
|
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 () => {
|
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()
|
return operation()
|
||||||
.then(() => ({ success: true }) as ValidateSuccess)
|
.then(() => ({ success: true }) as CaptchaSaveSuccess)
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
const error = err instanceof CaptchaError
|
const error = err instanceof CaptchaError
|
||||||
? err
|
? 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 = <K extends keyof typeof metaPartial>(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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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_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_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_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_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_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js';
|
||||||
import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.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_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_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_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_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_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 };
|
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_delete,
|
||||||
$admin_avatarDecorations_list,
|
$admin_avatarDecorations_list,
|
||||||
$admin_avatarDecorations_update,
|
$admin_avatarDecorations_update,
|
||||||
$admin_captcha_test,
|
$admin_captcha_save,
|
||||||
$admin_deleteAllFilesOfAUser,
|
$admin_deleteAllFilesOfAUser,
|
||||||
$admin_unsetUserAvatar,
|
$admin_unsetUserAvatar,
|
||||||
$admin_unsetUserBanner,
|
$admin_unsetUserBanner,
|
||||||
|
@ -1197,7 +1197,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||||
$admin_avatarDecorations_delete,
|
$admin_avatarDecorations_delete,
|
||||||
$admin_avatarDecorations_list,
|
$admin_avatarDecorations_list,
|
||||||
$admin_avatarDecorations_update,
|
$admin_avatarDecorations_update,
|
||||||
$admin_captcha_test,
|
$admin_captcha_save,
|
||||||
$admin_deleteAllFilesOfAUser,
|
$admin_deleteAllFilesOfAUser,
|
||||||
$admin_unsetUserAvatar,
|
$admin_unsetUserAvatar,
|
||||||
$admin_unsetUserBanner,
|
$admin_unsetUserBanner,
|
||||||
|
|
|
@ -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_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_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_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_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_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js';
|
||||||
import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.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/delete', ep___admin_avatarDecorations_delete],
|
||||||
['admin/avatar-decorations/list', ep___admin_avatarDecorations_list],
|
['admin/avatar-decorations/list', ep___admin_avatarDecorations_list],
|
||||||
['admin/avatar-decorations/update', ep___admin_avatarDecorations_update],
|
['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/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser],
|
||||||
['admin/unset-user-avatar', ep___admin_unsetUserAvatar],
|
['admin/unset-user-avatar', ep___admin_unsetUserAvatar],
|
||||||
['admin/unset-user-banner', ep___admin_unsetUserBanner],
|
['admin/unset-user-banner', ep___admin_unsetUserBanner],
|
||||||
|
|
|
@ -46,17 +46,17 @@ export const paramDef = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
enum: supportedCaptchaProviders,
|
enum: supportedCaptchaProviders,
|
||||||
},
|
},
|
||||||
|
captchaResult: {
|
||||||
|
type: 'string', nullable: true,
|
||||||
|
},
|
||||||
sitekey: {
|
sitekey: {
|
||||||
type: 'string',
|
type: 'string', nullable: true,
|
||||||
},
|
},
|
||||||
secret: {
|
secret: {
|
||||||
type: 'string',
|
type: 'string', nullable: true,
|
||||||
},
|
},
|
||||||
instanceUrl: {
|
instanceUrl: {
|
||||||
type: 'string',
|
type: 'string', nullable: true,
|
||||||
},
|
|
||||||
captchaResult: {
|
|
||||||
type: 'string',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
required: ['provider'],
|
required: ['provider'],
|
||||||
|
@ -67,13 +67,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
constructor(
|
constructor(
|
||||||
private captchaService: CaptchaService,
|
private captchaService: CaptchaService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps) => {
|
||||||
const result = await this.captchaService.verify({
|
const result = await this.captchaService.save(ps.provider, ps.captchaResult, {
|
||||||
provider: ps.provider,
|
|
||||||
sitekey: ps.sitekey,
|
sitekey: ps.sitekey,
|
||||||
secret: ps.secret,
|
secret: ps.secret,
|
||||||
instanceUrl: ps.instanceUrl,
|
instanceUrl: ps.instanceUrl,
|
||||||
captchaResult: ps.captchaResult,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
|
@ -10,16 +10,19 @@ import {
|
||||||
CaptchaError,
|
CaptchaError,
|
||||||
CaptchaErrorCode,
|
CaptchaErrorCode,
|
||||||
captchaErrorCodes,
|
captchaErrorCodes,
|
||||||
|
CaptchaSaveResult,
|
||||||
CaptchaService,
|
CaptchaService,
|
||||||
ValidateResult,
|
|
||||||
} from '@/core/CaptchaService.js';
|
} from '@/core/CaptchaService.js';
|
||||||
import { GlobalModule } from '@/GlobalModule.js';
|
import { GlobalModule } from '@/GlobalModule.js';
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
|
import { MiMeta } from '@/models/Meta.js';
|
||||||
|
|
||||||
describe('CaptchaService', () => {
|
describe('CaptchaService', () => {
|
||||||
let app: TestingModule;
|
let app: TestingModule;
|
||||||
let service: CaptchaService;
|
let service: CaptchaService;
|
||||||
let httpRequestService: jest.Mocked<HttpRequestService>;
|
let httpRequestService: jest.Mocked<HttpRequestService>;
|
||||||
|
let metaService: jest.Mocked<MetaService>;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await Test.createTestingModule({
|
app = await Test.createTestingModule({
|
||||||
|
@ -31,6 +34,9 @@ describe('CaptchaService', () => {
|
||||||
{
|
{
|
||||||
provide: HttpRequestService, useFactory: () => ({ send: jest.fn() }),
|
provide: HttpRequestService, useFactory: () => ({ send: jest.fn() }),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: MetaService, useFactory: () => ({ update: jest.fn() }),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
|
@ -38,10 +44,12 @@ describe('CaptchaService', () => {
|
||||||
|
|
||||||
service = app.get(CaptchaService);
|
service = app.get(CaptchaService);
|
||||||
httpRequestService = app.get(HttpRequestService) as jest.Mocked<HttpRequestService>;
|
httpRequestService = app.get(HttpRequestService) as jest.Mocked<HttpRequestService>;
|
||||||
|
metaService = app.get(MetaService) as jest.Mocked<MetaService>;
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
httpRequestService.send.mockClear();
|
httpRequestService.send.mockClear();
|
||||||
|
metaService.update.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
@ -76,7 +84,6 @@ describe('CaptchaService', () => {
|
||||||
await test();
|
await test();
|
||||||
expect(false).toBe(true);
|
expect(false).toBe(true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
|
||||||
expect(e instanceof CaptchaError).toBe(true);
|
expect(e instanceof CaptchaError).toBe(true);
|
||||||
|
|
||||||
const _e = e as CaptchaError;
|
const _e = e as CaptchaError;
|
||||||
|
@ -184,81 +191,194 @@ describe('CaptchaService', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('validateSettings', () => {
|
describe('save', () => {
|
||||||
const host = 'https://localhost';
|
const host = 'https://localhost';
|
||||||
|
|
||||||
describe('success', () => {
|
describe('[success] 検証に成功した時だけ保存できる+他のプロバイダの設定値を誤って更新しない', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
successMock({ success: true, valid: true });
|
successMock({ success: true, valid: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
async function assertSuccess(promise: Promise<ValidateResult>) {
|
async function assertSuccess(promise: Promise<CaptchaSaveResult>, expectMeta: Partial<MiMeta>) {
|
||||||
await expect(promise)
|
await expect(promise)
|
||||||
.resolves
|
.resolves
|
||||||
.toStrictEqual({ success: true });
|
.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 () => {
|
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 () => {
|
test('mcaptcha', async () => {
|
||||||
await assertSuccess(service.verify({
|
await assertSuccess(
|
||||||
provider: 'mcaptcha',
|
service.save('mcaptcha', {
|
||||||
secret: 'secret',
|
sitekey: 'mcaptcha-sitekey',
|
||||||
sitekey: 'sitekey',
|
secret: 'mcaptcha-secret',
|
||||||
instanceUrl: host,
|
instanceUrl: host,
|
||||||
captchaResult: 'response',
|
captchaResult: 'mcaptcha-passed',
|
||||||
}));
|
}),
|
||||||
|
{
|
||||||
|
enableHcaptcha: false,
|
||||||
|
enableMcaptcha: true,
|
||||||
|
enableRecaptcha: false,
|
||||||
|
enableTurnstile: false,
|
||||||
|
enableTestcaptcha: false,
|
||||||
|
mcaptchaSitekey: 'mcaptcha-sitekey',
|
||||||
|
mcaptchaSecretKey: 'mcaptcha-secret',
|
||||||
|
mcaptchaInstanceUrl: host,
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('recaptcha', async () => {
|
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 () => {
|
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 () => {
|
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', () => {
|
describe('[failure] 検証に失敗した場合は保存できない+設定値の更新そのものが発生しない', () => {
|
||||||
async function assertFailure(code: CaptchaErrorCode, promise: Promise<ValidateResult>) {
|
async function assertFailure(code: CaptchaErrorCode, promise: Promise<CaptchaSaveResult>) {
|
||||||
const res = await promise;
|
const res = await promise;
|
||||||
expect(res.success).toBe(false);
|
expect(res.success).toBe(false);
|
||||||
if (!res.success) {
|
if (!res.success) {
|
||||||
expect(res.error.code).toBe(code);
|
expect(res.error.code).toBe(code);
|
||||||
}
|
}
|
||||||
|
expect(metaService.update).not.toBeCalled();
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('noResponseProvided', () => {
|
describe('invalidParameters', () => {
|
||||||
test('hcaptcha', async () => {
|
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 () => {
|
test('mcaptcha', async () => {
|
||||||
await assertFailure(captchaErrorCodes.noResponseProvided, service.verify({
|
await assertFailure(
|
||||||
provider: 'mcaptcha',
|
captchaErrorCodes.invalidParameters,
|
||||||
secret: 'secret',
|
service.save('mcaptcha', {
|
||||||
sitekey: 'sitekey',
|
sitekey: 'mcaptcha-sitekey',
|
||||||
instanceUrl: host,
|
secret: 'mcaptcha-secret',
|
||||||
captchaResult: null,
|
instanceUrl: host,
|
||||||
}));
|
captchaResult: null,
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('recaptcha', async () => {
|
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 () => {
|
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 () => {
|
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 () => {
|
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 () => {
|
test('mcaptcha', async () => {
|
||||||
await assertFailure(captchaErrorCodes.requestFailed, service.verify({
|
await assertFailure(
|
||||||
provider: 'mcaptcha',
|
captchaErrorCodes.requestFailed,
|
||||||
secret: 'secret',
|
service.save('mcaptcha', {
|
||||||
sitekey: 'sitekey',
|
sitekey: 'mcaptcha-sitekey',
|
||||||
instanceUrl: host,
|
secret: 'mcaptcha-secret',
|
||||||
captchaResult: 'res',
|
instanceUrl: host,
|
||||||
}));
|
captchaResult: 'mcaptcha-passed',
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('recaptcha', async () => {
|
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 () => {
|
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が発生しない
|
// testchapchaはrequestFailedがない
|
||||||
// test('testcaptcha', () => {});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('verificationFailed', () => {
|
describe('verificationFailed', () => {
|
||||||
|
@ -299,29 +441,57 @@ describe('CaptchaService', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('hcaptcha', async () => {
|
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 () => {
|
test('mcaptcha', async () => {
|
||||||
await assertFailure(captchaErrorCodes.verificationFailed, service.verify({
|
await assertFailure(
|
||||||
provider: 'mcaptcha',
|
captchaErrorCodes.verificationFailed,
|
||||||
secret: 'secret',
|
service.save('mcaptcha', {
|
||||||
sitekey: 'sitekey',
|
sitekey: 'mcaptcha-sitekey',
|
||||||
instanceUrl: host,
|
secret: 'mcaptcha-secret',
|
||||||
captchaResult: 'res',
|
instanceUrl: host,
|
||||||
}));
|
captchaResult: 'mcaptcha-passed',
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('recaptcha', async () => {
|
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 () => {
|
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 () => {
|
test('testcaptcha', async () => {
|
||||||
await assertFailure(captchaErrorCodes.verificationFailed, service.verify({ provider: 'testcaptcha', captchaResult: 'testcaptcha-failed' }));
|
await assertFailure(
|
||||||
|
captchaErrorCodes.verificationFailed,
|
||||||
|
service.save('testcaptcha', {
|
||||||
|
captchaResult: 'testcaptcha-failed',
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -258,9 +258,9 @@ watch(captchaResult, async () => {
|
||||||
if (captchaResult.value) {
|
if (captchaResult.value) {
|
||||||
const result = await misskeyApi('admin/captcha/test', {
|
const result = await misskeyApi('admin/captcha/test', {
|
||||||
provider: provider as Misskey.entities.AdminCaptchaTestRequest['provider'],
|
provider: provider as Misskey.entities.AdminCaptchaTestRequest['provider'],
|
||||||
sitekey: sitekey ?? undefined,
|
sitekey: sitekey,
|
||||||
secret: secret ?? undefined,
|
secret: secret,
|
||||||
instanceUrl: botProtectionForm.state.mcaptchaInstanceUrl ?? undefined,
|
instanceUrl: botProtectionForm.state.mcaptchaInstanceUrl,
|
||||||
captchaResult: captchaResult.value,
|
captchaResult: captchaResult.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -6586,11 +6586,11 @@ export type operations = {
|
||||||
content: {
|
content: {
|
||||||
'application/json': {
|
'application/json': {
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
provider: 'hcaptcha' | 'mcaptcha' | 'recaptcha' | 'turnstile' | 'testcaptcha';
|
provider: 'none' | 'hcaptcha' | 'mcaptcha' | 'recaptcha' | 'turnstile' | 'testcaptcha';
|
||||||
sitekey?: string;
|
captchaResult?: string | null;
|
||||||
secret?: string;
|
sitekey?: string | null;
|
||||||
instanceUrl?: string;
|
secret?: string | null;
|
||||||
captchaResult?: string;
|
instanceUrl?: string | null;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue