summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorCyberRex <hspwinx86@gmail.com>2022-10-13 09:19:57 +0900
committerGitHub <noreply@github.com>2022-10-13 09:19:57 +0900
commit1309367884197f4f4d94686fddfbd99fa20262bc (patch)
tree3f21e60ee1bc155a903c403106fbdc6c0e0440b9 /packages
parentadd webhookId to api request (#9113) (diff)
downloadsharkey-1309367884197f4f4d94686fddfbd99fa20262bc.tar.gz
sharkey-1309367884197f4f4d94686fddfbd99fa20262bc.tar.bz2
sharkey-1309367884197f4f4d94686fddfbd99fa20262bc.zip
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>
Diffstat (limited to 'packages')
-rw-r--r--packages/backend/migration/1664694635394-turnstile.js15
-rw-r--r--packages/backend/src/core/CaptchaService.ts11
-rw-r--r--packages/backend/src/models/entities/Meta.ts17
-rw-r--r--packages/backend/src/server/api/SignupApiService.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/admin/meta.ts15
-rw-r--r--packages/backend/src/server/api/endpoints/admin/update-meta.ts15
-rw-r--r--packages/backend/src/server/api/endpoints/meta.ts11
-rw-r--r--packages/client/src/components/MkCaptcha.vue4
-rw-r--r--packages/client/src/components/MkSignup.vue6
-rw-r--r--packages/client/src/pages/admin/bot-protection.vue24
-rw-r--r--packages/client/src/pages/admin/index.vue2
-rw-r--r--packages/client/src/pages/admin/security.vue3
12 files changed, 126 insertions, 3 deletions
diff --git a/packages/backend/migration/1664694635394-turnstile.js b/packages/backend/migration/1664694635394-turnstile.js
new file mode 100644
index 0000000000..4a33443950
--- /dev/null
+++ b/packages/backend/migration/1664694635394-turnstile.js
@@ -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"`);
+ }
+}
diff --git a/packages/backend/src/core/CaptchaService.ts b/packages/backend/src/core/CaptchaService.ts
index 67b4b90061..acfa7d5910 100644
--- a/packages/backend/src/core/CaptchaService.ts
+++ b/packages/backend/src/core/CaptchaService.ts
@@ -66,5 +66,16 @@ export class CaptchaService {
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}`;
+ }
+ }
}
diff --git a/packages/backend/src/models/entities/Meta.ts b/packages/backend/src/models/entities/Meta.ts
index f528b7ac08..fb25e370d2 100644
--- a/packages/backend/src/models/entities/Meta.ts
+++ b/packages/backend/src/models/entities/Meta.ts
@@ -188,6 +188,23 @@ export class Meta {
})
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', {
enum: ['none', 'all', 'local', 'remote'],
default: 'none',
diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts
index 6552dac4bf..edb8e4e8e6 100644
--- a/packages/backend/src/server/api/SignupApiService.ts
+++ b/packages/backend/src/server/api/SignupApiService.ts
@@ -61,6 +61,12 @@ export class SignupApiService {
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'];
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index 5b43c180d8..e5b8b6f8fe 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -47,6 +47,14 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
+ enableTurnstile: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ turnstileSiteKey: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
swPublickey: {
type: 'string',
optional: false, nullable: true,
@@ -197,6 +205,10 @@ export const meta = {
type: 'string',
optional: true, nullable: true,
},
+ turnstileSecretKey: {
+ type: 'string',
+ optional: true, nullable: true,
+ }
sensitiveMediaDetection: {
type: 'string',
optional: true, nullable: false,
@@ -374,6 +386,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
hcaptchaSiteKey: instance.hcaptchaSiteKey,
enableRecaptcha: instance.enableRecaptcha,
recaptchaSiteKey: instance.recaptchaSiteKey,
+ enableTurnstile: instance.enableTurnstile,
+ turnstileSiteKey: instance.turnstileSiteKey,
swPublickey: instance.swPublicKey,
themeColor: instance.themeColor,
mascotImageUrl: instance.mascotImageUrl,
@@ -400,6 +414,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
blockedHosts: instance.blockedHosts,
hcaptchaSecretKey: instance.hcaptchaSecretKey,
recaptchaSecretKey: instance.recaptchaSecretKey,
+ turnstileSecretKey: instance.turnstileSecretKey,
sensitiveMediaDetection: instance.sensitiveMediaDetection,
sensitiveMediaDetectionSensitivity: instance.sensitiveMediaDetectionSensitivity,
setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically,
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index 48fae9b947..2a19b1df5f 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -52,6 +52,9 @@ export const paramDef = {
enableRecaptcha: { type: 'boolean' },
recaptchaSiteKey: { 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'] },
sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] },
setSensitiveFlagAutomatically: { type: 'boolean' },
@@ -231,6 +234,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
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) {
set.sensitiveMediaDetection = ps.sensitiveMediaDetection;
}
diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts
index 5c09c33941..f2e6e6aea8 100644
--- a/packages/backend/src/server/api/endpoints/meta.ts
+++ b/packages/backend/src/server/api/endpoints/meta.ts
@@ -119,6 +119,14 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
+ enableTurnstile: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ turnstileSiteKey: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
swPublickey: {
type: 'string',
optional: false, nullable: true,
@@ -372,6 +380,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
hcaptchaSiteKey: instance.hcaptchaSiteKey,
enableRecaptcha: instance.enableRecaptcha,
recaptchaSiteKey: instance.recaptchaSiteKey,
+ enableTurnstile: instance.enableTurnstile,
+ turnstileSiteKey: instance.turnstileSiteKey,
swPublickey: instance.swPublicKey,
themeColor: instance.themeColor,
mascotImageUrl: instance.mascotImageUrl,
@@ -423,6 +433,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
elasticsearch: this.config.elasticsearch ? true : false,
hcaptcha: instance.enableHcaptcha,
recaptcha: instance.enableRecaptcha,
+ turnstile: instance.enableTurnstile,
objectStorage: instance.useObjectStorage,
twitter: instance.enableTwitterIntegration,
github: instance.enableGithubIntegration,
diff --git a/packages/client/src/components/MkCaptcha.vue b/packages/client/src/components/MkCaptcha.vue
index 7360734914..b399bb8921 100644
--- a/packages/client/src/components/MkCaptcha.vue
+++ b/packages/client/src/components/MkCaptcha.vue
@@ -20,7 +20,7 @@ type Captcha = {
getResponse(id: string): string;
};
-type CaptchaProvider = 'hcaptcha' | 'recaptcha';
+type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile';
type CaptchaContainer = {
readonly [_ in CaptchaProvider]?: Captcha;
@@ -48,6 +48,7 @@ const variable = computed(() => {
switch (props.provider) {
case 'hcaptcha': return 'hcaptcha';
case 'recaptcha': return 'grecaptcha';
+ case 'turnstile': return 'turnstile';
}
});
@@ -57,6 +58,7 @@ const src = computed(() => {
switch (props.provider) {
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 'turnstile': return 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
}
});
diff --git a/packages/client/src/components/MkSignup.vue b/packages/client/src/components/MkSignup.vue
index a324bb6f5a..c1f91b18c2 100644
--- a/packages/client/src/components/MkSignup.vue
+++ b/packages/client/src/components/MkSignup.vue
@@ -59,6 +59,7 @@
</MkSwitch>
<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.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>
</form>
</template>
@@ -92,6 +93,7 @@ const host = toUnicode(config.host);
let hcaptcha = $ref();
let recaptcha = $ref();
+let turnstile = $ref();
let username: string = $ref('');
let password: string = $ref('');
@@ -106,12 +108,14 @@ let submitting: boolean = $ref(false);
let ToSAgreement: boolean = $ref(false);
let hCaptchaResponse = $ref(null);
let reCaptchaResponse = $ref(null);
+let turnstileResponse = $ref(null);
const shouldDisableSubmitting = $computed((): boolean => {
return submitting ||
instance.tosUrl && !ToSAgreement ||
instance.enableHcaptcha && !hCaptchaResponse ||
instance.enableRecaptcha && !reCaptchaResponse ||
+ instance.enableTurnstile && !turnstileResponse ||
passwordRetypeState === 'not-match';
});
@@ -198,6 +202,7 @@ function onSubmit(): void {
invitationCode,
'hcaptcha-response': hCaptchaResponse,
'g-recaptcha-response': reCaptchaResponse,
+ 'turnstile-response': turnstileResponse,
}).then(() => {
if (instance.emailRequiredForSignup) {
os.alert({
@@ -222,6 +227,7 @@ function onSubmit(): void {
submitting = false;
hcaptcha.reset?.();
recaptcha.reset?.();
+ turnstile.reset?.();
os.alert({
type: 'error',
diff --git a/packages/client/src/pages/admin/bot-protection.vue b/packages/client/src/pages/admin/bot-protection.vue
index 72d5e379de..484a9d1a1a 100644
--- a/packages/client/src/pages/admin/bot-protection.vue
+++ b/packages/client/src/pages/admin/bot-protection.vue
@@ -6,6 +6,7 @@
<option :value="null">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option>
<option value="hcaptcha">hCaptcha</option>
<option value="recaptcha">reCAPTCHA</option>
+ <option value="turnstile">Turnstile</option>
</FormRadios>
<template v-if="provider === 'hcaptcha'">
@@ -36,6 +37,20 @@
<MkCaptcha provider="recaptcha" :sitekey="recaptchaSiteKey"/>
</FormSlot>
</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>
</div>
@@ -61,6 +76,8 @@ let hcaptchaSiteKey: string | null = $ref(null);
let hcaptchaSecretKey: string | null = $ref(null);
let recaptchaSiteKey: 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() {
const meta = await os.api('admin/meta');
@@ -68,8 +85,10 @@ async function init() {
hcaptchaSecretKey = meta.hcaptchaSecretKey;
recaptchaSiteKey = meta.recaptchaSiteKey;
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() {
@@ -80,6 +99,9 @@ function save() {
enableRecaptcha: provider === 'recaptcha',
recaptchaSiteKey,
recaptchaSecretKey,
+ enableTurnstile: provider === 'turnstile',
+ turnstileSiteKey,
+ turnstileSecretKey,
}).then(() => {
fetchInstance();
});
diff --git a/packages/client/src/pages/admin/index.vue b/packages/client/src/pages/admin/index.vue
index 9200b5d547..20f82bba28 100644
--- a/packages/client/src/pages/admin/index.vue
+++ b/packages/client/src/pages/admin/index.vue
@@ -53,7 +53,7 @@ let view = $ref(null);
let el = $ref(null);
let pageProps = $ref({});
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 thereIsUnresolvedAbuseReport = $ref(false);
let currentPage = $computed(() => router.currentRef.value.child);
diff --git a/packages/client/src/pages/admin/security.vue b/packages/client/src/pages/admin/security.vue
index c36cedb312..65d079c2cf 100644
--- a/packages/client/src/pages/admin/security.vue
+++ b/packages/client/src/pages/admin/security.vue
@@ -9,6 +9,7 @@
<template #label>{{ i18n.ts.botProtection }}</template>
<template v-if="enableHcaptcha" #suffix>hCaptcha</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>
<XBotProtection/>
@@ -120,6 +121,7 @@ import { definePageMetadata } from '@/scripts/page-metadata';
let summalyProxy: string = $ref('');
let enableHcaptcha: boolean = $ref(false);
let enableRecaptcha: boolean = $ref(false);
+let enableTurnstile: boolean = $ref(false);
let sensitiveMediaDetection: string = $ref('none');
let sensitiveMediaDetectionSensitivity: number = $ref(0);
let setSensitiveFlagAutomatically: boolean = $ref(false);
@@ -132,6 +134,7 @@ async function init() {
summalyProxy = meta.summalyProxy;
enableHcaptcha = meta.enableHcaptcha;
enableRecaptcha = meta.enableRecaptcha;
+ enableTurnstile = meta.enableTurnstile;
sensitiveMediaDetection = meta.sensitiveMediaDetection;
sensitiveMediaDetectionSensitivity =
meta.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0 :