const luxon = require('luxon')
const crypto = require('crypto')
const { encode } = require('punycode')
const DateTime = luxon.DateTime
const db = require('./dbConnection')
/**
* Convert a decimal number to a binary array, optionally padded to a certain length
* Generated by github copilot
* @param {*} N the denary value to be converted
* @param {*} length
* @returns {Array} array of binary digits as strings
*/
function decimalToBinary(N, length = 0) {
const bin = (N >>> 0).toString(2)
const padded = length ? bin.padStart(length, '0') : bin
return padded.split('').reverse()
}
// console.log(encodePermissionBitmask(["MANAGE_USERS", "MANAGE_EVENTS"]))
// console.log(decodePermissionBitmask(12))
/**
* Encode an array of permission names into a bitmask integer
* @param {Array} permissions
* @returns {Integer} value of permission bitmask
*/
function encodePermissionBitmask(permissions) {
let bitmask = ""
let permData = [...global.permData]
permData.reverse().forEach(permission => {
if (permissions.includes(permission.permissionName)) {
bitmask += "1"
} else {
bitmask += "0"
}
})
return parseInt(bitmask, 2)
}
/**
* Decode a permission bitmask integer into an object of permission names and boolean values
* @param {Integer} bitmask
* @returns {Object} object of permission names and boolean values
*/
function decodePermissionBitmask(bitmask) {
const permsBinary = decimalToBinary(bitmask)
let userPerms = {}
// Split up the binary string and reverse it to match permission order
// Then iterate through each bit, referencing to the global permissions data and assign permission names accordingly into an object to be returned
permsBinary.forEach((bit, index) => {
let permission = global.permDataMapped[index]
if (bit == "1") {
userPerms[permission.permissionName] = true
} else {
userPerms[permission.permissionName] = false
}
})
return userPerms
}
/**
* Back end logic to check if a user's provided permissions meet the requirements specified. Contains the logic for both "AND" and "OR" operations as well as universal access for admins.
* Will accept either an Integer bitmask or a decoded permissions object.
* @param {Array} requiredPerms
* @param {Object} userPerms Can also be an integer value
* @param {String} logicType "AND" or "OR"
* @returns {Boolean}
*/
function hasPermissions(requiredPerms, userPerms, logicType = "AND") {
if (typeof userPerms === 'object') {
decodedUserPerms = userPerms
} else {
let decodedUserPerms = decodePermissionBitmask(userPerms)
}
if (decodedUserPerms["ADMINISTRATOR"] === true) {
return true
} else {
if (logicType === "OR") {
// If the logic type is set to OR then only one permission from the requiredPerms array needs to be true
let accessGranted = false
requiredPerms.forEach(perm => {
if (decodedUserPerms[perm] === true) {
accessGranted = true
}
})
return accessGranted
} else {
// If the logic type is set to AND (Default option) then all permissions from the requiredPerms array need to be true
let accessGranted = true
requiredPerms.forEach(perm => {
if (decodedUserPerms[perm] !== true) {
accessGranted = false
}
})
return accessGranted
}
}
}
/**
* Hashes a password using the scrypt algorithm with a random salt, returning the combined salt and hash for storage
* @param {String} password
* @returns {String} Salt and Hash
*/
function hashPassword(password) {
// Generate random salt for the user
const salt = crypto.randomBytes(16).toString('hex')
// Hash the password with the salt
const hash = crypto.scryptSync(password, salt, 64).toString('hex')
// Return the combined result for storage
return (`${salt}:${hash}`)
}
/**
* Verifies a password by hashing it with the same salt and comparing it to the stored hash
* @param {String} password
* @param {String} storedPassword
* @returns {Boolean}
*/
function verifyPassword(password, storedPassword) {
// Split the stored hashed password into salt and hash, trim any white space to remove chance of errors
const parts = String(storedPassword).trim().split(':')
const [salt, hash] = parts.map(p => p.trim())
// Hash the password with the salt
const hashedPassword = crypto.scryptSync(password, salt, 64)
const originalPassword = Buffer.from(hash, 'hex')
if (hashedPassword.length !== originalPassword.length) return false
// Compare the hashed password with the stored hash and return true or false if they match or not
return crypto.timingSafeEqual(hashedPassword, originalPassword)
}
// Filter out columns that are in the exception list
function filterColumns(array, exceptionList) {
let filteredColumns = array.map(user => {
let filteredColumn = {}
Object.keys(user).forEach(key => {
if (!exceptionList.includes(key)) {
filteredColumn[key] = user[key]
}
})
return filteredColumn;
})
return filteredColumns
}
/**
* Takes a userID and returns the user data from the database corresponding to that userID.
* Returns first value as a boolean, false indicating failure, true indicating success.
* The second value is either the the error data if it failed or the requested data if it succeeded.
* Filters out so only selected data will be returned, password hash is not included.
* @param {string} userID
* @param {function} cb
*/
function getUserData(userID, cb) {
if (userID) {
db.query('SELECT * FROM users WHERE userID = ?', [userID], (err, results) => {
if (err) {
cb(false, err)
} else {
if (results.length === 0) {
cb(false, 'No user found with that ID')
} else {
let userData = {
userID: results[0].userID,
username: results[0].username,
perms: decodePermissionBitmask(results[0].perms),
permsValue: results[0].perms,
email: results[0].email,
DOB: results[0].DOB,
firstName: results[0].firstName,
lastName: results[0].lastName,
timezone: results[0].timezone,
accountCreated: DateTime.fromSeconds(results[0].accountCreated)
}
cb(true, userData)
}
}
})
} else {
// Just returns a blank user profile for when there is no one logged in
cb
(
true,
{
userID: '',
username: '',
perms: decodePermissionBitmask(0),
permsValue: 0,
email: '',
DOB: '',
firstName: '',
lastName: '',
timezone: '',
accountCreated: DateTime.utc()
}
)
}
}
module.exports = {
encodePermissionBitmask,
decodePermissionBitmask,
hasPermissions,
filterColumns,
hashPassword,
verifyPassword,
getUserData
}