| 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, |
| }; |
|
|