shack/src/users.js

149 lines
3.1 KiB
JavaScript

import config from 'config';
import util from 'util';
import crypto from 'crypto';
import bhttp from 'bhttp';
import initLogger from './logger';
import { HttpError } from './errors';
import knex from './knex';
const logger = initLogger();
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',
});
}
}
export {
createUser,
login,
};