2023-12-14 07:49:27 -05:00
|
|
|
const mongoose = require('mongoose');
|
|
|
|
|
const signPayload = require('~/server/services/signPayload');
|
2024-08-04 23:59:45 -04:00
|
|
|
const { hashToken } = require('~/server/utils/crypto');
|
2025-03-07 17:55:44 +01:00
|
|
|
const { sessionSchema } = require('@librechat/data-schemas');
|
2023-12-14 07:49:27 -05:00
|
|
|
const { logger } = require('~/config');
|
|
|
|
|
|
2024-12-23 11:12:07 +01:00
|
|
|
const Session = mongoose.model('Session', sessionSchema);
|
|
|
|
|
|
2023-09-11 13:10:46 -04:00
|
|
|
const { REFRESH_TOKEN_EXPIRY } = process.env ?? {};
|
2024-12-23 11:12:07 +01:00
|
|
|
const expires = eval(REFRESH_TOKEN_EXPIRY) ?? 1000 * 60 * 60 * 24 * 7; // 7 days default
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Error class for Session-related errors
|
|
|
|
|
*/
|
|
|
|
|
class SessionError extends Error {
|
|
|
|
|
constructor(message, code = 'SESSION_ERROR') {
|
|
|
|
|
super(message);
|
|
|
|
|
this.name = 'SessionError';
|
|
|
|
|
this.code = code;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Creates a new session for a user
|
|
|
|
|
* @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');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const session = new Session({
|
|
|
|
|
user: userId,
|
|
|
|
|
expiration: options.expiration || new Date(Date.now() + expires),
|
|
|
|
|
});
|
|
|
|
|
const refreshToken = await generateRefreshToken(session);
|
|
|
|
|
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 }) => {
|
2023-09-11 13:10:46 -04:00
|
|
|
try {
|
2024-12-23 11:12:07 +01:00
|
|
|
const query = {};
|
|
|
|
|
|
|
|
|
|
if (!params.refreshToken && !params.userId && !params.sessionId) {
|
|
|
|
|
throw new SessionError('At least one search parameter is required', 'INVALID_SEARCH_PARAMS');
|
2023-09-11 13:10:46 -04:00
|
|
|
}
|
|
|
|
|
|
2024-12-23 11:12:07 +01:00
|
|
|
if (params.refreshToken) {
|
|
|
|
|
const tokenHash = await hashToken(params.refreshToken);
|
|
|
|
|
query.refreshTokenHash = tokenHash;
|
|
|
|
|
}
|
2023-09-11 13:10:46 -04:00
|
|
|
|
2024-12-23 11:12:07 +01:00
|
|
|
if (params.userId) {
|
|
|
|
|
query.user = params.userId;
|
|
|
|
|
}
|
2023-09-11 13:10:46 -04:00
|
|
|
|
2024-12-23 11:12:07 +01:00
|
|
|
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;
|
|
|
|
|
}
|
2023-09-11 13:10:46 -04:00
|
|
|
|
2024-12-23 11:12:07 +01:00
|
|
|
// 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;
|
2023-09-11 13:10:46 -04:00
|
|
|
} catch (error) {
|
2024-12-23 11:12:07 +01:00
|
|
|
logger.error('[deleteSession] Error deleting session:', error);
|
|
|
|
|
throw new SessionError('Failed to delete session', 'DELETE_SESSION_FAILED');
|
2023-09-11 13:10:46 -04:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2024-12-23 11:12:07 +01:00
|
|
|
/**
|
|
|
|
|
* 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 = {}) => {
|
feat: Message Rate Limiters, Violation Logging, & Ban System 🔨 (#903)
* refactor: require Auth middleware in route index files
* feat: concurrent message limiter
* feat: complete concurrent message limiter with caching
* refactor: SSE response methods separated from handleText
* fix(abortMiddleware): fix req and res order to standard, use endpointOption in req.body
* chore: minor name changes
* refactor: add isUUID condition to saveMessage
* fix(concurrentLimiter): logic correctly handles the max number of concurrent messages and res closing/finalization
* chore: bump keyv and remove console.log from Message
* fix(concurrentLimiter): ensure messages are only saved in later message children
* refactor(concurrentLimiter): use KeyvFile instead, could make other stores configurable in the future
* feat: add denyRequest function for error responses
* feat(utils): add isStringTruthy function
Introduce the isStringTruthy function to the utilities module to check if a string value is a case-insensitive match for 'true'
* feat: add optional message rate limiters by IP and userId
* feat: add optional message rate limiters by IP and userId to edit route
* refactor: rename isStringTruthy to isTrue for brevity
* refactor(getError): use map to make code cleaner
* refactor: use memory for concurrent rate limiter to prevent clearing on startup/exit, add multiple log files, fix error message for concurrent violation
* feat: check if errorMessage is object, stringify if so
* chore: send object to denyRequest which will stringify it
* feat: log excessive requests
* fix(getError): correctly pluralize messages
* refactor(limiters): make type consistent between logs and errorMessage
* refactor(cache): move files out of lib/db into separate cache dir
>> feat: add getLogStores function so Keyv instance is not redundantly created on every violation
feat: separate violation logging to own function with logViolation
* fix: cache/index.js export, properly record userViolations
* refactor(messageLimiters): use new logging method, add logging to registrations
* refactor(logViolation): make userLogs an array of logs per user
* feat: add logging to login limiter
* refactor: pass req as first param to logViolation and record offending IP
* refactor: rename isTrue helper fn to isEnabled
* feat: add simple non_browser check and log violation
* fix: open handles in unit tests, remove KeyvMongo as not used and properly mock global fetch
* chore: adjust nodemon ignore paths to properly ignore logs
* feat: add math helper function for safe use of eval
* refactor(api/convos): use middleware at top of file to avoid redundancy
* feat: add delete all static method for Sessions
* fix: redirect to login on refresh if user is not found, or the session is not found but hasn't expired (ban case)
* refactor(getLogStores): adjust return type
* feat: add ban violation and check ban logic
refactor(logViolation): pass both req and res objects
* feat: add removePorts helper function
* refactor: rename getError to getMessageError and add getLoginError for displaying different login errors
* fix(AuthContext): fix type issue and remove unused code
* refactor(bans): ban by ip and user id, send response based on origin
* chore: add frontend ban messages
* refactor(routes/oauth): add ban check to handler, also consolidate logic to avoid redundancy
* feat: add ban check to AI messaging routes
* feat: add ban check to login/registration
* fix(ci/api): mock KeyvMongo to avoid tests hanging
* docs: update .env.example
> refactor(banViolation): calculate interval rate crossover, early return if duration is invalid
ci(banViolation): add tests to ensure users are only banned when expected
* docs: improve wording for mod system
* feat: add configurable env variables for violation scores
* chore: add jsdoc for uaParser.js
* chore: improve ban text log
* chore: update bun test scripts
* refactor(math.js): add fallback values
* fix(KeyvMongo/banLogs): refactor keyv instances to top of files to avoid memory leaks, refactor ban logic to use getLogStores instead
refactor(getLogStores): get a single log store by type
* fix(ci): refactor tests due to banLogs changes, also make sure to clear and revoke sessions even if ban duration is 0
* fix(banViolation.js): getLogStores import
* feat: handle 500 code error at login
* fix(middleware): handle case where user.id is _id and not just id
* ci: add ban secrets for backend unit tests
* refactor: logout user upon ban
* chore: log session delete message only if deletedCount > 0
* refactor: change default ban duration (2h) and make logic more clear in JSDOC
* fix: login and registration limiters will now return rate limiting error
* fix: userId not parsable as non ObjectId string
* feat: add useTimeout hook to properly clear timeouts when invoking functions within them
refactor(AuthContext): cleanup code by using new hook and defining types in ~/common
* fix: login error message for rate limits
* docs: add info for automated mod system and rate limiters, update other docs accordingly
* chore: bump data-provider version
2023-09-13 10:57:07 -04:00
|
|
|
try {
|
|
|
|
|
if (!userId) {
|
2024-12-23 11:12:07 +01:00
|
|
|
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 };
|
feat: Message Rate Limiters, Violation Logging, & Ban System 🔨 (#903)
* refactor: require Auth middleware in route index files
* feat: concurrent message limiter
* feat: complete concurrent message limiter with caching
* refactor: SSE response methods separated from handleText
* fix(abortMiddleware): fix req and res order to standard, use endpointOption in req.body
* chore: minor name changes
* refactor: add isUUID condition to saveMessage
* fix(concurrentLimiter): logic correctly handles the max number of concurrent messages and res closing/finalization
* chore: bump keyv and remove console.log from Message
* fix(concurrentLimiter): ensure messages are only saved in later message children
* refactor(concurrentLimiter): use KeyvFile instead, could make other stores configurable in the future
* feat: add denyRequest function for error responses
* feat(utils): add isStringTruthy function
Introduce the isStringTruthy function to the utilities module to check if a string value is a case-insensitive match for 'true'
* feat: add optional message rate limiters by IP and userId
* feat: add optional message rate limiters by IP and userId to edit route
* refactor: rename isStringTruthy to isTrue for brevity
* refactor(getError): use map to make code cleaner
* refactor: use memory for concurrent rate limiter to prevent clearing on startup/exit, add multiple log files, fix error message for concurrent violation
* feat: check if errorMessage is object, stringify if so
* chore: send object to denyRequest which will stringify it
* feat: log excessive requests
* fix(getError): correctly pluralize messages
* refactor(limiters): make type consistent between logs and errorMessage
* refactor(cache): move files out of lib/db into separate cache dir
>> feat: add getLogStores function so Keyv instance is not redundantly created on every violation
feat: separate violation logging to own function with logViolation
* fix: cache/index.js export, properly record userViolations
* refactor(messageLimiters): use new logging method, add logging to registrations
* refactor(logViolation): make userLogs an array of logs per user
* feat: add logging to login limiter
* refactor: pass req as first param to logViolation and record offending IP
* refactor: rename isTrue helper fn to isEnabled
* feat: add simple non_browser check and log violation
* fix: open handles in unit tests, remove KeyvMongo as not used and properly mock global fetch
* chore: adjust nodemon ignore paths to properly ignore logs
* feat: add math helper function for safe use of eval
* refactor(api/convos): use middleware at top of file to avoid redundancy
* feat: add delete all static method for Sessions
* fix: redirect to login on refresh if user is not found, or the session is not found but hasn't expired (ban case)
* refactor(getLogStores): adjust return type
* feat: add ban violation and check ban logic
refactor(logViolation): pass both req and res objects
* feat: add removePorts helper function
* refactor: rename getError to getMessageError and add getLoginError for displaying different login errors
* fix(AuthContext): fix type issue and remove unused code
* refactor(bans): ban by ip and user id, send response based on origin
* chore: add frontend ban messages
* refactor(routes/oauth): add ban check to handler, also consolidate logic to avoid redundancy
* feat: add ban check to AI messaging routes
* feat: add ban check to login/registration
* fix(ci/api): mock KeyvMongo to avoid tests hanging
* docs: update .env.example
> refactor(banViolation): calculate interval rate crossover, early return if duration is invalid
ci(banViolation): add tests to ensure users are only banned when expected
* docs: improve wording for mod system
* feat: add configurable env variables for violation scores
* chore: add jsdoc for uaParser.js
* chore: improve ban text log
* chore: update bun test scripts
* refactor(math.js): add fallback values
* fix(KeyvMongo/banLogs): refactor keyv instances to top of files to avoid memory leaks, refactor ban logic to use getLogStores instead
refactor(getLogStores): get a single log store by type
* fix(ci): refactor tests due to banLogs changes, also make sure to clear and revoke sessions even if ban duration is 0
* fix(banViolation.js): getLogStores import
* feat: handle 500 code error at login
* fix(middleware): handle case where user.id is _id and not just id
* ci: add ban secrets for backend unit tests
* refactor: logout user upon ban
* chore: log session delete message only if deletedCount > 0
* refactor: change default ban duration (2h) and make logic more clear in JSDOC
* fix: login and registration limiters will now return rate limiting error
* fix: userId not parsable as non ObjectId string
* feat: add useTimeout hook to properly clear timeouts when invoking functions within them
refactor(AuthContext): cleanup code by using new hook and defining types in ~/common
* fix: login error message for rate limits
* docs: add info for automated mod system and rate limiters, update other docs accordingly
* chore: bump data-provider version
2023-09-13 10:57:07 -04:00
|
|
|
}
|
2024-12-23 11:12:07 +01:00
|
|
|
|
|
|
|
|
const result = await Session.deleteMany(query);
|
|
|
|
|
|
|
|
|
|
if (result.deletedCount > 0) {
|
2023-12-14 07:49:27 -05:00
|
|
|
logger.debug(
|
2024-12-23 11:12:07 +01:00
|
|
|
`[deleteAllUserSessions] Deleted ${result.deletedCount} sessions for user ${userIdString}.`,
|
2023-12-14 07:49:27 -05:00
|
|
|
);
|
feat: Message Rate Limiters, Violation Logging, & Ban System 🔨 (#903)
* refactor: require Auth middleware in route index files
* feat: concurrent message limiter
* feat: complete concurrent message limiter with caching
* refactor: SSE response methods separated from handleText
* fix(abortMiddleware): fix req and res order to standard, use endpointOption in req.body
* chore: minor name changes
* refactor: add isUUID condition to saveMessage
* fix(concurrentLimiter): logic correctly handles the max number of concurrent messages and res closing/finalization
* chore: bump keyv and remove console.log from Message
* fix(concurrentLimiter): ensure messages are only saved in later message children
* refactor(concurrentLimiter): use KeyvFile instead, could make other stores configurable in the future
* feat: add denyRequest function for error responses
* feat(utils): add isStringTruthy function
Introduce the isStringTruthy function to the utilities module to check if a string value is a case-insensitive match for 'true'
* feat: add optional message rate limiters by IP and userId
* feat: add optional message rate limiters by IP and userId to edit route
* refactor: rename isStringTruthy to isTrue for brevity
* refactor(getError): use map to make code cleaner
* refactor: use memory for concurrent rate limiter to prevent clearing on startup/exit, add multiple log files, fix error message for concurrent violation
* feat: check if errorMessage is object, stringify if so
* chore: send object to denyRequest which will stringify it
* feat: log excessive requests
* fix(getError): correctly pluralize messages
* refactor(limiters): make type consistent between logs and errorMessage
* refactor(cache): move files out of lib/db into separate cache dir
>> feat: add getLogStores function so Keyv instance is not redundantly created on every violation
feat: separate violation logging to own function with logViolation
* fix: cache/index.js export, properly record userViolations
* refactor(messageLimiters): use new logging method, add logging to registrations
* refactor(logViolation): make userLogs an array of logs per user
* feat: add logging to login limiter
* refactor: pass req as first param to logViolation and record offending IP
* refactor: rename isTrue helper fn to isEnabled
* feat: add simple non_browser check and log violation
* fix: open handles in unit tests, remove KeyvMongo as not used and properly mock global fetch
* chore: adjust nodemon ignore paths to properly ignore logs
* feat: add math helper function for safe use of eval
* refactor(api/convos): use middleware at top of file to avoid redundancy
* feat: add delete all static method for Sessions
* fix: redirect to login on refresh if user is not found, or the session is not found but hasn't expired (ban case)
* refactor(getLogStores): adjust return type
* feat: add ban violation and check ban logic
refactor(logViolation): pass both req and res objects
* feat: add removePorts helper function
* refactor: rename getError to getMessageError and add getLoginError for displaying different login errors
* fix(AuthContext): fix type issue and remove unused code
* refactor(bans): ban by ip and user id, send response based on origin
* chore: add frontend ban messages
* refactor(routes/oauth): add ban check to handler, also consolidate logic to avoid redundancy
* feat: add ban check to AI messaging routes
* feat: add ban check to login/registration
* fix(ci/api): mock KeyvMongo to avoid tests hanging
* docs: update .env.example
> refactor(banViolation): calculate interval rate crossover, early return if duration is invalid
ci(banViolation): add tests to ensure users are only banned when expected
* docs: improve wording for mod system
* feat: add configurable env variables for violation scores
* chore: add jsdoc for uaParser.js
* chore: improve ban text log
* chore: update bun test scripts
* refactor(math.js): add fallback values
* fix(KeyvMongo/banLogs): refactor keyv instances to top of files to avoid memory leaks, refactor ban logic to use getLogStores instead
refactor(getLogStores): get a single log store by type
* fix(ci): refactor tests due to banLogs changes, also make sure to clear and revoke sessions even if ban duration is 0
* fix(banViolation.js): getLogStores import
* feat: handle 500 code error at login
* fix(middleware): handle case where user.id is _id and not just id
* ci: add ban secrets for backend unit tests
* refactor: logout user upon ban
* chore: log session delete message only if deletedCount > 0
* refactor: change default ban duration (2h) and make logic more clear in JSDOC
* fix: login and registration limiters will now return rate limiting error
* fix: userId not parsable as non ObjectId string
* feat: add useTimeout hook to properly clear timeouts when invoking functions within them
refactor(AuthContext): cleanup code by using new hook and defining types in ~/common
* fix: login error message for rate limits
* docs: add info for automated mod system and rate limiters, update other docs accordingly
* chore: bump data-provider version
2023-09-13 10:57:07 -04:00
|
|
|
}
|
2024-12-23 11:12:07 +01:00
|
|
|
|
|
|
|
|
return result;
|
feat: Message Rate Limiters, Violation Logging, & Ban System 🔨 (#903)
* refactor: require Auth middleware in route index files
* feat: concurrent message limiter
* feat: complete concurrent message limiter with caching
* refactor: SSE response methods separated from handleText
* fix(abortMiddleware): fix req and res order to standard, use endpointOption in req.body
* chore: minor name changes
* refactor: add isUUID condition to saveMessage
* fix(concurrentLimiter): logic correctly handles the max number of concurrent messages and res closing/finalization
* chore: bump keyv and remove console.log from Message
* fix(concurrentLimiter): ensure messages are only saved in later message children
* refactor(concurrentLimiter): use KeyvFile instead, could make other stores configurable in the future
* feat: add denyRequest function for error responses
* feat(utils): add isStringTruthy function
Introduce the isStringTruthy function to the utilities module to check if a string value is a case-insensitive match for 'true'
* feat: add optional message rate limiters by IP and userId
* feat: add optional message rate limiters by IP and userId to edit route
* refactor: rename isStringTruthy to isTrue for brevity
* refactor(getError): use map to make code cleaner
* refactor: use memory for concurrent rate limiter to prevent clearing on startup/exit, add multiple log files, fix error message for concurrent violation
* feat: check if errorMessage is object, stringify if so
* chore: send object to denyRequest which will stringify it
* feat: log excessive requests
* fix(getError): correctly pluralize messages
* refactor(limiters): make type consistent between logs and errorMessage
* refactor(cache): move files out of lib/db into separate cache dir
>> feat: add getLogStores function so Keyv instance is not redundantly created on every violation
feat: separate violation logging to own function with logViolation
* fix: cache/index.js export, properly record userViolations
* refactor(messageLimiters): use new logging method, add logging to registrations
* refactor(logViolation): make userLogs an array of logs per user
* feat: add logging to login limiter
* refactor: pass req as first param to logViolation and record offending IP
* refactor: rename isTrue helper fn to isEnabled
* feat: add simple non_browser check and log violation
* fix: open handles in unit tests, remove KeyvMongo as not used and properly mock global fetch
* chore: adjust nodemon ignore paths to properly ignore logs
* feat: add math helper function for safe use of eval
* refactor(api/convos): use middleware at top of file to avoid redundancy
* feat: add delete all static method for Sessions
* fix: redirect to login on refresh if user is not found, or the session is not found but hasn't expired (ban case)
* refactor(getLogStores): adjust return type
* feat: add ban violation and check ban logic
refactor(logViolation): pass both req and res objects
* feat: add removePorts helper function
* refactor: rename getError to getMessageError and add getLoginError for displaying different login errors
* fix(AuthContext): fix type issue and remove unused code
* refactor(bans): ban by ip and user id, send response based on origin
* chore: add frontend ban messages
* refactor(routes/oauth): add ban check to handler, also consolidate logic to avoid redundancy
* feat: add ban check to AI messaging routes
* feat: add ban check to login/registration
* fix(ci/api): mock KeyvMongo to avoid tests hanging
* docs: update .env.example
> refactor(banViolation): calculate interval rate crossover, early return if duration is invalid
ci(banViolation): add tests to ensure users are only banned when expected
* docs: improve wording for mod system
* feat: add configurable env variables for violation scores
* chore: add jsdoc for uaParser.js
* chore: improve ban text log
* chore: update bun test scripts
* refactor(math.js): add fallback values
* fix(KeyvMongo/banLogs): refactor keyv instances to top of files to avoid memory leaks, refactor ban logic to use getLogStores instead
refactor(getLogStores): get a single log store by type
* fix(ci): refactor tests due to banLogs changes, also make sure to clear and revoke sessions even if ban duration is 0
* fix(banViolation.js): getLogStores import
* feat: handle 500 code error at login
* fix(middleware): handle case where user.id is _id and not just id
* ci: add ban secrets for backend unit tests
* refactor: logout user upon ban
* chore: log session delete message only if deletedCount > 0
* refactor: change default ban duration (2h) and make logic more clear in JSDOC
* fix: login and registration limiters will now return rate limiting error
* fix: userId not parsable as non ObjectId string
* feat: add useTimeout hook to properly clear timeouts when invoking functions within them
refactor(AuthContext): cleanup code by using new hook and defining types in ~/common
* fix: login error message for rate limits
* docs: add info for automated mod system and rate limiters, update other docs accordingly
* chore: bump data-provider version
2023-09-13 10:57:07 -04:00
|
|
|
} catch (error) {
|
2024-12-23 11:12:07 +01:00
|
|
|
logger.error('[deleteAllUserSessions] Error deleting user sessions:', error);
|
|
|
|
|
throw new SessionError('Failed to delete user sessions', 'DELETE_ALL_SESSIONS_FAILED');
|
feat: Message Rate Limiters, Violation Logging, & Ban System 🔨 (#903)
* refactor: require Auth middleware in route index files
* feat: concurrent message limiter
* feat: complete concurrent message limiter with caching
* refactor: SSE response methods separated from handleText
* fix(abortMiddleware): fix req and res order to standard, use endpointOption in req.body
* chore: minor name changes
* refactor: add isUUID condition to saveMessage
* fix(concurrentLimiter): logic correctly handles the max number of concurrent messages and res closing/finalization
* chore: bump keyv and remove console.log from Message
* fix(concurrentLimiter): ensure messages are only saved in later message children
* refactor(concurrentLimiter): use KeyvFile instead, could make other stores configurable in the future
* feat: add denyRequest function for error responses
* feat(utils): add isStringTruthy function
Introduce the isStringTruthy function to the utilities module to check if a string value is a case-insensitive match for 'true'
* feat: add optional message rate limiters by IP and userId
* feat: add optional message rate limiters by IP and userId to edit route
* refactor: rename isStringTruthy to isTrue for brevity
* refactor(getError): use map to make code cleaner
* refactor: use memory for concurrent rate limiter to prevent clearing on startup/exit, add multiple log files, fix error message for concurrent violation
* feat: check if errorMessage is object, stringify if so
* chore: send object to denyRequest which will stringify it
* feat: log excessive requests
* fix(getError): correctly pluralize messages
* refactor(limiters): make type consistent between logs and errorMessage
* refactor(cache): move files out of lib/db into separate cache dir
>> feat: add getLogStores function so Keyv instance is not redundantly created on every violation
feat: separate violation logging to own function with logViolation
* fix: cache/index.js export, properly record userViolations
* refactor(messageLimiters): use new logging method, add logging to registrations
* refactor(logViolation): make userLogs an array of logs per user
* feat: add logging to login limiter
* refactor: pass req as first param to logViolation and record offending IP
* refactor: rename isTrue helper fn to isEnabled
* feat: add simple non_browser check and log violation
* fix: open handles in unit tests, remove KeyvMongo as not used and properly mock global fetch
* chore: adjust nodemon ignore paths to properly ignore logs
* feat: add math helper function for safe use of eval
* refactor(api/convos): use middleware at top of file to avoid redundancy
* feat: add delete all static method for Sessions
* fix: redirect to login on refresh if user is not found, or the session is not found but hasn't expired (ban case)
* refactor(getLogStores): adjust return type
* feat: add ban violation and check ban logic
refactor(logViolation): pass both req and res objects
* feat: add removePorts helper function
* refactor: rename getError to getMessageError and add getLoginError for displaying different login errors
* fix(AuthContext): fix type issue and remove unused code
* refactor(bans): ban by ip and user id, send response based on origin
* chore: add frontend ban messages
* refactor(routes/oauth): add ban check to handler, also consolidate logic to avoid redundancy
* feat: add ban check to AI messaging routes
* feat: add ban check to login/registration
* fix(ci/api): mock KeyvMongo to avoid tests hanging
* docs: update .env.example
> refactor(banViolation): calculate interval rate crossover, early return if duration is invalid
ci(banViolation): add tests to ensure users are only banned when expected
* docs: improve wording for mod system
* feat: add configurable env variables for violation scores
* chore: add jsdoc for uaParser.js
* chore: improve ban text log
* chore: update bun test scripts
* refactor(math.js): add fallback values
* fix(KeyvMongo/banLogs): refactor keyv instances to top of files to avoid memory leaks, refactor ban logic to use getLogStores instead
refactor(getLogStores): get a single log store by type
* fix(ci): refactor tests due to banLogs changes, also make sure to clear and revoke sessions even if ban duration is 0
* fix(banViolation.js): getLogStores import
* feat: handle 500 code error at login
* fix(middleware): handle case where user.id is _id and not just id
* ci: add ban secrets for backend unit tests
* refactor: logout user upon ban
* chore: log session delete message only if deletedCount > 0
* refactor: change default ban duration (2h) and make logic more clear in JSDOC
* fix: login and registration limiters will now return rate limiting error
* fix: userId not parsable as non ObjectId string
* feat: add useTimeout hook to properly clear timeouts when invoking functions within them
refactor(AuthContext): cleanup code by using new hook and defining types in ~/common
* fix: login error message for rate limits
* docs: add info for automated mod system and rate limiters, update other docs accordingly
* chore: bump data-provider version
2023-09-13 10:57:07 -04:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2024-12-23 11:12:07 +01:00
|
|
|
/**
|
|
|
|
|
* 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);
|
|
|
|
|
}
|
2023-09-11 13:10:46 -04:00
|
|
|
|
2024-12-23 11:12:07 +01:00
|
|
|
const refreshToken = await signPayload({
|
|
|
|
|
payload: {
|
|
|
|
|
id: session.user,
|
|
|
|
|
sessionId: session._id,
|
|
|
|
|
},
|
|
|
|
|
secret: process.env.JWT_REFRESH_SECRET,
|
|
|
|
|
expirationTime: Math.floor((expiresIn - Date.now()) / 1000),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
session.refreshTokenHash = await hashToken(refreshToken);
|
|
|
|
|
await session.save();
|
|
|
|
|
|
|
|
|
|
return refreshToken;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error('[generateRefreshToken] Error generating refresh token:', error);
|
|
|
|
|
throw new SessionError('Failed to generate refresh token', 'GENERATE_TOKEN_FAILED');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Counts active sessions for a user
|
|
|
|
|
* @param {string} userId - The ID of the user
|
|
|
|
|
* @returns {Promise<number>}
|
|
|
|
|
* @throws {SessionError}
|
|
|
|
|
*/
|
|
|
|
|
const countActiveSessions = async (userId) => {
|
|
|
|
|
try {
|
|
|
|
|
if (!userId) {
|
|
|
|
|
throw new SessionError('User ID is required', 'INVALID_USER_ID');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return await Session.countDocuments({
|
|
|
|
|
user: userId,
|
|
|
|
|
expiration: { $gt: new Date() },
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error('[countActiveSessions] Error counting active sessions:', error);
|
|
|
|
|
throw new SessionError('Failed to count active sessions', 'COUNT_SESSIONS_FAILED');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
|
createSession,
|
|
|
|
|
findSession,
|
|
|
|
|
updateExpiration,
|
|
|
|
|
deleteSession,
|
|
|
|
|
deleteAllUserSessions,
|
|
|
|
|
generateRefreshToken,
|
|
|
|
|
countActiveSessions,
|
|
|
|
|
SessionError,
|
|
|
|
|
};
|