Merge branch 'main' into feat/Multitenant-login-OIDC

This commit is contained in:
Ruben Talstra 2025-05-14 21:23:24 +02:00 committed by GitHub
commit a85e853e12
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
535 changed files with 28767 additions and 13591 deletions

View file

@ -1,5 +1,6 @@
const express = require('express');
const jwt = require('jsonwebtoken');
const { CacheKeys } = require('librechat-data-provider');
const { getAccessToken } = require('~/server/services/TokenService');
const { logger, getFlowStateManager } = require('~/config');
const { getLogStores } = require('~/cache');
@ -19,8 +20,8 @@ const JWT_SECRET = process.env.JWT_SECRET;
router.get('/:action_id/oauth/callback', async (req, res) => {
const { action_id } = req.params;
const { code, state } = req.query;
const flowManager = await getFlowStateManager(getLogStores);
const flowsCache = getLogStores(CacheKeys.FLOWS);
const flowManager = getFlowStateManager(flowsCache);
let identifier = action_id;
try {
let decodedState;

View file

@ -58,7 +58,7 @@ router.post('/:agent_id', async (req, res) => {
}
let { domain } = metadata;
domain = await domainParser(req, domain, true);
domain = await domainParser(domain, true);
if (!domain) {
return res.status(400).json({ message: 'No domain provided' });
@ -164,7 +164,7 @@ router.delete('/:agent_id/:action_id', async (req, res) => {
return true;
});
domain = await domainParser(req, domain, true);
domain = await domainParser(domain, true);
if (!domain) {
return res.status(400).json({ message: 'No domain provided' });

View file

@ -2,7 +2,7 @@ const express = require('express');
const { PermissionTypes, Permissions } = require('librechat-data-provider');
const {
setHeaders,
handleAbort,
moderateText,
// validateModel,
generateCheckAccess,
validateConvoAccess,
@ -14,28 +14,37 @@ const addTitle = require('~/server/services/Endpoints/agents/title');
const router = express.Router();
router.post('/abort', handleAbort());
router.use(moderateText);
const checkAgentAccess = generateCheckAccess(PermissionTypes.AGENTS, [Permissions.USE]);
router.use(checkAgentAccess);
router.use(validateConvoAccess);
router.use(buildEndpointOption);
router.use(setHeaders);
const controller = async (req, res, next) => {
await AgentController(req, res, next, initializeClient, addTitle);
};
/**
* @route POST /
* @route POST / (regular endpoint)
* @desc Chat with an assistant
* @access Public
* @param {express.Request} req - The request object, containing the request data.
* @param {express.Response} res - The response object, used to send back a response.
* @returns {void}
*/
router.post(
'/',
// validateModel,
checkAgentAccess,
validateConvoAccess,
buildEndpointOption,
setHeaders,
async (req, res, next) => {
await AgentController(req, res, next, initializeClient, addTitle);
},
);
router.post('/', controller);
/**
* @route POST /:endpoint (ephemeral agents)
* @desc Chat with an assistant
* @access Public
* @param {express.Request} req - The request object, containing the request data.
* @param {express.Response} res - The response object, used to send back a response.
* @returns {void}
*/
router.post('/:endpoint', controller);
module.exports = router;

View file

@ -1,21 +1,40 @@
const express = require('express');
const router = express.Router();
const {
uaParser,
checkBan,
requireJwtAuth,
// concurrentLimiter,
// messageIpLimiter,
// messageUserLimiter,
messageIpLimiter,
concurrentLimiter,
messageUserLimiter,
} = require('~/server/middleware');
const { isEnabled } = require('~/server/utils');
const { v1 } = require('./v1');
const chat = require('./chat');
const { LIMIT_CONCURRENT_MESSAGES, LIMIT_MESSAGE_IP, LIMIT_MESSAGE_USER } = process.env ?? {};
const router = express.Router();
router.use(requireJwtAuth);
router.use(checkBan);
router.use(uaParser);
router.use('/', v1);
router.use('/chat', chat);
const chatRouter = express.Router();
if (isEnabled(LIMIT_CONCURRENT_MESSAGES)) {
chatRouter.use(concurrentLimiter);
}
if (isEnabled(LIMIT_MESSAGE_IP)) {
chatRouter.use(messageIpLimiter);
}
if (isEnabled(LIMIT_MESSAGE_USER)) {
chatRouter.use(messageUserLimiter);
}
chatRouter.use('/', chat);
router.use('/chat', chatRouter);
module.exports = router;

View file

@ -1,4 +1,4 @@
const Keyv = require('keyv');
const { Keyv } = require('keyv');
const { KeyvFile } = require('keyv-file');
const { logger } = require('~/config');

View file

@ -11,8 +11,6 @@ const {
const router = express.Router();
router.post('/abort', handleAbort());
router.post(
'/',
validateEndpoint,

View file

@ -3,7 +3,6 @@ const AskController = require('~/server/controllers/AskController');
const { initializeClient } = require('~/server/services/Endpoints/custom');
const { addTitle } = require('~/server/services/Endpoints/openAI');
const {
handleAbort,
setHeaders,
validateModel,
validateEndpoint,
@ -12,8 +11,6 @@ const {
const router = express.Router();
router.post('/abort', handleAbort());
router.post(
'/',
validateEndpoint,

View file

@ -3,7 +3,6 @@ const AskController = require('~/server/controllers/AskController');
const { initializeClient, addTitle } = require('~/server/services/Endpoints/google');
const {
setHeaders,
handleAbort,
validateModel,
validateEndpoint,
buildEndpointOption,
@ -11,8 +10,6 @@ const {
const router = express.Router();
router.post('/abort', handleAbort());
router.post(
'/',
validateEndpoint,

View file

@ -20,7 +20,6 @@ const { logger } = require('~/config');
const router = express.Router();
router.use(moderateText);
router.post('/abort', handleAbort());
router.post(
'/',
@ -196,7 +195,8 @@ router.post(
logger.debug('[/ask/gptPlugins]', response);
const { conversation = {} } = await client.responsePromise;
const { conversation = {} } = await response.databasePromise;
delete response.databasePromise;
conversation.title =
conversation && !conversation.title ? null : conversation?.title || 'New Chat';

View file

@ -1,10 +1,4 @@
const express = require('express');
const openAI = require('./openAI');
const custom = require('./custom');
const google = require('./google');
const anthropic = require('./anthropic');
const gptPlugins = require('./gptPlugins');
const { isEnabled } = require('~/server/utils');
const { EModelEndpoint } = require('librechat-data-provider');
const {
uaParser,
@ -15,6 +9,12 @@ const {
messageUserLimiter,
validateConvoAccess,
} = require('~/server/middleware');
const { isEnabled } = require('~/server/utils');
const gptPlugins = require('./gptPlugins');
const anthropic = require('./anthropic');
const custom = require('./custom');
const google = require('./google');
const openAI = require('./openAI');
const { LIMIT_CONCURRENT_MESSAGES, LIMIT_MESSAGE_IP, LIMIT_MESSAGE_USER } = process.env ?? {};

View file

@ -12,7 +12,6 @@ const {
const router = express.Router();
router.use(moderateText);
router.post('/abort', handleAbort());
router.post(
'/',

View file

@ -36,7 +36,7 @@ router.post('/:assistant_id', async (req, res) => {
}
let { domain } = metadata;
domain = await domainParser(req, domain, true);
domain = await domainParser(domain, true);
if (!domain) {
return res.status(400).json({ message: 'No domain provided' });
@ -172,7 +172,7 @@ router.delete('/:assistant_id/:action_id/:model', async (req, res) => {
return true;
});
domain = await domainParser(req, domain, true);
domain = await domainParser(domain, true);
if (!domain) {
return res.status(400).json({ message: 'No domain provided' });

View file

@ -23,6 +23,7 @@ const {
checkInviteUser,
registerLimiter,
requireLdapAuth,
setBalanceConfig,
requireLocalAuth,
resetPasswordLimiter,
validateRegistration,
@ -40,6 +41,7 @@ router.post(
loginLimiter,
checkBan,
ldapAuth ? requireLdapAuth : requireLocalAuth,
setBalanceConfig,
loginController,
);
router.post('/refresh', refreshController);

View file

@ -4,6 +4,7 @@ const router = express.Router();
const {
setHeaders,
handleAbort,
moderateText,
// validateModel,
// validateEndpoint,
buildEndpointOption,
@ -12,7 +13,7 @@ const { initializeClient } = require('~/server/services/Endpoints/bedrock');
const AgentController = require('~/server/controllers/agents/request');
const addTitle = require('~/server/services/Endpoints/agents/title');
router.post('/abort', handleAbort());
router.use(moderateText);
/**
* @route POST /

View file

@ -1,19 +1,35 @@
const express = require('express');
const router = express.Router();
const {
uaParser,
checkBan,
requireJwtAuth,
// concurrentLimiter,
// messageIpLimiter,
// messageUserLimiter,
messageIpLimiter,
concurrentLimiter,
messageUserLimiter,
} = require('~/server/middleware');
const { isEnabled } = require('~/server/utils');
const chat = require('./chat');
const { LIMIT_CONCURRENT_MESSAGES, LIMIT_MESSAGE_IP, LIMIT_MESSAGE_USER } = process.env ?? {};
const router = express.Router();
router.use(requireJwtAuth);
router.use(checkBan);
router.use(uaParser);
if (isEnabled(LIMIT_CONCURRENT_MESSAGES)) {
router.use(concurrentLimiter);
}
if (isEnabled(LIMIT_MESSAGE_IP)) {
router.use(messageIpLimiter);
}
if (isEnabled(LIMIT_MESSAGE_USER)) {
router.use(messageUserLimiter);
}
router.use('/chat', chat);
module.exports = router;

View file

@ -68,7 +68,6 @@ router.get('/', async function (req, res) {
!!process.env.EMAIL_PASSWORD &&
!!process.env.EMAIL_FROM,
passwordResetEnabled,
checkBalance: isEnabled(process.env.CHECK_BALANCE),
showBirthdayIcon:
isBirthday() ||
isEnabled(process.env.SHOW_BIRTHDAY_ICON) ||
@ -76,11 +75,13 @@ router.get('/', async function (req, res) {
helpAndFaqURL: process.env.HELP_AND_FAQ_URL || 'https://librechat.ai',
interface: req.app.locals.interfaceConfig,
modelSpecs: req.app.locals.modelSpecs,
balance: req.app.locals.balance,
sharedLinksEnabled,
publicSharedLinksEnabled,
analyticsGtmId: process.env.ANALYTICS_GTM_ID,
instanceProjectId: instanceProject._id.toString(),
bundlerURL: process.env.SANDPACK_BUNDLER_URL,
staticBundlerURL: process.env.SANDPACK_STATIC_BUNDLER_URL,
};
if (ldap) {

View file

@ -1,16 +1,17 @@
const multer = require('multer');
const express = require('express');
const { CacheKeys, EModelEndpoint } = require('librechat-data-provider');
const { getConvosByPage, deleteConvos, getConvo, saveConvo } = require('~/models/Conversation');
const { getConvosByCursor, deleteConvos, getConvo, saveConvo } = require('~/models/Conversation');
const { forkConversation, duplicateConversation } = require('~/server/utils/import/fork');
const { storage, importFileFilter } = require('~/server/routes/files/multer');
const requireJwtAuth = require('~/server/middleware/requireJwtAuth');
const { importConversations } = require('~/server/utils/import');
const { createImportLimiters } = require('~/server/middleware');
const { deleteToolCalls } = require('~/models/ToolCall');
const { isEnabled, sleep } = require('~/server/utils');
const getLogStores = require('~/cache/getLogStores');
const { sleep } = require('~/server/utils');
const { logger } = require('~/config');
const assistantClients = {
[EModelEndpoint.azureAssistants]: require('~/server/services/Endpoints/azureAssistants'),
[EModelEndpoint.assistants]: require('~/server/services/Endpoints/assistants'),
@ -20,28 +21,30 @@ const router = express.Router();
router.use(requireJwtAuth);
router.get('/', async (req, res) => {
let pageNumber = req.query.pageNumber || 1;
pageNumber = parseInt(pageNumber, 10);
const limit = parseInt(req.query.limit, 10) || 25;
const cursor = req.query.cursor;
const isArchived = isEnabled(req.query.isArchived);
const search = req.query.search ? decodeURIComponent(req.query.search) : undefined;
const order = req.query.order || 'desc';
if (isNaN(pageNumber) || pageNumber < 1) {
return res.status(400).json({ error: 'Invalid page number' });
}
let pageSize = req.query.pageSize || 25;
pageSize = parseInt(pageSize, 10);
if (isNaN(pageSize) || pageSize < 1) {
return res.status(400).json({ error: 'Invalid page size' });
}
const isArchived = req.query.isArchived === 'true';
let tags;
if (req.query.tags) {
tags = Array.isArray(req.query.tags) ? req.query.tags : [req.query.tags];
} else {
tags = undefined;
}
res.status(200).send(await getConvosByPage(req.user.id, pageNumber, pageSize, isArchived, tags));
try {
const result = await getConvosByCursor(req.user.id, {
cursor,
limit,
isArchived,
tags,
search,
order,
});
res.status(200).json(result);
} catch (error) {
res.status(500).json({ error: 'Error fetching conversations' });
}
});
router.get('/:conversationId', async (req, res) => {
@ -76,22 +79,28 @@ router.post('/gen_title', async (req, res) => {
}
});
router.post('/clear', async (req, res) => {
router.delete('/', async (req, res) => {
let filter = {};
const { conversationId, source, thread_id, endpoint } = req.body.arg;
if (conversationId) {
filter = { conversationId };
// Prevent deletion of all conversations
if (!conversationId && !source && !thread_id && !endpoint) {
return res.status(400).json({
error: 'no parameters provided',
});
}
if (source === 'button' && !conversationId) {
if (conversationId) {
filter = { conversationId };
} else if (source === 'button') {
return res.status(200).send('No conversationId provided');
}
if (
typeof endpoint != 'undefined' &&
typeof endpoint !== 'undefined' &&
Object.prototype.propertyIsEnumerable.call(assistantClients, endpoint)
) {
/** @type {{ openai: OpenAI}} */
/** @type {{ openai: OpenAI }} */
const { openai } = await assistantClients[endpoint].initializeClient({ req, res });
try {
const response = await openai.beta.threads.del(thread_id);
@ -101,9 +110,6 @@ router.post('/clear', async (req, res) => {
}
}
// for debugging deletion source
// logger.debug('source:', source);
try {
const dbResponse = await deleteConvos(req.user.id, filter);
await deleteToolCalls(req.user.id, filter.conversationId);
@ -114,6 +120,17 @@ router.post('/clear', async (req, res) => {
}
});
router.delete('/all', async (req, res) => {
try {
const dbResponse = await deleteConvos(req.user.id, {});
await deleteToolCalls(req.user.id);
res.status(201).json(dbResponse);
} catch (error) {
logger.error('Error clearing conversations', error);
res.status(500).send('Error clearing conversations');
}
});
router.post('/update', async (req, res) => {
const update = req.body.arg;

View file

@ -3,7 +3,6 @@ const EditController = require('~/server/controllers/EditController');
const { initializeClient } = require('~/server/services/Endpoints/anthropic');
const {
setHeaders,
handleAbort,
validateModel,
validateEndpoint,
buildEndpointOption,
@ -11,8 +10,6 @@ const {
const router = express.Router();
router.post('/abort', handleAbort());
router.post(
'/',
validateEndpoint,

View file

@ -12,8 +12,6 @@ const {
const router = express.Router();
router.post('/abort', handleAbort());
router.post(
'/',
validateEndpoint,

View file

@ -3,7 +3,6 @@ const EditController = require('~/server/controllers/EditController');
const { initializeClient } = require('~/server/services/Endpoints/google');
const {
setHeaders,
handleAbort,
validateModel,
validateEndpoint,
buildEndpointOption,
@ -11,8 +10,6 @@ const {
const router = express.Router();
router.post('/abort', handleAbort());
router.post(
'/',
validateEndpoint,

View file

@ -2,7 +2,6 @@ const express = require('express');
const { getResponseSender } = require('librechat-data-provider');
const {
setHeaders,
handleAbort,
moderateText,
validateModel,
handleAbortError,
@ -19,7 +18,6 @@ const { logger } = require('~/config');
const router = express.Router();
router.use(moderateText);
router.post('/abort', handleAbort());
router.post(
'/',
@ -173,7 +171,8 @@ router.post(
logger.debug('[/edit/gptPlugins] CLIENT RESPONSE', response);
const { conversation = {} } = await client.responsePromise;
const { conversation = {} } = await response.databasePromise;
delete response.databasePromise;
conversation.title =
conversation && !conversation.title ? null : conversation?.title || 'New Chat';

View file

@ -2,7 +2,6 @@ const express = require('express');
const EditController = require('~/server/controllers/EditController');
const { initializeClient } = require('~/server/services/Endpoints/openAI');
const {
handleAbort,
setHeaders,
validateModel,
validateEndpoint,
@ -12,7 +11,6 @@ const {
const router = express.Router();
router.use(moderateText);
router.post('/abort', handleAbort());
router.post(
'/',

View file

@ -2,7 +2,9 @@ const fs = require('fs').promises;
const express = require('express');
const { EnvVar } = require('@librechat/agents');
const {
Time,
isUUID,
CacheKeys,
FileSources,
EModelEndpoint,
isAgentsEndpoint,
@ -17,8 +19,11 @@ const {
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { getOpenAIClient } = require('~/server/controllers/assistants/helpers');
const { loadAuthValues } = require('~/server/services/Tools/credentials');
const { refreshS3FileUrls } = require('~/server/services/Files/S3/crud');
const { getFiles, batchUpdateFiles } = require('~/models/File');
const { getAssistant } = require('~/models/Assistant');
const { getAgent } = require('~/models/Agent');
const { getFiles } = require('~/models/File');
const { getLogStores } = require('~/cache');
const { logger } = require('~/config');
const router = express.Router();
@ -26,6 +31,18 @@ const router = express.Router();
router.get('/', async (req, res) => {
try {
const files = await getFiles({ user: req.user.id });
if (req.app.locals.fileStrategy === FileSources.s3) {
try {
const cache = getLogStores(CacheKeys.S3_EXPIRY_INTERVAL);
const alreadyChecked = await cache.get(req.user.id);
if (!alreadyChecked) {
await refreshS3FileUrls(files, batchUpdateFiles);
await cache.set(req.user.id, true, Time.THIRTY_MINUTES);
}
} catch (error) {
logger.warn('[/files] Error refreshing S3 file URLs:', error);
}
}
res.status(200).send(files);
} catch (error) {
logger.error('[/files] Error getting files:', error);
@ -78,7 +95,7 @@ router.delete('/', async (req, res) => {
});
}
/* Handle entity unlinking even if no valid files to delete */
/* Handle agent unlinking even if no valid files to delete */
if (req.body.agent_id && req.body.tool_resource && dbFiles.length === 0) {
const agent = await getAgent({
id: req.body.agent_id,
@ -88,7 +105,21 @@ router.delete('/', async (req, res) => {
const agentFiles = files.filter((f) => toolResourceFiles.includes(f.file_id));
await processDeleteRequest({ req, files: agentFiles });
res.status(200).json({ message: 'File associations removed successfully' });
res.status(200).json({ message: 'File associations removed successfully from agent' });
return;
}
/* Handle assistant unlinking even if no valid files to delete */
if (req.body.assistant_id && req.body.tool_resource && dbFiles.length === 0) {
const assistant = await getAssistant({
id: req.body.assistant_id,
});
const toolResourceFiles = assistant.tool_resources?.[req.body.tool_resource]?.file_ids ?? [];
const assistantFiles = files.filter((f) => toolResourceFiles.includes(f.file_id));
await processDeleteRequest({ req, files: assistantFiles });
res.status(200).json({ message: 'File associations removed successfully from assistant' });
return;
}

View file

@ -10,6 +10,7 @@ const balance = require('./balance');
const plugins = require('./plugins');
const bedrock = require('./bedrock');
const actions = require('./actions');
const banner = require('./banner');
const search = require('./search');
const models = require('./models');
const convos = require('./convos');
@ -25,7 +26,6 @@ const edit = require('./edit');
const keys = require('./keys');
const user = require('./user');
const ask = require('./ask');
const banner = require('./banner');
module.exports = {
ask,
@ -38,13 +38,14 @@ module.exports = {
oauth,
files,
share,
banner,
agents,
bedrock,
convos,
search,
prompts,
config,
models,
bedrock,
prompts,
plugins,
actions,
presets,
@ -55,5 +56,4 @@ module.exports = {
assistants,
categories,
staticRoute,
banner,
};

View file

@ -10,12 +10,90 @@ const {
} = require('~/models');
const { findAllArtifacts, replaceArtifactContent } = require('~/server/services/Artifacts/update');
const { requireJwtAuth, validateMessageReq } = require('~/server/middleware');
const { cleanUpPrimaryKeyValue } = require('~/lib/utils/misc');
const { getConvosQueried } = require('~/models/Conversation');
const { countTokens } = require('~/server/utils');
const { Message } = require('~/models/Message');
const { logger } = require('~/config');
const router = express.Router();
router.use(requireJwtAuth);
router.get('/', async (req, res) => {
try {
const user = req.user.id ?? '';
const {
cursor = null,
sortBy = 'createdAt',
sortDirection = 'desc',
pageSize: pageSizeRaw,
conversationId,
messageId,
search,
} = req.query;
const pageSize = parseInt(pageSizeRaw, 10) || 25;
let response;
const sortField = ['endpoint', 'createdAt', 'updatedAt'].includes(sortBy)
? sortBy
: 'createdAt';
const sortOrder = sortDirection === 'asc' ? 1 : -1;
if (conversationId && messageId) {
const message = await Message.findOne({ conversationId, messageId, user: user }).lean();
response = { messages: message ? [message] : [], nextCursor: null };
} else if (conversationId) {
const filter = { conversationId, user: user };
if (cursor) {
filter[sortField] = sortOrder === 1 ? { $gt: cursor } : { $lt: cursor };
}
const messages = await Message.find(filter)
.sort({ [sortField]: sortOrder })
.limit(pageSize + 1)
.lean();
const nextCursor = messages.length > pageSize ? messages.pop()[sortField] : null;
response = { messages, nextCursor };
} else if (search) {
const searchResults = await Message.meiliSearch(search, undefined, true);
const messages = searchResults.hits || [];
const result = await getConvosQueried(req.user.id, messages, cursor);
const activeMessages = [];
for (let i = 0; i < messages.length; i++) {
let message = messages[i];
if (message.conversationId.includes('--')) {
message.conversationId = cleanUpPrimaryKeyValue(message.conversationId);
}
if (result.convoMap[message.conversationId]) {
const convo = result.convoMap[message.conversationId];
const dbMessage = await getMessage({ user, messageId: message.messageId });
activeMessages.push({
...message,
title: convo.title,
conversationId: message.conversationId,
model: convo.model,
isCreatedByUser: dbMessage?.isCreatedByUser,
endpoint: dbMessage?.endpoint,
iconURL: dbMessage?.iconURL,
});
}
}
response = { messages: activeMessages, nextCursor: null };
} else {
response = { messages: [], nextCursor: null };
}
res.status(200).json(response);
} catch (error) {
logger.error('Error fetching messages:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
router.post('/artifact/:messageId', async (req, res) => {
try {
const { messageId } = req.params;

View file

@ -1,7 +1,13 @@
// file deepcode ignore NoRateLimitingForLogin: Rate limiting is handled by the `loginLimiter` middleware
const express = require('express');
const passport = require('passport');
const { loginLimiter, logHeaders, checkBan, checkDomainAllowed } = require('~/server/middleware');
const {
checkBan,
logHeaders,
loginLimiter,
setBalanceConfig,
checkDomainAllowed,
} = require('~/server/middleware');
const { setAuthTokens } = require('~/server/services/AuthService');
const { logger } = require('~/config');
const { chooseOpenIdStrategy } = require('~/server/utils/openidHelper');
@ -57,6 +63,7 @@ router.get(
session: false,
scope: ['openid', 'profile', 'email'],
}),
setBalanceConfig,
oauthHandler,
);
@ -81,6 +88,7 @@ router.get(
scope: ['public_profile'],
profileFields: ['id', 'email', 'name'],
}),
setBalanceConfig,
oauthHandler,
);
@ -113,6 +121,7 @@ router.get(
next(err);
}
},
setBalanceConfig,
oauthHandler,
);
@ -135,6 +144,7 @@ router.get(
session: false,
scope: ['user:email', 'read:user'],
}),
setBalanceConfig,
oauthHandler,
);
@ -157,6 +167,7 @@ router.get(
session: false,
scope: ['identify', 'email'],
}),
setBalanceConfig,
oauthHandler,
);
@ -177,6 +188,7 @@ router.post(
failureMessage: true,
session: false,
}),
setBalanceConfig,
oauthHandler,
);

View file

@ -48,7 +48,7 @@ router.put('/:roleName/prompts', checkAdmin, async (req, res) => {
const { roleName: _r } = req.params;
// TODO: TEMP, use a better parsing for roleName
const roleName = _r.toUpperCase();
/** @type {TRole['PROMPTS']} */
/** @type {TRole['permissions']['PROMPTS']} */
const updates = req.body;
try {
@ -59,10 +59,16 @@ router.put('/:roleName/prompts', checkAdmin, async (req, res) => {
return res.status(404).send({ message: 'Role not found' });
}
const currentPermissions =
role.permissions?.[PermissionTypes.PROMPTS] || role[PermissionTypes.PROMPTS] || {};
const mergedUpdates = {
[PermissionTypes.PROMPTS]: {
...role[PermissionTypes.PROMPTS],
...parsedUpdates,
permissions: {
...role.permissions,
[PermissionTypes.PROMPTS]: {
...currentPermissions,
...parsedUpdates,
},
},
};
@ -81,7 +87,7 @@ router.put('/:roleName/agents', checkAdmin, async (req, res) => {
const { roleName: _r } = req.params;
// TODO: TEMP, use a better parsing for roleName
const roleName = _r.toUpperCase();
/** @type {TRole['AGENTS']} */
/** @type {TRole['permissions']['AGENTS']} */
const updates = req.body;
try {
@ -92,17 +98,23 @@ router.put('/:roleName/agents', checkAdmin, async (req, res) => {
return res.status(404).send({ message: 'Role not found' });
}
const currentPermissions =
role.permissions?.[PermissionTypes.AGENTS] || role[PermissionTypes.AGENTS] || {};
const mergedUpdates = {
[PermissionTypes.AGENTS]: {
...role[PermissionTypes.AGENTS],
...parsedUpdates,
permissions: {
...role.permissions,
[PermissionTypes.AGENTS]: {
...currentPermissions,
...parsedUpdates,
},
},
};
const updatedRole = await updateRoleByName(roleName, mergedUpdates);
res.status(200).send(updatedRole);
} catch (error) {
return res.status(400).send({ message: 'Invalid prompt permissions.', error: error.errors });
return res.status(400).send({ message: 'Invalid agent permissions.', error: error.errors });
}
});

View file

@ -1,93 +1,17 @@
const Keyv = require('keyv');
const express = require('express');
const { MeiliSearch } = require('meilisearch');
const { Conversation, getConvosQueried } = require('~/models/Conversation');
const requireJwtAuth = require('~/server/middleware/requireJwtAuth');
const { cleanUpPrimaryKeyValue } = require('~/lib/utils/misc');
const { reduceHits } = require('~/lib/utils/reduceHits');
const { isEnabled } = require('~/server/utils');
const { Message } = require('~/models/Message');
const keyvRedis = require('~/cache/keyvRedis');
const { logger } = require('~/config');
const router = express.Router();
const expiration = 60 * 1000;
const cache = isEnabled(process.env.USE_REDIS)
? new Keyv({ store: keyvRedis })
: new Keyv({ namespace: 'search', ttl: expiration });
router.use(requireJwtAuth);
router.get('/sync', async function (req, res) {
await Message.syncWithMeili();
await Conversation.syncWithMeili();
res.send('synced');
});
router.get('/', async function (req, res) {
try {
let user = req.user.id ?? '';
const { q } = req.query;
const pageNumber = req.query.pageNumber || 1;
const key = `${user}:search:${q}`;
const cached = await cache.get(key);
if (cached) {
logger.debug('[/search] cache hit: ' + key);
const { pages, pageSize, messages } = cached;
res
.status(200)
.send({ conversations: cached[pageNumber], pages, pageNumber, pageSize, messages });
return;
}
const messages = (await Message.meiliSearch(q, undefined, true)).hits;
const titles = (await Conversation.meiliSearch(q)).hits;
const sortedHits = reduceHits(messages, titles);
const result = await getConvosQueried(user, sortedHits, pageNumber);
const activeMessages = [];
for (let i = 0; i < messages.length; i++) {
let message = messages[i];
if (message.conversationId.includes('--')) {
message.conversationId = cleanUpPrimaryKeyValue(message.conversationId);
}
if (result.convoMap[message.conversationId]) {
const convo = result.convoMap[message.conversationId];
const { title, chatGptLabel, model } = convo;
message = { ...message, ...{ title, chatGptLabel, model } };
activeMessages.push(message);
}
}
result.messages = activeMessages;
if (result.cache) {
result.cache.messages = activeMessages;
cache.set(key, result.cache, expiration);
delete result.cache;
}
delete result.convoMap;
res.status(200).send(result);
} catch (error) {
logger.error('[/search] Error while searching messages & conversations', error);
res.status(500).send({ message: 'Error searching' });
}
});
router.get('/test', async function (req, res) {
const { q } = req.query;
const messages = (
await Message.meiliSearch(q, { attributesToHighlight: ['text'] }, true)
).hits.map((message) => {
const { _formatted, ...rest } = message;
return { ...rest, searchResult: true, text: _formatted.text };
});
res.send(messages);
});
router.get('/enable', async function (req, res) {
let result = false;
if (!isEnabled(process.env.SEARCH)) {
return res.send(false);
}
try {
const client = new MeiliSearch({
host: process.env.MEILI_HOST,
@ -95,8 +19,7 @@ router.get('/enable', async function (req, res) {
});
const { status } = await client.health();
result = status === 'available' && !!process.env.SEARCH;
return res.send(result);
return res.send(status === 'available');
} catch (error) {
return res.send(false);
}