2026-06-24 15:52:42 +00:00

422 lines
12 KiB
JavaScript

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;
}
}
};