mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-22 07:36:33 +01:00
* fix: use ACL ownership for prompt group cleanup on user deletion deleteUserPrompts previously called getAllPromptGroups with only an author filter, which defaults to searchShared=true and drops the author filter for shared/global project entries. This caused any user deleting their account to strip shared prompt group associations and ACL entries for other users. Replace the author-based query with ACL-based ownership lookup: - Find prompt groups where the user has OWNER permission (DELETE bit) - Only delete groups where the user is the sole owner - Preserve multi-owned groups and their ACL entries for other owners * fix: use ACL ownership for agent cleanup on user deletion deleteUserAgents used the deprecated author field to find and delete agents, then unconditionally removed all ACL entries for those agents. This could destroy ACL entries for agents shared with or co-owned by other users. Replace the author-based query with ACL-based ownership lookup: - Find agents where the user has OWNER permission (DELETE bit) - Only delete agents where the user is the sole owner - Preserve multi-owned agents and their ACL entries for other owners - Also clean up handoff edges referencing deleted agents * fix: add MCP server cleanup on user deletion User deletion had no cleanup for MCP servers, leaving solely-owned servers orphaned in the database with dangling ACL entries for other users. Add deleteUserMcpServers that follows the same ACL ownership pattern as prompt groups and agents: find servers with OWNER permission, check for sole ownership, and only delete those with no other owners. * style: fix prettier formatting in Prompt.spec.js * refactor: extract getSoleOwnedResourceIds to PermissionService The ACL sole-ownership detection algorithm was duplicated across deleteUserPrompts, deleteUserAgents, and deleteUserMcpServers. Centralizes the three-step pattern (find owned entries, find other owners, compute sole-owned set) into a single reusable utility. * refactor: use getSoleOwnedResourceIds in all deletion functions - Replace inline ACL queries with the centralized utility - Remove vestigial _req parameter from deleteUserPrompts - Use Promise.all for parallel project removal instead of sequential awaits - Disconnect live MCP sessions and invalidate tool cache before deleting sole-owned MCP server documents - Export deleteUserMcpServers for testability * test: improve deletion test coverage and quality - Move deleteUserPrompts call to beforeAll to eliminate execution-order dependency between tests - Standardize on test() instead of it() for consistency in Prompt.spec.js - Add assertion for deleting user's own ACL entry preservation on multi-owned agents - Add deleteUserMcpServers integration test suite with 6 tests covering sole-owner deletion, multi-owner preservation, session disconnect, cache invalidation, model-not-registered guard, and missing MCPManager - Add PermissionService mock to existing deleteUser.spec.js to fix import chain * fix: add legacy author-based fallback for unmigrated resources Resources created before the ACL system have author set but no AclEntry records. The sole-ownership detection returns empty for these, causing deleteUserPrompts, deleteUserAgents, and deleteUserMcpServers to silently skip them — permanently orphaning data on user deletion. Add a fallback that identifies author-owned resources with zero ACL entries (truly unmigrated) and includes them in the deletion set. This preserves the multi-owner safety of the ACL path while ensuring pre-ACL resources are still cleaned up regardless of migration status. * style: fix prettier formatting across all changed files * test: add resource type coverage guard for user deletion Ensures every ResourceType in the ACL system has a corresponding cleanup handler wired into deleteUserController. When a new ResourceType is added (e.g. WORKFLOW), this test fails immediately, preventing silent data orphaning on user account deletion. * style: fix import order in PermissionService destructure * test: add opt-out set and fix test lifecycle in coverage guard Add NO_USER_CLEANUP_NEEDED set for resource types that legitimately require no per-user deletion. Move fs.readFileSync into beforeAll so path errors surface as clean test failures instead of unhandled crashes.
524 lines
18 KiB
JavaScript
524 lines
18 KiB
JavaScript
const mongoose = require('mongoose');
|
|
const { logger, webSearchKeys } = require('@librechat/data-schemas');
|
|
const {
|
|
MCPOAuthHandler,
|
|
MCPTokenStorage,
|
|
normalizeHttpError,
|
|
extractWebSearchEnvVars,
|
|
} = require('@librechat/api');
|
|
const {
|
|
Tools,
|
|
CacheKeys,
|
|
Constants,
|
|
FileSources,
|
|
ResourceType,
|
|
} = require('librechat-data-provider');
|
|
const {
|
|
deleteAllUserSessions,
|
|
deleteAllSharedLinks,
|
|
updateUserPlugins,
|
|
deleteUserById,
|
|
deleteMessages,
|
|
deletePresets,
|
|
deleteUserKey,
|
|
getUserById,
|
|
deleteConvos,
|
|
deleteFiles,
|
|
updateUser,
|
|
findToken,
|
|
getFiles,
|
|
} = require('~/models');
|
|
const {
|
|
ConversationTag,
|
|
AgentApiKey,
|
|
Transaction,
|
|
MemoryEntry,
|
|
Assistant,
|
|
AclEntry,
|
|
Balance,
|
|
Action,
|
|
Group,
|
|
Token,
|
|
User,
|
|
} = require('~/db/models');
|
|
const { updateUserPluginAuth, deleteUserPluginAuth } = require('~/server/services/PluginService');
|
|
const { verifyOTPOrBackupCode } = require('~/server/services/twoFactorService');
|
|
const { verifyEmail, resendVerificationEmail } = require('~/server/services/AuthService');
|
|
const { getMCPManager, getFlowStateManager, getMCPServersRegistry } = require('~/config');
|
|
const { invalidateCachedTools } = require('~/server/services/Config/getCachedTools');
|
|
const { needsRefresh, getNewS3URL } = require('~/server/services/Files/S3/crud');
|
|
const { processDeleteRequest } = require('~/server/services/Files/process');
|
|
const { getAppConfig } = require('~/server/services/Config');
|
|
const { deleteToolCalls } = require('~/models/ToolCall');
|
|
const { deleteUserPrompts } = require('~/models/Prompt');
|
|
const { deleteUserAgents } = require('~/models/Agent');
|
|
const { getSoleOwnedResourceIds } = require('~/server/services/PermissionService');
|
|
const { getLogStores } = require('~/cache');
|
|
|
|
const getUserController = async (req, res) => {
|
|
const appConfig = await getAppConfig({ role: req.user?.role });
|
|
/** @type {IUser} */
|
|
const userData = req.user.toObject != null ? req.user.toObject() : { ...req.user };
|
|
/**
|
|
* These fields should not exist due to secure field selection, but deletion
|
|
* is done in case of alternate database incompatibility with Mongo API
|
|
* */
|
|
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);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Deletes MCP servers solely owned by the user and cleans up their ACLs.
|
|
* Disconnects live sessions for deleted servers before removing DB records.
|
|
* Servers with other owners are left intact; the caller is responsible for
|
|
* removing the user's own ACL principal entries separately.
|
|
*
|
|
* Also handles legacy (pre-ACL) MCP servers that only have the author field set,
|
|
* ensuring they are not orphaned if no permission migration has been run.
|
|
* @param {string} userId - The ID of the user.
|
|
*/
|
|
const deleteUserMcpServers = async (userId) => {
|
|
try {
|
|
const MCPServer = mongoose.models.MCPServer;
|
|
if (!MCPServer) {
|
|
return;
|
|
}
|
|
|
|
const userObjectId = new mongoose.Types.ObjectId(userId);
|
|
const soleOwnedIds = await getSoleOwnedResourceIds(userObjectId, ResourceType.MCPSERVER);
|
|
|
|
const authoredServers = await MCPServer.find({ author: userObjectId })
|
|
.select('_id serverName')
|
|
.lean();
|
|
|
|
const migratedEntries =
|
|
authoredServers.length > 0
|
|
? await AclEntry.find({
|
|
resourceType: ResourceType.MCPSERVER,
|
|
resourceId: { $in: authoredServers.map((s) => s._id) },
|
|
})
|
|
.select('resourceId')
|
|
.lean()
|
|
: [];
|
|
const migratedIds = new Set(migratedEntries.map((e) => e.resourceId.toString()));
|
|
const legacyServers = authoredServers.filter((s) => !migratedIds.has(s._id.toString()));
|
|
const legacyServerIds = legacyServers.map((s) => s._id);
|
|
|
|
const allServerIdsToDelete = [...soleOwnedIds, ...legacyServerIds];
|
|
|
|
if (allServerIdsToDelete.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const aclOwnedServers =
|
|
soleOwnedIds.length > 0
|
|
? await MCPServer.find({ _id: { $in: soleOwnedIds } })
|
|
.select('serverName')
|
|
.lean()
|
|
: [];
|
|
const allServersToDelete = [...aclOwnedServers, ...legacyServers];
|
|
|
|
const mcpManager = getMCPManager();
|
|
if (mcpManager) {
|
|
await Promise.all(
|
|
allServersToDelete.map(async (s) => {
|
|
await mcpManager.disconnectUserConnection(userId, s.serverName);
|
|
await invalidateCachedTools({ userId, serverName: s.serverName });
|
|
}),
|
|
);
|
|
}
|
|
|
|
await AclEntry.deleteMany({
|
|
resourceType: ResourceType.MCPSERVER,
|
|
resourceId: { $in: allServerIdsToDelete },
|
|
});
|
|
|
|
await MCPServer.deleteMany({ _id: { $in: allServerIdsToDelete } });
|
|
} catch (error) {
|
|
logger.error('[deleteUserMcpServers] General error:', 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) {
|
|
await updateUserPlugins(user._id, user.plugins, pluginKey, action);
|
|
}
|
|
|
|
if (auth == null) {
|
|
return res.status(200).send();
|
|
}
|
|
|
|
let keys = Object.keys(auth);
|
|
const values = Object.values(auth); // Used in 'install' block
|
|
|
|
const isMCPTool = pluginKey.startsWith('mcp_') || pluginKey.includes(Constants.mcp_delimiter);
|
|
|
|
// Early exit condition:
|
|
// If keys are empty (meaning auth: {} was likely sent for uninstall, or auth was empty for install)
|
|
// AND it's not web_search (which has special key handling to populate `keys` for uninstall)
|
|
// AND it's NOT (an uninstall action FOR an MCP tool - we need to proceed for this case to clear all its auth)
|
|
// THEN return.
|
|
if (
|
|
keys.length === 0 &&
|
|
pluginKey !== Tools.web_search &&
|
|
!(action === 'uninstall' && isMCPTool)
|
|
) {
|
|
return res.status(200).send();
|
|
}
|
|
|
|
/** @type {number} */
|
|
let status = 200;
|
|
/** @type {string} */
|
|
let message;
|
|
/** @type {IPluginAuth | Error} */
|
|
let authService;
|
|
|
|
if (pluginKey === Tools.web_search) {
|
|
/** @type {TCustomConfig['webSearch']} */
|
|
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') {
|
|
// const isMCPTool was defined earlier
|
|
if (isMCPTool && keys.length === 0) {
|
|
// This handles the case where auth: {} is sent for an MCP tool uninstall.
|
|
// It means "delete all credentials associated with this MCP pluginKey".
|
|
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 {
|
|
// if the MCP server uses OAuth, perform a full cleanup and token revocation
|
|
await maybeUninstallOAuthMCP(user.id, pluginKey, appConfig);
|
|
} catch (error) {
|
|
logger.error(
|
|
`[updateUserPluginsController] Error uninstalling OAuth MCP for ${pluginKey}:`,
|
|
error,
|
|
);
|
|
}
|
|
} else {
|
|
// This handles:
|
|
// 1. Web_search uninstall (keys will be populated with all webSearchKeys if auth was {}).
|
|
// 2. Other tools uninstall (if keys were provided).
|
|
// 3. MCP tool uninstall if specific keys were provided in `auth` (not current frontend behavior).
|
|
// If keys is empty for non-MCP tools (and not web_search), this loop won't run, and nothing is deleted.
|
|
for (let i = 0; i < keys.length; i++) {
|
|
authService = await deleteUserPluginAuth(user.id, keys[i]); // Deletes by authField name
|
|
if (authService instanceof Error) {
|
|
logger.error('[authService] Error deleting specific auth key:', authService);
|
|
({ status, message } = normalizeHttpError(authService));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (status === 200) {
|
|
// If auth was updated successfully, disconnect MCP sessions as they might use these credentials
|
|
if (pluginKey.startsWith(Constants.mcp_prefix)) {
|
|
try {
|
|
const mcpManager = getMCPManager();
|
|
if (mcpManager) {
|
|
// Extract server name from pluginKey (format: "mcp_<serverName>")
|
|
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);
|
|
await invalidateCachedTools({ userId: user.id, serverName });
|
|
}
|
|
} catch (disconnectError) {
|
|
logger.error(
|
|
`[updateUserPluginsController] Error disconnecting MCP connection for user ${user.id} after plugin auth update:`,
|
|
disconnectError,
|
|
);
|
|
// Do not fail the request for this, but log it.
|
|
}
|
|
}
|
|
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 {
|
|
const existingUser = await getUserById(
|
|
user.id,
|
|
'+totpSecret +backupCodes _id twoFactorEnabled',
|
|
);
|
|
if (existingUser && existingUser.twoFactorEnabled) {
|
|
const { token, backupCode } = req.body;
|
|
const result = await verifyOTPOrBackupCode({ user: existingUser, token, backupCode });
|
|
|
|
if (!result.verified) {
|
|
const msg =
|
|
result.message ??
|
|
'TOTP token or backup code is required to delete account with 2FA enabled';
|
|
return res.status(result.status ?? 400).json({ message: msg });
|
|
}
|
|
}
|
|
|
|
await deleteMessages({ user: user.id }); // delete user messages
|
|
await deleteAllUserSessions({ userId: user.id }); // delete user sessions
|
|
await Transaction.deleteMany({ user: user.id }); // delete user transactions
|
|
await deleteUserKey({ userId: user.id, all: true }); // delete user keys
|
|
await Balance.deleteMany({ user: user._id }); // delete user balances
|
|
await deletePresets(user.id); // delete user presets
|
|
try {
|
|
await deleteConvos(user.id); // delete user convos
|
|
} catch (error) {
|
|
logger.error('[deleteUserController] Error deleting user convos, likely no convos', error);
|
|
}
|
|
await deleteUserPluginAuth(user.id, null, true); // delete user plugin auth
|
|
await deleteUserById(user.id); // delete user
|
|
await deleteAllSharedLinks(user.id); // delete user shared links
|
|
await deleteUserFiles(req); // delete user files
|
|
await deleteFiles(null, user.id); // delete database files in case of orphaned files from previous steps
|
|
await deleteToolCalls(user.id); // delete user tool calls
|
|
await deleteUserAgents(user.id); // delete user agents
|
|
await AgentApiKey.deleteMany({ user: user._id }); // delete user agent API keys
|
|
await Assistant.deleteMany({ user: user.id }); // delete user assistants
|
|
await ConversationTag.deleteMany({ user: user.id }); // delete user conversation tags
|
|
await MemoryEntry.deleteMany({ userId: user.id }); // delete user memory entries
|
|
await deleteUserPrompts(user.id); // delete user prompts
|
|
await deleteUserMcpServers(user.id); // delete user MCP servers
|
|
await Action.deleteMany({ user: user.id }); // delete user actions
|
|
await Token.deleteMany({ userId: user.id }); // delete user OAuth tokens
|
|
await Group.updateMany(
|
|
// remove user from all groups
|
|
{ memberIds: user.id },
|
|
{ $pull: { memberIds: user.id } },
|
|
);
|
|
await AclEntry.deleteMany({ principalId: user._id }); // delete user ACL entries
|
|
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.' });
|
|
}
|
|
};
|
|
|
|
/**
|
|
* OAuth MCP specific uninstall logic
|
|
*/
|
|
const maybeUninstallOAuthMCP = async (userId, pluginKey, appConfig) => {
|
|
if (!pluginKey.startsWith(Constants.mcp_prefix)) {
|
|
// this is not an MCP server, so nothing to do here
|
|
return;
|
|
}
|
|
|
|
const serverName = pluginKey.replace(Constants.mcp_prefix, '');
|
|
const serverConfig =
|
|
(await getMCPServersRegistry().getServerConfig(serverName, userId)) ??
|
|
appConfig?.mcpServers?.[serverName];
|
|
const oauthServers = await getMCPServersRegistry().getOAuthServers(userId);
|
|
if (!oauthServers.has(serverName)) {
|
|
// this server does not use OAuth, so nothing to do here as well
|
|
return;
|
|
}
|
|
|
|
// 1. get client info used for revocation (client id, secret)
|
|
const clientTokenData = await MCPTokenStorage.getClientInfoAndMetadata({
|
|
userId,
|
|
serverName,
|
|
findToken,
|
|
});
|
|
if (clientTokenData == null) {
|
|
return;
|
|
}
|
|
const { clientInfo, clientMetadata } = clientTokenData;
|
|
|
|
// 2. get decrypted tokens before deletion
|
|
const tokens = await MCPTokenStorage.getTokens({
|
|
userId,
|
|
serverName,
|
|
findToken,
|
|
});
|
|
|
|
// 3. revoke OAuth tokens at the provider
|
|
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 ?? {};
|
|
const allowedDomains = getMCPServersRegistry().getAllowedDomains();
|
|
|
|
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,
|
|
allowedDomains,
|
|
);
|
|
} 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,
|
|
allowedDomains,
|
|
);
|
|
} catch (error) {
|
|
logger.error(`Error revoking OAuth refresh token for ${serverName}:`, error);
|
|
}
|
|
}
|
|
|
|
// 4. delete tokens from the DB after revocation attempts
|
|
await MCPTokenStorage.deleteUserTokens({
|
|
userId,
|
|
serverName,
|
|
deleteToken: async (filter) => {
|
|
await Token.deleteOne(filter);
|
|
},
|
|
});
|
|
|
|
// 5. clear the flow state for the OAuth tokens
|
|
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,
|
|
deleteUserMcpServers,
|
|
};
|