LibreChat/api/server/services/AuthService.js
nangelovv 6a04fb89e2
📬 fix: Honor Admin-Panel allowedDomains Override at Registration (#13204)
* fix: honor admin-panel allowedDomains override at registration

registerUser called getAppConfig({ baseOnly: true }), which short-
circuits before any DB override merge. As a result, admin-panel edits to
registration.allowedDomains were silently ignored at signup, even though
they correctly apply to SSO callbacks via checkDomainAllowed (which
calls getAppConfig() with the full resolution).

The admin panel writes registration.allowedDomains to the __base__
principal in the configs collection. That principal is unconditionally
injected by getApplicableConfigs (no user identity required), so a
fully-resolved getAppConfig call picks up the override even before any
user exists. This aligns native signup with the SSO paths and lets
admins tighten or relax the allowed list without a backend restart.

Per review feedback: pass the ALS tenantId explicitly. /api/auth runs
through preAuthTenantMiddleware, which puts a tenantId into
AsyncLocalStorage. Mongoose queries inside getApplicableConfigs are
ALS-scoped, but the per-principal merged-config cache key uses the
*explicit* tenantId parameter (see overrideCacheKey in
packages/api/src/app/service.ts). If we leave tenantId undefined while
ALS holds tenant A, the merged result caches at `__default__` — and a
later request from tenant B would hit that entry, leaking tenant A's
allowedDomains (and balance) across tenants. Reading getTenantId() and
forwarding it makes the cache key match the DB scope, so __base__
overrides apply per-tenant correctly.

Behavior when no admin override exists is unchanged (the merged config
equals the YAML config; optional chaining handles missing fields).

Tests in AuthService.spec.js:
- Regression guard that getAppConfig is called with `{}` (no baseOnly)
  when ALS has no tenant — protects against reintroduction of the
  short-circuit.
- New tenant-context test verifying getAppConfig({ tenantId }) when
  getTenantId() returns a tenant ID — protects against cross-tenant
  cache bleed.
- Behavioral test confirming a disallowed domain returns 403 before any
  DB user lookup.

* test: remove unused registerSchema import after merge resolution

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2026-05-30 10:52:05 -04:00

786 lines
24 KiB
JavaScript

const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const { webcrypto } = require('node:crypto');
const {
logger,
getTenantId,
DEFAULT_SESSION_EXPIRY,
DEFAULT_REFRESH_TOKEN_EXPIRY,
} = require('@librechat/data-schemas');
const { ErrorTypes, SystemRoles, errorsToString } = require('librechat-data-provider');
const {
math,
isEnabled,
checkEmailConfig,
setCloudFrontCookies,
getCloudFrontConfig,
parseCloudFrontCookieScope,
CLOUDFRONT_SCOPE_COOKIE,
isEmailDomainAllowed,
shouldUseSecureCookie,
resolveAppConfigForUser,
} = require('@librechat/api');
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 genericVerificationMessage = 'Please check your email to verify your email address.';
const OPENID_SESSION_ID_TOKEN_EXPIRY_BUFFER_SECONDS = 30;
const getUnexpiredOpenIDSessionIdToken = (idToken) => {
if (!idToken) {
return;
}
const decoded = jwt.decode(idToken);
const now = Math.floor(Date.now() / 1000);
if (
decoded &&
typeof decoded === 'object' &&
decoded.exp > now + OPENID_SESSION_ID_TOKEN_EXPIRY_BUFFER_SECONDS
) {
return idToken;
}
};
/**
* Logout user
*
* @param {ServerRequest} req
* @param {string} refreshToken
* @returns
*/
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 };
}
};
/**
* Creates Token and corresponding Hash for verification
* @returns {[string, string]}
*/
const createTokenHash = () => {
const token = Buffer.from(webcrypto.getRandomValues(new Uint8Array(32))).toString('hex');
const hash = bcrypt.hashSync(token, 10);
return [token, hash];
};
/**
* Send Verification Email
* @param {Partial<IUser>} user
* @returns {Promise<void>}
*/
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}]`);
};
/**
* Verify Email
* @param {ServerRequest} req
*/
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' };
};
/**
* Register a new user.
* @param {IUser} user <email, password, name, username>
* @param {Partial<IUser>} [additionalData={}] Trusted server-provided fields, such as CLI overrides.
* @returns {Promise<{status: number, message: string, user?: IUser}>}
*/
const registerUser = async (user, additionalData = {}) => {
const result = registerSchema.safeParse(user);
if (!result.success) {
const errorMessage = errorsToString(result.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 } = result.data;
const { provider, ...trustedAdditionalData } = additionalData ?? {};
let newUserId;
try {
const tenantId = getTenantId();
const appConfig = await getAppConfig(tenantId ? { tenantId } : {});
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 },
);
// Sleep for 1 second
await new Promise((resolve) => setTimeout(resolve, 1000));
return { status: 200, message: genericVerificationMessage };
}
//determine if this is the first registered user (not counting anonymous_user)
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),
...trustedAdditionalData,
};
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' };
}
};
/**
* Request password reset.
*
* Uses a two-phase domain check: fast-fail with the memory-cached base config
* (zero DB queries) to block globally denied domains before user lookup, then
* re-check with tenant-scoped config after user lookup so tenant-specific
* restrictions are enforced.
*
* Phase 1 (base check) returns an Error (HTTP 400) — this intentionally reveals
* that the domain is globally blocked, but fires before any DB lookup so it
* cannot confirm user existence. Phase 2 (tenant check) returns the generic
* success message (HTTP 200) to prevent user-enumeration via status codes.
*
* @param {ServerRequest} req
*/
const requestPasswordReset = async (req) => {
const { email } = req.body;
const baseConfig = await getAppConfig({ baseOnly: true });
if (!isEmailDomainAllowed(email, baseConfig?.registration?.allowedDomains)) {
logger.warn(
`[requestPasswordReset] Blocked - email domain not allowed [Email: ${email}] [IP: ${req.ip}]`,
);
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 role tenantId');
let appConfig = baseConfig;
if (user?.tenantId) {
try {
appConfig = await resolveAppConfigForUser(getAppConfig, user);
} catch (err) {
logger.error('[requestPasswordReset] Failed to resolve tenant config, using base:', err);
}
}
if (!isEmailDomainAllowed(email, appConfig?.registration?.allowedDomains)) {
logger.warn(
`[requestPasswordReset] Tenant config blocked domain [Email: ${email}] [IP: ${req.ip}]`,
);
return {
message: 'If an account with that email exists, a password reset link has been sent to it.',
};
}
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.',
};
};
/**
* Reset Password
*
* @param {*} userId
* @param {String} token
* @param {String} password
* @returns
*/
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' };
};
/**
* Reads the previously issued CloudFront cookie scope used for stale cookie cleanup.
* @param {ServerRequest | null} [req=null]
* @returns {import('@librechat/api').CloudFrontCookieScope | null}
*/
const getPreviousCloudFrontScope = (req) =>
parseCloudFrontCookieScope(req?.cookies?.[CLOUDFRONT_SCOPE_COOKIE]);
const normalizeCloudFrontScopeValue = (value) => (value == null ? undefined : String(value));
const getCloudFrontScopeValue = (optionsValue, userValue, requestValue) =>
normalizeCloudFrontScopeValue(optionsValue ?? userValue ?? requestValue);
const getCloudFrontAuthCookieSkipReason = (scope) => {
const config = getCloudFrontConfig();
if (!config || config.imageSigning !== 'cookies' || !config.privateKey || !config.keyPairId) {
return 'cloudfront_disabled';
}
if (!config.cookieDomain) {
return 'missing_cookie_domain';
}
if (!scope.userId) {
return 'missing_user_id';
}
return null;
};
const shouldLogCloudFrontAuthCookieSkip = (reason) => reason !== 'cloudfront_disabled';
/**
* Refreshes CloudFront signed cookies for authenticated image/avatar access.
* @param {ServerRequest | null} req
* @param {ServerResponse} res
* @param {Partial<IUser> | null} user
* @param {import('@librechat/api').CloudFrontCookieScope & { orgId?: string }} [options={}]
* @returns {boolean}
*/
const setCloudFrontAuthCookies = (req, res, user, options = {}) => {
const storageRegion = getCloudFrontScopeValue(
options.storageRegion,
user?.storageRegion,
req?.user?.storageRegion,
);
const scope = {
userId: getCloudFrontScopeValue(
options.userId,
user?._id ?? user?.id,
req?.user?._id ?? req?.user?.id,
),
tenantId: getCloudFrontScopeValue(
options.tenantId ?? options.orgId,
user?.tenantId ?? user?.orgId,
req?.user?.tenantId ?? req?.user?.orgId,
),
...(storageRegion ? { storageRegion } : {}),
};
const skipReason = getCloudFrontAuthCookieSkipReason(scope);
if (skipReason) {
if (shouldLogCloudFrontAuthCookieSkip(skipReason)) {
logger.debug('[setCloudFrontAuthCookies] CloudFront auth cookies skipped', {
attempted: false,
set: false,
reason: skipReason,
has_user_id: Boolean(scope.userId),
has_tenant_scope: Boolean(scope.tenantId),
has_storage_region: Boolean(scope.storageRegion),
has_previous_scope: Boolean(getPreviousCloudFrontScope(req)?.userId),
});
}
return false;
}
const previousScope = getPreviousCloudFrontScope(req);
const cookiesSet = setCloudFrontCookies(res, scope, previousScope);
logger.debug('[setCloudFrontAuthCookies] CloudFront auth cookies refreshed', {
attempted: true,
set: cookiesSet,
reason: cookiesSet ? undefined : 'set_failed',
has_user_id: true,
has_tenant_scope: Boolean(scope.tenantId),
has_storage_region: Boolean(scope.storageRegion),
has_previous_scope: Boolean(previousScope?.userId),
});
return cookiesSet;
};
/**
* Set Auth Tokens
* @param {String | ObjectId} userId
* @param {ServerResponse} res
* @param {ISession | null} [_session=null]
* @param {ServerRequest | null} [req=null]
* @returns
*/
const setAuthTokens = async (userId, res, _session = null, req = null) => {
try {
let session = _session;
let refreshToken;
let refreshTokenExpires;
const expiresIn = math(process.env.REFRESH_TOKEN_EXPIRY, DEFAULT_REFRESH_TOKEN_EXPIRY);
if (session && session._id && session.expiration != null) {
refreshTokenExpires = session.expiration.getTime();
refreshToken = await generateRefreshToken(session);
} else {
const result = await createSession(userId, { expiresIn });
session = result.session;
refreshToken = result.refreshToken;
refreshTokenExpires = session.expiration.getTime();
}
const user = await getUserById(userId);
const sessionExpiry = math(process.env.SESSION_EXPIRY, DEFAULT_SESSION_EXPIRY);
const token = await generateToken(user, sessionExpiry);
res.cookie('refreshToken', refreshToken, {
expires: new Date(refreshTokenExpires),
httpOnly: true,
secure: shouldUseSecureCookie(),
sameSite: 'strict',
});
res.cookie('token_provider', 'librechat', {
expires: new Date(refreshTokenExpires),
httpOnly: true,
secure: shouldUseSecureCookie(),
sameSite: 'strict',
});
setCloudFrontAuthCookies(req, res, user, { userId: user?._id ?? userId });
return token;
} catch (error) {
logger.error('[setAuthTokens] Error in setting authentication tokens:', error);
throw error;
}
};
const resolveOpenIDAuthTokenOptions = (optionsOrUserId, existingRefreshToken, tenantId) => {
if (optionsOrUserId != null && typeof optionsOrUserId === 'object') {
if (
'userId' in optionsOrUserId ||
'existingRefreshToken' in optionsOrUserId ||
'tenantId' in optionsOrUserId
) {
return optionsOrUserId;
}
return {};
}
return { userId: optionsOrUserId, existingRefreshToken, tenantId };
};
/**
* @function setOpenIDAuthTokens
* Set OpenID Authentication Tokens
* Stores tokens server-side in express-session to avoid large cookie sizes
* that can exceed HTTP/2 header limits (especially for users with many group memberships).
*
* @param {import('openid-client').TokenEndpointResponse & import('openid-client').TokenEndpointResponseHelpers} tokenset
* - The tokenset object containing access and refresh tokens
* @param {Object} req - request object (for session access)
* @param {Object} res - response object
* @param {Object} [options] - Optional token/cookie context
* @param {string} [options.userId] - Optional MongoDB user ID for image path validation
* @param {string} [options.existingRefreshToken] - Optional existing refresh token to preserve
* @param {string} [options.tenantId] - Optional tenant identifier for CloudFront cookie scoping
* @returns {String} - id_token (preferred) or access_token as the app auth token
*/
const setOpenIDAuthTokens = (
tokenset,
req,
res,
optionsOrUserId = null,
existingRefreshTokenArg,
tenantIdArg,
) => {
try {
const { userId, existingRefreshToken, tenantId } = resolveOpenIDAuthTokenOptions(
optionsOrUserId,
existingRefreshTokenArg,
tenantIdArg,
);
if (!tokenset) {
logger.error('[setOpenIDAuthTokens] No tokenset found in request');
return;
}
const expiryInMilliseconds = math(
process.env.REFRESH_TOKEN_EXPIRY,
DEFAULT_REFRESH_TOKEN_EXPIRY,
);
const expirationDate = new Date(Date.now() + expiryInMilliseconds);
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;
}
/**
* Use id_token as the app authentication token (Bearer token for JWKS validation).
* The id_token is always a standard JWT signed by the IdP's JWKS keys with the app's
* client_id as audience. The access_token may be opaque or intended for a different
* audience (e.g., Microsoft Graph API), which fails JWKS validation.
* Falls back to access_token for providers where id_token is not available.
*/
const sessionIdToken = req.session?.openidTokens?.idToken;
const appAuthToken =
tokenset.id_token ||
getUnexpiredOpenIDSessionIdToken(sessionIdToken) ||
tokenset.access_token;
const logoutIdToken = tokenset.id_token || sessionIdToken;
/**
* Always set refresh token cookie so it survives express session expiry.
* The session cookie maxAge (SESSION_EXPIRY, default 15 min) is typically shorter
* than the OIDC token lifetime (~1 hour). Without this cookie fallback, the refresh
* token stored only in the session is lost when the session expires, causing the user
* to be signed out on the next token refresh attempt.
* The refresh token is small (opaque string) so it doesn't hit the HTTP/2 header
* size limits that motivated session storage for the larger access_token/id_token.
*/
res.cookie('refreshToken', refreshToken, {
expires: expirationDate,
httpOnly: true,
secure: shouldUseSecureCookie(),
sameSite: 'strict',
});
/** Store tokens server-side in session to avoid large cookies */
if (req.session) {
req.session.openidTokens = {
accessToken: tokenset.access_token,
idToken: logoutIdToken,
refreshToken: refreshToken,
expiresAt: expirationDate.getTime(),
lastRefreshedAt: Date.now(),
};
} else {
logger.warn('[setOpenIDAuthTokens] No session available, falling back to cookies');
res.cookie('openid_access_token', tokenset.access_token, {
expires: expirationDate,
httpOnly: true,
secure: shouldUseSecureCookie(),
sameSite: 'strict',
});
if (tokenset.id_token) {
res.cookie('openid_id_token', tokenset.id_token, {
expires: expirationDate,
httpOnly: true,
secure: shouldUseSecureCookie(),
sameSite: 'strict',
});
}
}
/** Small cookie to indicate token provider (required for auth middleware) */
res.cookie('token_provider', 'openid', {
expires: expirationDate,
httpOnly: true,
secure: shouldUseSecureCookie(),
sameSite: 'strict',
});
if (userId && isEnabled(process.env.OPENID_REUSE_TOKENS)) {
/** JWT-signed user ID cookie for image path validation when OPENID_REUSE_TOKENS is enabled */
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: shouldUseSecureCookie(),
sameSite: 'strict',
});
}
setCloudFrontAuthCookies(req, res, req.user, { userId, tenantId });
return appAuthToken;
} catch (error) {
logger.error('[setOpenIDAuthTokens] Error in setting authentication tokens:', error);
throw error;
}
};
/**
* Resend Verification Email
* @param {Object} req
* @param {Object} req.body
* @param {String} req.body.email
* @returns {Promise<{status: number, message: string}>}
*/
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,
setCloudFrontAuthCookies,
requestPasswordReset,
resendVerificationEmail,
};