|
const crypto = require('crypto'); |
|
const bcrypt = require('bcryptjs'); |
|
const User = require('../../models/User'); |
|
const Session = require('../../models/Session'); |
|
const Token = require('../../models/schema/tokenSchema'); |
|
const { registerSchema, errorsToString } = require('../../strategies/validators'); |
|
const config = require('../../../config/loader'); |
|
const { sendEmail } = require('../utils'); |
|
const domains = config.domains; |
|
const isProduction = config.isProduction; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const logoutUser = async (userId, refreshToken) => { |
|
try { |
|
const hash = crypto.createHash('sha256').update(refreshToken).digest('hex'); |
|
|
|
|
|
const session = await Session.findOne({ user: userId, refreshTokenHash: hash }); |
|
if (session) { |
|
try { |
|
await Session.deleteOne({ _id: session._id }); |
|
} catch (deleteErr) { |
|
console.error(deleteErr); |
|
return { status: 500, message: 'Failed to delete session.' }; |
|
} |
|
} |
|
|
|
return { status: 200, message: 'Logout successful' }; |
|
} catch (err) { |
|
return { status: 500, message: err.message }; |
|
} |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const registerUser = async (user) => { |
|
const { error } = registerSchema.safeParse(user); |
|
if (error) { |
|
const errorMessage = errorsToString(error.errors); |
|
console.info( |
|
'Route: register - Validation Error', |
|
{ name: 'Request params:', value: user }, |
|
{ name: 'Validation error:', value: errorMessage }, |
|
); |
|
|
|
return { status: 422, message: errorMessage }; |
|
} |
|
|
|
const { email, password, name, username } = user; |
|
|
|
try { |
|
const existingUser = await User.findOne({ email }).lean(); |
|
|
|
if (existingUser) { |
|
console.info( |
|
'Register User - Email in use', |
|
{ name: 'Request params:', value: user }, |
|
{ name: 'Existing user:', value: existingUser }, |
|
); |
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 1000)); |
|
|
|
|
|
return { status: 500, message: 'Something went wrong' }; |
|
} |
|
|
|
|
|
const isFirstRegisteredUser = (await User.countDocuments({})) === 0; |
|
|
|
const newUser = await new User({ |
|
provider: 'local', |
|
email, |
|
password, |
|
username, |
|
name, |
|
avatar: null, |
|
role: isFirstRegisteredUser ? 'ADMIN' : 'USER', |
|
}); |
|
|
|
const salt = bcrypt.genSaltSync(10); |
|
const hash = bcrypt.hashSync(newUser.password, salt); |
|
newUser.password = hash; |
|
newUser.save(); |
|
|
|
return { status: 200, user: newUser }; |
|
} catch (err) { |
|
return { status: 500, message: err?.message || 'Something went wrong' }; |
|
} |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const requestPasswordReset = async (email) => { |
|
const user = await User.findOne({ email }).lean(); |
|
if (!user) { |
|
return new Error('Email does not exist'); |
|
} |
|
|
|
let token = await Token.findOne({ userId: user._id }); |
|
if (token) { |
|
await token.deleteOne(); |
|
} |
|
|
|
let resetToken = crypto.randomBytes(32).toString('hex'); |
|
const hash = await bcrypt.hashSync(resetToken, 10); |
|
|
|
await new Token({ |
|
userId: user._id, |
|
token: hash, |
|
createdAt: Date.now(), |
|
}).save(); |
|
|
|
const link = `${domains.client}/reset-password?token=${resetToken}&userId=${user._id}`; |
|
|
|
const emailEnabled = |
|
!!process.env.EMAIL_SERVICE && |
|
!!process.env.EMAIL_USERNAME && |
|
!!process.env.EMAIL_PASSWORD && |
|
!!process.env.EMAIL_FROM; |
|
|
|
if (emailEnabled) { |
|
sendEmail( |
|
user.email, |
|
'Password Reset Request', |
|
{ |
|
name: user.name, |
|
link: link, |
|
}, |
|
'requestPasswordReset.handlebars', |
|
); |
|
return { link: '' }; |
|
} else { |
|
return { link }; |
|
} |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const resetPassword = async (userId, token, password) => { |
|
let passwordResetToken = await Token.findOne({ userId }); |
|
|
|
if (!passwordResetToken) { |
|
return new Error('Invalid or expired password reset token'); |
|
} |
|
|
|
const isValid = bcrypt.compareSync(token, passwordResetToken.token); |
|
|
|
if (!isValid) { |
|
return new Error('Invalid or expired password reset token'); |
|
} |
|
|
|
const hash = bcrypt.hashSync(password, 10); |
|
|
|
await User.updateOne({ _id: userId }, { $set: { password: hash } }, { new: true }); |
|
|
|
const user = await User.findById({ _id: userId }); |
|
|
|
sendEmail( |
|
user.email, |
|
'Password Reset Successfully', |
|
{ |
|
name: user.name, |
|
}, |
|
'resetPassword.handlebars', |
|
); |
|
|
|
await passwordResetToken.deleteOne(); |
|
|
|
return { message: 'Password reset was successful' }; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const setAuthTokens = async (userId, res, sessionId = null) => { |
|
try { |
|
const user = await User.findOne({ _id: userId }); |
|
const token = await user.generateToken(); |
|
|
|
let session; |
|
let refreshTokenExpires; |
|
if (sessionId) { |
|
session = await Session.findById(sessionId); |
|
refreshTokenExpires = session.expiration.getTime(); |
|
} else { |
|
session = new Session({ user: userId }); |
|
const { REFRESH_TOKEN_EXPIRY } = process.env ?? {}; |
|
const expires = eval(REFRESH_TOKEN_EXPIRY) ?? 1000 * 60 * 60 * 24 * 7; |
|
refreshTokenExpires = Date.now() + expires; |
|
} |
|
|
|
const refreshToken = await session.generateRefreshToken(); |
|
|
|
res.cookie('refreshToken', refreshToken, { |
|
expires: new Date(refreshTokenExpires), |
|
httpOnly: true, |
|
secure: isProduction, |
|
sameSite: 'strict', |
|
}); |
|
|
|
return token; |
|
} catch (error) { |
|
console.log('Error in setting authentication tokens:', error); |
|
throw error; |
|
} |
|
}; |
|
|
|
module.exports = { |
|
registerUser, |
|
logoutUser, |
|
requestPasswordReset, |
|
resetPassword, |
|
setAuthTokens, |
|
}; |
|
|