Skip to content
This repository has been archived by the owner on Oct 5, 2023. It is now read-only.

Commit

Permalink
refactor: captcha (#249)
Browse files Browse the repository at this point in the history
* refactor: captcha

wip: hcaptcha and turnstile

* wip
  • Loading branch information
sousuke0422 authored Oct 20, 2022
1 parent 82f4f17 commit 1af1013
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 26 deletions.
18 changes: 18 additions & 0 deletions migration/1664548052072-turnstile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {MigrationInterface, QueryRunner} from "typeorm";

export class turnstile1664548052072 implements MigrationInterface {
name = 'turnstile1664548052072'

public async up(queryRunner: QueryRunner): Promise<void> {
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)`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
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"`);
}

}
121 changes: 121 additions & 0 deletions src/client/app/common/views/components/captcha.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<template>
<div>
<span v-if="!available">{{ $t('waiting') }}<mk-ellipsis/></span>
<div ref="captcha"></div>
</div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import i18n from '../../../i18n';
type Captcha = {
render(container: string | Node, options: {
readonly [_ in 'sitekey' | 'theme' | 'type' | 'size' | 'tabindex' | 'callback' | 'expired' | 'expired-callback' | 'error-callback' | 'endpoint']?: unknown;
}): string;
remove(id: string): void;
execute(id: string): void;
reset(id: string): void;
getResponse(id: string): string;
};
type CaptchaProvider = 'hcaptcha' | 'grecaptcha' | 'turnstile';
type CaptchaContainer = {
readonly [_ in CaptchaProvider]?: Captcha;
};
declare global {
interface Window extends CaptchaContainer {
}
}
export default defineComponent({
i18n: i18n(),
props: {
provider: {
type: String,
required: true,
},
sitekey: {
type: String,
required: true,
},
value: {
type: String,
},
},
data() {
return {
available: false,
};
},
computed: {
loaded() {
return !!window[this.provider as CaptchaProvider];
},
src() {
const endpoint = ({
hcaptcha: 'https://hcaptcha.com/1',
grecaptcha: 'https://www.google.com/recaptcha',
turnstile: 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit',
} as Record<PropertyKey, unknown>)[this.provider];
return `${typeof endpoint === 'string' ? endpoint : 'about:invalid'}/api.js?render=explicit`;
},
captcha() {
return window[this.provider as CaptchaProvider] || {} as unknown as Captcha;
},
},
created() {
if (this.loaded) {
this.available = true;
} else {
(document.getElementById(this.provider) || document.head.appendChild(Object.assign(document.createElement('script'), {
async: true,
id: this.provider,
src: this.src,
})))
.addEventListener('load', () => this.available = true);
}
},
mounted() {
if (this.available) {
this.requestRender();
} else {
this.$watch('available', this.requestRender);
}
},
beforeDestroy() {
this.reset();
},
methods: {
reset() {
this.captcha?.reset();
},
requestRender() {
if (this.captcha.render && this.$refs.captcha instanceof Element) {
this.captcha.render(this.$refs.captcha, {
sitekey: this.sitekey,
theme: this.$store.state.device.darkMode ? 'dark' : 'light',
callback: this.callback,
'expired-callback': this.callback,
'error-callback': this.callback,
});
} else {
setTimeout(this.requestRender.bind(this), 1);
}
},
callback(response?: string) {
this.$emit('input', typeof response === 'string' ? response : null);
},
},
});
</script>

29 changes: 14 additions & 15 deletions src/client/app/common/views/components/signup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,23 +42,28 @@
<a :href="meta.ToSUrl" target="_blank">{{ $t('tos') }}</a>
</i18n>
</ui-switch>
<div v-if="meta.enableRecaptcha" class="g-recaptcha" :data-sitekey="meta.recaptchaSiteKey" style="margin: 16px 0;"></div>
<captcha v-if="meta.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="captcha" provider="grecaptcha" :sitekey="meta.recaptchaSiteKey"/>
<captcha v-if="meta.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="captcha" provider="hcaptcha" :sitekey="meta.hcaptchaSiteKey"/>
<ui-button type="submit" :disabled=" submitting || !(meta.ToSUrl ? ToSAgreement : true) || passwordRetypeState == 'not-match'">{{ $t('create') }}</ui-button>
</template>
</form>
</template>

<script lang="ts">
import Vue from 'vue';
import { defineComponent } from 'vue';
import i18n from '../../../i18n';
const getPasswordStrength = require('syuilo-password-strength');
import { host, url } from '../../../config';
import { toUnicode } from 'punycode';
import { $i } from '../../../account';
export default Vue.extend({
export default defineComponent({
i18n: i18n('common/views/components/signup.vue'),
components: {
captcha: () => import('./captcha.vue').then(x => x.default),
},
data() {
return {
host: toUnicode(host),
Expand All @@ -73,6 +78,8 @@ export default Vue.extend({
meta: {},
submitting: false,
ToSAgreement: false,
reCaptchaResponse: null,
hCaptchaResponse: null,
};
},
Expand All @@ -91,13 +98,6 @@ export default Vue.extend({
});
},
mounted() {
const head = document.getElementsByTagName('head')[0];
const script = document.createElement('script');
script.setAttribute('src', 'https://www.recaptcha.net/recaptcha/api.js');
head.appendChild(script);
},
methods: {
onChangeUsername() {
if (this.username == '') {
Expand Down Expand Up @@ -154,7 +154,8 @@ export default Vue.extend({
username: this.username,
password: this.password,
invitationCode: this.invitationCode,
'g-recaptcha-response': this.meta.enableRecaptcha ? (window as any).grecaptcha.getResponse() : null,
'hcaptcha-response': this.hCaptchaResponse,
'g-recaptcha-response': this.reCaptchaResponse,
}).then(() => {
this.$root.api('signin', {
username: this.username,
Expand All @@ -166,15 +167,13 @@ export default Vue.extend({
});
}).catch(() => {
this.submitting = false;
this.$refs.hcaptcha?.reset?.();
this.$refs.recaptcha?.reset?.();
this.$root.dialog({
type: 'error',
text: this.$t('some-error'),
});
if (this.meta.enableRecaptcha) {
(window as any).grecaptcha.reset();
}
});
},
},
Expand Down
11 changes: 11 additions & 0 deletions src/misc/captcha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ export async function verifyHcaptcha(secret: string, response: string) {
}
}

export async function verifyTurnstile(secret: string, response: string) {
const result = await 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}`;
}
}

type CaptchaResponse = {
success: boolean;
'error-codes'?: string[];
Expand Down
17 changes: 17 additions & 0 deletions src/models/entities/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,4 +383,21 @@ export class Meta {
default: true,
})
public objectStorageS3ForcePathStyle: boolean;

@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;
}
33 changes: 33 additions & 0 deletions src/server/api/endpoints/admin/update-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,27 @@ export const meta = {
},
},

enableTurnstile: {
validator: $.optional.bool,
desc: {
'ja-JP': 'reCAPTCHAを使用するか否か',
},
},

turnstileSiteKey: {
validator: $.optional.nullable.str,
desc: {
'ja-JP': 'reCAPTCHA site key',
},
},

turnstileSecretKey: {
validator: $.optional.nullable.str,
desc: {
'ja-JP': 'reCAPTCHA secret key',
},
},

proxyAccount: {
validator: $.optional.nullable.str,
desc: {
Expand Down Expand Up @@ -520,6 +541,18 @@ export default define(meta, async (ps, me) => {
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.proxyAccount !== undefined) {
set.proxyAccount = ps.proxyAccount;
}
Expand Down
4 changes: 4 additions & 0 deletions src/server/api/endpoints/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ export default define(meta, async (ps, me) => {
hcaptchaSiteKey: null,
enableRecaptcha: instance.enableRecaptcha,
recaptchaSiteKey: instance.recaptchaSiteKey,
enableTurnstile: instance.enableTurnstile,
turnstileSiteKey: instance.turnstileSiteKey,
turnstileSecretKey: instance.turnstileSecretKey,
swPublickey: instance.swPublicKey,
mascotImageUrl: instance.mascotImageUrl,
bannerUrl: instance.bannerUrl,
Expand Down Expand Up @@ -186,6 +189,7 @@ export default define(meta, async (ps, me) => {
elasticsearch: config.elasticsearch ? true : false,
hcaptcha: false,
recaptcha: instance.enableRecaptcha,
turnstile: instance.enableTurnstile,
objectStorage: instance.useObjectStorage,
twitter: instance.enableTwitterIntegration,
github: instance.enableGithubIntegration,
Expand Down
Loading

0 comments on commit 1af1013

Please sign in to comment.