From 846e34b1d73589a9ce9f7eeaa823646da1bc59d4 Mon Sep 17 00:00:00 2001 From: WhammyLeaf Date: Fri, 21 Nov 2025 18:03:26 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20fix:=20Remove=20All=20U?= =?UTF-8?q?ser=20Metadata=20on=20Deletion=20(#10534)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * remove all user metadata on deletion * chore: import order * fix: Update JSDoc types for deleteMessages function parameters and return value * fix: Enhance user deletion process by removing associated data and updating group memberships * fix: Add missing config middleware to user deletion route * fix: Refactor agent and prompt deletion processes to bulk delete and remove associated ACL entries * fix: Add deletion of OAuth tokens and ACL entries in user deletion process --------- Co-authored-by: Danny Avila --- api/models/Agent.js | 34 +++++++++++++++- api/models/Message.js | 4 +- api/models/Prompt.js | 32 ++++++++++++++- api/server/controllers/UserController.js | 50 ++++++++++++++++++------ api/server/routes/user.js | 9 ++++- config/delete-user.js | 46 +++++++++++++--------- e2e/setup/cleanupUser.ts | 11 +++--- 7 files changed, 144 insertions(+), 42 deletions(-) diff --git a/api/models/Agent.js b/api/models/Agent.js index b802ca187b..1cd6ba3ed9 100644 --- a/api/models/Agent.js +++ b/api/models/Agent.js @@ -12,8 +12,8 @@ const { } = require('./Project'); const { removeAllPermissions } = require('~/server/services/PermissionService'); const { getMCPServerTools } = require('~/server/services/Config'); +const { Agent, AclEntry } = require('~/db/models'); const { getActions } = require('./Action'); -const { Agent } = require('~/db/models'); /** * Create an agent with the provided data. @@ -539,6 +539,37 @@ const deleteAgent = async (searchParameter) => { return agent; }; +/** + * Deletes all agents created by a specific user. + * @param {string} userId - The ID of the user whose agents should be deleted. + * @returns {Promise} A promise that resolves when all user agents have been deleted. + */ +const deleteUserAgents = async (userId) => { + try { + const userAgents = await getAgents({ author: userId }); + + if (userAgents.length === 0) { + return; + } + + const agentIds = userAgents.map((agent) => agent.id); + const agentObjectIds = userAgents.map((agent) => agent._id); + + for (const agentId of agentIds) { + await removeAgentFromAllProjects(agentId); + } + + await AclEntry.deleteMany({ + resourceType: ResourceType.AGENT, + resourceId: { $in: agentObjectIds }, + }); + + await Agent.deleteMany({ author: userId }); + } catch (error) { + logger.error('[deleteUserAgents] General error:', error); + } +}; + /** * Get agents by accessible IDs with optional cursor-based pagination. * @param {Object} params - The parameters for getting accessible agents. @@ -856,6 +887,7 @@ module.exports = { createAgent, updateAgent, deleteAgent, + deleteUserAgents, getListAgents, revertAgentVersion, updateAgentProjects, diff --git a/api/models/Message.js b/api/models/Message.js index 02b74ec71e..8fe04f6f54 100644 --- a/api/models/Message.js +++ b/api/models/Message.js @@ -346,8 +346,8 @@ async function getMessage({ user, messageId }) { * * @async * @function deleteMessages - * @param {Object} filter - The filter criteria to find messages to delete. - * @returns {Promise} The metadata with count of deleted messages. + * @param {import('mongoose').FilterQuery} filter - The filter criteria to find messages to delete. + * @returns {Promise} The metadata with count of deleted messages. * @throws {Error} If there is an error in deleting messages. */ async function deleteMessages(filter) { diff --git a/api/models/Prompt.js b/api/models/Prompt.js index d96780a038..fbc161e97d 100644 --- a/api/models/Prompt.js +++ b/api/models/Prompt.js @@ -13,7 +13,7 @@ const { getProjectByName, } = require('./Project'); const { removeAllPermissions } = require('~/server/services/PermissionService'); -const { PromptGroup, Prompt } = require('~/db/models'); +const { PromptGroup, Prompt, AclEntry } = require('~/db/models'); const { escapeRegExp } = require('~/server/utils'); /** @@ -591,6 +591,36 @@ module.exports = { return { prompt: 'Prompt deleted successfully' }; } }, + /** + * Delete all prompts and prompt groups created by a specific user. + * @param {ServerRequest} req - The server request object. + * @param {string} userId - The ID of the user whose prompts and prompt groups are to be deleted. + */ + deleteUserPrompts: async (req, userId) => { + try { + const promptGroups = await getAllPromptGroups(req, { author: new ObjectId(userId) }); + + if (promptGroups.length === 0) { + return; + } + + const groupIds = promptGroups.map((group) => group._id); + + for (const groupId of groupIds) { + await removeGroupFromAllProjects(groupId); + } + + await AclEntry.deleteMany({ + resourceType: ResourceType.PROMPTGROUP, + resourceId: { $in: groupIds }, + }); + + await PromptGroup.deleteMany({ author: new ObjectId(userId) }); + await Prompt.deleteMany({ author: new ObjectId(userId) }); + } catch (error) { + logger.error('[deleteUserPrompts] General error:', error); + } + }, /** * Update prompt group * @param {Partial} filter - Filter to find prompt group diff --git a/api/server/controllers/UserController.js b/api/server/controllers/UserController.js index b488864a93..9bdf6c5e28 100644 --- a/api/server/controllers/UserController.js +++ b/api/server/controllers/UserController.js @@ -3,32 +3,45 @@ const { Tools, CacheKeys, Constants, FileSources } = require('librechat-data-pro const { MCPOAuthHandler, MCPTokenStorage, + mcpServersRegistry, normalizeHttpError, extractWebSearchEnvVars, } = require('@librechat/api'); const { - getFiles, - findToken, - updateUser, - deleteFiles, - deleteConvos, - deletePresets, - deleteMessages, - deleteUserById, - deleteAllSharedLinks, 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 { Transaction, Balance, User, Token } = require('~/db/models'); 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 { mcpServersRegistry } = require('@librechat/api'); const getUserController = async (req, res) => { const appConfig = await getAppConfig({ role: req.user?.role }); @@ -237,7 +250,6 @@ const deleteUserController = async (req, res) => { 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 - /* TODO: Delete Assistant Threads */ try { await deleteConvos(user.id); // delete user convos } catch (error) { @@ -249,7 +261,19 @@ const deleteUserController = async (req, res) => { 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 - /* TODO: queue job for cleaning actions and assistants of non-existant users */ + await deleteUserAgents(user.id); // delete user agents + 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(req, user.id); // delete user prompts + 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) { diff --git a/api/server/routes/user.js b/api/server/routes/user.js index 05d4e850c8..7efab9d026 100644 --- a/api/server/routes/user.js +++ b/api/server/routes/user.js @@ -8,7 +8,12 @@ const { deleteUserController, getUserController, } = require('~/server/controllers/UserController'); -const { requireJwtAuth, canDeleteAccount, verifyEmailLimiter } = require('~/server/middleware'); +const { + verifyEmailLimiter, + configMiddleware, + canDeleteAccount, + requireJwtAuth, +} = require('~/server/middleware'); const router = express.Router(); @@ -16,7 +21,7 @@ router.get('/', requireJwtAuth, getUserController); router.get('/terms', requireJwtAuth, getTermsStatusController); router.post('/terms/accept', requireJwtAuth, acceptTermsController); router.post('/plugins', requireJwtAuth, updateUserPluginsController); -router.delete('/delete', requireJwtAuth, canDeleteAccount, deleteUserController); +router.delete('/delete', requireJwtAuth, canDeleteAccount, configMiddleware, deleteUserController); router.post('/verify', verifyEmailController); router.post('/verify/resend', verifyEmailLimiter, resendVerificationController); diff --git a/config/delete-user.js b/config/delete-user.js index 242cd160ca..2d4dea0b37 100644 --- a/config/delete-user.js +++ b/config/delete-user.js @@ -1,26 +1,31 @@ #!/usr/bin/env node +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck const path = require('path'); const mongoose = require('mongoose'); const { - User, - Agent, - Assistant, - Balance, - Transaction, - ConversationTag, - Conversation, - Message, - File, Key, - MemoryEntry, - PluginAuth, - Prompt, - PromptGroup, - Preset, - Session, - SharedLink, - ToolCall, + User, + File, + Agent, Token, + Group, + Action, + Preset, + Prompt, + Balance, + Message, + Session, + AclEntry, + ToolCall, + Assistant, + SharedLink, + PluginAuth, + MemoryEntry, + PromptGroup, + Transaction, + Conversation, + ConversationTag, } = require('@librechat/data-schemas').createModels(mongoose); require('module-alias')({ base: path.resolve(__dirname, '..', 'api') }); const { askQuestion, silentExit } = require('./helpers'); @@ -72,6 +77,7 @@ async function gracefulExit(code = 0) { // 5) Build and run deletion tasks const tasks = [ + Action.deleteMany({ user: uid }), Agent.deleteMany({ author: uid }), Assistant.deleteMany({ user: uid }), Balance.deleteMany({ user: uid }), @@ -89,6 +95,7 @@ async function gracefulExit(code = 0) { SharedLink.deleteMany({ user: uid }), ToolCall.deleteMany({ user: uid }), Token.deleteMany({ userId: uid }), + AclEntry.deleteMany({ principalId: user._id }), ]; if (deleteTx) { @@ -97,7 +104,10 @@ async function gracefulExit(code = 0) { await Promise.all(tasks); - // 6) Finally delete the user document itself + // 6) Remove user from all groups + await Group.updateMany({ memberIds: user._id }, { $pull: { memberIds: user._id } }); + + // 7) Finally delete the user document itself await User.deleteOne({ _id: uid }); console.green(`✔ Successfully deleted user ${email} and all associated data.`); diff --git a/e2e/setup/cleanupUser.ts b/e2e/setup/cleanupUser.ts index 01f59142e8..20ad661a5d 100644 --- a/e2e/setup/cleanupUser.ts +++ b/e2e/setup/cleanupUser.ts @@ -5,6 +5,7 @@ import { deleteMessages, deleteAllUserSessions, } from '@librechat/backend/models'; +import { User, Balance, Transaction, AclEntry, Token, Group } from '@librechat/backend/db/models'; type TUser = { email: string; password: string }; @@ -40,13 +41,13 @@ export default async function cleanupUser(user: TUser) { // Delete all user sessions await deleteAllUserSessions(userId.toString()); - // Get models from the registered models - const { User, Balance, Transaction } = getModels(); - - // Delete user, balance, and transactions using the registered models - await User.deleteMany({ _id: userId }); + // Delete user, balance, transactions, tokens, ACL entries, and remove from groups await Balance.deleteMany({ user: userId }); await Transaction.deleteMany({ user: userId }); + await Token.deleteMany({ userId: userId }); + await AclEntry.deleteMany({ principalId: userId }); + await Group.updateMany({ memberIds: userId }, { $pull: { memberIds: userId } }); + await User.deleteMany({ _id: userId }); console.log('🤖: ✅ Deleted user from Database');