#241 [sec] add captcha to login
This commit is contained in:
parent
5f3e89a83c
commit
b96ed0c347
|
@ -24,3 +24,6 @@ AWS_REGION=
|
|||
AWS_KEY=
|
||||
AWS_SECRET=
|
||||
AWS_S3_BUCKET=
|
||||
|
||||
HCAPTCHA_SITEKEY=10000000-ffff-ffff-ffff-000000000001
|
||||
HCAPTCHA_SECRET=0x0000000000000000000000000000000000000000
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
<template>
|
||||
<div class="h-captcha"
|
||||
:data-theme="isDark ? 'dark' : 'light'"
|
||||
:data-sitekey="siteKey"
|
||||
data-callback="hCaptchaDone"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import dark from "../plugins/dark";
|
||||
|
||||
export default {
|
||||
mixins: [dark],
|
||||
props: {
|
||||
value: {},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
siteKey: process.env.HCAPTCHA_SITEKEY,
|
||||
isDark: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (!process.client) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.isDark = this.detectDark();
|
||||
this.$loadScript('hcaptcha', 'https://js.hcaptcha.com/1/api.js');
|
||||
window.hCaptchaDone = (token) => {
|
||||
this.$emit('input', token);
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -3,34 +3,45 @@
|
|||
<Alert type="danger" :message="error"/>
|
||||
|
||||
<div v-if="token === null">
|
||||
<form @submit.prevent="login" :disabled="saving">
|
||||
<p>
|
||||
<div class="alert alert-info mb-3">
|
||||
<p class="mb-0">
|
||||
<Icon v="info-circle"/>
|
||||
<T>user.login.why</T>
|
||||
</p>
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-control" v-model="usernameOrEmail"
|
||||
:placeholder="$t('user.login.placeholder')" autofocus required/>
|
||||
<button class="btn btn-primary">
|
||||
<Icon v="sign-in"/>
|
||||
<T>user.login.action</T>
|
||||
</button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-8">
|
||||
<form @submit.prevent="login" :disabled="saving">
|
||||
<input type="text" class="form-control mb-3" v-model="usernameOrEmail"
|
||||
:placeholder="$t('user.login.placeholder')" autofocus required/>
|
||||
<p class="small text-muted mb-1">
|
||||
<Icon v="info-circle"/>
|
||||
<T>captcha.reason</T>
|
||||
</p>
|
||||
<Captcha class="h-captcha" v-model="captchaToken"/>
|
||||
<button class="btn btn-primary mt-3" :disabled="!canInit">
|
||||
<Icon v="sign-in"/>
|
||||
<T>user.login.action</T>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="btn-group w-100 mb-3">
|
||||
<a :href="`/api/connect/${provider}`" v-for="(providerOptions, provider) in socialProviders" class="btn btn-outline-primary">
|
||||
<Icon :v="providerOptions.icon || provider" set="b"/>
|
||||
{{ providerOptions.name }}
|
||||
</a>
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="btn-group-vertical w-100 mb-3">
|
||||
<a :href="`/api/connect/${provider}`" v-for="(providerOptions, provider) in socialProviders" class="btn btn-outline-primary">
|
||||
<Icon :v="providerOptions.icon || provider" set="b"/>
|
||||
{{ providerOptions.name }}
|
||||
</a>
|
||||
</div>
|
||||
<p class="small text-muted">
|
||||
<Icon v="gavel"/>
|
||||
<T>terms.consent</T>
|
||||
</p>
|
||||
<p class="small text-muted">
|
||||
<Icon v="lock"/>
|
||||
<T>user.login.passwordless</T>
|
||||
</p>
|
||||
</div>
|
||||
<p class="small text-muted">
|
||||
<Icon v="gavel"/>
|
||||
<T>terms.consent</T>
|
||||
</p>
|
||||
<p class="small text-muted">
|
||||
<Icon v="lock"/>
|
||||
<T>user.login.passwordless</T>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="payload && !payload.code">
|
||||
<div class="alert alert-success">
|
||||
|
@ -74,6 +85,8 @@
|
|||
socialProviders,
|
||||
|
||||
saving: false,
|
||||
|
||||
captchaToken: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -90,7 +103,10 @@
|
|||
audience: this.$base,
|
||||
issuer: this.$base,
|
||||
});
|
||||
}
|
||||
},
|
||||
canInit() {
|
||||
return this.usernameOrEmail && this.captchaToken;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async login() {
|
||||
|
@ -98,24 +114,31 @@
|
|||
return;
|
||||
}
|
||||
this.saving = true;
|
||||
await this.post(`/user/init`, {
|
||||
usernameOrEmail: this.usernameOrEmail
|
||||
});
|
||||
this.saving = false;
|
||||
try {
|
||||
await this.post(`/user/init`, {
|
||||
usernameOrEmail: this.usernameOrEmail,
|
||||
captchaToken: this.captchaToken,
|
||||
});
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
async validate() {
|
||||
if (this.saving) {
|
||||
return;
|
||||
}
|
||||
this.saving = true;
|
||||
await this.post(`/user/validate`, {
|
||||
code: this.code
|
||||
}, {
|
||||
headers: {
|
||||
authorization: 'Bearer ' + this.token,
|
||||
},
|
||||
});
|
||||
this.saving = false;
|
||||
try {
|
||||
await this.post(`/user/validate`, {
|
||||
code: this.code
|
||||
}, {
|
||||
headers: {
|
||||
authorization: 'Bearer ' + this.token,
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
async post(url, data, options = {}) {
|
||||
this.error = '';
|
||||
|
|
|
@ -526,6 +526,11 @@ images:
|
|||
error:
|
||||
generic: 'Etwas ist schief gelaufen. Bitte versuche es erneut…'
|
||||
|
||||
# TODO
|
||||
captcha:
|
||||
reason: 'Please prove you''re not a bot to mitigate spam and DDoS attacks.'
|
||||
invalid: 'Invalid CAPTCHA, please try again'
|
||||
|
||||
mode:
|
||||
dark: 'Dunkelmodus'
|
||||
light: 'Lichtmodus'
|
||||
|
|
|
@ -611,6 +611,10 @@ images:
|
|||
error:
|
||||
generic: 'Something went wrong, please try again…'
|
||||
|
||||
captcha:
|
||||
reason: 'Please prove you''re not a bot to mitigate spam and DDoS attacks.'
|
||||
invalid: 'Invalid CAPTCHA, please try again'
|
||||
|
||||
mode:
|
||||
dark: 'Dark mode'
|
||||
light: 'Light mode'
|
||||
|
|
|
@ -538,6 +538,11 @@ images:
|
|||
error:
|
||||
generic: 'Algo salió mal. Por favor, vuelve a intentarlo…'
|
||||
|
||||
# TODO
|
||||
captcha:
|
||||
reason: 'Please prove you''re not a bot to mitigate spam and DDoS attacks.'
|
||||
invalid: 'Invalid CAPTCHA, please try again'
|
||||
|
||||
mode:
|
||||
dark: 'Modo oscuro'
|
||||
light: 'Modo claro'
|
||||
|
|
|
@ -531,6 +531,11 @@ images:
|
|||
error:
|
||||
generic: 'Quelque chose s’est mal passé, réessayez s’il vous plaît…'
|
||||
|
||||
# TODO
|
||||
captcha:
|
||||
reason: 'Please prove you''re not a bot to mitigate spam and DDoS attacks.'
|
||||
invalid: 'Invalid CAPTCHA, please try again'
|
||||
|
||||
mode:
|
||||
dark: 'Mode sombre'
|
||||
light: 'Mode clair'
|
||||
|
|
|
@ -521,6 +521,11 @@ images:
|
|||
error:
|
||||
generic: 'Er is iets misgegaan, probeer het opnieuw…'
|
||||
|
||||
# TODO
|
||||
captcha:
|
||||
reason: 'Please prove you''re not a bot to mitigate spam and DDoS attacks.'
|
||||
invalid: 'Invalid CAPTCHA, please try again'
|
||||
|
||||
mode:
|
||||
dark: 'Donkere modus'
|
||||
light: 'Lichte modus'
|
||||
|
|
|
@ -1149,6 +1149,10 @@ images:
|
|||
error:
|
||||
generic: 'Coś poszło nie tak, spróbuj ponownie…'
|
||||
|
||||
captcha:
|
||||
reason: 'Prosimy o udowodnienie, że nie jesteś botem, by chronić się przed spamem i DDoS-ami'
|
||||
invalid: 'Nieprawidłowa CAPTCHA, spróbuj ponownie'
|
||||
|
||||
mode:
|
||||
dark: 'Tryb nocny'
|
||||
light: 'Tryb dzienny'
|
||||
|
|
|
@ -536,6 +536,11 @@ images:
|
|||
error:
|
||||
generic: 'Alguma coisa deu errado, tente novamente'
|
||||
|
||||
# TODO
|
||||
captcha:
|
||||
reason: 'Please prove you''re not a bot to mitigate spam and DDoS attacks.'
|
||||
invalid: 'Invalid CAPTCHA, please try again'
|
||||
|
||||
mode:
|
||||
dark: 'Modo escuro'
|
||||
light: 'Modo claro'
|
||||
|
|
|
@ -1095,6 +1095,11 @@ images:
|
|||
error:
|
||||
generic: 'Coś poszło nie tak, spróbuj ponownie…'
|
||||
|
||||
# TODO
|
||||
captcha:
|
||||
reason: 'Please prove you''re not a bot to mitigate spam and DDoS attacks.'
|
||||
invalid: 'Invalid CAPTCHA, please try again'
|
||||
|
||||
mode:
|
||||
dark: 'Dark mode' # TODO
|
||||
light: 'Light mode'
|
||||
|
|
|
@ -539,6 +539,11 @@ images:
|
|||
error:
|
||||
generic: 'Something went wrong, please try again…'
|
||||
|
||||
# TODO
|
||||
captcha:
|
||||
reason: 'Please prove you''re not a bot to mitigate spam and DDoS attacks.'
|
||||
invalid: 'Invalid CAPTCHA, please try again'
|
||||
|
||||
mode:
|
||||
dark: 'טונקלמאָדוס'
|
||||
light: 'ליכטמאָדוס'
|
||||
|
|
|
@ -506,6 +506,11 @@ images:
|
|||
error:
|
||||
generic: '出了點問題,請重試...'
|
||||
|
||||
# TODO
|
||||
captcha:
|
||||
reason: 'Please prove you''re not a bot to mitigate spam and DDoS attacks.'
|
||||
invalid: 'Invalid CAPTCHA, please try again'
|
||||
|
||||
mode:
|
||||
dark: '黑暗模式'
|
||||
light: '明亮模式'
|
||||
|
|
|
@ -153,6 +153,7 @@ export default {
|
|||
FLAGS: buildFlags(),
|
||||
BUCKET: `https://${process.env.AWS_S3_BUCKET}.s3-${process.env.AWS_REGION}.amazonaws.com`,
|
||||
STATS_FILE: process.env.STATS_FILE,
|
||||
HCAPTCHA_SITEKEY: process.env.HCAPTCHA_SITEKEY,
|
||||
},
|
||||
serverMiddleware: ['~/server/no-ssr.js', '~/server/index.js'],
|
||||
axios: {
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
"mailer": "^0.6.7",
|
||||
"markdown-loader": "^6.0.0",
|
||||
"multer": "^1.4.2",
|
||||
"node-fetch": "^2.6.1",
|
||||
"nuxt": "^2.15.2",
|
||||
"pageres": "^6.2.3",
|
||||
"rtlcss": "^3.1.2",
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import fetch from 'node-fetch';
|
||||
|
||||
export const validateCaptcha = async (token) => {
|
||||
const res = await fetch('https://hcaptcha.com/siteverify', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `response=${encodeURIComponent(token)}&secret=${encodeURIComponent(process.env.HCAPTCHA_SECRET)}`
|
||||
});
|
||||
const body = await res.json();
|
||||
return body['success'];
|
||||
}
|
|
@ -8,6 +8,7 @@ import { loadSuml } from '../loader';
|
|||
import avatar from '../avatar';
|
||||
import { config as socialLoginConfig, handlers as socialLoginHandlers } from '../social';
|
||||
import cookieSettings from "../../src/cookieSettings";
|
||||
import {validateCaptcha} from "../captcha";
|
||||
|
||||
const config = loadSuml('config');
|
||||
const translations = loadSuml('translations');
|
||||
|
@ -199,6 +200,10 @@ router.post('/user/init', handleErrorAsync(async (req, res) => {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!await validateCaptcha(req.body.captchaToken)) {
|
||||
return res.json({error: 'captcha.invalid'});
|
||||
}
|
||||
|
||||
let user = undefined;
|
||||
let usernameOrEmail = req.body.usernameOrEmail;
|
||||
|
||||
|
|
Reference in New Issue