const { CacheKeys, SystemRoles, roleDefaults, permissionsSchema, removeNullishValues, } = require('librechat-data-provider'); const { logger } = require('@librechat/data-schemas'); const getLogStores = require('~/cache/getLogStores'); const { Role } = require('~/db/models'); /** * Retrieve a role by name and convert the found role document to a plain object. * If the role with the given name doesn't exist and the name is a system defined role, * create it and return the lean version. * * @param {string} roleName - The name of the role to find or create. * @param {string|string[]} [fieldsToSelect] - The fields to include or exclude in the returned document. * @returns {Promise} Role document. */ const getRoleByName = async function (roleName, fieldsToSelect = null) { const cache = getLogStores(CacheKeys.ROLES); try { const cachedRole = await cache.get(roleName); if (cachedRole) { return cachedRole; } let query = Role.findOne({ name: roleName }); if (fieldsToSelect) { query = query.select(fieldsToSelect); } let role = await query.lean().exec(); if (!role && SystemRoles[roleName]) { role = await new Role(roleDefaults[roleName]).save(); await cache.set(roleName, role); return role.toObject(); } await cache.set(roleName, role); return role; } catch (error) { throw new Error(`Failed to retrieve or create role: ${error.message}`); } }; /** * Update role values by name. * * @param {string} roleName - The name of the role to update. * @param {Partial} updates - The fields to update. * @returns {Promise} Updated role document. */ const updateRoleByName = async function (roleName, updates) { const cache = getLogStores(CacheKeys.ROLES); try { const role = await Role.findOneAndUpdate( { name: roleName }, { $set: updates }, { new: true, lean: true }, ) .select('-__v') .lean() .exec(); await cache.set(roleName, role); return role; } catch (error) { throw new Error(`Failed to update role: ${error.message}`); } }; /** * Updates access permissions for a specific role and multiple permission types. * @param {string} roleName - The role to update. * @param {Object.>} permissionsUpdate - Permissions to update and their values. * @param {IRole} [roleData] - Optional role data to use instead of fetching from the database. */ async function updateAccessPermissions(roleName, permissionsUpdate, roleData) { // Filter and clean the permission updates based on our schema definition. const updates = {}; for (const [permissionType, permissions] of Object.entries(permissionsUpdate)) { if (permissionsSchema.shape && permissionsSchema.shape[permissionType]) { updates[permissionType] = removeNullishValues(permissions); } } if (!Object.keys(updates).length) { return; } try { const role = roleData ?? (await getRoleByName(roleName)); if (!role) { return; } const currentPermissions = role.permissions || {}; const updatedPermissions = { ...currentPermissions }; let hasChanges = false; const unsetFields = {}; const permissionTypes = Object.keys(permissionsSchema.shape || {}); for (const permType of permissionTypes) { if (role[permType] && typeof role[permType] === 'object') { logger.info( `Migrating '${roleName}' role from old schema: found '${permType}' at top level`, ); updatedPermissions[permType] = { ...updatedPermissions[permType], ...role[permType], }; unsetFields[permType] = 1; hasChanges = true; } } for (const [permissionType, permissions] of Object.entries(updates)) { const currentTypePermissions = currentPermissions[permissionType] || {}; updatedPermissions[permissionType] = { ...currentTypePermissions }; for (const [permission, value] of Object.entries(permissions)) { if (currentTypePermissions[permission] !== value) { updatedPermissions[permissionType][permission] = value; hasChanges = true; logger.info( `Updating '${roleName}' role permission '${permissionType}' '${permission}' from ${currentTypePermissions[permission]} to: ${value}`, ); } } } if (hasChanges) { const updateObj = { permissions: updatedPermissions }; if (Object.keys(unsetFields).length > 0) { logger.info( `Unsetting old schema fields for '${roleName}' role: ${Object.keys(unsetFields).join(', ')}`, ); try { await Role.updateOne( { name: roleName }, { $set: updateObj, $unset: unsetFields, }, ); const cache = getLogStores(CacheKeys.ROLES); const updatedRole = await Role.findOne({ name: roleName }).select('-__v').lean().exec(); await cache.set(roleName, updatedRole); logger.info(`Updated role '${roleName}' and removed old schema fields`); } catch (updateError) { logger.error(`Error during role migration update: ${updateError.message}`); throw updateError; } } else { // Standard update if no migration needed await updateRoleByName(roleName, updateObj); } logger.info(`Updated '${roleName}' role permissions`); } else { logger.info(`No changes needed for '${roleName}' role permissions`); } } catch (error) { logger.error(`Failed to update ${roleName} role permissions:`, error); } } /** * Migrates roles from old schema to new schema structure. * This can be called directly to fix existing roles. * * @param {string} [roleName] - Optional specific role to migrate. If not provided, migrates all roles. * @returns {Promise} Number of roles migrated. */ const migrateRoleSchema = async function (roleName) { try { // Get roles to migrate let roles; if (roleName) { const role = await Role.findOne({ name: roleName }); roles = role ? [role] : []; } else { roles = await Role.find({}); } logger.info(`Migrating ${roles.length} roles to new schema structure`); let migratedCount = 0; for (const role of roles) { const permissionTypes = Object.keys(permissionsSchema.shape || {}); const unsetFields = {}; let hasOldSchema = false; // Check for old schema fields for (const permType of permissionTypes) { if (role[permType] && typeof role[permType] === 'object') { hasOldSchema = true; // Ensure permissions object exists role.permissions = role.permissions || {}; // Migrate permissions from old location to new role.permissions[permType] = { ...role.permissions[permType], ...role[permType], }; // Mark field for removal unsetFields[permType] = 1; } } if (hasOldSchema) { try { logger.info(`Migrating role '${role.name}' from old schema structure`); // Simple update operation await Role.updateOne( { _id: role._id }, { $set: { permissions: role.permissions }, $unset: unsetFields, }, ); // Refresh cache const cache = getLogStores(CacheKeys.ROLES); const updatedRole = await Role.findById(role._id).lean().exec(); await cache.set(role.name, updatedRole); migratedCount++; logger.info(`Migrated role '${role.name}'`); } catch (error) { logger.error(`Failed to migrate role '${role.name}': ${error.message}`); } } } logger.info(`Migration complete: ${migratedCount} roles migrated`); return migratedCount; } catch (error) { logger.error(`Role schema migration failed: ${error.message}`); throw error; } }; module.exports = { getRoleByName, updateRoleByName, migrateRoleSchema, updateAccessPermissions, };