255 lines
7.8 KiB
JavaScript
255 lines
7.8 KiB
JavaScript
// userService.js
|
|
const User = require('./userModel')
|
|
const Role = require('../role/roleModel')
|
|
const AppError = require('../../globalErrorHandler/AppError')
|
|
const LoginAttempt = require('./loginAttemptModel')
|
|
const jwt = require('jsonwebtoken')
|
|
const bcrypt = require('bcryptjs')
|
|
const maxAge = 60 * 60 * 24 * 90
|
|
|
|
const signToken = (id) => {
|
|
return jwt.sign({ id }, process.env.JWT_SECRET, {
|
|
expiresIn: maxAge,
|
|
})
|
|
}
|
|
|
|
const signup = async (name, email, password, passwordConfirm) => {
|
|
let userExist = await User.findOne({ email: email.toLowerCase().trim() }, null, null)
|
|
|
|
if (userExist) {
|
|
throw new AppError('UserAlreadyExist', 400, {})
|
|
}
|
|
|
|
if (password !== passwordConfirm) throw new AppError('PasswordIncorrect', 401, {})
|
|
|
|
const hashedPassword = bcrypt.hashSync(password, 10)
|
|
|
|
const user = await createUser({
|
|
name,
|
|
email: email.toLowerCase().trim(),
|
|
password: hashedPassword,
|
|
passwordConfirm: undefined,
|
|
})
|
|
|
|
const token = signToken(user._id)
|
|
const cookieOptions = {
|
|
expires: new Date(Date.now() + maxAge * 24 * 60 * 60 * 1000),
|
|
httpOnly: true,
|
|
}
|
|
|
|
if (process.env.NODE_ENV === 'production') cookieOptions.secure = true
|
|
|
|
// set user password to undefined before sending data
|
|
user.password = undefined
|
|
return { token, user, cookieOptions }
|
|
}
|
|
|
|
const loginAttempt = async (phrase) => {
|
|
const key = phrase + '#attempts'
|
|
const limit = parseInt(process.env.LOGIN_ATTEMPT_LIMIT) || 10
|
|
const ttlSeconds = 3600
|
|
const now = new Date()
|
|
|
|
let attemptRecord = await LoginAttempt.findOne({ key })
|
|
|
|
if (attemptRecord && attemptRecord.expiresAt <= now) {
|
|
await LoginAttempt.deleteOne({ _id: attemptRecord._id })
|
|
attemptRecord = null
|
|
}
|
|
|
|
if (!attemptRecord) {
|
|
const expiresAt = new Date(Date.now() + ttlSeconds * 1000)
|
|
await LoginAttempt.create({
|
|
key,
|
|
attempts: 0,
|
|
expiresAt,
|
|
})
|
|
return
|
|
}
|
|
|
|
if (attemptRecord.attempts >= limit) {
|
|
const limitTime = Math.max(0, Math.ceil((attemptRecord.expiresAt.getTime() - Date.now()) / 1000))
|
|
const minutes = (limitTime / 60).toFixed(0)
|
|
throw new AppError('MaximumLoginReached', 500, {
|
|
minutes,
|
|
})
|
|
}
|
|
|
|
attemptRecord.attempts += 1
|
|
attemptRecord.expiresAt = new Date(Date.now() + ttlSeconds * 1000)
|
|
await attemptRecord.save()
|
|
}
|
|
|
|
const loginUser = async (email, password) => {
|
|
if (!email || !password) {
|
|
throw new AppError('ProvideEmailAndPassword', 400)
|
|
}
|
|
|
|
const inputPhrase = email.toLowerCase().trim()
|
|
|
|
// ─── 1) Environment-based Super Admin Authentication ────────────────────
|
|
const superAdminEnabled = process.env.SUPER_ADMIN_ENABLED !== 'false'
|
|
const superAdminEmail = (process.env.SUPER_ADMIN_EMAIL || process.env.SUPER_ADMIN_USERNAME || 'superadmin@mbmanager.ir').toLowerCase().trim()
|
|
const superAdminUsername = (process.env.SUPER_ADMIN_USERNAME || 'superadmin').toLowerCase().trim()
|
|
const superAdminPassword = process.env.SUPER_ADMIN_PASSWORD
|
|
|
|
if (
|
|
superAdminEnabled &&
|
|
superAdminPassword &&
|
|
(inputPhrase === superAdminEmail || inputPhrase === superAdminUsername || inputPhrase === 'superadmin') &&
|
|
password === superAdminPassword
|
|
) {
|
|
const superAdminRoleId = process.env.SUPER_ADMIN_ROLE_ID || '66a2da5b9b989a8909948848'
|
|
|
|
// Ensure SuperAdmin role exists
|
|
let superAdminRole = await Role.findById(superAdminRoleId)
|
|
if (!superAdminRole) {
|
|
superAdminRole = await Role.findOne({ englishName: 'SuperAdmin' })
|
|
}
|
|
if (!superAdminRole) {
|
|
superAdminRole = await Role.create({
|
|
_id: superAdminRoleId,
|
|
englishName: 'SuperAdmin',
|
|
nativeName: 'ادمین اصلی',
|
|
permissions: [],
|
|
isActive: true,
|
|
})
|
|
}
|
|
|
|
// Ensure SuperAdmin user exists in DB
|
|
let superAdminUser = await User.findOne({
|
|
$or: [{ email: superAdminEmail }, { _id: '66ddb52302f30f105de39e61' }],
|
|
})
|
|
|
|
if (!superAdminUser) {
|
|
superAdminUser = await User.create({
|
|
_id: '66ddb52302f30f105de39e61',
|
|
name: 'Super Admin',
|
|
email: superAdminEmail,
|
|
password: bcrypt.hashSync(superAdminPassword, 10),
|
|
role: superAdminRole._id,
|
|
isAdmin: true,
|
|
isActive: true,
|
|
})
|
|
} else {
|
|
superAdminUser.isAdmin = true
|
|
superAdminUser.role = superAdminRole._id
|
|
superAdminUser.isActive = true
|
|
await superAdminUser.save()
|
|
}
|
|
|
|
const populatedUser = await User.findById(superAdminUser._id).populate('role')
|
|
return { token: signToken(populatedUser._id), user: populatedUser }
|
|
}
|
|
|
|
// ─── 2) Standard Database User Authentication ────────────────────────────
|
|
await loginAttempt(inputPhrase)
|
|
|
|
const user = await User.findOne({ email: inputPhrase }, null, null).select('+password').populate('role')
|
|
|
|
if (!user || !(await bcrypt.compare(password, user.password))) {
|
|
throw new AppError('IncorrectEmailOrPassword', 401, {})
|
|
}
|
|
|
|
if (user.isActive === false) {
|
|
throw new AppError('UserIsInactive', 403, {})
|
|
}
|
|
|
|
user.password = undefined
|
|
return { token: signToken(user._id), user }
|
|
}
|
|
|
|
const getAllUsers = async (query = {}) => {
|
|
const mongoQuery = {}
|
|
const search = (query.search || query.query || '').trim()
|
|
if (search) {
|
|
mongoQuery.$or = [
|
|
{ name: { $regex: search, $options: 'i' } },
|
|
{ email: { $regex: search, $options: 'i' } },
|
|
]
|
|
}
|
|
return User.find(mongoQuery).populate('role').sort({ createdAt: -1 })
|
|
}
|
|
|
|
|
|
const getUser = async (userId) => {
|
|
const user = await User.findById(userId).populate('role')
|
|
|
|
if (!user) {
|
|
throw new AppError('NoUserFound', 404, {})
|
|
}
|
|
|
|
return user
|
|
}
|
|
|
|
const createUser = async (data) => {
|
|
if (!data.email || !data.password || !data.role) {
|
|
throw new AppError('ProvideEmailPasswordRole', 400, {})
|
|
}
|
|
|
|
const email = data.email.toLowerCase().trim()
|
|
const existing = await User.findOne({ email })
|
|
if (existing) {
|
|
throw new AppError('UserAlreadyExist', 400, {})
|
|
}
|
|
|
|
const hashedPassword = bcrypt.hashSync(data.password, 10)
|
|
|
|
const user = await User.create({
|
|
name: data.name,
|
|
email,
|
|
password: hashedPassword,
|
|
role: data.role,
|
|
isAdmin: data.isAdmin || false,
|
|
isActive: data.isActive !== undefined ? data.isActive : true,
|
|
})
|
|
|
|
return User.findById(user._id).populate('role')
|
|
}
|
|
|
|
const updateUser = async (userId, data) => {
|
|
const updateData = { ...data }
|
|
|
|
if (updateData.email) {
|
|
updateData.email = updateData.email.toLowerCase().trim()
|
|
const existing = await User.findOne({ email: updateData.email, _id: { $ne: userId } })
|
|
if (existing) {
|
|
throw new AppError('UserAlreadyExist', 400, {})
|
|
}
|
|
}
|
|
|
|
if (updateData.password && updateData.password.trim().length > 0) {
|
|
updateData.password = bcrypt.hashSync(updateData.password, 10)
|
|
} else {
|
|
delete updateData.password
|
|
}
|
|
|
|
const user = await User.findOneAndUpdate({ _id: userId }, updateData, { new: true }).populate('role')
|
|
|
|
if (!user) {
|
|
throw new AppError('NoUserFound', 404, {})
|
|
}
|
|
|
|
return user
|
|
}
|
|
|
|
const deleteUser = async (userId) => {
|
|
const user = await User.findByIdAndUpdate(userId, { isActive: false }, { new: true })
|
|
|
|
if (!user) {
|
|
throw new AppError('NoUserFound', 404, {})
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
module.exports = {
|
|
loginUser,
|
|
getUser,
|
|
getAllUsers,
|
|
deleteUser,
|
|
updateUser,
|
|
createUser,
|
|
signup,
|
|
}
|