|
|
const prisma = require("../utils/prisma"); |
|
|
const { EventLogs } = require("./eventLogs"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const User = { |
|
|
usernameRegex: new RegExp(/^[a-z0-9_\-.]+$/), |
|
|
writable: [ |
|
|
|
|
|
"username", |
|
|
"password", |
|
|
"pfpFilename", |
|
|
"role", |
|
|
"suspended", |
|
|
"dailyMessageLimit", |
|
|
"bio", |
|
|
], |
|
|
validations: { |
|
|
username: (newValue = "") => { |
|
|
try { |
|
|
if (String(newValue).length > 100) |
|
|
throw new Error("Username cannot be longer than 100 characters"); |
|
|
if (String(newValue).length < 2) |
|
|
throw new Error("Username must be at least 2 characters"); |
|
|
return String(newValue); |
|
|
} catch (e) { |
|
|
throw new Error(e.message); |
|
|
} |
|
|
}, |
|
|
role: (role = "default") => { |
|
|
const VALID_ROLES = ["default", "admin", "manager"]; |
|
|
if (!VALID_ROLES.includes(role)) { |
|
|
throw new Error( |
|
|
`Invalid role. Allowed roles are: ${VALID_ROLES.join(", ")}` |
|
|
); |
|
|
} |
|
|
return String(role); |
|
|
}, |
|
|
dailyMessageLimit: (dailyMessageLimit = null) => { |
|
|
if (dailyMessageLimit === null) return null; |
|
|
const limit = Number(dailyMessageLimit); |
|
|
if (isNaN(limit) || limit < 1) { |
|
|
throw new Error( |
|
|
"Daily message limit must be null or a number greater than or equal to 1" |
|
|
); |
|
|
} |
|
|
return limit; |
|
|
}, |
|
|
bio: (bio = "") => { |
|
|
if (!bio || typeof bio !== "string") return ""; |
|
|
if (bio.length > 1000) |
|
|
throw new Error("Bio cannot be longer than 1,000 characters"); |
|
|
return String(bio); |
|
|
}, |
|
|
}, |
|
|
|
|
|
castColumnValue: function (key, value) { |
|
|
switch (key) { |
|
|
case "suspended": |
|
|
return Number(Boolean(value)); |
|
|
case "dailyMessageLimit": |
|
|
return value === null ? null : Number(value); |
|
|
default: |
|
|
return String(value); |
|
|
} |
|
|
}, |
|
|
|
|
|
filterFields: function (user = {}) { |
|
|
const { password, ...rest } = user; |
|
|
return { ...rest }; |
|
|
}, |
|
|
|
|
|
create: async function ({ |
|
|
username, |
|
|
password, |
|
|
role = "default", |
|
|
dailyMessageLimit = null, |
|
|
bio = "", |
|
|
}) { |
|
|
const passwordCheck = this.checkPasswordComplexity(password); |
|
|
if (!passwordCheck.checkedOK) { |
|
|
return { user: null, error: passwordCheck.error }; |
|
|
} |
|
|
|
|
|
try { |
|
|
|
|
|
if (!this.usernameRegex.test(username)) |
|
|
throw new Error( |
|
|
"Username must only contain lowercase letters, periods, numbers, underscores, and hyphens with no spaces" |
|
|
); |
|
|
|
|
|
const bcrypt = require("bcrypt"); |
|
|
const hashedPassword = bcrypt.hashSync(password, 10); |
|
|
const user = await prisma.users.create({ |
|
|
data: { |
|
|
username: this.validations.username(username), |
|
|
password: hashedPassword, |
|
|
role: this.validations.role(role), |
|
|
bio: this.validations.bio(bio), |
|
|
dailyMessageLimit: |
|
|
this.validations.dailyMessageLimit(dailyMessageLimit), |
|
|
}, |
|
|
}); |
|
|
return { user: this.filterFields(user), error: null }; |
|
|
} catch (error) { |
|
|
console.error("FAILED TO CREATE USER.", error.message); |
|
|
return { user: null, error: error.message }; |
|
|
} |
|
|
}, |
|
|
|
|
|
|
|
|
loggedChanges: function (updates, prev = {}) { |
|
|
const changes = {}; |
|
|
const sensitiveFields = ["password"]; |
|
|
|
|
|
Object.keys(updates).forEach((key) => { |
|
|
if (!sensitiveFields.includes(key) && updates[key] !== prev[key]) { |
|
|
changes[key] = `${prev[key]} => ${updates[key]}`; |
|
|
} |
|
|
}); |
|
|
|
|
|
return changes; |
|
|
}, |
|
|
|
|
|
update: async function (userId, updates = {}) { |
|
|
try { |
|
|
if (!userId) throw new Error("No user id provided for update"); |
|
|
const currentUser = await prisma.users.findUnique({ |
|
|
where: { id: parseInt(userId) }, |
|
|
}); |
|
|
if (!currentUser) return { success: false, error: "User not found" }; |
|
|
|
|
|
|
|
|
Object.entries(updates).forEach(([key, value]) => { |
|
|
if (this.writable.includes(key)) { |
|
|
if (this.validations.hasOwnProperty(key)) { |
|
|
updates[key] = this.validations[key]( |
|
|
this.castColumnValue(key, value) |
|
|
); |
|
|
} else { |
|
|
updates[key] = this.castColumnValue(key, value); |
|
|
} |
|
|
return; |
|
|
} |
|
|
delete updates[key]; |
|
|
}); |
|
|
|
|
|
if (Object.keys(updates).length === 0) |
|
|
return { success: false, error: "No valid updates applied." }; |
|
|
|
|
|
|
|
|
if (updates.hasOwnProperty("password")) { |
|
|
const passwordCheck = this.checkPasswordComplexity(updates.password); |
|
|
if (!passwordCheck.checkedOK) { |
|
|
return { success: false, error: passwordCheck.error }; |
|
|
} |
|
|
const bcrypt = require("bcrypt"); |
|
|
updates.password = bcrypt.hashSync(updates.password, 10); |
|
|
} |
|
|
|
|
|
if ( |
|
|
updates.hasOwnProperty("username") && |
|
|
currentUser.username !== updates.username && |
|
|
!this.usernameRegex.test(updates.username) |
|
|
) |
|
|
return { |
|
|
success: false, |
|
|
error: |
|
|
"Username must only contain lowercase letters, periods, numbers, underscores, and hyphens with no spaces", |
|
|
}; |
|
|
|
|
|
const user = await prisma.users.update({ |
|
|
where: { id: parseInt(userId) }, |
|
|
data: updates, |
|
|
}); |
|
|
|
|
|
await EventLogs.logEvent( |
|
|
"user_updated", |
|
|
{ |
|
|
username: user.username, |
|
|
changes: this.loggedChanges(updates, currentUser), |
|
|
}, |
|
|
userId |
|
|
); |
|
|
return { success: true, error: null }; |
|
|
} catch (error) { |
|
|
console.error(error.message); |
|
|
return { success: false, error: error.message }; |
|
|
} |
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_update: async function (id = null, data = {}) { |
|
|
if (!id) throw new Error("No user id provided for update"); |
|
|
|
|
|
try { |
|
|
const user = await prisma.users.update({ |
|
|
where: { id }, |
|
|
data, |
|
|
}); |
|
|
return { user, message: null }; |
|
|
} catch (error) { |
|
|
console.error(error.message); |
|
|
return { user: null, message: error.message }; |
|
|
} |
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
get: async function (clause = {}) { |
|
|
try { |
|
|
const user = await prisma.users.findFirst({ where: clause }); |
|
|
return user ? this.filterFields({ ...user }) : null; |
|
|
} catch (error) { |
|
|
console.error(error.message); |
|
|
return null; |
|
|
} |
|
|
}, |
|
|
|
|
|
_get: async function (clause = {}) { |
|
|
try { |
|
|
const user = await prisma.users.findFirst({ where: clause }); |
|
|
return user ? { ...user } : null; |
|
|
} catch (error) { |
|
|
console.error(error.message); |
|
|
return null; |
|
|
} |
|
|
}, |
|
|
|
|
|
count: async function (clause = {}) { |
|
|
try { |
|
|
const count = await prisma.users.count({ where: clause }); |
|
|
return count; |
|
|
} catch (error) { |
|
|
console.error(error.message); |
|
|
return 0; |
|
|
} |
|
|
}, |
|
|
|
|
|
delete: async function (clause = {}) { |
|
|
try { |
|
|
await prisma.users.deleteMany({ where: clause }); |
|
|
return true; |
|
|
} catch (error) { |
|
|
console.error(error.message); |
|
|
return false; |
|
|
} |
|
|
}, |
|
|
|
|
|
where: async function (clause = {}, limit = null) { |
|
|
try { |
|
|
const users = await prisma.users.findMany({ |
|
|
where: clause, |
|
|
...(limit !== null ? { take: limit } : {}), |
|
|
}); |
|
|
return users.map((usr) => this.filterFields(usr)); |
|
|
} catch (error) { |
|
|
console.error(error.message); |
|
|
return []; |
|
|
} |
|
|
}, |
|
|
|
|
|
checkPasswordComplexity: function (passwordInput = "") { |
|
|
const passwordComplexity = require("joi-password-complexity"); |
|
|
|
|
|
|
|
|
const complexityOptions = { |
|
|
min: process.env.PASSWORDMINCHAR || 8, |
|
|
max: process.env.PASSWORDMAXCHAR || 250, |
|
|
lowerCase: process.env.PASSWORDLOWERCASE || 0, |
|
|
upperCase: process.env.PASSWORDUPPERCASE || 0, |
|
|
numeric: process.env.PASSWORDNUMERIC || 0, |
|
|
symbol: process.env.PASSWORDSYMBOL || 0, |
|
|
|
|
|
requirementCount: process.env.PASSWORDREQUIREMENTS || 0, |
|
|
}; |
|
|
|
|
|
const complexityCheck = passwordComplexity( |
|
|
complexityOptions, |
|
|
"password" |
|
|
).validate(passwordInput); |
|
|
if (complexityCheck.hasOwnProperty("error")) { |
|
|
let myError = ""; |
|
|
let prepend = ""; |
|
|
for (let i = 0; i < complexityCheck.error.details.length; i++) { |
|
|
myError += prepend + complexityCheck.error.details[i].message; |
|
|
prepend = ", "; |
|
|
} |
|
|
return { checkedOK: false, error: myError }; |
|
|
} |
|
|
|
|
|
return { checkedOK: true, error: "No error." }; |
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
canSendChat: async function (user) { |
|
|
const { ROLES } = require("../utils/middleware/multiUserProtected"); |
|
|
if (!user || user.dailyMessageLimit === null || user.role === ROLES.admin) |
|
|
return true; |
|
|
|
|
|
const { WorkspaceChats } = require("./workspaceChats"); |
|
|
const currentChatCount = await WorkspaceChats.count({ |
|
|
user_id: user.id, |
|
|
createdAt: { |
|
|
gte: new Date(new Date() - 24 * 60 * 60 * 1000), |
|
|
}, |
|
|
}); |
|
|
|
|
|
return currentChatCount < user.dailyMessageLimit; |
|
|
}, |
|
|
}; |
|
|
|
|
|
module.exports = { User }; |
|
|
|