mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-07-01 11:53:55 +00:00
fix: cascade forced retention when sharing an older conversation
Creating or updating a shared link computed a capped expiry for the SharedLink itself but never converted the source conversation, so under forced ephemeral retention sharing an older permanent chat left it expiredAt: null and visible indefinitely after the public link expired, unlike every other write path that converts a touched permanent chat. Run applyForcedRetention on the source conversation after the share is created/updated, converting it and its messages and capping its shares; a no-op outside forced retention.
This commit is contained in:
parent
c02143f57b
commit
3b8fe3ba3c
2 changed files with 67 additions and 0 deletions
|
|
@ -81,6 +81,7 @@ jest.mock('~/models', () => ({
|
|||
getSharedLink: jest.fn(),
|
||||
getSharedLinkFile: jest.fn(),
|
||||
backfillSharedLinkFiles: jest.fn(),
|
||||
applyForcedRetention: jest.fn(),
|
||||
getRoleByName: jest.fn(),
|
||||
}));
|
||||
|
||||
|
|
@ -123,6 +124,7 @@ const {
|
|||
updateSharedLink,
|
||||
getSharedLinkFile,
|
||||
backfillSharedLinkFiles,
|
||||
applyForcedRetention,
|
||||
getRoleByName,
|
||||
} = require('~/models');
|
||||
const shareRouter = require('../share');
|
||||
|
|
@ -341,6 +343,50 @@ describe('share routes', () => {
|
|||
expect(createSharedLink).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('converts the source conversation under forced retention when creating a share', async () => {
|
||||
mockGetSharedLinkExpiration.mockResolvedValue(activeExpiration);
|
||||
createSharedLink.mockResolvedValue({ _id: 'link-123', shareId: 'share-123' });
|
||||
|
||||
const response = await request(buildApp({ retentionMode: RetentionMode.EPHEMERAL }))
|
||||
.post('/api/share/convo-123')
|
||||
.send({ targetMessageId: 'msg-123' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(applyForcedRetention).toHaveBeenCalledWith(
|
||||
{ userId: 'user-123', interfaceConfig: { retentionMode: RetentionMode.EPHEMERAL } },
|
||||
{ conversationId: 'convo-123' },
|
||||
expect.objectContaining({ context: expect.any(String) }),
|
||||
);
|
||||
});
|
||||
|
||||
it('does not convert the source conversation when the share is not created', async () => {
|
||||
mockGetSharedLinkExpiration.mockResolvedValue(activeExpiration);
|
||||
createSharedLink.mockResolvedValue(null);
|
||||
|
||||
const response = await request(buildApp({ retentionMode: RetentionMode.EPHEMERAL }))
|
||||
.post('/api/share/convo-123')
|
||||
.send({ targetMessageId: 'msg-123' });
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(applyForcedRetention).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('converts the source conversation under forced retention when updating a share', async () => {
|
||||
mongoose.models.SharedLink.findOne.mockReturnValue(lean({ conversationId: 'convo-123' }));
|
||||
mockGetSharedLinkExpiration.mockResolvedValue(activeExpiration);
|
||||
updateSharedLink.mockResolvedValue({ _id: 'link-456', shareId: 'share-456' });
|
||||
|
||||
await request(buildApp({ retentionMode: RetentionMode.EPHEMERAL }))
|
||||
.patch('/api/share/share-123')
|
||||
.send({ snapshotFiles: false });
|
||||
|
||||
expect(applyForcedRetention).toHaveBeenCalledWith(
|
||||
{ userId: 'user-123', interfaceConfig: { retentionMode: RetentionMode.EPHEMERAL } },
|
||||
{ conversationId: 'convo-123' },
|
||||
expect.objectContaining({ context: expect.any(String) }),
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects new shares for expired conversations in all retention mode', async () => {
|
||||
mockGetSharedLinkExpiration.mockResolvedValue(expiredExpiration);
|
||||
createSharedLink.mockResolvedValue({ _id: 'link-123', shareId: 'share-123' });
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ const {
|
|||
getSharedLink,
|
||||
getSharedLinkFile,
|
||||
backfillSharedLinkFiles,
|
||||
applyForcedRetention,
|
||||
getRoleByName,
|
||||
} = require('~/models');
|
||||
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
||||
|
|
@ -66,6 +67,18 @@ const resolveSharedLinkExpiration = (req, conversationId) =>
|
|||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Converts the shared source conversation (and its messages) under forced (ephemeral)
|
||||
* retention, so sharing an older permanent chat does not leave it visible and non-expiring
|
||||
* after the public link itself expires; a no-op outside forced retention.
|
||||
*/
|
||||
const enforceForcedRetention = (req, conversationId, context) =>
|
||||
applyForcedRetention(
|
||||
{ userId: req?.user?.id, interfaceConfig: req?.config?.interfaceConfig },
|
||||
{ conversationId },
|
||||
{ context },
|
||||
);
|
||||
|
||||
/**
|
||||
* Shared messages
|
||||
*/
|
||||
|
|
@ -441,6 +454,11 @@ router.post(
|
|||
);
|
||||
if (created) {
|
||||
await grantCreationPermissions(created._id, req.user.id, grantPublic, expiredAt);
|
||||
await enforceForcedRetention(
|
||||
req,
|
||||
req.params.conversationId,
|
||||
'POST /api/share/:conversationId',
|
||||
);
|
||||
res.status(200).json(created);
|
||||
} else {
|
||||
res.status(404).end();
|
||||
|
|
@ -483,6 +501,9 @@ router.patch('/:shareId', requireJwtAuth, configMiddleware, async (req, res) =>
|
|||
if (updatedShare._id && expiredAt !== undefined) {
|
||||
await updateSharedLinkPermissionsExpiration(updatedShare._id, expiredAt);
|
||||
}
|
||||
if (existing?.conversationId) {
|
||||
await enforceForcedRetention(req, existing.conversationId, 'PATCH /api/share/:shareId');
|
||||
}
|
||||
res.status(200).json(updatedShare);
|
||||
} else {
|
||||
res.status(404).end();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue