traxxx-web/src/auth.js

125 lines
3.2 KiB
JavaScript
Executable File

import config from 'config';
import util from 'util';
import crypto from 'crypto';
import fs from 'fs/promises';
import { createAvatar } from '@dicebear/core';
import { shapes } from '@dicebear/collection';
import knex from './knex.js';
import { curateUser, fetchUser } from './users.js';
import { HttpError } from './errors.js';
const scrypt = util.promisify(crypto.scrypt);
async function verifyPassword(password, storedPassword) {
const [salt, hash] = storedPassword.split('/');
const hashedPassword = (await scrypt(password, salt, 64)).toString('hex');
if (hashedPassword === hash) {
return true;
}
throw new HttpError('Username or password incorrect', 401);
}
async function generateAvatar(user) {
const avatar = createAvatar(shapes, {
seed: user.username,
backgroundColor: ['f65596', '9b004b', '006b68', '5abab6'],
shape1Color: ['f65596', 'ff6d7e', 'ff8d69', 'ffb15b', 'ffd55c', 'f9f871'],
shape2Color: ['c162c6', '6074dd', '007dd2', '007ba9', '007170'],
shape3Color: ['f65596', 'ff6d7e', 'ff8d69', 'ffb15b', 'ffd55c', 'f9f871'],
});
await fs.mkdir('media/avatars', { recursive: true });
await avatar.png().toFile(`media/avatars/${user.id}_${user.username}.png`);
}
export async function login(credentials) {
if (!config.auth.login) {
throw new HttpError('Authentication is disabled', 405);
}
const user = await fetchUser(credentials.username.trim(), true);
if (!user) {
throw new HttpError('Username or password incorrect', 401);
}
await verifyPassword(credentials.password, user.password);
await knex('users')
.update('last_login', 'NOW()')
.where('id', user.id);
if (!user.avatar) {
await generateAvatar(user);
}
return curateUser(user);
}
export async function signup(credentials) {
if (!config.auth.signup) {
throw new HttpError('Authentication is disabled', 405);
}
const curatedUsername = credentials.username.trim();
if (!curatedUsername) {
throw new HttpError('Username required', 400);
}
if (curatedUsername.length < config.auth.usernameLength[0]) {
throw new HttpError('Username is too short', 400);
}
if (curatedUsername.length > config.auth.usernameLength[1]) {
throw new HttpError('Username is too long', 400);
}
if (!config.auth.usernamePattern.test(curatedUsername)) {
throw new HttpError('Username contains invalid characters', 400);
}
if (!credentials.email) {
throw new HttpError('E-mail required', 400);
}
if (credentials.password?.length < 3) {
throw new HttpError('Password must be 3 characters or longer', 400);
}
const existingUser = await knex('users')
.where('username', curatedUsername)
.orWhere('email', credentials.email)
.first();
if (existingUser) {
throw new HttpError('Username or e-mail already in use', 409);
}
const salt = crypto.randomBytes(16).toString('hex');
const hashedPassword = (await scrypt(credentials.password, salt, 64)).toString('hex');
const storedPassword = `${salt}/${hashedPassword}`;
const [{ id: userId }] = await knex('users')
.insert({
username: curatedUsername,
email: credentials.email,
password: storedPassword,
})
.returning('id');
await knex('stashes').insert({
user_id: userId,
name: 'Favorites',
slug: 'favorites',
public: false,
primary: true,
});
return fetchUser(userId);
}