Skip to content

Commit

Permalink
fix
Browse files Browse the repository at this point in the history
  • Loading branch information
samunohito committed Dec 20, 2024
1 parent 3dade7a commit ce7f205
Show file tree
Hide file tree
Showing 7 changed files with 351 additions and 109 deletions.
138 changes: 106 additions & 32 deletions packages/backend/src/core/CaptchaService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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;
Expand All @@ -48,6 +50,7 @@ type CaptchaResponse = {
export class CaptchaService {
constructor(
private httpRequestService: HttpRequestService,
private metaService: MetaService,
) {
}

Expand Down Expand Up @@ -166,73 +169,86 @@ 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
* @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)) {
public async save(
provider: CaptchaProvider,
params?: {
sitekey?: string | null;
secret?: string | null;
instanceUrl?: string | null;
captchaResult?: string | null;
},
): Promise<CaptchaSaveResult> {
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
Expand All @@ -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);
}
}

8 changes: 4 additions & 4 deletions packages/backend/src/server/api/EndpointsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 };
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions packages/backend/src/server/api/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand All @@ -67,13 +67,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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) {
Expand Down
Loading

0 comments on commit ce7f205

Please sign in to comment.