diff --git a/api/server/controllers/UserController.js b/api/server/controllers/UserController.js index f3f8b5d34b6..3dc4e0a6a6b 100644 --- a/api/server/controllers/UserController.js +++ b/api/server/controllers/UserController.js @@ -581,7 +581,7 @@ const deleteUserController = async (req, res) => { logger.error('[deleteUserController] code environment cache invalidation failed:', error); }); await purgeAgentTriggerDeliveriesForUser(user.id); - logger.info(`User deleted account. Email: ${user.email} ID: ${user.id}`); + logger.info(`User deleted account. ID: ${user.id} Tenant: ${tenantId ?? 'none'}`); res.status(200).send({ message: 'User deleted' }); } catch (err) { // The account survives this failed attempt, so its schedules must too: restore the diff --git a/api/server/controllers/__tests__/deleteUser.spec.js b/api/server/controllers/__tests__/deleteUser.spec.js index 15cb6b8ba9c..da4efd59acf 100644 --- a/api/server/controllers/__tests__/deleteUser.spec.js +++ b/api/server/controllers/__tests__/deleteUser.spec.js @@ -271,6 +271,27 @@ describe('deleteUserController - 2FA enforcement', () => { expect(mockCancelAgentTriggerUserPurge).not.toHaveBeenCalled(); }); + it('logs the deleted account by id and tenant without the email address', async () => { + const { logger } = require('@librechat/data-schemas'); + logger.info.mockClear(); + const req = { + user: { id: 'user1', _id: 'user1', email: 'a@b.com', tenantId: 'tenant-1' }, + body: {}, + }; + mockGetUserById.mockResolvedValue({ + _id: 'user1', + email: 'a@b.com', + tenantId: 'tenant-1', + twoFactorEnabled: false, + }); + + await deleteUserController(req, createRes()); + + const lines = logger.info.mock.calls.map(([line]) => String(line)); + expect(lines).toContain('User deleted account. ID: user1 Tenant: tenant-1'); + expect(JSON.stringify(logger.info.mock.calls)).not.toContain('a@b.com'); + }); + it('aborts active generation jobs before deleting account-owned records', async () => { const req = { user: { id: 'user1', _id: 'user1', email: 'a@b.com', tenantId: 'tenant-1' }, diff --git a/e2e/specs/mock/scenarios/account-deletion.spec.ts b/e2e/specs/mock/scenarios/account-deletion.spec.ts new file mode 100644 index 00000000000..8902615e209 --- /dev/null +++ b/e2e/specs/mock/scenarios/account-deletion.spec.ts @@ -0,0 +1,41 @@ +import { randomUUID } from 'crypto'; +import { expect, test } from '@playwright/test'; + +const PASSWORD = 'Deletion-Scenario-1!'; + +test.describe('account deletion', () => { + test('a user who deletes their account is signed out and cannot sign back in @scenario:account-deletion-succeeds', async ({ + playwright, + baseURL, + }) => { + const request = await playwright.request.newContext({ baseURL }); + try { + const email = `deletion-${randomUUID().slice(0, 8)}@example.com`; + const register = await request.post('/api/auth/register', { + data: { + email, + name: 'Deletion Scenario', + password: PASSWORD, + confirm_password: PASSWORD, + }, + }); + expect(register.ok()).toBeTruthy(); + const login = await request.post('/api/auth/login', { data: { email, password: PASSWORD } }); + expect(login.ok()).toBeTruthy(); + const { token } = (await login.json()) as { token: string }; + const headers = { Authorization: `Bearer ${token}` }; + + const deletion = await request.delete('/api/user/delete', { headers }); + expect(deletion.status()).toBe(200); + expect(await deletion.json()).toEqual({ message: 'User deleted' }); + + expect((await request.get('/api/user', { headers })).status()).toBe(401); + const relogin = await request.post('/api/auth/login', { + data: { email, password: PASSWORD }, + }); + expect(relogin.ok()).toBeFalsy(); + } finally { + await request.dispose(); + } + }); +});