LibreChat/api/server/routes/share.js
Danny Avila 4b871a11ad
🧼 fix: Prevent Shared Link Caching and Strengthen Log Redaction (#13561)
* fix: tighten share caching and log redaction

* fix: sort changed imports

* fix: redact splat log arguments

* fix: avoid mutating log metadata during redaction

* fix: redact error and api_key log values

* fix: preserve error log context during redaction

* fix: cover remaining log redaction paths

* fix: bound log redaction work

* fix: align redaction scan cap with log config
2026-06-06 18:40:57 -04:00

217 lines
6.6 KiB
JavaScript

const mongoose = require('mongoose');
const express = require('express');
const {
isEnabled,
generateCheckAccess,
grantCreationPermissions,
ensureLinkPermissions,
deleteSharedLinkWithCleanup,
updateSharedLinkPermissionsExpiration,
isActiveExpirationDate,
getSharedLinkExpiration,
} = require('@librechat/api');
const { logger, createTempChatExpirationDate } = require('@librechat/data-schemas');
const { PermissionTypes, Permissions } = require('librechat-data-provider');
const {
getSharedMessages,
createSharedLink,
updateSharedLink,
getSharedLinks,
getSharedLink,
getRoleByName,
} = require('~/models');
const canAccessSharedLink = require('~/server/middleware/canAccessSharedLink');
const optionalJwtAuth = require('~/server/middleware/optionalJwtAuth');
const requireJwtAuth = require('~/server/middleware/requireJwtAuth');
const router = express.Router();
const checkSharedLinksAccess = generateCheckAccess({
permissionType: PermissionTypes.SHARED_LINKS,
permissions: [Permissions.CREATE],
getRoleByName,
});
const resolveSharedLinkExpiration = (req, conversationId) =>
getSharedLinkExpiration(
{ req, conversationId },
{
getConvo: async (userId, sourceConversationId) => {
const Conversation = mongoose.models.Conversation;
return Conversation.findOne(
{ conversationId: sourceConversationId, user: userId },
'isTemporary expiredAt',
).lean();
},
createExpirationDate: createTempChatExpirationDate,
logger,
},
);
/**
* Shared messages
*/
const allowSharedLinks =
process.env.ALLOW_SHARED_LINKS === undefined || isEnabled(process.env.ALLOW_SHARED_LINKS);
if (allowSharedLinks) {
router.get('/:shareId', optionalJwtAuth, canAccessSharedLink, async (req, res) => {
try {
const share = await getSharedMessages(req.params.shareId, req.shareResourceId);
if (share) {
res.set('Cache-Control', 'private, no-store');
res.status(200).json(share);
} else {
res.status(404).end();
}
} catch (error) {
logger.error('Error getting shared messages:', error);
res.status(500).json({ message: 'Error getting shared messages' });
}
});
}
/**
* Shared links
*/
router.get('/', requireJwtAuth, async (req, res) => {
try {
const params = {
pageParam: req.query.cursor,
pageSize: Math.max(1, parseInt(req.query.pageSize) || 10),
sortBy: ['createdAt', 'title'].includes(req.query.sortBy) ? req.query.sortBy : 'createdAt',
sortDirection: ['asc', 'desc'].includes(req.query.sortDirection)
? req.query.sortDirection
: 'desc',
search: req.query.search ? decodeURIComponent(req.query.search.trim()) : undefined,
};
const result = await getSharedLinks(
req.user.id,
params.pageParam,
params.pageSize,
params.sortBy,
params.sortDirection,
params.search,
);
res.status(200).send({
links: result.links,
nextCursor: result.nextCursor,
hasNextPage: result.hasNextPage,
});
} catch (error) {
logger.error('Error getting shared links:', error);
res.status(500).json({
message: 'Error getting shared links',
error: error.message,
});
}
});
router.get('/link/:conversationId', requireJwtAuth, async (req, res) => {
try {
const share = await getSharedLink(req.user.id, req.params.conversationId);
if (share._id && share.success) {
await ensureLinkPermissions(share._id, req.user.id);
}
return res.status(200).json({
_id: share._id,
success: share.success,
shareId: share.shareId,
targetMessageId: share.targetMessageId,
conversationId: req.params.conversationId,
});
} catch (error) {
logger.error('Error getting shared link:', error);
res.status(500).json({ message: 'Error getting shared link' });
}
});
router.post('/:conversationId', requireJwtAuth, checkSharedLinksAccess, async (req, res) => {
try {
const { targetMessageId } = req.body;
const expiredAt = await resolveSharedLinkExpiration(req, req.params.conversationId);
if (expiredAt != null && !isActiveExpirationDate(expiredAt)) {
return res.status(404).end();
}
const role = await getRoleByName(req.user.role);
const sharedLinksPerms = role?.permissions?.[PermissionTypes.SHARED_LINKS] || {};
const grantPublic = sharedLinksPerms[Permissions.SHARE_PUBLIC] === true;
const created = await createSharedLink(
req.user.id,
req.params.conversationId,
targetMessageId,
expiredAt,
);
if (created) {
await grantCreationPermissions(created._id, req.user.id, grantPublic, expiredAt);
res.status(200).json(created);
} else {
res.status(404).end();
}
} catch (error) {
logger.error('Error creating shared link:', error);
res.status(500).json({ message: 'Error creating shared link' });
}
});
router.patch('/:shareId', requireJwtAuth, async (req, res) => {
try {
const { targetMessageId } = req.body ?? {};
if (targetMessageId !== undefined && typeof targetMessageId !== 'string') {
return res.status(400).json({ message: 'targetMessageId must be a string' });
}
let expiredAt;
const SharedLink = mongoose.models.SharedLink;
const existing = await SharedLink.findOne(
{ shareId: req.params.shareId, user: req.user.id },
'conversationId',
).lean();
if (existing?.conversationId) {
expiredAt = await resolveSharedLinkExpiration(req, existing.conversationId);
}
if (expiredAt != null && !isActiveExpirationDate(expiredAt)) {
return res.status(404).end();
}
const updatedShare = await updateSharedLink(
req.user.id,
req.params.shareId,
targetMessageId,
expiredAt,
);
if (updatedShare) {
if (updatedShare._id && expiredAt !== undefined) {
await updateSharedLinkPermissionsExpiration(updatedShare._id, expiredAt);
}
res.status(200).json(updatedShare);
} else {
res.status(404).end();
}
} catch (error) {
logger.error('Error updating shared link:', error);
res.status(500).json({ message: 'Error updating shared link' });
}
});
router.delete('/:shareId', requireJwtAuth, async (req, res) => {
try {
const result = await deleteSharedLinkWithCleanup(req.user.id, req.params.shareId);
if (!result) {
return res.status(404).json({ message: 'Share not found' });
}
return res.status(200).json(result);
} catch (error) {
logger.error('Error deleting shared link:', error);
return res.status(400).json({ message: 'Error deleting shared link' });
}
});
module.exports = router;