From 91c24165bff88bc23f7902116098a1549c4b936c Mon Sep 17 00:00:00 2001 From: Dmitri Date: Tue, 31 Mar 2026 12:45:22 +0400 Subject: [PATCH] fixed project settings issue --- backend/src/db/api/assets.js | 6 ++- backend/src/db/api/base.api.js | 49 +++++++++++++------ backend/src/db/api/project_audio_tracks.js | 22 ++++----- .../src/db/api/project_element_defaults.js | 21 ++++---- backend/src/db/api/projects.js | 5 +- backend/src/db/api/tour_pages.js | 22 ++++----- backend/src/db/api/users.js | 49 +++++++++---------- backend/src/db/utils.js | 42 +++++++++++----- 8 files changed, 129 insertions(+), 87 deletions(-) diff --git a/backend/src/db/api/assets.js b/backend/src/db/api/assets.js index 8f9cac8..c347d4e 100644 --- a/backend/src/db/api/assets.js +++ b/backend/src/db/api/assets.js @@ -19,7 +19,11 @@ class AssetsDBApi extends GenericDBApi { } static get ENUM_FIELDS() { - return ['asset_type', 'type', 'is_public', 'projectId']; + return ['asset_type', 'type', 'is_public']; + } + + static get UUID_FIELDS() { + return ['projectId']; } static get CSV_FIELDS() { diff --git a/backend/src/db/api/base.api.js b/backend/src/db/api/base.api.js index 1ccd562..5e29331 100644 --- a/backend/src/db/api/base.api.js +++ b/backend/src/db/api/base.api.js @@ -26,6 +26,17 @@ class GenericDBApi { return []; } + /** + * UUID fields that require validation before querying. + * These are typically foreign key fields like 'projectId'. + * Invalid UUIDs will return empty results instead of causing DB errors. + * Override in subclass to specify fields. + * Example: return ['projectId', 'userId']; + */ + static get UUID_FIELDS() { + return []; + } + static get RELATION_FILTERS() { return []; } @@ -265,13 +276,10 @@ class GenericDBApi { let include = [...this.FIND_ALL_INCLUDES]; if (filter.id) { - const validId = Utils.uuid(filter.id); - if (validId) { - where.id = validId; - } else { - // Invalid UUID provided - return empty results immediately + if (!Utils.isValidUuid(filter.id)) { return { rows: [], count: 0 }; } + where.id = filter.id; } for (const field of this.SEARCHABLE_FIELDS) { @@ -299,6 +307,16 @@ class GenericDBApi { } } + // Validate UUID fields - return empty results for invalid UUIDs + for (const field of this.UUID_FIELDS) { + if (filter[field] !== undefined) { + if (!Utils.isValidUuid(filter[field])) { + return { rows: [], count: 0 }; + } + where[field] = filter[field]; + } + } + if (filter.active !== undefined) { where.active = filter.active === true || filter.active === 'true'; } @@ -316,11 +334,7 @@ class GenericDBApi { for (const rel of this.RELATION_FILTERS) { if (filter[rel.filterKey]) { const searchTerms = filter[rel.filterKey].split('|'); - - // Filter out null UUIDs - only keep valid ones - const validUuids = searchTerms - .map((term) => Utils.uuid(term)) - .filter((id) => id !== null); + const validUuids = Utils.filterValidUuids(searchTerms); // Build OR conditions array const orConditions = []; @@ -384,12 +398,15 @@ class GenericDBApi { let where = {}; if (query) { - where = { - [Op.or]: [ - { id: Utils.uuid(query) }, - Utils.ilike(this.TABLE_NAME, this.AUTOCOMPLETE_FIELD, query), - ], - }; + const orConditions = [ + Utils.ilike(this.TABLE_NAME, this.AUTOCOMPLETE_FIELD, query), + ]; + + if (Utils.isValidUuid(query)) { + orConditions.unshift({ id: query }); + } + + where = { [Op.or]: orConditions }; } const records = await this.MODEL.findAll({ diff --git a/backend/src/db/api/project_audio_tracks.js b/backend/src/db/api/project_audio_tracks.js index 3ad952d..d271574 100644 --- a/backend/src/db/api/project_audio_tracks.js +++ b/backend/src/db/api/project_audio_tracks.js @@ -93,6 +93,9 @@ class Project_audio_tracksDBApi extends GenericDBApi { let where = {}; + const terms = filter.project ? filter.project.split('|') : []; + const validUuids = Utils.filterValidUuids(terms); + let include = [ { model: db.projects, @@ -100,18 +103,12 @@ class Project_audio_tracksDBApi extends GenericDBApi { where: filter.project ? { [Op.or]: [ - { - id: { - [Op.in]: filter.project - .split('|') - .map((term) => Utils.uuid(term)), - }, - }, + ...(validUuids.length > 0 + ? [{ id: { [Op.in]: validUuids } }] + : []), { name: { - [Op.or]: filter.project - .split('|') - .map((term) => ({ [Op.iLike]: `%${term}%` })), + [Op.or]: terms.map((term) => ({ [Op.iLike]: `%${term}%` })), }, }, ], @@ -123,7 +120,10 @@ class Project_audio_tracksDBApi extends GenericDBApi { include[0] = applyRuntimeProjectFilter(include[0], options); if (filter.id) { - where.id = Utils.uuid(filter.id); + if (!Utils.isValidUuid(filter.id)) { + return { rows: [], count: 0 }; + } + where.id = filter.id; } for (const field of this.SEARCHABLE_FIELDS) { diff --git a/backend/src/db/api/project_element_defaults.js b/backend/src/db/api/project_element_defaults.js index aaeea2c..f21c11f 100644 --- a/backend/src/db/api/project_element_defaults.js +++ b/backend/src/db/api/project_element_defaults.js @@ -111,6 +111,8 @@ class Project_element_defaultsDBApi extends GenericDBApi { // Support both 'project' and 'projectId' query params const projectFilter = filter.project || filter.projectId; + const terms = projectFilter ? projectFilter.split('|') : []; + const validUuids = Utils.filterValidUuids(terms); let include = [ { @@ -119,18 +121,12 @@ class Project_element_defaultsDBApi extends GenericDBApi { where: projectFilter ? { [Op.or]: [ - { - id: { - [Op.in]: projectFilter - .split('|') - .map((term) => Utils.uuid(term)), - }, - }, + ...(validUuids.length > 0 + ? [{ id: { [Op.in]: validUuids } }] + : []), { name: { - [Op.or]: projectFilter - .split('|') - .map((term) => ({ [Op.iLike]: `%${term}%` })), + [Op.or]: terms.map((term) => ({ [Op.iLike]: `%${term}%` })), }, }, ], @@ -145,7 +141,10 @@ class Project_element_defaultsDBApi extends GenericDBApi { ]; if (filter.id) { - where.id = Utils.uuid(filter.id); + if (!Utils.isValidUuid(filter.id)) { + return { rows: [], count: 0 }; + } + where.id = filter.id; } for (const field of this.SEARCHABLE_FIELDS) { diff --git a/backend/src/db/api/projects.js b/backend/src/db/api/projects.js index e57adc9..22efedd 100644 --- a/backend/src/db/api/projects.js +++ b/backend/src/db/api/projects.js @@ -136,7 +136,10 @@ class ProjectsDBApi extends GenericDBApi { let include = []; if (filter.id) { - where.id = Utils.uuid(filter.id); + if (!Utils.isValidUuid(filter.id)) { + return { rows: [], count: 0 }; + } + where.id = filter.id; } for (const field of this.SEARCHABLE_FIELDS) { diff --git a/backend/src/db/api/tour_pages.js b/backend/src/db/api/tour_pages.js index a6513d0..2cdfc02 100644 --- a/backend/src/db/api/tour_pages.js +++ b/backend/src/db/api/tour_pages.js @@ -125,6 +125,9 @@ class Tour_pagesDBApi extends GenericDBApi { let where = {}; + const terms = filter.project ? filter.project.split('|') : []; + const validUuids = Utils.filterValidUuids(terms); + let include = [ { model: db.projects, @@ -132,18 +135,12 @@ class Tour_pagesDBApi extends GenericDBApi { where: filter.project ? { [Op.or]: [ - { - id: { - [Op.in]: filter.project - .split('|') - .map((term) => Utils.uuid(term)), - }, - }, + ...(validUuids.length > 0 + ? [{ id: { [Op.in]: validUuids } }] + : []), { name: { - [Op.or]: filter.project - .split('|') - .map((term) => ({ [Op.iLike]: `%${term}%` })), + [Op.or]: terms.map((term) => ({ [Op.iLike]: `%${term}%` })), }, }, ], @@ -155,7 +152,10 @@ class Tour_pagesDBApi extends GenericDBApi { include[0] = applyRuntimeProjectFilter(include[0], options); if (filter.id) { - where.id = Utils.uuid(filter.id); + if (!Utils.isValidUuid(filter.id)) { + return { rows: [], count: 0 }; + } + where.id = filter.id; } for (const field of this.SEARCHABLE_FIELDS) { diff --git a/backend/src/db/api/users.js b/backend/src/db/api/users.js index 7b39b6d..cc77d19 100644 --- a/backend/src/db/api/users.js +++ b/backend/src/db/api/users.js @@ -298,6 +298,9 @@ module.exports = class UsersDBApi { offset = currentPage * limit; + const appRoleTerms = filter.app_role ? filter.app_role.split('|') : []; + const appRoleValidUuids = Utils.filterValidUuids(appRoleTerms); + let include = [ { model: db.roles, @@ -306,18 +309,14 @@ module.exports = class UsersDBApi { where: filter.app_role ? { [Op.or]: [ - { - id: { - [Op.in]: filter.app_role - .split('|') - .map((term) => Utils.uuid(term)), - }, - }, + ...(appRoleValidUuids.length > 0 + ? [{ id: { [Op.in]: appRoleValidUuids } }] + : []), { name: { - [Op.or]: filter.app_role - .split('|') - .map((term) => ({ [Op.iLike]: `%${term}%` })), + [Op.or]: appRoleTerms.map((term) => ({ + [Op.iLike]: `%${term}%`, + })), }, }, ], @@ -339,10 +338,10 @@ module.exports = class UsersDBApi { if (filter) { if (filter.id) { - where = { - ...where, - ['id']: Utils.uuid(filter.id), - }; + if (!Utils.isValidUuid(filter.id)) { + return { rows: [], count: 0 }; + } + where = { ...where, id: filter.id }; } if (filter.firstName) { @@ -480,6 +479,7 @@ module.exports = class UsersDBApi { if (filter.custom_permissions) { const searchTerms = filter.custom_permissions.split('|'); + const permissionValidUuids = Utils.filterValidUuids(searchTerms); include = [ { @@ -490,11 +490,9 @@ module.exports = class UsersDBApi { searchTerms.length > 0 ? { [Op.or]: [ - { - id: { - [Op.in]: searchTerms.map((term) => Utils.uuid(term)), - }, - }, + ...(permissionValidUuids.length > 0 + ? [{ id: { [Op.in]: permissionValidUuids } }] + : []), { name: { [Op.or]: searchTerms.map((term) => ({ @@ -568,12 +566,13 @@ module.exports = class UsersDBApi { let where = {}; if (query) { - where = { - [Op.or]: [ - { ['id']: Utils.uuid(query) }, - Utils.ilike('users', 'firstName', query), - ], - }; + const orConditions = [Utils.ilike('users', 'firstName', query)]; + + if (Utils.isValidUuid(query)) { + orConditions.unshift({ id: query }); + } + + where = { [Op.or]: orConditions }; } const records = await db.users.findAll({ diff --git a/backend/src/db/utils.js b/backend/src/db/utils.js index 6a36d5c..9641d8f 100644 --- a/backend/src/db/utils.js +++ b/backend/src/db/utils.js @@ -1,25 +1,45 @@ const validator = require('validator'); +const { v4: uuidv4 } = require('uuid'); const Sequelize = require('./models').Sequelize; module.exports = class Utils { /** - * Validates a UUID string. - * @param {*} value - The value to validate as UUID - * @returns {string|null} - The valid UUID string, or null if invalid + * Check if value is a valid UUID + * @param {*} value - The value to check + * @returns {boolean} - True if valid UUID, false otherwise */ - static uuid(value) { - if (value && validator.isUUID(String(value))) { - return value; - } - return null; + static isValidUuid(value) { + return Boolean(value && validator.isUUID(String(value))); } + /** + * Generate a new UUID v4 + * @returns {string} - A new UUID v4 string + */ + static generateUuid() { + return uuidv4(); + } + + /** + * Filter array to only valid UUIDs + * @param {Array} values - Array of values to filter + * @returns {string[]} - Array containing only valid UUIDs + */ + static filterValidUuids(values) { + return values.filter((v) => this.isValidUuid(v)); + } + + /** + * Case-insensitive LIKE query + * @param {string} model - The model/table name + * @param {string} column - The column name + * @param {string} value - The value to search for + * @returns {Object} - Sequelize where clause + */ static ilike(model, column, value) { return Sequelize.where( Sequelize.fn('lower', Sequelize.col(`${model}.${column}`)), - { - [Sequelize.Op.like]: `%${value}%`.toLowerCase(), - }, + { [Sequelize.Op.like]: `%${value}%`.toLowerCase() }, ); } };