422 lines
12 KiB
JavaScript
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;
|
|
}
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|