| const bcrypt = require('bcryptjs'); |
| const jwt = require('jsonwebtoken'); |
| const { webcrypto } = require('node:crypto'); |
| const { logger } = require('@librechat/data-schemas'); |
| const { isEnabled, checkEmailConfig, isEmailDomainAllowed } = require('@librechat/api'); |
| const { ErrorTypes, SystemRoles, errorsToString } = require('librechat-data-provider'); |
| const { |
| findUser, |
| findToken, |
| createUser, |
| updateUser, |
| countUsers, |
| getUserById, |
| findSession, |
| createToken, |
| deleteTokens, |
| deleteSession, |
| createSession, |
| generateToken, |
| deleteUserById, |
| generateRefreshToken, |
| } = require('~/models'); |
| const { registerSchema } = require('~/strategies/validators'); |
| const { getAppConfig } = require('~/server/services/Config'); |
| const { sendEmail } = require('~/server/utils'); |
|
|
| 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 (req, refreshToken) => { |
| try { |
| const userId = req.user._id; |
| const session = await findSession({ userId: userId, refreshToken }); |
|
|
| if (session) { |
| try { |
| await deleteSession({ sessionId: session._id }); |
| } catch (deleteErr) { |
| logger.error('[logoutUser] Failed to delete session.', deleteErr); |
| return { status: 500, message: 'Failed to delete session.' }; |
| } |
| } |
|
|
| try { |
| req.session.destroy(); |
| } catch (destroyErr) { |
| logger.debug('[logoutUser] Failed to destroy session.', destroyErr); |
| } |
|
|
| return { status: 200, message: 'Logout successful' }; |
| } catch (err) { |
| return { status: 500, message: err.message }; |
| } |
| }; |
|
|
| |
| |
| |
| |
| const createTokenHash = () => { |
| const token = Buffer.from(webcrypto.getRandomValues(new Uint8Array(32))).toString('hex'); |
| const hash = bcrypt.hashSync(token, 10); |
| return [token, hash]; |
| }; |
|
|
| |
| |
| |
| |
| |
| const sendVerificationEmail = async (user) => { |
| const [verifyToken, hash] = createTokenHash(); |
|
|
| 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 || user.username || user.email, |
| verificationLink: verificationLink, |
| year: new Date().getFullYear(), |
| }, |
| template: 'verifyEmail.handlebars', |
| }); |
|
|
| await createToken({ |
| userId: user._id, |
| email: user.email, |
| token: hash, |
| createdAt: Date.now(), |
| expiresIn: 900, |
| }); |
|
|
| logger.info(`[sendVerificationEmail] Verification link issued. [Email: ${user.email}]`); |
| }; |
|
|
| |
| |
| |
| |
| const verifyEmail = async (req) => { |
| const { email, token } = req.body; |
| const decodedEmail = decodeURIComponent(email); |
|
|
| const user = await findUser({ email: decodedEmail }, 'email _id emailVerified'); |
|
|
| if (!user) { |
| logger.warn(`[verifyEmail] [User not found] [Email: ${decodedEmail}]`); |
| return new Error('User not found'); |
| } |
|
|
| if (user.emailVerified) { |
| logger.info(`[verifyEmail] Email already verified [Email: ${decodedEmail}]`); |
| return { message: 'Email already verified', status: 'success' }; |
| } |
|
|
| let emailVerificationData = await findToken({ email: decodedEmail }, { sort: { createdAt: -1 } }); |
|
|
| if (!emailVerificationData) { |
| logger.warn(`[verifyEmail] [No email verification data found] [Email: ${decodedEmail}]`); |
| 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: ${decodedEmail}]`, |
| ); |
| return new Error('Invalid or expired email verification token'); |
| } |
|
|
| const updatedUser = await updateUser(emailVerificationData.userId, { emailVerified: true }); |
|
|
| if (!updatedUser) { |
| logger.warn(`[verifyEmail] [User update failed] [Email: ${decodedEmail}]`); |
| return new Error('Failed to update user verification status'); |
| } |
|
|
| await deleteTokens({ token: emailVerificationData.token }); |
| logger.info(`[verifyEmail] Email verification successful [Email: ${decodedEmail}]`); |
| return { message: 'Email verification was successful', status: 'success' }; |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| const registerUser = async (user, additionalData = {}) => { |
| 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, provider } = user; |
|
|
| let newUserId; |
| try { |
| const appConfig = await getAppConfig(); |
| if (!isEmailDomainAllowed(email, appConfig?.registration?.allowedDomains)) { |
| 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 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 }; |
| } |
|
|
| |
| const isFirstRegisteredUser = (await countUsers()) === 0; |
|
|
| const salt = bcrypt.genSaltSync(10); |
| const newUserData = { |
| provider: provider ?? 'local', |
| email, |
| username, |
| name, |
| avatar: null, |
| role: isFirstRegisteredUser ? SystemRoles.ADMIN : SystemRoles.USER, |
| password: bcrypt.hashSync(password, salt), |
| ...additionalData, |
| }; |
|
|
| const emailEnabled = checkEmailConfig(); |
| const disableTTL = isEnabled(process.env.ALLOW_UNVERIFIED_EMAIL_LOGIN); |
|
|
| const newUser = await createUser(newUserData, appConfig.balance, disableTTL, true); |
| newUserId = newUser._id; |
| if (emailEnabled && !newUser.emailVerified) { |
| 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 appConfig = await getAppConfig(); |
| if (!isEmailDomainAllowed(email, appConfig?.registration?.allowedDomains)) { |
| const error = new Error(ErrorTypes.AUTH_FAILED); |
| error.code = ErrorTypes.AUTH_FAILED; |
| error.message = 'Email domain not allowed'; |
| return error; |
| } |
| 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.', |
| }; |
| } |
|
|
| await deleteTokens({ userId: user._id }); |
|
|
| const [resetToken, hash] = createTokenHash(); |
|
|
| await createToken({ |
| userId: user._id, |
| token: hash, |
| createdAt: Date.now(), |
| expiresIn: 900, |
| }); |
|
|
| 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 || user.username || user.email, |
| 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 findToken( |
| { |
| userId, |
| }, |
| { sort: { createdAt: -1 } }, |
| ); |
|
|
| 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 || user.username || user.email, |
| year: new Date().getFullYear(), |
| }, |
| template: 'passwordReset.handlebars', |
| }); |
| } |
|
|
| await deleteTokens({ token: passwordResetToken.token }); |
| logger.info(`[resetPassword] Password reset successful. [Email: ${user.email}]`); |
| return { message: 'Password reset was successful' }; |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| const setAuthTokens = async (userId, res, _session = null) => { |
| try { |
| let session = _session; |
| let refreshToken; |
| let refreshTokenExpires; |
|
|
| if (session && session._id && session.expiration != null) { |
| refreshTokenExpires = session.expiration.getTime(); |
| refreshToken = await generateRefreshToken(session); |
| } else { |
| const result = await createSession(userId); |
| session = result.session; |
| refreshToken = result.refreshToken; |
| refreshTokenExpires = session.expiration.getTime(); |
| } |
|
|
| const user = await getUserById(userId); |
| const token = await generateToken(user); |
|
|
| res.cookie('refreshToken', refreshToken, { |
| expires: new Date(refreshTokenExpires), |
| httpOnly: true, |
| secure: isProduction, |
| sameSite: 'strict', |
| }); |
| res.cookie('token_provider', 'librechat', { |
| 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 setOpenIDAuthTokens = (tokenset, res, userId, existingRefreshToken) => { |
| try { |
| if (!tokenset) { |
| logger.error('[setOpenIDAuthTokens] No tokenset found in request'); |
| return; |
| } |
| const { REFRESH_TOKEN_EXPIRY } = process.env ?? {}; |
| const expiryInMilliseconds = REFRESH_TOKEN_EXPIRY |
| ? eval(REFRESH_TOKEN_EXPIRY) |
| : 1000 * 60 * 60 * 24 * 7; |
| const expirationDate = new Date(Date.now() + expiryInMilliseconds); |
| if (tokenset == null) { |
| logger.error('[setOpenIDAuthTokens] No tokenset found in request'); |
| return; |
| } |
| if (!tokenset.access_token) { |
| logger.error('[setOpenIDAuthTokens] No access token found in tokenset'); |
| return; |
| } |
|
|
| const refreshToken = tokenset.refresh_token || existingRefreshToken; |
|
|
| if (!refreshToken) { |
| logger.error('[setOpenIDAuthTokens] No refresh token available'); |
| return; |
| } |
|
|
| res.cookie('refreshToken', refreshToken, { |
| expires: expirationDate, |
| httpOnly: true, |
| secure: isProduction, |
| sameSite: 'strict', |
| }); |
| res.cookie('openid_access_token', tokenset.access_token, { |
| expires: expirationDate, |
| httpOnly: true, |
| secure: isProduction, |
| sameSite: 'strict', |
| }); |
| res.cookie('token_provider', 'openid', { |
| expires: expirationDate, |
| httpOnly: true, |
| secure: isProduction, |
| sameSite: 'strict', |
| }); |
| if (userId && isEnabled(process.env.OPENID_REUSE_TOKENS)) { |
| |
| const signedUserId = jwt.sign({ id: userId }, process.env.JWT_REFRESH_SECRET, { |
| expiresIn: expiryInMilliseconds / 1000, |
| }); |
| res.cookie('openid_user_id', signedUserId, { |
| expires: expirationDate, |
| httpOnly: true, |
| secure: isProduction, |
| sameSite: 'strict', |
| }); |
| } |
| return tokenset.access_token; |
| } catch (error) { |
| logger.error('[setOpenIDAuthTokens] Error in setting authentication tokens:', error); |
| throw error; |
| } |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| const resendVerificationEmail = async (req) => { |
| try { |
| const { email } = req.body; |
| await deleteTokens({ email }); |
| const user = await findUser({ email }, 'email _id name'); |
|
|
| if (!user) { |
| logger.warn(`[resendVerificationEmail] [No user found] [Email: ${email}]`); |
| return { status: 200, message: genericVerificationMessage }; |
| } |
|
|
| const [verifyToken, hash] = createTokenHash(); |
|
|
| 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 || user.username || user.email, |
| verificationLink: verificationLink, |
| year: new Date().getFullYear(), |
| }, |
| template: 'verifyEmail.handlebars', |
| }); |
|
|
| await createToken({ |
| userId: user._id, |
| email: user.email, |
| token: hash, |
| createdAt: Date.now(), |
| expiresIn: 900, |
| }); |
|
|
| 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, |
| setOpenIDAuthTokens, |
| requestPasswordReset, |
| resendVerificationEmail, |
| }; |
|
|