shack/src/users.js

148 lines
3.1 KiB
JavaScript

const config = require('config');
const util = require('util');
const crypto = require('crypto');
const bhttp = require('bhttp');
const logger = require('./logger')(__filename);
const { HttpError } = require('./errors');
const knex = require('./knex');
const scrypt = util.promisify(crypto.scrypt);
function curateDatabaseUser(user) {
return {
id: user.id,
username: user.username,
};
}
async function hashPassword(password) {
const salt = crypto.randomBytes(16).toString('hex');
const hash = await scrypt(password, salt, 64);
return `${salt}/${hash.toString('hex')}`;
}
async function verifyPassword(password, storedPassword) {
const [salt, hash] = storedPassword.split('/');
const hashedPassword = await scrypt(password, salt, 64);
if (hashedPassword.toString('hex') !== hash) {
throw new HttpError({
statusCode: 400,
statusMessage: 'Username or password incorrect',
});
}
}
async function verifyCaptcha(captchaToken) {
if (!captchaToken) {
throw new HttpError({
statusCode: 400,
statusMessage: 'No CAPTCHA provided',
});
}
const res = await bhttp.post('https://hcaptcha.com/siteverify', {
response: captchaToken,
secret: config.auth.captcha.secret,
});
if (res.statusCode !== 200 || !res.body.success) {
throw new HttpError({
statusCode: 498,
statusMessage: 'Invalid CAPTCHA',
});
}
}
async function login(credentials) {
if (!credentials.username) {
throw new HttpError({
statusCode: 400,
statusMessage: 'You must provide a username',
});
}
if (!credentials.password) {
throw new HttpError({
statusCode: 400,
statusMessage: 'You must provide a password',
});
}
const user = await knex('users')
.where('username', credentials.username)
.orWhere('email', credentials.username)
.first();
if (!user) {
throw new HttpError({
statusCode: 400,
statusMessage: 'Username or password incorrect',
});
}
await verifyPassword(credentials.password, user.password);
return curateDatabaseUser(user);
}
async function createUser(credentials, context) {
if (!credentials.username) {
throw new HttpError({
statusCode: 400,
statusMessage: 'You must provide a username',
});
}
if (!credentials.password) {
throw new HttpError({
statusCode: 400,
statusMessage: 'You must provide a password',
});
}
if (config.auth.captcha.enabled) {
await verifyCaptcha(credentials.captchaToken);
}
const hashedPassword = await hashPassword(credentials.password);
try {
const [userEntry] = await knex('users')
.insert({
username: credentials.username,
email: credentials.email,
password: hashedPassword,
ip: context.ip,
})
.returning('*');
const user = curateDatabaseUser(userEntry);
logger.info(`Registered user ${user.username} (${user.id}, ${user.email}, ${userEntry.ip})`);
return user;
} catch (error) {
if (error.code === '23505') {
throw new HttpError({
statusCode: 409,
statusMessage: 'Username or e-mail already registered',
});
}
logger.error(error.message);
throw new HttpError({
statusCode: 500,
statusMessage: 'Sign-up failed',
});
}
}
module.exports = {
createUser,
login,
};