feat(frontend): Botプロテクションの設定変更時は実際に検証を通過しないと保存できないようにする (#15151)

* feat(frontend): CAPTCHAの設定変更時は実際に検証を通過しないと保存できないようにする

* なしでも保存できるようにした

* fix CHANGELOG.md

* フォームが増殖するのを修正

* add comment

* add server-side verify

* fix ci

* fix

* fix

* fix i18n

* add current.ts

* fix text

* fix

* regenerate locales

* fix MkFormFooter.vue

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
This commit is contained in:
おさむのひと
2025-01-14 19:57:58 +09:00
committed by GitHub
parent 7fbfc2e046
commit 64501c69a1
19 changed files with 1597 additions and 89 deletions

View File

@@ -28,6 +28,8 @@ import * as ep___admin_avatarDecorations_create from './endpoints/admin/avatar-d
import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-decorations/delete.js';
import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js';
import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
import * as ep___admin_captcha_current from './endpoints/admin/captcha/current.js';
import * as ep___admin_captcha_save from './endpoints/admin/captcha/save.js';
import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
import * as ep___admin_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js';
import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.js';
@@ -416,6 +418,8 @@ const $admin_avatarDecorations_create: Provider = { provide: 'ep:admin/avatar-de
const $admin_avatarDecorations_delete: Provider = { provide: 'ep:admin/avatar-decorations/delete', useClass: ep___admin_avatarDecorations_delete.default };
const $admin_avatarDecorations_list: Provider = { provide: 'ep:admin/avatar-decorations/list', useClass: ep___admin_avatarDecorations_list.default };
const $admin_avatarDecorations_update: Provider = { provide: 'ep:admin/avatar-decorations/update', useClass: ep___admin_avatarDecorations_update.default };
const $admin_captcha_current: Provider = { provide: 'ep:admin/captcha/current', useClass: ep___admin_captcha_current.default };
const $admin_captcha_save: Provider = { provide: 'ep:admin/captcha/save', useClass: ep___admin_captcha_save.default };
const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default };
const $admin_unsetUserAvatar: Provider = { provide: 'ep:admin/unset-user-avatar', useClass: ep___admin_unsetUserAvatar.default };
const $admin_unsetUserBanner: Provider = { provide: 'ep:admin/unset-user-banner', useClass: ep___admin_unsetUserBanner.default };
@@ -808,6 +812,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_avatarDecorations_delete,
$admin_avatarDecorations_list,
$admin_avatarDecorations_update,
$admin_captcha_current,
$admin_captcha_save,
$admin_deleteAllFilesOfAUser,
$admin_unsetUserAvatar,
$admin_unsetUserBanner,
@@ -1194,6 +1200,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_avatarDecorations_delete,
$admin_avatarDecorations_list,
$admin_avatarDecorations_update,
$admin_captcha_current,
$admin_captcha_save,
$admin_deleteAllFilesOfAUser,
$admin_unsetUserAvatar,
$admin_unsetUserBanner,

View File

@@ -33,6 +33,8 @@ import * as ep___admin_avatarDecorations_create from './endpoints/admin/avatar-d
import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-decorations/delete.js';
import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js';
import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
import * as ep___admin_captcha_current from './endpoints/admin/captcha/current.js';
import * as ep___admin_captcha_save from './endpoints/admin/captcha/save.js';
import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
import * as ep___admin_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js';
import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.js';
@@ -420,6 +422,8 @@ const eps = [
['admin/avatar-decorations/delete', ep___admin_avatarDecorations_delete],
['admin/avatar-decorations/list', ep___admin_avatarDecorations_list],
['admin/avatar-decorations/update', ep___admin_avatarDecorations_update],
['admin/captcha/current', ep___admin_captcha_current],
['admin/captcha/save', ep___admin_captcha_save],
['admin/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser],
['admin/unset-user-avatar', ep___admin_unsetUserAvatar],
['admin/unset-user-banner', ep___admin_unsetUserBanner],

View File

@@ -0,0 +1,70 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { CaptchaService, supportedCaptchaProviders } from '@/core/CaptchaService.js';
export const meta = {
tags: ['admin', 'captcha'],
requireCredential: true,
requireAdmin: true,
// 実態はmetaの取得であるため
kind: 'read:admin:meta',
res: {
type: 'object',
properties: {
provider: {
type: 'string',
enum: supportedCaptchaProviders,
},
hcaptcha: {
type: 'object',
properties: {
siteKey: { type: 'string', nullable: true },
secretKey: { type: 'string', nullable: true },
},
},
mcaptcha: {
type: 'object',
properties: {
siteKey: { type: 'string', nullable: true },
secretKey: { type: 'string', nullable: true },
instanceUrl: { type: 'string', nullable: true },
},
},
recaptcha: {
type: 'object',
properties: {
siteKey: { type: 'string', nullable: true },
secretKey: { type: 'string', nullable: true },
},
},
turnstile: {
type: 'object',
properties: {
siteKey: { type: 'string', nullable: true },
secretKey: { type: 'string', nullable: true },
},
},
},
},
} as const;
export const paramDef = {} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private captchaService: CaptchaService,
) {
super(meta, paramDef, async () => {
return this.captchaService.get();
});
}
}

View File

@@ -0,0 +1,129 @@
/*
* 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 { captchaErrorCodes, CaptchaService, supportedCaptchaProviders } from '@/core/CaptchaService.js';
import { ApiError } from '@/server/api/error.js';
export const meta = {
tags: ['admin', 'captcha'],
requireCredential: true,
requireAdmin: true,
// 実態はmetaの更新であるため
kind: 'write:admin:meta',
errors: {
invalidProvider: {
message: 'Invalid provider.',
code: 'INVALID_PROVIDER',
id: '14bf7ae1-80cc-4363-acb2-4fd61d086af0',
httpStatusCode: 400,
},
invalidParameters: {
message: 'Invalid parameters.',
code: 'INVALID_PARAMETERS',
id: '26654194-410e-44e2-b42e-460ff6f92476',
httpStatusCode: 400,
},
noResponseProvided: {
message: 'No response provided.',
code: 'NO_RESPONSE_PROVIDED',
id: '40acbba8-0937-41fb-bb3f-474514d40afe',
httpStatusCode: 400,
},
requestFailed: {
message: 'Request failed.',
code: 'REQUEST_FAILED',
id: '0f4fe2f1-2c15-4d6e-b714-efbfcde231cd',
httpStatusCode: 500,
},
verificationFailed: {
message: 'Verification failed.',
code: 'VERIFICATION_FAILED',
id: 'c41c067f-24f3-4150-84b2-b5a3ae8c2214',
httpStatusCode: 400,
},
unknown: {
message: 'unknown',
code: 'UNKNOWN',
id: 'f868d509-e257-42a9-99c1-42614b031a97',
httpStatusCode: 500,
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
provider: {
type: 'string',
enum: supportedCaptchaProviders,
},
captchaResult: {
type: 'string', nullable: true,
},
sitekey: {
type: 'string', nullable: true,
},
secret: {
type: 'string', nullable: true,
},
instanceUrl: {
type: 'string', nullable: true,
},
},
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) => {
const result = await this.captchaService.save(ps.provider, {
sitekey: ps.sitekey,
secret: ps.secret,
instanceUrl: ps.instanceUrl,
captchaResult: ps.captchaResult,
});
if (!result.success) {
switch (result.error.code) {
case captchaErrorCodes.invalidProvider:
throw new ApiError({
...meta.errors.invalidProvider,
message: result.error.message,
});
case captchaErrorCodes.invalidParameters:
throw new ApiError({
...meta.errors.invalidParameters,
message: result.error.message,
});
case captchaErrorCodes.noResponseProvided:
throw new ApiError({
...meta.errors.noResponseProvided,
message: result.error.message,
});
case captchaErrorCodes.requestFailed:
throw new ApiError({
...meta.errors.requestFailed,
message: result.error.message,
});
case captchaErrorCodes.verificationFailed:
throw new ApiError({
...meta.errors.verificationFailed,
message: result.error.message,
});
default:
throw new ApiError(meta.errors.unknown);
}
}
});
}
}