mirror of
https://github.com/misskey-dev/misskey.git
synced 2024-12-28 01:30:23 +01:00
add server-side verify
This commit is contained in:
parent
f1c894a81e
commit
738d86f734
11 changed files with 748 additions and 36 deletions
|
@ -7,6 +7,38 @@ import { Injectable } from '@nestjs/common';
|
|||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
export const supportedCaptchaProviders = ['hcaptcha', 'mcaptcha', 'recaptcha', 'turnstile', 'testcaptcha'] as const;
|
||||
export type CaptchaProvider = typeof supportedCaptchaProviders[number];
|
||||
|
||||
export const captchaErrorCodes = {
|
||||
invalidProvider: Symbol('invalidProvider'),
|
||||
invalidParameters: Symbol('invalidParameters'),
|
||||
noResponseProvided: Symbol('noResponseProvided'),
|
||||
requestFailed: Symbol('requestFailed'),
|
||||
verificationFailed: Symbol('verificationFailed'),
|
||||
unknown: Symbol('unknown'),
|
||||
} as const;
|
||||
export type CaptchaErrorCode = typeof captchaErrorCodes[keyof typeof captchaErrorCodes];
|
||||
|
||||
export class CaptchaError extends Error {
|
||||
public readonly code: CaptchaErrorCode;
|
||||
|
||||
constructor(code: CaptchaErrorCode, message: string) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
this.name = 'CaptchaError';
|
||||
}
|
||||
}
|
||||
|
||||
export type ValidateSuccess = {
|
||||
success: true;
|
||||
}
|
||||
export type ValidateFailure = {
|
||||
success: false;
|
||||
error: CaptchaError;
|
||||
}
|
||||
export type ValidateResult = ValidateSuccess | ValidateFailure;
|
||||
|
||||
type CaptchaResponse = {
|
||||
success: boolean;
|
||||
'error-codes'?: string[];
|
||||
|
@ -44,32 +76,32 @@ export class CaptchaService {
|
|||
@bindThis
|
||||
public async verifyRecaptcha(secret: string, response: string | null | undefined): Promise<void> {
|
||||
if (response == null) {
|
||||
throw new Error('recaptcha-failed: no response provided');
|
||||
throw new CaptchaError(captchaErrorCodes.noResponseProvided, 'recaptcha-failed: no response provided');
|
||||
}
|
||||
|
||||
const result = await this.getCaptchaResponse('https://www.recaptcha.net/recaptcha/api/siteverify', secret, response).catch(err => {
|
||||
throw new Error(`recaptcha-request-failed: ${err}`);
|
||||
throw new CaptchaError(captchaErrorCodes.requestFailed, `recaptcha-request-failed: ${err}`);
|
||||
});
|
||||
|
||||
if (result.success !== true) {
|
||||
const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : '';
|
||||
throw new Error(`recaptcha-failed: ${errorCodes}`);
|
||||
throw new CaptchaError(captchaErrorCodes.verificationFailed, `recaptcha-failed: ${errorCodes}`);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async verifyHcaptcha(secret: string, response: string | null | undefined): Promise<void> {
|
||||
if (response == null) {
|
||||
throw new Error('hcaptcha-failed: no response provided');
|
||||
throw new CaptchaError(captchaErrorCodes.noResponseProvided, 'hcaptcha-failed: no response provided');
|
||||
}
|
||||
|
||||
const result = await this.getCaptchaResponse('https://hcaptcha.com/siteverify', secret, response).catch(err => {
|
||||
throw new Error(`hcaptcha-request-failed: ${err}`);
|
||||
throw new CaptchaError(captchaErrorCodes.requestFailed, `hcaptcha-request-failed: ${err}`);
|
||||
});
|
||||
|
||||
if (result.success !== true) {
|
||||
const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : '';
|
||||
throw new Error(`hcaptcha-failed: ${errorCodes}`);
|
||||
throw new CaptchaError(captchaErrorCodes.verificationFailed, `hcaptcha-failed: ${errorCodes}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -77,7 +109,7 @@ export class CaptchaService {
|
|||
@bindThis
|
||||
public async verifyMcaptcha(secret: string, siteKey: string, instanceHost: string, response: string | null | undefined): Promise<void> {
|
||||
if (response == null) {
|
||||
throw new Error('mcaptcha-failed: no response provided');
|
||||
throw new CaptchaError(captchaErrorCodes.noResponseProvided, 'mcaptcha-failed: no response provided');
|
||||
}
|
||||
|
||||
const endpointUrl = new URL('/api/v1/pow/siteverify', instanceHost);
|
||||
|
@ -94,43 +126,122 @@ export class CaptchaService {
|
|||
});
|
||||
|
||||
if (result.status !== 200) {
|
||||
throw new Error('mcaptcha-failed: mcaptcha didn\'t return 200 OK');
|
||||
throw new CaptchaError(captchaErrorCodes.requestFailed, 'mcaptcha-failed: mcaptcha didn\'t return 200 OK');
|
||||
}
|
||||
|
||||
const resp = (await result.json()) as { valid: boolean };
|
||||
|
||||
if (!resp.valid) {
|
||||
throw new Error('mcaptcha-request-failed');
|
||||
throw new CaptchaError(captchaErrorCodes.verificationFailed, 'mcaptcha-request-failed');
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async verifyTurnstile(secret: string, response: string | null | undefined): Promise<void> {
|
||||
if (response == null) {
|
||||
throw new Error('turnstile-failed: no response provided');
|
||||
throw new CaptchaError(captchaErrorCodes.noResponseProvided, 'turnstile-failed: no response provided');
|
||||
}
|
||||
|
||||
const result = await this.getCaptchaResponse('https://challenges.cloudflare.com/turnstile/v0/siteverify', secret, response).catch(err => {
|
||||
throw new Error(`turnstile-request-failed: ${err}`);
|
||||
throw new CaptchaError(captchaErrorCodes.requestFailed, `turnstile-request-failed: ${err}`);
|
||||
});
|
||||
|
||||
if (result.success !== true) {
|
||||
const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : '';
|
||||
throw new Error(`turnstile-failed: ${errorCodes}`);
|
||||
throw new CaptchaError(captchaErrorCodes.verificationFailed, `turnstile-failed: ${errorCodes}`);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async verifyTestcaptcha(response: string | null | undefined): Promise<void> {
|
||||
if (response == null) {
|
||||
throw new Error('testcaptcha-failed: no response provided');
|
||||
throw new CaptchaError(captchaErrorCodes.noResponseProvided, 'testcaptcha-failed: no response provided');
|
||||
}
|
||||
|
||||
const success = response === 'testcaptcha-passed';
|
||||
|
||||
if (!success) {
|
||||
throw new Error('testcaptcha-failed');
|
||||
throw new CaptchaError(captchaErrorCodes.verificationFailed, 'testcaptcha-failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* フロントエンド側で受け取ったcaptchaからの戻り値を検証します.
|
||||
* 実際の検証処理はサービス内で定義されている各captchaプロバイダの検証関数に委譲します.
|
||||
*
|
||||
* @param params
|
||||
* @param params.provider 検証するcaptchaのプロバイダ
|
||||
* @param params.sitekey 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
|
||||
* @see verifyTurnstile
|
||||
* @see verifyTestcaptcha
|
||||
*/
|
||||
@bindThis
|
||||
public async verify(params: {
|
||||
provider: CaptchaProvider;
|
||||
sitekey?: string;
|
||||
secret?: string;
|
||||
instanceUrl?: string;
|
||||
captchaResult?: string | null;
|
||||
}): Promise<ValidateResult> {
|
||||
if (!supportedCaptchaProviders.includes(params.provider)) {
|
||||
return {
|
||||
success: false,
|
||||
error: new CaptchaError(captchaErrorCodes.invalidProvider, `Invalid captcha provider: ${params.provider}`),
|
||||
};
|
||||
}
|
||||
|
||||
const operation = {
|
||||
hcaptcha: async () => {
|
||||
if (!params.secret) {
|
||||
throw new CaptchaError(captchaErrorCodes.invalidParameters, 'hcaptcha-failed: secret and response are required');
|
||||
}
|
||||
|
||||
return this.verifyHcaptcha(params.secret, params.captchaResult);
|
||||
},
|
||||
mcaptcha: async () => {
|
||||
if (!params.secret || !params.sitekey || !params.instanceUrl) {
|
||||
throw new CaptchaError(captchaErrorCodes.invalidParameters, 'mcaptcha-failed: secret, sitekey, instanceUrl and response are required');
|
||||
}
|
||||
|
||||
return this.verifyMcaptcha(params.secret, params.sitekey, params.instanceUrl, params.captchaResult);
|
||||
},
|
||||
recaptcha: async () => {
|
||||
if (!params.secret) {
|
||||
throw new CaptchaError(captchaErrorCodes.invalidParameters, 'recaptcha-failed: secret and response are required');
|
||||
}
|
||||
|
||||
return this.verifyRecaptcha(params.secret, params.captchaResult);
|
||||
},
|
||||
turnstile: async () => {
|
||||
if (!params.secret) {
|
||||
throw new CaptchaError(captchaErrorCodes.invalidParameters, 'turnstile-failed: secret and response are required');
|
||||
}
|
||||
|
||||
return this.verifyTurnstile(params.secret, params.captchaResult);
|
||||
},
|
||||
testcaptcha: async () => {
|
||||
return this.verifyTestcaptcha(params.captchaResult);
|
||||
},
|
||||
}[params.provider];
|
||||
|
||||
return operation()
|
||||
.then(() => ({ success: true }) as ValidateSuccess)
|
||||
.catch(err => {
|
||||
const error = err instanceof CaptchaError
|
||||
? err
|
||||
: new CaptchaError(captchaErrorCodes.unknown, `unknown error: ${err}`);
|
||||
return {
|
||||
success: false,
|
||||
error,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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_test from './endpoints/admin/captcha/test.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';
|
||||
|
@ -416,6 +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_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 };
|
||||
|
@ -808,6 +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_deleteAllFilesOfAUser,
|
||||
$admin_unsetUserAvatar,
|
||||
$admin_unsetUserBanner,
|
||||
|
@ -1194,6 +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_deleteAllFilesOfAUser,
|
||||
$admin_unsetUserAvatar,
|
||||
$admin_unsetUserBanner,
|
||||
|
|
|
@ -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_test from './endpoints/admin/captcha/test.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';
|
||||
|
@ -420,6 +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/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser],
|
||||
['admin/unset-user-avatar', ep___admin_unsetUserAvatar],
|
||||
['admin/unset-user-banner', ep___admin_unsetUserBanner],
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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,
|
||||
requireModerator: true,
|
||||
secure: true,
|
||||
|
||||
kind: 'read:admin:captcha',
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
},
|
||||
error: {
|
||||
type: 'object',
|
||||
nullable: true,
|
||||
optional: false,
|
||||
properties: {
|
||||
code: {
|
||||
type: 'string',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
provider: {
|
||||
type: 'string',
|
||||
enum: supportedCaptchaProviders,
|
||||
},
|
||||
sitekey: {
|
||||
type: 'string',
|
||||
},
|
||||
secret: {
|
||||
type: 'string',
|
||||
},
|
||||
instanceUrl: {
|
||||
type: 'string',
|
||||
},
|
||||
captchaResult: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['provider'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private captchaService: CaptchaService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const result = await this.captchaService.verify({
|
||||
provider: ps.provider,
|
||||
sitekey: ps.sitekey,
|
||||
secret: ps.secret,
|
||||
instanceUrl: ps.instanceUrl,
|
||||
captchaResult: ps.captchaResult,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
return { success: true, error: null };
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: result.error.code.toString(),
|
||||
message: result.error.message,
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
324
packages/backend/test/unit/CaptchaService.ts
Normal file
324
packages/backend/test/unit/CaptchaService.ts
Normal file
|
@ -0,0 +1,324 @@
|
|||
import { afterAll, beforeAll, beforeEach, describe, expect, jest } from '@jest/globals';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Response } from 'node-fetch';
|
||||
import {
|
||||
CaptchaError,
|
||||
CaptchaErrorCode,
|
||||
captchaErrorCodes,
|
||||
CaptchaService,
|
||||
ValidateResult,
|
||||
} from '@/core/CaptchaService.js';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
|
||||
describe('CaptchaService', () => {
|
||||
let app: TestingModule;
|
||||
let service: CaptchaService;
|
||||
let httpRequestService: jest.Mocked<HttpRequestService>;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await Test.createTestingModule({
|
||||
imports: [
|
||||
GlobalModule,
|
||||
],
|
||||
providers: [
|
||||
CaptchaService,
|
||||
{
|
||||
provide: HttpRequestService, useFactory: () => ({ send: jest.fn() }),
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
app.enableShutdownHooks();
|
||||
|
||||
service = app.get(CaptchaService);
|
||||
httpRequestService = app.get(HttpRequestService) as jest.Mocked<HttpRequestService>;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
httpRequestService.send.mockClear();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
function successMock(result: object) {
|
||||
httpRequestService.send.mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => (result),
|
||||
} as Response);
|
||||
}
|
||||
|
||||
function failureHttpMock() {
|
||||
httpRequestService.send.mockResolvedValue({
|
||||
ok: false,
|
||||
status: 400,
|
||||
} as Response);
|
||||
}
|
||||
|
||||
function failureVerificationMock(result: object) {
|
||||
httpRequestService.send.mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => (result),
|
||||
} as Response);
|
||||
}
|
||||
|
||||
async function testCaptchaError(code: CaptchaErrorCode, test: () => Promise<void>) {
|
||||
try {
|
||||
await test();
|
||||
expect(false).toBe(true);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
expect(e instanceof CaptchaError).toBe(true);
|
||||
|
||||
const _e = e as CaptchaError;
|
||||
expect(_e.code).toBe(code);
|
||||
}
|
||||
}
|
||||
|
||||
describe('verifyRecaptcha', () => {
|
||||
test('success', async () => {
|
||||
successMock({ success: true });
|
||||
await service.verifyRecaptcha('secret', 'response');
|
||||
});
|
||||
|
||||
test('noResponseProvided', async () => {
|
||||
await testCaptchaError(captchaErrorCodes.noResponseProvided, () => service.verifyRecaptcha('secret', null));
|
||||
});
|
||||
|
||||
test('requestFailed', async () => {
|
||||
failureHttpMock();
|
||||
await testCaptchaError(captchaErrorCodes.requestFailed, () => service.verifyRecaptcha('secret', 'response'));
|
||||
});
|
||||
|
||||
test('verificationFailed', async () => {
|
||||
failureVerificationMock({ success: false, 'error-codes': ['code01', 'code02'] });
|
||||
await testCaptchaError(captchaErrorCodes.verificationFailed, () => service.verifyRecaptcha('secret', 'response'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyHcaptcha', () => {
|
||||
test('success', async () => {
|
||||
successMock({ success: true });
|
||||
await service.verifyHcaptcha('secret', 'response');
|
||||
});
|
||||
|
||||
test('noResponseProvided', async () => {
|
||||
await testCaptchaError(captchaErrorCodes.noResponseProvided, () => service.verifyHcaptcha('secret', null));
|
||||
});
|
||||
|
||||
test('requestFailed', async () => {
|
||||
failureHttpMock();
|
||||
await testCaptchaError(captchaErrorCodes.requestFailed, () => service.verifyHcaptcha('secret', 'response'));
|
||||
});
|
||||
|
||||
test('verificationFailed', async () => {
|
||||
failureVerificationMock({ success: false, 'error-codes': ['code01', 'code02'] });
|
||||
await testCaptchaError(captchaErrorCodes.verificationFailed, () => service.verifyHcaptcha('secret', 'response'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyMcaptcha', () => {
|
||||
const host = 'https://localhost';
|
||||
|
||||
test('success', async () => {
|
||||
successMock({ valid: true });
|
||||
await service.verifyMcaptcha('secret', 'sitekey', host, 'response');
|
||||
});
|
||||
|
||||
test('noResponseProvided', async () => {
|
||||
await testCaptchaError(captchaErrorCodes.noResponseProvided, () => service.verifyMcaptcha('secret', 'sitekey', host, null));
|
||||
});
|
||||
|
||||
test('requestFailed', async () => {
|
||||
failureHttpMock();
|
||||
await testCaptchaError(captchaErrorCodes.requestFailed, () => service.verifyMcaptcha('secret', 'sitekey', host, 'response'));
|
||||
});
|
||||
|
||||
test('verificationFailed', async () => {
|
||||
failureVerificationMock({ valid: false });
|
||||
await testCaptchaError(captchaErrorCodes.verificationFailed, () => service.verifyMcaptcha('secret', 'sitekey', host, 'response'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyTurnstile', () => {
|
||||
test('success', async () => {
|
||||
successMock({ success: true });
|
||||
await service.verifyTurnstile('secret', 'response');
|
||||
});
|
||||
|
||||
test('noResponseProvided', async () => {
|
||||
await testCaptchaError(captchaErrorCodes.noResponseProvided, () => service.verifyTurnstile('secret', null));
|
||||
});
|
||||
|
||||
test('requestFailed', async () => {
|
||||
failureHttpMock();
|
||||
await testCaptchaError(captchaErrorCodes.requestFailed, () => service.verifyTurnstile('secret', 'response'));
|
||||
});
|
||||
|
||||
test('verificationFailed', async () => {
|
||||
failureVerificationMock({ success: false, 'error-codes': ['code01', 'code02'] });
|
||||
await testCaptchaError(captchaErrorCodes.verificationFailed, () => service.verifyTurnstile('secret', 'response'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyTestcaptcha', () => {
|
||||
test('success', async () => {
|
||||
await service.verifyTestcaptcha('testcaptcha-passed');
|
||||
});
|
||||
|
||||
test('noResponseProvided', async () => {
|
||||
await testCaptchaError(captchaErrorCodes.noResponseProvided, () => service.verifyTestcaptcha(null));
|
||||
});
|
||||
|
||||
test('verificationFailed', async () => {
|
||||
await testCaptchaError(captchaErrorCodes.verificationFailed, () => service.verifyTestcaptcha('testcaptcha-failed'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateSettings', () => {
|
||||
const host = 'https://localhost';
|
||||
|
||||
describe('success', () => {
|
||||
beforeEach(() => {
|
||||
successMock({ success: true, valid: true });
|
||||
});
|
||||
|
||||
async function assertSuccess(promise: Promise<ValidateResult>) {
|
||||
await expect(promise)
|
||||
.resolves
|
||||
.toStrictEqual({ success: true });
|
||||
}
|
||||
|
||||
test('hcaptcha', async () => {
|
||||
await assertSuccess(service.verify({ provider: 'hcaptcha', secret: 'secret', captchaResult: 'response' }));
|
||||
});
|
||||
|
||||
test('mcaptcha', async () => {
|
||||
await assertSuccess(service.verify({
|
||||
provider: 'mcaptcha',
|
||||
secret: 'secret',
|
||||
sitekey: 'sitekey',
|
||||
instanceUrl: host,
|
||||
captchaResult: 'response',
|
||||
}));
|
||||
});
|
||||
|
||||
test('recaptcha', async () => {
|
||||
await assertSuccess(service.verify({ provider: 'recaptcha', secret: 'secret', captchaResult: 'response' }));
|
||||
});
|
||||
|
||||
test('turnstile', async () => {
|
||||
await assertSuccess(service.verify({ provider: 'turnstile', secret: 'secret', captchaResult: 'response' }));
|
||||
});
|
||||
|
||||
test('testcaptcha', async () => {
|
||||
await assertSuccess(service.verify({ provider: 'testcaptcha', captchaResult: 'testcaptcha-passed' }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('failure', () => {
|
||||
async function assertFailure(code: CaptchaErrorCode, promise: Promise<ValidateResult>) {
|
||||
const res = await promise;
|
||||
expect(res.success).toBe(false);
|
||||
if (!res.success) {
|
||||
expect(res.error.code).toBe(code);
|
||||
}
|
||||
}
|
||||
|
||||
describe('noResponseProvided', () => {
|
||||
test('hcaptcha', async () => {
|
||||
await assertFailure(captchaErrorCodes.noResponseProvided, service.verify({ provider: 'hcaptcha', secret: 'secret', captchaResult: null }));
|
||||
});
|
||||
|
||||
test('mcaptcha', async () => {
|
||||
await assertFailure(captchaErrorCodes.noResponseProvided, service.verify({
|
||||
provider: 'mcaptcha',
|
||||
secret: 'secret',
|
||||
sitekey: 'sitekey',
|
||||
instanceUrl: host,
|
||||
captchaResult: null,
|
||||
}));
|
||||
});
|
||||
|
||||
test('recaptcha', async () => {
|
||||
await assertFailure(captchaErrorCodes.noResponseProvided, service.verify({ provider: 'recaptcha', secret: 'secret', captchaResult: null }));
|
||||
});
|
||||
|
||||
test('turnstile', async () => {
|
||||
await assertFailure(captchaErrorCodes.noResponseProvided, service.verify({ provider: 'turnstile', secret: 'secret', captchaResult: null }));
|
||||
});
|
||||
|
||||
test('testcaptcha', async () => {
|
||||
await assertFailure(captchaErrorCodes.noResponseProvided, service.verify({ provider: 'testcaptcha', captchaResult: null }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestFailed', () => {
|
||||
beforeEach(() => {
|
||||
failureHttpMock();
|
||||
});
|
||||
|
||||
test('hcaptcha', async () => {
|
||||
await assertFailure(captchaErrorCodes.requestFailed, service.verify({ provider: 'hcaptcha', secret: 'secret', captchaResult: 'res' }));
|
||||
});
|
||||
|
||||
test('mcaptcha', async () => {
|
||||
await assertFailure(captchaErrorCodes.requestFailed, service.verify({
|
||||
provider: 'mcaptcha',
|
||||
secret: 'secret',
|
||||
sitekey: 'sitekey',
|
||||
instanceUrl: host,
|
||||
captchaResult: 'res',
|
||||
}));
|
||||
});
|
||||
|
||||
test('recaptcha', async () => {
|
||||
await assertFailure(captchaErrorCodes.requestFailed, service.verify({ provider: 'recaptcha', secret: 'secret', captchaResult: 'res' }));
|
||||
});
|
||||
|
||||
test('turnstile', async () => {
|
||||
await assertFailure(captchaErrorCodes.requestFailed, service.verify({ provider: 'turnstile', secret: 'secret', captchaResult: 'res' }));
|
||||
});
|
||||
|
||||
// testcaptchaはrequestFailedが発生しない
|
||||
// test('testcaptcha', () => {});
|
||||
});
|
||||
|
||||
describe('verificationFailed', () => {
|
||||
beforeEach(() => {
|
||||
failureVerificationMock({ success: false, valid: false, 'error-codes': ['code01', 'code02'] });
|
||||
});
|
||||
|
||||
test('hcaptcha', async () => {
|
||||
await assertFailure(captchaErrorCodes.verificationFailed, service.verify({ provider: 'hcaptcha', secret: 'secret', captchaResult: 'res' }));
|
||||
});
|
||||
|
||||
test('mcaptcha', async () => {
|
||||
await assertFailure(captchaErrorCodes.verificationFailed, service.verify({
|
||||
provider: 'mcaptcha',
|
||||
secret: 'secret',
|
||||
sitekey: 'sitekey',
|
||||
instanceUrl: host,
|
||||
captchaResult: 'res',
|
||||
}));
|
||||
});
|
||||
|
||||
test('recaptcha', async () => {
|
||||
await assertFailure(captchaErrorCodes.verificationFailed, service.verify({ provider: 'recaptcha', secret: 'secret', captchaResult: 'res' }));
|
||||
});
|
||||
|
||||
test('turnstile', async () => {
|
||||
await assertFailure(captchaErrorCodes.verificationFailed, service.verify({ provider: 'turnstile', secret: 'secret', captchaResult: 'res' }));
|
||||
});
|
||||
|
||||
test('testcaptcha', async () => {
|
||||
await assertFailure(captchaErrorCodes.verificationFailed, service.verify({ provider: 'testcaptcha', captchaResult: 'testcaptcha-failed' }));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -38,12 +38,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkInput>
|
||||
<FormSlot v-if="botProtectionForm.state.hcaptchaSiteKey">
|
||||
<template #label>{{ i18n.ts.preview }}</template>
|
||||
<MkCaptcha v-model="hCaptchaResponse" provider="hcaptcha" :sitekey="botProtectionForm.state.hcaptchaSiteKey"/>
|
||||
<MkCaptcha v-model="captchaResult" provider="hcaptcha" :sitekey="botProtectionForm.state.hcaptchaSiteKey"/>
|
||||
</FormSlot>
|
||||
<MkInfo>
|
||||
<div :class="$style.captchaInfoMsg">
|
||||
<div>サイトキーに"10000000-ffff-ffff-ffff-000000000001"と入力することで動作をテスト出来ます。<br/>本番運用時には必ず正規のサイトキーを設定してください。</div>
|
||||
<div>ref: <a href="https://docs.hcaptcha.com/#integration-testing-test-keys" target="_blank">hCaptcha Developer Guide</a></div>
|
||||
<div>
|
||||
サイトキーに"10000000-ffff-ffff-ffff-000000000001"と入力することで動作をテスト出来ます。<br/>本番運用時には必ず正規のサイトキーを設定してください。
|
||||
</div>
|
||||
<div>
|
||||
ref: <a href="https://docs.hcaptcha.com/#integration-testing-test-keys" target="_blank">hCaptcha
|
||||
Developer Guide</a>
|
||||
</div>
|
||||
</div>
|
||||
</MkInfo>
|
||||
</template>
|
||||
|
@ -63,7 +68,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkInput>
|
||||
<FormSlot v-if="botProtectionForm.state.mcaptchaSiteKey && botProtectionForm.state.mcaptchaInstanceUrl">
|
||||
<template #label>{{ i18n.ts.preview }}</template>
|
||||
<MkCaptcha v-model="mCaptchaResponse" provider="mcaptcha" :sitekey="botProtectionForm.state.mcaptchaSiteKey" :instanceUrl="botProtectionForm.state.mcaptchaInstanceUrl"/>
|
||||
<MkCaptcha
|
||||
v-model="captchaResult" provider="mcaptcha" :sitekey="botProtectionForm.state.mcaptchaSiteKey"
|
||||
:instanceUrl="botProtectionForm.state.mcaptchaInstanceUrl"
|
||||
/>
|
||||
</FormSlot>
|
||||
</template>
|
||||
|
||||
|
@ -78,12 +86,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkInput>
|
||||
<FormSlot v-if="botProtectionForm.state.recaptchaSiteKey">
|
||||
<template #label>{{ i18n.ts.preview }}</template>
|
||||
<MkCaptcha v-model="reCaptchaResponse" provider="recaptcha" :sitekey="botProtectionForm.state.recaptchaSiteKey"/>
|
||||
<MkCaptcha
|
||||
v-model="captchaResult" provider="recaptcha"
|
||||
:sitekey="botProtectionForm.state.recaptchaSiteKey"
|
||||
/>
|
||||
</FormSlot>
|
||||
<MkInfo>
|
||||
<div :class="$style.captchaInfoMsg">
|
||||
<div>サイトキーに"6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI"と入力することで動作をテスト出来ます。<br/>本番運用時には必ず正規のサイトキーを設定してください。</div>
|
||||
<div>ref: <a href="https://developers.google.com/recaptcha/docs/faq?hl=ja#id-like-to-run-automated-tests-with-recaptcha.-what-should-i-do" target="_blank">reCAPTCHA FAQ</a></div>
|
||||
<div>
|
||||
サイトキーに"6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI"と入力することで動作をテスト出来ます。<br/>本番運用時には必ず正規のサイトキーを設定してください。
|
||||
</div>
|
||||
<div>
|
||||
ref: <a
|
||||
href="https://developers.google.com/recaptcha/docs/faq?hl=ja#id-like-to-run-automated-tests-with-recaptcha.-what-should-i-do"
|
||||
target="_blank"
|
||||
>reCAPTCHA FAQ</a>
|
||||
</div>
|
||||
</div>
|
||||
</MkInfo>
|
||||
</template>
|
||||
|
@ -99,12 +117,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</MkInput>
|
||||
<FormSlot v-if="botProtectionForm.state.turnstileSiteKey">
|
||||
<template #label>{{ i18n.ts.preview }}</template>
|
||||
<MkCaptcha v-model="turnstileResponse" provider="turnstile" :sitekey="botProtectionForm.state.turnstileSiteKey"/>
|
||||
<MkCaptcha
|
||||
v-model="captchaResult" provider="turnstile"
|
||||
:sitekey="botProtectionForm.state.turnstileSiteKey"
|
||||
/>
|
||||
</FormSlot>
|
||||
<MkInfo>
|
||||
<div :class="$style.captchaInfoMsg">
|
||||
<div>サイトキーに"1x00000000000000000000AA"と入力することで動作をテスト出来ます。<br/>本番運用時には必ず正規のサイトキーを設定してください。</div>
|
||||
<div>ref: <a href="https://developers.cloudflare.com/turnstile/troubleshooting/testing/" target="_blank">Cloudflare Docs</a></div>
|
||||
<div>
|
||||
サイトキーに"1x00000000000000000000AA"と入力することで動作をテスト出来ます。<br/>本番運用時には必ず正規のサイトキーを設定してください。
|
||||
</div>
|
||||
<div>
|
||||
ref: <a href="https://developers.cloudflare.com/turnstile/troubleshooting/testing/" target="_blank">Cloudflare
|
||||
Docs</a>
|
||||
</div>
|
||||
</div>
|
||||
</MkInfo>
|
||||
</template>
|
||||
|
@ -113,15 +139,20 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkInfo warn><span v-html="i18n.ts.testCaptchaWarning"></span></MkInfo>
|
||||
<FormSlot>
|
||||
<template #label>{{ i18n.ts.preview }}</template>
|
||||
<MkCaptcha v-model="testCaptchaResponse" provider="testcaptcha" :sitekey="null"/>
|
||||
<MkCaptcha v-model="captchaResult" provider="testcaptcha" :sitekey="null"/>
|
||||
</FormSlot>
|
||||
</template>
|
||||
|
||||
<MkInfo v-if="!verifyResult && verifyErrorText" warn>
|
||||
{{ verifyErrorText }}
|
||||
</MkInfo>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, ref } from 'vue';
|
||||
import { computed, defineAsyncComponent, ref, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkRadios from '@/components/MkRadios.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import FormSlot from '@/components/form/slot.vue';
|
||||
|
@ -138,19 +169,17 @@ const MkCaptcha = defineAsyncComponent(() => import('@/components/MkCaptcha.vue'
|
|||
|
||||
const meta = await misskeyApi('admin/meta');
|
||||
|
||||
const hCaptchaResponse = ref<string | null>(null);
|
||||
const mCaptchaResponse = ref<string | null>(null);
|
||||
const reCaptchaResponse = ref<string | null>(null);
|
||||
const turnstileResponse = ref<string | null>(null);
|
||||
const testCaptchaResponse = ref<string | null>(null);
|
||||
const captchaResult = ref<string | null>(null);
|
||||
const verifyResult = ref<boolean>(false);
|
||||
const verifyErrorText = ref<string | null>(null);
|
||||
|
||||
const canSaving = computed((): boolean => {
|
||||
return (botProtectionForm.state.provider === null) ||
|
||||
(botProtectionForm.state.provider === 'hcaptcha' && !!hCaptchaResponse.value) ||
|
||||
(botProtectionForm.state.provider === 'mcaptcha' && !!mCaptchaResponse.value) ||
|
||||
(botProtectionForm.state.provider === 'recaptcha' && !!reCaptchaResponse.value) ||
|
||||
(botProtectionForm.state.provider === 'turnstile' && !!turnstileResponse.value) ||
|
||||
(botProtectionForm.state.provider === 'testcaptcha' && !!testCaptchaResponse.value);
|
||||
(botProtectionForm.state.provider === 'hcaptcha' && verifyResult.value) ||
|
||||
(botProtectionForm.state.provider === 'mcaptcha' && verifyResult.value) ||
|
||||
(botProtectionForm.state.provider === 'recaptcha' && verifyResult.value) ||
|
||||
(botProtectionForm.state.provider === 'turnstile' && verifyResult.value) ||
|
||||
(botProtectionForm.state.provider === 'testcaptcha' && verifyResult.value);
|
||||
});
|
||||
|
||||
const botProtectionForm = useForm({
|
||||
|
@ -193,6 +222,56 @@ const botProtectionForm = useForm({
|
|||
});
|
||||
fetchInstance(true);
|
||||
});
|
||||
|
||||
watch(botProtectionForm.state, () => {
|
||||
captchaResult.value = null;
|
||||
if (botProtectionForm.state.provider === null) {
|
||||
verifyResult.value = true;
|
||||
} else {
|
||||
verifyResult.value = false;
|
||||
verifyErrorText.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
watch(captchaResult, async () => {
|
||||
const provider = botProtectionForm.state.provider;
|
||||
|
||||
const sitekey = provider === 'hcaptcha'
|
||||
? botProtectionForm.state.hcaptchaSiteKey
|
||||
: provider === 'mcaptcha'
|
||||
? botProtectionForm.state.mcaptchaSiteKey
|
||||
: provider === 'recaptcha'
|
||||
? botProtectionForm.state.recaptchaSiteKey
|
||||
: provider === 'turnstile'
|
||||
? botProtectionForm.state.turnstileSiteKey
|
||||
: null;
|
||||
const secret = provider === 'hcaptcha'
|
||||
? botProtectionForm.state.hcaptchaSecretKey
|
||||
: provider === 'mcaptcha'
|
||||
? botProtectionForm.state.mcaptchaSecretKey
|
||||
: provider === 'recaptcha'
|
||||
? botProtectionForm.state.recaptchaSecretKey
|
||||
: provider === 'turnstile'
|
||||
? botProtectionForm.state.turnstileSecretKey
|
||||
: null;
|
||||
|
||||
if (captchaResult.value) {
|
||||
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,
|
||||
captchaResult: captchaResult.value,
|
||||
});
|
||||
|
||||
verifyResult.value = result.success;
|
||||
verifyErrorText.value = result.error
|
||||
? result.error.message
|
||||
: null;
|
||||
} else {
|
||||
verifyResult.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -136,6 +136,12 @@ type AdminAvatarDecorationsListResponse = operations['admin___avatar-decorations
|
|||
// @public (undocumented)
|
||||
type AdminAvatarDecorationsUpdateRequest = operations['admin___avatar-decorations___update']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type AdminCaptchaTestRequest = operations['admin___captcha___test']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type AdminCaptchaTestResponse = operations['admin___captcha___test']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type AdminDeleteAccountRequest = operations['admin___delete-account']['requestBody']['content']['application/json'];
|
||||
|
||||
|
@ -1261,6 +1267,8 @@ declare namespace entities {
|
|||
AdminAvatarDecorationsListRequest,
|
||||
AdminAvatarDecorationsListResponse,
|
||||
AdminAvatarDecorationsUpdateRequest,
|
||||
AdminCaptchaTestRequest,
|
||||
AdminCaptchaTestResponse,
|
||||
AdminDeleteAllFilesOfAUserRequest,
|
||||
AdminUnsetUserAvatarRequest,
|
||||
AdminUnsetUserBannerRequest,
|
||||
|
|
|
@ -250,6 +250,18 @@ declare module '../api.js' {
|
|||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
|
||||
* **Credential required**: *Yes* / **Permission**: *read:admin:captcha*
|
||||
*/
|
||||
request<E extends 'admin/captcha/test', P extends Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
params: P,
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
|
|
|
@ -36,6 +36,8 @@ import type {
|
|||
AdminAvatarDecorationsListRequest,
|
||||
AdminAvatarDecorationsListResponse,
|
||||
AdminAvatarDecorationsUpdateRequest,
|
||||
AdminCaptchaTestRequest,
|
||||
AdminCaptchaTestResponse,
|
||||
AdminDeleteAllFilesOfAUserRequest,
|
||||
AdminUnsetUserAvatarRequest,
|
||||
AdminUnsetUserBannerRequest,
|
||||
|
@ -604,6 +606,7 @@ export type Endpoints = {
|
|||
'admin/avatar-decorations/delete': { req: AdminAvatarDecorationsDeleteRequest; res: EmptyResponse };
|
||||
'admin/avatar-decorations/list': { req: AdminAvatarDecorationsListRequest; res: AdminAvatarDecorationsListResponse };
|
||||
'admin/avatar-decorations/update': { req: AdminAvatarDecorationsUpdateRequest; res: EmptyResponse };
|
||||
'admin/captcha/test': { req: AdminCaptchaTestRequest; res: AdminCaptchaTestResponse };
|
||||
'admin/delete-all-files-of-a-user': { req: AdminDeleteAllFilesOfAUserRequest; res: EmptyResponse };
|
||||
'admin/unset-user-avatar': { req: AdminUnsetUserAvatarRequest; res: EmptyResponse };
|
||||
'admin/unset-user-banner': { req: AdminUnsetUserBannerRequest; res: EmptyResponse };
|
||||
|
|
|
@ -39,6 +39,8 @@ export type AdminAvatarDecorationsDeleteRequest = operations['admin___avatar-dec
|
|||
export type AdminAvatarDecorationsListRequest = operations['admin___avatar-decorations___list']['requestBody']['content']['application/json'];
|
||||
export type AdminAvatarDecorationsListResponse = operations['admin___avatar-decorations___list']['responses']['200']['content']['application/json'];
|
||||
export type AdminAvatarDecorationsUpdateRequest = operations['admin___avatar-decorations___update']['requestBody']['content']['application/json'];
|
||||
export type AdminCaptchaTestRequest = operations['admin___captcha___test']['requestBody']['content']['application/json'];
|
||||
export type AdminCaptchaTestResponse = operations['admin___captcha___test']['responses']['200']['content']['application/json'];
|
||||
export type AdminDeleteAllFilesOfAUserRequest = operations['admin___delete-all-files-of-a-user']['requestBody']['content']['application/json'];
|
||||
export type AdminUnsetUserAvatarRequest = operations['admin___unset-user-avatar']['requestBody']['content']['application/json'];
|
||||
export type AdminUnsetUserBannerRequest = operations['admin___unset-user-banner']['requestBody']['content']['application/json'];
|
||||
|
|
|
@ -215,6 +215,16 @@ export type paths = {
|
|||
*/
|
||||
post: operations['admin___avatar-decorations___update'];
|
||||
};
|
||||
'/admin/captcha/test': {
|
||||
/**
|
||||
* admin/captcha/test
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
|
||||
* **Credential required**: *Yes* / **Permission**: *read:admin:captcha*
|
||||
*/
|
||||
post: operations['admin___captcha___test'];
|
||||
};
|
||||
'/admin/delete-all-files-of-a-user': {
|
||||
/**
|
||||
* admin/delete-all-files-of-a-user
|
||||
|
@ -6564,6 +6574,71 @@ export type operations = {
|
|||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* admin/captcha/test
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
|
||||
* **Credential required**: *Yes* / **Permission**: *read:admin:captcha*
|
||||
*/
|
||||
admin___captcha___test: {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
/** @enum {string} */
|
||||
provider: 'hcaptcha' | 'mcaptcha' | 'recaptcha' | 'turnstile' | 'testcaptcha';
|
||||
sitekey?: string;
|
||||
secret?: string;
|
||||
instanceUrl?: string;
|
||||
captchaResult?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK (with results) */
|
||||
200: {
|
||||
content: {
|
||||
'application/json': {
|
||||
success: boolean;
|
||||
error: {
|
||||
code: string;
|
||||
message: string;
|
||||
} | null;
|
||||
};
|
||||
};
|
||||
};
|
||||
/** @description Client error */
|
||||
400: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Authentication error */
|
||||
401: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden error */
|
||||
403: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description I'm Ai */
|
||||
418: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* admin/delete-all-files-of-a-user
|
||||
* @description No description provided.
|
||||
|
|
Loading…
Reference in a new issue