| const crypto = require('crypto'); |
| const bcrypt = require('bcryptjs'); |
| const { SystemRoles, errorsToString } = require('librechat-data-provider'); |
| const { |
| findUser, |
| countUsers, |
| createUser, |
| updateUser, |
| getUserById, |
| generateToken, |
| deleteUserById, |
| } = require('~/models/userMethods'); |
| const { sendEmail, checkEmailConfig } = require('~/server/utils'); |
| const { registerSchema } = require('~/strategies/validators'); |
| const isDomainAllowed = require('./isDomainAllowed'); |
| const Token = require('~/models/schema/tokenSchema'); |
| const Session = require('~/models/Session'); |
| const { logger } = require('~/config'); |
|
|
| const domains = { |
| client: process.env.DOMAIN_CLIENT, |
| server: process.env.DOMAIN_SERVER, |
| }; |
|
|
| const isProduction = process.env.NODE_ENV === 'production'; |
| const genericVerificationMessage = 'Please check your email to verify your email address.'; |
|
|
| |
| |
| |
| |
| |
| |
| |
| 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) { |
| logger.error('[logoutUser] Failed to delete session.', deleteErr); |
| return { status: 500, message: 'Failed to delete session.' }; |
| } |
| } |
|
|
| return { status: 200, message: 'Logout successful' }; |
| } catch (err) { |
| return { status: 500, message: err.message }; |
| } |
| }; |
|
|
| |
| |
| |
| |
| |
| const sendVerificationEmail = async (user) => { |
| let verifyToken = crypto.randomBytes(32).toString('hex'); |
| const hash = bcrypt.hashSync(verifyToken, 10); |
|
|
| const verificationLink = `${domains.client}/verify?token=${verifyToken}&email=${encodeURIComponent(user.email)}`; |
| await sendEmail({ |
| email: user.email, |
| subject: 'Verify your email', |
| payload: { |
| appName: process.env.APP_TITLE || 'LibreChat', |
| name: user.name, |
| verificationLink: verificationLink, |
| year: new Date().getFullYear(), |
| }, |
| template: 'verifyEmail.handlebars', |
| }); |
|
|
| await new Token({ |
| userId: user._id, |
| email: user.email, |
| token: hash, |
| createdAt: Date.now(), |
| }).save(); |
|
|
| logger.info(`[sendVerificationEmail] Verification link issued. [Email: ${user.email}]`); |
| }; |
|
|
| |
| |
| |
| |
| const verifyEmail = async (req) => { |
| const { email, token } = req.body; |
| let emailVerificationData = await Token.findOne({ email: decodeURIComponent(email) }); |
|
|
| if (!emailVerificationData) { |
| logger.warn(`[verifyEmail] [No email verification data found] [Email: ${email}]`); |
| return new Error('Invalid or expired password reset token'); |
| } |
|
|
| const isValid = bcrypt.compareSync(token, emailVerificationData.token); |
|
|
| if (!isValid) { |
| logger.warn(`[verifyEmail] [Invalid or expired email verification token] [Email: ${email}]`); |
| return new Error('Invalid or expired email verification token'); |
| } |
|
|
| const updatedUser = await updateUser(emailVerificationData.userId, { emailVerified: true }); |
| if (!updatedUser) { |
| logger.warn(`[verifyEmail] [User not found] [Email: ${email}]`); |
| return new Error('User not found'); |
| } |
|
|
| await emailVerificationData.deleteOne(); |
| logger.info(`[verifyEmail] Email verification successful. [Email: ${email}]`); |
| return { message: 'Email verification was successful' }; |
| }; |
|
|
| |
| |
| |
| |
| |
| const registerUser = async (user) => { |
| const { error } = registerSchema.safeParse(user); |
| if (error) { |
| const errorMessage = errorsToString(error.errors); |
| logger.info( |
| 'Route: register - Validation Error', |
| { name: 'Request params:', value: user }, |
| { name: 'Validation error:', value: errorMessage }, |
| ); |
|
|
| return { status: 404, message: errorMessage }; |
| } |
|
|
| const { email, password, name, username } = user; |
|
|
| let newUserId; |
| try { |
| const existingUser = await findUser({ email }, 'email _id'); |
|
|
| if (existingUser) { |
| logger.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: 200, message: genericVerificationMessage }; |
| } |
|
|
| if (!(await isDomainAllowed(email))) { |
| const errorMessage = |
| 'The email address provided cannot be used. Please use a different email address.'; |
| logger.error(`[registerUser] [Registration not allowed] [Email: ${user.email}]`); |
| return { status: 403, message: errorMessage }; |
| } |
|
|
| |
| const isFirstRegisteredUser = (await countUsers()) === 0; |
|
|
| const salt = bcrypt.genSaltSync(10); |
| const newUserData = { |
| provider: 'local', |
| email, |
| username, |
| name, |
| avatar: null, |
| role: isFirstRegisteredUser ? SystemRoles.ADMIN : SystemRoles.USER, |
| password: bcrypt.hashSync(password, salt), |
| }; |
|
|
| const emailEnabled = checkEmailConfig(); |
| newUserId = await createUser(newUserData, false); |
| if (emailEnabled) { |
| await sendVerificationEmail({ |
| _id: newUserId, |
| email, |
| name, |
| }); |
| } else { |
| await updateUser(newUserId, { emailVerified: true }); |
| } |
|
|
| return { status: 200, message: genericVerificationMessage }; |
| } catch (err) { |
| logger.error('[registerUser] Error in registering user:', err); |
| if (newUserId) { |
| const result = await deleteUserById(newUserId); |
| logger.warn( |
| `[registerUser] [Email: ${email}] [Temporary User deleted: ${JSON.stringify(result)}]`, |
| ); |
| } |
| return { status: 500, message: 'Something went wrong' }; |
| } |
| }; |
|
|
| |
| |
| |
| |
| const requestPasswordReset = async (req) => { |
| const { email } = req.body; |
| const user = await findUser({ email }, 'email _id'); |
| const emailEnabled = checkEmailConfig(); |
|
|
| logger.warn(`[requestPasswordReset] [Password reset request initiated] [Email: ${email}]`); |
|
|
| if (!user) { |
| logger.warn(`[requestPasswordReset] [No user found] [Email: ${email}] [IP: ${req.ip}]`); |
| return { |
| message: 'If an account with that email exists, a password reset link has been sent to it.', |
| }; |
| } |
|
|
| let token = await Token.findOne({ userId: user._id }); |
| if (token) { |
| await token.deleteOne(); |
| } |
|
|
| let resetToken = crypto.randomBytes(32).toString('hex'); |
| const hash = 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}`; |
|
|
| if (emailEnabled) { |
| await sendEmail({ |
| email: user.email, |
| subject: 'Password Reset Request', |
| payload: { |
| appName: process.env.APP_TITLE || 'LibreChat', |
| name: user.name, |
| link: link, |
| year: new Date().getFullYear(), |
| }, |
| template: 'requestPasswordReset.handlebars', |
| }); |
| logger.info( |
| `[requestPasswordReset] Link emailed. [Email: ${email}] [ID: ${user._id}] [IP: ${req.ip}]`, |
| ); |
| } else { |
| logger.info( |
| `[requestPasswordReset] Link issued. [Email: ${email}] [ID: ${user._id}] [IP: ${req.ip}]`, |
| ); |
| return { link }; |
| } |
|
|
| return { |
| message: 'If an account with that email exists, a password reset link has been sent to it.', |
| }; |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| 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); |
| const user = await updateUser(userId, { password: hash }); |
|
|
| if (checkEmailConfig()) { |
| await sendEmail({ |
| email: user.email, |
| subject: 'Password Reset Successfully', |
| payload: { |
| appName: process.env.APP_TITLE || 'LibreChat', |
| name: user.name, |
| year: new Date().getFullYear(), |
| }, |
| template: 'passwordReset.handlebars', |
| }); |
| } |
|
|
| await passwordResetToken.deleteOne(); |
| logger.info(`[resetPassword] Password reset successful. [Email: ${user.email}]`); |
| return { message: 'Password reset was successful' }; |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| const setAuthTokens = async (userId, res, sessionId = null) => { |
| try { |
| const user = await getUserById(userId); |
| const token = await generateToken(user); |
|
|
| 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) { |
| logger.error('[setAuthTokens] Error in setting authentication tokens:', error); |
| throw error; |
| } |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| const resendVerificationEmail = async (req) => { |
| try { |
| const { email } = req.body; |
| await Token.deleteMany({ email }); |
| const user = await findUser({ email }, 'email _id name'); |
|
|
| if (!user) { |
| logger.warn(`[resendVerificationEmail] [No user found] [Email: ${email}]`); |
| return { status: 200, message: genericVerificationMessage }; |
| } |
|
|
| let verifyToken = crypto.randomBytes(32).toString('hex'); |
| const hash = bcrypt.hashSync(verifyToken, 10); |
|
|
| const verificationLink = `${domains.client}/verify?token=${verifyToken}&email=${encodeURIComponent(user.email)}`; |
|
|
| await sendEmail({ |
| email: user.email, |
| subject: 'Verify your email', |
| payload: { |
| appName: process.env.APP_TITLE || 'LibreChat', |
| name: user.name, |
| verificationLink: verificationLink, |
| year: new Date().getFullYear(), |
| }, |
| template: 'verifyEmail.handlebars', |
| }); |
|
|
| await new Token({ |
| userId: user._id, |
| email: user.email, |
| token: hash, |
| createdAt: Date.now(), |
| }).save(); |
|
|
| logger.info(`[resendVerificationEmail] Verification link issued. [Email: ${user.email}]`); |
|
|
| return { |
| status: 200, |
| message: genericVerificationMessage, |
| }; |
| } catch (error) { |
| logger.error(`[resendVerificationEmail] Error resending verification email: ${error.message}`); |
| return { |
| status: 500, |
| message: 'Something went wrong.', |
| }; |
| } |
| }; |
|
|
| module.exports = { |
| logoutUser, |
| verifyEmail, |
| registerUser, |
| setAuthTokens, |
| resetPassword, |
| isDomainAllowed, |
| requestPasswordReset, |
| resendVerificationEmail, |
| }; |
|
|