Integrated hCaptcha.
This commit is contained in:
parent
9933b4fbf0
commit
b7bd0fac03
|
|
@ -63,6 +63,11 @@ module.exports = {
|
||||||
signup: true,
|
signup: true,
|
||||||
usernameLength: [2, 24],
|
usernameLength: [2, 24],
|
||||||
usernamePattern: /^[a-zA-Z0-9_-]+$/,
|
usernamePattern: /^[a-zA-Z0-9_-]+$/,
|
||||||
|
captcha: {
|
||||||
|
enabled: false,
|
||||||
|
siteKey: '10000000-ffff-ffff-ffff-000000000001',
|
||||||
|
secretKey: '0x0000000000000000000000000000000000000000',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
bans: {
|
bans: {
|
||||||
defaultExpiry: 60 * 24 * 3, // in minutes, 3 days
|
defaultExpiry: 60 * 24 * 3, // in minutes, 3 days
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
"@faker-js/faker": "^8.4.1",
|
"@faker-js/faker": "^8.4.1",
|
||||||
"@floating-ui/dom": "^1.5.3",
|
"@floating-ui/dom": "^1.5.3",
|
||||||
"@floating-ui/vue": "^1.0.2",
|
"@floating-ui/vue": "^1.0.2",
|
||||||
|
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
|
||||||
"@resvg/resvg-js": "^2.6.0",
|
"@resvg/resvg-js": "^2.6.0",
|
||||||
"@toycode/markdown-it-class": "^1.2.4",
|
"@toycode/markdown-it-class": "^1.2.4",
|
||||||
"@vitejs/plugin-vue": "^4.5.2",
|
"@vitejs/plugin-vue": "^4.5.2",
|
||||||
|
|
@ -38,6 +39,7 @@
|
||||||
"graphql": "^16.9.0",
|
"graphql": "^16.9.0",
|
||||||
"graphql-parse-resolve-info": "^4.13.0",
|
"graphql-parse-resolve-info": "^4.13.0",
|
||||||
"graphql-scalars": "^1.24.2",
|
"graphql-scalars": "^1.24.2",
|
||||||
|
"hcaptcha": "^0.2.0",
|
||||||
"ip-cidr": "^4.0.0",
|
"ip-cidr": "^4.0.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"knex": "^3.1.0",
|
"knex": "^3.1.0",
|
||||||
|
|
@ -3023,6 +3025,18 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@hcaptcha/vue3-hcaptcha": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@hcaptcha/vue3-hcaptcha/-/vue3-hcaptcha-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-IEonS6JiYdU7uy6aeib8cYtMO4nj8utwStbA9bWHyYbOvOvhpkV+AW8vfSKh6SntYxqle/TRwhv+kU9p92CfsA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"vue": "^3.2.19"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanwhocodes/config-array": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.11.13",
|
"version": "0.11.13",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
|
||||||
|
|
@ -7319,6 +7333,12 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hcaptcha": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/hcaptcha/-/hcaptcha-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-x25z3RoEa9oqfyuQsgk2olc+LCNVDAJaGKUP1qFhpAybB6qjqOf4qB2y1E3LJpXDvM229JWEywc6iWnzWvGjNw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/html-encoding-sniffer": {
|
"node_modules/html-encoding-sniffer": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
|
||||||
|
|
@ -13761,6 +13781,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@hcaptcha/vue3-hcaptcha": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@hcaptcha/vue3-hcaptcha/-/vue3-hcaptcha-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-IEonS6JiYdU7uy6aeib8cYtMO4nj8utwStbA9bWHyYbOvOvhpkV+AW8vfSKh6SntYxqle/TRwhv+kU9p92CfsA==",
|
||||||
|
"requires": {
|
||||||
|
"vue": "^3.2.19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@humanwhocodes/config-array": {
|
"@humanwhocodes/config-array": {
|
||||||
"version": "0.11.13",
|
"version": "0.11.13",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
|
||||||
|
|
@ -16771,6 +16799,11 @@
|
||||||
"function-bind": "^1.1.2"
|
"function-bind": "^1.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"hcaptcha": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/hcaptcha/-/hcaptcha-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-x25z3RoEa9oqfyuQsgk2olc+LCNVDAJaGKUP1qFhpAybB6qjqOf4qB2y1E3LJpXDvM229JWEywc6iWnzWvGjNw=="
|
||||||
|
},
|
||||||
"html-encoding-sniffer": {
|
"html-encoding-sniffer": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
"@faker-js/faker": "^8.4.1",
|
"@faker-js/faker": "^8.4.1",
|
||||||
"@floating-ui/dom": "^1.5.3",
|
"@floating-ui/dom": "^1.5.3",
|
||||||
"@floating-ui/vue": "^1.0.2",
|
"@floating-ui/vue": "^1.0.2",
|
||||||
|
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
|
||||||
"@resvg/resvg-js": "^2.6.0",
|
"@resvg/resvg-js": "^2.6.0",
|
||||||
"@toycode/markdown-it-class": "^1.2.4",
|
"@toycode/markdown-it-class": "^1.2.4",
|
||||||
"@vitejs/plugin-vue": "^4.5.2",
|
"@vitejs/plugin-vue": "^4.5.2",
|
||||||
|
|
@ -38,6 +39,7 @@
|
||||||
"graphql": "^16.9.0",
|
"graphql": "^16.9.0",
|
||||||
"graphql-parse-resolve-info": "^4.13.0",
|
"graphql-parse-resolve-info": "^4.13.0",
|
||||||
"graphql-scalars": "^1.24.2",
|
"graphql-scalars": "^1.24.2",
|
||||||
|
"hcaptcha": "^0.2.0",
|
||||||
"ip-cidr": "^4.0.0",
|
"ip-cidr": "^4.0.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"knex": "^3.1.0",
|
"knex": "^3.1.0",
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<VueHCaptcha
|
||||||
|
v-if="env.captcha.enabled"
|
||||||
|
:sitekey="env.captcha.siteKey"
|
||||||
|
class="captcha"
|
||||||
|
@verify="(verification) => captcha = verification"
|
||||||
|
@expired="captcha = null"
|
||||||
|
/>
|
||||||
|
|
||||||
<button class="button button-submit">Sign up</button>
|
<button class="button button-submit">Sign up</button>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
|
|
@ -124,12 +132,13 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, inject } from 'vue';
|
import { ref, onMounted, inject } from 'vue';
|
||||||
|
import VueHCaptcha from '@hcaptcha/vue3-hcaptcha';
|
||||||
|
|
||||||
import { post } from '#/src/api.js';
|
import { post } from '#/src/api.js';
|
||||||
import navigate from '#/src/navigate.js';
|
import navigate from '#/src/navigate.js';
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
const pageContext = inject('pageContext');
|
||||||
const user = pageContext.user;
|
const { user, env } = pageContext;
|
||||||
|
|
||||||
const username = ref('');
|
const username = ref('');
|
||||||
const email = ref('');
|
const email = ref('');
|
||||||
|
|
@ -139,6 +148,7 @@ const passwordConfirm = ref('');
|
||||||
const errorMsg = ref(null);
|
const errorMsg = ref(null);
|
||||||
const userInput = ref(null);
|
const userInput = ref(null);
|
||||||
const showPassword = ref(false);
|
const showPassword = ref(false);
|
||||||
|
const captcha = ref(null);
|
||||||
|
|
||||||
async function signup() {
|
async function signup() {
|
||||||
errorMsg.value = null;
|
errorMsg.value = null;
|
||||||
|
|
@ -148,12 +158,20 @@ async function signup() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
if (env.captcha.enabled && !captcha.value) {
|
||||||
|
errorMsg.value = 'Please complete the CAPTCHA';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newUser = await post('/users', {
|
const newUser = await post('/users', {
|
||||||
username: username.value,
|
username: username.value,
|
||||||
email: email.value,
|
email: email.value,
|
||||||
password: password.value,
|
password: password.value,
|
||||||
redirect: pageContext.urlParsed.search.r,
|
redirect: pageContext.urlParsed.search.r,
|
||||||
|
captcha: captcha.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
navigate(`/user/${newUser.username}`, null, { redirect: true });
|
navigate(`/user/${newUser.username}`, null, { redirect: true });
|
||||||
|
|
@ -229,6 +247,16 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.captcha {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-submit {
|
||||||
|
margin-top: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
background: var(--error);
|
background: var(--error);
|
||||||
color: var(--text-light);
|
color: var(--text-light);
|
||||||
|
|
|
||||||
12
src/auth.js
12
src/auth.js
|
|
@ -5,6 +5,7 @@ import fs from 'fs/promises';
|
||||||
import { createAvatar } from '@dicebear/core';
|
import { createAvatar } from '@dicebear/core';
|
||||||
import { shapes } from '@dicebear/collection';
|
import { shapes } from '@dicebear/collection';
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
|
import { verify } from 'hcaptcha';
|
||||||
|
|
||||||
import { knexOwner as knex } from './knex.js';
|
import { knexOwner as knex } from './knex.js';
|
||||||
import redis from './redis.js';
|
import redis from './redis.js';
|
||||||
|
|
@ -105,6 +106,15 @@ export async function signup(credentials, userIp) {
|
||||||
throw new HttpError('Password must be 3 characters or longer', 400);
|
throw new HttpError('Password must be 3 characters or longer', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.auth.captcha.enabled) {
|
||||||
|
const captchaVerification = await verify(config.auth.captcha.secretKey, credentials.captcha);
|
||||||
|
|
||||||
|
if (!captchaVerification.success) {
|
||||||
|
logger.warn(`Invalid sign-up CAPTCHA from '${curatedUsername}' (${credentials.email}, ${userIp})`);
|
||||||
|
throw new HttpError('Invalid CAPTCHA', 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const existingUser = await knex('users')
|
const existingUser = await knex('users')
|
||||||
.where(knex.raw('lower(username)'), curatedUsername.toLowerCase())
|
.where(knex.raw('lower(username)'), curatedUsername.toLowerCase())
|
||||||
.orWhere(knex.raw('lower(email)'), credentials.email.toLowerCase())
|
.orWhere(knex.raw('lower(email)'), credentials.email.toLowerCase())
|
||||||
|
|
@ -134,7 +144,7 @@ export async function signup(credentials, userIp) {
|
||||||
primary: true,
|
primary: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.verbose(`Signup from '${curatedUsername}' (${userId}, ${credentials.email}, ${userIp})`);
|
logger.info(`Signup from '${curatedUsername}' (${userId}, ${credentials.email}, ${userIp})`);
|
||||||
|
|
||||||
await generateAvatar({
|
await generateAvatar({
|
||||||
id: userId,
|
id: userId,
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,10 @@ export default async function mainHandler(req, res, next) {
|
||||||
psa: config.psa,
|
psa: config.psa,
|
||||||
links: config.links,
|
links: config.links,
|
||||||
socials,
|
socials,
|
||||||
|
captcha: {
|
||||||
|
enabled: config.auth.captcha.enabled,
|
||||||
|
siteKey: config.auth.captcha.siteKey,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
now: new Date().toISOString(),
|
now: new Date().toISOString(),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue