| | const { logger, webSearchKeys } = require('@librechat/data-schemas'); |
| | const { Tools, CacheKeys, Constants, FileSources } = require('librechat-data-provider'); |
| | const { |
| | MCPOAuthHandler, |
| | MCPTokenStorage, |
| | mcpServersRegistry, |
| | normalizeHttpError, |
| | extractWebSearchEnvVars, |
| | } = require('@librechat/api'); |
| | const { |
| | deleteAllUserSessions, |
| | deleteAllSharedLinks, |
| | deleteUserById, |
| | deleteMessages, |
| | deletePresets, |
| | deleteConvos, |
| | deleteFiles, |
| | updateUser, |
| | findToken, |
| | getFiles, |
| | } = require('~/models'); |
| | const { |
| | ConversationTag, |
| | Transaction, |
| | MemoryEntry, |
| | Assistant, |
| | AclEntry, |
| | Balance, |
| | Action, |
| | Group, |
| | Token, |
| | User, |
| | } = require('~/db/models'); |
| | const { updateUserPluginAuth, deleteUserPluginAuth } = require('~/server/services/PluginService'); |
| | const { updateUserPluginsService, deleteUserKey } = require('~/server/services/UserService'); |
| | const { verifyEmail, resendVerificationEmail } = require('~/server/services/AuthService'); |
| | const { needsRefresh, getNewS3URL } = require('~/server/services/Files/S3/crud'); |
| | const { processDeleteRequest } = require('~/server/services/Files/process'); |
| | const { getMCPManager, getFlowStateManager } = require('~/config'); |
| | const { getAppConfig } = require('~/server/services/Config'); |
| | const { deleteToolCalls } = require('~/models/ToolCall'); |
| | const { deleteUserPrompts } = require('~/models/Prompt'); |
| | const { deleteUserAgents } = require('~/models/Agent'); |
| | const { getLogStores } = require('~/cache'); |
| |
|
| | const getUserController = async (req, res) => { |
| | const appConfig = await getAppConfig({ role: req.user?.role }); |
| | |
| | const userData = req.user.toObject != null ? req.user.toObject() : { ...req.user }; |
| | |
| | |
| | |
| | |
| | delete userData.password; |
| | delete userData.totpSecret; |
| | delete userData.backupCodes; |
| | if (appConfig.fileStrategy === FileSources.s3 && userData.avatar) { |
| | const avatarNeedsRefresh = needsRefresh(userData.avatar, 3600); |
| | if (!avatarNeedsRefresh) { |
| | return res.status(200).send(userData); |
| | } |
| | const originalAvatar = userData.avatar; |
| | try { |
| | userData.avatar = await getNewS3URL(userData.avatar); |
| | await updateUser(userData.id, { avatar: userData.avatar }); |
| | } catch (error) { |
| | userData.avatar = originalAvatar; |
| | logger.error('Error getting new S3 URL for avatar:', error); |
| | } |
| | } |
| | res.status(200).send(userData); |
| | }; |
| |
|
| | const getTermsStatusController = async (req, res) => { |
| | try { |
| | const user = await User.findById(req.user.id); |
| | if (!user) { |
| | return res.status(404).json({ message: 'User not found' }); |
| | } |
| | res.status(200).json({ termsAccepted: !!user.termsAccepted }); |
| | } catch (error) { |
| | logger.error('Error fetching terms acceptance status:', error); |
| | res.status(500).json({ message: 'Error fetching terms acceptance status' }); |
| | } |
| | }; |
| |
|
| | const acceptTermsController = async (req, res) => { |
| | try { |
| | const user = await User.findByIdAndUpdate(req.user.id, { termsAccepted: true }, { new: true }); |
| | if (!user) { |
| | return res.status(404).json({ message: 'User not found' }); |
| | } |
| | res.status(200).json({ message: 'Terms accepted successfully' }); |
| | } catch (error) { |
| | logger.error('Error accepting terms:', error); |
| | res.status(500).json({ message: 'Error accepting terms' }); |
| | } |
| | }; |
| |
|
| | const deleteUserFiles = async (req) => { |
| | try { |
| | const userFiles = await getFiles({ user: req.user.id }); |
| | await processDeleteRequest({ |
| | req, |
| | files: userFiles, |
| | }); |
| | } catch (error) { |
| | logger.error('[deleteUserFiles]', error); |
| | } |
| | }; |
| |
|
| | const updateUserPluginsController = async (req, res) => { |
| | const appConfig = await getAppConfig({ role: req.user?.role }); |
| | const { user } = req; |
| | const { pluginKey, action, auth, isEntityTool } = req.body; |
| | try { |
| | if (!isEntityTool) { |
| | const userPluginsService = await updateUserPluginsService(user, pluginKey, action); |
| |
|
| | if (userPluginsService instanceof Error) { |
| | logger.error('[userPluginsService]', userPluginsService); |
| | const { status, message } = normalizeHttpError(userPluginsService); |
| | return res.status(status).send({ message }); |
| | } |
| | } |
| |
|
| | if (auth == null) { |
| | return res.status(200).send(); |
| | } |
| |
|
| | let keys = Object.keys(auth); |
| | const values = Object.values(auth); |
| |
|
| | const isMCPTool = pluginKey.startsWith('mcp_') || pluginKey.includes(Constants.mcp_delimiter); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | if ( |
| | keys.length === 0 && |
| | pluginKey !== Tools.web_search && |
| | !(action === 'uninstall' && isMCPTool) |
| | ) { |
| | return res.status(200).send(); |
| | } |
| |
|
| | |
| | let status = 200; |
| | |
| | let message; |
| | |
| | let authService; |
| |
|
| | if (pluginKey === Tools.web_search) { |
| | |
| | const webSearchConfig = appConfig?.webSearch; |
| | keys = extractWebSearchEnvVars({ |
| | keys: action === 'install' ? keys : webSearchKeys, |
| | config: webSearchConfig, |
| | }); |
| | } |
| |
|
| | if (action === 'install') { |
| | for (let i = 0; i < keys.length; i++) { |
| | authService = await updateUserPluginAuth(user.id, keys[i], pluginKey, values[i]); |
| | if (authService instanceof Error) { |
| | logger.error('[authService]', authService); |
| | ({ status, message } = normalizeHttpError(authService)); |
| | } |
| | } |
| | } else if (action === 'uninstall') { |
| | |
| | if (isMCPTool && keys.length === 0) { |
| | |
| | |
| | authService = await deleteUserPluginAuth(user.id, null, true, pluginKey); |
| | if (authService instanceof Error) { |
| | logger.error( |
| | `[authService] Error deleting all auth for MCP tool ${pluginKey}:`, |
| | authService, |
| | ); |
| | ({ status, message } = normalizeHttpError(authService)); |
| | } |
| | try { |
| | |
| | await maybeUninstallOAuthMCP(user.id, pluginKey, appConfig); |
| | } catch (error) { |
| | logger.error( |
| | `[updateUserPluginsController] Error uninstalling OAuth MCP for ${pluginKey}:`, |
| | error, |
| | ); |
| | } |
| | } else { |
| | |
| | |
| | |
| | |
| | |
| | for (let i = 0; i < keys.length; i++) { |
| | authService = await deleteUserPluginAuth(user.id, keys[i]); |
| | if (authService instanceof Error) { |
| | logger.error('[authService] Error deleting specific auth key:', authService); |
| | ({ status, message } = normalizeHttpError(authService)); |
| | } |
| | } |
| | } |
| | } |
| |
|
| | if (status === 200) { |
| | |
| | if (pluginKey.startsWith(Constants.mcp_prefix)) { |
| | try { |
| | const mcpManager = getMCPManager(); |
| | if (mcpManager) { |
| | |
| | const serverName = pluginKey.replace(Constants.mcp_prefix, ''); |
| | logger.info( |
| | `[updateUserPluginsController] Attempting disconnect of MCP server "${serverName}" for user ${user.id} after plugin auth update.`, |
| | ); |
| | await mcpManager.disconnectUserConnection(user.id, serverName); |
| | } |
| | } catch (disconnectError) { |
| | logger.error( |
| | `[updateUserPluginsController] Error disconnecting MCP connection for user ${user.id} after plugin auth update:`, |
| | disconnectError, |
| | ); |
| | |
| | } |
| | } |
| | return res.status(status).send(); |
| | } |
| |
|
| | const normalized = normalizeHttpError({ status, message }); |
| | return res.status(normalized.status).send({ message: normalized.message }); |
| | } catch (err) { |
| | logger.error('[updateUserPluginsController]', err); |
| | return res.status(500).json({ message: 'Something went wrong.' }); |
| | } |
| | }; |
| |
|
| | const deleteUserController = async (req, res) => { |
| | const { user } = req; |
| |
|
| | try { |
| | await deleteMessages({ user: user.id }); |
| | await deleteAllUserSessions({ userId: user.id }); |
| | await Transaction.deleteMany({ user: user.id }); |
| | await deleteUserKey({ userId: user.id, all: true }); |
| | await Balance.deleteMany({ user: user._id }); |
| | await deletePresets(user.id); |
| | try { |
| | await deleteConvos(user.id); |
| | } catch (error) { |
| | logger.error('[deleteUserController] Error deleting user convos, likely no convos', error); |
| | } |
| | await deleteUserPluginAuth(user.id, null, true); |
| | await deleteUserById(user.id); |
| | await deleteAllSharedLinks(user.id); |
| | await deleteUserFiles(req); |
| | await deleteFiles(null, user.id); |
| | await deleteToolCalls(user.id); |
| | await deleteUserAgents(user.id); |
| | await Assistant.deleteMany({ user: user.id }); |
| | await ConversationTag.deleteMany({ user: user.id }); |
| | await MemoryEntry.deleteMany({ userId: user.id }); |
| | await deleteUserPrompts(req, user.id); |
| | await Action.deleteMany({ user: user.id }); |
| | await Token.deleteMany({ userId: user.id }); |
| | await Group.updateMany( |
| | |
| | { memberIds: user.id }, |
| | { $pull: { memberIds: user.id } }, |
| | ); |
| | await AclEntry.deleteMany({ principalId: user._id }); |
| | logger.info(`User deleted account. Email: ${user.email} ID: ${user.id}`); |
| | res.status(200).send({ message: 'User deleted' }); |
| | } catch (err) { |
| | logger.error('[deleteUserController]', err); |
| | return res.status(500).json({ message: 'Something went wrong.' }); |
| | } |
| | }; |
| |
|
| | const verifyEmailController = async (req, res) => { |
| | try { |
| | const verifyEmailService = await verifyEmail(req); |
| | if (verifyEmailService instanceof Error) { |
| | return res.status(400).json(verifyEmailService); |
| | } else { |
| | return res.status(200).json(verifyEmailService); |
| | } |
| | } catch (e) { |
| | logger.error('[verifyEmailController]', e); |
| | return res.status(500).json({ message: 'Something went wrong.' }); |
| | } |
| | }; |
| |
|
| | const resendVerificationController = async (req, res) => { |
| | try { |
| | const result = await resendVerificationEmail(req); |
| | if (result instanceof Error) { |
| | return res.status(400).json(result); |
| | } else { |
| | return res.status(200).json(result); |
| | } |
| | } catch (e) { |
| | logger.error('[verifyEmailController]', e); |
| | return res.status(500).json({ message: 'Something went wrong.' }); |
| | } |
| | }; |
| |
|
| | |
| | |
| | |
| | const maybeUninstallOAuthMCP = async (userId, pluginKey, appConfig) => { |
| | if (!pluginKey.startsWith(Constants.mcp_prefix)) { |
| | |
| | return; |
| | } |
| |
|
| | const serverName = pluginKey.replace(Constants.mcp_prefix, ''); |
| | const serverConfig = |
| | (await mcpServersRegistry.getServerConfig(serverName, userId)) ?? |
| | appConfig?.mcpServers?.[serverName]; |
| | const oauthServers = await mcpServersRegistry.getOAuthServers(); |
| | if (!oauthServers.has(serverName)) { |
| | |
| | return; |
| | } |
| |
|
| | |
| | const clientTokenData = await MCPTokenStorage.getClientInfoAndMetadata({ |
| | userId, |
| | serverName, |
| | findToken, |
| | }); |
| | if (clientTokenData == null) { |
| | return; |
| | } |
| | const { clientInfo, clientMetadata } = clientTokenData; |
| |
|
| | |
| | const tokens = await MCPTokenStorage.getTokens({ |
| | userId, |
| | serverName, |
| | findToken, |
| | }); |
| |
|
| | |
| | const revocationEndpoint = |
| | serverConfig.oauth?.revocation_endpoint ?? clientMetadata.revocation_endpoint; |
| | const revocationEndpointAuthMethodsSupported = |
| | serverConfig.oauth?.revocation_endpoint_auth_methods_supported ?? |
| | clientMetadata.revocation_endpoint_auth_methods_supported; |
| | const oauthHeaders = serverConfig.oauth_headers ?? {}; |
| |
|
| | if (tokens?.access_token) { |
| | try { |
| | await MCPOAuthHandler.revokeOAuthToken( |
| | serverName, |
| | tokens.access_token, |
| | 'access', |
| | { |
| | serverUrl: serverConfig.url, |
| | clientId: clientInfo.client_id, |
| | clientSecret: clientInfo.client_secret ?? '', |
| | revocationEndpoint, |
| | revocationEndpointAuthMethodsSupported, |
| | }, |
| | oauthHeaders, |
| | ); |
| | } catch (error) { |
| | logger.error(`Error revoking OAuth access token for ${serverName}:`, error); |
| | } |
| | } |
| |
|
| | if (tokens?.refresh_token) { |
| | try { |
| | await MCPOAuthHandler.revokeOAuthToken( |
| | serverName, |
| | tokens.refresh_token, |
| | 'refresh', |
| | { |
| | serverUrl: serverConfig.url, |
| | clientId: clientInfo.client_id, |
| | clientSecret: clientInfo.client_secret ?? '', |
| | revocationEndpoint, |
| | revocationEndpointAuthMethodsSupported, |
| | }, |
| | oauthHeaders, |
| | ); |
| | } catch (error) { |
| | logger.error(`Error revoking OAuth refresh token for ${serverName}:`, error); |
| | } |
| | } |
| |
|
| | |
| | await MCPTokenStorage.deleteUserTokens({ |
| | userId, |
| | serverName, |
| | deleteToken: async (filter) => { |
| | await Token.deleteOne(filter); |
| | }, |
| | }); |
| |
|
| | |
| | const flowsCache = getLogStores(CacheKeys.FLOWS); |
| | const flowManager = getFlowStateManager(flowsCache); |
| | const flowId = MCPOAuthHandler.generateFlowId(userId, serverName); |
| | await flowManager.deleteFlow(flowId, 'mcp_get_tokens'); |
| | await flowManager.deleteFlow(flowId, 'mcp_oauth'); |
| | }; |
| |
|
| | module.exports = { |
| | getUserController, |
| | getTermsStatusController, |
| | acceptTermsController, |
| | deleteUserController, |
| | verifyEmailController, |
| | updateUserPluginsController, |
| | resendVerificationController, |
| | }; |
| |
|