Readlebee/server/controllers/account.js

235 lines
5.9 KiB
JavaScript

const crypto = require('crypto');
class AccountController {
constructor (userModel) {
this.model = userModel;
}
// hashPassword and validatePassword modified from https://stackoverflow.com/a/17201493
static hashPassword (password) {
var salt = crypto.randomBytes(128).toString('base64');
var iterations = 10000;
var hash = crypto.pbkdf2Sync(password, salt, iterations, 128, 'sha512').toString('base64');
return {
salt,
hash,
iterations,
};
}
static verifyPassword (savedHash, savedSalt, passwordAttempt) {
const attemptedHash = crypto.pbkdf2Sync(passwordAttempt, savedSalt, 10000, 128, 'sha512').toString('base64');
return savedHash == attemptedHash;
}
static createAccountDataIsValid (createAccountData) {
if (typeof createAccountData.email === 'undefined'
|| typeof createAccountData.username === 'undefined'
|| typeof createAccountData.password === 'undefined'
|| typeof createAccountData.permissionLevel === 'undefined'
|| createAccountData.password === '') {
return {
error: true,
message: 'api.account.create.required_data_missing',
};
}
if (createAccountData.email.length < 5 || !/^.+@.+\..+$/.test(createAccountData.email)) {
return {
error: true,
message: 'api.account.create.invalid_email',
};
}
if (createAccountData.username.length < 2 || !/^[a-z0-9_]+$/i.test(createAccountData.username)) {
return {
error: true,
message: 'api.account.create.invalid_username',
};
}
if (![100, 33, 0].includes(createAccountData.permissionLevel)) {
return {
error: true,
message: 'api.account.create.invalid_permissionLevel',
};
}
return true;
}
static loginDataIsValid (loginData) {
if (typeof loginData.email === 'undefined'
|| typeof loginData.password === 'undefined'
|| loginData.password === '') {
return {
error: true,
message: 'api.account.login.required_data_missing',
};
}
if (loginData.email.length < 5 || !/^.+@.+\..+$/.test(loginData.email)) {
return {
error: true,
message: 'api.account.login.invalid_email',
};
}
return true;
}
static cleanCreateAccountFormData (formData) {
var displayName = typeof formData.displayName !== 'undefined' ? formData.displayName.toString().trim() : '';
return {
email: formData.email.trim(),
username: formData.username.toString().trim(),
displayName: displayName.length > 0 ? displayName : 'A Bee',
password: formData.password,
permissionLevel: formData.permissionLevel,
}
}
static confirmAccountDataIsValid(createAccountData) {
if (typeof createAccountData.id === 'undefined'
|| typeof createAccountData.confirm === 'undefined'
|| !createAccountData.confirm) {
return {
error: true,
message: 'api.account.confirm.required_data_missing',
};
}
return true;
}
async emailExists (email) {
const existingUser = await this.model.findOne({
attributes: ['id'],
where: {
email,
},
});
return existingUser != null;
}
async usernameExists (username) {
const existingUser = await this.model.findOne({
attributes: ['id'],
where: {
username,
},
});
return existingUser != null;
}
async canCreateUser (email, username) {
const emailExists = await this.emailExists(email);
const usernameExists = await this.usernameExists(username);
if (emailExists) {
return {
error: true,
message: 'api.account.email_exists',
};
}
if (usernameExists) {
return {
error: true,
message: 'api.account.username_exists',
};
}
return true;
}
async createUser (email, username, displayName, permissionLevel, password, needsConfirmation) {
const hashData = AccountController.hashPassword(password);
// The data should already have gone through AccountController.cleanCreateAccountFormData()
try {
return await this.model.create({
email,
username,
displayName,
permissionLevel,
passwordHash: hashData.hash,
passwordSalt: hashData.salt,
accountConfirm: needsConfirmation ? crypto.randomBytes(32).toString('hex') : null,
});
} catch (error) {
return {
error,
};
}
}
async confirmUser (id, accountConfirm) {
const userToConfirm = await this.model.findOne({
where: {
id,
accountConfirm,
},
});
if (!userToConfirm) {
return {
error: true,
message: 'api.account.confirm.invalid_code',
}
}
return await this.model.update({
accountConfirm: null,
}, {
where: {
id,
accountConfirm,
},
}).then(success => {
if (success[0] < 1) {
return {
error: true,
message: 'api.account.confirm.update_fail',
}
}
return userToConfirm;
});
}
async validateLogin (email, password) {
const existingUser = await this.model.findOne({
attributes: ['id', 'passwordHash', 'passwordSalt', 'accountConfirm'],
where: {
email,
},
});
if (existingUser == null) {
return {
error: true,
message: 'api.account.login.invalid_email',
};
}
if (existingUser.accountConfirm !== null) {
return {
error: true,
message: 'api.account.login.not_confirmed',
};
}
const passwordIsValid = AccountController.verifyPassword(existingUser.passwordHash, existingUser.passwordSalt, password);
if (!passwordIsValid) {
return {
error: true,
message: 'api.account.login.invalid_password',
};
}
return {
error: false,
id: existingUser.id,
};
}
async deleteUser(user) {
return await user.destroy();
}
}
module.exports = AccountController;