| | 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, |
| | }; |
| |
|