diff --git a/.env.dist b/.env.dist index 080d7894..7dd99ded 100644 --- a/.env.dist +++ b/.env.dist @@ -24,3 +24,6 @@ AWS_REGION= AWS_KEY= AWS_SECRET= AWS_S3_BUCKET= + +HCAPTCHA_SITEKEY=10000000-ffff-ffff-ffff-000000000001 +HCAPTCHA_SECRET=0x0000000000000000000000000000000000000000 diff --git a/components/Captcha.vue b/components/Captcha.vue new file mode 100644 index 00000000..407ef15b --- /dev/null +++ b/components/Captcha.vue @@ -0,0 +1,35 @@ + + + diff --git a/components/Login.vue b/components/Login.vue index e5a3c4ec..fd7117fc 100644 --- a/components/Login.vue +++ b/components/Login.vue @@ -3,34 +3,45 @@
-
-

+

+

user.login.why

-
- - +
+
+
+ + +

+ + captcha.reason +

+ + +
-
- - - {{ providerOptions.name }} - +
+ +

+ + terms.consent +

+

+ + user.login.passwordless +

-

- - terms.consent -

-

- - user.login.passwordless -

- +
@@ -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 = ''; diff --git a/locale/de/translations.suml b/locale/de/translations.suml index 176c89c7..dc182ee2 100644 --- a/locale/de/translations.suml +++ b/locale/de/translations.suml @@ -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' diff --git a/locale/en/translations.suml b/locale/en/translations.suml index c130315e..57648e83 100644 --- a/locale/en/translations.suml +++ b/locale/en/translations.suml @@ -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' diff --git a/locale/es/translations.suml b/locale/es/translations.suml index f41466b5..c79dc61c 100644 --- a/locale/es/translations.suml +++ b/locale/es/translations.suml @@ -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' diff --git a/locale/fr/translations.suml b/locale/fr/translations.suml index ff3f4b97..4eb56816 100644 --- a/locale/fr/translations.suml +++ b/locale/fr/translations.suml @@ -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' diff --git a/locale/nl/translations.suml b/locale/nl/translations.suml index 3bcd1cf9..7b912dfc 100644 --- a/locale/nl/translations.suml +++ b/locale/nl/translations.suml @@ -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' diff --git a/locale/pl/translations.suml b/locale/pl/translations.suml index 0c471b36..e0009ea9 100644 --- a/locale/pl/translations.suml +++ b/locale/pl/translations.suml @@ -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' diff --git a/locale/pt/translations.suml b/locale/pt/translations.suml index 2209e4db..fef7c1fc 100644 --- a/locale/pt/translations.suml +++ b/locale/pt/translations.suml @@ -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' diff --git a/locale/ru/translations.suml b/locale/ru/translations.suml index aa8c43c9..b3d828fe 100644 --- a/locale/ru/translations.suml +++ b/locale/ru/translations.suml @@ -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' diff --git a/locale/yi/translations.suml b/locale/yi/translations.suml index d791e089..2840be55 100644 --- a/locale/yi/translations.suml +++ b/locale/yi/translations.suml @@ -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: 'ליכטמאָדוס' diff --git a/locale/zh/translations.suml b/locale/zh/translations.suml index 1f73a5c2..64ff5dd6 100644 --- a/locale/zh/translations.suml +++ b/locale/zh/translations.suml @@ -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: '明亮模式' diff --git a/nuxt.config.js b/nuxt.config.js index 4267cc99..38c6af60 100644 --- a/nuxt.config.js +++ b/nuxt.config.js @@ -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: { diff --git a/package.json b/package.json index 6d3f5b44..99815063 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/server/captcha.js b/server/captcha.js new file mode 100644 index 00000000..3b172e89 --- /dev/null +++ b/server/captcha.js @@ -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']; +} diff --git a/server/routes/user.js b/server/routes/user.js index 6a1ef0a0..03b644b6 100644 --- a/server/routes/user.js +++ b/server/routes/user.js @@ -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;