diff options
| author | Acid Chicken (硫酸鶏) <root@acid-chicken.com> | 2020-04-28 14:29:33 +0900 |
|---|---|---|
| committer | Acid Chicken (硫酸鶏) <root@acid-chicken.com> | 2020-04-28 14:29:33 +0900 |
| commit | 78608392208cb73351354cda5678daee232159d8 (patch) | |
| tree | a20fadeac4df4ddbd1773f2ced1c513c5ba74994 /src | |
| parent | Merge pull request #6302 from syuilo/patch/autogen/v11 (diff) | |
| download | misskey-78608392208cb73351354cda5678daee232159d8.tar.gz misskey-78608392208cb73351354cda5678daee232159d8.tar.bz2 misskey-78608392208cb73351354cda5678daee232159d8.zip | |
Add support for hCaptcha
Diffstat (limited to 'src')
| -rw-r--r-- | src/@types/hcaptcha.d.ts | 9 | ||||
| -rw-r--r-- | src/client/components/hcaptcha.vue | 76 | ||||
| -rw-r--r-- | src/client/components/signup.vue | 24 | ||||
| -rw-r--r-- | src/client/components/url-preview.vue | 2 | ||||
| -rw-r--r-- | src/client/pages/instance/settings.vue | 67 | ||||
| -rw-r--r-- | src/models/entities/meta.ts | 17 | ||||
| -rw-r--r-- | src/server/api/endpoints/admin/update-meta.ts | 33 | ||||
| -rw-r--r-- | src/server/api/endpoints/meta.ts | 4 | ||||
| -rw-r--r-- | src/server/api/private/signup.ts | 13 | ||||
| -rw-r--r-- | src/server/nodeinfo.ts | 1 | ||||
| -rw-r--r-- | src/server/web/views/info.pug | 3 |
11 files changed, 229 insertions, 20 deletions
diff --git a/src/@types/hcaptcha.d.ts b/src/@types/hcaptcha.d.ts new file mode 100644 index 0000000000..ef3d44256c --- /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 0000000000..e54eb314a3 --- /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 9f3ae8db28..5d16a82bac 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 94d07cbaed..c2dd0038be 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 f7db4aa10b..088db8763e 100644 --- a/src/client/pages/instance/settings.vue +++ b/src/client/pages/instance/settings.vue @@ -39,6 +39,24 @@ </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"> <mk-switch v-model="enableRecaptcha">{{ $t('enableRecaptcha') }}</mk-switch> @@ -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 bb463c52f4..622d28bead 100644 --- a/src/models/entities/meta.ts +++ b/src/models/entities/meta.ts @@ -127,6 +127,23 @@ export class Meta { @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, + }) public enableRecaptcha: boolean; @Column('varchar', { diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts index 1bc20029ef..dffe7ffe7f 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 179355489b..eefc370124 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 79ee74389c..e23fe43583 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 2ff924e68d..442e946df3 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 992e652a60..4553d2e2b9 100644 --- a/src/server/web/views/info.pug +++ b/src/server/web/views/info.pug @@ -107,6 +107,9 @@ html 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' tr |