mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 08:12:00 +02:00
🔒 fix: resolve session persistence post password reset (#5077)
* ✨ feat: Implement session management with CRUD operations and integrate into user workflows * ✨ refactor: Update session model import paths and enhance session creation logic in AuthService * ✨ refactor: Validate session and user ID formats in session management functions * ✨ style: Enhance UI components with improved styling and accessibility features * chore: Update login form tests to use getByTestId instead of getByRole, remove console.log() * chore: Update login form tests to use getByTestId instead of getByRole --------- Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
9bca2ae953
commit
bdb222d5f4
17 changed files with 402 additions and 116 deletions
4
api/cache/banViolation.js
vendored
4
api/cache/banViolation.js
vendored
|
@ -1,7 +1,7 @@
|
||||||
const { ViolationTypes } = require('librechat-data-provider');
|
const { ViolationTypes } = require('librechat-data-provider');
|
||||||
const { isEnabled, math, removePorts } = require('~/server/utils');
|
const { isEnabled, math, removePorts } = require('~/server/utils');
|
||||||
|
const { deleteAllUserSessions } = require('~/models');
|
||||||
const getLogStores = require('./getLogStores');
|
const getLogStores = require('./getLogStores');
|
||||||
const Session = require('~/models/Session');
|
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
const { BAN_VIOLATIONS, BAN_INTERVAL } = process.env ?? {};
|
const { BAN_VIOLATIONS, BAN_INTERVAL } = process.env ?? {};
|
||||||
|
@ -46,7 +46,7 @@ const banViolation = async (req, res, errorMessage) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Session.deleteAllUserSessions(user_id);
|
await deleteAllUserSessions({ userId: user_id });
|
||||||
res.clearCookie('refreshToken');
|
res.clearCookie('refreshToken');
|
||||||
|
|
||||||
const banLogs = getLogStores(ViolationTypes.BAN);
|
const banLogs = getLogStores(ViolationTypes.BAN);
|
||||||
|
|
|
@ -1,75 +1,275 @@
|
||||||
const mongoose = require('mongoose');
|
const mongoose = require('mongoose');
|
||||||
const signPayload = require('~/server/services/signPayload');
|
const signPayload = require('~/server/services/signPayload');
|
||||||
const { hashToken } = require('~/server/utils/crypto');
|
const { hashToken } = require('~/server/utils/crypto');
|
||||||
|
const sessionSchema = require('./schema/session');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
|
const Session = mongoose.model('Session', sessionSchema);
|
||||||
|
|
||||||
const { REFRESH_TOKEN_EXPIRY } = process.env ?? {};
|
const { REFRESH_TOKEN_EXPIRY } = process.env ?? {};
|
||||||
const expires = eval(REFRESH_TOKEN_EXPIRY) ?? 1000 * 60 * 60 * 24 * 7;
|
const expires = eval(REFRESH_TOKEN_EXPIRY) ?? 1000 * 60 * 60 * 24 * 7; // 7 days default
|
||||||
|
|
||||||
const sessionSchema = mongoose.Schema({
|
/**
|
||||||
refreshTokenHash: {
|
* Error class for Session-related errors
|
||||||
type: String,
|
*/
|
||||||
required: true,
|
class SessionError extends Error {
|
||||||
},
|
constructor(message, code = 'SESSION_ERROR') {
|
||||||
expiration: {
|
super(message);
|
||||||
type: Date,
|
this.name = 'SessionError';
|
||||||
required: true,
|
this.code = code;
|
||||||
expires: 0,
|
}
|
||||||
},
|
}
|
||||||
user: {
|
|
||||||
type: mongoose.Schema.Types.ObjectId,
|
/**
|
||||||
ref: 'User',
|
* Creates a new session for a user
|
||||||
required: true,
|
* @param {string} userId - The ID of the user
|
||||||
},
|
* @param {Object} options - Additional options for session creation
|
||||||
});
|
* @param {Date} options.expiration - Custom expiration date
|
||||||
|
* @returns {Promise<{session: Session, refreshToken: string}>}
|
||||||
|
* @throws {SessionError}
|
||||||
|
*/
|
||||||
|
const createSession = async (userId, options = {}) => {
|
||||||
|
if (!userId) {
|
||||||
|
throw new SessionError('User ID is required', 'INVALID_USER_ID');
|
||||||
|
}
|
||||||
|
|
||||||
sessionSchema.methods.generateRefreshToken = async function () {
|
|
||||||
try {
|
try {
|
||||||
let expiresIn;
|
const session = new Session({
|
||||||
if (this.expiration) {
|
user: userId,
|
||||||
expiresIn = this.expiration.getTime();
|
expiration: options.expiration || new Date(Date.now() + expires),
|
||||||
} else {
|
});
|
||||||
expiresIn = Date.now() + expires;
|
const refreshToken = await generateRefreshToken(session);
|
||||||
this.expiration = new Date(expiresIn);
|
return { session, refreshToken };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[createSession] Error creating session:', error);
|
||||||
|
throw new SessionError('Failed to create session', 'CREATE_SESSION_FAILED');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a session by various parameters
|
||||||
|
* @param {Object} params - Search parameters
|
||||||
|
* @param {string} [params.refreshToken] - The refresh token to search by
|
||||||
|
* @param {string} [params.userId] - The user ID to search by
|
||||||
|
* @param {string} [params.sessionId] - The session ID to search by
|
||||||
|
* @param {Object} [options] - Additional options
|
||||||
|
* @param {boolean} [options.lean=true] - Whether to return plain objects instead of documents
|
||||||
|
* @returns {Promise<Session|null>}
|
||||||
|
* @throws {SessionError}
|
||||||
|
*/
|
||||||
|
const findSession = async (params, options = { lean: true }) => {
|
||||||
|
try {
|
||||||
|
const query = {};
|
||||||
|
|
||||||
|
if (!params.refreshToken && !params.userId && !params.sessionId) {
|
||||||
|
throw new SessionError('At least one search parameter is required', 'INVALID_SEARCH_PARAMS');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.refreshToken) {
|
||||||
|
const tokenHash = await hashToken(params.refreshToken);
|
||||||
|
query.refreshTokenHash = tokenHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.userId) {
|
||||||
|
query.user = params.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.sessionId) {
|
||||||
|
const sessionId = params.sessionId.sessionId || params.sessionId;
|
||||||
|
if (!mongoose.Types.ObjectId.isValid(sessionId)) {
|
||||||
|
throw new SessionError('Invalid session ID format', 'INVALID_SESSION_ID');
|
||||||
|
}
|
||||||
|
query._id = sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add expiration check to only return valid sessions
|
||||||
|
query.expiration = { $gt: new Date() };
|
||||||
|
|
||||||
|
const sessionQuery = Session.findOne(query);
|
||||||
|
|
||||||
|
if (options.lean) {
|
||||||
|
return await sessionQuery.lean();
|
||||||
|
}
|
||||||
|
|
||||||
|
return await sessionQuery.exec();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[findSession] Error finding session:', error);
|
||||||
|
throw new SessionError('Failed to find session', 'FIND_SESSION_FAILED');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates session expiration
|
||||||
|
* @param {Session|string} session - The session or session ID to update
|
||||||
|
* @param {Date} [newExpiration] - Optional new expiration date
|
||||||
|
* @returns {Promise<Session>}
|
||||||
|
* @throws {SessionError}
|
||||||
|
*/
|
||||||
|
const updateExpiration = async (session, newExpiration) => {
|
||||||
|
try {
|
||||||
|
const sessionDoc = typeof session === 'string' ? await Session.findById(session) : session;
|
||||||
|
|
||||||
|
if (!sessionDoc) {
|
||||||
|
throw new SessionError('Session not found', 'SESSION_NOT_FOUND');
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionDoc.expiration = newExpiration || new Date(Date.now() + expires);
|
||||||
|
return await sessionDoc.save();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[updateExpiration] Error updating session:', error);
|
||||||
|
throw new SessionError('Failed to update session expiration', 'UPDATE_EXPIRATION_FAILED');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a session by refresh token or session ID
|
||||||
|
* @param {Object} params - Delete parameters
|
||||||
|
* @param {string} [params.refreshToken] - The refresh token of the session to delete
|
||||||
|
* @param {string} [params.sessionId] - The ID of the session to delete
|
||||||
|
* @returns {Promise<Object>}
|
||||||
|
* @throws {SessionError}
|
||||||
|
*/
|
||||||
|
const deleteSession = async (params) => {
|
||||||
|
try {
|
||||||
|
if (!params.refreshToken && !params.sessionId) {
|
||||||
|
throw new SessionError(
|
||||||
|
'Either refreshToken or sessionId is required',
|
||||||
|
'INVALID_DELETE_PARAMS',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = {};
|
||||||
|
|
||||||
|
if (params.refreshToken) {
|
||||||
|
query.refreshTokenHash = await hashToken(params.refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.sessionId) {
|
||||||
|
query._id = params.sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await Session.deleteOne(query);
|
||||||
|
|
||||||
|
if (result.deletedCount === 0) {
|
||||||
|
logger.warn('[deleteSession] No session found to delete');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[deleteSession] Error deleting session:', error);
|
||||||
|
throw new SessionError('Failed to delete session', 'DELETE_SESSION_FAILED');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes all sessions for a user
|
||||||
|
* @param {string} userId - The ID of the user
|
||||||
|
* @param {Object} [options] - Additional options
|
||||||
|
* @param {boolean} [options.excludeCurrentSession] - Whether to exclude the current session
|
||||||
|
* @param {string} [options.currentSessionId] - The ID of the current session to exclude
|
||||||
|
* @returns {Promise<Object>}
|
||||||
|
* @throws {SessionError}
|
||||||
|
*/
|
||||||
|
const deleteAllUserSessions = async (userId, options = {}) => {
|
||||||
|
try {
|
||||||
|
if (!userId) {
|
||||||
|
throw new SessionError('User ID is required', 'INVALID_USER_ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract userId if it's passed as an object
|
||||||
|
const userIdString = userId.userId || userId;
|
||||||
|
|
||||||
|
if (!mongoose.Types.ObjectId.isValid(userIdString)) {
|
||||||
|
throw new SessionError('Invalid user ID format', 'INVALID_USER_ID_FORMAT');
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = { user: userIdString };
|
||||||
|
|
||||||
|
if (options.excludeCurrentSession && options.currentSessionId) {
|
||||||
|
query._id = { $ne: options.currentSessionId };
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await Session.deleteMany(query);
|
||||||
|
|
||||||
|
if (result.deletedCount > 0) {
|
||||||
|
logger.debug(
|
||||||
|
`[deleteAllUserSessions] Deleted ${result.deletedCount} sessions for user ${userIdString}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[deleteAllUserSessions] Error deleting user sessions:', error);
|
||||||
|
throw new SessionError('Failed to delete user sessions', 'DELETE_ALL_SESSIONS_FAILED');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a refresh token for a session
|
||||||
|
* @param {Session} session - The session to generate a token for
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
* @throws {SessionError}
|
||||||
|
*/
|
||||||
|
const generateRefreshToken = async (session) => {
|
||||||
|
if (!session || !session.user) {
|
||||||
|
throw new SessionError('Invalid session object', 'INVALID_SESSION');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const expiresIn = session.expiration ? session.expiration.getTime() : Date.now() + expires;
|
||||||
|
|
||||||
|
if (!session.expiration) {
|
||||||
|
session.expiration = new Date(expiresIn);
|
||||||
}
|
}
|
||||||
|
|
||||||
const refreshToken = await signPayload({
|
const refreshToken = await signPayload({
|
||||||
payload: { id: this.user },
|
payload: {
|
||||||
|
id: session.user,
|
||||||
|
sessionId: session._id,
|
||||||
|
},
|
||||||
secret: process.env.JWT_REFRESH_SECRET,
|
secret: process.env.JWT_REFRESH_SECRET,
|
||||||
expirationTime: Math.floor((expiresIn - Date.now()) / 1000),
|
expirationTime: Math.floor((expiresIn - Date.now()) / 1000),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.refreshTokenHash = await hashToken(refreshToken);
|
session.refreshTokenHash = await hashToken(refreshToken);
|
||||||
|
await session.save();
|
||||||
await this.save();
|
|
||||||
|
|
||||||
return refreshToken;
|
return refreshToken;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error('[generateRefreshToken] Error generating refresh token:', error);
|
||||||
'Error generating refresh token. Is a `JWT_REFRESH_SECRET` set in the .env file?\n\n',
|
throw new SessionError('Failed to generate refresh token', 'GENERATE_TOKEN_FAILED');
|
||||||
error,
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
sessionSchema.statics.deleteAllUserSessions = async function (userId) {
|
/**
|
||||||
|
* Counts active sessions for a user
|
||||||
|
* @param {string} userId - The ID of the user
|
||||||
|
* @returns {Promise<number>}
|
||||||
|
* @throws {SessionError}
|
||||||
|
*/
|
||||||
|
const countActiveSessions = async (userId) => {
|
||||||
try {
|
try {
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return;
|
throw new SessionError('User ID is required', 'INVALID_USER_ID');
|
||||||
}
|
|
||||||
const result = await this.deleteMany({ user: userId });
|
|
||||||
if (result && result?.deletedCount > 0) {
|
|
||||||
logger.debug(
|
|
||||||
`[deleteAllUserSessions] Deleted ${result.deletedCount} sessions for user ${userId}.`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return await Session.countDocuments({
|
||||||
|
user: userId,
|
||||||
|
expiration: { $gt: new Date() },
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[deleteAllUserSessions] Error in deleting user sessions:', error);
|
logger.error('[countActiveSessions] Error counting active sessions:', error);
|
||||||
throw error;
|
throw new SessionError('Failed to count active sessions', 'COUNT_SESSIONS_FAILED');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const Session = mongoose.model('Session', sessionSchema);
|
module.exports = {
|
||||||
|
createSession,
|
||||||
module.exports = Session;
|
findSession,
|
||||||
|
updateExpiration,
|
||||||
|
deleteSession,
|
||||||
|
deleteAllUserSessions,
|
||||||
|
generateRefreshToken,
|
||||||
|
countActiveSessions,
|
||||||
|
SessionError,
|
||||||
|
};
|
||||||
|
|
|
@ -26,10 +26,18 @@ const {
|
||||||
deleteMessagesSince,
|
deleteMessagesSince,
|
||||||
deleteMessages,
|
deleteMessages,
|
||||||
} = require('./Message');
|
} = require('./Message');
|
||||||
|
const {
|
||||||
|
createSession,
|
||||||
|
findSession,
|
||||||
|
updateExpiration,
|
||||||
|
deleteSession,
|
||||||
|
deleteAllUserSessions,
|
||||||
|
generateRefreshToken,
|
||||||
|
countActiveSessions,
|
||||||
|
} = require('./Session');
|
||||||
const { getConvoTitle, getConvo, saveConvo, deleteConvos } = require('./Conversation');
|
const { getConvoTitle, getConvo, saveConvo, deleteConvos } = require('./Conversation');
|
||||||
const { getPreset, getPresets, savePreset, deletePresets } = require('./Preset');
|
const { getPreset, getPresets, savePreset, deletePresets } = require('./Preset');
|
||||||
const { createToken, findToken, updateToken, deleteTokens } = require('./Token');
|
const { createToken, findToken, updateToken, deleteTokens } = require('./Token');
|
||||||
const Session = require('./Session');
|
|
||||||
const Balance = require('./Balance');
|
const Balance = require('./Balance');
|
||||||
const User = require('./User');
|
const User = require('./User');
|
||||||
const Key = require('./Key');
|
const Key = require('./Key');
|
||||||
|
@ -75,8 +83,15 @@ module.exports = {
|
||||||
updateToken,
|
updateToken,
|
||||||
deleteTokens,
|
deleteTokens,
|
||||||
|
|
||||||
|
createSession,
|
||||||
|
findSession,
|
||||||
|
updateExpiration,
|
||||||
|
deleteSession,
|
||||||
|
deleteAllUserSessions,
|
||||||
|
generateRefreshToken,
|
||||||
|
countActiveSessions,
|
||||||
|
|
||||||
User,
|
User,
|
||||||
Key,
|
Key,
|
||||||
Session,
|
|
||||||
Balance,
|
Balance,
|
||||||
};
|
};
|
||||||
|
|
20
api/models/schema/session.js
Normal file
20
api/models/schema/session.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
const sessionSchema = mongoose.Schema({
|
||||||
|
refreshTokenHash: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
expiration: {
|
||||||
|
type: Date,
|
||||||
|
required: true,
|
||||||
|
expires: 0,
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = sessionSchema;
|
|
@ -6,8 +6,8 @@ const {
|
||||||
setAuthTokens,
|
setAuthTokens,
|
||||||
requestPasswordReset,
|
requestPasswordReset,
|
||||||
} = require('~/server/services/AuthService');
|
} = require('~/server/services/AuthService');
|
||||||
|
const { findSession, getUserById, deleteAllUserSessions } = require('~/models');
|
||||||
const { hashToken } = require('~/server/utils/crypto');
|
const { hashToken } = require('~/server/utils/crypto');
|
||||||
const { Session, getUserById } = require('~/models');
|
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
const registrationController = async (req, res) => {
|
const registrationController = async (req, res) => {
|
||||||
|
@ -45,6 +45,7 @@ const resetPasswordController = async (req, res) => {
|
||||||
if (resetPasswordService instanceof Error) {
|
if (resetPasswordService instanceof Error) {
|
||||||
return res.status(400).json(resetPasswordService);
|
return res.status(400).json(resetPasswordService);
|
||||||
} else {
|
} else {
|
||||||
|
await deleteAllUserSessions({ userId: req.body.userId });
|
||||||
return res.status(200).json(resetPasswordService);
|
return res.status(200).json(resetPasswordService);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -77,7 +78,7 @@ const refreshController = async (req, res) => {
|
||||||
const hashedToken = await hashToken(refreshToken);
|
const hashedToken = await hashToken(refreshToken);
|
||||||
|
|
||||||
// Find the session with the hashed refresh token
|
// Find the session with the hashed refresh token
|
||||||
const session = await Session.findOne({ user: userId, refreshTokenHash: hashedToken });
|
const session = await findSession({ userId: userId, refreshToken: hashedToken });
|
||||||
if (session && session.expiration > new Date()) {
|
if (session && session.expiration > new Date()) {
|
||||||
const token = await setAuthTokens(userId, res, session._id);
|
const token = await setAuthTokens(userId, res, session._id);
|
||||||
res.status(200).send({ token, user });
|
res.status(200).send({ token, user });
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
const {
|
const {
|
||||||
Session,
|
|
||||||
Balance,
|
Balance,
|
||||||
getFiles,
|
getFiles,
|
||||||
deleteFiles,
|
deleteFiles,
|
||||||
|
@ -7,6 +6,7 @@ const {
|
||||||
deletePresets,
|
deletePresets,
|
||||||
deleteMessages,
|
deleteMessages,
|
||||||
deleteUserById,
|
deleteUserById,
|
||||||
|
deleteAllUserSessions,
|
||||||
} = require('~/models');
|
} = require('~/models');
|
||||||
const User = require('~/models/User');
|
const User = require('~/models/User');
|
||||||
const { updateUserPluginAuth, deleteUserPluginAuth } = require('~/server/services/PluginService');
|
const { updateUserPluginAuth, deleteUserPluginAuth } = require('~/server/services/PluginService');
|
||||||
|
@ -112,7 +112,7 @@ const deleteUserController = async (req, res) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await deleteMessages({ user: user.id }); // delete user messages
|
await deleteMessages({ user: user.id }); // delete user messages
|
||||||
await Session.deleteMany({ user: user.id }); // delete user sessions
|
await deleteAllUserSessions({ userId: user.id }); // delete user sessions
|
||||||
await Transaction.deleteMany({ user: user.id }); // delete user transactions
|
await Transaction.deleteMany({ user: user.id }); // delete user transactions
|
||||||
await deleteUserKey({ userId: user.id, all: true }); // delete user keys
|
await deleteUserKey({ userId: user.id, all: true }); // delete user keys
|
||||||
await Balance.deleteMany({ user: user._id }); // delete user balances
|
await Balance.deleteMany({ user: user._id }); // delete user balances
|
||||||
|
|
|
@ -10,7 +10,15 @@ const {
|
||||||
generateToken,
|
generateToken,
|
||||||
deleteUserById,
|
deleteUserById,
|
||||||
} = require('~/models/userMethods');
|
} = require('~/models/userMethods');
|
||||||
const { createToken, findToken, deleteTokens, Session } = require('~/models');
|
const {
|
||||||
|
createToken,
|
||||||
|
findToken,
|
||||||
|
deleteTokens,
|
||||||
|
findSession,
|
||||||
|
deleteSession,
|
||||||
|
createSession,
|
||||||
|
generateRefreshToken,
|
||||||
|
} = require('~/models');
|
||||||
const { isEnabled, checkEmailConfig, sendEmail } = require('~/server/utils');
|
const { isEnabled, checkEmailConfig, sendEmail } = require('~/server/utils');
|
||||||
const { isEmailDomainAllowed } = require('~/server/services/domains');
|
const { isEmailDomainAllowed } = require('~/server/services/domains');
|
||||||
const { registerSchema } = require('~/strategies/validators');
|
const { registerSchema } = require('~/strategies/validators');
|
||||||
|
@ -37,10 +45,11 @@ const logoutUser = async (userId, refreshToken) => {
|
||||||
const hash = await hashToken(refreshToken);
|
const hash = await hashToken(refreshToken);
|
||||||
|
|
||||||
// Find the session with the matching user and refreshTokenHash
|
// Find the session with the matching user and refreshTokenHash
|
||||||
const session = await Session.findOne({ user: userId, refreshTokenHash: hash });
|
const session = await findSession({ userId: userId, refreshToken: hash });
|
||||||
|
|
||||||
if (session) {
|
if (session) {
|
||||||
try {
|
try {
|
||||||
await Session.deleteOne({ _id: session._id });
|
await deleteSession({ sessionId: session._id });
|
||||||
} catch (deleteErr) {
|
} catch (deleteErr) {
|
||||||
logger.error('[logoutUser] Failed to delete session.', deleteErr);
|
logger.error('[logoutUser] Failed to delete session.', deleteErr);
|
||||||
return { status: 500, message: 'Failed to delete session.' };
|
return { status: 500, message: 'Failed to delete session.' };
|
||||||
|
@ -330,18 +339,19 @@ const setAuthTokens = async (userId, res, sessionId = null) => {
|
||||||
const token = await generateToken(user);
|
const token = await generateToken(user);
|
||||||
|
|
||||||
let session;
|
let session;
|
||||||
|
let refreshToken;
|
||||||
let refreshTokenExpires;
|
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();
|
if (sessionId) {
|
||||||
|
session = await findSession({ sessionId: sessionId });
|
||||||
|
refreshTokenExpires = session.expiration.getTime();
|
||||||
|
refreshToken = await generateRefreshToken(session);
|
||||||
|
} else {
|
||||||
|
const result = await createSession(userId);
|
||||||
|
session = result.session;
|
||||||
|
refreshToken = result.refreshToken;
|
||||||
|
refreshTokenExpires = session.expiration.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
res.cookie('refreshToken', refreshToken, {
|
res.cookie('refreshToken', refreshToken, {
|
||||||
expires: new Date(refreshTokenExpires),
|
expires: new Date(refreshTokenExpires),
|
||||||
|
|
|
@ -2,7 +2,7 @@ export const ErrorMessage = ({ children }: { children: React.ReactNode }) => (
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
aria-live="assertive"
|
aria-live="assertive"
|
||||||
className="rounded-md border border-red-500 bg-red-500/10 px-3 py-2 text-sm text-gray-600 dark:text-gray-200"
|
className="relative mt-6 rounded-lg border border-red-500/20 bg-red-50/50 px-6 py-4 text-red-700 shadow-sm transition-all dark:bg-red-950/30 dark:text-red-100"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -26,7 +26,10 @@ function Login() {
|
||||||
<p className="my-4 text-center text-sm font-light text-gray-700 dark:text-white">
|
<p className="my-4 text-center text-sm font-light text-gray-700 dark:text-white">
|
||||||
{' '}
|
{' '}
|
||||||
{localize('com_auth_no_account')}{' '}
|
{localize('com_auth_no_account')}{' '}
|
||||||
<a href="/register" className="p-1 text-green-500">
|
<a
|
||||||
|
href="/register"
|
||||||
|
className="inline-flex p-1 text-sm font-medium text-green-600 transition-colors hover:text-green-700 dark:text-green-400 dark:hover:text-green-300"
|
||||||
|
>
|
||||||
{localize('com_auth_sign_up')}
|
{localize('com_auth_sign_up')}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -153,16 +153,24 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error,
|
||||||
{renderError('password')}
|
{renderError('password')}
|
||||||
</div>
|
</div>
|
||||||
{startupConfig.passwordResetEnabled && (
|
{startupConfig.passwordResetEnabled && (
|
||||||
<a href="/forgot-password" className="text-sm text-green-500">
|
<a
|
||||||
|
href="/forgot-password"
|
||||||
|
className="inline-flex p-1 text-sm font-medium text-green-600 transition-colors hover:text-green-700 dark:text-green-400 dark:hover:text-green-300"
|
||||||
|
>
|
||||||
{localize('com_auth_password_forgot')}
|
{localize('com_auth_password_forgot')}
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<button
|
<button
|
||||||
aria-label="Sign in"
|
aria-label={localize('com_auth_continue')}
|
||||||
data-testid="login-button"
|
data-testid="login-button"
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn-primary w-full transform rounded-2xl px-4 py-3 tracking-wide transition-colors duration-200"
|
className="
|
||||||
|
w-full rounded-2xl bg-green-600 px-4 py-3 text-sm font-medium text-white
|
||||||
|
transition-colors hover:bg-green-700 focus:outline-none focus:ring-2
|
||||||
|
focus:ring-green-500 focus:ring-offset-2 disabled:opacity-50
|
||||||
|
disabled:hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700
|
||||||
|
"
|
||||||
>
|
>
|
||||||
{localize('com_auth_continue')}
|
{localize('com_auth_continue')}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -183,7 +183,12 @@ const Registration: React.FC = () => {
|
||||||
disabled={Object.keys(errors).length > 0}
|
disabled={Object.keys(errors).length > 0}
|
||||||
type="submit"
|
type="submit"
|
||||||
aria-label="Submit registration"
|
aria-label="Submit registration"
|
||||||
className="btn-primary w-full transform rounded-2xl px-4 py-3 tracking-wide transition-colors duration-200"
|
className="
|
||||||
|
w-full rounded-2xl bg-green-600 px-4 py-3 text-sm font-medium text-white
|
||||||
|
transition-colors hover:bg-green-700 focus:outline-none focus:ring-2
|
||||||
|
focus:ring-green-500 focus:ring-offset-2 disabled:opacity-50
|
||||||
|
disabled:hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700
|
||||||
|
"
|
||||||
>
|
>
|
||||||
{isSubmitting ? <Spinner /> : localize('com_auth_continue')}
|
{isSubmitting ? <Spinner /> : localize('com_auth_continue')}
|
||||||
</button>
|
</button>
|
||||||
|
@ -192,7 +197,11 @@ const Registration: React.FC = () => {
|
||||||
|
|
||||||
<p className="my-4 text-center text-sm font-light text-gray-700 dark:text-white">
|
<p className="my-4 text-center text-sm font-light text-gray-700 dark:text-white">
|
||||||
{localize('com_auth_already_have_account')}{' '}
|
{localize('com_auth_already_have_account')}{' '}
|
||||||
<a href="/login" aria-label="Login" className="p-1 text-green-500">
|
<a
|
||||||
|
href="/login"
|
||||||
|
aria-label="Login"
|
||||||
|
className="inline-flex p-1 text-sm font-medium text-green-600 transition-colors hover:text-green-700 dark:text-green-400 dark:hover:text-green-300"
|
||||||
|
>
|
||||||
{localize('com_auth_login')}
|
{localize('com_auth_login')}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { useLocalize } from '~/hooks';
|
||||||
const BodyTextWrapper: FC<{ children: ReactNode }> = ({ children }) => {
|
const BodyTextWrapper: FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative mt-4 rounded border border-green-400 bg-green-100 px-4 py-3 text-green-700 dark:bg-green-900 dark:text-white"
|
className="relative mt-6 rounded-lg border border-green-500/20 bg-green-50/50 px-6 py-4 text-green-700 shadow-sm transition-all dark:bg-green-950/30 dark:text-green-100"
|
||||||
role="alert"
|
role="alert"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
@ -21,13 +21,14 @@ const BodyTextWrapper: FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
const ResetPasswordBodyText = () => {
|
const ResetPasswordBodyText = () => {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col space-y-4">
|
||||||
{localize('com_auth_reset_password_if_email_exists')}
|
<p>{localize('com_auth_reset_password_if_email_exists')}</p>
|
||||||
<span>
|
<a
|
||||||
<a className="text-sm text-green-500 hover:underline" href="/login">
|
className="inline-flex text-sm font-medium text-green-600 transition-colors hover:text-green-700 dark:text-green-400 dark:hover:text-green-300"
|
||||||
{localize('com_auth_back_to_login')}
|
href="/login"
|
||||||
</a>
|
>
|
||||||
</span>
|
{localize('com_auth_back_to_login')}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -76,12 +77,12 @@ function RequestPasswordReset() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className="mt-6"
|
className="mt-8 space-y-6"
|
||||||
aria-label="Password reset form"
|
aria-label="Password reset form"
|
||||||
method="POST"
|
method="POST"
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
>
|
>
|
||||||
<div className="mb-2">
|
<div className="space-y-2">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
|
@ -105,42 +106,51 @@ function RequestPasswordReset() {
|
||||||
})}
|
})}
|
||||||
aria-invalid={!!errors.email}
|
aria-invalid={!!errors.email}
|
||||||
className="
|
className="
|
||||||
webkit-dark-styles transition-color peer w-full rounded-2xl border border-border-light
|
peer w-full rounded-lg border border-gray-300 bg-transparent px-4 py-3
|
||||||
bg-surface-primary px-3.5 pb-2.5 pt-3 text-text-primary duration-200 focus:border-green-500 focus:outline-none
|
text-base text-gray-900 placeholder-transparent transition-all
|
||||||
|
focus:border-green-500 focus:outline-none focus:ring-2 focus:ring-green-500/20
|
||||||
|
dark:border-gray-700 dark:text-white dark:focus:border-green-500
|
||||||
"
|
"
|
||||||
placeholder=" "
|
placeholder="email@example.com"
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
htmlFor="email"
|
htmlFor="email"
|
||||||
className="
|
className="
|
||||||
absolute start-3 top-1.5 z-10 origin-[0] -translate-y-4 scale-75 transform bg-surface-primary px-2 text-sm text-text-secondary-alt duration-200
|
absolute -top-2 left-2 z-10 bg-white px-2 text-sm text-gray-600
|
||||||
peer-placeholder-shown:top-1/2 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:scale-100
|
transition-all peer-placeholder-shown:top-3 peer-placeholder-shown:text-base
|
||||||
peer-focus:top-1.5 peer-focus:-translate-y-4 peer-focus:scale-75 peer-focus:px-2 peer-focus:text-green-500
|
peer-placeholder-shown:text-gray-500 peer-focus:-top-2 peer-focus:text-sm
|
||||||
rtl:peer-focus:left-auto rtl:peer-focus:translate-x-1/4
|
peer-focus:text-green-600 dark:bg-gray-900 dark:text-gray-400
|
||||||
|
dark:peer-focus:text-green-500
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
{localize('com_auth_email_address')}
|
{localize('com_auth_email_address')}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{errors.email && (
|
{errors.email && (
|
||||||
<span role="alert" className="mt-1 text-sm text-red-500 dark:text-red-900">
|
<p role="alert" className="text-sm font-medium text-red-600 dark:text-red-400">
|
||||||
{errors.email.message}
|
{errors.email.message}
|
||||||
</span>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6">
|
<div className="space-y-4">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!!errors.email}
|
disabled={!!errors.email}
|
||||||
className="btn-primary w-full transform rounded-2xl px-4 py-3 tracking-wide transition-colors duration-200"
|
className="
|
||||||
|
w-full rounded-2xl bg-green-600 px-4 py-3 text-sm font-medium text-white
|
||||||
|
transition-colors hover:bg-green-700 focus:outline-none focus:ring-2
|
||||||
|
focus:ring-green-500 focus:ring-offset-2 disabled:opacity-50
|
||||||
|
disabled:hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700
|
||||||
|
"
|
||||||
>
|
>
|
||||||
{localize('com_auth_continue')}
|
{localize('com_auth_continue')}
|
||||||
</button>
|
</button>
|
||||||
<div className="mt-4 flex justify-center">
|
<a
|
||||||
<a href="/login" className="text-sm text-green-500">
|
href="/login"
|
||||||
{localize('com_auth_back_to_login')}
|
className="block text-center text-sm font-medium text-green-600 transition-colors hover:text-green-700 dark:text-green-400 dark:hover:text-green-300"
|
||||||
</a>
|
>
|
||||||
</div>
|
{localize('com_auth_back_to_login')}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|
|
@ -35,7 +35,7 @@ function ResetPassword() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="relative mb-8 mt-4 rounded border border-green-400 bg-green-100 px-4 py-3 text-center text-green-700 dark:bg-gray-900 dark:text-white"
|
className="relative mb-8 mt-4 rounded-2xl border border-green-400 bg-green-100 px-4 py-3 text-center text-green-700 dark:bg-gray-900 dark:text-white"
|
||||||
role="alert"
|
role="alert"
|
||||||
>
|
>
|
||||||
{localize('com_auth_login_with_new_password')}
|
{localize('com_auth_login_with_new_password')}
|
||||||
|
@ -43,7 +43,7 @@ function ResetPassword() {
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/login')}
|
onClick={() => navigate('/login')}
|
||||||
aria-label={localize('com_auth_sign_in')}
|
aria-label={localize('com_auth_sign_in')}
|
||||||
className="w-full transform rounded-md bg-green-500 px-4 py-3 tracking-wide text-white transition-colors duration-200 hover:bg-green-600 focus:bg-green-600 focus:outline-none"
|
className="w-full transform rounded-2xl bg-green-500 px-4 py-3 tracking-wide text-white transition-colors duration-200 hover:bg-green-600 focus:bg-green-600 focus:outline-none"
|
||||||
>
|
>
|
||||||
{localize('com_auth_continue')}
|
{localize('com_auth_continue')}
|
||||||
</button>
|
</button>
|
||||||
|
@ -163,7 +163,12 @@ function ResetPassword() {
|
||||||
disabled={!!errors.password || !!errors.confirm_password}
|
disabled={!!errors.password || !!errors.confirm_password}
|
||||||
type="submit"
|
type="submit"
|
||||||
aria-label={localize('com_auth_submit_registration')}
|
aria-label={localize('com_auth_submit_registration')}
|
||||||
className="btn-primary w-full transform rounded-2xl px-4 py-3 tracking-wide transition-colors duration-200"
|
className="
|
||||||
|
w-full rounded-2xl bg-green-600 px-4 py-3 text-sm font-medium text-white
|
||||||
|
transition-colors hover:bg-green-700 focus:outline-none focus:ring-2
|
||||||
|
focus:ring-green-500 focus:ring-offset-2 disabled:opacity-50
|
||||||
|
disabled:hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700
|
||||||
|
"
|
||||||
>
|
>
|
||||||
{localize('com_auth_continue')}
|
{localize('com_auth_continue')}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import reactRouter from 'react-router-dom';
|
import reactRouter from 'react-router-dom';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { render, waitFor } from 'test/layout-test-utils';
|
import { getByTestId, render, waitFor } from 'test/layout-test-utils';
|
||||||
import * as mockDataProvider from 'librechat-data-provider/react-query';
|
import * as mockDataProvider from 'librechat-data-provider/react-query';
|
||||||
import type { TStartupConfig } from 'librechat-data-provider';
|
import type { TStartupConfig } from 'librechat-data-provider';
|
||||||
import AuthLayout from '~/components/Auth/AuthLayout';
|
import AuthLayout from '~/components/Auth/AuthLayout';
|
||||||
|
@ -117,7 +117,7 @@ test('renders login form', () => {
|
||||||
const { getByLabelText, getByRole } = setup();
|
const { getByLabelText, getByRole } = setup();
|
||||||
expect(getByLabelText(/email/i)).toBeInTheDocument();
|
expect(getByLabelText(/email/i)).toBeInTheDocument();
|
||||||
expect(getByLabelText(/password/i)).toBeInTheDocument();
|
expect(getByLabelText(/password/i)).toBeInTheDocument();
|
||||||
expect(getByRole('button', { name: /Sign in/i })).toBeInTheDocument();
|
expect(getByTestId(document.body, 'login-button')).toBeInTheDocument();
|
||||||
expect(getByRole('link', { name: /Sign up/i })).toBeInTheDocument();
|
expect(getByRole('link', { name: /Sign up/i })).toBeInTheDocument();
|
||||||
expect(getByRole('link', { name: /Sign up/i })).toHaveAttribute('href', '/register');
|
expect(getByRole('link', { name: /Sign up/i })).toHaveAttribute('href', '/register');
|
||||||
expect(getByRole('link', { name: /Continue with Google/i })).toBeInTheDocument();
|
expect(getByRole('link', { name: /Continue with Google/i })).toBeInTheDocument();
|
||||||
|
@ -144,7 +144,7 @@ test('renders login form', () => {
|
||||||
|
|
||||||
test('calls loginUser.mutate on login', async () => {
|
test('calls loginUser.mutate on login', async () => {
|
||||||
const mutate = jest.fn();
|
const mutate = jest.fn();
|
||||||
const { getByLabelText, getByRole } = setup({
|
const { getByLabelText } = setup({
|
||||||
// @ts-ignore - we don't need all parameters of the QueryObserverResult
|
// @ts-ignore - we don't need all parameters of the QueryObserverResult
|
||||||
useLoginUserReturnValue: {
|
useLoginUserReturnValue: {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
@ -155,7 +155,7 @@ test('calls loginUser.mutate on login', async () => {
|
||||||
|
|
||||||
const emailInput = getByLabelText(/email/i);
|
const emailInput = getByLabelText(/email/i);
|
||||||
const passwordInput = getByLabelText(/password/i);
|
const passwordInput = getByLabelText(/password/i);
|
||||||
const submitButton = getByRole('button', { name: /Sign in/i });
|
const submitButton = getByTestId(document.body, 'login-button');
|
||||||
|
|
||||||
await userEvent.type(emailInput, 'test@test.com');
|
await userEvent.type(emailInput, 'test@test.com');
|
||||||
await userEvent.type(passwordInput, 'password');
|
await userEvent.type(passwordInput, 'password');
|
||||||
|
@ -165,7 +165,7 @@ test('calls loginUser.mutate on login', async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Navigates to / on successful login', async () => {
|
test('Navigates to / on successful login', async () => {
|
||||||
const { getByLabelText, getByRole, history } = setup({
|
const { getByLabelText, history } = setup({
|
||||||
// @ts-ignore - we don't need all parameters of the QueryObserverResult
|
// @ts-ignore - we don't need all parameters of the QueryObserverResult
|
||||||
useLoginUserReturnValue: {
|
useLoginUserReturnValue: {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
@ -185,7 +185,7 @@ test('Navigates to / on successful login', async () => {
|
||||||
|
|
||||||
const emailInput = getByLabelText(/email/i);
|
const emailInput = getByLabelText(/email/i);
|
||||||
const passwordInput = getByLabelText(/password/i);
|
const passwordInput = getByLabelText(/password/i);
|
||||||
const submitButton = getByRole('button', { name: /Sign in/i });
|
const submitButton = getByTestId(document.body, 'login-button');
|
||||||
|
|
||||||
await userEvent.type(emailInput, 'test@test.com');
|
await userEvent.type(emailInput, 'test@test.com');
|
||||||
await userEvent.type(passwordInput, 'password');
|
await userEvent.type(passwordInput, 'password');
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { render } from 'test/layout-test-utils';
|
import { render, getByTestId } from 'test/layout-test-utils';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import * as mockDataProvider from 'librechat-data-provider/react-query';
|
import * as mockDataProvider from 'librechat-data-provider/react-query';
|
||||||
import type { TStartupConfig } from 'librechat-data-provider';
|
import type { TStartupConfig } from 'librechat-data-provider';
|
||||||
|
@ -112,7 +112,7 @@ test('submits login form', async () => {
|
||||||
);
|
);
|
||||||
const emailInput = getByLabelText(/email/i);
|
const emailInput = getByLabelText(/email/i);
|
||||||
const passwordInput = getByLabelText(/password/i);
|
const passwordInput = getByLabelText(/password/i);
|
||||||
const submitButton = getByRole('button', { name: /Sign in/i });
|
const submitButton = getByTestId(document.body, 'login-button');
|
||||||
|
|
||||||
await userEvent.type(emailInput, 'test@example.com');
|
await userEvent.type(emailInput, 'test@example.com');
|
||||||
await userEvent.type(passwordInput, 'password');
|
await userEvent.type(passwordInput, 'password');
|
||||||
|
@ -127,7 +127,7 @@ test('displays validation error messages', async () => {
|
||||||
);
|
);
|
||||||
const emailInput = getByLabelText(/email/i);
|
const emailInput = getByLabelText(/email/i);
|
||||||
const passwordInput = getByLabelText(/password/i);
|
const passwordInput = getByLabelText(/password/i);
|
||||||
const submitButton = getByRole('button', { name: /Sign in/i });
|
const submitButton = getByTestId(document.body, 'login-button');
|
||||||
|
|
||||||
await userEvent.type(emailInput, 'test');
|
await userEvent.type(emailInput, 'test');
|
||||||
await userEvent.type(passwordInput, 'pass');
|
await userEvent.type(passwordInput, 'pass');
|
||||||
|
|
|
@ -579,8 +579,6 @@ describe('Conversation Utilities with Fake Data', () => {
|
||||||
5,
|
5,
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(normalizedData);
|
|
||||||
|
|
||||||
expect(normalizedData.pages[0].conversations).toHaveLength(0);
|
expect(normalizedData.pages[0].conversations).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
import connectDb from '@librechat/backend/lib/db/connectDb';
|
import connectDb from '@librechat/backend/lib/db/connectDb';
|
||||||
import { deleteMessages, deleteConvos, User, Session, Balance } from '@librechat/backend/models';
|
import {
|
||||||
|
deleteMessages,
|
||||||
|
deleteConvos,
|
||||||
|
User,
|
||||||
|
deleteAllUserSessions,
|
||||||
|
Balance,
|
||||||
|
} from '@librechat/backend/models';
|
||||||
import { Transaction } from '@librechat/backend/models/Transaction';
|
import { Transaction } from '@librechat/backend/models/Transaction';
|
||||||
type TUser = { email: string; password: string };
|
type TUser = { email: string; password: string };
|
||||||
|
|
||||||
|
@ -26,7 +32,8 @@ export default async function cleanupUser(user: TUser) {
|
||||||
console.log(`🤖: ✅ Deleted ${deletedMessages} remaining message(s)`);
|
console.log(`🤖: ✅ Deleted ${deletedMessages} remaining message(s)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Session.deleteAllUserSessions(user);
|
// TODO: fix this to delete all user sessions with the user's email
|
||||||
|
await deleteAllUserSessions(user);
|
||||||
|
|
||||||
await User.deleteMany({ _id: user });
|
await User.deleteMany({ _id: user });
|
||||||
await Balance.deleteMany({ user });
|
await Balance.deleteMany({ user });
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue