const db = require('../db/models'); const RolesDBApi = require('../db/api/roles'); const processFile = require("../middlewares/upload"); const ValidationError = require('./notifications/errors/validation'); const csv = require('csv-parser'); const axios = require('axios'); const config = require('../config'); const stream = require('stream'); const WIDGET_SQL_MAX_LENGTH = 5000; const WIDGET_SQL_MAX_ROWS = 1000; const WIDGET_SQL_TIMEOUT_MS = 5000; const validateWidgetSql = (sql) => { if (typeof sql !== 'string' || !sql.trim()) { throw new ValidationError('Widget query must be a non-empty SQL string'); } if (sql.length > WIDGET_SQL_MAX_LENGTH) { throw new ValidationError(`Widget query is too long (max ${WIDGET_SQL_MAX_LENGTH} characters)`); } const normalized = sql.trim().replace(/;+\s*$/, ''); if (!/^(select|with)\b/i.test(normalized)) { throw new ValidationError('Widget query must be a SELECT statement'); } if (normalized.includes(';')) { throw new ValidationError('Widget query must contain a single statement'); } if (/--|\/\*/.test(normalized)) { throw new ValidationError('SQL comments are not allowed in widget queries'); } if (/\b(pg_sleep|set_config|copy)\b/i.test(normalized)) { throw new ValidationError('Restricted SQL function detected in widget query'); } return normalized; }; const runSafeWidgetQuery = async (sql) => { const normalized = validateWidgetSql(sql); const wrappedSql = `SELECT * FROM (${normalized}) AS widget_query_result LIMIT ${WIDGET_SQL_MAX_ROWS}`; return db.sequelize.transaction(async (transaction) => { await db.sequelize.query(`SET LOCAL statement_timeout = ${WIDGET_SQL_TIMEOUT_MS}`, { transaction }); return db.sequelize.query(wrappedSql, { transaction }); }); }; module.exports = class RolesService { static async create(data, currentUser) { const transaction = await db.sequelize.transaction(); try { const createdRole = await RolesDBApi.create( data, { currentUser, transaction, }, ); await transaction.commit(); return createdRole; } catch (error) { await transaction.rollback(); throw error; } } static async bulkImport(req, res) { const transaction = await db.sequelize.transaction(); try { await processFile(req, res); const bufferStream = new stream.PassThrough(); const results = []; await bufferStream.end(Buffer.from(req.file.buffer, "utf-8")); // convert Buffer to Stream await new Promise((resolve, reject) => { bufferStream .pipe(csv()) .on('data', (data) => results.push(data)) .on('end', async () => { console.log('CSV results', results); resolve(); }) .on('error', (error) => reject(error)); }) 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) { const regexExpForUuid = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/gi; const widgetIdIsUUID = regexExpForUuid.test(widgetId); const transaction = await db.sequelize.transaction(); 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'); } try { let customization = {}; try { customization = JSON.parse(role.role_customization || '{}'); } catch (e) { console.log(e); } if (widgetIdIsUUID && Array.isArray(customization[key])) { const el = customization[key].find((e) => e === widgetId); !el ? customization[key].unshift(widgetId) : null; } if (widgetIdIsUUID && !customization[key]) { customization[key] = [widgetId]; } const newRole = await RolesDBApi.update( role.id, { role_customization: JSON.stringify(customization), name: role.name, permissions: role.permissions, }, { currentUser, transaction, }, ); await transaction.commit(); return newRole; } catch (error) { await transaction.rollback(); throw error; } } static async removeRoleInfoById(infoId, roleId, key, currentUser) { const transaction = await db.sequelize.transaction(); let role; if (roleId) { role = await RolesDBApi.findBy({ id: roleId }, { transaction }); } else { role = await RolesDBApi.findBy({ name: 'User' }, { transaction }); } if (!role) { await transaction.rollback(); throw new ValidationError('rolesNotFound'); } let customization = {}; try { customization = JSON.parse(role.role_customization || '{}'); } catch (e) { console.log(e); } customization[key] = customization[key].filter( (item) => item !== infoId, ); await axios.delete(`${config.flHost}/${config.project_uuid}/project_customization_widgets/${infoId}.json`); try { const result = await RolesDBApi.update( role.id, { role_customization: JSON.stringify(customization), name: role.name, permissions: role.permissions, }, { currentUser, transaction, }, ); await transaction.commit(); return result; } catch (error) { await transaction.rollback(); throw error; } } static async getRoleInfoByKey(key, roleId) { const transaction = await db.sequelize.transaction(); let role; try { if (roleId) { role = await RolesDBApi.findBy({ id: roleId }, { transaction }); } else { role = await RolesDBApi.findBy({ name: 'User' }, { transaction }); } await transaction.commit(); } catch (error) { await transaction.rollback(); throw error; } if (!role) { throw new ValidationError('Role not found'); } let customization = '{}'; try { customization = JSON.parse(role.role_customization || '{}'); } catch (e) { console.error('Failed to parse role customization JSON:', e); throw e; } if (key === 'widgets') { const widgets = (customization[key] || []); const widgetArray = widgets.map(widget => { return axios.get(`${config.flHost}/${config.project_uuid}/project_customization_widgets/${widget}.json`) }) const widgetResults = await Promise.allSettled(widgetArray); const fulfilledWidgets = widgetResults .filter((result) => result.status === 'fulfilled') .map((result) => result.value.data); const widgetsResults = []; if (Array.isArray(fulfilledWidgets)) { for (const widget of fulfilledWidgets) { const result = await runSafeWidgetQuery(widget.query); if (result[0] && result[0].length) { const key = Object.keys(result[0][0])[0]; const value = widget.widget_type === 'scalar' ? result[0][0][key] : result[0]; const widgetData = JSON.parse(widget.data); widgetsResults.push({ ...widget, ...widgetData, value }); } else { widgetsResults.push({ ...widget, value: null }); } } } return widgetsResults; } return customization[key]; } };