const db = require('../db/models'); const RolesDBApi = require('../db/api/roles'); const parseImportFile = require('./importFileParser'); const ValidationError = require('./notifications/errors/validation'); const axios = require('axios'); const config = require('../config'); const { getCurrentUserSchoolId } = require('../db/api/schoolScope'); const { executeReadOnlySelect } = require('./sqlSafety'); const WIDGET_CUSTOMIZATION_KEY = 'widgets'; const UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; function badRequest(message) { const error = new Error(message); error.code = 400; return error; } function assertWidgetKey(key) { if (key !== WIDGET_CUSTOMIZATION_KEY) { throw badRequest('Invalid role customization key.'); } } function assertUuid(value, label) { if (typeof value !== 'string' || !UUID_REGEX.test(value)) { throw badRequest(`${label} must be a valid UUID.`); } } function buildWidgetResult(widget, rows, queryString) { if (Array.isArray(rows) && rows.length) { const key = Object.keys(rows[0])[0]; const value = widget.widget_type === 'scalar' ? rows[0][key] : rows; const widgetData = JSON.parse(widget.data); return { ...widget, ...widgetData, value, query: queryString }; } else { return { ...widget, value: [], query: queryString }; } } async function executeQuery(queryString, replacements) { try { return await executeReadOnlySelect(queryString, { replacements, maxRows: 1000, timeoutMs: 5000, }); } catch (e) { console.error('Widget SQL execution failed:', e); throw e; } } function insertWhereConditions(queryString, whereConditions) { if (!whereConditions) return queryString; const whereIndex = queryString.toLowerCase().indexOf('where'); const groupByIndex = queryString.toLowerCase().indexOf('group by'); const insertIndex = whereIndex === -1 ? (groupByIndex !== -1 ? groupByIndex : queryString.length) : whereIndex + 5; const prefix = queryString.substring(0, insertIndex); const suffix = queryString.substring(insertIndex); const conditionString = whereIndex === -1 ? ` WHERE ${whereConditions} ` : ` ${whereConditions} AND `; return `${prefix}${conditionString}${suffix}`; } function constructWhereConditions(mainTable, currentUser, replacements) { const globalAccess = currentUser?.app_role?.globalAccess; const tablesWithoutSchoolScope = ['permissions', 'roles']; const model = db[mainTable]; const rawAttributes = model?.rawAttributes || {}; const conditions = []; if (!globalAccess && !tablesWithoutSchoolScope.includes(mainTable)) { const schoolId = getCurrentUserSchoolId(currentUser); if (!schoolId) { conditions.push('1 = 0'); } else if (mainTable === 'schools') { conditions.push(`"${mainTable}"."id" = :schoolId`); replacements.schoolId = schoolId; } else if (rawAttributes.schoolId) { conditions.push(`"${mainTable}"."schoolId" = :schoolId`); replacements.schoolId = schoolId; } else if (rawAttributes.schoolsId) { conditions.push(`"${mainTable}"."schoolsId" = :schoolId`); replacements.schoolId = schoolId; } else { conditions.push('1 = 0'); } } if (rawAttributes.deletedAt) { conditions.push(`"${mainTable}"."deletedAt" IS NULL`); } return conditions.join(' AND '); } function extractTableName(queryString) { const tableNameRegex = /FROM\s+("?)([^"\s]+)\1\s*/i; const match = tableNameRegex.exec(queryString); return match ? match[2] : null; } function buildQuery(widget, currentUser) { let queryString = widget?.query || ''; const tableName = extractTableName(queryString); const mainTable = JSON.parse(widget?.data)?.main_table || tableName; const replacements = {}; const whereConditions = constructWhereConditions(mainTable, currentUser, replacements); queryString = insertWhereConditions(queryString, whereConditions); return { queryString, replacements }; } async function constructWidgetsResults(widgets, currentUser) { const widgetsResults = []; for (const widget of widgets) { if (!widget) continue; const { queryString, replacements } = buildQuery(widget, currentUser); const queryResult = await executeQuery(queryString, replacements); widgetsResults.push(buildWidgetResult(widget, queryResult, queryString)); } return widgetsResults; } async function fetchWidgetsData(widgets) { const widgetPromises = (widgets || []).map(widgetId => axios.get(`${config.flHost}/${config.project_uuid}/project_customization_widgets/${widgetId}.json`) ); const widgetResults = widgetPromises ? await Promise.allSettled(widgetPromises) : []; widgetResults .filter(result => result.status === 'rejected') .forEach(result => console.error('Widget fetch failed:', result.reason)); return widgetResults .filter(result => result.status === 'fulfilled') .map(result => result.value.data); } async function processWidgets(widgets, currentUser) { const widgetData = await fetchWidgetsData(widgets); return constructWidgetsResults(widgetData, currentUser); } function parseCustomization(role) { try { return JSON.parse(role.role_customization || '{}'); } catch (e) { console.log(e); return {}; } } async function findRole(roleId) { const transaction = await db.sequelize.transaction(); try { const role = roleId ? await RolesDBApi.findBy({ id: roleId }, { transaction }) : await RolesDBApi.findBy({ name: 'User' }, { transaction }); await transaction.commit(); return role; } catch (error) { await transaction.rollback(); throw error; } } module.exports = class RolesService { static async create(data, currentUser) { const transaction = await db.sequelize.transaction(); try { await RolesDBApi.create( data, { currentUser, transaction, }, ); await transaction.commit(); } catch (error) { await transaction.rollback(); throw error; } } static async bulkImport(req, res) { const transaction = await db.sequelize.transaction(); try { const results = await parseImportFile(req, res); await RolesDBApi.bulkImport(results, { transaction, ignoreDuplicates: true, validate: true, currentUser: req.currentUser }); await transaction.commit(); } catch (error) { await transaction.rollback(); throw error; } } static async update(data, id, currentUser) { const transaction = await db.sequelize.transaction(); try { let roles = await RolesDBApi.findBy( {id}, {transaction}, ); if (!roles) { throw new ValidationError( 'rolesNotFound', ); } const updatedRoles = await RolesDBApi.update( id, data, { currentUser, transaction, }, ); await transaction.commit(); return updatedRoles; } catch (error) { await transaction.rollback(); throw error; } } static async deleteByIds(ids, currentUser) { const transaction = await db.sequelize.transaction(); try { await RolesDBApi.deleteByIds(ids, { currentUser, transaction, }); await transaction.commit(); } catch (error) { await transaction.rollback(); throw error; } } static async remove(id, currentUser) { const transaction = await db.sequelize.transaction(); try { await RolesDBApi.remove( id, { currentUser, transaction, }, ); await transaction.commit(); } catch (error) { await transaction.rollback(); throw error; } } static async addRoleInfo(roleId, userId, key, widgetId, currentUser) { assertWidgetKey(key); assertUuid(widgetId, 'Widget ID'); const transaction = await db.sequelize.transaction(); try { let role; if (roleId) { role = await RolesDBApi.findBy({ id: roleId }, { transaction }); } else { role = await RolesDBApi.findBy({ name: 'User' }, { transaction }); } if (!role) { throw new ValidationError('rolesNotFound'); } let customization = {}; try { customization = JSON.parse(role.role_customization || '{}'); } catch (e) { console.log(e); } if (Array.isArray(customization[key])) { const el = customization[key].find((e) => e === widgetId); if (!el) { customization[key].unshift(widgetId); } } else { customization[key] = [widgetId]; } const newRole = await RolesDBApi.update( role.id, { role_customization: JSON.stringify(customization), name: role.name, permissions: role.permissions, globalAccess: role.globalAccess, }, { currentUser, transaction, }, ); await transaction.commit(); return newRole; } catch (error) { await transaction.rollback(); throw error; } } static async removeRoleInfoById(infoId, roleId, key, currentUser) { assertWidgetKey(key); assertUuid(infoId, 'Widget ID'); const transaction = await db.sequelize.transaction(); try { let role; if (roleId) { role = await RolesDBApi.findBy({ id: roleId }, { transaction }); } else { role = await RolesDBApi.findBy({ name: 'User' }, { transaction }); } if (!role) { throw new ValidationError('rolesNotFound'); } let customization = {}; try { customization = JSON.parse(role.role_customization || '{}'); } catch (e) { console.log(e); } if (!Array.isArray(customization[key])) { throw badRequest('Widget customization not found.'); } customization[key] = customization[key].filter( (item) => item !== infoId, ); await axios.delete(`${config.flHost}/${config.project_uuid}/project_customization_widgets/${infoId}.json`); const result = await RolesDBApi.update( role.id, { role_customization: JSON.stringify(customization), name: role.name, permissions: role.permissions, globalAccess: role.globalAccess, }, { currentUser, transaction, }, ); await transaction.commit(); return result; } catch (error) { await transaction.rollback(); throw error; } } static async getRoleInfoByKey(key, roleId, currentUser) { const transaction = await db.sequelize.transaction(); try { const role = await findRole(roleId); const customization = parseCustomization(role); let result; if (key === 'widgets') { result = await processWidgets(customization[key], currentUser); } else { result = customization[key]; } await transaction.commit(); return result; } catch (error) { console.error(error); await transaction.rollback(); throw error; } } };