Add Cloudflare Turnstile CAPTCHA support (#9111)
* Add Cloudflare Turnstile CAPTCHA support * Update packages/client/src/components/MkCaptcha.vue Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com> Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
This commit is contained in:
		
							parent
							
								
									166067f746
								
							
						
					
					
						commit
						1309367884
					
				
					 13 changed files with 130 additions and 3 deletions
				
			
		|  | @ -349,6 +349,10 @@ recaptcha: "reCAPTCHA" | ||||||
| enableRecaptcha: "reCAPTCHAを有効にする" | enableRecaptcha: "reCAPTCHAを有効にする" | ||||||
| recaptchaSiteKey: "サイトキー" | recaptchaSiteKey: "サイトキー" | ||||||
| recaptchaSecretKey: "シークレットキー" | recaptchaSecretKey: "シークレットキー" | ||||||
|  | turnstile: "Turnstile" | ||||||
|  | enableTurnstile: "Turnstileを有効にする" | ||||||
|  | turnstileSiteKey: "サイトキー" | ||||||
|  | turnstileSecretKey: "シークレットキー" | ||||||
| avoidMultiCaptchaConfirm: "複数のCaptchaを使用すると干渉を起こす可能性があります。他のCaptchaを無効にしますか?キャンセルして複数のCaptchaを有効化したままにすることも可能です。" | avoidMultiCaptchaConfirm: "複数のCaptchaを使用すると干渉を起こす可能性があります。他のCaptchaを無効にしますか?キャンセルして複数のCaptchaを有効化したままにすることも可能です。" | ||||||
| antennas: "アンテナ" | antennas: "アンテナ" | ||||||
| manageAntennas: "アンテナの管理" | manageAntennas: "アンテナの管理" | ||||||
|  |  | ||||||
							
								
								
									
										15
									
								
								packages/backend/migration/1664694635394-turnstile.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								packages/backend/migration/1664694635394-turnstile.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | ||||||
|  | export class turnstile1664694635394 { | ||||||
|  |     name = 'turnstile1664694635394' | ||||||
|  | 
 | ||||||
|  |     async up(queryRunner) { | ||||||
|  |         await queryRunner.query(`ALTER TABLE "meta" ADD "enableTurnstile" boolean NOT NULL DEFAULT false`); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "meta" ADD "turnstileSiteKey" character varying(64)`); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "meta" ADD "turnstileSecretKey" character varying(64)`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async down(queryRunner) { | ||||||
|  |         await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "turnstileSecretKey"`); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "turnstileSiteKey"`); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableTurnstile"`); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -66,5 +66,16 @@ export class CaptchaService { | ||||||
| 			throw `hcaptcha-failed: ${errorCodes}`; | 			throw `hcaptcha-failed: ${errorCodes}`; | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	public async verifyTurnstile(secret: string, response: string): Promise<void> { | ||||||
|  | 		const result = await this.getCaptchaResponse('https://challenges.cloudflare.com/turnstile/v0/siteverify', secret, response).catch(e => { | ||||||
|  | 			throw `turnstile-request-failed: ${e}`; | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		if (result.success !== true) { | ||||||
|  | 			const errorCodes = result['error-codes'] ? result['error-codes'].join(', ') : ''; | ||||||
|  | 			throw `turnstile-failed: ${errorCodes}`; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -188,6 +188,23 @@ export class Meta { | ||||||
| 	}) | 	}) | ||||||
| 	public recaptchaSecretKey: string | null; | 	public recaptchaSecretKey: string | null; | ||||||
| 
 | 
 | ||||||
|  | 	@Column('boolean', { | ||||||
|  | 		default: false, | ||||||
|  | 	}) | ||||||
|  | 	public enableTurnstile: boolean; | ||||||
|  | 
 | ||||||
|  | 	@Column('varchar', { | ||||||
|  | 		length: 64, | ||||||
|  | 		nullable: true, | ||||||
|  | 	}) | ||||||
|  | 	public turnstileSiteKey: string | null; | ||||||
|  | 
 | ||||||
|  | 	@Column('varchar', { | ||||||
|  | 		length: 64, | ||||||
|  | 		nullable: true, | ||||||
|  | 	}) | ||||||
|  | 	public turnstileSecretKey: string | null; | ||||||
|  | 
 | ||||||
| 	@Column('enum', { | 	@Column('enum', { | ||||||
| 		enum: ['none', 'all', 'local', 'remote'], | 		enum: ['none', 'all', 'local', 'remote'], | ||||||
| 		default: 'none', | 		default: 'none', | ||||||
|  |  | ||||||
|  | @ -61,6 +61,12 @@ export class SignupApiService { | ||||||
| 					ctx.throw(400, e); | 					ctx.throw(400, e); | ||||||
| 				}); | 				}); | ||||||
| 			} | 			} | ||||||
|  | 
 | ||||||
|  | 			if (instance.enableTurnstile && instance.turnstileSecretKey) { | ||||||
|  | 				await this.captchaService.verifyTurnstile(instance.turnstileSecretKey, body['turnstile-response']).catch(e => { | ||||||
|  | 					ctx.throw(400, e); | ||||||
|  | 				}); | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	 | 	 | ||||||
| 		const username = body['username']; | 		const username = body['username']; | ||||||
|  |  | ||||||
|  | @ -47,6 +47,14 @@ export const meta = { | ||||||
| 				type: 'string', | 				type: 'string', | ||||||
| 				optional: false, nullable: true, | 				optional: false, nullable: true, | ||||||
| 			}, | 			}, | ||||||
|  | 			enableTurnstile: { | ||||||
|  | 				type: 'boolean', | ||||||
|  | 				optional: false, nullable: false, | ||||||
|  | 			}, | ||||||
|  | 			turnstileSiteKey: { | ||||||
|  | 				type: 'string', | ||||||
|  | 				optional: false, nullable: true, | ||||||
|  | 			}, | ||||||
| 			swPublickey: { | 			swPublickey: { | ||||||
| 				type: 'string', | 				type: 'string', | ||||||
| 				optional: false, nullable: true, | 				optional: false, nullable: true, | ||||||
|  | @ -197,6 +205,10 @@ export const meta = { | ||||||
| 				type: 'string', | 				type: 'string', | ||||||
| 				optional: true, nullable: true, | 				optional: true, nullable: true, | ||||||
| 			}, | 			}, | ||||||
|  | 			turnstileSecretKey: { | ||||||
|  | 				type: 'string', | ||||||
|  | 				optional: true, nullable: true, | ||||||
|  | 			} | ||||||
| 			sensitiveMediaDetection: { | 			sensitiveMediaDetection: { | ||||||
| 				type: 'string', | 				type: 'string', | ||||||
| 				optional: true, nullable: false, | 				optional: true, nullable: false, | ||||||
|  | @ -374,6 +386,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | ||||||
| 				hcaptchaSiteKey: instance.hcaptchaSiteKey, | 				hcaptchaSiteKey: instance.hcaptchaSiteKey, | ||||||
| 				enableRecaptcha: instance.enableRecaptcha, | 				enableRecaptcha: instance.enableRecaptcha, | ||||||
| 				recaptchaSiteKey: instance.recaptchaSiteKey, | 				recaptchaSiteKey: instance.recaptchaSiteKey, | ||||||
|  | 				enableTurnstile: instance.enableTurnstile, | ||||||
|  | 				turnstileSiteKey: instance.turnstileSiteKey, | ||||||
| 				swPublickey: instance.swPublicKey, | 				swPublickey: instance.swPublicKey, | ||||||
| 				themeColor: instance.themeColor, | 				themeColor: instance.themeColor, | ||||||
| 				mascotImageUrl: instance.mascotImageUrl, | 				mascotImageUrl: instance.mascotImageUrl, | ||||||
|  | @ -400,6 +414,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | ||||||
| 				blockedHosts: instance.blockedHosts, | 				blockedHosts: instance.blockedHosts, | ||||||
| 				hcaptchaSecretKey: instance.hcaptchaSecretKey, | 				hcaptchaSecretKey: instance.hcaptchaSecretKey, | ||||||
| 				recaptchaSecretKey: instance.recaptchaSecretKey, | 				recaptchaSecretKey: instance.recaptchaSecretKey, | ||||||
|  | 				turnstileSecretKey: instance.turnstileSecretKey, | ||||||
| 				sensitiveMediaDetection: instance.sensitiveMediaDetection, | 				sensitiveMediaDetection: instance.sensitiveMediaDetection, | ||||||
| 				sensitiveMediaDetectionSensitivity: instance.sensitiveMediaDetectionSensitivity, | 				sensitiveMediaDetectionSensitivity: instance.sensitiveMediaDetectionSensitivity, | ||||||
| 				setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically, | 				setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically, | ||||||
|  |  | ||||||
|  | @ -52,6 +52,9 @@ export const paramDef = { | ||||||
| 		enableRecaptcha: { type: 'boolean' }, | 		enableRecaptcha: { type: 'boolean' }, | ||||||
| 		recaptchaSiteKey: { type: 'string', nullable: true }, | 		recaptchaSiteKey: { type: 'string', nullable: true }, | ||||||
| 		recaptchaSecretKey: { type: 'string', nullable: true }, | 		recaptchaSecretKey: { type: 'string', nullable: true }, | ||||||
|  | 		enableTurnstile: { type: 'boolean' }, | ||||||
|  | 		turnstileSiteKey: { type: 'string', nullable: true }, | ||||||
|  | 		turnstileSecretKey: { type: 'string', nullable: true }, | ||||||
| 		sensitiveMediaDetection: { type: 'string', enum: ['none', 'all', 'local', 'remote'] }, | 		sensitiveMediaDetection: { type: 'string', enum: ['none', 'all', 'local', 'remote'] }, | ||||||
| 		sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] }, | 		sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] }, | ||||||
| 		setSensitiveFlagAutomatically: { type: 'boolean' }, | 		setSensitiveFlagAutomatically: { type: 'boolean' }, | ||||||
|  | @ -231,6 +234,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | ||||||
| 				set.recaptchaSecretKey = ps.recaptchaSecretKey; | 				set.recaptchaSecretKey = ps.recaptchaSecretKey; | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
|  | 			if (ps.enableTurnstile !== undefined) { | ||||||
|  | 				set.enableTurnstile = ps.enableTurnstile; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			if (ps.turnstileSiteKey !== undefined) { | ||||||
|  | 				set.turnstileSiteKey = ps.turnstileSiteKey; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			if (ps.turnstileSecretKey !== undefined) { | ||||||
|  | 				set.turnstileSecretKey = ps.turnstileSecretKey; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
| 			if (ps.sensitiveMediaDetection !== undefined) { | 			if (ps.sensitiveMediaDetection !== undefined) { | ||||||
| 				set.sensitiveMediaDetection = ps.sensitiveMediaDetection; | 				set.sensitiveMediaDetection = ps.sensitiveMediaDetection; | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
|  | @ -119,6 +119,14 @@ export const meta = { | ||||||
| 				type: 'string', | 				type: 'string', | ||||||
| 				optional: false, nullable: true, | 				optional: false, nullable: true, | ||||||
| 			}, | 			}, | ||||||
|  | 			enableTurnstile: { | ||||||
|  | 				type: 'boolean', | ||||||
|  | 				optional: false, nullable: false, | ||||||
|  | 			}, | ||||||
|  | 			turnstileSiteKey: { | ||||||
|  | 				type: 'string', | ||||||
|  | 				optional: false, nullable: true, | ||||||
|  | 			}, | ||||||
| 			swPublickey: { | 			swPublickey: { | ||||||
| 				type: 'string', | 				type: 'string', | ||||||
| 				optional: false, nullable: true, | 				optional: false, nullable: true, | ||||||
|  | @ -372,6 +380,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | ||||||
| 				hcaptchaSiteKey: instance.hcaptchaSiteKey, | 				hcaptchaSiteKey: instance.hcaptchaSiteKey, | ||||||
| 				enableRecaptcha: instance.enableRecaptcha, | 				enableRecaptcha: instance.enableRecaptcha, | ||||||
| 				recaptchaSiteKey: instance.recaptchaSiteKey, | 				recaptchaSiteKey: instance.recaptchaSiteKey, | ||||||
|  | 				enableTurnstile: instance.enableTurnstile, | ||||||
|  | 				turnstileSiteKey: instance.turnstileSiteKey, | ||||||
| 				swPublickey: instance.swPublicKey, | 				swPublickey: instance.swPublicKey, | ||||||
| 				themeColor: instance.themeColor, | 				themeColor: instance.themeColor, | ||||||
| 				mascotImageUrl: instance.mascotImageUrl, | 				mascotImageUrl: instance.mascotImageUrl, | ||||||
|  | @ -423,6 +433,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { | ||||||
| 					elasticsearch: this.config.elasticsearch ? true : false, | 					elasticsearch: this.config.elasticsearch ? true : false, | ||||||
| 					hcaptcha: instance.enableHcaptcha, | 					hcaptcha: instance.enableHcaptcha, | ||||||
| 					recaptcha: instance.enableRecaptcha, | 					recaptcha: instance.enableRecaptcha, | ||||||
|  | 					turnstile: instance.enableTurnstile, | ||||||
| 					objectStorage: instance.useObjectStorage, | 					objectStorage: instance.useObjectStorage, | ||||||
| 					twitter: instance.enableTwitterIntegration, | 					twitter: instance.enableTwitterIntegration, | ||||||
| 					github: instance.enableGithubIntegration, | 					github: instance.enableGithubIntegration, | ||||||
|  |  | ||||||
|  | @ -20,7 +20,7 @@ type Captcha = { | ||||||
| 	getResponse(id: string): string; | 	getResponse(id: string): string; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| type CaptchaProvider = 'hcaptcha' | 'recaptcha'; | type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile'; | ||||||
| 
 | 
 | ||||||
| type CaptchaContainer = { | type CaptchaContainer = { | ||||||
| 	readonly [_ in CaptchaProvider]?: Captcha; | 	readonly [_ in CaptchaProvider]?: Captcha; | ||||||
|  | @ -48,6 +48,7 @@ const variable = computed(() => { | ||||||
| 	switch (props.provider) { | 	switch (props.provider) { | ||||||
| 		case 'hcaptcha': return 'hcaptcha'; | 		case 'hcaptcha': return 'hcaptcha'; | ||||||
| 		case 'recaptcha': return 'grecaptcha'; | 		case 'recaptcha': return 'grecaptcha'; | ||||||
|  | 		case 'turnstile': return 'turnstile'; | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | @ -57,6 +58,7 @@ const src = computed(() => { | ||||||
| 	switch (props.provider) { | 	switch (props.provider) { | ||||||
| 		case 'hcaptcha': return 'https://js.hcaptcha.com/1/api.js?render=explicit&recaptchacompat=off'; | 		case 'hcaptcha': return 'https://js.hcaptcha.com/1/api.js?render=explicit&recaptchacompat=off'; | ||||||
| 		case 'recaptcha': return 'https://www.recaptcha.net/recaptcha/api.js?render=explicit'; | 		case 'recaptcha': return 'https://www.recaptcha.net/recaptcha/api.js?render=explicit'; | ||||||
|  | 		case 'turnstile': return 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'; | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -59,6 +59,7 @@ | ||||||
| 	</MkSwitch> | 	</MkSwitch> | ||||||
| 	<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/> | 	<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/> | ||||||
| 	<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/> | 	<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/> | ||||||
|  | 	<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" class="_formBlock captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/> | ||||||
| 	<MkButton class="_formBlock" type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ i18n.ts.start }}</MkButton> | 	<MkButton class="_formBlock" type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ i18n.ts.start }}</MkButton> | ||||||
| </form> | </form> | ||||||
| </template> | </template> | ||||||
|  | @ -92,6 +93,7 @@ const host = toUnicode(config.host); | ||||||
| 
 | 
 | ||||||
| let hcaptcha = $ref(); | let hcaptcha = $ref(); | ||||||
| let recaptcha = $ref(); | let recaptcha = $ref(); | ||||||
|  | let turnstile = $ref(); | ||||||
| 
 | 
 | ||||||
| let username: string = $ref(''); | let username: string = $ref(''); | ||||||
| let password: string = $ref(''); | let password: string = $ref(''); | ||||||
|  | @ -106,12 +108,14 @@ let submitting: boolean = $ref(false); | ||||||
| let ToSAgreement: boolean = $ref(false); | let ToSAgreement: boolean = $ref(false); | ||||||
| let hCaptchaResponse = $ref(null); | let hCaptchaResponse = $ref(null); | ||||||
| let reCaptchaResponse = $ref(null); | let reCaptchaResponse = $ref(null); | ||||||
|  | let turnstileResponse = $ref(null); | ||||||
| 
 | 
 | ||||||
| const shouldDisableSubmitting = $computed((): boolean => { | const shouldDisableSubmitting = $computed((): boolean => { | ||||||
| 	return submitting || | 	return submitting || | ||||||
| 		instance.tosUrl && !ToSAgreement || | 		instance.tosUrl && !ToSAgreement || | ||||||
| 		instance.enableHcaptcha && !hCaptchaResponse || | 		instance.enableHcaptcha && !hCaptchaResponse || | ||||||
| 		instance.enableRecaptcha && !reCaptchaResponse || | 		instance.enableRecaptcha && !reCaptchaResponse || | ||||||
|  | 		instance.enableTurnstile && !turnstileResponse || | ||||||
| 		passwordRetypeState === 'not-match'; | 		passwordRetypeState === 'not-match'; | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | @ -198,6 +202,7 @@ function onSubmit(): void { | ||||||
| 		invitationCode, | 		invitationCode, | ||||||
| 		'hcaptcha-response': hCaptchaResponse, | 		'hcaptcha-response': hCaptchaResponse, | ||||||
| 		'g-recaptcha-response': reCaptchaResponse, | 		'g-recaptcha-response': reCaptchaResponse, | ||||||
|  | 		'turnstile-response': turnstileResponse, | ||||||
| 	}).then(() => { | 	}).then(() => { | ||||||
| 		if (instance.emailRequiredForSignup) { | 		if (instance.emailRequiredForSignup) { | ||||||
| 			os.alert({ | 			os.alert({ | ||||||
|  | @ -222,6 +227,7 @@ function onSubmit(): void { | ||||||
| 		submitting = false; | 		submitting = false; | ||||||
| 		hcaptcha.reset?.(); | 		hcaptcha.reset?.(); | ||||||
| 		recaptcha.reset?.(); | 		recaptcha.reset?.(); | ||||||
|  | 		turnstile.reset?.(); | ||||||
| 
 | 
 | ||||||
| 		os.alert({ | 		os.alert({ | ||||||
| 			type: 'error', | 			type: 'error', | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ | ||||||
| 				<option :value="null">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option> | 				<option :value="null">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option> | ||||||
| 				<option value="hcaptcha">hCaptcha</option> | 				<option value="hcaptcha">hCaptcha</option> | ||||||
| 				<option value="recaptcha">reCAPTCHA</option> | 				<option value="recaptcha">reCAPTCHA</option> | ||||||
|  | 				<option value="turnstile">Turnstile</option> | ||||||
| 			</FormRadios> | 			</FormRadios> | ||||||
| 
 | 
 | ||||||
| 			<template v-if="provider === 'hcaptcha'"> | 			<template v-if="provider === 'hcaptcha'"> | ||||||
|  | @ -36,6 +37,20 @@ | ||||||
| 					<MkCaptcha provider="recaptcha" :sitekey="recaptchaSiteKey"/> | 					<MkCaptcha provider="recaptcha" :sitekey="recaptchaSiteKey"/> | ||||||
| 				</FormSlot> | 				</FormSlot> | ||||||
| 			</template> | 			</template> | ||||||
|  | 			<template v-else-if="provider === 'turnstile'"> | ||||||
|  | 				<FormInput v-model="turnstileSiteKey" class="_formBlock"> | ||||||
|  | 					<template #prefix><i class="fas fa-key"></i></template> | ||||||
|  | 					<template #label>{{ i18n.ts.turnstileSiteKey }}</template> | ||||||
|  | 				</FormInput> | ||||||
|  | 				<FormInput v-model="turnstileSecretKey" class="_formBlock"> | ||||||
|  | 					<template #prefix><i class="fas fa-key"></i></template> | ||||||
|  | 					<template #label>{{ i18n.ts.turnstileSecretKey }}</template> | ||||||
|  | 				</FormInput> | ||||||
|  | 				<FormSlot class="_formBlock"> | ||||||
|  | 					<template #label>{{ i18n.ts.preview }}</template> | ||||||
|  | 					<MkCaptcha provider="turnstile" :sitekey="turnstileSiteKey || '1x00000000000000000000AA'"/> | ||||||
|  | 				</FormSlot> | ||||||
|  | 			</template> | ||||||
| 
 | 
 | ||||||
| 			<FormButton primary @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton> | 			<FormButton primary @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton> | ||||||
| 		</div> | 		</div> | ||||||
|  | @ -61,6 +76,8 @@ let hcaptchaSiteKey: string | null = $ref(null); | ||||||
| let hcaptchaSecretKey: string | null = $ref(null); | let hcaptchaSecretKey: string | null = $ref(null); | ||||||
| let recaptchaSiteKey: string | null = $ref(null); | let recaptchaSiteKey: string | null = $ref(null); | ||||||
| let recaptchaSecretKey: string | null = $ref(null); | let recaptchaSecretKey: string | null = $ref(null); | ||||||
|  | let turnstileSiteKey: string | null = $ref(null); | ||||||
|  | let turnstileSecretKey: string | null = $ref(null); | ||||||
| 
 | 
 | ||||||
| async function init() { | async function init() { | ||||||
| 	const meta = await os.api('admin/meta'); | 	const meta = await os.api('admin/meta'); | ||||||
|  | @ -68,8 +85,10 @@ async function init() { | ||||||
| 	hcaptchaSecretKey = meta.hcaptchaSecretKey; | 	hcaptchaSecretKey = meta.hcaptchaSecretKey; | ||||||
| 	recaptchaSiteKey = meta.recaptchaSiteKey; | 	recaptchaSiteKey = meta.recaptchaSiteKey; | ||||||
| 	recaptchaSecretKey = meta.recaptchaSecretKey; | 	recaptchaSecretKey = meta.recaptchaSecretKey; | ||||||
|  | 	turnstileSiteKey = meta.turnstileSiteKey; | ||||||
|  | 	turnstileSecretKey = meta.turnstileSecretKey; | ||||||
| 
 | 
 | ||||||
| 	provider = meta.enableHcaptcha ? 'hcaptcha' : meta.enableRecaptcha ? 'recaptcha' : null; | 	provider = meta.enableHcaptcha ? 'hcaptcha' : meta.enableRecaptcha ? 'recaptcha' : meta.enableTurnstile ? 'turnstile' : null; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function save() { | function save() { | ||||||
|  | @ -80,6 +99,9 @@ function save() { | ||||||
| 		enableRecaptcha: provider === 'recaptcha', | 		enableRecaptcha: provider === 'recaptcha', | ||||||
| 		recaptchaSiteKey, | 		recaptchaSiteKey, | ||||||
| 		recaptchaSecretKey, | 		recaptchaSecretKey, | ||||||
|  | 		enableTurnstile: provider === 'turnstile', | ||||||
|  | 		turnstileSiteKey, | ||||||
|  | 		turnstileSecretKey, | ||||||
| 	}).then(() => { | 	}).then(() => { | ||||||
| 		fetchInstance(); | 		fetchInstance(); | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
|  | @ -53,7 +53,7 @@ let view = $ref(null); | ||||||
| let el = $ref(null); | let el = $ref(null); | ||||||
| let pageProps = $ref({}); | let pageProps = $ref({}); | ||||||
| let noMaintainerInformation = isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail); | let noMaintainerInformation = isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail); | ||||||
| let noBotProtection = !instance.disableRegistration && !instance.enableHcaptcha && !instance.enableRecaptcha; | let noBotProtection = !instance.disableRegistration && !instance.enableHcaptcha && !instance.enableRecaptcha && !instance.enableTurnstile; | ||||||
| let noEmailServer = !instance.enableEmail; | let noEmailServer = !instance.enableEmail; | ||||||
| let thereIsUnresolvedAbuseReport = $ref(false); | let thereIsUnresolvedAbuseReport = $ref(false); | ||||||
| let currentPage = $computed(() => router.currentRef.value.child); | let currentPage = $computed(() => router.currentRef.value.child); | ||||||
|  |  | ||||||
|  | @ -9,6 +9,7 @@ | ||||||
| 					<template #label>{{ i18n.ts.botProtection }}</template> | 					<template #label>{{ i18n.ts.botProtection }}</template> | ||||||
| 					<template v-if="enableHcaptcha" #suffix>hCaptcha</template> | 					<template v-if="enableHcaptcha" #suffix>hCaptcha</template> | ||||||
| 					<template v-else-if="enableRecaptcha" #suffix>reCAPTCHA</template> | 					<template v-else-if="enableRecaptcha" #suffix>reCAPTCHA</template> | ||||||
|  | 					<template v-else-if="enableTurnstile" #suffix>Turnstile</template> | ||||||
| 					<template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template> | 					<template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template> | ||||||
| 
 | 
 | ||||||
| 					<XBotProtection/> | 					<XBotProtection/> | ||||||
|  | @ -120,6 +121,7 @@ import { definePageMetadata } from '@/scripts/page-metadata'; | ||||||
| let summalyProxy: string = $ref(''); | let summalyProxy: string = $ref(''); | ||||||
| let enableHcaptcha: boolean = $ref(false); | let enableHcaptcha: boolean = $ref(false); | ||||||
| let enableRecaptcha: boolean = $ref(false); | let enableRecaptcha: boolean = $ref(false); | ||||||
|  | let enableTurnstile: boolean = $ref(false); | ||||||
| let sensitiveMediaDetection: string = $ref('none'); | let sensitiveMediaDetection: string = $ref('none'); | ||||||
| let sensitiveMediaDetectionSensitivity: number = $ref(0); | let sensitiveMediaDetectionSensitivity: number = $ref(0); | ||||||
| let setSensitiveFlagAutomatically: boolean = $ref(false); | let setSensitiveFlagAutomatically: boolean = $ref(false); | ||||||
|  | @ -132,6 +134,7 @@ async function init() { | ||||||
| 	summalyProxy = meta.summalyProxy; | 	summalyProxy = meta.summalyProxy; | ||||||
| 	enableHcaptcha = meta.enableHcaptcha; | 	enableHcaptcha = meta.enableHcaptcha; | ||||||
| 	enableRecaptcha = meta.enableRecaptcha; | 	enableRecaptcha = meta.enableRecaptcha; | ||||||
|  | 	enableTurnstile = meta.enableTurnstile; | ||||||
| 	sensitiveMediaDetection = meta.sensitiveMediaDetection; | 	sensitiveMediaDetection = meta.sensitiveMediaDetection; | ||||||
| 	sensitiveMediaDetectionSensitivity = | 	sensitiveMediaDetectionSensitivity = | ||||||
| 		meta.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0 : | 		meta.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0 : | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue