Merge pull request 'hasan_backend' (#9) from hasan_backend into dev

Reviewed-on: https://gitea.maskantech.in/gitea_admin/productcatalogue_backend/pulls/9
This commit is contained in:
Hasan
2026-07-23 06:48:03 +00:00
21 changed files with 748 additions and 186 deletions
@@ -19,7 +19,7 @@ export default (sequelize) => {
allowNull: false
},
changed_by: {
type: DataTypes.UUID,
type: DataTypes.INTEGER,
allowNull: true
},
changes: {
@@ -19,7 +19,7 @@ export default (sequelize) => {
allowNull: false
},
changed_by: {
type: DataTypes.UUID,
type: DataTypes.INTEGER,
allowNull: true
},
changes: {
@@ -5,13 +5,13 @@ function serializeAttribute(attr) {
const json = attr.toJSON ? attr.toJSON() : { ...attr };
// Mapping to camelCase
json.isRequired = json.is_required ?? false;
json.isUnique = json.is_unique ?? false;
json.isLocalizable = json.is_localizable ?? false;
json.isVariantEligible = json.is_variant_eligible ?? false;
json.isSearchable = json.is_searchable ?? false;
json.isFilterable = json.is_filterable ?? false;
json.isChannelSpecific = json.is_channel_specific ?? false;
json.isRequired = json.is_required ?? json.isRequired ?? false;
json.isUnique = json.is_unique ?? json.isUnique ?? false;
json.isLocalizable = json.is_localizable ?? json.isLocalizable ?? false;
json.isVariantEligible = json.is_variant_eligible ?? json.isVariantEligible ?? false;
json.isSearchable = json.is_searchable ?? json.isSearchable ?? false;
json.isFilterable = json.is_filterable ?? json.isFilterable ?? false;
json.isChannelSpecific = json.is_channel_specific ?? json.isChannelSpecific ?? false;
json.minLength = json.min_length ?? 0;
json.maxLength = json.max_length ?? 255;
json.regexPattern = json.regex_pattern ?? null;
@@ -119,15 +119,15 @@ export default (sequelize) => {
defaultValue: 0
},
created_by: {
type: DataTypes.UUID,
type: DataTypes.INTEGER,
allowNull: true
},
updated_by: {
type: DataTypes.UUID,
type: DataTypes.INTEGER,
allowNull: true
},
deleted_by: {
type: DataTypes.UUID,
type: DataTypes.INTEGER,
allowNull: true
},
help_text: {
@@ -19,7 +19,7 @@ export default (sequelize) => {
allowNull: false
},
changed_by: {
type: DataTypes.UUID,
type: DataTypes.INTEGER,
allowNull: true
},
changes: {
@@ -1,49 +1,49 @@
import { Model, DataTypes } from 'sequelize';
export class Role extends Model {
static associate(models) {}
static associate(models) { }
}
export default (sequelize) => {
Role.init({
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
tenant_id: {
type: DataTypes.INTEGER,
allowNull: true
type: DataTypes.INTEGER,
allowNull: true
},
role_code: {
type: DataTypes.STRING(50),
allowNull: false
type: DataTypes.STRING(50),
allowNull: false
},
role_name: {
type: DataTypes.STRING(100),
allowNull: false
type: DataTypes.STRING(100),
allowNull: false
},
description: {
type: DataTypes.TEXT
type: DataTypes.TEXT
},
role_type: {
type: DataTypes.STRING(20),
defaultValue: 'tenant',
allowNull: false
type: DataTypes.STRING(20),
defaultValue: 'tenant',
allowNull: false
},
is_system_role: {
type: DataTypes.BOOLEAN,
defaultValue: false
type: DataTypes.BOOLEAN,
defaultValue: false
},
status: {
type: DataTypes.BOOLEAN,
defaultValue: true
type: DataTypes.BOOLEAN,
defaultValue: true
},
created_by: {
type: DataTypes.INTEGER
type: DataTypes.INTEGER
},
updated_by: {
type: DataTypes.INTEGER
type: DataTypes.INTEGER
}
}, {
sequelize,
@@ -7,6 +7,13 @@ export class RoleRepository {
const tenantFilter = context.userType !== 'platform' && context.tenantId
? { tenant_id: context.tenantId }
: {};
const { where: optionsWhere, ...restOptions } = options;
const whereClause = {
...(optionsWhere || {}),
...tenantFilter
};
return await models.Role.findAll({
include: [
{
@@ -15,11 +22,8 @@ export class RoleRepository {
through: { attributes: ['can_view', 'can_create', 'can_edit', 'can_delete', 'can_alter', 'can_import', 'can_export'] }
}
],
...options,
where: {
...(options.where || {}),
...tenantFilter
}
...restOptions,
where: whereClause
});
}
@@ -48,7 +52,7 @@ export class RoleRepository {
roleData.tenant_id = context.tenantId;
}
const role = await models.Role.create(roleData, { transaction });
if (permissions && permissions.length > 0) {
const validPermissions = permissions.filter(p => p.node_id != null);
if (validPermissions.length > 0) {
@@ -66,7 +70,7 @@ export class RoleRepository {
await models.RolePermission.bulkCreate(rolePermissions, { transaction });
}
}
await transaction.commit();
return await this.findById(role.id, {}, context);
} catch (error) {
@@ -137,9 +141,9 @@ export class RoleRepository {
// Delete associations first
await models.RolePermission.destroy({ where: { role_id: id }, transaction });
await models.UserRole.destroy({ where: { role_id: id }, transaction });
await role.destroy({ transaction });
await transaction.commit();
return true;
} catch (error) {
@@ -1,6 +1,7 @@
import repository from './role.repository.js';
import { models } from '../../../shared/database/models.js';
import { ApiError } from '../../../utils/helpers/ApiError.utils.js';
import { Op } from 'sequelize';
export class RoleService {
async getAll(query = {}, context = {}) {
@@ -8,13 +9,17 @@ export class RoleService {
}
async getByTenant(tenantId, context = {}) {
if (!tenantId || tenantId === 'platform' || tenantId === 'null' || tenantId === 'undefined') {
return await repository.findAll({ where: { tenant_id: null } }, context);
}
if (tenantId === 'all') {
return await repository.findAll({}, context);
}
const numericId = isNaN(Number(tenantId)) ? tenantId : Number(tenantId);
return await repository.findAll({
where: tenantId
? { tenant_id: tenantId }
: { tenant_id: null }
where: {
tenant_id: numericId
}
}, context);
}
@@ -60,6 +65,15 @@ export class RoleService {
}
async delete(id, context = {}) {
const role = await repository.findById(id, {}, context);
if (!role) {
throw new ApiError(404, 'Role not found');
}
if (role.is_system_role || role.role_code === 'SUPER_ADMIN' || role.role_code === 'SUPERADMIN' || role.role_name?.toLowerCase().includes('super admin')) {
throw new ApiError(403, 'Forbidden: Super Admin and System roles cannot be deleted');
}
const deleted = await repository.delete(id, context);
if (!deleted) {
throw new ApiError(404, 'Role not found');
@@ -82,6 +82,22 @@ export class UserService {
}
async delete(id, context = {}) {
const user = await repository.findById(id, {}, context);
if (!user) {
throw new ApiError(404, 'User not found');
}
const isSuperAdmin = user.email === 'admin@admin.com' || (user.roles && user.roles.some(r =>
r.role_code === 'SUPER_ADMIN' ||
r.role_code === 'SUPERADMIN' ||
r.role_name?.toLowerCase().includes('super admin') ||
r.is_system_role
));
if (isSuperAdmin) {
throw new ApiError(403, 'Forbidden: Super Admin user cannot be deleted');
}
const deleted = await repository.delete(id, context);
if (!deleted) {
throw new ApiError(404, 'User not found');
+12 -10
View File
@@ -4,11 +4,11 @@ import { authenticate } from '../../../shared/middleware/auth.middleware.js';
import { validate } from '../../../shared/middleware/validation.middleware.js';
import { authorize } from '../../../shared/middleware/permission.middleware.js';
import { audit } from '../../../shared/middleware/audit.middleware.js';
import {
createValidation,
updateValidation,
deleteValidation,
getByIdValidation
import {
createValidation,
updateValidation,
deleteValidation,
getByIdValidation
} from './metric.validation.js';
const router = Router();
@@ -26,7 +26,7 @@ const router = Router();
router.get(
'/',
authenticate,
authorize(['reports.sales']),
authorize(['reports']),
controller.getAll
);
@@ -45,11 +45,13 @@ router.get(
* responses:
* 200:
* description: Success
* 404:
* description: Not Found
*/
router.get(
'/:id',
authenticate,
authorize(['reports.sales']),
authorize(['reports']),
getByIdValidation,
validate,
controller.getById
@@ -68,7 +70,7 @@ router.get(
router.post(
'/',
authenticate,
authorize(['reports.sales']),
authorize(['reports']),
createValidation,
validate,
audit('CREATE_METRIC'),
@@ -92,7 +94,7 @@ router.post(
router.put(
'/:id',
authenticate,
authorize(['reports.sales']),
authorize(['reports']),
updateValidation,
validate,
audit('UPDATE_METRIC'),
@@ -116,7 +118,7 @@ router.put(
router.delete(
'/:id',
authenticate,
authorize(['reports.sales']),
authorize(['reports']),
deleteValidation,
validate,
audit('DELETE_METRIC'),
@@ -25,7 +25,7 @@ router.get(
router.patch(
'/read-all',
authenticate,
authorize(['notifications.update']),
authorize(['notifications']),
controller.markAllRead
);
@@ -43,7 +43,7 @@ router.get(
router.patch(
'/:id/read',
authenticate,
authorize(['notifications.update']),
authorize(['notifications']),
markReadValidation,
validate,
controller.markRead
+5 -5
View File
@@ -26,7 +26,7 @@ const router = Router();
router.get(
'/',
authenticate,
authorize(['TENANTS_MANAGEMENT']),
authorize(['settings.tenants']),
controller.getAll
);
@@ -49,7 +49,7 @@ router.get(
router.get(
'/:id',
authenticate,
authorize(['TENANTS_MANAGEMENT']),
authorize(['settings.tenants']),
getByIdValidation,
validate,
controller.getById
@@ -68,7 +68,7 @@ router.get(
router.post(
'/',
authenticate,
authorize(['TENANTS_MANAGEMENT']),
authorize(['settings.tenants']),
createValidation,
validate,
audit('CREATE_ORG'),
@@ -92,7 +92,7 @@ router.post(
router.put(
'/:id',
authenticate,
authorize(['TENANTS_MANAGEMENT']),
authorize(['settings.tenants']),
updateValidation,
validate,
audit('UPDATE_ORG'),
@@ -116,7 +116,7 @@ router.put(
router.delete(
'/:id',
authenticate,
authorize(['TENANTS_MANAGEMENT']),
authorize(['settings.tenants']),
deleteValidation,
validate,
audit('DELETE_ORG'),
@@ -22,7 +22,7 @@ const router = Router();
router.get(
'/',
authenticate,
authorize(['TENANTS_MANAGEMENT']),
authorize(['settings.tenants']),
controller.getAll
);
@@ -49,7 +49,7 @@ router.get(
router.get(
'/:id',
authenticate,
authorize(['TENANTS_MANAGEMENT']),
authorize(['settings.tenants']),
controller.getById
);
@@ -82,7 +82,7 @@ router.get(
router.post(
'/',
authenticate,
authorize(['TENANTS_MANAGEMENT']),
authorize(['settings.tenants']),
controller.create
);
@@ -107,7 +107,7 @@ router.post(
router.put(
'/:id',
authenticate,
authorize(['TENANTS_MANAGEMENT']),
authorize(['settings.tenants']),
controller.update
);
@@ -132,7 +132,7 @@ router.put(
router.delete(
'/:id',
authenticate,
authorize(['TENANTS_MANAGEMENT']),
authorize(['settings.tenants']),
controller.delete
);
@@ -34,7 +34,7 @@ router.get(
router.post(
'/generate',
authenticate,
authorize(['write:products']),
authorize(['products.variants']),
audit('GENERATE_VARIANTS'),
controller.generateBatch
);
@@ -43,7 +43,7 @@ router.post(
router.post(
'/bulk-update',
authenticate,
authorize(['write:products']),
authorize(['products.variants']),
audit('BULK_UPDATE_VARIANTS'),
controller.bulkUpdate
);
@@ -0,0 +1,91 @@
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
// 1. Alter attributes table
const attributesTable = await queryInterface.describeTable('attributes').catch(() => null);
if (attributesTable) {
if (attributesTable.created_by) {
await queryInterface.removeColumn('attributes', 'created_by');
}
if (attributesTable.updated_by) {
await queryInterface.removeColumn('attributes', 'updated_by');
}
if (attributesTable.deleted_by) {
await queryInterface.removeColumn('attributes', 'deleted_by');
}
await queryInterface.addColumn('attributes', 'created_by', {
type: Sequelize.INTEGER,
allowNull: true
});
await queryInterface.addColumn('attributes', 'updated_by', {
type: Sequelize.INTEGER,
allowNull: true
});
await queryInterface.addColumn('attributes', 'deleted_by', {
type: Sequelize.INTEGER,
allowNull: true
});
}
// 2. Alter history tables
const historyTables = ['attribute_history', 'attribute_set_history', 'attribute_group_history'];
for (const table of historyTables) {
const tableInfo = await queryInterface.describeTable(table).catch(() => null);
if (tableInfo) {
if (tableInfo.changed_by) {
await queryInterface.removeColumn(table, 'changed_by');
}
await queryInterface.addColumn(table, 'changed_by', {
type: Sequelize.INTEGER,
allowNull: true
});
}
}
},
down: async (queryInterface, Sequelize) => {
// Revert attributes table
const attributesTable = await queryInterface.describeTable('attributes').catch(() => null);
if (attributesTable) {
if (attributesTable.created_by) {
await queryInterface.removeColumn('attributes', 'created_by');
}
if (attributesTable.updated_by) {
await queryInterface.removeColumn('attributes', 'updated_by');
}
if (attributesTable.deleted_by) {
await queryInterface.removeColumn('attributes', 'deleted_by');
}
await queryInterface.addColumn('attributes', 'created_by', {
type: Sequelize.UUID,
allowNull: true
});
await queryInterface.addColumn('attributes', 'updated_by', {
type: Sequelize.UUID,
allowNull: true
});
await queryInterface.addColumn('attributes', 'deleted_by', {
type: Sequelize.UUID,
allowNull: true
});
}
// Revert history tables
const historyTables = ['attribute_history', 'attribute_set_history', 'attribute_group_history'];
for (const table of historyTables) {
const tableInfo = await queryInterface.describeTable(table).catch(() => null);
if (tableInfo) {
if (tableInfo.changed_by) {
await queryInterface.removeColumn(table, 'changed_by');
}
await queryInterface.addColumn(table, 'changed_by', {
type: Sequelize.UUID,
allowNull: true
});
}
}
}
};
@@ -0,0 +1,259 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
const transaction = await queryInterface.sequelize.transaction();
try {
const now = new Date();
// 1. Insert parent groups if not exist
await queryInterface.sequelize.query(`
INSERT INTO permission_nodes (node_code, node_name, node_type, parent_id, node_level, display_order, can_view, can_create, can_edit, can_delete, created_at, updated_at)
VALUES
('products', 'Products', 'group', NULL, 1, 10, true, false, false, false, NOW(), NOW()),
('masters', 'Masters', 'group', NULL, 1, 20, true, false, false, false, NOW(), NOW()),
('settings', 'Settings', 'group', NULL, 1, 30, true, false, false, false, NOW(), NOW()),
('reports', 'Reports', 'group', NULL, 1, 40, true, false, false, false, NOW(), NOW())
ON CONFLICT (node_code) DO NOTHING;
`, { transaction });
// Fetch parent group IDs
const parentGroups = await queryInterface.sequelize.query(
`SELECT id, node_code FROM permission_nodes WHERE node_code IN ('products', 'masters', 'settings', 'reports')`,
{ type: queryInterface.sequelize.QueryTypes.SELECT, transaction }
);
const pid = {};
parentGroups.forEach(g => { pid[g.node_code] = g.id; });
// 2. Insert new permission nodes
await queryInterface.sequelize.query(`
INSERT INTO permission_nodes (node_code, node_name, node_type, parent_id, node_level, display_order, can_view, can_create, can_edit, can_delete, created_at, updated_at)
VALUES
('settings.tenants', 'Tenants', 'feature', ${pid['settings']}, 2, 35, true, true, true, true, NOW(), NOW()),
('notifications', 'Notifications', 'feature', ${pid['settings']}, 2, 34, true, false, true, false, NOW(), NOW())
ON CONFLICT (node_code) DO NOTHING;
`, { transaction });
// Fetch settings.tenants and notifications IDs
const [tenantsNode] = await queryInterface.sequelize.query(
`SELECT id FROM permission_nodes WHERE node_code = 'settings.tenants'`,
{ type: queryInterface.sequelize.QueryTypes.SELECT, transaction }
);
const [notificationsNode] = await queryInterface.sequelize.query(
`SELECT id FROM permission_nodes WHERE node_code = 'notifications'`,
{ type: queryInterface.sequelize.QueryTypes.SELECT, transaction }
);
// 3. Map legacy roles assignments from TENANTS_MANAGEMENT to settings.tenants
const [legacyTenantsNode] = await queryInterface.sequelize.query(
`SELECT id FROM permission_nodes WHERE node_code = 'TENANTS_MANAGEMENT'`,
{ type: queryInterface.sequelize.QueryTypes.SELECT, transaction }
);
if (legacyTenantsNode && tenantsNode) {
const legacyMappings = await queryInterface.sequelize.query(
`SELECT role_id, can_view, can_create, can_edit, can_delete FROM role_permissions WHERE node_id = ${legacyTenantsNode.id}`,
{ type: queryInterface.sequelize.QueryTypes.SELECT, transaction }
);
for (const mapping of legacyMappings) {
// Check if mapping already exists to prevent duplicate insertion
const [exists] = await queryInterface.sequelize.query(
`SELECT id FROM role_permissions WHERE role_id = ${mapping.role_id} AND node_id = ${tenantsNode.id}`,
{ type: queryInterface.sequelize.QueryTypes.SELECT, transaction }
);
if (!exists) {
await queryInterface.bulkInsert('role_permissions', [{
role_id: mapping.role_id,
node_id: tenantsNode.id,
can_view: mapping.can_view,
can_create: mapping.can_create,
can_edit: mapping.can_edit,
can_delete: mapping.can_delete,
can_alter: false,
can_export: false,
can_import: false,
created_at: now,
updated_at: now
}], { transaction });
}
}
}
// 4. Default notifications permission mapping (mimic settings.roles permission level)
const [rolesNode] = await queryInterface.sequelize.query(
`SELECT id FROM permission_nodes WHERE node_code = 'settings.roles'`,
{ type: queryInterface.sequelize.QueryTypes.SELECT, transaction }
);
if (rolesNode && notificationsNode) {
const roleMappings = await queryInterface.sequelize.query(
`SELECT role_id, can_view, can_edit FROM role_permissions WHERE node_id = ${rolesNode.id}`,
{ type: queryInterface.sequelize.QueryTypes.SELECT, transaction }
);
for (const mapping of roleMappings) {
const [exists] = await queryInterface.sequelize.query(
`SELECT id FROM role_permissions WHERE role_id = ${mapping.role_id} AND node_id = ${notificationsNode.id}`,
{ type: queryInterface.sequelize.QueryTypes.SELECT, transaction }
);
if (!exists) {
await queryInterface.bulkInsert('role_permissions', [{
role_id: mapping.role_id,
node_id: notificationsNode.id,
can_view: mapping.can_view,
can_create: false,
can_edit: mapping.can_edit,
can_delete: false,
can_alter: false,
can_export: false,
can_import: false,
created_at: now,
updated_at: now
}], { transaction });
}
}
}
// 5. Move child nodes under proper parent groups
const mappings = [
{ code: 'products.items', parent: 'products', level: 2, order: 11 },
{ code: 'products.variants', parent: 'products', level: 2, order: 12 },
{ code: 'products.families', parent: 'products', level: 2, order: 13 },
{ code: 'products.categories', parent: 'products', level: 2, order: 14 },
{ code: 'products.attributes', parent: 'products', level: 2, order: 15 },
{ code: 'masters.brands', parent: 'masters', level: 2, order: 21 },
{ code: 'masters.units', parent: 'masters', level: 2, order: 22 },
{ code: 'settings.users', parent: 'settings', level: 2, order: 31 },
{ code: 'settings.roles', parent: 'settings', level: 2, order: 32 },
{ code: 'settings.integrations',parent: 'settings', level: 2, order: 33 },
{ code: 'reports.sales', parent: 'reports', level: 2, order: 41 },
{ code: 'reports.inventory', parent: 'reports', level: 2, order: 42 }
];
for (const m of mappings) {
await queryInterface.sequelize.query(
`UPDATE permission_nodes
SET parent_id = ${pid[m.parent]}, node_level = ${m.level}, display_order = ${m.order}
WHERE node_code = '${m.code}'`,
{ transaction }
);
}
// 6. Safely Delete Legacy and Unwanted/Obsolete Permission Nodes (PIM_SYSTEM, USERS_MANAGEMENT, ROLES_MANAGEMENT, TENANTS_MANAGEMENT, inventory.stock, inventory.adjustments)
// Cascade delete rules will clear all related role_permissions mappings automatically
await queryInterface.sequelize.query(
`DELETE FROM permission_nodes WHERE node_code IN ('PIM_SYSTEM', 'inventory.stock', 'inventory.adjustments')`,
{ transaction }
);
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
},
async down(queryInterface, Sequelize) {
const transaction = await queryInterface.sequelize.transaction();
try {
const now = new Date();
// 1. Re-create Root Permission Node (PIM System)
await queryInterface.sequelize.query(`
INSERT INTO permission_nodes (node_code, node_name, node_type, parent_id, node_level, display_order, can_view, can_create, can_edit, can_delete, created_at, updated_at)
VALUES ('PIM_SYSTEM', 'PIM System', 'module', NULL, 1, 1, true, false, false, false, NOW(), NOW())
ON CONFLICT (node_code) DO NOTHING;
`, { transaction });
const [pimSystemNode] = await queryInterface.sequelize.query(
`SELECT id FROM permission_nodes WHERE node_code = 'PIM_SYSTEM'`,
{ type: queryInterface.sequelize.QueryTypes.SELECT, transaction }
);
// 2. Re-create child nodes (Settings, Users, Roles)
const legacyChildren = [
{ code: 'USERS_MANAGEMENT', name: 'Users Management' },
{ code: 'ROLES_MANAGEMENT', name: 'Roles Management' },
{ code: 'TENANTS_MANAGEMENT', name: 'Tenants Management' }
];
for (let i = 0; i < legacyChildren.length; i++) {
const child = legacyChildren[i];
await queryInterface.sequelize.query(`
INSERT INTO permission_nodes (node_code, node_name, node_type, parent_id, node_level, display_order, can_view, can_create, can_edit, can_delete, created_at, updated_at)
VALUES ('${child.code}', '${child.name}', 'feature', ${pimSystemNode.id}, 2, ${i + 1}, true, true, true, true, NOW(), NOW())
ON CONFLICT (node_code) DO NOTHING;
`, { transaction });
}
// Fetch TENANTS_MANAGEMENT ID
const [legacyTenantsNode] = await queryInterface.sequelize.query(
`SELECT id FROM permission_nodes WHERE node_code = 'TENANTS_MANAGEMENT'`,
{ type: queryInterface.sequelize.QueryTypes.SELECT, transaction }
);
// Fetch settings.tenants ID
const [tenantsNode] = await queryInterface.sequelize.query(
`SELECT id FROM permission_nodes WHERE node_code = 'settings.tenants'`,
{ type: queryInterface.sequelize.QueryTypes.SELECT, transaction }
);
// 3. Rollback role permissions mapping back to TENANTS_MANAGEMENT
if (tenantsNode && legacyTenantsNode) {
const mappings = await queryInterface.sequelize.query(
`SELECT role_id, can_view, can_create, can_edit, can_delete FROM role_permissions WHERE node_id = ${tenantsNode.id}`,
{ type: queryInterface.sequelize.QueryTypes.SELECT, transaction }
);
for (const mapping of mappings) {
const [exists] = await queryInterface.sequelize.query(
`SELECT id FROM role_permissions WHERE role_id = ${mapping.role_id} AND node_id = ${legacyTenantsNode.id}`,
{ type: queryInterface.sequelize.QueryTypes.SELECT, transaction }
);
if (!exists) {
await queryInterface.bulkInsert('role_permissions', [{
role_id: mapping.role_id,
node_id: legacyTenantsNode.id,
can_view: mapping.can_view,
can_create: mapping.can_create,
can_edit: mapping.can_edit,
can_delete: mapping.can_delete,
can_alter: false,
can_export: false,
can_import: false,
created_at: now,
updated_at: now
}], { transaction });
}
}
}
// 4. Decouple child nodes from parent groups BEFORE deleting parent groups to avoid cascading delete lockout
await queryInterface.sequelize.query(
`UPDATE permission_nodes SET parent_id = NULL`,
{ transaction }
);
// 5. Delete new permission nodes
await queryInterface.sequelize.query(
`DELETE FROM permission_nodes WHERE node_code IN ('settings.tenants', 'notifications')`,
{ transaction }
);
// 6. Delete parent groups
await queryInterface.sequelize.query(
`DELETE FROM permission_nodes WHERE node_code IN ('products', 'masters', 'settings', 'reports')`,
{ transaction }
);
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
}
};
@@ -0,0 +1,190 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
const transaction = await queryInterface.sequelize.transaction();
try {
// 1. Locate existing legacy permission nodes
const legacyNodes = await queryInterface.sequelize.query(
`SELECT id, node_code, node_name FROM permission_nodes WHERE node_code IN ('PIM_SYSTEM', 'USERS_MANAGEMENT', 'ROLES_MANAGEMENT', 'TENANTS_MANAGEMENT')`,
{ type: queryInterface.sequelize.QueryTypes.SELECT, transaction }
);
console.log('--- LEGACY RBAC PROJECT AUDIT & LOGGING ---');
console.log('Affected Permission Nodes:', legacyNodes);
if (legacyNodes.length > 0) {
const legacyNodeIds = legacyNodes.map(n => n.id);
// Fetch and log affected role_permissions mapping records
const affectedMappings = await queryInterface.sequelize.query(
`SELECT rp.role_id, rp.node_id, pn.node_code, rp.can_view, rp.can_create, rp.can_edit, rp.can_delete
FROM role_permissions rp
JOIN permission_nodes pn ON rp.node_id = pn.id
WHERE rp.node_id IN (${legacyNodeIds.join(',')})`,
{ type: queryInterface.sequelize.QueryTypes.SELECT, transaction }
);
console.log('Affected Role Permission mappings:', affectedMappings);
// 2. Create the backup table to hold mappings for potential rollback
await queryInterface.sequelize.query(`
CREATE TABLE IF NOT EXISTS _backup_legacy_role_permissions (
id SERIAL PRIMARY KEY,
role_id INTEGER NOT NULL,
node_code VARCHAR(50) NOT NULL,
can_view BOOLEAN DEFAULT false,
can_create BOOLEAN DEFAULT false,
can_edit BOOLEAN DEFAULT false,
can_delete BOOLEAN DEFAULT false,
can_alter BOOLEAN DEFAULT false,
can_export BOOLEAN DEFAULT false,
can_import BOOLEAN DEFAULT false
)
`, { transaction });
// Backup existing mappings dynamically (idempotent: avoid duplicate inserts if rerun)
for (const mapping of affectedMappings) {
const [exists] = await queryInterface.sequelize.query(
`SELECT id FROM _backup_legacy_role_permissions WHERE role_id = ${mapping.role_id} AND node_code = '${mapping.node_code}'`,
{ type: queryInterface.sequelize.QueryTypes.SELECT, transaction }
);
if (!exists) {
await queryInterface.sequelize.query(`
INSERT INTO _backup_legacy_role_permissions (role_id, node_code, can_view, can_create, can_edit, can_delete, can_alter, can_export, can_import)
VALUES (
${mapping.role_id},
'${mapping.node_code}',
${mapping.can_view},
${mapping.can_create},
${mapping.can_edit},
${mapping.can_delete},
false,
false,
false
)
`, { transaction });
}
}
// 3. Safely delete legacy role permissions
await queryInterface.sequelize.query(
`DELETE FROM role_permissions WHERE node_id IN (${legacyNodeIds.join(',')})`,
{ transaction }
);
// 4. Safely delete legacy permission nodes (cascade check handled by deleting role_permissions first)
await queryInterface.sequelize.query(
`DELETE FROM permission_nodes WHERE id IN (${legacyNodeIds.join(',')})`,
{ transaction }
);
console.log('Legacy permissions and mappings successfully cleared.');
} else {
console.log('No legacy permission nodes found. Skipping deletion.');
}
await transaction.commit();
} catch (error) {
await transaction.rollback();
console.error('Migration UP failed, transaction rolled back:', error);
throw error;
}
},
async down(queryInterface, Sequelize) {
const transaction = await queryInterface.sequelize.transaction();
try {
const now = new Date();
// 1. Re-create root node PIM_SYSTEM if not exists
await queryInterface.sequelize.query(`
INSERT INTO permission_nodes (node_code, node_name, node_type, parent_id, node_level, display_order, can_view, can_create, can_edit, can_delete, created_at, updated_at)
VALUES ('PIM_SYSTEM', 'PIM System', 'module', NULL, 1, 1, true, false, false, false, NOW(), NOW())
ON CONFLICT (node_code) DO NOTHING
`, { transaction });
// Fetch root node ID
const [pimNode] = await queryInterface.sequelize.query(
`SELECT id FROM permission_nodes WHERE node_code = 'PIM_SYSTEM'`,
{ type: queryInterface.sequelize.QueryTypes.SELECT, transaction }
);
if (!pimNode) {
throw new Error('Failed to retrieve or create PIM_SYSTEM permission node during rollback.');
}
// 2. Re-create child feature nodes USERS_MANAGEMENT, ROLES_MANAGEMENT, TENANTS_MANAGEMENT
const children = [
{ code: 'USERS_MANAGEMENT', name: 'Users Management', order: 1 },
{ code: 'ROLES_MANAGEMENT', name: 'Roles Management', order: 2 },
{ code: 'TENANTS_MANAGEMENT', name: 'Tenants Management', order: 3 }
];
for (const child of children) {
await queryInterface.sequelize.query(`
INSERT INTO permission_nodes (node_code, node_name, node_type, parent_id, node_level, display_order, can_view, can_create, can_edit, can_delete, created_at, updated_at)
VALUES ('${child.code}', '${child.name}', 'feature', ${pimNode.id}, 2, ${child.order}, true, true, true, true, NOW(), NOW())
ON CONFLICT (node_code) DO NOTHING
`, { transaction });
}
// 3. Restore role mappings from backup table if it exists
const tableExists = await queryInterface.sequelize.query(
`SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = '_backup_legacy_role_permissions')`,
{ type: queryInterface.sequelize.QueryTypes.SELECT, transaction }
);
if (tableExists[0]?.exists) {
const backups = await queryInterface.sequelize.query(
`SELECT role_id, node_code, can_view, can_create, can_edit, can_delete, can_alter, can_export, can_import FROM _backup_legacy_role_permissions`,
{ type: queryInterface.sequelize.QueryTypes.SELECT, transaction }
);
for (const backup of backups) {
const [node] = await queryInterface.sequelize.query(
`SELECT id FROM permission_nodes WHERE node_code = '${backup.node_code}'`,
{ type: queryInterface.sequelize.QueryTypes.SELECT, transaction }
);
if (node) {
// Check if active mapping exists to prevent duplicate insertion
const [exists] = await queryInterface.sequelize.query(
`SELECT id FROM role_permissions WHERE role_id = ${backup.role_id} AND node_id = ${node.id}`,
{ type: queryInterface.sequelize.QueryTypes.SELECT, transaction }
);
if (!exists) {
await queryInterface.sequelize.query(`
INSERT INTO role_permissions (role_id, node_id, can_view, can_create, can_edit, can_delete, can_alter, can_export, can_import, created_at, updated_at)
VALUES (
${backup.role_id},
${node.id},
${backup.can_view},
${backup.can_create},
${backup.can_edit},
${backup.can_delete},
${backup.can_alter},
${backup.can_export},
${backup.can_import},
NOW(),
NOW()
)
`, { transaction });
}
}
}
// Clean up the backup table post rollback
await queryInterface.sequelize.query(`DROP TABLE IF EXISTS _backup_legacy_role_permissions`, { transaction });
console.log('Legacy permissions restored and backup table dropped.');
}
await transaction.commit();
} catch (error) {
await transaction.rollback();
console.error('Migration DOWN failed, transaction rolled back:', error);
throw error;
}
}
};
+91 -110
View File
@@ -4,124 +4,105 @@ const bcrypt = require('bcrypt');
module.exports = {
async up(queryInterface, Sequelize) {
const transaction = await queryInterface.sequelize.transaction();
try {
// 1. Create Root Permission Node (PIM System)
const [pimSystemNode] = await queryInterface.bulkInsert('permission_nodes', [{
node_code: 'PIM_SYSTEM',
node_name: 'PIM System',
node_type: 'module',
parent_id: null,
node_level: 1,
display_order: 1,
can_view: true,
can_create: false,
can_edit: false,
can_delete: false,
created_at: new Date(),
updated_at: new Date()
}], { returning: true, transaction });
const now = new Date();
// 2. Create child nodes (Settings, Users, Roles)
const childNodes = await queryInterface.bulkInsert('permission_nodes', [
{
node_code: 'USERS_MANAGEMENT',
node_name: 'Users Management',
node_type: 'feature',
parent_id: pimSystemNode.id,
node_level: 2,
display_order: 1,
can_view: true,
can_create: true,
can_edit: true,
can_delete: true,
created_at: new Date(),
updated_at: new Date()
},
{
node_code: 'ROLES_MANAGEMENT',
node_name: 'Roles Management',
node_type: 'feature',
parent_id: pimSystemNode.id,
node_level: 2,
display_order: 2,
can_view: true,
can_create: true,
can_edit: true,
can_delete: true,
created_at: new Date(),
updated_at: new Date()
},
{
node_code: 'TENANTS_MANAGEMENT',
node_name: 'Tenants Management',
node_type: 'feature',
parent_id: pimSystemNode.id,
node_level: 2,
display_order: 3,
can_view: true,
can_create: true,
can_edit: true,
can_delete: true,
created_at: new Date(),
updated_at: new Date()
// 1. Create Super Admin Role
let superAdminRoleId;
const [existingRole] = await queryInterface.sequelize.query(
`SELECT id FROM roles WHERE role_code = 'SUPER_ADMIN'`,
{ type: queryInterface.sequelize.QueryTypes.SELECT, transaction }
);
if (existingRole) {
superAdminRoleId = existingRole.id;
} else {
const [newRole] = await queryInterface.bulkInsert('roles', [{
tenant_id: null,
role_code: 'SUPER_ADMIN',
role_name: 'Super Admin',
description: 'System Administrator with full access to all tenant and platform features',
role_type: 'platform',
is_system_role: true,
status: true,
created_at: now,
updated_at: now
}], { returning: true, transaction });
superAdminRoleId = newRole.id;
}
// Fetch all existing permission nodes to map them to Super Admin role
const allActiveNodes = await queryInterface.sequelize.query(
`SELECT id, node_code FROM permission_nodes`,
{ type: queryInterface.sequelize.QueryTypes.SELECT, transaction }
);
// 2. Assign all permissions to Super Admin role
for (const node of allActiveNodes) {
const [existingMapping] = await queryInterface.sequelize.query(
`SELECT id FROM role_permissions WHERE role_id = ${superAdminRoleId} AND node_id = ${node.id}`,
{ type: queryInterface.sequelize.QueryTypes.SELECT, transaction }
);
if (!existingMapping) {
await queryInterface.bulkInsert('role_permissions', [{
role_id: superAdminRoleId,
node_id: node.id,
can_view: true,
can_create: true,
can_edit: true,
can_delete: true,
can_alter: true,
can_export: true,
can_import: true,
created_at: now,
updated_at: now
}], { transaction });
}
], { returning: true, transaction });
// 3. Create Super Admin Role
const [superAdminRole] = await queryInterface.bulkInsert('roles', [{
tenant_id: null,
role_code: 'SUPER_ADMIN',
role_name: 'Super Admin',
description: 'System Administrator with full access to all tenant and platform features',
role_type: 'platform',
is_system_role: true,
status: true,
created_at: new Date(),
updated_at: new Date()
}], { returning: true, transaction });
// 4. Assign all permissions to Super Admin role
const allNodes = [pimSystemNode, ...childNodes];
const rolePermissions = allNodes.map(node => ({
role_id: superAdminRole.id,
node_id: node.id,
can_view: node.can_view,
can_create: node.can_create,
can_edit: node.can_edit,
can_delete: node.can_delete,
can_alter: false,
can_export: false,
can_import: false,
created_at: new Date(),
updated_at: new Date()
}));
await queryInterface.bulkInsert('role_permissions', rolePermissions, { transaction });
}
// 5. Create Super Admin User
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash('Admin@123', salt);
let superAdminUserId;
const [existingUser] = await queryInterface.sequelize.query(
`SELECT id FROM users WHERE email = 'superadmin@maskan.com'`,
{ type: queryInterface.sequelize.QueryTypes.SELECT, transaction }
);
const [superAdminUser] = await queryInterface.bulkInsert('users', [{
tenant_id: null,
email: 'superadmin@maskan.com',
user_name: 'Super Admin',
user_type: 'platform',
password_hash: hashedPassword,
status: true,
created_at: new Date(),
updated_at: new Date()
}], { returning: true, transaction });
if (existingUser) {
superAdminUserId = existingUser.id;
} else {
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash('Admin@123', salt);
const [newUser] = await queryInterface.bulkInsert('users', [{
tenant_id: null,
email: 'superadmin@maskan.com',
user_name: 'Super Admin',
user_type: 'platform',
password_hash: hashedPassword,
status: true,
created_at: now,
updated_at: now
}], { returning: true, transaction });
superAdminUserId = newUser.id;
}
// 6. Assign Super Admin Role to Super Admin User
await queryInterface.bulkInsert('user_roles', [{
user_id: superAdminUser.id,
role_id: superAdminRole.id,
status: true,
created_at: new Date(),
updated_at: new Date()
}], { transaction });
const [existingUserRole] = await queryInterface.sequelize.query(
`SELECT id FROM user_roles WHERE user_id = ${superAdminUserId} AND role_id = ${superAdminRoleId}`,
{ type: queryInterface.sequelize.QueryTypes.SELECT, transaction }
);
if (!existingUserRole) {
await queryInterface.bulkInsert('user_roles', [{
user_id: superAdminUserId,
role_id: superAdminRoleId,
status: true,
created_at: now,
updated_at: now
}], { transaction });
}
await transaction.commit();
} catch (error) {
@@ -18,13 +18,13 @@ module.exports = {
('products', 'Products', 'group', NULL, 1, 10, true, false, false, false, false, false, false, '${now}', '${now}'),
('masters', 'Masters', 'group', NULL, 1, 20, true, false, false, false, false, false, false, '${now}', '${now}'),
('settings', 'Settings', 'group', NULL, 1, 30, true, false, false, false, false, false, false, '${now}', '${now}'),
('reports', 'Reports', 'group', NULL, 1, 40, true, false, false, false, false, false, false, '${now}', '${now}')
('reports', 'Reports', 'feature', NULL, 1, 40, true, true, true, true, false, true, false, '${now}', '${now}')
ON CONFLICT (node_code) DO NOTHING;
`);
// Fetch parent IDs
const parents = await queryInterface.sequelize.query(
`SELECT id, node_code FROM permission_nodes WHERE node_code IN ('products','masters','settings','reports')`,
`SELECT id, node_code FROM permission_nodes WHERE node_code IN ('products','masters','settings')`,
{ type: queryInterface.sequelize.QueryTypes.SELECT }
);
const pid = {};
@@ -45,9 +45,11 @@ module.exports = {
('settings.users', 'Users', 'feature', '${pid['settings']}', 2, 31, true, true, true, true, false, false, false, '${now}', '${now}'),
('settings.roles', 'Roles', 'feature', '${pid['settings']}', 2, 32, true, true, true, true, false, false, false, '${now}', '${now}'),
('settings.integrations', 'Integrations', 'feature', '${pid['settings']}', 2, 33, true, true, true, true, false, false, false, '${now}', '${now}'),
('reports.sales', 'Sales Reports', 'feature', '${pid['reports']}', 2, 41, true, false, false, false, false, true, false, '${now}', '${now}'),
('reports.inventory', 'Inventory Reports', 'feature', '${pid['reports']}', 2, 42, true, false, false, false, false, true, false, '${now}', '${now}')
ON CONFLICT (node_code) DO NOTHING;
('settings.tenants', 'Tenants', 'feature', '${pid['settings']}', 2, 35, true, true, true, true, false, false, false, '${now}', '${now}')
ON CONFLICT (node_code) DO UPDATE SET
parent_id = EXCLUDED.parent_id,
node_level = EXCLUDED.node_level,
display_order = EXCLUDED.display_order;
`);
},
@@ -58,7 +60,7 @@ module.exports = {
'products.items', 'products.variants', 'products.families',
'products.categories', 'products.attributes',
'masters.brands', 'masters.units',
'settings.users', 'settings.roles', 'settings.integrations',
'settings.users', 'settings.roles', 'settings.integrations', 'settings.tenants',
'reports.sales', 'reports.inventory',
],
}, {});
@@ -19,7 +19,10 @@ module.exports = {
'notifications', 'Notifications', 'feature', ${parentId ? `'${parentId}'` : 'NULL'}, 2, 34,
true, false, true, false, false, false, false, '${now}', '${now}'
)
ON CONFLICT (node_code) DO NOTHING;
ON CONFLICT (node_code) DO UPDATE SET
parent_id = EXCLUDED.parent_id,
node_level = EXCLUDED.node_level,
display_order = EXCLUDED.display_order;
`);
},