mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-07-02 04:12:36 +00:00
* 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
217 lines
6.6 KiB
JavaScript
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;
|