From 78608392208cb73351354cda5678daee232159d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?= <root@acid-chicken.com> Date: Tue, 28 Apr 2020 14:29:33 +0900 Subject: [PATCH 01/12] Add support for hCaptcha --- locales/ja-JP.yml | 4 + migration/1588044505511-hCaptcha.ts | 18 +++++ package.json | 1 + src/@types/hcaptcha.d.ts | 9 +++ src/client/components/hcaptcha.vue | 76 +++++++++++++++++++ src/client/components/signup.vue | 24 ++++-- src/client/components/url-preview.vue | 2 +- src/client/pages/instance/settings.vue | 67 +++++++++++++--- src/models/entities/meta.ts | 17 +++++ src/server/api/endpoints/admin/update-meta.ts | 33 ++++++++ src/server/api/endpoints/meta.ts | 4 + src/server/api/private/signup.ts | 13 +++- src/server/nodeinfo.ts | 1 + src/server/web/views/info.pug | 3 + yarn.lock | 5 ++ 15 files changed, 257 insertions(+), 20 deletions(-) create mode 100644 migration/1588044505511-hCaptcha.ts create mode 100644 src/@types/hcaptcha.d.ts create mode 100644 src/client/components/hcaptcha.vue diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 3db383e37..de44d89a0 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -299,6 +299,10 @@ bannerUrl: "バナー画像のURL" basicInfo: "基本情報" pinnedUsers: "ピン留めユーザー" pinnedUsersDescription: "「みつける」ページなどにピン留めしたいユーザーを改行で区切って記述します。" +hcaptcha: "hCaptcha" +enableHcaptcha: "hCaptchaを有効にする" +hcaptchaSiteKey: "サイトキー" +hcaptchaSecretKey: "シークレットキー" recaptcha: "reCAPTCHA" enableRecaptcha: "reCAPTCHAを有効にする" recaptchaSiteKey: "サイトキー" diff --git a/migration/1588044505511-hCaptcha.ts b/migration/1588044505511-hCaptcha.ts new file mode 100644 index 000000000..a3f4e9367 --- /dev/null +++ b/migration/1588044505511-hCaptcha.ts @@ -0,0 +1,18 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class hCaptcha1588044505511 implements MigrationInterface { + name = 'hCaptcha1588044505511' + + public async up(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(`ALTER TABLE "meta" ADD "enableHcaptcha" boolean NOT NULL DEFAULT false`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ADD "hcaptchaSiteKey" character varying(64)`, undefined); + await queryRunner.query(`ALTER TABLE "meta" ADD "hcaptchaSecretKey" character varying(64)`, undefined); + } + + public async down(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "hcaptchaSecretKey"`, undefined); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "hcaptchaSiteKey"`, undefined); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableHcaptcha"`, undefined); + } + +} diff --git a/package.json b/package.json index 07d88db0e..de669ee48 100644 --- a/package.json +++ b/package.json @@ -144,6 +144,7 @@ "gulp-tslint": "8.1.4", "gulp-typescript": "6.0.0-alpha.1", "hard-source-webpack-plugin": "0.13.1", + "hcaptcha": "0.0.1", "html-minifier": "4.0.0", "http-proxy-agent": "4.0.1", "http-signature": "1.3.4", diff --git a/src/@types/hcaptcha.d.ts b/src/@types/hcaptcha.d.ts new file mode 100644 index 000000000..ef3d44256 --- /dev/null +++ b/src/@types/hcaptcha.d.ts @@ -0,0 +1,9 @@ +declare module 'hcaptcha' { + export function verify(secret: string, token: string): Promise<{ + success: boolean; + challenge_ts: string; + hostname: string; + credit?: boolean; + 'error-codes'?: unknown[]; + }>; +} diff --git a/src/client/components/hcaptcha.vue b/src/client/components/hcaptcha.vue new file mode 100644 index 000000000..e54eb314a --- /dev/null +++ b/src/client/components/hcaptcha.vue @@ -0,0 +1,76 @@ +<template> + <div ref="hCaptcha"></div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +declare global { + interface Window { + hcaptcha?: { + 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; + }; + } +} + +export default Vue.extend({ + props: { + sitekey: { + type: String, + required: true, + }, + value: { + type: String, + }, + }, + + data() { + return { + available: false, + }; + }, + + created() { + if (window.hcaptcha) { + this.available = true; + } else { + const script = document.createElement('script'); + script.addEventListener('load', () => this.available = true); + script.src = 'https://hcaptcha.com/1/api.js?render=explicit'; + script.async = true; + document.head.appendChild(script); + } + }, + + mounted() { + if (this.available) { + this.render(); + } else { + this.$watch('available', this.render); + } + }, + + methods: { + render() { + if (this.$refs.hCaptcha instanceof Element) { + window.hcaptcha!.render(this.$refs.hCaptcha, { + sitekey: this.sitekey, + theme: this.$store.state.device.darkMode ? 'dark' : 'light', + callback: this.callback, + 'expired-callback': this.callback, + 'error-callback': this.callback, + }); + } + }, + callback(response?: string) { + this.$emit('input', typeof response == 'string' ? response : null); + }, + }, +}); +</script> diff --git a/src/client/components/signup.vue b/src/client/components/signup.vue index 9f3ae8db2..5d16a82ba 100644 --- a/src/client/components/signup.vue +++ b/src/client/components/signup.vue @@ -42,7 +42,8 @@ </i18n> </mk-switch> <div v-if="meta.enableRecaptcha" class="g-recaptcha" :data-sitekey="meta.recaptchaSiteKey" style="margin: 16px 0;"></div> - <mk-button type="submit" :disabled=" submitting || !(meta.tosUrl ? ToSAgreement : true) || passwordRetypeState == 'not-match'" primary>{{ $t('start') }}</mk-button> + <h-captcha v-if="meta.enableHcaptcha" v-model="hCaptchaResponse" :sitekey="meta.hcaptchaSiteKey"/> + <mk-button type="submit" :disabled="shouldDisableSubmitting" primary>{{ $t('start') }}</mk-button> </template> </form> </template> @@ -65,6 +66,7 @@ export default Vue.extend({ MkButton, MkInput, MkSwitch, + hCaptcha: () => import('./hcaptcha.vue').then(x => x.default), }, data() { @@ -80,6 +82,7 @@ export default Vue.extend({ passwordRetypeState: null, submitting: false, ToSAgreement: false, + hCaptchaResponse: null, faLock, faExclamationTriangle, faSpinner, faCheck, faKey } }, @@ -96,7 +99,14 @@ export default Vue.extend({ meta() { return this.$store.state.instance.meta; }, - + + shouldDisableSubmitting(): boolean { + return this.submitting || + this.meta.tosUrl && !this.ToSAgreement || + this.meta.enableHcaptcha && !this.hCaptchaResponse || + this.passwordRetypeState == 'not-match'; + }, + shouldShowProfileUrl(): boolean { return (this.username != '' && this.usernameState != 'invalid-format' && @@ -115,10 +125,11 @@ export default Vue.extend({ }, mounted() { - const head = document.getElementsByTagName('head')[0]; - const script = document.createElement('script'); - script.setAttribute('src', 'https://www.google.com/recaptcha/api.js'); - head.appendChild(script); + if (this.meta.enableRecaptcha) { + const script = document.createElement('script'); + script.setAttribute('src', 'https://www.google.com/recaptcha/api.js'); + document.head.appendChild(script); + } }, methods: { @@ -177,6 +188,7 @@ export default Vue.extend({ username: this.username, password: this.password, invitationCode: this.invitationCode, + 'hcaptcha-response': this.hCaptchaResponse, 'g-recaptcha-response': this.meta.enableRecaptcha ? (window as any).grecaptcha.getResponse() : null }).then(() => { this.$root.api('signin', { diff --git a/src/client/components/url-preview.vue b/src/client/components/url-preview.vue index 94d07cbae..c2dd0038b 100644 --- a/src/client/components/url-preview.vue +++ b/src/client/components/url-preview.vue @@ -4,7 +4,7 @@ <iframe :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen /> </div> <div v-else-if="tweetUrl && detail" class="twitter"> - <blockquote ref="tweet" class="twitter-tweet" :data-theme="$store.state.device.darkmode ? 'dark' : null"> + <blockquote ref="tweet" class="twitter-tweet" :data-theme="$store.state.device.darkMode ? 'dark' : null"> <a :href="url"></a> </blockquote> </div> diff --git a/src/client/pages/instance/settings.vue b/src/client/pages/instance/settings.vue index f7db4aa10..088db8763 100644 --- a/src/client/pages/instance/settings.vue +++ b/src/client/pages/instance/settings.vue @@ -38,6 +38,24 @@ </div> </section> + <section class="_card"> + <div class="_title"><fa :icon="faShieldAlt"/> {{ $t('hcaptcha') }}</div> + <div class="_content"> + <mk-switch v-model="enableHcaptcha">{{ $t('enableHcaptcha') }}</mk-switch> + <template v-if="enableHcaptcha"> + <mk-input v-model="hcaptchaSiteKey" :disabled="!enableHcaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('hcaptchaSiteKey') }}</mk-input> + <mk-input v-model="hcaptchaSecretKey" :disabled="!enableHcaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('hcaptchaSecretKey') }}</mk-input> + </template> + </div> + <div class="_content" v-if="enableHcaptcha && hcaptchaSiteKey"> + <header>{{ $t('preview') }}</header> + <h-captcha v-if="enableHcaptcha" :sitekey="hcaptchaSiteKey"/> + </div> + <div class="_footer"> + <mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button> + </div> + </section> + <section class="_card"> <div class="_title"><fa :icon="faShieldAlt"/> {{ $t('recaptcha') }}</div> <div class="_content"> @@ -195,6 +213,12 @@ import { url } from '../../config'; import i18n from '../../i18n'; import getAcct from '../../../misc/acct/render'; +declare global { + interface Window { + onRecaptchaLoad?: Function; + } +} + export default Vue.extend({ i18n, @@ -210,6 +234,7 @@ export default Vue.extend({ MkTextarea, MkSwitch, MkInfo, + hCaptcha: () => import('../../components/hcaptcha.vue').then(x => x.default), }, data() { @@ -234,6 +259,9 @@ export default Vue.extend({ enableRegistration: false, enableLocalTimeline: false, enableGlobalTimeline: false, + enableHcaptcha: false, + hcaptchaSiteKey: null, + hcaptchaSecretKey: null, enableRecaptcha: false, recaptchaSiteKey: null, recaptchaSecretKey: null, @@ -282,6 +310,9 @@ export default Vue.extend({ this.enableRegistration = !this.meta.disableRegistration; this.enableLocalTimeline = !this.meta.disableLocalTimeline; this.enableGlobalTimeline = !this.meta.disableGlobalTimeline; + this.enableHcaptcha = this.meta.enableHcaptcha; + this.hcaptchaSiteKey = this.meta.hcaptchaSiteKey; + this.hcaptchaSecretKey = this.meta.hcaptchaSecretKey; this.enableRecaptcha = this.meta.enableRecaptcha; this.recaptchaSiteKey = this.meta.recaptchaSiteKey; this.recaptchaSecretKey = this.meta.recaptchaSecretKey; @@ -327,24 +358,33 @@ export default Vue.extend({ const renderRecaptchaPreview = () => { if (!(window as any).grecaptcha) return; if (!this.$refs.recaptcha) return; + if (!this.enableRecaptcha) return; if (!this.recaptchaSiteKey) return; (window as any).grecaptcha.render(this.$refs.recaptcha, { sitekey: this.recaptchaSiteKey }); }; - window.onRecaotchaLoad = () => { - renderRecaptchaPreview(); + let recaptchaLoaded: boolean = false; + const requestRenderRecaptchaPreview = () => { + if (window.onRecaptchaLoad) { // loading + return; + } + + if (recaptchaLoaded) { // loaded + delete window.onRecaptchaLoad; + renderRecaptchaPreview(); + } else { // init + window.onRecaptchaLoad = () => { + recaptchaLoaded = true; + renderRecaptchaPreview(); + }; + const script = document.createElement('script'); + script.setAttribute('src', 'https://www.google.com/recaptcha/api.js?onload=onRecaptchaLoad'); + document.head.appendChild(script); + } }; - const head = document.getElementsByTagName('head')[0]; - const script = document.createElement('script'); - script.setAttribute('src', 'https://www.google.com/recaptcha/api.js?onload=onRecaotchaLoad'); - head.appendChild(script); - this.$watch('enableRecaptcha', () => { - renderRecaptchaPreview(); - }); - this.$watch('recaptchaSiteKey', () => { - renderRecaptchaPreview(); - }); + this.$watch('enableRecaptcha', requestRenderRecaptchaPreview); + this.$watch('recaptchaSiteKey', requestRenderRecaptchaPreview); }, methods: { @@ -391,6 +431,9 @@ export default Vue.extend({ disableRegistration: !this.enableRegistration, disableLocalTimeline: !this.enableLocalTimeline, disableGlobalTimeline: !this.enableGlobalTimeline, + enableHcaptcha: this.enableHcaptcha, + hcaptchaSiteKey: this.hcaptchaSiteKey, + hcaptchaSecretKey: this.hcaptchaSecretKey, enableRecaptcha: this.enableRecaptcha, recaptchaSiteKey: this.recaptchaSiteKey, recaptchaSecretKey: this.recaptchaSecretKey, diff --git a/src/models/entities/meta.ts b/src/models/entities/meta.ts index bb463c52f..622d28bea 100644 --- a/src/models/entities/meta.ts +++ b/src/models/entities/meta.ts @@ -124,6 +124,23 @@ export class Meta { @JoinColumn() public proxyAccount: User | null; + @Column('boolean', { + default: false, + }) + public enableHcaptcha: boolean; + + @Column('varchar', { + length: 64, + nullable: true + }) + public hcaptchaSiteKey: string | null; + + @Column('varchar', { + length: 64, + nullable: true + }) + public hcaptchaSecretKey: string | null; + @Column('boolean', { default: false, }) diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts index 1bc20029e..dffe7ffe7 100644 --- a/src/server/api/endpoints/admin/update-meta.ts +++ b/src/server/api/endpoints/admin/update-meta.ts @@ -145,6 +145,27 @@ export const meta = { } }, + enableHcaptcha: { + validator: $.optional.bool, + desc: { + 'ja-JP': 'hCaptchaを使用するか否か' + } + }, + + hcaptchaSiteKey: { + validator: $.optional.nullable.str, + desc: { + 'ja-JP': 'hCaptcha site key' + } + }, + + hcaptchaSecretKey: { + validator: $.optional.nullable.str, + desc: { + 'ja-JP': 'hCaptcha secret key' + } + }, + enableRecaptcha: { validator: $.optional.bool, desc: { @@ -472,6 +493,18 @@ export default define(meta, async (ps, me) => { set.proxyRemoteFiles = ps.proxyRemoteFiles; } + if (ps.enableHcaptcha !== undefined) { + set.enableHcaptcha = ps.enableHcaptcha; + } + + if (ps.hcaptchaSiteKey !== undefined) { + set.hcaptchaSiteKey = ps.hcaptchaSiteKey; + } + + if (ps.hcaptchaSecretKey !== undefined) { + set.hcaptchaSecretKey = ps.hcaptchaSecretKey; + } + if (ps.enableRecaptcha !== undefined) { set.enableRecaptcha = ps.enableRecaptcha; } diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts index 179355489..eefc37012 100644 --- a/src/server/api/endpoints/meta.ts +++ b/src/server/api/endpoints/meta.ts @@ -122,6 +122,8 @@ export default define(meta, async (ps, me) => { driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb, cacheRemoteFiles: instance.cacheRemoteFiles, proxyRemoteFiles: instance.proxyRemoteFiles, + enableHcaptcha: instance.enableHcaptcha, + hcaptchaSiteKey: instance.hcaptchaSiteKey, enableRecaptcha: instance.enableRecaptcha, recaptchaSiteKey: instance.recaptchaSiteKey, swPublickey: instance.swPublicKey, @@ -149,6 +151,7 @@ export default define(meta, async (ps, me) => { localTimeLine: !instance.disableLocalTimeline, globalTimeLine: !instance.disableGlobalTimeline, elasticsearch: config.elasticsearch ? true : false, + hcaptcha: instance.enableHcaptcha, recaptcha: instance.enableRecaptcha, objectStorage: instance.useObjectStorage, twitter: instance.enableTwitterIntegration, @@ -164,6 +167,7 @@ export default define(meta, async (ps, me) => { response.pinnedUsers = instance.pinnedUsers; response.hiddenTags = instance.hiddenTags; response.blockedHosts = instance.blockedHosts; + response.hcaptchaSecretKey = instance.hcaptchaSecretKey; response.recaptchaSecretKey = instance.recaptchaSecretKey; response.proxyAccountId = instance.proxyAccountId; response.twitterConsumerKey = instance.twitterConsumerKey; diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts index 79ee74389..e23fe4358 100644 --- a/src/server/api/private/signup.ts +++ b/src/server/api/private/signup.ts @@ -1,5 +1,6 @@ import * as Koa from 'koa'; import { fetchMeta } from '../../../misc/fetch-meta'; +import { verify } from 'hcaptcha'; import * as recaptcha from 'recaptcha-promise'; import { Users, RegistrationTickets } from '../../../models'; import { signup } from '../common/signup'; @@ -9,8 +10,18 @@ export default async (ctx: Koa.Context) => { const instance = await fetchMeta(true); - // Verify recaptcha + // Verify *Captcha // ただしテスト時はこの機構は障害となるため無効にする + if (process.env.NODE_ENV !== 'test' && instance.enableHcaptcha && instance.hcaptchaSecretKey) { + const success = await verify(instance.hcaptchaSecretKey, body['hcaptcha-response']).then( + ({ 'error-codes': x }) => !x || !x.length, + () => false, + ); + + if (!success) { + ctx.throw(400, 'hcaptcha-failed'); + } + } if (process.env.NODE_ENV !== 'test' && instance.enableRecaptcha && instance.recaptchaSecretKey) { recaptcha.init({ secret_key: instance.recaptchaSecretKey diff --git a/src/server/nodeinfo.ts b/src/server/nodeinfo.ts index 2ff924e68..442e946df 100644 --- a/src/server/nodeinfo.ts +++ b/src/server/nodeinfo.ts @@ -65,6 +65,7 @@ const nodeinfo2 = async () => { disableRegistration: meta.disableRegistration, disableLocalTimeline: meta.disableLocalTimeline, disableGlobalTimeline: meta.disableGlobalTimeline, + enableHcaptcha: meta.enableHcaptcha, enableRecaptcha: meta.enableRecaptcha, maxNoteTextLength: meta.maxNoteTextLength, enableTwitterIntegration: meta.enableTwitterIntegration, diff --git a/src/server/web/views/info.pug b/src/server/web/views/info.pug index 992e652a6..4553d2e2b 100644 --- a/src/server/web/views/info.pug +++ b/src/server/web/views/info.pug @@ -106,6 +106,9 @@ html tr th Registration td= !meta.disableRegistration ? 'yes' : 'no' + tr + th hCaptcha enabled + td= meta.enableHcaptcha ? 'enabled' : 'disabled' tr th reCAPTCHA enabled td= meta.enableRecaptcha ? 'enabled' : 'disabled' diff --git a/yarn.lock b/yarn.lock index 07fe0f88d..1958dde9f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4302,6 +4302,11 @@ hash-sum@^1.0.2: resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-1.0.2.tgz#33b40777754c6432573c120cc3808bbd10d47f04" integrity sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ= +hcaptcha@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/hcaptcha/-/hcaptcha-0.0.1.tgz#e8c5e25a943083d06630bf077bae8a3053fa3da5" + integrity sha512-xGU7wSg3BENwEsOplfMghyR7SL/AXKllmCRMkmt3WQHxhINVNs2u7pP7V5FhigNFBNt6zz32GDRzLqfeDzqPyA== + he@1.2.0, he@^1.1.0, he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" From b862c055ae81a0ed1f239fb43edf699cd70f711e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?= <root@acid-chicken.com> Date: Tue, 28 Apr 2020 14:55:12 +0900 Subject: [PATCH 02/12] Fix bug --- src/client/components/hcaptcha.vue | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/client/components/hcaptcha.vue b/src/client/components/hcaptcha.vue index e54eb314a..4711b303d 100644 --- a/src/client/components/hcaptcha.vue +++ b/src/client/components/hcaptcha.vue @@ -23,7 +23,7 @@ export default Vue.extend({ props: { sitekey: { type: String, - required: true, + required: true, }, value: { type: String, @@ -37,13 +37,22 @@ export default Vue.extend({ }, created() { - if (window.hcaptcha) { + if (window.hcaptcha) { // loaded this.available = true; } else { + const alreadyLoading = document.getElementById('hcaptcha'); + + if (alreadyLoading) { // loading + alreadyLoading.addEventListener('load', () => this.available = true); + + return; + } // init + const script = document.createElement('script'); script.addEventListener('load', () => this.available = true); - script.src = 'https://hcaptcha.com/1/api.js?render=explicit'; script.async = true; + script.id = 'hcaptcha'; + script.src = 'https://hcaptcha.com/1/api.js?render=explicit'; document.head.appendChild(script); } }, From 9daa9007935578ed1d12d46916d913d802816087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?= <root@acid-chicken.com> Date: Wed, 29 Apr 2020 05:30:58 +0900 Subject: [PATCH 03/12] Refactor codes --- src/client/components/hcaptcha.vue | 20 ++++++-------------- src/client/pages/instance/settings.vue | 3 +-- src/server/api/private/signup.ts | 2 +- 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/client/components/hcaptcha.vue b/src/client/components/hcaptcha.vue index 4711b303d..f49336648 100644 --- a/src/client/components/hcaptcha.vue +++ b/src/client/components/hcaptcha.vue @@ -40,20 +40,12 @@ export default Vue.extend({ if (window.hcaptcha) { // loaded this.available = true; } else { - const alreadyLoading = document.getElementById('hcaptcha'); - - if (alreadyLoading) { // loading - alreadyLoading.addEventListener('load', () => this.available = true); - - return; - } // init - - const script = document.createElement('script'); - script.addEventListener('load', () => this.available = true); - script.async = true; - script.id = 'hcaptcha'; - script.src = 'https://hcaptcha.com/1/api.js?render=explicit'; - document.head.appendChild(script); + (document.getElementById('hcaptcha') || (x => document.head.appendChild(Object.assign(x, { + async: true, + id: 'hcaptcha', + src: 'https://hcaptcha.com/1/api.js?render=explicit', + })))(document.createElement('script'))) + .addEventListener('load', () => this.available = true); } }, diff --git a/src/client/pages/instance/settings.vue b/src/client/pages/instance/settings.vue index 088db8763..1292316b2 100644 --- a/src/client/pages/instance/settings.vue +++ b/src/client/pages/instance/settings.vue @@ -371,11 +371,10 @@ export default Vue.extend({ } if (recaptchaLoaded) { // loaded - delete window.onRecaptchaLoad; renderRecaptchaPreview(); } else { // init window.onRecaptchaLoad = () => { - recaptchaLoaded = true; + recaptchaLoaded = delete window.onRecaptchaLoad; renderRecaptchaPreview(); }; const script = document.createElement('script'); diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts index e23fe4358..04d0501b6 100644 --- a/src/server/api/private/signup.ts +++ b/src/server/api/private/signup.ts @@ -14,7 +14,7 @@ export default async (ctx: Koa.Context) => { // ただしテスト時はこの機構は障害となるため無効にする if (process.env.NODE_ENV !== 'test' && instance.enableHcaptcha && instance.hcaptchaSecretKey) { const success = await verify(instance.hcaptchaSecretKey, body['hcaptcha-response']).then( - ({ 'error-codes': x }) => !x || !x.length, + ({ success }) => success, () => false, ); From 01411327b89c6b5c1a4b1d559d81d63c4b120c02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?= <root@acid-chicken.com> Date: Wed, 29 Apr 2020 06:04:05 +0900 Subject: [PATCH 04/12] Add *captcha settings guide --- locales/ja-JP.yml | 2 ++ src/client/pages/instance/settings.vue | 38 ++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index de44d89a0..12b28019d 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -107,6 +107,7 @@ customEmojis: "カスタム絵文字" emojiName: "絵文字名" emojiUrl: "絵文字画像URL" addEmoji: "絵文字を追加" +settingGuide: "おすすめ設定" cacheRemoteFiles: "リモートのファイルをキャッシュする" cacheRemoteFilesDescription: "この設定を無効にすると、リモートファイルをキャッシュせず直リンクするようになります。サーバーのストレージを節約できますが、サムネイルが生成されないので通信量が増加します。" flagAsBot: "Botとして設定" @@ -307,6 +308,7 @@ recaptcha: "reCAPTCHA" enableRecaptcha: "reCAPTCHAを有効にする" recaptchaSiteKey: "サイトキー" recaptchaSecretKey: "シークレットキー" +avoidMultiCaptchaConfirm: "単一のCaptchaのみの使用が推奨されます。他のCaptchaを無効にしますか?キャンセルして複数のCaptchaを有効化したままにすることも可能です。" antennas: "アンテナ" manageAntennas: "アンテナの管理" name: "名前" diff --git a/src/client/pages/instance/settings.vue b/src/client/pages/instance/settings.vue index 1292316b2..008d6ef1a 100644 --- a/src/client/pages/instance/settings.vue +++ b/src/client/pages/instance/settings.vue @@ -239,6 +239,7 @@ export default Vue.extend({ data() { return { + loaded: false, url, proxyAccount: null, proxyAccountId: null, @@ -298,6 +299,41 @@ export default Vue.extend({ }, }, + watch: { + enableHcaptcha(enabled) { + if (enabled && this.loaded && this.enableRecaptcha) { + this.$root.dialog({ + type: 'question', // warning だと間違って cancel するかもしれない + showCancelButton: true, + title: this.$t('settingGuide'), + text: this.$t('avoidMultiCaptchaConfirm'), + }).then(({ canceled }) => { + if (canceled) { + return; + } + + this.enableRecaptcha = false; + }); + } + }, + enableRecaptcha(enabled) { + if (enabled && this.loaded && this.enableHcaptcha) { + this.$root.dialog({ + type: 'question', // warning だと間違って cancel するかもしれない + showCancelButton: true, + title: this.$t('settingGuide'), + text: this.$t('avoidMultiCaptchaConfirm'), + }).then(({ canceled }) => { + if (canceled) { + return; + } + + this.enableHcaptcha = false; + }); + } + } + }, + created() { this.name = this.meta.name; this.description = this.meta.description; @@ -352,6 +388,8 @@ export default Vue.extend({ this.proxyAccount = proxyAccount; }); } + + this.loaded = true; }, mounted() { From 805472482e4197dc0bbda64e163d8fc6ff3d8d57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?= <root@acid-chicken.com> Date: Wed, 29 Apr 2020 06:09:17 +0900 Subject: [PATCH 05/12] Refactor code --- src/client/components/hcaptcha.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/components/hcaptcha.vue b/src/client/components/hcaptcha.vue index f49336648..4bc03342f 100644 --- a/src/client/components/hcaptcha.vue +++ b/src/client/components/hcaptcha.vue @@ -40,11 +40,11 @@ export default Vue.extend({ if (window.hcaptcha) { // loaded this.available = true; } else { - (document.getElementById('hcaptcha') || (x => document.head.appendChild(Object.assign(x, { + (document.getElementById('hcaptcha') || document.head.appendChild(Object.assign(document.createElement('script'), { async: true, id: 'hcaptcha', src: 'https://hcaptcha.com/1/api.js?render=explicit', - })))(document.createElement('script'))) + }))) .addEventListener('load', () => this.available = true); } }, From 77adf26236855b4b43a1072ad2dc6a904bef7d39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?= <root@acid-chicken.com> Date: Wed, 29 Apr 2020 09:15:18 +0900 Subject: [PATCH 06/12] Factorize *captcha component --- src/client/components/captcha.vue | 119 ++++++++++++++++++++++ src/client/components/hcaptcha.vue | 77 --------------- src/client/components/signup-dialog.vue | 2 +- src/client/components/signup.vue | 33 ++++--- src/client/pages/instance/settings.vue | 125 ++++++++---------------- 5 files changed, 180 insertions(+), 176 deletions(-) create mode 100644 src/client/components/captcha.vue delete mode 100644 src/client/components/hcaptcha.vue diff --git a/src/client/components/captcha.vue b/src/client/components/captcha.vue new file mode 100644 index 000000000..6b1ee6f0b --- /dev/null +++ b/src/client/components/captcha.vue @@ -0,0 +1,119 @@ +<template> +<div> + <span v-if="!available">{{ $t('waiting') }}<mk-ellipsis/></span> + <div ref="captcha"></div> +</div> +</template> + +<script lang="ts"> +import Vue 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'; + +type CaptchaContainer = { + readonly [_ in CaptchaProvider]?: Captcha; +}; + +declare global { + interface Window extends CaptchaContainer { + } +} + +export default Vue.extend({ + 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', + } 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> diff --git a/src/client/components/hcaptcha.vue b/src/client/components/hcaptcha.vue deleted file mode 100644 index 4bc03342f..000000000 --- a/src/client/components/hcaptcha.vue +++ /dev/null @@ -1,77 +0,0 @@ -<template> - <div ref="hCaptcha"></div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -declare global { - interface Window { - hcaptcha?: { - 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; - }; - } -} - -export default Vue.extend({ - props: { - sitekey: { - type: String, - required: true, - }, - value: { - type: String, - }, - }, - - data() { - return { - available: false, - }; - }, - - created() { - if (window.hcaptcha) { // loaded - this.available = true; - } else { - (document.getElementById('hcaptcha') || document.head.appendChild(Object.assign(document.createElement('script'), { - async: true, - id: 'hcaptcha', - src: 'https://hcaptcha.com/1/api.js?render=explicit', - }))) - .addEventListener('load', () => this.available = true); - } - }, - - mounted() { - if (this.available) { - this.render(); - } else { - this.$watch('available', this.render); - } - }, - - methods: { - render() { - if (this.$refs.hCaptcha instanceof Element) { - window.hcaptcha!.render(this.$refs.hCaptcha, { - sitekey: this.sitekey, - theme: this.$store.state.device.darkMode ? 'dark' : 'light', - callback: this.callback, - 'expired-callback': this.callback, - 'error-callback': this.callback, - }); - } - }, - callback(response?: string) { - this.$emit('input', typeof response == 'string' ? response : null); - }, - }, -}); -</script> diff --git a/src/client/components/signup-dialog.vue b/src/client/components/signup-dialog.vue index 10cdf3a56..4db79af51 100644 --- a/src/client/components/signup-dialog.vue +++ b/src/client/components/signup-dialog.vue @@ -1,5 +1,5 @@ <template> -<x-window ref="window" @closed="() => { $emit('closed'); destroyDom(); }"> +<x-window ref="window" :width="366" :height="506" @closed="() => { $emit('closed'); destroyDom(); }"> <template #header>{{ $t('signup') }}</template> <x-signup :auto-set="autoSet" @signup="onSignup"/> </x-window> diff --git a/src/client/components/signup.vue b/src/client/components/signup.vue index 5d16a82ba..7e8bac776 100644 --- a/src/client/components/signup.vue +++ b/src/client/components/signup.vue @@ -41,8 +41,8 @@ <a :href="meta.tosUrl" class="_link" target="_blank">{{ $t('tos') }}</a> </i18n> </mk-switch> - <div v-if="meta.enableRecaptcha" class="g-recaptcha" :data-sitekey="meta.recaptchaSiteKey" style="margin: 16px 0;"></div> - <h-captcha v-if="meta.enableHcaptcha" v-model="hCaptchaResponse" :sitekey="meta.hcaptchaSiteKey"/> + <captcha v-if="meta.enableHcaptcha" class="captcha" provider="hcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :sitekey="meta.hcaptchaSiteKey"/> + <captcha v-if="meta.enableRecaptcha" class="captcha" provider="grecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :sitekey="meta.recaptchaSiteKey"/> <mk-button type="submit" :disabled="shouldDisableSubmitting" primary>{{ $t('start') }}</mk-button> </template> </form> @@ -66,7 +66,7 @@ export default Vue.extend({ MkButton, MkInput, MkSwitch, - hCaptcha: () => import('./hcaptcha.vue').then(x => x.default), + captcha: () => import('./captcha.vue').then(x => x.default), }, data() { @@ -83,6 +83,7 @@ export default Vue.extend({ submitting: false, ToSAgreement: false, hCaptchaResponse: null, + reCaptchaResponse: null, faLock, faExclamationTriangle, faSpinner, faCheck, faKey } }, @@ -124,14 +125,6 @@ export default Vue.extend({ } }, - mounted() { - if (this.meta.enableRecaptcha) { - const script = document.createElement('script'); - script.setAttribute('src', 'https://www.google.com/recaptcha/api.js'); - document.head.appendChild(script); - } - }, - methods: { onChangeUsername() { if (this.username == '') { @@ -189,7 +182,7 @@ export default Vue.extend({ password: this.password, invitationCode: this.invitationCode, 'hcaptcha-response': this.hCaptchaResponse, - 'g-recaptcha-response': this.meta.enableRecaptcha ? (window as any).grecaptcha.getResponse() : null + 'g-recaptcha-response': this.meta.reCaptchaResponse, }).then(() => { this.$root.api('signin', { username: this.username, @@ -199,17 +192,25 @@ export default Vue.extend({ }); }).catch(() => { this.submitting = false; + this.$refs.hcaptcha?.reset?.(); + this.$refs.recaptcha?.reset?.(); this.$root.dialog({ type: 'error', text: this.$t('error') }); - - if (this.meta.enableRecaptcha) { - (window as any).grecaptcha.reset(); - } }); } } }); </script> + +<style lang="scss" scoped> +.mk-signup { + padding: 32px 0 0; + + .captcha { + margin: 16px 0; + } +} +</style> diff --git a/src/client/pages/instance/settings.vue b/src/client/pages/instance/settings.vue index 008d6ef1a..f0fa84699 100644 --- a/src/client/pages/instance/settings.vue +++ b/src/client/pages/instance/settings.vue @@ -41,15 +41,15 @@ <section class="_card"> <div class="_title"><fa :icon="faShieldAlt"/> {{ $t('hcaptcha') }}</div> <div class="_content"> - <mk-switch v-model="enableHcaptcha">{{ $t('enableHcaptcha') }}</mk-switch> + <mk-switch v-model="enableHcaptcha" @input="guide('enableHcaptcha')">{{ $t('enableHcaptcha') }}</mk-switch> <template v-if="enableHcaptcha"> <mk-input v-model="hcaptchaSiteKey" :disabled="!enableHcaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('hcaptchaSiteKey') }}</mk-input> <mk-input v-model="hcaptchaSecretKey" :disabled="!enableHcaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('hcaptchaSecretKey') }}</mk-input> </template> </div> - <div class="_content" v-if="enableHcaptcha && hcaptchaSiteKey"> + <div class="_content" v-if="enableHcaptcha"> <header>{{ $t('preview') }}</header> - <h-captcha v-if="enableHcaptcha" :sitekey="hcaptchaSiteKey"/> + <captcha v-if="enableHcaptcha" provider="hcaptcha" :sitekey="hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/> </div> <div class="_footer"> <mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button> @@ -59,7 +59,7 @@ <section class="_card"> <div class="_title"><fa :icon="faShieldAlt"/> {{ $t('recaptcha') }}</div> <div class="_content"> - <mk-switch v-model="enableRecaptcha">{{ $t('enableRecaptcha') }}</mk-switch> + <mk-switch v-model="enableRecaptcha" @input="guide('enableRecaptcha')">{{ $t('enableRecaptcha') }}</mk-switch> <template v-if="enableRecaptcha"> <mk-input v-model="recaptchaSiteKey" :disabled="!enableRecaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('recaptchaSiteKey') }}</mk-input> <mk-input v-model="recaptchaSecretKey" :disabled="!enableRecaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('recaptchaSecretKey') }}</mk-input> @@ -67,7 +67,7 @@ </div> <div class="_content" v-if="enableRecaptcha && recaptchaSiteKey"> <header>{{ $t('preview') }}</header> - <div ref="recaptcha" style="margin: 16px 0 0 0;" :key="recaptchaSiteKey"></div> + <captcha v-if="enableRecaptcha" provider="grecaptcha" :sitekey="recaptchaSiteKey"/> </div> <div class="_footer"> <mk-button primary @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</mk-button> @@ -213,12 +213,6 @@ import { url } from '../../config'; import i18n from '../../i18n'; import getAcct from '../../../misc/acct/render'; -declare global { - interface Window { - onRecaptchaLoad?: Function; - } -} - export default Vue.extend({ i18n, @@ -234,12 +228,11 @@ export default Vue.extend({ MkTextarea, MkSwitch, MkInfo, - hCaptcha: () => import('../../components/hcaptcha.vue').then(x => x.default), + Captcha: () => import('../../components/captcha.vue').then(x => x.default), }, data() { return { - loaded: false, url, proxyAccount: null, proxyAccountId: null, @@ -299,41 +292,6 @@ export default Vue.extend({ }, }, - watch: { - enableHcaptcha(enabled) { - if (enabled && this.loaded && this.enableRecaptcha) { - this.$root.dialog({ - type: 'question', // warning だと間違って cancel するかもしれない - showCancelButton: true, - title: this.$t('settingGuide'), - text: this.$t('avoidMultiCaptchaConfirm'), - }).then(({ canceled }) => { - if (canceled) { - return; - } - - this.enableRecaptcha = false; - }); - } - }, - enableRecaptcha(enabled) { - if (enabled && this.loaded && this.enableHcaptcha) { - this.$root.dialog({ - type: 'question', // warning だと間違って cancel するかもしれない - showCancelButton: true, - title: this.$t('settingGuide'), - text: this.$t('avoidMultiCaptchaConfirm'), - }).then(({ canceled }) => { - if (canceled) { - return; - } - - this.enableHcaptcha = false; - }); - } - } - }, - created() { this.name = this.meta.name; this.description = this.meta.description; @@ -388,43 +346,46 @@ export default Vue.extend({ this.proxyAccount = proxyAccount; }); } - - this.loaded = true; - }, - - mounted() { - const renderRecaptchaPreview = () => { - if (!(window as any).grecaptcha) return; - if (!this.$refs.recaptcha) return; - if (!this.enableRecaptcha) return; - if (!this.recaptchaSiteKey) return; - (window as any).grecaptcha.render(this.$refs.recaptcha, { - sitekey: this.recaptchaSiteKey - }); - }; - let recaptchaLoaded: boolean = false; - const requestRenderRecaptchaPreview = () => { - if (window.onRecaptchaLoad) { // loading - return; - } - - if (recaptchaLoaded) { // loaded - renderRecaptchaPreview(); - } else { // init - window.onRecaptchaLoad = () => { - recaptchaLoaded = delete window.onRecaptchaLoad; - renderRecaptchaPreview(); - }; - const script = document.createElement('script'); - script.setAttribute('src', 'https://www.google.com/recaptcha/api.js?onload=onRecaptchaLoad'); - document.head.appendChild(script); - } - }; - this.$watch('enableRecaptcha', requestRenderRecaptchaPreview); - this.$watch('recaptchaSiteKey', requestRenderRecaptchaPreview); }, methods: { + guide(key: 'enableHcaptcha' | 'enableRecaptcha') { + ({ + enableHcaptcha() { + if (this.enableHcaptcha && this.enableRecaptcha) { + this.$root.dialog({ + type: 'question', // warning だと間違って cancel するかもしれない + showCancelButton: true, + title: this.$t('settingGuide'), + text: this.$t('avoidMultiCaptchaConfirm'), + }).then(({ canceled }) => { + if (canceled) { + return; + } + + this.enableRecaptcha = false; + }); + } + }, + enableRecaptcha() { + if (this.enableRecaptcha && this.enableHcaptcha) { + this.$root.dialog({ + type: 'question', // warning だと間違って cancel するかもしれない + showCancelButton: true, + title: this.$t('settingGuide'), + text: this.$t('avoidMultiCaptchaConfirm'), + }).then(({ canceled }) => { + if (canceled) { + return; + } + + this.enableHcaptcha = false; + }); + } + }, + })[key](); + }, + invite() { this.$root.api('admin/invite').then(x => { this.$root.dialog({ From 9a611b24f3f03dcfe841ff30dd7ae273eda905df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?= <root@acid-chicken.com> Date: Wed, 29 Apr 2020 09:35:37 +0900 Subject: [PATCH 07/12] Fix bug --- locales/ja-JP.yml | 2 +- src/client/pages/instance/settings.vue | 77 +++++++++++++------------- 2 files changed, 39 insertions(+), 40 deletions(-) diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 12b28019d..f22a76917 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -308,7 +308,7 @@ recaptcha: "reCAPTCHA" enableRecaptcha: "reCAPTCHAを有効にする" recaptchaSiteKey: "サイトキー" recaptchaSecretKey: "シークレットキー" -avoidMultiCaptchaConfirm: "単一のCaptchaのみの使用が推奨されます。他のCaptchaを無効にしますか?キャンセルして複数のCaptchaを有効化したままにすることも可能です。" +avoidMultiCaptchaConfirm: "複数のCaptchaを使用すると干渉を起こす可能性があります。他のCaptchaを無効にしますか?キャンセルして複数のCaptchaを有効化したままにすることも可能です。" antennas: "アンテナ" manageAntennas: "アンテナの管理" name: "名前" diff --git a/src/client/pages/instance/settings.vue b/src/client/pages/instance/settings.vue index f0fa84699..afd6d4cc6 100644 --- a/src/client/pages/instance/settings.vue +++ b/src/client/pages/instance/settings.vue @@ -41,7 +41,7 @@ <section class="_card"> <div class="_title"><fa :icon="faShieldAlt"/> {{ $t('hcaptcha') }}</div> <div class="_content"> - <mk-switch v-model="enableHcaptcha" @input="guide('enableHcaptcha')">{{ $t('enableHcaptcha') }}</mk-switch> + <mk-switch v-model="enableHcaptcha" ref="enableHcaptcha">{{ $t('enableHcaptcha') }}</mk-switch> <template v-if="enableHcaptcha"> <mk-input v-model="hcaptchaSiteKey" :disabled="!enableHcaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('hcaptchaSiteKey') }}</mk-input> <mk-input v-model="hcaptchaSecretKey" :disabled="!enableHcaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('hcaptchaSecretKey') }}</mk-input> @@ -59,7 +59,7 @@ <section class="_card"> <div class="_title"><fa :icon="faShieldAlt"/> {{ $t('recaptcha') }}</div> <div class="_content"> - <mk-switch v-model="enableRecaptcha" @input="guide('enableRecaptcha')">{{ $t('enableRecaptcha') }}</mk-switch> + <mk-switch v-model="enableRecaptcha" ref="enableRecaptcha">{{ $t('enableRecaptcha') }}</mk-switch> <template v-if="enableRecaptcha"> <mk-input v-model="recaptchaSiteKey" :disabled="!enableRecaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('recaptchaSiteKey') }}</mk-input> <mk-input v-model="recaptchaSecretKey" :disabled="!enableRecaptcha"><template #icon><fa :icon="faKey"/></template>{{ $t('recaptchaSecretKey') }}</mk-input> @@ -348,44 +348,43 @@ export default Vue.extend({ } }, + mounted() { + this.$refs.enableHcaptcha.$on('change', () => { + if (this.enableHcaptcha && this.enableRecaptcha) { + this.$root.dialog({ + type: 'question', // warning だと間違って cancel するかもしれない + showCancelButton: true, + title: this.$t('settingGuide'), + text: this.$t('avoidMultiCaptchaConfirm'), + }).then(({ canceled }) => { + if (canceled) { + return; + } + + this.enableRecaptcha = false; + }); + } + }); + + this.$refs.enableRecaptcha.$on('change', () => { + if (this.enableRecaptcha && this.enableHcaptcha) { + this.$root.dialog({ + type: 'question', // warning だと間違って cancel するかもしれない + showCancelButton: true, + title: this.$t('settingGuide'), + text: this.$t('avoidMultiCaptchaConfirm'), + }).then(({ canceled }) => { + if (canceled) { + return; + } + + this.enableHcaptcha = false; + }); + } + }); + }, + methods: { - guide(key: 'enableHcaptcha' | 'enableRecaptcha') { - ({ - enableHcaptcha() { - if (this.enableHcaptcha && this.enableRecaptcha) { - this.$root.dialog({ - type: 'question', // warning だと間違って cancel するかもしれない - showCancelButton: true, - title: this.$t('settingGuide'), - text: this.$t('avoidMultiCaptchaConfirm'), - }).then(({ canceled }) => { - if (canceled) { - return; - } - - this.enableRecaptcha = false; - }); - } - }, - enableRecaptcha() { - if (this.enableRecaptcha && this.enableHcaptcha) { - this.$root.dialog({ - type: 'question', // warning だと間違って cancel するかもしれない - showCancelButton: true, - title: this.$t('settingGuide'), - text: this.$t('avoidMultiCaptchaConfirm'), - }).then(({ canceled }) => { - if (canceled) { - return; - } - - this.enableHcaptcha = false; - }); - } - }, - })[key](); - }, - invite() { this.$root.api('admin/invite').then(x => { this.$root.dialog({ From ca6a70e5557ed8c99470133cde3755de5a2318fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?= <root@acid-chicken.com> Date: Wed, 29 Apr 2020 09:54:21 +0900 Subject: [PATCH 08/12] Update signup.vue --- src/client/components/signup.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client/components/signup.vue b/src/client/components/signup.vue index 7e8bac776..6452afc88 100644 --- a/src/client/components/signup.vue +++ b/src/client/components/signup.vue @@ -105,6 +105,7 @@ export default Vue.extend({ return this.submitting || this.meta.tosUrl && !this.ToSAgreement || this.meta.enableHcaptcha && !this.hCaptchaResponse || + this.meta.enableRecaptcha && !this.reCaptchaResponse || this.passwordRetypeState == 'not-match'; }, From 9053b9635e1b2f1daf233f644d6f8f3bb8411cc0 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Sat, 2 May 2020 10:28:45 +0900 Subject: [PATCH 09/12] refactor --- src/server/api/private/signup.ts | 33 +++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts index 04d0501b6..4e475e198 100644 --- a/src/server/api/private/signup.ts +++ b/src/server/api/private/signup.ts @@ -12,25 +12,28 @@ export default async (ctx: Koa.Context) => { // Verify *Captcha // ただしテスト時はこの機構は障害となるため無効にする - if (process.env.NODE_ENV !== 'test' && instance.enableHcaptcha && instance.hcaptchaSecretKey) { - const success = await verify(instance.hcaptchaSecretKey, body['hcaptcha-response']).then( - ({ success }) => success, - () => false, - ); + if (process.env.NODE_ENV !== 'test') { + if (instance.enableHcaptcha && instance.hcaptchaSecretKey) { + const success = await verify(instance.hcaptchaSecretKey, body['hcaptcha-response']).then( + ({ success }) => success, + () => false, + ); - if (!success) { - ctx.throw(400, 'hcaptcha-failed'); + if (!success) { + ctx.throw(400, 'hcaptcha-failed'); + } } - } - if (process.env.NODE_ENV !== 'test' && instance.enableRecaptcha && instance.recaptchaSecretKey) { - recaptcha.init({ - secret_key: instance.recaptchaSecretKey - }); - const success = await recaptcha(body['g-recaptcha-response']); + if (instance.enableRecaptcha && instance.recaptchaSecretKey) { + recaptcha.init({ + secret_key: instance.recaptchaSecretKey + }); - if (!success) { - ctx.throw(400, 'recaptcha-failed'); + const success = await recaptcha(body['g-recaptcha-response']); + + if (!success) { + ctx.throw(400, 'recaptcha-failed'); + } } } From b2c703c173ccd19439bf7ec38a9570fd2eb432e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?= <root@acid-chicken.com> Date: Sat, 2 May 2020 10:31:37 +0900 Subject: [PATCH 10/12] Update src/server/api/private/signup.ts --- src/server/api/private/signup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts index 4e475e198..6dc252ac4 100644 --- a/src/server/api/private/signup.ts +++ b/src/server/api/private/signup.ts @@ -13,7 +13,7 @@ export default async (ctx: Koa.Context) => { // Verify *Captcha // ただしテスト時はこの機構は障害となるため無効にする if (process.env.NODE_ENV !== 'test') { - if (instance.enableHcaptcha && instance.hcaptchaSecretKey) { + if (instance.enableHcaptcha && instance.hcaptchaSecretKey) { const success = await verify(instance.hcaptchaSecretKey, body['hcaptcha-response']).then( ({ success }) => success, () => false, From f57888eb4baeaa254d06e0169db2934fcef93122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?= <root@acid-chicken.com> Date: Sat, 2 May 2020 10:50:29 +0900 Subject: [PATCH 11/12] Update src/@types/hcaptcha.d.ts --- src/@types/hcaptcha.d.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/@types/hcaptcha.d.ts b/src/@types/hcaptcha.d.ts index ef3d44256..694c8388a 100644 --- a/src/@types/hcaptcha.d.ts +++ b/src/@types/hcaptcha.d.ts @@ -1,9 +1,11 @@ declare module 'hcaptcha' { - export function verify(secret: string, token: string): Promise<{ + interface IVerifyResponse { success: boolean; challenge_ts: string; hostname: string; credit?: boolean; 'error-codes'?: unknown[]; - }>; + } + + export function verify(secret: string, token: string): Promise<IVerifyResponse>; } From a3ff1bfda74a3f7b8fdffd90c64530fb28cf1f8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?= <root@acid-chicken.com> Date: Sat, 2 May 2020 10:51:12 +0900 Subject: [PATCH 12/12] Update hcaptcha.d.ts --- src/@types/hcaptcha.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/@types/hcaptcha.d.ts b/src/@types/hcaptcha.d.ts index 694c8388a..afed58756 100644 --- a/src/@types/hcaptcha.d.ts +++ b/src/@types/hcaptcha.d.ts @@ -1,11 +1,11 @@ declare module 'hcaptcha' { - interface IVerifyResponse { + interface IVerifyResponse { success: boolean; challenge_ts: string; hostname: string; credit?: boolean; 'error-codes'?: unknown[]; - } + } export function verify(secret: string, token: string): Promise<IVerifyResponse>; }