fix: enforce forced retention on global tag rename and delete

Per-conversation tag writes already convert their conversation under
forced ephemeral retention, but global bookmark-tag renames and deletes
rewrite conversation rows via Conversation.updateMany without setting
isTemporary/expiredAt. A permanent chat tagged before the install
switched to ephemeral, touched only by a tag rename or delete, would
keep its retention fields unset and stay visible and non-expiring.

Add cascadeForcedRetentionByTag plus an applyForcedRetentionToTag method
that bulk-converts every conversation carrying a tag (and backfills its
messages and caps its shares) through the shared gap filter, so it never
extends a chat that already expires sooner. Load the interface config on
PUT /:tag and DELETE /:tag and route those writes through it.
This commit is contained in:
Marco Beretta 2026-06-24 15:38:10 +02:00
parent a3cad799dd
commit c392db0dd8
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
4 changed files with 210 additions and 2 deletions

View file

@ -9,6 +9,7 @@ const {
deleteConversationTag,
getConversationTags,
applyForcedRetention,
applyForcedRetentionToTag,
getRoleByName,
} = require('~/models');
const { requireJwtAuth, configMiddleware } = require('~/server/middleware');
@ -35,6 +36,18 @@ const enforceForcedRetention = (req, conversationId, context) =>
{ context },
);
/**
* Enforces forced (ephemeral) retention on every conversation carrying a tag, for global
* tag renames/deletes that rewrite conversation rows without converting them; a no-op
* outside forced retention.
*/
const enforceForcedRetentionForTag = (req, tag, context) =>
applyForcedRetentionToTag(
{ userId: req?.user?.id, interfaceConfig: req?.config?.interfaceConfig },
{ tag },
{ context },
);
/**
* GET /
* Retrieves all conversation tags for the authenticated user.
@ -80,11 +93,12 @@ router.post('/', configMiddleware, async (req, res) => {
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
router.put('/:tag', async (req, res) => {
router.put('/:tag', configMiddleware, async (req, res) => {
try {
const decodedTag = decodeURIComponent(req.params.tag);
const tag = await updateConversationTag(req.user.id, decodedTag, req.body);
if (tag) {
await enforceForcedRetentionForTag(req, req.body?.tag || decodedTag, 'PUT /api/tags/:tag');
res.status(200).json(tag);
} else {
res.status(404).json({ error: 'Tag not found' });
@ -101,9 +115,10 @@ router.put('/:tag', async (req, res) => {
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
router.delete('/:tag', async (req, res) => {
router.delete('/:tag', configMiddleware, async (req, res) => {
try {
const decodedTag = decodeURIComponent(req.params.tag);
await enforceForcedRetentionForTag(req, decodedTag, 'DELETE /api/tags/:tag');
const tag = await deleteConversationTag(req.user.id, decodedTag);
if (tag) {
res.status(200).json(tag);